Repository navigation
Expand file tree
/
Copy pathRunFinalizationService.ts
More file actions
131 lines (122 loc) · 5.15 KB
/
Copy pathRunFinalizationService.ts
File metadata and controls
131 lines (122 loc) · 5.15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
import { CheckpointScopeId, ProjectId, RunId, ThreadId } from "@t3tools/contracts";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as FiberMap from "effect/FiberMap";
import * as Layer from "effect/Layer";
import * as Schema from "effect/Schema";
import * as PullRequestService from "../pullRequest/PullRequestService.ts";
import * as VcsStatusBroadcaster from "../vcs/VcsStatusBroadcaster.ts";
import * as WorkspaceEntries from "../workspace/WorkspaceEntries.ts";
import * as CheckpointCapture from "./CheckpointCaptureService.ts";
import * as ProjectionStore from "./ProjectionStore.ts";
export class RunFinalizationError extends Schema.TaggedError<RunFinalizationError>()(
"RunFinalizationError",
{
threadId: ThreadId,
runId: RunId,
scopeId: CheckpointScopeId,
operation: Schema.Literals(["capture-checkpoint", "refresh-workspace"]),
cause: Schema.Defect(),
},
) {}
export class RunFinalizationRefreshError extends Schema.TaggedError<RunFinalizationRefreshError>()(
"RunFinalizationRefreshError",
{ cwd: Schema.String, cause: Schema.Defect() },
) {}
export class RunFinalizationObserver extends Context.Reference<{
readonly refreshAfterTurn: (projectId: ProjectId) => Effect.Effect<void>;
readonly refresh: (input: {
readonly cwd: string;
readonly threadId: ThreadId;
readonly runId: RunId;
}) => Effect.Effect<void, RunFinalizationRefreshError>;
}>("t3/orchestration-v2/RunFinalizationObserver", {
defaultValue: () => ({ refresh: () => Effect.void, refreshAfterTurn: () => Effect.void }),
}) {}
export class RunFinalizationService extends Context.Service<
RunFinalizationService,
{
readonly finalize: (input: {
readonly threadId: ThreadId;
readonly runId: RunId;
readonly scopeId: CheckpointScopeId;
}) => Effect.Effect<void, RunFinalizationError>;
}
>()("t3/orchestration-v2/RunFinalizationService") {}
const make = Effect.gen(function* () {
const checkpointCapture = yield* CheckpointCapture.CheckpointCaptureServiceV2;
const projections = yield* ProjectionStore.ProjectionStoreV2;
const observer = yield* RunFinalizationObserver;
const finalize: RunFinalizationService["Service"]["finalize"] = Effect.fn(
"RunFinalizationService.finalize",
)(function* (input) {
yield* checkpointCapture
.execute(input)
.pipe(
Effect.mapError(
(cause) => new RunFinalizationError({ ...input, operation: "capture-checkpoint", cause }),
),
);
const projection = yield* projections
.getCheckpointContext(input.threadId)
.pipe(
Effect.mapError(
(cause) => new RunFinalizationError({ ...input, operation: "refresh-workspace", cause }),
),
);
const cwd = projection.checkpointScopes.find((scope) => scope.id === input.scopeId)?.cwd;
if (cwd !== undefined) {
// Retrying a display refresh after the checkpoint is durable blocks the next turn.
yield* observer.refresh({ cwd, threadId: input.threadId, runId: input.runId }).pipe(
Effect.catchTags({
RunFinalizationRefreshError: (cause) =>
Effect.logWarning("Failed to refresh workspace after checkpoint capture", {
...input,
cause,
}),
}),
);
}
});
return RunFinalizationService.of({ finalize });
});
export const layer = Layer.effect(RunFinalizationService, make);
export const layerObserver = Layer.effect(
RunFinalizationObserver,
Effect.gen(function* () {
const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries;
const vcsStatus = yield* VcsStatusBroadcaster.VcsStatusBroadcaster;
const projections = yield* ProjectionStore.ProjectionStoreV2;
const pullRequests = yield* PullRequestService.PullRequestService;
const pullRequestLookups = yield* FiberMap.make<string>();
return {
refreshAfterTurn: pullRequests.refreshAfterTurn,
refresh: Effect.fn("RunFinalizationService.refresh")(
function* ({ cwd, threadId, runId }) {
const [, local] = yield* Effect.all(
[workspaceEntries.refresh(cwd), vcsStatus.refreshLocalStatus(cwd)],
{ concurrency: "unbounded" },
);
if (local.refName === null || local.isDefaultRef) return;
const thread = yield* projections.getThreadShell(threadId);
if (!thread || thread.branch !== local.refName) return;
if (thread.activeRunId !== null && thread.activeRunId !== runId) return;
// The forge CLI can take minutes, so the next turn must not wait for it. A newer
// turn's lookup replaces a running one instead of queueing behind the cwd's lock.
yield* vcsStatus.refreshPullRequestStatus(cwd).pipe(
Effect.catch((error) =>
Effect.logWarning("failed to refresh pull request status after run completion", {
threadId,
cwd,
detail: error.message,
}),
),
FiberMap.run(pullRequestLookups, cwd),
);
},
(effect, { cwd }) =>
effect.pipe(Effect.mapError((cause) => new RunFinalizationRefreshError({ cwd, cause }))),
),
};
}),
);