Skip to content

Worker resume: the activity loop continues the worker it dispatched, keeping its identity - #410

Merged
m2ux merged 10 commits into
workflowsfrom
workflow/resume-preserves-delivery-identity
Aug 3, 2026
Merged

Worker resume: the activity loop continues the worker it dispatched, keeping its identity#410
m2ux merged 10 commits into
workflowsfrom
workflow/resume-preserves-delivery-identity

Conversation

@m2ux

@m2ux m2ux commented Aug 3, 2026

Copy link
Copy Markdown
Owner

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.yaml drives a client workflow from one while loop. The body binds dispatch-activity, then — when the worker yielded — present-checkpoint-to-user and respond-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: at dispatch-activity.

That operation mints a worker agent_id inside 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's resume-preserves-delivery-scope rule, and nothing on the gate path reaches it. workflow-orchestrator said "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-activity declares worker_agent_id on 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-worker continues the worker already holding the activity — it composes the continuation stub under the bound worker_agent_id and applies harness-compat::continue-agent, returning the worker's next envelope unchanged. It mirrors dispatch-activity's shape, so the loop reads as spawn-or-resume.

One home for identity binding. compose-prompt accepts workflow-engine::resume-from-checkpoint as 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-activity is gated on no identity being held, a resume-yielded-worker step binds resume-worker after 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 to dispatch-activity only once the activity is done.

Scope of change

Five files under meta/: the client dispatch activity, the new resume-worker operation, and prose plus contract edits on dispatch-activity, compose-prompt, and workflow-orchestrator. No other workflow is touched.

Acceptance criteria

  • A resumed worker carries the server-side identity its dispatch bound.
  • The loop cannot dispatch a second worker for an activity a live worker is holding.
  • Identity binding for spawn and continuation stubs has a single home.
  • All 21 repository guards pass, including binding fidelity, when-expression parsing, and the technique template.

Non-goals

  • Threading the harness-level agent handle through the bag. continue-agent resolves it as it does today; the identity at issue here is the server-side one the ledger is keyed on.
  • Worker failure handling. Worker death is rare, externally caused, and already covered by the existing replacement path.

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

…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>
m2ux and others added 4 commits August 3, 2026 09:16
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>
m2ux and others added 4 commits August 3, 2026 10:28
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>
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant