From cc8e7bf6241ee2399776a1e437070b797f0eed17 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 31 Oct 2026 02:19:56 +0000 Subject: [PATCH 1/3] test(server): reproduce #27945, closed PR watch with unreadable activity A closed pull request whose activity read fails ends with "could not read it" instead of "closed, stopped watching". Co-Authored-By: Claude Opus 5.5 --- .../src/orchestration-v2/runtimeLayer.test.ts | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/apps/server/src/orchestration-v2/runtimeLayer.test.ts b/apps/server/src/orchestration-v2/runtimeLayer.test.ts index a04fe4c7c137..0b24c33f0ec5 100644 --- a/apps/server/src/orchestration-v2/runtimeLayer.test.ts +++ b/apps/server/src/orchestration-v2/runtimeLayer.test.ts @@ -2983,6 +2983,73 @@ it.layer(layerTest)("OrchestrationV2LayerLive lifecycle", (it) => { }), ); + it.effect("ends a watch on a closed pull request whose activity cannot be read", () => + Effect.gen(function* () { + const orchestrator = yield* Orchestrator.OrchestratorV2; + const threadId = ThreadId.make("runtime-pull-request-watch-closed-unreadable"); + const projectId = ProjectId.make("pr-watch-closed-unreadable-project"); + yield* seedProject({ + projectId, + title: "Watch closed unreadable", + workspaceRoot: "/workspace/watch-closed-unreadable", + defaultModelSelection: null, + createdAt: "2026-10-01T00:00:00.000Z", + }); + yield* orchestrator.dispatch({ + type: "thread.create", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("pr-watch-closed-unreadable-create"), + threadId, + projectId, + title: "Watch closed unreadable", + modelSelection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + }); + yield* orchestrator.dispatch({ + type: "thread.pull-request.watch", + commandId: CommandId.make("pr-watch-closed-unreadable-start"), + threadId, + host: "github.com", + repository: "pingdotgg/t3code", + number: 8, + watching: true, + link: { url: "https://github.com/pingdotgg/t3code/pull/8", source: "agent" }, + }); + const reactor = yield* PullRequestWatchReactor.make.pipe( + Effect.provide( + Layer.mergeAll( + NodeServices.layer, + Layer.mock(PullRequestService.PullRequestService)({ + detail: () => + Effect.succeed({ + ...watchedPullRequestDetail({ + projectId, + number: 8, + at: "2026-10-02T12:00:00.000Z", + }), + state: "closed" as const, + }), + activity: () => Effect.die("host unreachable"), + }), + ), + ), + ); + for (let pass = 0; pass < 15; pass += 1) yield* reactor.sweep; + + const thread = yield* orchestrator.getThreadShell(threadId); + assert.isUndefined(thread?.pullRequests?.[0]?.watch); + const { messages } = yield* orchestrator.getThreadRecords(threadId, ["messages"]); + assert.deepEqual( + messages.flatMap((message) => message.notification?.summary ?? []), + ["#8: closed, stopped watching"], + ); + }), + ); + it.effect("wakes once for a state it reported, across a restart and a new watch", () => Effect.gen(function* () { const orchestrator = yield* Orchestrator.OrchestratorV2; From c9e32fb8760a937c9116d5f8464cb2ff777426b2 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 31 Oct 2026 02:24:36 +0000 Subject: [PATCH 2/3] fix(server): closed PR watches end even when activity is unreadable A pull request watch reads detail and activity together. When activity failed, the whole read counted as a failure even if detail said the pull request was closed or merged, so the watch never ended and eventually gave up, telling the agent to re-arm it. Activity is now read into its own exit, and is only consulted when the pull request is still open. The give-up message now asks the agent to watch again only if the pull request is still open. Fixes pingdotgg/t3code#27945 Co-Authored-By: Claude Opus 5.5 --- .../orchestration-v2/PullRequestWatchReactor.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/apps/server/src/orchestration-v2/PullRequestWatchReactor.ts b/apps/server/src/orchestration-v2/PullRequestWatchReactor.ts index 03a16e770efa..d0bc1b4bc69f 100644 --- a/apps/server/src/orchestration-v2/PullRequestWatchReactor.ts +++ b/apps/server/src/orchestration-v2/PullRequestWatchReactor.ts @@ -280,7 +280,7 @@ export const make = Effect.gen(function* () { const giveUp = (target: WatchTarget) => record(target, null, { - text: `T3 Code stopped watching pull request #${target.link.number} (${target.link.url}) because it failed to read it from the host ${READ_FAILURE_LIMIT} times in a row. Check it yourself, and call watch_pull_request to watch it again.`, + text: `T3 Code stopped watching pull request #${target.link.number} (${target.link.url}) because it failed to read it from the host ${READ_FAILURE_LIMIT} times in a row. Check it yourself, and call watch_pull_request only if it is still open.`, notification: { source: { kind: "monitor" }, outcome: "failed", @@ -463,11 +463,20 @@ export const make = Effect.gen(function* () { Effect.all( [ pullRequests.detail({ ...reference, allowStale: false }), - plan.activity - ? pullRequests.activity(reference).pipe(Effect.asSome) - : Effect.succeed(Option.none()), + Effect.exit( + plan.activity + ? pullRequests.activity(reference).pipe(Effect.asSome) + : Effect.succeed(Option.none()), + ), ], { concurrency: 2 }, + ).pipe( + // Activity is irrelevant once the pull request is closed or merged, so it cannot block ending. + Effect.flatMap(([detail, activity]) => + detail.state === "open" + ? Effect.map(activity, (activity) => [detail, activity] as const) + : Effect.succeed([detail, Option.none()] as const), + ), ), ); if (Exit.isFailure(read)) { From 0c59b10711ddd202f1525fea7dc40d2da14cae3d Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 31 Oct 2026 02:35:07 +0000 Subject: [PATCH 3/3] test(server): closed PR watch with unreadable activity ends on the first sweep Co-Authored-By: Claude Opus 5.5 --- apps/server/src/orchestration-v2/runtimeLayer.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/orchestration-v2/runtimeLayer.test.ts b/apps/server/src/orchestration-v2/runtimeLayer.test.ts index 0b24c33f0ec5..d5675555c141 100644 --- a/apps/server/src/orchestration-v2/runtimeLayer.test.ts +++ b/apps/server/src/orchestration-v2/runtimeLayer.test.ts @@ -3038,7 +3038,7 @@ it.layer(layerTest)("OrchestrationV2LayerLive lifecycle", (it) => { ), ), ); - for (let pass = 0; pass < 15; pass += 1) yield* reactor.sweep; + yield* reactor.sweep; const thread = yield* orchestrator.getThreadShell(threadId); assert.isUndefined(thread?.pullRequests?.[0]?.watch);