Repository navigation
Description
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area
apps/server
Steps to reproduce
Copy this block into a shell in an empty folder on Linux, with GH_TOKEN set to a token that can read public repositories, and run it. It clones main at the commit below, starts a server from source with an empty data folder, links 110 open pull requests of this repository to one thread, restarts the server with a cold pull request cache, and prints what the startup sweep sent. No provider account is needed.
# Run in an empty folder on Linux with git, curl, python3, jq and vp (Vite+) installed. Uses port 14711.
# Needs GH_TOKEN set to a GitHub token that can read public repositories, for example: export GH_TOKEN="$(gh auth token)"
# The server reads 110 open pull requests of pingdotgg/t3code. One summary request can carry 25 of them,
# so its startup sweep needs about 5 requests. Count it with a cold pull request cache, as after a restart
# once cached reads have expired.
WORK=$PWD
SRC=${T3_SRC:-$WORK/t3code}
if [ ! -d "$SRC" ]; then
git clone https://github.com/pingdotgg/t3code.git "$SRC"
git -C "$SRC" checkout b68aedfdb71534bfdfbe55771ef2e686742faa8e
(cd "$SRC" && vp i)
fi
t3() { (cd "$SRC/apps/server" && vp env exec --node 26.12.1 node src/bin.ts "$@" --base-dir "$WORK/home"); }
serve() { t3 serve --host 127.0.0.1 --port 14711 --no-browser --dev-url http://127.0.0.1:9 >> "$WORK/server.log" 2>&1 & until curl -s -o /dev/null http://127.0.0.1:14711/; do sleep 1; done; }
stop() { kill "$(jq -r .pid "$WORK/home/userdata/server-runtime.json")"; while curl -s -o /dev/null http://127.0.0.1:14711/; do sleep 1; done; }
trace=$WORK/home/userdata/logs/server.trace.ndjson
# 1. A project whose origin is pingdotgg/t3code, a server, and one thread (on the free "Replay (dev)" provider).
git init -b main "$WORK/sample-app"
git -C "$WORK/sample-app" -c user.name=Sample -c user.email=sample@example.test commit --allow-empty -m "Initial commit"
git -C "$WORK/sample-app" remote add origin https://github.com/pingdotgg/t3code.git
serve
t3 project add "$WORK/sample-app"
thread=$(t3 thread start "$WORK/sample-app" "Track pull requests" --provider devReplay --model showcase)
# 2. Link 110 open pull requests of pingdotgg/t3code to that thread. The t3 CLI has no link command, so this
# small script signs in the way `t3 thread` does and calls the server's link_pull_requests tool.
urls=$(for page in 1 2; do curl -s -H "Authorization: Bearer $GH_TOKEN" "https://api.github.com/repos/pingdotgg/t3code/pulls?state=open&per_page=100&page=$page" | jq -r '.[].html_url'; done | head -110)
cat > "$SRC/apps/server/link-prs.ts" <<'TS'
// node link-prs.ts <base-dir> <thread-id> <url>...: calls link_pull_requests on the running server, signed in the way `t3 thread` does.
import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
import * as NodeServices from "@effect/platform-node/NodeServices";
import * as NetService from "@t3tools/shared/Net";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as EnvironmentAuth from "./src/auth/EnvironmentAuth.ts";
import { resolveCliAuthConfig } from "./src/cli/config.ts";
import * as ServerConfig from "./src/config.ts";
import { mcpToolSession } from "./src/mcp/AcpMcpStdioBridge.ts";
import { readLiveServerRuntimeState } from "./src/serverRuntimeState.ts";
const [baseDir, threadId, ...urls] = process.argv.slice(2);
const program = Effect.gen(function* () {
const config = yield* resolveCliAuthConfig({ baseDir: Option.some(baseDir!) }, Option.none());
return yield* Effect.gen(function* () {
const origin = Option.getOrThrow(yield* readLiveServerRuntimeState(config.serverRuntimeStatePath)).origin;
const auth = yield* EnvironmentAuth.EnvironmentAuth;
const issued = yield* auth.issueMcpClientSession({ label: "link-prs", access: "full-access", peerLink: false, client: { deviceType: "bot" } });
const session = yield* mcpToolSession({ endpoint: new URL("/mcp", origin).href, authorization: `Bearer ${issued.token}`, clientName: "link-prs" });
const result = yield* session.call("link_pull_requests", { threadId, urls });
console.log(JSON.stringify(result.structuredContent));
}).pipe(Effect.scoped, Effect.provide(EnvironmentAuth.layerRuntime.pipe(Layer.provide(ServerConfig.layer(config)))));
});
NodeRuntime.runMain(program.pipe(Effect.provide(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer, NetService.layer))));
TS
(cd "$SRC/apps/server" && vp env exec --node 26.12.1 node link-prs.ts "$WORK/home" "$thread" $urls) | jq -c '{linkedCount}'
sleep 30
# 3. Restart with a cold pull request cache: stop the server, remove only its pull request cache folder, start it again.
stop
rm -rf "$WORK/home/caches/pull-requests"
before=$(wc -l < "$trace")
serve
until tail -n +"$((before + 1))" "$trace" | jq -e 'select(.name == "PullRequestSyncReactor.sweep")' > /dev/null; do sleep 2; done
# 4. The startup sweep: pull requests it read, and the GitHub GraphQL summary requests the server sent for them.
# Every GitHub GraphQL request is one GitHubGraphQlAuth.execute span in the server's own trace file.
tail -n +"$((before + 1))" "$trace" | jq -s -c '
(map(select(.name == "PullRequestSyncReactor.sweep")) | first) as $sweep
| ($sweep.startTimeUnixNano | tonumber) as $from | ($from + $sweep.durationMs * 1000000) as $to
| def inside: (.startTimeUnixNano | tonumber) as $t | $t >= $from and $t <= $to;
{ pullRequestsRead: map(select(.name == "PullRequestSyncReactor.syncGroup" and inside)) | length,
summaryRequests: map(select(.name == "GitHubGraphQlAuth.execute" and .attributes["github.operation"] == "getPullRequestSummary" and inside)) | length,
summaryRequestsFailed: map(select(.name == "GitHubGraphQlAuth.execute" and .attributes["github.operation"] == "getPullRequestSummary" and inside and .exit._tag != "Success")) | length }'
stopExpected behavior
The startup sweep reads the 110 pull requests in about 5 GitHub GraphQL summary requests, since one request carries up to 25 pull requests.
Actual behavior
The startup sweep sends 21 to 48 summary requests for the same 109 or 110 pull requests, in three runs on the same machine. None of them failed, so this is not retries. Each request counts against the token's GraphQL budget. #18295 reported a sweep splitting into many more requests than needed and was closed as fixed on 2026-10-21; on main b68aedf I still see the startup sweep split this way.
Impact
Minor bug or occasional failure
Version or commit
main, commit b68aedf
Environment
Linux, server from source (t3 serve) on Node 26.12.1, a read-only GitHub token. No provider involved.
Logs or stack traces
{"pullRequestsRead":110,"summaryRequests":48,"summaryRequestsFailed":0}
{"pullRequestsRead":109,"summaryRequests":26,"summaryRequestsFailed":0}
{"pullRequestsRead":109,"summaryRequests":21,"summaryRequestsFailed":0}Screenshots, recordings, or supporting files
Workaround
None that I found.
A fix is in #28422.
Activity