Skip to content

Re-dispatch overhead is 31% of a work-package run: context-scoped delivery ledger, resume-time references, and checkpoint-at-entry #353

Description

@m2ux

A full work-package review walk (PR #1877, planning folder 2026-07-28-review-midnight-node-pr-1877) was measured end to end: 921 tool calls, 33 dispatches, 4,130,657 subagent tokens, 3h 51m agent time. Splitting its 21 activity entries by dispatch count gives the governing result:

Group Activities Tool calls Tokens tok/call
Single-dispatch 11 369 1,141,290 3,093
Multi-dispatch (resumed) 10 552 2,989,367 5,416

Cost tracks dispatch count, not call count — ratio 1.75×. Applying that delta across the 552 multi-dispatch calls implies ~1,282,000 tokens, 31% of the run, is re-dispatch overhead. That corroborates by an independent route the run's own attribution of ~1,020,000 tokens (24.7%) to its 12 resumed dispatches.

It is emphatically not long-context thrash inside a single worker. codebase-comprehension ran 88 calls in one dispatch at 2,543 tok/call — the cheapest in the run. submit-for-review ran 65 calls across three dispatches at 7,621 — the dearest. Long activities are cheap; re-dispatched activities are expensive.

What causes the re-dispatches: checkpoints firing mid-activity. The run resolved 10 checkpoints and paid 12 resumes; per its own trace, the resumes were "checkpoint continuations, and one recovery from an organisation spend limit" — so ~10 of 12 are checkpoint-driven, at roughly 116K tokens each. Of the 8 client checkpoints that fired, mapped against their step position in the definitions:

Checkpoint Activity Step position Steps after
local-validation-permission 11-validate 0 of 11 10
file-index-table 10-post-impl-review 3 of 18 14
unsigned-commits-prompt 12-strategic-review 2 of 14 11
review-summary-approval 13-submit-for-review 5 of 32 26
research-convergence#pass-1 04-research 5 of 20 (loop body) 14
retrospective-confirm 14-complete 7 of 11 3
ticket-completeness 02-design-philosophy 9 of 11 1
review-findings 12-strategic-review 13 of 14 (last) 0

Seven of eight fired mid-activity. Only review-findings sat at the boundary, where a yield is free.

This issue tracks two PRs. The server change is a hard prerequisite for the corpus change, so they cannot be reviewed in either order.


PR 1 — server (main)

1.1 Scope the delivery ledger to a worker context — the ~25–31% item

Two rules look like they contradict each other:

  • workers-need-full-delivery: "Dispatched workers are fresh contexts with no prior deliveries… never instruct a worker to pass bundle: "reference": an unchanged-reference points at content the new worker has never received."
  • resume-is-optimisation: "Harness-level resume preserves the context window."

The rule is not the defect — the ledger is. deliveredHash reads state.deliveredContent[state.agentId][key] (src/utils/delivery.ts:44), and agentId is the session's agent id, shared by every worker dispatched against that session. Reference delivery on a worker session would therefore hand worker B unchanged-markers for content worker A received. workers-need-full-delivery is a correct guard over a ledger that cannot distinguish worker contexts. It can only relax once the ledger can.

  • Mint a worker-context id per spawn; carry it on the worker's calls and reuse it on a harness resume. Fresh spawn ⇒ empty ledger ⇒ full delivery (unchanged behaviour). Resume ⇒ populated ledger ⇒ references.
  • Key deliveredContent on that context id rather than state.agentId, preserving the existing shape for solo persistent walks.
  • Keep force-full-after-summarization (bundle: "full" / full: true) as the escape for a resume that did not preserve context.

1.2 Add the per-call reference opt-in to get_technique and get_resource

get_activity already has one: bundle: "reference" works independently of session mode (src/tools/workflow-tools.ts:674). Techniques and resources do not — they gate on state.contextMode === 'persistent' alone (src/tools/resource-tools.ts:738, :764, :847), with only full: true to force the other way. Without this, a resumed worker cannot dedup the 86 technique and 30 resource deliveries this run paid for, and 1.1 delivers only part of its value.

  • Accept a per-call reference opt-in on get_technique and get_resource, mirroring get_activity's bundle parameter
  • Leave the contextMode === 'persistent' default path intact

1.3 Ledger the invisible third — the instrument for verifying 1.1 and 1.2

You cannot confirm 1.1 worked without this, and three gaps are measurable in the run's own session.json:

  • activity_usage fired 11 times against 33 dispatches. No event type represents a resumed dispatch, an out-of-band dispatch, or an abandoned session, so cost accounting understated the run by a third. Emit a dispatch event carrying a fresh/resume discriminator.
  • Delivery events carry no magnitude. technique_bundled / technique_fetched / resource_fetched data holds only techniqueId / resourceId / stepId, so delivery cost is unmeasurable from the ledger — every payload figure in the planning folder is an estimate for exactly this reason. Add a char count.
  • Out-of-band dispatches are invisible. The run's out-of-band prism structural analysis cost 176,215 tokens and produced CR-0 and B7 — the two Critical findings the review's verdict rested on — with no server record at all.

1.4 Guard: a checkpoint may not be an activity's first step

Guards live in the server tree (scripts/check-*.ts, registered via scripts/check-all.ts), so the mechanical half of the corpus fix in 2.2 belongs here.

  • New guard rejecting steps[0].kind === "checkpoint", wired into check-all

PR 2 — corpus (workflows)

Merges after PR 1. Relaxing the delivery rule before the ledger is context-scoped would hand workers each other's unchanged-markers.

2.1 Relax workers-need-full-delivery to key on context identity

  • Rewrite the rule so the predicate is "is this the same agent context?" rather than "is this a worker session?" — fresh spawn ⇒ full, harness resume ⇒ reference
  • Have workflow-engine::dispatch-activity and harness-compat::continue-agent carry the worker-context id, and request reference delivery on the resume path only
  • Restate force-full-after-summarization as the escape at both call sites

2.2 Delete checkpoint-at-entry in 11-validate

workflows/work-package/activities/11-validate.yaml opens with steps[0] = kind: checkpoint, id local-validation-permission. The worker is spawned, pays full delivery, immediately yields, and is resumed — the entire first dispatch exists only to ask a question. validate cost 92,793 tokens over 2 dispatches for 18 tool calls.

It is also the wrong owner: the checkpoint asks whether the local environment can run the validation suite and sets run_local_validation / mark_progress_na. That is an environment-capability question, i.e. a dispatch precondition, not worker work.

  • Relocate the decision to the preceding activity's tail, or to the orchestrator's dispatch precondition
  • Confirm no other activity in the corpus opens with a checkpoint (guard from 1.4 will enforce it)

2.3 Submodule pointer and e2e re-baseline

Same obligation as #348 §1 — 2.1 edits meta techniques, which the e2e walks enumerate.

  • Bump the workflows submodule pointer in the server tree to the merged PR 2 commit
  • Run npm run baseline:stamp and commit the refreshed tests/e2e/__snapshots__/corpus-sha.json and snapshot.test.ts.snap in the same commit

Deliberately out of scope

Item Why excluded
Trace-token emission The run's trace_tokens resolved empty, so L7 produced nothing and error/validation-warning history is unavailable for the whole walk. But this needs diagnosis before it can be scoped: on server v2.1.0 a next_activity call returned {activity_id, name, session_index} with no _meta envelope at all, so there was no trace_token for an orchestrator to accumulate. Whether that is a bug or conditional-by-design is unknown — do not put an undiagnosed item in a PR scope. Worth its own issue.
Artifact handoff digests 403 KB of planning artifacts across 21 files (57 KB assumptions log, 50 KB structural analysis, 40 KB research doc) are re-read on every cold start, ~7% of the run. Deliberately sequenced after re-measuring, because 1.1 changes the cold-start baseline this would be sized against. Overlaps 2026-07-17-slim-work-package-planning-artifacts and 2026-07-28-workflow-design-slim-down.
Repo-binding waste The abandoned first meta run (wrong repo binding) burned 81,762 tokens, 2%. Already addressed by #345 / #348 / #349 — verify rather than re-derive.
Packaged execution manifests The idea that opened this analysis: deliver each activity's resolved execution manifest to the planning folder to cut tool calls. Measured and dropped. ~157 of 921 calls are server protocol and the packageable subset is 54 — 5.9% of calls, ~0% of tokens, since a fresh worker needs the content in its context window regardless of transport, and the filesystem path forfeits reference delivery and #section slicing. 83% of calls are the agent's own domain work. Rationale in the planning folder.
Splitting post-impl-review It is the run's heaviest activity (133 calls, 641,275 tokens, 42m) and "split the big one" is the obvious move. The data says it is wrong: single-dispatch activities are the cheapest per call, and splitting adds dispatches. Split only where the split lands on a mid-activity checkpoint — and note that is an alternative to 1.1, not a complement, since once a resume is cheap, keeping it beats paying a second cold start.

Success criteria

Re-run a comparable work-package walk after both PRs merge and compare against this baseline:

  • Multi-dispatch tok/call falls from 5,416 toward the single-dispatch 3,093 (a full close would recover ~1.28M tokens on a run of this size)
  • activity_usage event count equals actual dispatch count, not 11-of-33
  • Delivery events carry byte counts, so the payload figures in the planning folder stop being estimates
  • No corpus activity opens with a checkpoint

Context

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions