authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 13:16:32-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 13:16:32-07:00
logc637dfc454342c156dbe61ee8513f5e0ac0c4691
treee0b1f3a079ddccfd1edd2352b42f6728855fcae4
parent6e9b43cb84c1b93c348d76880a0bf0083f395e47
signature Signed by PGP key B5690EEEBB952194

[codex] enrich Git workflow errors (#3241)

Co-authored-by: codex <codex@users.noreply.github.com>

7 files changed, 359 insertions(+), 131 deletions(-)

apps/server/src/git/GitManager.test.ts+8-6
......@@ -1589,16 +1589,18 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
15891589 yield* initRepo(repoDir);
15901590
15911591 const { manager } = yield* makeManager();
1592 const errorMessage = yield* runStackedAction(manager, {
1592 const error = yield* runStackedAction(manager, {
15931593 cwd: repoDir,
15941594 action: "commit",
15951595 featureBranch: true,
1596 }).pipe(
1597 Effect.flip,
1598 Effect.map((error) => error.message),
1599 );
1596 }).pipe(Effect.flip);
16001597
1601 expect(errorMessage).toContain("no changes to commit");
1598 expect(error).toMatchObject({
1599 _tag: "GitManagerError",
1600 operation: "runFeatureBranchStep",
1601 cwd: repoDir,
1602 });
1603 expect(error.message).toContain("no changes to commit");
16021604 }),
16031605 );
16041606
apps/server/src/git/GitManager.ts+86-57
......@@ -320,14 +320,6 @@ function toPullRequestInfo(summary: ChangeRequest): PullRequestInfo {
320320 };
321321}
322322
323function gitManagerError(operation: string, detail: string, cause?: unknown): GitManagerError {
324 return new GitManagerError({
325 operation,
326 detail,
327 ...(cause !== undefined ? { cause } : {}),
328 });
329}
330
331323function limitContext(value: string, maxChars: number): string {
332324 if (value.length <= maxChars) return value;
333325 return `${value.slice(0, maxChars)}\n\n[truncated]`;
......@@ -535,17 +527,27 @@ export const make = Effect.gen(function* () {
535527
536528 const sourceControlProvider = (cwd: string) => sourceControlProviders.resolve({ cwd });
537529 const serverSettingsService = yield* ServerSettings.ServerSettingsService;
538 const randomUUIDv4 = crypto.randomUUIDv4.pipe(
539 Effect.mapError((cause) =>
540 gitManagerError("randomUUIDv4", "Failed to generate Git operation identifier.", cause),
541 ),
542 );
530 const randomUUIDv4 = (cwd: string) =>
531 crypto.randomUUIDv4.pipe(
532 Effect.mapError(
533 (cause) =>
534 new GitManagerError({
535 operation: "randomUUIDv4",
536 cwd,
537 detail: "Failed to generate Git operation identifier.",
538 cause,
539 }),
540 ),
541 );
543542
544543 const createProgressEmitter = (
545544 input: { cwd: string; action: GitStackedAction },
546545 options?: GitRunStackedActionOptions,
547546 ) =>
548 (options?.actionId === undefined ? randomUUIDv4 : Effect.succeed(options.actionId)).pipe(
547 (options?.actionId === undefined
548 ? randomUUIDv4(input.cwd)
549 : Effect.succeed(options.actionId)
550 ).pipe(
549551 Effect.map((actionId) => {
550552 const reporter = options?.progressReporter;
551553 const emit = (event: GitActionProgressPayload) =>
......@@ -1284,16 +1286,18 @@ export const make = Effect.gen(function* () {
12841286 const details = yield* gitCore.statusDetails(cwd);
12851287 const branch = details.branch ?? fallbackBranch;
12861288 if (!branch) {
1287 return yield* gitManagerError(
1288 "runPrStep",
1289 "Cannot create a pull request from detached HEAD.",
1290 );
1289 return yield* new GitManagerError({
1290 operation: "runPrStep",
1291 cwd,
1292 detail: "Cannot create a pull request from detached HEAD.",
1293 });
12911294 }
12921295 if (!details.hasUpstream) {
1293 return yield* gitManagerError(
1294 "runPrStep",
1295 "Current branch has not been pushed. Push before creating a PR.",
1296 );
1296 return yield* new GitManagerError({
1297 operation: "runPrStep",
1298 cwd,
1299 detail: "Current branch has not been pushed. Push before creating a PR.",
1300 });
12971301 }
12981302
12991303 const headContext = yield* resolveBranchHeadContext(cwd, {
......@@ -1332,14 +1336,21 @@ export const make = Effect.gen(function* () {
13321336 modelSelection,
13331337 });
13341338
1335 const bodyFile = path.join(tempDir, `t3code-pr-body-${process.pid}-${yield* randomUUIDv4}.md`);
1336 yield* fileSystem
1337 .writeFileString(bodyFile, generated.body)
1338 .pipe(
1339 Effect.mapError((cause) =>
1340 gitManagerError("runPrStep", "Failed to write pull request body temp file.", cause),
1341 ),
1342 );
1339 const bodyFile = path.join(
1340 tempDir,
1341 `t3code-pr-body-${process.pid}-${yield* randomUUIDv4(cwd)}.md`,
1342 );
1343 yield* fileSystem.writeFileString(bodyFile, generated.body).pipe(
1344 Effect.mapError(
1345 (cause) =>
1346 new GitManagerError({
1347 operation: "runPrStep",
1348 cwd,
1349 detail: "Failed to write pull request body temp file.",
1350 cause,
1351 }),
1352 ),
1353 );
13431354 yield* emit({
13441355 kind: "phase_started",
13451356 phase: "pr",
......@@ -1541,10 +1552,12 @@ export const make = Effect.gen(function* () {
15411552 };
15421553 }
15431554 if (existingBranchBeforeFetchPath === rootWorktreePath) {
1544 return yield* gitManagerError(
1545 "preparePullRequestThread",
1546 "This PR branch is already checked out in the main repo. Use Local, or switch the main repo off that branch before creating a worktree thread.",
1547 );
1555 return yield* new GitManagerError({
1556 operation: "preparePullRequestThread",
1557 cwd: input.cwd,
1558 detail:
1559 "This PR branch is already checked out in the main repo. Use Local, or switch the main repo off that branch before creating a worktree thread.",
1560 });
15481561 }
15491562
15501563 yield* materializePullRequestHeadBranch(
......@@ -1569,10 +1582,12 @@ export const make = Effect.gen(function* () {
15691582 };
15701583 }
15711584 if (existingBranchAfterFetchPath === rootWorktreePath) {
1572 return yield* gitManagerError(
1573 "preparePullRequestThread",
1574 "This PR branch is already checked out in the main repo. Use Local, or switch the main repo off that branch before creating a worktree thread.",
1575 );
1585 return yield* new GitManagerError({
1586 operation: "preparePullRequestThread",
1587 cwd: input.cwd,
1588 detail:
1589 "This PR branch is already checked out in the main repo. Use Local, or switch the main repo off that branch before creating a worktree thread.",
1590 });
15761591 }
15771592
15781593 const worktree = yield* gitCore.createWorktree({
......@@ -1607,10 +1622,11 @@ export const make = Effect.gen(function* () {
16071622 modelSelection,
16081623 });
16091624 if (!suggestion) {
1610 return yield* gitManagerError(
1611 "runFeatureBranchStep",
1612 "Cannot create a feature branch because there are no changes to commit.",
1613 );
1625 return yield* new GitManagerError({
1626 operation: "runFeatureBranchStep",
1627 cwd,
1628 detail: "Cannot create a feature branch because there are no changes to commit.",
1629 });
16141630 }
16151631
16161632 const preferredBranch = suggestion.branch ?? sanitizeFeatureBranchName(suggestion.subject);
......@@ -1647,16 +1663,18 @@ export const make = Effect.gen(function* () {
16471663 const wantsPr = input.action === "create_pr" || input.action === "commit_push_pr";
16481664
16491665 if (input.featureBranch && !wantsCommit) {
1650 return yield* gitManagerError(
1651 "runStackedAction",
1652 "Feature-branch checkout is only supported for commit actions.",
1653 );
1666 return yield* new GitManagerError({
1667 operation: "runStackedAction",
1668 cwd: input.cwd,
1669 detail: "Feature-branch checkout is only supported for commit actions.",
1670 });
16541671 }
16551672 if (input.action === "create_pr" && initialStatus.hasWorkingTreeChanges) {
1656 return yield* gitManagerError(
1657 "runStackedAction",
1658 "Commit local changes before creating a PR.",
1659 );
1673 return yield* new GitManagerError({
1674 operation: "runStackedAction",
1675 cwd: input.cwd,
1676 detail: "Commit local changes before creating a PR.",
1677 });
16601678 }
16611679
16621680 const phases: GitActionProgressPhase[] = [
......@@ -1672,13 +1690,18 @@ export const make = Effect.gen(function* () {
16721690 });
16731691
16741692 if (!input.featureBranch && wantsPush && !initialStatus.branch) {
1675 return yield* gitManagerError("runStackedAction", "Cannot push from detached HEAD.");
1693 return yield* new GitManagerError({
1694 operation: "runStackedAction",
1695 cwd: input.cwd,
1696 detail: "Cannot push from detached HEAD.",
1697 });
16761698 }
16771699 if (!input.featureBranch && wantsPr && !initialStatus.branch) {
1678 return yield* gitManagerError(
1679 "runStackedAction",
1680 "Cannot create a pull request from detached HEAD.",
1681 );
1700 return yield* new GitManagerError({
1701 operation: "runStackedAction",
1702 cwd: input.cwd,
1703 detail: "Cannot create a pull request from detached HEAD.",
1704 });
16821705 }
16831706
16841707 let branchStep: { status: "created" | "skipped_not_requested"; name?: string };
......@@ -1687,8 +1710,14 @@ export const make = Effect.gen(function* () {
16871710
16881711 const modelSelection = yield* serverSettingsService.getSettings.pipe(
16891712 Effect.map((settings) => settings.textGenerationModelSelection),
1690 Effect.mapError((cause) =>
1691 gitManagerError("runStackedAction", "Failed to get server settings.", cause),
1713 Effect.mapError(
1714 (cause) =>
1715 new GitManagerError({
1716 operation: "runStackedAction",
1717 cwd: input.cwd,
1718 detail: "Failed to get server settings.",
1719 cause,
1720 }),
16921721 ),
16931722 );
16941723
apps/server/src/git/GitWorkflowService.test.ts+58-1
......@@ -1,7 +1,9 @@
1import { assert, describe, it, vi } from "@effect/vitest";
1import { assert, describe, expect, it, vi } from "@effect/vitest";
22import * as Effect from "effect/Effect";
33import * as Layer from "effect/Layer";
44
5import { VcsRepositoryDetectionError } from "@t3tools/contracts";
6
57import * as GitManager from "./GitManager.ts";
68import * as GitWorkflowService from "./GitWorkflowService.ts";
79import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";
......@@ -132,4 +134,59 @@ describe("GitWorkflowService", () => {
132134 ),
133135 ),
134136 );
137
138 it.effect("structures workflow detection failures without exposing upstream details", () => {
139 const cause = new VcsRepositoryDetectionError({
140 operation: "VcsDriverRegistry.detect",
141 cwd: "/repo",
142 detail: "upstream detail must stay in the cause chain",
143 });
144
145 return Effect.gen(function* () {
146 const workflow = yield* GitWorkflowService.GitWorkflowService;
147 const error = yield* workflow.status({ cwd: "/repo" }).pipe(Effect.flip);
148
149 expect(error).toMatchObject({
150 _tag: "GitManagerError",
151 operation: "GitWorkflowService.status",
152 cwd: "/repo",
153 detail: "Failed to detect a VCS repository for this Git workflow.",
154 });
155 expect(error.message).not.toContain(cause.detail);
156 }).pipe(
157 Effect.provide(
158 makeLayer({
159 detect: () => Effect.fail(cause),
160 }),
161 ),
162 );
163 });
164
165 it.effect("structures command detection failures without exposing upstream details", () => {
166 const cause = new VcsRepositoryDetectionError({
167 operation: "VcsDriverRegistry.detect",
168 cwd: "/repo",
169 detail: "upstream command detail must stay in the cause chain",
170 });
171
172 return Effect.gen(function* () {
173 const workflow = yield* GitWorkflowService.GitWorkflowService;
174 const error = yield* workflow.listRefs({ cwd: "/repo" }).pipe(Effect.flip);
175
176 expect(error).toMatchObject({
177 _tag: "GitCommandError",
178 operation: "GitWorkflowService.listRefs",
179 command: "vcs-route",
180 cwd: "/repo",
181 detail: "Failed to detect a VCS repository for this Git command.",
182 });
183 expect(error.message).not.toContain(cause.detail);
184 }).pipe(
185 Effect.provide(
186 makeLayer({
187 detect: () => Effect.fail(cause),
188 }),
189 ),
190 );
191 });
135192});
apps/server/src/git/GitWorkflowService.ts+52-62
......@@ -94,20 +94,6 @@ export class GitWorkflowService extends Context.Service<
9494 }
9595>()("t3/git/GitWorkflowService") {}
9696
97const unsupportedGitWorkflow = (operation: string, cwd: string, detail: string) =>
98 new GitManagerError({
99 operation,
100 detail: `${detail} (${cwd})`,
101 });
102
103const unsupportedGitCommand = (operation: string, cwd: string, detail: string) =>
104 new GitCommandError({
105 operation,
106 command: "vcs-route",
107 cwd,
108 detail,
109 });
110
11197function nonRepositoryLocalStatus(): VcsStatusLocalResult {
11298 return {
11399 isRepo: false,
......@@ -153,23 +139,23 @@ export const make = Effect.gen(function* () {
153139 operation: string,
154140 cwd: string,
155141 ) {
156 const handle = yield* registry
157 .resolve({ cwd })
158 .pipe(
159 Effect.mapError((error) =>
160 unsupportedGitWorkflow(
142 const handle = yield* registry.resolve({ cwd }).pipe(
143 Effect.mapError(
144 (cause) =>
145 new GitManagerError({
161146 operation,
162147 cwd,
163 error instanceof Error ? error.message : String(error),
164 ),
165 ),
166 );
148 detail: "Failed to resolve the VCS driver for this Git workflow.",
149 cause,
150 }),
151 ),
152 );
167153 if (handle.kind !== "git") {
168 return yield* unsupportedGitWorkflow(
154 return yield* new GitManagerError({
169155 operation,
170156 cwd,
171 `The ${operation} workflow currently supports Git repositories only; detected ${handle.kind}.`,
172 );
157 detail: `The ${operation} workflow currently supports Git repositories only; detected ${handle.kind}. (${cwd})`,
158 });
173159 }
174160 });
175161
......@@ -177,48 +163,50 @@ export const make = Effect.gen(function* () {
177163 operation: string,
178164 cwd: string,
179165 ) {
180 const handle = yield* registry
181 .resolve({ cwd })
182 .pipe(
183 Effect.mapError((error) =>
184 unsupportedGitCommand(
166 const handle = yield* registry.resolve({ cwd }).pipe(
167 Effect.mapError(
168 (cause) =>
169 new GitCommandError({
185170 operation,
171 command: "vcs-route",
186172 cwd,
187 error instanceof Error ? error.message : String(error),
188 ),
189 ),
190 );
173 detail: "Failed to resolve the VCS driver for this Git command.",
174 cause,
175 }),
176 ),
177 );
191178 if (handle.kind !== "git") {
192 return yield* unsupportedGitCommand(
179 return yield* new GitCommandError({
193180 operation,
181 command: "vcs-route",
194182 cwd,
195 `The ${operation} command currently supports Git repositories only; detected ${handle.kind}.`,
196 );
183 detail: `The ${operation} command currently supports Git repositories only; detected ${handle.kind}.`,
184 });
197185 }
198186 });
199187
200188 const detectGitRepositoryForStatus = Effect.fn("GitWorkflowService.detectGitRepositoryForStatus")(
201189 function* (operation: string, cwd: string) {
202 const handle = yield* registry
203 .detect({ cwd })
204 .pipe(
205 Effect.mapError((error) =>
206 unsupportedGitWorkflow(
190 const handle = yield* registry.detect({ cwd }).pipe(
191 Effect.mapError(
192 (cause) =>
193 new GitManagerError({
207194 operation,
208195 cwd,
209 error instanceof Error ? error.message : String(error),
210 ),
211 ),
212 );
196 detail: "Failed to detect a VCS repository for this Git workflow.",
197 cause,
198 }),
199 ),
200 );
213201 if (!handle) {
214202 return false;
215203 }
216204 if (handle.kind !== "git") {
217 return yield* unsupportedGitWorkflow(
205 return yield* new GitManagerError({
218206 operation,
219207 cwd,
220 `The ${operation} workflow currently supports Git repositories only; detected ${handle.kind}.`,
221 );
208 detail: `The ${operation} workflow currently supports Git repositories only; detected ${handle.kind}. (${cwd})`,
209 });
222210 }
223211 return true;
224212 },
......@@ -227,26 +215,28 @@ export const make = Effect.gen(function* () {
227215 const detectGitRepositoryForCommand = Effect.fn(
228216 "GitWorkflowService.detectGitRepositoryForCommand",
229217 )(function* (operation: string, cwd: string) {
230 const handle = yield* registry
231 .detect({ cwd })
232 .pipe(
233 Effect.mapError((error) =>
234 unsupportedGitCommand(
218 const handle = yield* registry.detect({ cwd }).pipe(
219 Effect.mapError(
220 (cause) =>
221 new GitCommandError({
235222 operation,
223 command: "vcs-route",
236224 cwd,
237 error instanceof Error ? error.message : String(error),
238 ),
239 ),
240 );
225 detail: "Failed to detect a VCS repository for this Git command.",
226 cause,
227 }),
228 ),
229 );
241230 if (!handle) {
242231 return false;
243232 }
244233 if (handle.kind !== "git") {
245 return yield* unsupportedGitCommand(
234 return yield* new GitCommandError({
246235 operation,
236 command: "vcs-route",
247237 cwd,
248 `The ${operation} command currently supports Git repositories only; detected ${handle.kind}.`,
249 );
238 detail: `The ${operation} command currently supports Git repositories only; detected ${handle.kind}.`,
239 });
250240 }
251241 return true;
252242 });
apps/server/src/vcs/VcsStatusBroadcaster.test.ts+61-3
......@@ -1,11 +1,13 @@
11import { assert, it, describe } from "@effect/vitest";
22import * as NodeServices from "@effect/platform-node/NodeServices";
3import * as Cause from "effect/Cause";
34import * as Deferred from "effect/Deferred";
45import * as Duration from "effect/Duration";
56import * as Effect from "effect/Effect";
67import * as Exit from "effect/Exit";
78import * as FileSystem from "effect/FileSystem";
89import * as Layer from "effect/Layer";
10import * as Logger from "effect/Logger";
911import * as Option from "effect/Option";
1012import * as Path from "effect/Path";
1113import * as Scope from "effect/Scope";
......@@ -190,6 +192,7 @@ describe("VcsStatusBroadcaster", () => {
190192 ? Effect.fail(
191193 new GitManagerError({
192194 operation: "VcsStatusBroadcaster.test",
195 cwd: "/repo",
193196 detail: "remote status failed",
194197 }),
195198 )
......@@ -431,6 +434,12 @@ describe("VcsStatusBroadcaster", () => {
431434 remoteInvalidationCalls: 0,
432435 remoteStatusRefreshUpstreamValues: [] as Array<boolean | undefined>,
433436 };
437 const privateCwd = "/private/user/workspace/repo";
438 const nestedCause = new Error("private nested VCS failure");
439 const messages: Array<ReadonlyArray<unknown>> = [];
440 const logger = Logger.make<unknown, void>(({ message }) => {
441 messages.push(message as ReadonlyArray<unknown>);
442 });
434443 let firstRemoteAttemptDeferred: Deferred.Deferred<void> | null = null;
435444 const testLayer = VcsStatusBroadcaster.layer.pipe(
436445 Layer.provideMerge(NodeServices.layer),
......@@ -449,7 +458,9 @@ describe("VcsStatusBroadcaster", () => {
449458 return Effect.fail(
450459 new GitManagerError({
451460 operation: "VcsStatusBroadcaster.test",
452 detail: "initial remote status failed",
461 cwd: privateCwd,
462 detail: "private initial remote status failure",
463 cause: nestedCause,
453464 }),
454465 ).pipe(
455466 Effect.ensuring(
......@@ -480,7 +491,7 @@ describe("VcsStatusBroadcaster", () => {
480491 const remoteUpdatedDeferred = yield* Deferred.make<VcsStatusStreamEvent>();
481492 yield* Stream.runForEach(
482493 broadcaster.streamStatus(
483 { cwd: "/repo" },
494 { cwd: privateCwd },
484495 { automaticRemoteRefreshInterval: Effect.succeed(Duration.zero) },
485496 ),
486497 (event) =>
......@@ -492,6 +503,24 @@ describe("VcsStatusBroadcaster", () => {
492503 yield* Deferred.await(firstRemoteAttemptDeferred);
493504 yield* Effect.yieldNow;
494505 assert.equal(state.remoteStatusCalls, 1);
506 assert.deepStrictEqual(
507 messages.find((message) => message[0] === "VCS remote status refresh failed"),
508 [
509 "VCS remote status refresh failed",
510 {
511 cwdLength: privateCwd.length,
512 reasonCount: 1,
513 failureCount: 1,
514 failureTags: ["GitManagerError"],
515 failureOperations: ["VcsStatusBroadcaster.test"],
516 defectCount: 0,
517 defectTags: [],
518 interruptionCount: 0,
519 consecutiveFailures: 1,
520 nextDelayMs: 30_000,
521 },
522 ],
523 );
495524
496525 yield* TestClock.adjust(Duration.seconds(30));
497526 const remoteUpdated = yield* Deferred.await(remoteUpdatedDeferred);
......@@ -505,7 +534,15 @@ describe("VcsStatusBroadcaster", () => {
505534 assert.deepStrictEqual(state.remoteStatusRefreshUpstreamValues, [false, false]);
506535
507536 yield* Scope.close(scope, Exit.void);
508 }).pipe(Effect.provide(Layer.merge(testLayer, TestClock.layer())));
537 }).pipe(
538 Effect.provide(
539 Layer.mergeAll(
540 testLayer,
541 TestClock.layer(),
542 Logger.layer([logger], { mergeWithExisting: false }),
543 ),
544 ),
545 );
509546 });
510547
511548 it.effect("delays automatic refresh when a cached remote snapshot is available", () => {
......@@ -573,6 +610,27 @@ describe("VcsStatusBroadcaster", () => {
573610 );
574611 });
575612
613 it("summarizes refresh causes without exposing nested failure details", () => {
614 const nestedCause = new Error("private nested failure detail");
615 const failure = new GitManagerError({
616 operation: "VcsStatusBroadcaster.remoteStatus",
617 cwd: "/private/user/workspace/repo",
618 detail: "private Git failure detail",
619 cause: nestedCause,
620 });
621 const cause = Cause.combine(Cause.fail(failure), Cause.die(new TypeError("private defect")));
622
623 assert.deepStrictEqual(VcsStatusBroadcaster.remoteRefreshFailureDiagnostics(cause), {
624 reasonCount: 2,
625 failureCount: 1,
626 failureTags: ["GitManagerError"],
627 failureOperations: ["VcsStatusBroadcaster.remoteStatus"],
628 defectCount: 1,
629 defectTags: ["TypeError"],
630 interruptionCount: 0,
631 });
632 });
633
576634 it.effect("stops the remote poller after the last stream subscriber disconnects", () => {
577635 const state = {
578636 currentLocalStatus: baseLocalStatus,
apps/server/src/vcs/VcsStatusBroadcaster.ts+93-2
......@@ -1,3 +1,4 @@
1import * as Cause from "effect/Cause";
12import * as Context from "effect/Context";
23import * as Duration from "effect/Duration";
34import * as Effect from "effect/Effect";
......@@ -26,6 +27,91 @@ import * as GitWorkflowService from "../git/GitWorkflowService.ts";
2627const DEFAULT_VCS_STATUS_REFRESH_INTERVAL = Duration.seconds(30);
2728const VCS_STATUS_REFRESH_FAILURE_BASE_DELAY = Duration.seconds(30);
2829const VCS_STATUS_REFRESH_FAILURE_MAX_DELAY = Duration.minutes(15);
30const MAX_FAILURE_DIAGNOSTIC_VALUES = 8;
31const MAX_FAILURE_DIAGNOSTIC_VALUE_LENGTH = 128;
32
33function boundedDiagnosticValue(value: string): string {
34 return value.slice(0, MAX_FAILURE_DIAGNOSTIC_VALUE_LENGTH);
35}
36
37function diagnosticValueTag(value: unknown): string {
38 try {
39 if (
40 typeof value === "object" &&
41 value !== null &&
42 "_tag" in value &&
43 typeof value._tag === "string"
44 ) {
45 return boundedDiagnosticValue(value._tag);
46 }
47 if (value instanceof Error) {
48 return boundedDiagnosticValue(value.name);
49 }
50 return typeof value;
51 } catch {
52 return "Uninspectable";
53 }
54}
55
56function diagnosticFailureOperation(value: unknown): string | undefined {
57 try {
58 if (
59 typeof value === "object" &&
60 value !== null &&
61 "operation" in value &&
62 typeof value.operation === "string"
63 ) {
64 return boundedDiagnosticValue(value.operation);
65 }
66 } catch {
67 return undefined;
68 }
69 return undefined;
70}
71
72function addUniqueDiagnosticValue(values: Array<string>, value: string | undefined): void {
73 if (
74 value !== undefined &&
75 values.length < MAX_FAILURE_DIAGNOSTIC_VALUES &&
76 !values.includes(value)
77 ) {
78 values.push(value);
79 }
80}
81
82export function remoteRefreshFailureDiagnostics(cause: Cause.Cause<unknown>) {
83 const failureTags: Array<string> = [];
84 const failureOperations: Array<string> = [];
85 const defectTags: Array<string> = [];
86 let failureCount = 0;
87 let defectCount = 0;
88 let interruptionCount = 0;
89
90 for (const reason of cause.reasons) {
91 if (Cause.isFailReason(reason)) {
92 failureCount += 1;
93 addUniqueDiagnosticValue(failureTags, diagnosticValueTag(reason.error));
94 addUniqueDiagnosticValue(failureOperations, diagnosticFailureOperation(reason.error));
95 continue;
96 }
97 if (Cause.isDieReason(reason)) {
98 defectCount += 1;
99 addUniqueDiagnosticValue(defectTags, diagnosticValueTag(reason.defect));
100 continue;
101 }
102 interruptionCount += 1;
103 }
104
105 return {
106 reasonCount: cause.reasons.length,
107 failureCount,
108 failureTags,
109 failureOperations,
110 defectCount,
111 defectTags,
112 interruptionCount,
113 };
114}
29115
30116interface VcsStatusChange {
31117 readonly cwd: string;
......@@ -318,14 +404,19 @@ export const make = Effect.gen(function* () {
318404 return activeInterval;
319405 }
320406
407 const interruptionReasons = exit.cause.reasons.filter(Cause.isInterruptReason);
408 if (interruptionReasons.length > 0) {
409 return yield* Effect.failCause(Cause.fromReasons<never>(interruptionReasons));
410 }
411
321412 const consecutiveFailures = yield* Ref.updateAndGet(
322413 consecutiveFailuresRef,
323414 (count) => count + 1,
324415 );
325416 const nextDelay = remoteRefreshFailureDelay(consecutiveFailures, activeInterval);
326417 yield* Effect.logWarning("VCS remote status refresh failed", {
327 cwd,
328 detail: exit.cause.toString(),
418 cwdLength: cwd.length,
419 ...remoteRefreshFailureDiagnostics(exit.cause),
329420 consecutiveFailures,
330421 nextDelayMs: Duration.toMillis(nextDelay),
331422 });
packages/contracts/src/git.ts+1
......@@ -352,6 +352,7 @@ export class TextGenerationError extends Schema.TaggedErrorClass<TextGenerationE
352352
353353export class GitManagerError extends Schema.TaggedErrorClass<GitManagerError>()("GitManagerError", {
354354 operation: Schema.String,
355 cwd: Schema.String,
355356 detail: Schema.String,
356357 cause: Schema.optional(Schema.Defect()),
357358}) {