Skip to content

feat(server): accept pull-request state from an external syncer - #217

Merged
tusharbhardwaj-bk merged 2 commits into
expbkmainfrom
t3code/perf-9-pull-request-state
Sep 26, 2026
Merged

tusharbhardwaj-bk merged 2 commits into
expbkmainfrom
t3code/perf-9-pull-request-state

Conversation

@tusharbhardwaj-bk

@tusharbhardwaj-bk tusharbhardwaj-bk commented Sep 26, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #216 (event feed): this branch includes #216's commit until that merges. It reuses #216's forkRouteAuth.expbkt3.ts. I will re-sync the branch onto expbkmain before merging.

Problem

ThreadPullRequestReactor and PullRequestSyncReactor (pure upstream code) sweep every unsettled branch thread each minute with git and gh.

  • On bkt3 that is ~113 threads and ~500 processes per burst.
  • /api/health goes from 2 ms to 400–820 ms for 40–60 s out of every ~110 s.
  • About 2,800 "thread branch pull request lookup failed" rate-limit warnings a day.

The approved plan moves PR state to the Linear bridge, which already receives GitHub webhooks. bkt3 needs a way to accept that state, and a way to stop its own sweep.

Fix

This implements sections 2 and 3 of the bridge API contract.

POST /api/orchestration/pull-request-state

New fork module orchestration/pullRequestStateHttp.expbkt3.ts. Body: { deliveryId, host, repository, number, url, snapshot: ThreadPullRequestSnapshot }. bkt3 makes no outside calls.

Which threads a write applies to:

  • (a) Threads linked to the PR, from projection_thread_pull_requests. Their snapshot is replaced through thread.pull-request-link.sync, and the stored stack is kept.
  • (b) Unarchived threads on snapshot.headBranch whose project's repository identity (the cached RepositoryIdentityResolver, via getProjectShellById) is this PR's repository. Their branch PR is set through thread.pull-request.sync, with the same expected guard upstream's branch discovery uses.

When a write is ignored:

  • stale: snapshot.updatedAt is older than the stored one, or a closed/merged PR would replace a different branch PR.
  • unchanged: every field except syncedAt is equal, using the same comparison as PullRequestSyncReactor.
  • Replaying a delivery is a no-op. Command ids are bridge:pr-state:<deliveryId>:<threadId>:link|branch.

Response and errors:

  • 200 { applied: [threadId], ignored: [{ threadId, reason }] }. No match returns applied: [], never a 404.
  • 400 { error: "invalid-request" } for a body that does not decode.
  • 403 without the scope.

New scope external-sync:write

  • Marked and additive in packages/contracts/src/auth.ts. It is not in the standard or administrative sets.
  • New marked, repeatable flag t3 auth session issue --with-scope external-sync:write, needed because scopes are fixed when a token is issued. The exact command is in docs/operations/external-pr-sync.md.

T3_EXTERNAL_PR_SYNC=1

  • Two marked seams in Layers/OrchestrationReactor.ts skip starting both reactors, logging pull request reactor not started: external sync owns PR state.
  • Unset keeps upstream behaviour.
  • Not set in deploy/bkt3/start.sh: that flip is a later, human-gated step.

Not reproduced, by design

  • Stack discovery (thread.pull-request.link with source stack) needs host reads the webhook does not carry.
  • Upstream's replacement of a terminal legacy linkedPullRequest needs the old PR's state.

Follow-up, outside this PR

ThreadSettlementReactor does its own pullRequests.summary and git.branchPullRequest reads rather than reading the stored snapshot. It keeps working with the switch on, but it still calls GitHub.

Evidence

pullRequestStateHttp.expbkt3.test.ts, 6 tests, against a real in-memory engine and temporary git repos with GitHub remotes:

  • a linked thread gets the snapshot;
  • a replay is unchanged;
  • an older updatedAt is stale, and the title is kept;
  • a newer merged write applies;
  • the branch PR is set only for the same repository (another repo on the same branch, and another branch in the same repo, are untouched);
  • a re-send is unchanged;
  • an old closed PR on the same branch is stale;
  • no match returns empty;
  • over HTTP: 200, 403 for an admin token without the scope, 400 twice;
  • switch parsing, and the start being skipped only while the switch is on.

Mutation checks: disabling the branch path fails the branch test, and disabling the stale guard fails the linked test.

CLI checked against a scratch base dir. --with-scope external-sync:write issues a session with the 8 administrative scopes plus external-sync:write, and --with-scope bogus is rejected.

Also run:

  • vp run typecheck in apps/server and packages/contracts: clean;
  • OrchestrationReactor.test.ts: passes;
  • lint: clean;
  • fork-marker check: passes.

Model/harness: Claude Opus 5.5 via Claude Code in T3 Code.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

bk-agent-01 and others added 2 commits September 26, 2026 08:37
GET /api/orchestration/events?after=<seq>&limit=<1..1000>&wait=<0..25>
returns events after a cursor in sequence order, shaped like the WebSocket
replay. An empty read is held until the engine publishes a newer event or
the wait elapses (woken by the live event stream, no polling). A cursor
ahead of the log gets 410 cursor-invalid.

This lets the Linear bridge stop polling the full shell every 2 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
POST /api/orchestration/pull-request-state lets the Linear bridge push PR
state built from GitHub webhooks. bkt3 makes no outside calls: it refreshes
the snapshot of threads linked to the PR and sets the branch PR of
unarchived threads on its head branch in the same repository, with the same
commands the upstream PR reactors dispatch. Older or identical writes are
ignored; command ids derive from the delivery id.

The endpoint needs the new external-sync:write scope, granted with
`t3 auth session issue --with-scope external-sync:write`.

T3_EXTERNAL_PR_SYNC=1 stops ThreadPullRequestReactor and
PullRequestSyncReactor from starting. It is not set anywhere yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL labels Sep 26, 2026
@github-actions

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ℹ️ No successful main baseline artifact is available yet. This run establishes the initial measurement.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire — 13.8 KiB — 15.1 KiB ✅
Codex Thread snapshot wire — 7.3 KiB — 7.8 KiB ✅
Codex Live turn WebSocket wire — 6.5 KiB — 7.8 KiB ✅
Codex Live turn WebSocket decoded — 57.0 KiB — 66.4 KiB ✅
Codex Live turn messages — 8 — 21 ✅
Claude Total thread wire — 13.8 KiB — 15.1 KiB ✅
Claude Thread snapshot wire — 7.3 KiB — 7.8 KiB ✅
Claude Live turn WebSocket wire — 6.5 KiB — 7.8 KiB ✅
Claude Live turn WebSocket decoded — 57.9 KiB — 66.4 KiB ✅
Claude Live turn messages — 8 — 21 ✅

Baseline: unavailable · PR result: 7acc5dc · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 114.9 KiB
  • Claude decoded thread snapshot: 115.6 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

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

Labels

size:XL 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.

2 participants