Skip to content

fix(server): Claude publishes each background roster change once - #34952

Open
Vantrongs wants to merge 2 commits into
pingdotgg:mainfrom
Vantrongs:fix/claude-roster-once
Open

Vantrongs wants to merge 2 commits into
pingdotgg:mainfrom
Vantrongs:fix/claude-roster-once

Conversation

@Vantrongs

@Vantrongs Vantrongs commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Claude Code reports a background task's start both as task_started and as a background_tasks_changed level, and its end both as a level and as task_notification, in either order (the recorded fixtures claude_background_task_after_root, claude_background_task_wake and claude_background_monitor_wake under apps/server/src/orchestration-v2/testkit/fixtures/ show both orders). A level also lists subagents (local_agent), which parseClaudeBackgroundTaskEntry leaves out of the roster. applyBackgroundTaskRosterMessage in ClaudeAdapterV2.ts treats every level and every backgrounded task_started as a change, so emitProviderThreadRoster publishes the same roster two or three times, each with a new updatedAt.

Every copy becomes a persisted provider-thread.updated event. Each one appends an event row with its indexes, rewrites the provider thread projection and its indexes, and bumps the thread row: about 30 SQLite pages per event on a real database (#24003, breakdown by b-tree).

On a Linux desktop install (0.1.9-nightly.20261111.4501), one Claude thread that ran background shells and subagents for half an hour produced 496 provider-thread.updated events. 108 were byte-identical to the previous event for the same provider thread, written 1–2 ms after it, and 45 more differed only in updatedAt. 104 of these repeats came right after an event that added or removed a task.

Change

emitProviderThreadRoster compares the event it would send with the last provider_thread.updated the adapter sent for that provider thread, from any emitter. It ignores updatedAt, counts pendingBackgroundWork, and skips the event when nothing else changed. The adapter's internal roster bookkeeping still runs for every message; only the repeated publication is dropped.

  • emitProviderEvent records the last provider_thread.updated per provider thread after it adds the attachment budget, so the comparison sees the event as sent. The budget step moved into a helper, withAttachmentBudget, for that.
  • startTurn forgets the record for its provider thread. Provider events reach the server only while a run is subscribed, so an event sent between runs may have been dropped and must not stand in for the next one.
  • Every caller of emitProviderThreadRoster (roster messages, awaitBackgroundTasks, the wake retire path, resetBackgroundTaskStateForNativeThreadProcess) goes through the same check.
  • The turn-finalize, goal and attachment-budget emissions are unchanged. A finalize equal to the previous event can still be needed after the server writes the provider thread itself.

Scope

A small fix of an obvious bug, so no separate issue: the same state is written to the database two or three times per background task change. It is one part of the write amplification in #24003. #34909 shrinks the thread.visited payload; this PR removes whole events, and each event is its own commit of about 30 pages whatever its size, so fewer events is what lowers the write rate. The change is limited to the Claude adapter's roster publication; nothing T3 stores or shows changes.

Verification

  • New test in ClaudeAdapterV2.test.ts, "ClaudeAdapterV2 background wake turns" › "publishes each background roster change once". It feeds a Bash run_in_background launch, task_started, levels [shell], [shell, local_agent], [shell], [], task_notification, then a new task. It asserts the (task ids, pendingBackgroundWork) of every provider_thread.updated:
    • on main it fails with three extra ["bdqirlcyw"], false entries (expected [ [ [], false ], …(6) ] to deeply equal [ [ [], false ], …(3) ]);
    • with the change it passes with [[], false], [[bdqirlcyw], false], [[bdqirlcyw], true], [[b2], false], so the task's end and the new task are still published.
  • The 11 recorded Claude background replay scenarios stored 208 provider-thread.updated events with 65 repeats before the change and 191 with 49 repeats after. The remaining repeats come from the finalize and attachment-budget emissions, which this PR leaves alone.
  • vp run check:changed: format, lint, knip, typecheck and the two touched test files (402 tests) pass, rebased on 1e438c8f80. src/orchestration-v2/Adapters/ and testkit/: 1186/1186 pass.
  • Not measured: the write volume on a live install with the change applied.

Model and harness: Claude Opus 5.5, Claude Code in T3 Code

Claude Code reports a background task's start and end both as a task
event (task_started, task_notification) and as a background_tasks_changed
level, and a level also lists subagents, which the roster leaves out. The
adapter published the roster after every such message, so most roster
changes reached the server two or three times as identical
provider_thread.updated events, each persisted with its indexes.

The roster emission now compares itself with the last provider thread
update the adapter sent for that thread, ignoring updatedAt and keeping
pendingBackgroundWork, and skips an unchanged one. A turn start forgets
the last update, since provider events reach the server only while a run
listens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:unvouched Author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Nov 12, 2026
@macroscopeapp

macroscopeapp Bot commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved at d939148

Macroscope's review found this PR approvable — Claude background-roster updates are deduplicated while preserving changed thread contents, attachment budgets, and pending-work state. Comparison resets each turn, limiting the change to redundant event publication.

You can add or adjust custom eligibility rules. Learn more.

@coderabbitai

coderabbitai Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

No actionable comments were generated in the recent review.

Walkthrough

ClaudeAdapterV2 now projects provider-thread updates and skips roster emissions when the projected thread and pending-background-work state match the last emitted update. A test covers task events and the resulting distinct roster states.

Changes: Claude provider-thread roster

Layer / File(s) Summary
Build and deduplicate roster updates
apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts, apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts
The adapter projects provider-thread updates with attachment-budget metadata and cleared-session IDs, then compares thread content and pendingBackgroundWork with the cached update. It ignores updatedAt for comparison and clears the cached update at turn start. The test checks that overlapping task events emit only distinct roster states.

Estimated code review effort: 2 (Simple) | ~12 minutes

Change: Bug fix

Suggested reviewers: juliusmarminge, yash-singh1, maria-rcks

Merge Risk: Minimal · up to abc44

This change stops Claude from publishing duplicate background roster updates, which reduces repeated database writes. No concrete merge-blocking risk was found, and a regression test covers the new behavior.

Pre-merge checks: Passed 4

Check name Status Explanation
Title check Passed The title names the main change: Claude's adapter now publishes each background roster change once, which matches the adapter change that skips repeated provider_thread.updated emissions. It is concis…
Description check Passed The description follows the repository template closely. It has Problem, Change, Scope, and Verification sections, and each section gives specific information. The Problem section explains the duplica…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Title check

Explanation

The title names the main change: Claude's adapter now publishes each background roster change once, which matches the adapter change that skips repeated provider_thread.updated emissions. It is concise, specific, and follows the conventional commit style used in the repository template.

Full details: Description check

Explanation

The description follows the repository template closely. It has Problem, Change, Scope, and Verification sections, and each section gives specific information. The Problem section explains the duplicate roster publications with fixtures and measured event counts. The Change section lists the adapter modifications. The Scope section explains why no issue is linked and why the fix is small. The Verification section reports a focused test with expected and observed results, and it states the unmeasured live-install write volume. The template's agent-model line is present at the end. The description does not link a GitHub issue; it links the related PR #24003 and explains why no issue is needed, which the template allows for small obvious fixes.

Finishing Touches

Generate unit tests (beta)

  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Recent review info

Run configuration

Commits

Reviewing files that changed from the base of the PR and between d939148 and abc4442.

Files selected for processing: 2

  • apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts
  • apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.

@github-actions

Copy link
Copy Markdown
Contributor

This PR conflicts with main. Rebase it or merge main into it. If it still conflicts after 7 days with no activity, it will be closed automatically.

See the conflict window.

@github-actions github-actions Bot added the needs-rebase Conflicts with its base branch. Closes after 7 days with no activity. label Nov 13, 2026
@juliusmarminge juliusmarminge added the takeover:in-progress A takeover lead is working on this PR label Nov 14, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@juliusmarminge

Copy link
Copy Markdown
Member

The automated pipeline stopped on this pull request and will not merge it:

the capture step could not show the problem on the base commit, so there is nothing to prove fixed.

For each run, add a synthetic git project and start a new claudeAgent/claude-haiku-5-5 thread with the same prompt: launch three background Bash commands sleeping 25/50/75 seconds and two native background Agent subagents sleeping 35/60 seconds, then end the turn and acknowledge completion wakes. Observe persisted events for at least 100 seconds and until quiet. Compare consecutive provider-thread snapshots per provider thread, counting exact and updatedAt-only repeats separately. Record server-process write bytes and CPU time. Derive action duration from the first persisted run request through final run completion. Verify three shell commands, two completed provider-native subagents with expected results, completed runs, and an empty final roster. Exclude head run 3 because dependency installation overlapped it; replace it with run 6. Stop servers and remove both worktrees and scratch data after uploading.

It stays open for a person to finish or close.

@juliusmarminge juliusmarminge added takeover:blocked Takeover blocked; reason in the PR body and removed takeover:in-progress A takeover lead is working on this PR labels Nov 14, 2026

This branch has not been deployed

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

Labels

needs-rebase Conflicts with its base branch. Closes after 7 days with no activity. size:M 30-99 changed lines (additions + deletions). takeover:blocked Takeover blocked; reason in the PR body vouch:unvouched Author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants