Skip to content

perf(server): persist the wire projection for streaming tool.updated data - #6675

Merged
juliusmarminge merged 1 commit into
pingdotgg:mainfrom
mInrOz:fix/tool-updated-write-amplification
Aug 15, 2026
Merged

perf(server): persist the wire projection for streaming tool.updated data#6675
juliusmarminge merged 1 commit into
pingdotgg:mainfrom
mInrOz:fix/tool-updated-write-amplification

Conversation

@mInrOz

@mInrOz mInrOz commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Fixes #6673. Related: #6608 / #5855 (read side of the same event flood), #5550 (retention), #5651 (assistant message write path). Decision date: 2026-08-15.

Outcome

Non-terminal tool.updated activities now persist the same projected data that clients receive on the wire. projectActivityPayload already runs in ws.ts and in http.ts, so no client can receive the full data of any activity today. This PR applies the same projection at ingestion time, before the payload reaches orchestration_events and projection_thread_activities.

This PR does not change: terminal tool.completed payloads (still persisted in full), the event count per tool call, any schema, any existing rows, or any client-visible payload.

Before and after

Failure mode before Mechanism after
A streaming update persists the full accumulated rawOutput/content, so one tool call with N updates writes O(N²) bytes, twice. Measured: one 65 KB tool result persisted 238.7 MB across 2,226 updates (3,772×); one tool call produced 357 MB of payload. A non-terminal update persists the projected form: toolCallId, kind, command, changed files, and a one-line rawOutput summary. Bytes per update are bounded (~1 KB), so a tool call writes O(N) bytes.
The event store grows without bound from streaming payloads (~235 MB/day measured on one instance), which multiplies the full-thread rescan cost fixed by #6608. Streaming updates no longer carry payload bulk into the event store. Terminal payloads remain the only large rows.

Decisions taken

Finding disposition (#6673)

Verification

Ran on Linux, Node v24.18.1, pnpm 11.10.0:

  • pnpm exec vp test run src/orchestration/Layers/ProviderRuntimeIngestion.activity.test.ts — 4 passed (2 new: projected persistence for tool.updated; full payload kept for tool.completed).
  • pnpm exec vp test run src/orchestration/Layers/ProviderRuntimeIngestion.test.ts src/orchestration/Layers/ProviderRuntimeIngestion.approval.test.ts src/orchestration/ActivityPayloadProjection.test.ts — 52 passed.
  • pnpm exec tsgo --noEmit in apps/server — no errors (pre-existing suggestions in unrelated files only).

Not run: the full monorepo suite (left to CI) and a production soak (moves to the rollout gate below).

Rollout and proof gates

We run t3 on self-hosted instances with the exact workload from #6673 (render jobs with progress-bar output). Ordered steps to produce production proof:

  1. Deploy a build of this branch to an idle instance.
  2. Run a chatty render job to completion.
  3. Compare SELECT count(*), sum(length(payload_json)) FROM projection_thread_activities WHERE thread_id = ? against an unpatched run of the same job. Expected: similar row count, payload bytes down by >100×.
  4. Report the numbers on this PR.

Rollback: reinstall the released npm build. The change is write-path only; there is no schema change and old rows are untouched, so nothing durable is lost by rolling back.

🤖 Generated with Claude Code


Note

Medium Risk
Write-path change to persisted activity payloads for streaming tool updates; behavior aligns with existing ws/http projection but could affect any internal reader that expected full in-progress data in the DB.

Overview
Fixes O(N²) storage from streaming tool runs by persisting the same slim data clients already get on the wire for non-terminal tool.updated activities, instead of the full accumulated stdout/content on every chunk.

runtimeEventToActivities now runs projectActivityPayload on item.updated tool lifecycle events before rows hit the event store and projection_thread_activities. item.completed is unchanged and still stores the full terminal payload. Event count and client-visible payloads are unchanged; only what gets written on the ingestion path for in-progress updates.

Regression tests assert projected tool.updated data (bounded size, one-line rawOutput) and full streamingData on tool.completed.

Reviewed by Cursor Bugbot for commit 66d0ce4. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Project wire payload for streaming tool.updated activities to avoid quadratic growth

Streaming tool output accumulates over time, so persisting the full data payload on every item.updated event causes storage to grow quadratically with stream length. runtimeEventToActivities in ProviderRuntimeIngestion.ts now wraps item.updated tool activities with projectActivityPayload, which truncates raw output and omits content fields. Terminal item.completed events are unaffected and still persist the full payload.

Macroscope summarized 66d0ce4.

…data

Fixes pingdotgg#6673

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 09579cc8-4eac-448e-87e2-8372557c63b1

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:S 10-29 changed lines (additions + deletions). labels Aug 15, 2026
@macroscopeapp

macroscopeapp Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved 66d0ce4

Performance optimization applying an existing projection function to streaming tool updates at persistence time, reducing O(N²) storage overhead. Full data is maintained for completed events. Self-contained with comprehensive tests.

You can customize Macroscope's approvability policy. Learn more.

@mInrOz

mInrOz commented Aug 15, 2026

Copy link
Copy Markdown
Contributor Author

Rollout-gate results from our idle instance (Linux LXC, Node v24.19.0, 2026-08-15).

A/B benchmark through the real persistence stack

We ran one identical synthetic tool call through runtimeEventToActivitiesOrchestrationEventStore.appendOrchestrationProjectionPipeline.projectEvent, on this branch (66d0ce4) and on its parent (f0ebc62). The harness is an untracked test file on the repo's own test layers (SqlitePersistenceMemory + OrchestrationEventStoreLive + OrchestrationProjectionPipelineLive), so the SQL write path is the production code path. Workload: 300 item.updated events, each adds 10 output lines; final rawOutput is 76,892 bytes; one terminal item.completed carries the full output.

metric parent f0ebc62 this branch change
projection_thread_activities bytes 24,018,619 190,135 126× less
orchestration_events bytes 24,074,799 246,315 98× less
total persisted bytes 48.1 MB 436 KB 110× less
bytes persisted / final output 625× 5.7×
append+project wall time 8,719 ms 979 ms 8.9× faster
activity rows 301 301 unchanged

The row count is unchanged, as intended: this PR bounds bytes, not event count. The wall-time gain is a side effect of smaller JSON through the synchronous SQL path.

Deploy smoke

pnpm run build:bundle on this branch; ran dist/bin.mjs serve against a fresh --base-dir with the released web client assets linked in. Result: "Migrations ran successfully", "Listening on http://127.0.0.1:3782", GET / and GET /api/health return 200. Stopped after the probe.

@juliusmarminge
juliusmarminge enabled auto-merge (squash) August 15, 2026 10:43
@juliusmarminge
juliusmarminge merged commit f075a58 into pingdotgg:main Aug 15, 2026
16 checks passed
github-actions Bot added a commit to omarcresp/t3code-flake that referenced this pull request Aug 15, 2026
## What's Changed
* fix(desktop): app zoom no longer zooms the preview browser by @juliusmarminge in pingdotgg/t3code#6649
* fix(server): keep provider notification consumers alive past startSession by @tsouth89 in pingdotgg/t3code#6538
* fix(server): treat removed Bitbucket permissions endpoint as unknown, not blocking by @lnieuwenhuis in pingdotgg/t3code#6525
* fix(ssh): let cold remote servers finish starting by @gbarros-dev in pingdotgg/t3code#6168
* fix(web): preserve Claude insight line breaks by @nateEc in pingdotgg/t3code#4344
* feat(web): accept file drops across the chat workspace by @dbalders in pingdotgg/t3code#6636
* fix(web): widen ordered-list marker gutter for 3+ digit item numbers by @lnieuwenhuis in pingdotgg/t3code#6527
* fix(server): bound thread activity hydration by @t3-code[bot] in pingdotgg/t3code#6153
* fix(web): restore the Archive action in the default sidebar thread menu by @lnieuwenhuis in pingdotgg/t3code#6526
* fix(web): open diff files from nested projects by @gbarros-dev in pingdotgg/t3code#6174
* fix(mobile): use tryOpenExternalUrl for markdown links in ThreadFeed by @mohamedmastouri-hue in pingdotgg/t3code#5872
* fix(web): open the file a bare filename reference names by @Brechard in pingdotgg/t3code#6297
* fix(server): stop the provider title mirror from overwriting real thread titles by @repparw in pingdotgg/t3code#5941
* fix(shared): match source-control providers by DNS label by @gbarros-dev in pingdotgg/t3code#6175
* feat(desktop): Chrome-style hold-to-quit by @Bil0000 in pingdotgg/t3code#5508
* fix(gitlab): submit review comments on context lines by @tarik02 in pingdotgg/t3code#6348
* fix(marketing): keep Grok mark clear of mobile hero copy by @NicL9923 in pingdotgg/t3code#4542
* fix(mobile): recover the QR pairing scanner when camera access is denied by @hey-jj in pingdotgg/t3code#6487
* fix(web): keep a long path from running under the folder picker button by @Sy-D in pingdotgg/t3code#4823
* fix(terminal): right-click paste works in the terminal by @StiensWout in pingdotgg/t3code#5240
* fix(mobile): explain iOS-only settings on Android by @danvernon in pingdotgg/t3code#4981
* fix(web): stop counting a workflow coordinator as a working agent by @Rishet11 in pingdotgg/t3code#6672
* fix(web): keep floating preview anchored after panel closes by @AksharP5 in pingdotgg/t3code#6547
* fix(web): unstick /connect after in-modal sign-in by redirecting to the authorize endpoint by @TorbenWetter in pingdotgg/t3code#5133
* fix(web): keep send reachable while a turn is running on mobile by @AMohamedAakhil in pingdotgg/t3code#4781
* fix(web): reject unsupported composer image types at attach time by @mdshzb04 in pingdotgg/t3code#6574
* Make ClaudeTextGeneration tests hermetic on Windows by @mihneaptu in pingdotgg/t3code#4508
* fix(web): show command output in work log by @LikoKiko in pingdotgg/t3code#4083
* fix(web): reserve sibling column width when resizing the right panel by @Lasdw6 in pingdotgg/t3code#6279
* fix(web): replace whitespace in new ref names with dashes by @jorj-pineda in pingdotgg/t3code#6270
* fix(client-runtime): branch list no longer resets while paging through refs by @a20hek in pingdotgg/t3code#5858
* fix(web): support Shift+Insert terminal paste by @aoright in pingdotgg/t3code#5982
* fix(web): keep the composer glass aligned with the context strip at any interface font size by @Williawar in pingdotgg/t3code#5703
* fix(codex): keep background memory out of chats by @AksharP5 in pingdotgg/t3code#5468
* fix(server): treat a missing Codex rollout as a recoverable resume error by @Rishet11 in pingdotgg/t3code#6671
* fix(web): hide provider Update toast action while an update is running by @mrmg in pingdotgg/t3code#6544
* fix(desktop): agent shells inherit a UTF-8 locale on macOS by @Linus-Boehm in pingdotgg/t3code#6236
* fix(server): ignore Claude command lifecycle messages by @naveed949 in pingdotgg/t3code#6606
* docs: mention Bitbucket user read scope needed by auth probe by @thamrx in pingdotgg/t3code#6291
* fix(server): return valid preview action results by @duncan-vc in pingdotgg/t3code#5966
* fix(claude): make "Always allow for session" stick, and only for the session by @kakismash in pingdotgg/t3code#5041
* fix(ssh): surface a failed remote t3 install instead of a silent 0-byte server.log by @TorbenWetter in pingdotgg/t3code#5132
* perf(server): persist the wire projection for streaming tool.updated data by @mInrOz in pingdotgg/t3code#6675
* fix(web): stop wrapping partial code block selections in markdown fences by @JoeJoeflyn in pingdotgg/t3code#5069
* fix(web): hide T3 Connect toggle in web app settings by @JoeJoeflyn in pingdotgg/t3code#5068
* fix(web): show provider account accent badge in sidebar rows and hover card by @vitalyiegorov in pingdotgg/t3code#5980
* fix(server): wait for concurrent SQLite writers instead of failing with SQLITE_BUSY by @ostapondo in pingdotgg/t3code#5134
* fix(web): reject oversized prompts before provider turn start by @naveed949 in pingdotgg/t3code#6602
* feat(web): collapse the question prompt from its header by @Jardo-51 in pingdotgg/t3code#6773
* fix(shared): degrade an unknown system time zone to UTC in usage windows by @Rishet11 in pingdotgg/t3code#6670
* fix(claude): discover repo-local .agents/skills in skill discovery by @RoshanMhatre in pingdotgg/t3code#5488
* fix(server): let slow provider CLIs raise their discovery probe budget by @CDVolvik in pingdotgg/t3code#6223
* fix(web): retain terminal PR badges after checkout switch by @sebbonit in pingdotgg/t3code#4755
* fix(web): show selected model in context window tooltip by @nqrwhal in pingdotgg/t3code#4772
* fix(web): scale command details with code font by @Serendeep in pingdotgg/t3code#6510
* fix(web): preserve XML-like tags in user messages by @0utsights in pingdotgg/t3code#4133

## New Contributors
* @mohamedmastouri-hue made their first contribution in pingdotgg/t3code#5872
* @NicL9923 made their first contribution in pingdotgg/t3code#4542
* @hey-jj made their first contribution in pingdotgg/t3code#6487
* @danvernon made their first contribution in pingdotgg/t3code#4981
* @Rishet11 made their first contribution in pingdotgg/t3code#6672
* @AksharP5 made their first contribution in pingdotgg/t3code#6547
* @TorbenWetter made their first contribution in pingdotgg/t3code#5133
* @AMohamedAakhil made their first contribution in pingdotgg/t3code#4781
* @mdshzb04 made their first contribution in pingdotgg/t3code#6574
* @mihneaptu made their first contribution in pingdotgg/t3code#4508
* @LikoKiko made their first contribution in pingdotgg/t3code#4083
* @Lasdw6 made their first contribution in pingdotgg/t3code#6279
* @jorj-pineda made their first contribution in pingdotgg/t3code#6270
* @a20hek made their first contribution in pingdotgg/t3code#5858
* @aoright made their first contribution in pingdotgg/t3code#5982
* @Williawar made their first contribution in pingdotgg/t3code#5703
* @mrmg made their first contribution in pingdotgg/t3code#6544
* @Linus-Boehm made their first contribution in pingdotgg/t3code#6236
* @naveed949 made their first contribution in pingdotgg/t3code#6606
* @thamrx made their first contribution in pingdotgg/t3code#6291
* @duncan-vc made their first contribution in pingdotgg/t3code#5966
* @kakismash made their first contribution in pingdotgg/t3code#5041
* @mInrOz made their first contribution in pingdotgg/t3code#6675
* @JoeJoeflyn made their first contribution in pingdotgg/t3code#5069
* @vitalyiegorov made their first contribution in pingdotgg/t3code#5980
* @ostapondo made their first contribution in pingdotgg/t3code#5134
* @Jardo-51 made their first contribution in pingdotgg/t3code#6773
* @RoshanMhatre made their first contribution in pingdotgg/t3code#5488
* @CDVolvik made their first contribution in pingdotgg/t3code#6223
* @sebbonit made their first contribution in pingdotgg/t3code#4755
* @nqrwhal made their first contribution in pingdotgg/t3code#4772
* @Serendeep made their first contribution in pingdotgg/t3code#6510
* @0utsights made their first contribution in pingdotgg/t3code#4133

**Full Changelog**: pingdotgg/t3code@v0.0.34-nightly.20260815.1100...v0.0.34-nightly.20260815.1101

Upstream release: https://github.com/pingdotgg/t3code/releases/tag/v0.0.34-nightly.20260815.1101
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:S 10-29 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Streaming tool.updated persists the full accumulated rawOutput on every delta — O(N²) bytes and rows per tool call

2 participants