Repository navigation
[Bug]: Claude subagent progress re-stores the full subagent prompt on every update (~50x write amplification in statev2.sqlite) #18204
Description
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area
apps/server
Steps to reproduce
- On Orchestrator V2 with the Claude provider, start a thread that launches several subagents doing long, tool-heavy work (each runs for many minutes and emits frequent progress).
- Open
statev2.sqliteread-only and compare stored bytes with final state per subagent:
WITH x AS (
SELECT json_extract(payload_json, '$.id') AS id,
count(*) AS updates,
count(DISTINCT json_extract(payload_json, '$.prompt')) AS distinct_prompts,
sum(length(payload_json)) AS stored,
max(length(payload_json)) AS final
FROM orchestration_events
WHERE event_type = 'turn-item.updated'
AND json_extract(payload_json, '$.type') = 'subagent'
GROUP BY 1
)
SELECT count(*) AS subagents, sum(updates) AS updates,
sum(distinct_prompts = 1) AS prompt_never_changed,
sum(stored) AS stored_bytes, sum(final) AS final_bytes
FROM x;Run the same query with event_type = 'subagent.updated' (and without the $.type filter) for the second copy.
Expected behavior
Event-store growth is roughly proportional to what changed. A progress tick that changes only progress stores the change, not the whole subagent record again.
Actual behavior
Each Claude subagent progress tick is written as two full snapshots: a turn-item.updated (item type subagent) and a subagent.updated. Both carry the complete prompt, although the prompt almost never changes between ticks. ProviderEventIngestor persists every subagent.updated as-is, and nothing coalesces or diffs these events.
Measurements from one install, one UTC day (aggregate numbers only):
- 123,376 V2 events, 334 MiB of
payload_json+metadata_json. - By event type (count, payload MiB):
turn-item.updated62,295 / 200.0;subagent.updated15,469 / 68.2;node.updated35,068 / 28.2;message.updated2,599 / 4.4;provider-turn.updated3,617 / 3.3;checkpoint.captured245 / 2.3. Every other type was under 1.2 MiB. - Subagents: 137 subagents produced 15,469 progress updates (mean 113, max 1,258 for one subagent). Each update is stored twice, so there were 30,938 events.
- The prompt (mean 3.3 KiB, max 7.4 KiB) was byte-identical across all updates for 100 of the 137 subagents, and had at most 5 distinct values for the rest.
- The field that changes is
progress(13,229 distinct values). - Repeated
prompttext alone was 103.8 MiB, 31% of the day's event bytes.
- Amplification:
turn-item.updatedsubagent items stored 73.9 MiB for 1.5 MiB of final state (49.8x);subagent.updatedstored 68.2 MiB for 1.43 MiB of final state. - For comparison, other item types in the same day stay close to their final size:
command_execution69.9 MiB stored / 51.0 MiB final (1.4x),dynamic_tool30.7 / 29.5,reasoning16.7 / 16.3,assistant_message3.4 / 3.3. - Across six consecutive days, subagent snapshot events were 17–42% of each day's event bytes, 797 of 2,634 MiB in total (30%); repeated prompt text was 552 MiB (21%).
Effect on the database:
statev2.sqliteis 15.6 GB after one week on V2. That includes history imported from V1 at the migration.- V2 event bytes per day were 248, 630, 320, 579, 523 and 334 MiB. The data directory grew 0.8–2.0 GiB a day (mean about 1.35 GiB). The difference is indexes and projection rows; that split is an estimate, as I did not run a full
dbstatper day. PRAGMA auto_vacuumis 0 (NONE), so deleted rows would not shrink the file without a fullVACUUM.ProjectionMaintenanceV2.compactEventStorehas no caller in the installed builds or onmain(18656a3) apart fromFoundationPersistence.test.ts; fix(server): projection maintenance finally has an entrypoint #15759, which scheduled it, was closed pending approval. Even when it runs, it would not help here: it keepsturn-item.updatedby design ("replay assigns positions on first write") and does not treatsubagent.updatedas supersedable.
Impact
Minor bug or occasional failure
Version or commit
0.0.46-nightly.20261007.2787 (measured). On main @ 18656a3, ProviderEventIngestor still persists every subagent.updated payload as-is.
Environment
Linux x64 server (t3 serve), Orchestrator V2, statev2.sqlite in WAL mode, Claude provider (Claude Agent SDK) with heavy subagent use (100+ subagents a day).
Suggestions, in order of impact
- Progress events carry only changed fields (for example
progress, status, timestamps), never the unchangedprompt. Alternatively, skip asubagent.updated/turn-item.updatedwhose payload equals the last stored one apart fromprogress, as perf(server): ACP tool updates no longer persist a snapshot per streamed chunk #16682 did for ACP tool updates and perf(server): OpenCode 2 stops writing unchanged session status updates #16775 proposes for OpenCode 2. - Once a subagent finishes, collapse its progress snapshots to the final one (the approach
compactEventStorealready takes fornode.updatedandmessage.updated). - Run event compaction on a schedule (the cadence question from fix(server): projection maintenance finally has an entrypoint #15759).
- Create databases with
auto_vacuum = INCREMENTAL, convert existing ones with a one-timeVACUUMat upgrade, and runPRAGMA incremental_vacuumafter compaction so freed pages return to the filesystem.
Related
- [Bug]: V2 ACP adapter persists every streamed tool_call_update, Devin file writes bloat statev2.sqlite #16428 / perf(server): ACP tool updates no longer persist a snapshot per streamed chunk #16682: the same per-update snapshot problem for ACP tool calls, fixed for ACP only.
- perf(server): OpenCode 2 stops writing unchanged session status updates #16775: OpenCode 2 unchanged session and node updates.
- [Bug]: Codex subagent progress floods orchestration ingestion and delays unrelated threads #5681 / fix(server): bound Codex subagent progress events #5682 / fix(server): coalesce Codex child progress before ingestion #6004: Codex subagent progress flooding ingestion (V1).
- [Bug]: Linked-PR snapshot updates store the full thread in the event log #16558: linked-PR snapshot updates store the full thread.
- [Bug]: V2 stores tool-result image bytes twice per item, and no client reads them #16647 / fix(server): stop storing tool image bytes no client reads #16652: tool-result image bytes stored twice.
- [Bug]: Deleting a thread says it permanently clears its history, but V2 keeps every event and projection row #17845: deleting a thread keeps every V2 event and projection row.
- fix(server): projection maintenance finally has an entrypoint #15759: scheduler entrypoint for
compactEventStore(closed). - [Bug]: Upgrading is brittle on large database #17937, [Bug]: Server never collects SQLite planner statistics, so large databases full-scan and stop answering #17955, [Bug]: Server: the thread list read blocks the event loop and every other database call for seconds on large databases #14701: failures that get worse as
statev2.sqlitegrows.
Workaround
None supported. Deleting threads does not remove their events (#17845), and there is no compaction command.
Activity
juliusmarminge commentedon Oct 11, 2026
Note
Grok responding on behalf of Julius.
Confirmed write amplification on current main (
18656a340). Every Claude subagent progress tick is persisted as a full snapshot, and the unchangedpromptrides along.What main does.
ClaudeAdapterV2.updateClaudeSubagentNodeemits bothsubagent.updated(payload = the wholeOrchestrationV2Subagent, includingprompt) andturn_item.updatedwithtype: "subagent"andprompt: task.prompt. Progress frames call that path with onlyprogress/ workflow telemetry changing.ProviderEventIngestorthen writessubagent.updatedas-is (payload: input.event.subagent); unliketurn_item.updated, there is no strip/diff/coalesce step. So each tick stores two full copies of a mostly-static record.Compaction does not cover this today.
ProjectionMaintenanceV2.compactEventStorehas no production caller — onlyFoundationPersistence.test.ts. Even if it ran, it keepsturn-item.updatedby design ("replay assigns positions on first write") and only supersedesmessage.updated,node.updated, and a fixed set of thread-state events.subagent.updatedis not in that set. Scheduling that job and changingauto_vacuumstay with the product call that closed #15759; they are out of scope for this issue.Related, not duplicates. Same full-snapshot class as #16558 (linked-PR sync) and the growth/retention pressure in #17845 / #12758. #16682 already shipped the skip-unchanged gate for ACP tool updates; #16775 proposes the same idea for OpenCode 2. Neither touches Claude subagent progress.
A gate that drops a
subagent.updated/ subagentturn-item.updatedwhen onlyprogress(and timestamps) changed — or that persists only the delta fields — matches the #16682 precedent and would cut the amplification the report measured. Collapsing finished subagents' progress history is a separate follow-up once compaction policy is decided.ScottN-PV commentedon Oct 11, 2026
Read on main
ed0f7b9, source only: each accepted Claudetask_progressframe emits a fullsubagent.updatedand a full subagentturn-item.updated, both carrying the unchangedprompt. The numbers below are the report's; we have not reproduced it or measured a fix.The gate the triage describes, dropping a
subagent.updatedor subagentturn-item.updatedwhen onlyprogresschanged, would also stop live progress in the clients. Those two events are the only path into the server and client projections, andsubagentDetailPreviewleads withprogressfor a running subagent, in the web tooltip and the mobile agents list. The gate would freeze that text at its first value. #16682 kept every visible-output change and sampled only streamed arguments, so it does not cover dropping progress.Skipping snapshots that are identical apart from timestamps would skip few updates here. The report counts 13,229 distinct
progressvalues in 15,469 updates, and the adapter already skips empty progress frames.That leaves two fixes:
promptoptional onOrchestrationV2Subagentand the subagent turn item, and merge on upsert in both projections (serverupsertById, clientupsertEntity), which replace the record today. This stops repeating the prompt, but changes the event contract (these updates carry full records today) across contracts, server and client.ClaudeAdapterV2. Keep full snapshots but publish progress-only changes at most once every few seconds per subagent, always emitting the first update, lifecycle and other non-progress changes, and the final state. Adapter-local. Running subagents refresh progress less often, and it is a partial reduction: each kept snapshot still repeats the prompt, and the saving depends on how fast ticks arrive.I'd start with 2 and leave 1 for a contracts change you own. Which do you want?
Written by Claude Opus 5.5 and Claude Fable 5.1 on behalf of ScottN-PV.
AdEx-Partners-DE commentedon Oct 11, 2026
A second install, in case it helps to size this. I ran the two queries from the report read-only against my own
statev2.sqlite(Windows 11, Claude and Codex, moderate subagent use; the file is 1,590 MiB and holds 250,812 events with 597.6 MiB ofpayload_json, including history imported from V1).The shape is the same, the share is smaller:
turn-item.updated, item typesubagentsubagent.updatedcommand_execution164.4 MiB stored for 139.3 MiB final (1.18x),dynamic_tool79.6 for 71.8 (1.11x),reasoning24.8 for 20.7 (1.20x).PRAGMA auto_vacuumis 0 here as well.So on a lighter install the amplification per subagent is still about 22x, and the prompt is the bulk of it: storing it once would remove roughly 98% of the repeated prompt bytes. Not measured: per-day growth, and anything about a fix.
Would it be enough to drop
promptfrom the progress snapshots and keep it only on the first event of each subagent, so that live progress still reaches the clients?Posted by Claude (Opus 5.5, Claude Code via T3 Code) on behalf of @AdEx-Partners-DE.
juliusmarminge commentedon Oct 14, 2026
Note
Grok responding on behalf of Julius.
Fixed by #18375, now on main. New Claude subagent progress events no longer store the unchanged prompt again; the prompt is stored once per subagent and restored on read. Rows written before this change keep their full prompts, and reclaiming that existing space is tracked in #15759.
fix(server): Claude subagent progress no longer re-stores the unchang…
AdEx-Partners-DE commentedon Oct 14, 2026
Thanks for the fix. I took #18375 into our Windows fork right after it merged, and it does what it says on a real install. Read-only queries on that server's
statev2.sqlite,subagent.updatedevents from Claude subagents:The subagent
turn-item.updatedevents moved the same way: 4,636 bytes on average before, 1,527 after.Limits of this: the after sample is two subagents in three hours, so the averages will move as more run, and I compared stored rows only; I did not check how the prompt reads back in the UI. As you wrote, the rows from before keep their size until #15759 or something like it reclaims them.
Posted by Claude (Opus 5.5, Claude Code via T3 Code) on behalf of @AdEx-Partners-DE.