Skip to content

perf(server): t3_thread_list reads only the listed project's threads - #17843

Merged
Yash-Singh1 merged 1 commit into
pingdotgg:mainfrom
only21mil:perf/thread-list-project-scope
Oct 10, 2026
Merged

Yash-Singh1 merged 1 commit into
pingdotgg:mainfrom
only21mil:perf/thread-list-project-scope

Conversation

@only21mil

Copy link
Copy Markdown
Contributor

Problem

t3_thread_list lists one project's threads, but ThreadManagementService.listProjectThreads builds it from orchestrator.getShellSnapshot() with no options. So every call, and every page of a paged listing, runs selectShellThreadRows and its correlated subqueries over every non-deleted thread in every project, archived ones included. Only then does it keep the requested project's active threads in JS. The cost follows the size of the whole environment, not the size of the project being listed.

On a long-running server with 4,409 threads in 14 projects, server traces showed 3.2 to 6.5 s per t3_thread_list call, all on the main thread. #14701 explains why that blocks every client and every other statement. A monitoring agent that listed 6 projects every 4 minutes held the main thread about 20 s per cycle, 8.6% of wall time.

Change

  • ShellSnapshotOptions takes an optional projectId. selectShellThreadRows adds AND t.project_id = ?, which SQLite serves from orchestration_v2_projection_threads_project_updated_idx (project_id, updated_at). The in-memory store applies the same filter.
  • listProjectThreads asks for { projectId, location: "active" }. It only ever returned snapshot.threads, which is the active set, so archived threads were already dropped. Now SQL drops them before their subqueries run.
  • Fork sources still load by id through the existing loop, whatever their project or archive state, so a fork's visibleItemCount does not change.
  • The project_id column and the payload's projectId are written by the same upsert from the same event, so the SQL filter keeps exactly the rows the JS filter kept. The subagent filter, sort, MCP filters and paging are unchanged and receive the same array.

Scope and approval

This fixes one cause of #14701, which a maintainer triaged as a real bug: #14701 (comment). The triage named two callers of this read, the client shell load and the pull request pass. t3_thread_list is a third caller, and the widest one: it reads every project and the archive to answer for one project.

It also fits the small focused fix exception. One option is pushed into SQL, the output is identical, and two tests cover it. No contract, client or behavior change.

It is independent of the last_error join order in the same statement (#17842), which makes each row cheaper; this PR reads fewer rows. #14703 moves client shell loads and the pull request pass to worker reads and does not touch this path. Other callers that read the whole snapshot with no options (project deletion, checkpoint restore safety, the relay activity snapshot) are left alone.

Verification

New tests in apps/server/src/orchestration-v2/ProjectionStore.test.ts, on a fixture with two projects: project A has an active thread, an archived thread, and a fork of a run in an archived thread of project B.

  • "memory shell snapshots scope to one project" and "reads only the requested project's threads into a scoped shell snapshot" check that the scoped snapshot's threads deep-equal the unscoped snapshot filtered to the project, that location: "active" returns no archived threads, that { projectId } alone returns the project's archived threads, and that the fork keeps its inherited items.
  • The SQL test then makes a project B thread undecodable. The unscoped snapshot fails with ProjectionStoreReadError and the scoped one succeeds, so the scoped read never touched project B's rows.
  • On main (98beed1) both tests fail: the option is ignored, so project B's threads come back. With this change they pass.

Focused checks (Linux x64, Node 24.18.1):

  • vp test run on ProjectionStore.test.ts, ThreadManagementService.test.ts, ThreadLaunchService.test.ts, ThreadPullRequestService.test.ts, storageCleanup.test.ts and mcp/OrchestratorMcpToolkit.integration.test.ts: 6 files, 166 tests passed. The MCP toolkit test lists threads with settled, snoozed, includeSubagents: false and a foreign projectId.
  • vp lint and vp fmt --check on the three changed files: clean.
  • vp run --filter t3 typecheck: exit 0, no errors. The only diagnostics in the changed files are existing suggestions on untouched lines.

Synthetic file database built by the repo's migrations and ProjectionStore.apply: 14 projects, 4,410 threads (108 archived), one run and one item per thread, 2,520 threads sharing 1,434 sessions on one provider instance, two cross-project forks. "Before" is the call main makes (getShellSnapshot(), then the project filter); "after" is the scoped call. Same process, median of 5 runs after a warm-up:

project listed initial shell-query rows before / after before after
largest 1,961 4,410 / 1,961 3,919 ms 1,750 ms
mid 323 4,410 / 323 3,845 ms 285 ms
small 27 4,410 / 27 4,056 ms 29 ms

The row counts are the threads the initial shell query returns. Fork-source reads are additional and included in the timings: the largest project's scoped call also reads one cross-project fork source, 1,962 thread rows in all. The sorted lists deep-equal each other for all three projects, including the fork's visibleItemCount. EXPLAIN QUERY PLAN on the scoped statement shows SEARCH t USING INDEX orchestration_v2_projection_threads_project_updated_idx (project_id=?).

Real database, read-only with Node 24.18.1 node:sqlite (21 GB statev2.sqlite from 0.0.46-nightly.20261007.2787, whose shell SQL matches main): the shipped statement over all 4,409 threads took 3,448 ms. The same statement with only AND t.project_id = ? added, for a 331-thread project, took 413 ms.

Not checked: a patched server serving t3_thread_list end to end on that database, macOS and Windows, and the full suite (CI).

Model and harness: Claude Opus 5.5 (1M context) in Claude Code, run from T3 Code.

🤖 Generated with Claude Code

listProjectThreads asked for the whole shell snapshot, so every
t3_thread_list page ran the shell query over every non-deleted thread in
every project, archived ones included, then kept one project's active
threads in JS. Shell snapshots now take a projectId, which adds
t.project_id = ? and uses the (project_id, updated_at) index, and the
list asks for that project's active threads. Fork sources still load by
id from any project, so visible item counts are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the vouch:unvouched PR author is not yet trusted in the VOUCHED list. label Oct 10, 2026
@github-actions github-actions Bot added the size:S 10-29 changed lines (additions + deletions). label Oct 10, 2026
@macroscopeapp

macroscopeapp Bot commented Oct 10, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved at bf21d0f

Macroscope's review found this PR approvable — This is a narrowly scoped performance fix that pushes an existing project filter into the shell snapshot query while preserving fork-source data and existing unscoped callers. Focused SQL and in-memory tests cover the changed path, with no schema, default, deployment, or static-analysis changes.

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

@coderabbitai

coderabbitai Bot commented Oct 10, 2026

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Path: .coderabbit.config.ts
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 49fd6427-ee6e-4b3d-8345-e631459f6149

📥 Commits

Reviewing files that changed from the base of the PR and between 8c777fb and bf21d0f.


📒 Files selected for processing (3)
  • apps/server/src/orchestration-v2/ProjectionStore.test.ts
  • apps/server/src/orchestration-v2/ProjectionStore.ts
  • apps/server/src/orchestration-v2/ThreadManagementService.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.



📝 Walkthrough

Walkthrough

Shell snapshot reads now support an optional project filter in SQL and in-memory stores. Project thread listing uses that filter while retaining its subagent filtering and sort order.

Changes

Project-scoped shell snapshots

Layer / File(s) Summary
Add project filtering to shell snapshots
apps/server/src/orchestration-v2/ProjectionStore.ts, apps/server/src/orchestration-v2/ProjectionStore.test.ts
ShellSnapshotOptions accepts an optional projectId. SQL and in-memory reads filter target threads by project. Fork-source threads remain available for visible-item counts. Tests cover active and archived threads, fork items, and malformed data in another project.
Use scoped snapshots for project thread listing
apps/server/src/orchestration-v2/ThreadManagementService.ts
listProjectThreads requests an active snapshot for the specified project. It continues to filter subagents and sort by update time and ID.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant ThreadManagementService
  participant ProjectionStore
  participant SQL
  ThreadManagementService->>ProjectionStore: Request active snapshot with projectId
  ProjectionStore->>SQL: Select shell thread rows for projectId
  SQL-->>ProjectionStore: Return project thread rows
  ProjectionStore-->>ThreadManagementService: Return scoped shell snapshot
Loading

Suggested reviewers: t3dotgg


Merge Risk | ⚪ Minimal · up to bf21d

Merge Risk: ⚪ Minimal · up to bf21d

Project thread listing now reads only the requested project's active threads, which should reduce load time. No actionable merge-blocking risk was found in the supplied change.

Security Architecture Review

Security architecture risk: 🔵 Low · up to bf21d

The change narrows thread-list reads while preserving project selection and fork-derived counts. No new access expansion was identified. Broader authorization and runtime behavior were outside the verified scope.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The changed read path is confined to the server's orchestration store. Its effective data scope is the selected project's threads plus required fork ancestors, not newly granted tenant, service, credential, or environment authority.

Trust Boundaries and Controls

  • observed — The existing MCP listing path resolves the explicit or caller project before delegating to ThreadManagementService. The PR changes where project selection occurs, not that public entrypoint's project-resolution behavior. Project scoping is a selection contract, not evidence of a new authorization mechanism.
  • observed — The SQL project predicate uses value interpolation rather than concatenating project text into SQL. After fork-source loading, an explicit targetThreadIds filter prevents source-only shells from entering either returned thread collection.

Resilience and Maintainability Implications

  • observed — The added SQL test asserts that an unrelated project's undecodable payload fails an unscoped snapshot but does not fail the scoped listing. Shared assertions compare scoped and unscoped-filtered output, including preserved fork counts. This provides inspected failure-containment evidence, not an executed test result.

Pre-merge checks | Passed 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check Passed The title clearly and concisely describes the primary change: limiting t3_thread_list reads to the requested project's threads. It uses an appropriate conventional commit format.
Description check Passed The description includes complete Problem, Change, Scope and approval, and Verification sections. It explains the performance issue, implementation, issue approval, test coverage, benchmark results, a…
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.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@Yash-Singh1
Yash-Singh1 merged commit a11f464 into pingdotgg:main Oct 10, 2026
30 checks passed
github-actions Bot added a commit to omarcresp/t3code-flake that referenced this pull request Oct 10, 2026
## What's Changed
* feat(server): stop Claude subagents without stopping their owner by @Yash-Singh1 in pingdotgg/t3code#17826
* fix(server): stopped native subagents no longer read as Running by @im-kvijay in pingdotgg/t3code#17223
* perf(server): t3_thread_list reads only the listed project's threads by @only21mil in pingdotgg/t3code#17843
* fix(server): show diffs for projects outside the server cwd by @maria-rcks in pingdotgg/t3code#17724
* fix(server): check the specific scope for scripts, preview input, and full-access MCP grants by @juliusmarminge in pingdotgg/t3code#17772
* fix(source-control): stop Forgejo status refresh from scanning every pull request by @loispostula in pingdotgg/t3code#12223
* refactor(contracts): source control provider kind is an open branded slug by @juliusmarminge in pingdotgg/t3code#17739
* feat(source-control): each host package ships a client definition by @juliusmarminge in pingdotgg/t3code#17746
* refactor(client-runtime): add project clone sources come from host definitions by @juliusmarminge in pingdotgg/t3code#17756
* refactor(web): host presentation and behavior come from client definitions by @juliusmarminge in pingdotgg/t3code#17757
* refactor(source-control): reference parsing and project matching are host resolvers by @juliusmarminge in pingdotgg/t3code#17770
* feat(pull-requests): quick actions follow each host's capabilities, not GitHub by @juliusmarminge in pingdotgg/t3code#17774
* feat(projects): new projects can be published to any ready host by @juliusmarminge in pingdotgg/t3code#17860
* fix(server): send Claude MCP servers over the control channel by @juliusmarminge in pingdotgg/t3code#17898
* fix(web): a finished reply replaced by a steer is no longer labeled partial by @juliusmarminge in pingdotgg/t3code#17761
* fix(client-runtime): queued runs that start after a steer show up in the thread by @juliusmarminge in pingdotgg/t3code#17764
* feat(mobile): choose the microphone order for voice input by @juliusmarminge in pingdotgg/t3code#17896
* feat(source-control): host settings live on each host's definition, with a GitCafe token by @juliusmarminge in pingdotgg/t3code#17901

## New Contributors
* @only21mil made their first contribution in pingdotgg/t3code#17843
* @loispostula made their first contribution in pingdotgg/t3code#12223

**Full Changelog**: pingdotgg/t3code@v0.0.46-nightly.20261010.2935...v0.0.46-nightly.20261010.2948

Upstream release: https://github.com/pingdotgg/t3code/releases/tag/v0.0.46-nightly.20261010.2948
github-actions Bot added a commit to davidvanderklay/t3code-flake that referenced this pull request Oct 10, 2026
## What's Changed
* feat(server): stop Claude subagents without stopping their owner by @Yash-Singh1 in pingdotgg/t3code#17826
* fix(server): stopped native subagents no longer read as Running by @im-kvijay in pingdotgg/t3code#17223
* perf(server): t3_thread_list reads only the listed project's threads by @only21mil in pingdotgg/t3code#17843
* fix(server): show diffs for projects outside the server cwd by @maria-rcks in pingdotgg/t3code#17724
* fix(server): check the specific scope for scripts, preview input, and full-access MCP grants by @juliusmarminge in pingdotgg/t3code#17772
* fix(source-control): stop Forgejo status refresh from scanning every pull request by @loispostula in pingdotgg/t3code#12223
* refactor(contracts): source control provider kind is an open branded slug by @juliusmarminge in pingdotgg/t3code#17739
* feat(source-control): each host package ships a client definition by @juliusmarminge in pingdotgg/t3code#17746
* refactor(client-runtime): add project clone sources come from host definitions by @juliusmarminge in pingdotgg/t3code#17756
* refactor(web): host presentation and behavior come from client definitions by @juliusmarminge in pingdotgg/t3code#17757
* refactor(source-control): reference parsing and project matching are host resolvers by @juliusmarminge in pingdotgg/t3code#17770
* feat(pull-requests): quick actions follow each host's capabilities, not GitHub by @juliusmarminge in pingdotgg/t3code#17774
* feat(projects): new projects can be published to any ready host by @juliusmarminge in pingdotgg/t3code#17860
* fix(server): send Claude MCP servers over the control channel by @juliusmarminge in pingdotgg/t3code#17898
* fix(web): a finished reply replaced by a steer is no longer labeled partial by @juliusmarminge in pingdotgg/t3code#17761
* fix(client-runtime): queued runs that start after a steer show up in the thread by @juliusmarminge in pingdotgg/t3code#17764
* feat(mobile): choose the microphone order for voice input by @juliusmarminge in pingdotgg/t3code#17896
* feat(source-control): host settings live on each host's definition, with a GitCafe token by @juliusmarminge in pingdotgg/t3code#17901

## New Contributors
* @only21mil made their first contribution in pingdotgg/t3code#17843
* @loispostula made their first contribution in pingdotgg/t3code#12223

**Full Changelog**: pingdotgg/t3code@v0.0.46-nightly.20261010.2935...v0.0.46-nightly.20261010.2948

Upstream release: https://github.com/pingdotgg/t3code/releases/tag/v0.0.46-nightly.20261010.2948
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.

2 participants