4.2 KiB
Agent Note: Spilling the durable copy of Code Mode sub-dispatch results
Status: implemented
English | 中文
Scope: limiting the
tool/code-dispatchevent's content with the existing spill implementation. The host foundation note deliberately accepted the unlimited log and deferred spill support to this change; the live-parallel note defines the event pair that this listener processes.
Problem
After full-content dispatch logging was added, a run_code program that reads a large file wrote the complete rendered text into the session log without a limit or spill policy, while native results were limited to maxInlineBytes before logging. This treated the most likely large results differently: sub-calls are intended for bulk data work, and each affected turn added megabytes to the JSONL.
Decision
A tools/code-dispatch-log waterfall on the registry, with spill policy as its first listener.
- Extension point:
tools/code-dispatch-logis a scope-filtered waterfall that the bridge runs over each settled sub-dispatch before appendingtool/code-dispatch. The bridge receives the registry's privateshapeDispatchLoginvoker as a capability closure inRunCodeBridgeOptions; the waterfall is the public contract, and the invoker does not add a service method. If a listener throws, the invoker reports any thrown value safely and uses the original settled content. TheCodeDispatchLogpayload carries the outer execution, theagentrouting key, the sub-call identity, and the default content: the rendered result projection that a nativetool/resultwould carry, while the program receives the structuredvalue. A listener can replace only the durable copy, which the model never sees. The listener runs as tracked work outside the program's result path. When more thanmaxParallelSubCallslog tasks are pending, the ordered commit loop waits, so a slow spill backend limits later sub-call starts instead of accumulating unlimited pending I/O. Run settlement still waits for every task inside the open turn. - Policy:
dsh-spill-policyregisters a listener for this event and uses the same replacement code as its model-result listener: the samemaxInlineByteslimit, preview and locator, within-limit invariant, and best-effort fallback. The spill artifact is labeleddispatchunder the sub-call id. UIs and replay read its full text through the same path used for spilled native results, so both result kinds render with the same information. - One deliberate difference: the model-result listener skips
readto prevent aread → spill → read againloop. The dispatch-log listener also replaces oversizedreadsub-call content because a log copy is not model context, so that loop cannot occur, andreadis the tool most likely to produce a large log entry.
Alternatives considered
Apply a plain byte limit inside the bridge without spill storage. Rejected: truncation without a locator loses data that replay or UIs may need and restores the less informative "truncated summary" rendering that earlier changes removed.
Spill inside the bridge directly by calling ctx.spillStore from code-mode.ts. Rejected: the registry would require the spill capability. The waterfall keeps this policy with the other spill decisions and allows compositions to omit it; omitting maxInlineBytes still makes the listener a no-op.
Reuse tools/post-execute for nested calls instead of a new event. Rejected: post-execute can change the program-facing result, so nested calls deliberately skip it and programs receive complete data. The durable copy needs a separate listener that runs after the program has its value.
Consequences
Code Mode dispatch entries in the session log now have the configured byte limit, and the README's Known Limitations entry about unlimited dispatch logging now points here. Old logs with oversized dispatch content still replay because the event fields are unchanged; only future appends contain less text. The web UI renders spilled sub-call output as preview and locator text through the same path as native results, with no special case.