Repository navigation
Expand file tree
/
Copy pathRunFinalizationService.ts
More file actions
164 lines (155 loc) · 6.72 KB
/
Copy pathRunFinalizationService.ts
File metadata and controls
164 lines (155 loc) · 6.72 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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
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 Option from "effect/Option";
import * as Schema from "effect/Schema";
import * as ProjectEnrichment from "../project/ProjectEnrichmentService.ts";
import * as ProjectService from "../project/ProjectService.ts";
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 projects = yield* ProjectService.ProjectService;
const enrichment = yield* ProjectEnrichment.ProjectEnrichmentService;
const pullRequestLookups = yield* FiberMap.make<string>();
const identityRefreshes = yield* FiberMap.make<ProjectId>();
// A turn can add the first remote (`git remote add`, `gh repo create --push`), which the
// enrichment and resolver caches would otherwise keep reporting as missing for a minute.
const refreshMissingRepositoryIdentity = Effect.fn(
"RunFinalizationService.refreshMissingRepositoryIdentity",
)(
function* (projectId: ProjectId) {
const project = yield* projects.getById(projectId);
if (Option.isNone(project)) return;
const { workspaceRoot } = project.value;
if ((yield* enrichment.readRepositoryIdentity(workspaceRoot)) !== null) return;
yield* enrichment.refreshRepositoryIdentity(workspaceRoot);
},
(effect, projectId) =>
effect.pipe(
Effect.catch((error) =>
Effect.logWarning("failed to refresh repository identity after run completion", {
projectId,
detail: error.message,
}),
),
),
);
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" },
);
const thread = yield* projections.getThreadShell(threadId);
if (!thread) return;
// Forge discovery probes can take seconds; like the PR lookup, the next turn must not wait.
yield* refreshMissingRepositoryIdentity(thread.projectId).pipe(
FiberMap.run(identityRefreshes, thread.projectId),
);
if (local.refName === null || local.isDefaultRef) return;
if (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 }))),
),
};
}),
);