Repository navigation
[Bug]: V2 ACP adapter persists every streamed tool_call_update, Devin file writes bloat statev2.sqlite #16428
Description
Activity
Note
Grok responding on behalf of Julius.
Triage
Thanks @darjss for the excellent report, the repro numbers and database breakdown made this very easy to verify.
What I found
- This reproduces on current
main(1f2b10c25b). It looks like the bound added for [Bug]: Grok ACP resends full cumulative terminal output in everytool_call_update, flooding ingestion for all threads #6556 in fix(grok): bound cumulative tool output updates #7279 doesn't apply on the V2 path. [Bug]: Server: the thread list read blocks the event loop and every other database call for seconds on large databases #14701 and [Bug]: Orchestrator V2 event writes stall the server on a high-latency disk (156 ms fsync), environment flips Offline #15707 are downstream symptoms of this kind of growth rather than duplicates. decideToolCallUpdateEmissionstill runs, but only insideAcpSessionRuntime's queue handler.handleSessionUpdateis multicast andAcpAdapterV2registers its own handler; its roottool_call/tool_call_updatepath callsemitToolon everyToolCallUpdated.emitToolalways merges intocontext.toolsand offers bothnode.updatedandturn_item.updated, andProviderEventIngestorwrites both.ThreadLiveEventCoalesceronly collapses the live websocket push, so it doesn't limitstatev2.sqlite.- Devin makes this sharp because
AcpRegistryAdapterV2sendscognition.ai/messageGrouping, but any ACP agent that streams tool updates is affected. File writes are stored as afile_changewhosediffStris the patch so far (acpToolCallDiffPatch), so each persisted snapshot repeats the whole file. - There's a second call site: the child-session branch (
sessionId !== rootSessionId) writesturn_item.updateddirectly without going throughemitTool. Devin routes non-root agents by rewritingsessionIdtoparentAgentId, so subagent file writes would stay unbounded if onlyemitToolis gated.
Likely fix area
The approach you outlined lines up with what I found. Details that seem important for it in
AcpAdapterV2.ts:- Keep merging every update into
context.tools(and the background-task, hydration, and subagent bookkeeping), and skip only thenode.updated/turn_item.updatedoffers. Still callrearmDeferredFinalizewhen a write is skipped. - Let updates with a
projectedStatusbypass the gate. Turn-endterminalizeOpenForegroundTools/terminalizeOpenRunOwnedItemspass"completed"/"interrupted", and the gate only forces an emit forcompleted/failed, so an interrupted tool could otherwise lose its final write. - Keep
lastEmittedDetailLengthandskippedSinceEmitbeside the merged tool, sincecontext.toolschanges even when the write is skipped. - Apply the same gate to the child-session branch.
- A regression test in
AcpAdapterV2.test.ts(1tool_call+ 200 growing updates) would be expected to land around ~22 persisted rows: the first update, every 10th change, thecompletedupdate, and the turn-end write.
One limit worth noting:
toolCallProgressLengthcountsdetail, text content blocks, andrawOutputtext, but not diffnewTextorrawInput. So streamed file writes coalesce on the every-10th heartbeat rather than on 256 characters of growth. That's the existing V1 rule and matches your 849 KB → 89.5 KB numbers; snapshots still contain the whole file, so very large writes can still grow the log.Since you offered to open a PR, linking it to this issue would help reviewers connect the two. A maintainer will decide on the fix direction.
- This reproduces on current
- addedbugSomething is broken or behaving incorrectly.Something is broken or behaving incorrectly.via-triageFiled through npx t3 triageFiled through npx t3 triage
on Oct 6, 2026 - added a commit that references this issue
on Oct 8, 2026
Before submitting
Area
apps/server
Steps to reproduce
acpRegistry, agentdevin).SELECT count(*) FROM orchestration_events WHERE event_type = 'turn-item.updated' AND payload_json LIKE '%<toolCallId>%'.Deterministic version without Devin: inject one
tool_calland then 200tool_call_updates intoAcpAdapterV2. Each update carries the file so far as adiffcontent block plusrawInput, and the last one hasstatus: "completed". Then countturn_item.updatedevents for thefile_changeitem.Expected behavior
A streamed tool call persists a bounded number of snapshots, as V1 did after #7279 fixed #6556.
decideToolCallUpdateEmissionemits on status/title changes, on 256+ chars of growth, every 10th skipped update, and always on completed/failed.Actual behavior
V2 persists every streamed
tool_call_updateas a fullturn-item.updatedplus anode.updated. Each snapshot repeats the whole file so far, so the bytes grow quadratically with file size.Devin streams file writes as it generates them when T3 sends
cognition.ai/messageGroupinginclientCapabilities._meta, whichAcpRegistryAdapterV2does. I captured the raw ACP traffic with T3's exact initialize capabilities. Writing a 4.5 KB file produced 1,156tool_call_updates in 17 s. Without that meta flag, the same prompt produced 3.The 200-update repro above against main (
76d3c96fd5) gives:file_changeupdates persistedOn my server (184 threads, three weeks of mostly Devin use),
statev2.sqlitegrew to 17.8 GB:event_typeturn-item.updatednode.updatedmessage.updatedOne Devin write of a 79 line file produced 2,194
turn-item.updatedevents in 32 s. 15,486 of the last 23,026turn-item.updatedrows werependingsnapshots. The DB compresses 27x with zstd, so nearly all of it is repeated content.Cause: #7279's gate still runs in
AcpSessionRuntime.handleSessionUpdate(AcpSessionRuntime.ts:2895), but it only filters the runtime's event queue.AcpAdapterV2registers its ownruntime.handleSessionUpdate(...)handler and callsemitToolfor everyToolCallUpdatedfromparseSessionUpdateEvent, so V2 never goes through the gate.ThreadLiveEventCoalesceronly throttles the live push, not what is written.Possible fix: in
emitTool, rundecideToolCallUpdateEmissionfor updates without aprojectedStatus. Keep merging every update intocontext.tools, and skip only the persisted write. Turn-end terminalization passes aprojectedStatus, so tools it closes still get their latest merged state written. A background tool that keeps running after the turn can lag by up to 9 updates until its next write, which matches V1. This is about 25 lines inAcpAdapterV2.tsplus a regression test inAcpAdapterV2.test.ts, which fails on main with 202 updates. I can open a PR if this approach works for you.Impact
Major degradation or frequent failure
Version or commit
Nightly
0.0.46-nightly.20261003.2632. Code reading and repro on main76d3c96fd5.Environment
Headless
t3service on Linux (Ubuntu, x86_64), Devin CLI 3000.11.3 via the ACP registry.Logs or stack traces
No errors. The cost shows up as event volume. Related: #14701 (slow reads on large databases) and #15707 (event write stalls).
Workaround
None found in the app.
Investigated and written with Claude Opus 5.5 via Claude Code.