Repository navigation
Description
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Related: #20443 (closed; asked for database growth from a live OpenCode session), #21154 (closed; asked for a new issue covering only the turn-item.updated / OpenCode per-chunk part), #19780 and #20990 (compacting intermediate turn-item snapshots after the fact).
Area
apps/server
Steps to reproduce
- Leave
responseStreamingModeat its default,"paragraph". - Run OpenCode threads on a model that writes long reasoning (we used local models served through Ollama).
- On a copy of
statev2.sqlite, count streamed reasoning snapshots per item:
SELECT json_extract(payload_json, '$.id') AS item,
count(*) AS streaming_rows,
sum(length(payload_json)) AS bytes
FROM orchestration_events
WHERE event_type = 'turn-item.updated'
AND json_extract(payload_json, '$.type') = 'reasoning'
AND json_extract(payload_json, '$.streaming') = 1
GROUP BY item
ORDER BY bytes DESC;Expected behavior
The durable event log stores a bounded number of rows for a streamed reasoning item, so its storage cost grows roughly with the final text length, not with its square.
Actual behavior
In paragraph mode, makeAssistantStreamingFilter (apps/server/src/orchestration-v2/assistantStreaming.ts) passes one streaming turn_item.updated snapshot per item at most every 400 ms, whenever a new paragraph is ready. RunExecutionService hands each passed event to ProviderEventIngestor.ingestNormalized, so each snapshot becomes a permanent turn-item.updated row holding the full cumulative text. A reasoning block that streams for several minutes writes hundreds of rows, each larger than the last.
Measured read-only on a copy of one macOS install's statev2.sqlite (about 24 GiB, 3.76 M events), for one day (2026-10-20 UTC) in paragraph mode:
| provider / item type | items | streaming rows | payload |
|---|---|---|---|
| opencode / reasoning | 5,656 | 93,095 | 2,188 MB |
| opencode / assistant_message | 14,581 | 2,781 | 28 MB |
| claude / reasoning | 4,769 | 833 | 8 MB |
| codex / assistant_message | 1,976 | 441 | 5 MB |
| codex / reasoning | 969 | 3 | 2 MB |
- OpenCode reasoning averaged 17.5
turn-item.updatedrows per item that day. Every other provider and item type stayed at 1.22 or less. - The largest single item was one reasoning block: 612 rows, 69 MB. The next four were 448 rows (50 MB), 484 (43 MB), 416 (36 MB) and 392 (33 MB).
- The top 10% of streamed OpenCode reasoning items hold 86% of those bytes. p50 final reasoning length was about 1.7 k characters and p99 about 77 k.
- Total event payload that day was 2.78 GB, against 0.5 to 1.2 GB on other days.
- After switching the same install to
responseStreamingMode: "turn", the next day stored 0 streaming reasoning rows and 0.6 GB of event payload in total.
The 400 ms throttle decides how many rows are stored. The gap between consecutive stored snapshots of one item was p10 0.43 s, p50 0.53 s, p90 0.99 s. So adapter-side delta coalescing at the 50 ms interval the Claude, Codex and ACP adapters use would not change the stored row count. OpenCode stands out because its reasoning streams long and in many paragraphs. It is not because those adapters persist less. Claude reasoning goes through the same filter but is short, so it stays small.
Background compaction (#19435) does not remove these rows, because turn-item.updated is outside its scope (#19780). #20990 would remove them on a later pass, but they are still written: on this install, 2.2 GB a day of writes on the single writer connection, then the page churn.
assistantStreaming.ts, the OpenCode v2 adapter's text path and ProjectionMaintenance.ts are unchanged in this respect between v0.1.1 and main at a49d18c.
Impact
Major degradation or frequent failure
Version or commit
v0.1.1 (desktop); the code paths above are unchanged on main at a49d18c.
Environment
macOS, T3 Code desktop v0.1.1, OpenCode provider with local Ollama models, plus Claude and Codex on the same install for comparison.
Logs or stack traces
2026-10-20 UTC, turn-item.updated, type in (reasoning, assistant_message)
opencode reasoning items 5656 rows 98751 streaming rows 93095 payload 2188 MB
opencode assistant_message items 14581 rows 17362 streaming rows 2781 payload 28 MB
claude reasoning items 4769 rows 5602 streaming rows 833 payload 8 MB
codex assistant_message items 1976 rows 2417 streaming rows 441 payload 5 MB
codex reasoning items 969 rows 972 streaming rows 3 payload 2 MB
gap between stored streaming snapshots of one opencode reasoning item: p10 0.429 s, p50 0.531 s, p90 0.989 s (n = 89,568)
Counts and sizes only. No thread content was read beyond $.id, $.type and $.streaming.
Workaround
Set responseStreamingMode to "turn". That stops live paragraph streaming for every provider. On this install it brought streaming reasoning rows to zero and daily event payload from 2.78 GB to 0.6 GB.
Possible directions for maintainers to choose from (no PR opened, since this changes how streamed text is persisted):
- Deliver intermediate streaming snapshots to live clients without making each one a durable event. Store only the first snapshot (for item position) and the final one.
- Bound intermediate persistence per item, for example a minimum interval that grows with text length, or a byte budget per item.
- Land the compaction in fix(server): compact superseded turn-item snapshots #20990 so these rows are at least removed after a pass. That bounds what is retained but not what is written.
Activity
docs: record where fork patches stand upstream
jason5545 commentedon Oct 26, 2026
A related symptom of the same paragraph gate, for reasoning: a model that thinks in one unbroken paragraph shows no thought at all until it ends.
splitBufferedAssistantTextfinds no boundary, somakeAssistantStreamingFilterpasses nothing while the item streams.Checked with Node against
assistantStreaming.tsonmain@ f88b33a, feeding a reasoning item one sentence every 500 ms for 30 s:We hit it with local models run through a Pi-based CLI: the thought box stayed empty for the whole thought.
Our fork passes reasoning's whole text on the existing 400 ms gate instead of waiting for a boundary, and keeps paragraph buffering for assistant text (
assistantStreaming.ts). That runs straight into this issue's cost: every 400 ms of streamed reasoning becomes another full-text row, for every provider, so it is not something to upstream as is.#24700 keeps the paragraph gate, so it does not change this. Its growth rule might: if reasoning snapshots passed on growth alone (each at least a set fraction longer than the last one passed), a one-paragraph thought would stream live and its stored snapshots would still stay within a few times the final text.
docs: link the upstream reports filed for fork patches
joei4cm commentedon Nov 12, 2026
Per CONTRIBUTING's prior-approval rule, a scope proposal before anyone opens a PR here.
#24700 implemented direction 2 — once a streamed item passes 8,000 characters, a snapshot is stored only when its text is at least 25% longer than the last stored one, so past that point stored snapshots grow geometrically instead of with the square of the text. Macroscope approved it at
62bb46d; it was then closed at @StiensWout's request because it intentionally changes paragraph streaming and interrupted-stream durability and had no prior scope agreement.If direction 2 is the one you want, I'd like to take it over: rebase the same rule onto the current filter (
#24432landed yesterday and rewrote these lines), keep the constants as proposed there (8,000-character threshold, 1.25 growth), and re-run its tests — the long-stream bound test fails on main today. The two decisions listed on that PR still need your call:If you would rather have direction 1 — live delivery without durable intermediate events — that needs a non-durable path through the live stream and its resume cursors, a larger change I would want to scope with you first.
No PR until a direction is confirmed on this issue.
joei4cm commentedon Nov 13, 2026
Thanks for picking this one up — #36362 covers it far more thoroughly than my proposal would have, so I'm dropping mine and staying out of the way.