Responding to ds-review-bot round 2 on #658 (three critical findings, one warning — all rooted in the pump/commit split racing ordered stages): - ONE driver lane now owns every ordered stage: the start append, prepare (pre-execute/guards), and the head-of-line commit (post-execute, context deferral, settle append). start() is awaited before the next entry can start, so concurrent submissions can no longer run pre-execute pipelines concurrently; only the around-dispatch/body stage overlaps, matching the native loop's fillPool sequencing. - An exclusive call's barrier now holds through its COMMIT: later starts wait for the exclusive pipeline (post-execute included) to finish, the native exclusive-group semantics. - drainDispatches() awaits the driver run itself, so a commit already mid-flight when the program returns is drained before run_code closes the turn — the settle event and deferred contexts land inside it. - maxParallelSubCalls is resolved and validated at construction (positive integer), so direct construction can no longer wedge the pool with 0. New tests: overlapping-submission ordered-prepare, barrier-through-commit, drain-mid-commit, cap rejection. 96 keyless snapshots replay unchanged; Agent Note updated (both languages).
5.2 KiB
Agent Note: Code Mode live dispatch lifecycle and native-contract parallelism
Status: implemented
English | 中文
Scope: the third PR of the Code Mode UI stack — the
tool/code-dispatch-startevent, per-sub-call running state in the web chat, and the bridge's scheduler reusing the native concurrency contract. Builds on the host foundation and chat sub-call rows; the native contract itself is owned by the parallel tool-call note.
Problem
Two gaps remained after the first two PRs. Sub-call rows appeared only when each dispatch settled — while one ran, the UI showed nothing for it, so a slow sub-call read as a stalled parent. And the bridge serialized every binding call ("even Promise.all executes one at a time"), a placeholder from before tools carried concurrency metadata: isConcurrencySafe now exists, the loop scheduler already runs native siblings in bounded pools, and a Code Mode program awaiting three independent reads paid 3× the latency the native path would.
Decision
One lifecycle pair, one scheduling contract, shared with native.
- Event pair:
tool/code-dispatch-start(parent/sub ids, name, normalized args) is appended when the scheduler actually starts a call — not at submission, so a queued call abandoned by run settlement logs nothing. The existingtool/code-dispatchsettles the pair (samesubCallId); every started call settles exactly once (aborts settle asisErroroutcomes through the pipeline). Timing = the two events'timefields. Both stay log-only; model context is untouched; format stays v0. - Bridge scheduler: submitted calls are classified at start time via
registry.executionMode(the SAME fail-closedisConcurrencySafecontract the loop uses) and start strictly in submission order. One single-lane driver owns every ORDERED stage — the start append,prepare(pre-execute/guards), the head-of-linefinalize/finishcommit (post-execute + context deferral + settle append) — so ordered policy stages never overlap each other and only the around-dispatch/body stage runs concurrently, exactly the native loop's sequencing (fillPoolawaitsstartCallthencommitReady). Consecutive parallel-classified calls overlap up tomaxParallelSubCalls(aConfigfield validated by the Loader schema AND re-validated at direct construction, default 10 — the loop scheduler's own default;1restores serial dispatch); an exclusive call drains the pool, runs alone, and holds its barrier until its COMMIT completes (post-execute included), like a native exclusive group. Run settlement aborts in-flight dispatches and abandons queued-unstarted ones (binding rejection, no events), then drains to quiescence — including a commit already mid-flight when the program returned — before the outer result closes the turn. - Client:
CodeSubCallwidens toRunningToolCall | ToolResultNode— a start event lands the running shape in the dispatch index (rows derive the running ring from the shape, exactly as for native in-flight calls), and its settle replaces the entry in place, preserving start order under parallel completion and carrying the start'stimeascallTime(duration source). A settle with no observed start (window cut mid-pair, or a pre-start-event log) appends directly, so old logs keep rendering. - SDK prompt: the model-facing "calls execute sequentially" sentence is replaced with the true contract (independent safe calls may overlap under
Promise.all; dependent work sequences withawait) — a model-visible change, re-recorded across every code-mode snapshot.
Alternatives considered
Unrestricted parallelism (let Promise.all overlap everything). Rejected: writes could race; the native scheduler exists precisely because the tool, not the caller, owns the safety claim. One concurrency vocabulary across native and Code Mode was the settled requirement.
Emit the start event at submission instead of pool entry. Rejected: a submission-time start would show queued-but-never-run calls as "running" and would force a third "abandoned" terminal event to reconcile the log. Start-at-entry keeps the invariant started ⇔ settles exactly once and needs no third event.
Reuse the loop scheduler's implementation directly. Rejected: the loop schedules a fully-parsed batch with model-order result commitment; the bridge schedules an open-ended stream of submissions whose results return to the program (not the transcript), so only the contract (classification, pool, barriers) is shared, not the machinery.
Consequences
Programs get native-grade latency for independent reads with no new model-side API — Promise.all simply works better, and prompt guidance changed accordingly. The web UI shows per-sub-call running rings live (fixture emits start/settle pairs; jsdom pins the running shape; the runtime spec pins in-place settlement, out-of-order completion, and callTime pairing). PR6 (trajectory/waterfall spans) can now draw truthful spans from the pair's timing. The spill PR (next) inherits the settle event as its single bounding point.