fix(workflows): reject an automation transition with no recorded cause - #792
Merged
Conversation
#792) An automation-caused transition writes principal_id: null (#786) — the cause lives in generation_id / orchestration_run_id instead. If a dispatch fails before either id exists (e.g. an error thrown ahead of recordGenerationFailure), the on_failure transition could still be persisted with all three columns null: a transition history row with no recorded cause at all. Reject the write instead of silently persisting it; the task stays in its current state with automation_status: failed for a human to investigate. Audited every TaskTransition writer (tasksTransition.ts, tasks.ts, tasksApprovalGate.ts) — the routeOnComplete/handleFailure paths already never duplicate an orchestration run id into principal_id (fixed by #786 before this repo state), so this closes the remaining gap the issue's own suggested action #3 called out.
arantespp
enabled auto-merge (squash)
July 31, 2026 14:23
Deploy Outputs
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes the bug reported in the "transition writer missing cause" report (split out of #791): an
automationtransition could be persisted withprincipal_id,generation_id, andorchestration_run_idall null — a task move with no recorded cause whatsoever.Investigation
I audited every
TaskTransitionwriter (tasksTransition.ts,tasks.ts,tasksApprovalGate.ts). TherouteOnComplete/handleFailurepaths intasksAutomation.tsalready never duplicate a run/generation id intoprincipal_id— that was fixed by #786 before this repo's current state (confirmed by existing tests attasks.test.tsassertingprincipal_idis null whilegeneration_id/orchestration_run_idcarry the cause). So the literal writer bug described in the report (duplicating an orchestration run id into the principal column) is already fixed onmain.What was not covered: if a dispatch fails before any generation/run id exists (e.g. an error thrown ahead of
recordGenerationFailure),failedDispatchIdslegitimately returns{ generationId: null, orchestrationRunId: null }, and theon_failuretransition was still silently written with zero provenance — exactly the "meaningless record" the report's suggested action #3 called out.Fix
Added a defensive check (
assertAutomationHasProvenanceintasksTransition.ts) that rejects — via a newTASK_AUTOMATION_PROVENANCE_MISSING(500) error — persisting anyautomation-kind transition whereprincipal_id,generation_id, andorchestration_run_idwould all be null. The write is atomic (inside the existing row-locked transaction), so a rejected write also rolls back the state-change side of the same transaction: the task stays in its current state, flaggedautomation_status: failed, instead of silently moving to a state whose history carries no cause.Open Questions Gate
Tests
a failed dispatch sets automation_status and follows on_failureto reject with aDomainErrorcarryinggeneration_idin itsmeta, matching howcreateGenerationactually fails in production (a generation row always exists before the LLM call, so real failures always carry an id) — the previous plain-Errormock no longer exercises a realistic path now that the new guard exists.a failed dispatch with no recoverable cause id never persists a provenance-less automation transition (#792): simulates a dispatch failure with zero metadata and asserts the task stays in its current state (automation_status: failed) instead of transitioning, and that noto_failedhistory row is written.Docs
packages/website/docs/modules/workflows.md: added the newTASK_AUTOMATION_PROVENANCE_MISSINGerror code to the Error Codes table.Verification
pnpm typecheck— passes.pnpm eslint --fixon changed files — passes.pnpm --filter @soat/server test --testPathPatterns=tasks.test.ts— could not be run to completion in this sandbox: the integration suite requires a live PostgreSQL testcontainer, and Docker's daemon is unavailable in this environment (confirmed the same pre-existing failure occurs on an untouched file, e.g.users.test.ts, so it is an environment limitation, not something this change caused). CI'sbuild-and-testjob will run the full suite against the real Postgres container.Generated by Claude Code