Worker resume: the activity loop continues the worker it dispatched, keeping its identity - #410
Merged
Merged
Conversation
…d identity The client activity loop dispatches a worker, and after a gate is resolved resumes that same worker rather than entering the dispatch step again. dispatch-activity declares the server-side identity it binds, resume-worker carries that identity into the continuation stub, and the dispatch step is gated on no identity being held — so a live worker cannot be handed a second one. compose-prompt composes the continuation stub as well as the spawn stub, so identity binding has one home for both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
5 tasks
dispatch-activity's Rules carried procedure: the usage-accounting call with its omit branch, the routing cross-check with its escalation, and the recovery for a partial result. Each moves to the Protocol phase that performs it, and each rule keeps only what the phases cannot express — one entry per dispatch and what an absent entry means, which record governs on disagreement, and what makes a result unacceptable. Prohibitions lose their rationale tails: naming another actor's domain couples the rule to a contract it does not own. Conditional branches under a Protocol phase become blockquote notes, so a reader sees the work without evaluating every clause, and the note stays attached to the instruction it qualifies. resume-worker drops the delivery-identity rule that continue-agent already owns and cites that home through workflow-orchestrator; the identity it binds is carried by its declared input rather than by prose. Its Protocol records the continuation's cost, which the accounting invariant requires of every dispatch. The meta orchestrator roster names resume-worker beside dispatch-activity, and the READMEs describing compose-prompt name agent stubs rather than spawn stubs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The accounting rule names the record_usage channel and what an absent entry means, so the two phases that satisfy it — a dispatch and a continuation — cite it rather than each carrying the call and its omit branch. resume-worker awaits its envelope before accounting for the continuation, so every phase completes before the next begins. Its effects input states the value rather than what the worker does with it, and the identity output on dispatch-activity leaves the identity's lifetime to the rule that owns it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The effects slot on both ends of the resume bind names what the value is. The duty of propagating it to the worker stays on respond-checkpoint's Protocol phase, which already carries it; the bind topology stays in the activity YAML and in resume-worker's Protocol. resume-from-checkpoint's session_index states which worker it addresses. Index stability across a session is held by session-index-passes-on-each-call, which every operation in the group inherits. Conditional caveats under a Protocol phase become blockquote notes in both operations, so the parser reads them as part of the phase they qualify rather than as peer steps. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
resume-worker declared session_index and activity_id without referencing
either. The continuation now passes {session_index} to continue-agent, whose
Inputs declare it so the resumed worker can authenticate its calls, and the
accounting phase names the {activity_id} the entry is recorded against.
compose-prompt's substitutions contract requires activity_id for the
continuation stub as well as the worker stub, which its role line already
reads. The continuation bullet leaves the delivery consequence of agent_id to
agent-id-scopes-delivery, cited a bullet above.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4 tasks
A saved session and a gated worker are both resumed, and only one of them is a dispatch. orchestrator-no-inline-on-resume names the session case and points the gate case at resume-worker, so the rule no longer reads as an instruction to dispatch a worker that is already running. The orchestrator's trace rule cites its home without restating what that home owns; the home says it owns the accumulate half and the client owns the resolve, which the citer contradicted. agent-id-scopes-delivery names the stub that dispatches or continues the agent, since the identity is bound into both. The partial-result branch names the harness operation that continues a worker, as its sibling branch already does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
cargo-operations owns whether an operation carries the nice budget, and its fmt-uses-only-nice rule states that fmt-check and fmt-fix apply nice -n 19 — which both operations do. The conduct copy said the opposite, so an agent reading both had no way to tell which governed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… as a phase A blocking-equivalent wait observes a yield or a completion, so elapsed time is not a condition the orchestrator can read. The replacement branch keys on the harness reporting the worker ended without an envelope, and the accounting contract names a replacement worker rather than a threshold nothing declares. Reconciling a critical routing or path variable is an outcome the routing decision waits on, so it takes its own phase. The two branches left under the routing phase are genuine arms of it and stay as notes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…preserves-delivery-identity
4 tasks
The orchestrator's own delivery identity is the one its refetches are keyed on,
so the load phase names {agent_id} where that matters. dispatch-activity
declares the planning folder path its Progress phase applies, so the phase
passes a slot rather than a designator with no declaration behind it.
Three rules shed justification tails: which record wins needs no reason
attached, the trace split names the resolve without extending to artifacts, and
the persistent-mode prohibition stands without restating what a session serves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 3, 2026
Closed
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
When a worker stops at a gate and the user answers it, the client activity loop goes back to the top and dispatches again. Dispatching mints a fresh worker identity, so the worker that is still sitting there holding the whole activity is handed a new name — and the server, which keys its delivery ledger on that name, sees a context it has never met and sends the activity a second time. This change gives the loop a step that continues the worker it already has, under the identity that worker was dispatched with.
What happens today
03-dispatch-client-workflow.yamldrives a client workflow from onewhileloop. The body bindsdispatch-activity, then — when the worker yielded —present-checkpoint-to-userandrespond-checkpoint, and finally, only when the worker reported the activity finished, commits and advances the activity pointer. A gate leaves the pointer where it was, so the loop condition still holds and the next iteration starts where every iteration starts: atdispatch-activity.That operation mints a worker
agent_idinside its Protocol, one per dispatch. Entering it again after a gate therefore mints a second one for a worker that is still alive.There was no step in the loop that resumed a worker, and no declared value anywhere carrying the identity a dispatch had bound — so nothing downstream could have re-bound it even if it had tried. The one place stating the invariant is
harness-compat::continue-agent'sresume-preserves-delivery-scoperule, and nothing on the gate path reaches it.workflow-orchestratorsaid "resume the worker with resolved effects", naming a resume that had no operation behind it.That is a critical constraint carried by rule text on a path that never reads it — what the catalogue calls
structure-backed-constraints, framed by Encode Constraints as Structure.The fix
The identity becomes a declared value.
dispatch-activitydeclaresworker_agent_idon its Outputs: the server-side identity this dispatch bound, held by the worker until it reports the activity complete. Being declared, it lands in the bag and later steps can bind it.The gate path gets an operation.
workflow-engine::resume-workercontinues the worker already holding the activity — it composes the continuation stub under the boundworker_agent_idand appliesharness-compat::continue-agent, returning the worker's next envelope unchanged. It mirrorsdispatch-activity's shape, so the loop reads as spawn-or-resume.One home for identity binding.
compose-promptacceptsworkflow-engine::resume-from-checkpointas a third agent technique, with its own entry-tool line, rather than a second stub assembler that could drift from the first.Re-dispatch becomes impossible while a worker holds the activity. In the loop,
dispatch-activityis gated on no identity being held, aresume-yielded-workerstep bindsresume-workerafter the checkpoint is answered, and the advance step releases the identity when the worker finishes. A worker that gates twice takes present → respond → resume twice, under one identity, and the loop returns todispatch-activityonly once the activity is done.Scope of change
Five files under
meta/: the client dispatch activity, the newresume-workeroperation, and prose plus contract edits ondispatch-activity,compose-prompt, andworkflow-orchestrator. No other workflow is touched.Acceptance criteria
Non-goals
continue-agentresolves it as it does today; the identity at issue here is the server-side one the ledger is keyed on.Investigation detail
Where the identity is lost, the alternatives weighed, and the mapping onto the issue's criteria:
.engineering/artifacts/planning/2026-08-03-resume-preserves-delivery-identity
Server-side counterpart, with the tests: #408.
🤖 Generated with Claude Code