Skip to content

bug(subagents): overlapping refreshes fold child transcript records repeatedly #802

Description

@Juliusolsson05

Confirmed by an isolated source-level reproduction at 5d641845 (Node v24.14.1, macOS arm64). No live agents or private transcripts were used. Priority: high, because this corrupts the subagent cards as well as wasting I/O and CPU.

Expected and actual behavior

Each complete child JSONL record must be folded exactly once. Concurrent parent events and timer ticks should join an in-flight read, with a trailing refresh if new evidence arrived.

Both trackers can instead start another read before the previous one advances its byte offset. Each continuation folds the same range into the shared accumulator. One real record becomes multiple turns/tool calls and multiple emissions. The raw transcript itself is unchanged.

Source evidence

  • Codex public event trigger: every parent entry calls void this.refresh(); a separate 1,200 ms interval does the same.
  • Codex refresh has no in-flight guard. The offset is read before awaiting readRange, then advanced only after the await.
  • Claude refresh/tick likewise overlaps the 600 ms interval and parent-completion refresh. readAppended has the same read-offset/await/commit shape.
  • Production manager feeds parent records individually into these trackers, making bursts a real entry path.

Reproduction and measurements

Use a temporary directory containing sessions/rollout-child.jsonl with exactly one newline-terminated record:

{"type":"response_item","timestamp":"2026-09-05T00:00:00.000Z","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"synthetic"}]}}

Instantiate the actual CodexSubAgentTracker. Send a parent response_item/function_call_output with call_id: "spawn" and output: JSON.stringify({agent_id: "child"}) via observeParentEntry, pointing to sessions/parent.jsonl. Immediately send N-1 event_msg/token_count records via the same public method. Wait for pending I/O, inspect the last onChange payload, and stop the test tracker.

Parent records in burst Unique child responses Reported turnCount Emissions
1 1 1 1
10 1 10 10
50 1 50 50

The public-event reproduction required no private-state setup. A second direct-refresh experiment gave the same results at concurrency 1/2/10/50.

The Claude twin was exercised with a temporary agent-child.meta.json and a single assistant tool_use record. Concurrent invocations of the actual tick method yielded totalToolUses/turnCount 1/2/10/50; at 50, the display ring retained 40 duplicate tool calls. That test used the private tick seam to await all invocations deterministically.

Harness loaded unmodified source through esbuild in memory and used real temporary files; no application build, filesystem mocks, or running Electron process was involved.

User impact, next steps, and tradeoffs

  • Incorrect turn/tool counts, repeated activity cards, inflated earlier-tool counts, redundant child reads/parses and subagents emissions during bursts/resume.
  • Serialize refresh per tracker with a dirty/trailing-run mechanism. Do not simply drop refreshes while busy: parent completion or appended bytes must still be observed.
  • Preserve partial UTF-8/JSONL handling and stop semantics. Stop during I/O must not revive state or emit after teardown.
  • Add a deterministic deferred-read concurrency regression for both trackers: one unique row must produce one fold despite many overlapping triggers; an append during the read must appear on the trailing pass.
  • No live CPU/FPS improvement or production incidence rate is claimed. Fixing discovery frequency separately is also worthwhile; serialization alone does not prevent sequential repeated directory scans.

Refs #103, #743 (the completion-ledger fix does not serialize child readers).

The table is an observed sample, not a guarantee of exact amplification: overlapping filesystem scheduling changes how many reads share an offset. Repeat runs reproduced inflated counts; a regression test should control the read barrier explicitly. Related discovery amplification is separately tracked in #803.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions