Skip to content

perf(j5): field-scoped thread reads outside the Crew services - #810

Merged
Jacksondr5 merged 2 commits into
j5/mainfrom
j5/278-field-scoped-reads-part1
Oct 31, 2026
Merged

Jacksondr5 merged 2 commits into
j5/mainfrom
j5/278-field-scoped-reads-part1

Conversation

@Jacksondr5

Copy link
Copy Markdown
Owner

Problem

Since upstream pingdotgg#23902 (in J5 since #705), getThreadRecords reads on a separate reader connection, while getThreadProjection still reads on the writer. So every J5 full-thread read waits behind event commits, including the ones that run after every run and on every delivery. Part of #278.

What changed

Every J5 server site outside the in-flight Crew rework stack now calls getThreadRecords and asks only for the fields it uses, with a filter where one fits:

Site Read now
ArtifactRunFinalizationObserver (after every run) plans, turnItems filtered to that run's proposed_plan items
agentHandoffObserver (after every run) runs filtered to that run
DeliveryTransport.deliverAgent runs, provider turns/threads/sessions, the delivery's one message, and only the error turn items (all the usage-limit hold reads)
DeliveryTransport.cancelAgent, landed the one message, plus runs
CrewSeatFinishNotifier seat: runs + that run's error items; Captain: runs + user messages; sweep: runs
QueuedRunWatchdog (scan, VCS fact) that run, and its error items (its dedup facts are error items of the run)
SilenceDetector.failureDetail that run's error items
CrewRuntimeRequestService runtimeRequests + approval_request items
CrewStopService, ArchiveAgentService state runs
Existence and thread-only reads: ArchiveAgentService.readFacts, PlaybookCrewRelay ×2, CrewCaptainArchiveCascade restore/sweep, playbooks/workspace + workspaceRoot, agentInvocation, ThreadLineage []

getThreadRecordsIfPresent sits next to getThreadProjectionIfPresent and applies the same not-found rule. The old helper stays until its Crew-stack callers move in part 2.

Forks. getThreadProjection follows a fork's source history and getThreadRecords doesn't. In ProjectionStore.readProjection that history only fills visibleTurnItems. Every other field is the thread's own rows in both reads, and no J5 server code reads visibleTurnItems. So no swapped site sees different rows on a fork.

No behaviour change. Each filter leaves out only rows its caller already threw away.

Tests

  • Existing fakes now stub getThreadRecords.
  • DeliveryTransport.integration runs on the real SQL store. Its stale-snapshot hook now wraps getThreadRecords, so the two race tests still feed a stale read to deliverAgent; after the swap they had been passing without testing the race.
  • Two focused real-store tests where a filter decides the rows:
    • The plan observer exports the finished run's plan when a later run's plan item is newer.
    • The watchdog dedupes against the store's own error items. A wrong turnItemTypes filter fails it: checked by mutating it.

Deferred to part 2 (files touched by open Crew-stack PRs #750–#762, #770)

  • CrewProposalService (2 reads), CrewLaunchReporter (4), CrewLaunchService (1), SpawnAgentService (2 full reads, plus shell reads), a2a/mcp/handlers.ts (2)
  • CrewCaptainArchiveCascade settle cascade: needs the whole shell (threadShellFromProjection); follow upstream fix(server): avoid full shell read on resume pingdotgg/t3code#23987's narrow-query pattern or keep it
  • Delete getThreadProjectionIfPresent once those callers move

Already narrow, nothing to do: nativeQuestionInboxRead, NativeQuestionNotifier, receiverBacklog, SilenceDetector.readRunsAndMessages.

Live testing

Server-only change, so no UI. Runtime check on an isolated test server (the j5-live-test home in this worktree, never the live install) at 2a632e5:

# a Haiku thread created in the web client: run 1 completed ("ready")
$ j5 a2a send --to agent:j5:a2a:c6b12e4e-… --message "Reply with the single word: pong…" --client-request-id j5-278-check-3
{"ok":true,"result":{"messageId":"message:j5:a2a:…:j5-278-check-3","durableAtSeq":46}}
runs:      [{ordinal:1,status:"completed",model:"claude-haiku-5-5"},{ordinal:2,status:"completed",model:"claude-haiku-5-5",userMessageId:"message:j5:a2a:delivery:…check-3"}]
delivery:  [{status:"delivered"}]
messages:  user "Reply … ready" / assistant "ready" / user "[Message from automation machine:reads-check …" / assistant "pong"
# the same send retried: same receipt, no third run
# t3_thread_transfer fork of a fixture thread: the fork registered as agent:j5:a2a:mcp:…:fork (ThreadLineage placement)
# server log: no agent-handoff, plan-export, ThreadLineage or delivery-transport warnings

That run used both run-finalization observers and deliverAgent with the narrowed target read. Deliveries to fixture threads (and to the fork) were also delivered, but their runs failed: they try to resume the fixture's fake Claude session (Session … not found). That comes from the fixtures, not from this change.

Not exercised live: cancelAgent (covered by DeliveryTransport.integration), seat-finish notices, Crew stop and runtime requests (unit tests only).

Checklist

  • One concern: the description has no "also"
  • Tests cover the changed behavior (backend changes ship with focused tests)
  • UI changes: before/after screenshots above, and a video for motion or interaction (no UI change)
  • Live testing: the section above says what ran live and what did not
  • Upstream-owned files: none edited
  • Upstream product: no change to what upstream's product does
  • Surfaces: server-only reads; no contract, client, provider or connection-mode change
  • Docs: no product or user doc describes these reads

vp run check:changed origin/j5/main: format, lint, typecheck, graphql and the 19 test files beside the changes pass. Knip fails on existing unused exports, none added here.

Built by Claude Opus 5.5 in Claude Code (J5 Code Crew builder seat).

🤖 Generated with Claude Code

J5's full-thread reads (getThreadProjection) run on the writer connection
since upstream moved getThreadRecords to a reader, so every one queues
behind event commits. Move the sites outside the in-flight Crew stack to
getThreadRecords with only the fields, and filters, each caller uses.

Part of #278.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
j5-code Ready Ready Preview Oct 31, 2026 4:07pm UTC

Request Review

@coderabbitai

coderabbitai Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

  • Run on-demand review

This review includes 33 billable files and costs up to $8.25.

  • Ask an admin to make reviews automatic

Open in CodeRabbit

Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing.

Or wait 12 minutes for your next included review.

Check out review usage here.

View limit details

Limit details: You’ve used all 3 included reviews currently available. Your 44 included PR review attempts over the past 7 days set your current allowance at 3 reviews per hour.

Learn how review limits work.

Review configuration:

Run configuration
Commits

Reviewing files that changed from the base of the PR and between 16a9c43 and 633f584.

Files selected for processing: 33
  • apps/server/src/j5/a2a/ArchiveAgentService.test.ts
  • apps/server/src/j5/a2a/ArchiveAgentService.ts
  • apps/server/src/j5/a2a/CrewCaptainArchiveCascade.test.ts
  • apps/server/src/j5/a2a/CrewCaptainArchiveCascade.ts
  • apps/server/src/j5/a2a/CrewProposalService.test.ts
  • apps/server/src/j5/a2a/CrewRuntimeRequestService.test.ts
  • apps/server/src/j5/a2a/CrewRuntimeRequestService.ts
  • apps/server/src/j5/a2a/CrewSeatFinishNotifier.test.ts
  • apps/server/src/j5/a2a/CrewSeatFinishNotifier.ts
  • apps/server/src/j5/a2a/CrewStopService.test.ts
  • apps/server/src/j5/a2a/CrewStopService.ts
  • apps/server/src/j5/a2a/DeliveryTransport.integration.test.ts
  • apps/server/src/j5/a2a/DeliveryTransport.ts
  • apps/server/src/j5/a2a/DeliveryWorker.test.ts
  • apps/server/src/j5/a2a/SilenceDetector.test.ts
  • apps/server/src/j5/a2a/SilenceDetector.ts
  • apps/server/src/j5/a2a/ThreadLineage.ts
  • apps/server/src/j5/a2a/runFailures.ts
  • apps/server/src/j5/a2a/threadProjectionReads.ts
  • apps/server/src/j5/agents/agentHandoffObserver.ts
  • apps/server/src/j5/agents/agentInvocation.test.ts
  • apps/server/src/j5/agents/agentInvocation.ts
  • apps/server/src/j5/artifacts/ArtifactRunFinalizationObserver.test.ts
  • apps/server/src/j5/artifacts/ArtifactRunFinalizationObserver.ts
  • apps/server/src/j5/playbooks/PlaybookCrewRelay.test.ts
  • apps/server/src/j5/playbooks/PlaybookCrewRelay.ts
  • apps/server/src/j5/playbooks/mcp.test.ts
  • apps/server/src/j5/playbooks/playbookRpc.test.ts
  • apps/server/src/j5/playbooks/workspace.ts
  • apps/server/src/j5/playbooks/workspaceRoot.ts
  • apps/server/src/j5/run-observability/QueuedRunWatchdog.test.ts
  • apps/server/src/j5/run-observability/QueuedRunWatchdog.ts
  • apps/server/src/j5/run-observability/VcsFailureObservation.test.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 effective changed lines (test files excluded in mixed PRs). labels Oct 31, 2026

@Jacksondr5 Jacksondr5 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Combined review from the panel (reviewer-opus, Claude Opus 5.5; reviewer-sol, GPT Sol 6.1). We reviewed 2a632e5 separately and agree: no findings that need a change.

What we checked:

  • Fields. Every swapped site asks only for fields it uses, and the narrowed Pick<…> signatures (latestSeatNotice, queuedSeatDigest, runFailureDetail, selectCurrentProposedPlan, hasFact, inboxAnswerableApprovals) make the typecheck enforce that.
  • Filters drop only rows the caller already ignored. We checked each against its consumer:
    • deliverAgent: the usage-limit hold (usageLimitBlockedRun) reads only error items. The message checks only look at this delivery's own deliveryMessageId. latestSteerableRun, peerSteeringRun and requestedSteeringRun read only unfiltered fields (runs, provider turns, threads, sessions).
    • cancelAgent and landed: only the one message is narrowed, and all runs stay loaded, so heldRun and the run lookup are unchanged.
    • CrewSeatFinishNotifier: the Captain read's messageRoles: ["user"] matches the role === "user" checks in latestSeatNotice and queuedSeatDigest. The seat read's run and error filter matches runFailureDetail. All runs stay loaded for latestActiveRun and outstandingFinish.
    • QueuedRunWatchdog: the signal facts it writes and dedupes on are type: "error" items with runId set to that run, so the filtered read still sees them.
    • SilenceDetector.failureDetail, ArtifactRunFinalizationObserver (plans stays unfiltered) and CrewRuntimeRequestService (approval_request only, which is all inboxAnswerableApprovals reads) each match their consumer.
  • Forks. readProjection's fork recursion only fills visibleTurnItems, which getThreadRecords cannot load, and no J5 server code reads it. No swapped site sees different rows on a fork.
  • Not-found handling. OrchestratorV2.getThreadRecords wraps a store not-found exactly as getThreadProjection does, so getThreadRecordsIfPresent and workspaceRoot's isMissingThread keep their meaning.
  • Read-only connection. Every swapped read runs after a commit, outside any writer transaction: the run-finalization observers from the effect worker, and deliverAgent/cancelAgent outside the worker's sql.withTransaction blocks. The reader sees the same committed rows the writer would.
  • Spans. No Effect.fn names changed or were dropped. The new readRunFacts is a plain function, which is fine because its callers carry the spans.
  • Remaining full reads. At head, every one is in the PR's "Deferred to part 2" list: CrewLaunchReporter, CrewLaunchService, CrewProposalService, SpawnAgentService, mcp/handlers.ts, the cascade settle path, and the old helper.
  • Tests. No sleeps, polling or timeouts. The two new real-store tests cover the two places where a filter decides the rows. Moving the stale-snapshot hook in DeliveryTransport.integration to getThreadRecords is correct: without it, the race tests would pass without exercising the race. The usage-limit integration cases now exercise the filtered deliverAgent read on the real store.

One process note, not a code finding: DeliveryTransport.integration.test.ts and CrewCaptainArchiveCascade.test.ts are also changed by open PRs #758 and #756. The edits here only rename fake methods, which the source swap requires, so whichever PR merges second will need a small rebase.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Jacksondr5
Jacksondr5 merged commit 44b3e27 into j5/main Oct 31, 2026
35 checks passed
@Jacksondr5
Jacksondr5 deleted the j5/278-field-scoped-reads-part1 branch October 31, 2026 16:22

This branch was successfully deployed

1 active deployment
Preview — 633f5848 Deployed Oct 31, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 effective changed lines (test files excluded in mixed PRs). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant