Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions apps/server/src/orchestration-v2/PullRequestWatchReactor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,19 +329,29 @@ 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 only if it is still open.`,
const giveUp = (target: WatchTarget, cause: Cause.Cause<PullRequestService.PullRequestError>) => {
const error = Option.getOrUndefined(Cause.findErrorOption(cause));
// The message is the host's own setup instruction, never the subprocess output in `cause`.
const unusable =
error?._tag === "PullRequestUnavailableError" && error.reason !== "provider-unsupported"
? error.message
: null;
return record(target, null, {
text:
unusable === null
? `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.`
: `T3 Code stopped watching pull request #${target.link.number} (${target.link.url}) because the T3 Code server cannot use the host: ${unusable} Fix that on the server, then call watch_pull_request only if it is still open.`,
notification: {
source: { kind: "monitor" },
outcome: "failed",
summary: `#${target.link.number}: stopped watching, could not read it`,
summary: `#${target.link.number}: stopped watching, ${unusable === null ? "could not read it" : "host not set up on the server"}`,
},
}).pipe(
Effect.tap(() => woke(target)),
Effect.catch(() => record(target, null)),
Effect.tap(() => ended(target, "unreadable")),
);
};

const readRemarks = Effect.fn("PullRequestWatchReactor.readRemarks")(
function* (
Expand Down Expand Up @@ -630,7 +640,10 @@ export const make = Effect.gen(function* () {
}
const failures = (readFailures.get(group.key) ?? 0) + 1;
readFailures.set(group.key, failures);
if (failures >= READ_FAILURE_LIMIT && (yield* eachTarget(group, giveUp))) {
if (
failures >= READ_FAILURE_LIMIT &&
(yield* eachTarget(group, (target) => giveUp(target, read.cause)))
) {
readFailures.delete(group.key);
}
return yield* Effect.failCause(read.cause);
Expand Down
47 changes: 34 additions & 13 deletions apps/server/src/orchestration-v2/runtimeLayer.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { PullRequestProviderError } from "@t3tools/source-control-core/server/PullRequestProvider";
import { SourceControlProviderKind } from "@t3tools/contracts";
import { PullRequestUnavailableError, SourceControlProviderKind } from "@t3tools/contracts";
import { limitRecoveryCommand } from "./UsageLimitRecoveryWorker.ts";
import { createOrchestrationV2TurnItemVisibility } from "@t3tools/shared/orchestrationV2Timeline";
import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";
Expand Down Expand Up @@ -3948,6 +3948,7 @@ it.layer(layerTest)("OrchestrationV2LayerLive lifecycle", (it) => {
{ reason: "failed", activityFails: true },
{ reason: "not-found", activityFails: true },
{ reason: "forbidden", activityFails: true },
{ reason: "cli-unauthenticated", activityFails: false },
] as const)(
"handles repeated host read failures: $reason (activity fails: $activityFails)",
({ reason, activityFails }) =>
Expand Down Expand Up @@ -3992,17 +3993,24 @@ it.layer(layerTest)("OrchestrationV2LayerLive lifecycle", (it) => {
let passed = false;
let genericFailure = false;
const activityFailures = yield* Queue.unbounded<void>();
const readFailure = (operation: string, failure: "failed" | "not-found" | "forbidden") =>
new PullRequestOperationError({
operation,
detail: "host read failed",
cause: new PullRequestProviderError({
provider: SourceControlProviderKind.make("github"),
operation,
reason: failure,
detail: "host read failed",
}),
});
const readFailure = (operation: string, failure: typeof reason) =>
failure === "cli-unauthenticated"
? new PullRequestUnavailableError({
reason: failure,
provider: SourceControlProviderKind.make("github"),
requirement: "Run `gh auth login`.",
cause: new Error("private subprocess output"),
})
: new PullRequestOperationError({
operation,
detail: "host read failed",
cause: new PullRequestProviderError({
provider: SourceControlProviderKind.make("github"),
operation,
reason: failure,
detail: "host read failed",
}),
});
const reactor = yield* PullRequestWatchReactor.make.pipe(
Effect.provide(
Layer.mergeAll(
Expand Down Expand Up @@ -4068,7 +4076,20 @@ it.layer(layerTest)("OrchestrationV2LayerLive lifecycle", (it) => {
const thread = yield* orchestrator.getThreadShell(threadId);
if (reason !== "failed") {
assert.isUndefined(thread?.pullRequests?.[0]?.watch);
assert.deepEqual(yield* summaries, ["#8: stopped watching, could not read it"]);
const { messages } = yield* orchestrator.getThreadRecords(threadId, ["messages"]);
const stopped = messages.filter((message) => message.notification !== undefined);
assert.deepEqual(
stopped.map((message) => message.notification?.summary),
[
reason === "cli-unauthenticated"
? "#8: stopped watching, host not set up on the server"
: "#8: stopped watching, could not read it",
],
);
if (reason === "cli-unauthenticated") {
assert.include(stopped[0]?.text, "Run `gh auth login`.");
assert.notInclude(stopped[0]?.text, "private subprocess output");
}
return;
}
assert.isDefined(thread?.pullRequests?.[0]?.watch);
Expand Down
Loading