From 1c70fa3e79cb33c7cda14185bea6b9f067dad50c Mon Sep 17 00:00:00 2001 From: Emi Griffith Date: Mon, 9 Nov 2026 08:32:07 -0800 Subject: [PATCH 1/2] fix(server): keep PR check updates when activity reads fail --- .../PullRequestWatchReactor.ts | 21 ++++++++++++------- .../src/orchestration-v2/runtimeLayer.test.ts | 18 +++++++++------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/apps/server/src/orchestration-v2/PullRequestWatchReactor.ts b/apps/server/src/orchestration-v2/PullRequestWatchReactor.ts index 5050ac3272894..f22c7764f61d9 100644 --- a/apps/server/src/orchestration-v2/PullRequestWatchReactor.ts +++ b/apps/server/src/orchestration-v2/PullRequestWatchReactor.ts @@ -488,12 +488,8 @@ export const make = Effect.gen(function* () { ], { concurrency: 2 }, ); - // Activity failures must not cancel or hide a permanent detail failure. - const read = Exit.isFailure(detailRead) - ? Exit.failCause(detailRead.cause) - : Exit.isFailure(activityRead) - ? Exit.failCause(activityRead.cause) - : Exit.succeed([detailRead.value, activityRead.value] as const); + // Comment availability must not suppress check results or a permanent detail failure. + const read = detailRead; if (Exit.isFailure(read)) { if (Cause.hasInterruptsOnly(read.cause)) return yield* Effect.failCause(read.cause); lastReads.delete(group.key); @@ -512,7 +508,18 @@ export const make = Effect.gen(function* () { return yield* Effect.failCause(read.cause); } readFailures.delete(group.key); - const [detail, activity] = read.value; + const detail = read.value; + const activity = Exit.isSuccess(activityRead) + ? activityRead.value + : Option.none(); + if (Exit.isFailure(activityRead)) { + if (Cause.hasInterruptsOnly(activityRead.cause)) + return yield* Effect.failCause(activityRead.cause); + threadTails.delete(group.key); + yield* logFailure("pull request watch activity read failed", { pullRequest: group.key })( + activityRead.cause, + ); + } for (const target of group.targets) { const life = lifeOf(target); const headSha = detail.headSha ?? life.headSha; diff --git a/apps/server/src/orchestration-v2/runtimeLayer.test.ts b/apps/server/src/orchestration-v2/runtimeLayer.test.ts index b9f5cd049eec9..3a48b3736351a 100644 --- a/apps/server/src/orchestration-v2/runtimeLayer.test.ts +++ b/apps/server/src/orchestration-v2/runtimeLayer.test.ts @@ -3903,6 +3903,7 @@ it.layer(layerTest)("OrchestrationV2LayerLive lifecycle", (it) => { const headSha = "6074dd48ff"; let checks: PullRequestDetail["checks"] = []; + let activityUnavailable = false; const host = Layer.mergeAll( NodeServices.layer, NodeCrypto.layer, @@ -3918,13 +3919,15 @@ it.layer(layerTest)("OrchestrationV2LayerLive lifecycle", (it) => { checks, })), activity: () => - Effect.succeed({ - comments: [], - commentCount: 0, - commentsTruncated: false, - reviewThreads: [], - commits: [], - }), + activityUnavailable + ? Effect.die("comments unavailable") + : Effect.succeed({ + comments: [], + commentCount: 0, + commentsTruncated: false, + reviewThreads: [], + commits: [], + }), }), ); const wakes = Effect.map( @@ -3951,6 +3954,7 @@ it.layer(layerTest)("OrchestrationV2LayerLive lifecycle", (it) => { yield* sweep; assert.lengthOf(yield* wakes, 1); + activityUnavailable = true; checks = [{ name: "lint", status: "success", description: null, url: null, required: true }]; yield* TestClock.adjust("10 minutes"); yield* sweep; From bb28fd36d7523e2bc8146cce0d42a6d0312fb5fb Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 14 Nov 2026 16:20:41 +0000 Subject: [PATCH 2/2] refactor(server): fold PR watch activity-read failure handling into one block Drop the read alias, rely on logFailure for interrupts, and skip the warning when the comments read was rate limited, as the detail read does. Co-Authored-By: Claude Opus 5.5 --- .../PullRequestWatchReactor.ts | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/apps/server/src/orchestration-v2/PullRequestWatchReactor.ts b/apps/server/src/orchestration-v2/PullRequestWatchReactor.ts index 3155747e4724f..46ea6a2f3fcad 100644 --- a/apps/server/src/orchestration-v2/PullRequestWatchReactor.ts +++ b/apps/server/src/orchestration-v2/PullRequestWatchReactor.ts @@ -609,30 +609,26 @@ export const make = Effect.gen(function* () { ], { concurrency: 2 }, ); - // Comment availability must not suppress check results or a permanent detail failure. - const read = detailRead; - if (Exit.isFailure(read)) { - if (Cause.hasInterruptsOnly(read.cause)) return yield* Effect.failCause(read.cause); + if (Exit.isFailure(detailRead)) { + if (Cause.hasInterruptsOnly(detailRead.cause)) + return yield* Effect.failCause(detailRead.cause); lastReads.delete(group.key); - const reason = providerFailureReason(read.cause); + const reason = providerFailureReason(detailRead.cause); if (reason === "rate-limited") return; // Transport and host failures can outlast the failure limit; keep the watch for recovery. if (reason === "failed") { readFailures.delete(group.key); - return yield* Effect.failCause(read.cause); + return yield* Effect.failCause(detailRead.cause); } const failures = (readFailures.get(group.key) ?? 0) + 1; readFailures.set(group.key, failures); if (failures >= READ_FAILURE_LIMIT && (yield* eachTarget(group, giveUp))) { readFailures.delete(group.key); } - return yield* Effect.failCause(read.cause); + return yield* Effect.failCause(detailRead.cause); } readFailures.delete(group.key); - const detail = read.value; - const activity = Exit.isSuccess(activityRead) - ? activityRead.value - : Option.none(); + const detail = detailRead.value; for (const target of group.targets) { const life = lifeOf(target); const headSha = detail.headSha ?? life.headSha; @@ -655,14 +651,19 @@ export const make = Effect.gen(function* () { ); } + // A failed comments read leaves remarks incomplete, so the next pass reads them again; + // checks, head and conflicts from the detail still reach the evaluator. if (Exit.isFailure(activityRead)) { - if (Cause.hasInterruptsOnly(activityRead.cause)) - return yield* Effect.failCause(activityRead.cause); threadTails.delete(group.key); - yield* logFailure("pull request watch activity read failed", { pullRequest: group.key })( - activityRead.cause, - ); + if (providerFailureReason(activityRead.cause) !== "rate-limited") { + yield* logFailure("pull request watch activity read failed", { pullRequest: group.key })( + activityRead.cause, + ); + } } + const activity = Exit.isSuccess(activityRead) + ? activityRead.value + : Option.none(); // Never advance the remark watermark past comments an incomplete read could have missed. const remarks = Option.isSome(activity)