fix(flows-yaml): merge resolved-agent identity into TaskRequest #1123
No reviewers
Labels
No labels
area:agents
area:dashboard
area:database
area:design
area:design-review
area:flows
area:infra
area:meta
area:security
area:sessions
area:webhook
area:workdir
security
type:bug
type:chore
type:meta
type:user-story
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
charles/claude-hooks!1123
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/flows-yaml-dispatch-identity"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Closes the post-flows-yaml-cutover (#1083) crash loop. The
dispatchop constructed ataskPayloadwithoutgit_author/git_name/git_email/branch_prefix/forgejo_token, and droppedissue_numberentirely when the flow had no issue anchor. TheRecord<string, unknown>boundary cast hid this from TypeScript. At task end,persistTaskthen trippedNOT NULL constraint failed: task_history.user(or.issue_number), which became an unhandledRejection and crashed the service. systemd restarted, the cycle repeated — 65 crashes since the cutover landed on May 11.Symptom timeline (May 11 → May 12)
2026-05-10 21:28 UTC— #1083 cutover merged; flows-yaml becomes sole dispatch path.2026-05-11 00:46:27 CEST— first NOT NULL crash.[process] unhandledRejection: SQLiteError: NOT NULL constraint failed: task_history.user.2026-05-11 → 12— service restarts 6×.task_historyrows never written for affected tasks → janitor sees "no running task" → stale-idle-assigned bounces → re-dispatch cascade → session-resume invalidation (#1106 path) → multiple competing attempts on the same issue.2026-05-12 ~10:30— third attempt on the same issue finally runs to completion in worktree; would have crashed again at end without this fix.What the fix does
ops/dispatch.ts— pull identity fields from the resolved agent envelope (token→forgejo_token) into everyTaskRequest. Defaultissue_numberto0for repo-level flows (architect convention). Throw at dispatch time when the agent login is not a configured agent — without identity / token the task cannot do real work, and silently dispatching it just queues a row that crashes at end.domain/dispatch/registry.ts— defense in depth: wrap thepersistTaskcall inonFinishintry/catch(mirrors the existingstartTaskpattern atonStart) so any future incompleteTaskRequestwarns rather than crashing the process.ops/dispatch.test.ts— updated unknown-login test (now asserts throw) + new regression covering the identity merge andissue_number: 0default.Why not just revert
#1107(thestartTaskaudit-row PR) does not fix this — the crash site ispersistTask, which existed before#1107. Confirmed by countingNOT NULL constraintlines in journalctl: 65 before#1107deployed, 26 after. Both periods affected.#1083) would delete an entire milestone (M21 — Node Flows) to fix a 10-line bug. Wrong leverage.Test plan
bun test apps/server/src/domain/flows-yaml/ops/dispatch.test.ts— 6/6 pass (includes new regression for identity merge + updated throw-on-unknown-login).bun test apps/server/src/domain/dispatch/registry.test.ts apps/server/src/infrastructure/database/task-store.test.ts— 38/38 pass.bun run --filter @claude-hooks/server typecheck— clean.just restarton host — service boots clean, no NOT NULL, no unhandled rejection. Boot-sweep dropped one orphaned session row from the prior crash window.task_historyrow lands with non-nulluser+issue_numberon completion.🤖 Generated with Claude Code
APPROVED — CI green, fix is correct. Ready to squash-merge.
Root cause ✓
The diagnosis is accurate.
dispatch.tswas buildingtaskPayloadwithout ever pullinggit_author / git_name / git_email / branch_prefix / forgejo_tokenoff the resolved-agent envelope. TheRecord<string, unknown>boundary hid this from TypeScript, so those fields arrived asundefinedatpersistTask, which then tripped theNOT NULL constraintontask_history.userand blew up as anunhandledRejection. Confirmed bypersistTaskbeing a synchronousvoidfunction — the throw propagates straight out ofonFinishto whatever called it.Changes ✓
ops/dispatch.ts— the primary fix:throwwhenbyLoginreturnsnull: correct. An unknown login cannot provide the identity fields thatTaskRequestrequires; silently queuing the task just deferred the crash. Error message is specific enough to diagnose.git_author / git_name / git_email / branch_prefix / forgejo_token): covers everyNOT NULLcolumn that was missing.issue_number: args.issue_number ?? 0: matches the architect convention and thenonnegconstraint onissue_numberinargsSchema; safe sentinel for repo-level flows.branch_prefix: args.branch_prefix ?? r.branch_prefix: caller override is preserved, agent default fills in. When both are absent the key lands asundefinedin the payload rather than being omitted — minor behavioral delta, harmless in practice since all consumers treat it as optional.domain/dispatch/registry.ts— defense-in-depth:try/catcharoundpersistTaskmirrors the identical pattern already onstartTaskinonStart. Correct. A future dispatcher gap will warn rather than crash.ops/dispatch.test.ts— regression coverage:issue_number: 0default for the no-issue-anchor path.Minor nits (no blocker)
Redundant cast:
const r = resolved as Record<string, unknown>—AgentResolverCapability.byLoginalready returnsPromise<Record<string, unknown> | null>, so after the null guardresolvedis alreadyRecord<string, unknown>. You can writeresolved.git_authordirectly. One extra line, zero impact.First existing test still uses a minimal mock: "enqueues via workers and returns task_id + queued_at" passes
byLogin: async (login) => ({ login, type: "dev" })— the resolved envelope is now merged into the payload, sogit_author: undefinedetc. land in the captured call. Test still passes, but the mock is now slightly misleading. Fine to leave for a follow-up tidy.Nothing here warrants holding the merge — the service has been in a crash loop since May 11 and this is a targeted, well-tested fix.