@abelxiaoxing/cadence 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents/contract-reviewer.md +2 -2
- package/agents/diagnosis-worker.md +3 -2
- package/agents/implementation-worker.md +3 -2
- package/package.json +1 -1
- package/prompts/abel-design.md +13 -3
- package/prompts/abel-implement.md +53 -13
- package/skills/abel-workflow/SKILL.md +60 -22
- package/src/candidate-preflight.ts +1468 -0
- package/src/child-session.ts +136 -30
- package/src/contracts.ts +1343 -63
- package/src/file-snapshot.ts +62 -8
- package/src/index.ts +99 -28
- package/src/parent-payload-bridge.ts +442 -0
- package/src/parent-provider.ts +166 -21
- package/src/patch.ts +331 -33
- package/src/result-store.ts +282 -7
- package/src/runtime.ts +1584 -154
- package/src/scheduler.ts +60 -16
- package/src/scoped-tools.ts +27 -6
- package/src/subagent-activity.ts +56 -25
- package/src/submit-tool.ts +15 -3
- package/src/worker.ts +114 -85
package/src/runtime.ts
CHANGED
|
@@ -1,30 +1,375 @@
|
|
|
1
|
+
import { lstatSync, readdirSync, realpathSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { Usage } from "@earendil-works/pi-ai";
|
|
1
4
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
5
|
import { Activation, type ActivationState } from "./activation.ts";
|
|
3
6
|
import { loadAgentDefinitions } from "./agent-registry.ts";
|
|
4
|
-
import {
|
|
7
|
+
import type { BaselineEntry } from "./candidate-preflight.ts";
|
|
8
|
+
import { runChildSession, UsageAggregator } from "./child-session.ts";
|
|
5
9
|
import {
|
|
6
10
|
ACTIONS,
|
|
11
|
+
type AgentsCheckpointAttempt,
|
|
12
|
+
type AgentsCheckpointRequest,
|
|
13
|
+
type CandidateFailure,
|
|
14
|
+
type ChildFailure,
|
|
7
15
|
type DiffResult,
|
|
16
|
+
type ImplementApplyOperation,
|
|
17
|
+
type ImplementationPhase,
|
|
18
|
+
type ImplementDiscardOperation,
|
|
19
|
+
type ImplementOutcome,
|
|
20
|
+
type ImplementRunRequest,
|
|
8
21
|
LIMITS,
|
|
22
|
+
type PhaseAttempt,
|
|
9
23
|
type RequestEnvelope,
|
|
24
|
+
type RunRequest,
|
|
25
|
+
type TaskBoundary,
|
|
26
|
+
type TaskFailure,
|
|
27
|
+
validateAgentsCheckpointAttempt,
|
|
28
|
+
validateImplementApplyOperation,
|
|
29
|
+
validateImplementDiscardOperation,
|
|
30
|
+
validatePhaseAttemptAgainstBoundary,
|
|
10
31
|
validateRequestEnvelope,
|
|
11
32
|
} from "./contracts.ts";
|
|
12
33
|
import { drainStage } from "./drain.ts";
|
|
13
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
type Bound,
|
|
36
|
+
type DirBound,
|
|
37
|
+
type FileBound,
|
|
38
|
+
isCurrent,
|
|
39
|
+
mergeBounds,
|
|
40
|
+
snapshotDirManifest,
|
|
41
|
+
snapshotFile,
|
|
42
|
+
snapshotFiles,
|
|
43
|
+
} from "./file-snapshot.ts";
|
|
44
|
+
import type { ParentPayloadBridge } from "./parent-payload-bridge.ts";
|
|
14
45
|
import { runtimeFromContext } from "./parent-provider.ts";
|
|
15
|
-
import { applyRetainedPatch } from "./patch.ts";
|
|
16
|
-
import {
|
|
17
|
-
|
|
46
|
+
import { applyAgentsCheckpoint, applyRetainedPatch } from "./patch.ts";
|
|
47
|
+
import {
|
|
48
|
+
type BoundRetainedResult,
|
|
49
|
+
ResultStore,
|
|
50
|
+
type RetainedCandidateFacts,
|
|
51
|
+
type RetainedCandidateIdentity,
|
|
52
|
+
} from "./result-store.ts";
|
|
53
|
+
import { declarationsConflict, Scheduler } from "./scheduler.ts";
|
|
18
54
|
import {
|
|
19
|
-
|
|
20
|
-
|
|
55
|
+
type TaskRecord,
|
|
56
|
+
taskConflictOf,
|
|
57
|
+
taskRecordKey,
|
|
21
58
|
WorkerRegistry,
|
|
22
59
|
workerIdentity,
|
|
23
60
|
} from "./worker.ts";
|
|
24
61
|
|
|
25
62
|
const SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
26
63
|
|
|
27
|
-
function
|
|
64
|
+
function isFileSystemFailure(error: unknown): boolean {
|
|
65
|
+
return (
|
|
66
|
+
error instanceof Error &&
|
|
67
|
+
"code" in error &&
|
|
68
|
+
typeof (error as { code?: unknown }).code === "string"
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function canonicalWorkspaceRoot(root: string): string | null {
|
|
73
|
+
try {
|
|
74
|
+
return realpathSync(root);
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isImplementRunRequest(
|
|
81
|
+
request: RunRequest,
|
|
82
|
+
): request is ImplementRunRequest {
|
|
83
|
+
return request.stage === "abel-implement" && "kind" in request;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function targetsImplement(request: unknown): boolean {
|
|
87
|
+
return (
|
|
88
|
+
typeof request === "object" &&
|
|
89
|
+
request !== null &&
|
|
90
|
+
!Array.isArray(request) &&
|
|
91
|
+
(request as Record<string, unknown>).stage === "abel-implement"
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function cloneLegacyEnvelope(request: RequestEnvelope): RequestEnvelope {
|
|
96
|
+
return {
|
|
97
|
+
...request,
|
|
98
|
+
taskId: request.taskId ?? request.id,
|
|
99
|
+
roots: [...request.roots],
|
|
100
|
+
context: { ...request.context },
|
|
101
|
+
declared: {
|
|
102
|
+
...request.declared,
|
|
103
|
+
read: [...request.declared.read],
|
|
104
|
+
write: [...request.declared.write],
|
|
105
|
+
conflicts: [...request.declared.conflicts],
|
|
106
|
+
resources: [...request.declared.resources],
|
|
107
|
+
},
|
|
108
|
+
...(request.approvedDependencies === undefined
|
|
109
|
+
? {}
|
|
110
|
+
: { approvedDependencies: [...request.approvedDependencies] }),
|
|
111
|
+
...(request.impactClosure === undefined
|
|
112
|
+
? {}
|
|
113
|
+
: { impactClosure: structuredClone(request.impactClosure) }),
|
|
114
|
+
...(request.verification === undefined
|
|
115
|
+
? {}
|
|
116
|
+
: {
|
|
117
|
+
verification: {
|
|
118
|
+
...request.verification,
|
|
119
|
+
argv: [...request.verification.argv],
|
|
120
|
+
},
|
|
121
|
+
}),
|
|
122
|
+
...(request.snapshot === undefined
|
|
123
|
+
? {}
|
|
124
|
+
: { snapshot: structuredClone(request.snapshot) }),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function deriveImplementEnvelope(
|
|
129
|
+
record: TaskRecord,
|
|
130
|
+
attempt: PhaseAttempt,
|
|
131
|
+
): RequestEnvelope {
|
|
132
|
+
const boundary = record.boundary;
|
|
133
|
+
const phase = boundary.phases[attempt.phase];
|
|
134
|
+
if (!phase) throw new Error("task attempt phase is not declared");
|
|
135
|
+
return {
|
|
136
|
+
stage: "abel-implement",
|
|
137
|
+
role: "implementation-worker",
|
|
138
|
+
taskId: boundary.taskId,
|
|
139
|
+
id: attempt.requestId,
|
|
140
|
+
phase: attempt.phase,
|
|
141
|
+
objective: boundary.objective,
|
|
142
|
+
roots: [...boundary.roots],
|
|
143
|
+
context: { ...boundary.context },
|
|
144
|
+
declared: {
|
|
145
|
+
read: [...phase.read],
|
|
146
|
+
write: [...phase.write],
|
|
147
|
+
conflicts: [...boundary.scheduling.conflicts],
|
|
148
|
+
resources: [...boundary.scheduling.resources],
|
|
149
|
+
...(phase.verificationLock === undefined
|
|
150
|
+
? {}
|
|
151
|
+
: { verificationLock: phase.verificationLock }),
|
|
152
|
+
},
|
|
153
|
+
output: "diff",
|
|
154
|
+
agentsImpact: boundary.agents.impact,
|
|
155
|
+
...(boundary.agents.target === undefined
|
|
156
|
+
? {}
|
|
157
|
+
: { agentsTarget: boundary.agents.target }),
|
|
158
|
+
agentsManagedOnly: true,
|
|
159
|
+
approvedDependencies: [...boundary.approvedDependencies],
|
|
160
|
+
impactClosure: structuredClone(boundary.impactClosure),
|
|
161
|
+
verification: {
|
|
162
|
+
...phase.verification,
|
|
163
|
+
argv: [...phase.verification.argv],
|
|
164
|
+
},
|
|
165
|
+
snapshot: structuredClone(attempt.snapshot),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function nextDeclaredPhase(
|
|
170
|
+
boundary: TaskBoundary,
|
|
171
|
+
phase: PhaseAttempt["phase"],
|
|
172
|
+
): "green" | "refactor" | null {
|
|
173
|
+
if (phase === "red") return "green";
|
|
174
|
+
if (phase === "green" && boundary.phases.refactor) return "refactor";
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function assertAttemptAllowed(record: TaskRecord, attempt: PhaseAttempt): void {
|
|
179
|
+
switch (record.state.kind) {
|
|
180
|
+
case "candidate-pending":
|
|
181
|
+
throw new Error("current phase candidate is awaiting parent apply");
|
|
182
|
+
case "agents-checkpoint-pending":
|
|
183
|
+
throw new Error("AGENTS checkpoint is pending");
|
|
184
|
+
case "blocked":
|
|
185
|
+
if (attempt.phase !== record.state.phase) {
|
|
186
|
+
throw new Error("terminal task phase mismatch");
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
case "completed":
|
|
190
|
+
if (attempt.phase !== record.state.finalPhase) {
|
|
191
|
+
throw new Error("terminal task phase mismatch");
|
|
192
|
+
}
|
|
193
|
+
return;
|
|
194
|
+
case "ready":
|
|
195
|
+
if (attempt.phase !== record.state.phase) {
|
|
196
|
+
throw new Error("invalid implementation phase transition");
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function taskPhase(record: TaskRecord): PhaseAttempt["phase"] {
|
|
202
|
+
return record.state.kind === "agents-checkpoint-pending" ||
|
|
203
|
+
record.state.kind === "completed"
|
|
204
|
+
? record.state.finalPhase
|
|
205
|
+
: record.state.phase;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function taskLaunchIndex(record: TaskRecord): 0 | 1 {
|
|
209
|
+
return record.state.kind === "ready" ||
|
|
210
|
+
record.state.kind === "candidate-pending"
|
|
211
|
+
? record.state.launchIndex
|
|
212
|
+
: record.state.kind === "blocked" &&
|
|
213
|
+
record.state.failure.kind === "attempts-exhausted"
|
|
214
|
+
? 1
|
|
215
|
+
: 0;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
interface PreparedPreflight {
|
|
219
|
+
snapshot: Bound;
|
|
220
|
+
baseline: BaselineEntry[];
|
|
221
|
+
packageManifest: FileBound;
|
|
222
|
+
lockfile: FileBound;
|
|
223
|
+
dependencyTarget: FileBound | DirBound;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function addBaselineFile(
|
|
227
|
+
root: string,
|
|
228
|
+
relative: string,
|
|
229
|
+
executable: boolean,
|
|
230
|
+
entries: Map<string, BaselineEntry>,
|
|
231
|
+
observed: Bound,
|
|
232
|
+
): boolean {
|
|
233
|
+
const bound = snapshotFile(root, relative);
|
|
234
|
+
if (!bound) return false;
|
|
235
|
+
observed[relative] = bound;
|
|
236
|
+
entries.set(relative, {
|
|
237
|
+
path: relative,
|
|
238
|
+
kind: "file",
|
|
239
|
+
sha256: bound.sha256,
|
|
240
|
+
bytes: bound.bytes,
|
|
241
|
+
executable,
|
|
242
|
+
});
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function addDirectoryFiles(
|
|
247
|
+
root: string,
|
|
248
|
+
relative: string,
|
|
249
|
+
entries: Map<string, BaselineEntry>,
|
|
250
|
+
observed: Bound,
|
|
251
|
+
): boolean {
|
|
252
|
+
const directory = path.resolve(root, relative);
|
|
253
|
+
for (const name of readdirSync(directory).sort()) {
|
|
254
|
+
const child = path.posix.join(relative, name);
|
|
255
|
+
const stat = lstatSync(path.resolve(root, child));
|
|
256
|
+
if (stat.isSymbolicLink()) return false;
|
|
257
|
+
if (stat.isDirectory()) {
|
|
258
|
+
if (!addDirectoryFiles(root, child, entries, observed)) return false;
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (!stat.isFile()) return false;
|
|
262
|
+
if (
|
|
263
|
+
!addBaselineFile(
|
|
264
|
+
root,
|
|
265
|
+
child,
|
|
266
|
+
Boolean(stat.mode & 0o111),
|
|
267
|
+
entries,
|
|
268
|
+
observed,
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function preparePreflight(
|
|
277
|
+
root: string,
|
|
278
|
+
envelope: RequestEnvelope,
|
|
279
|
+
requestSnapshot: Bound,
|
|
280
|
+
): PreparedPreflight | null {
|
|
281
|
+
if (!envelope.verification) return null;
|
|
282
|
+
const packageManifest = snapshotFile(root, "package.json");
|
|
283
|
+
const lockPath = [
|
|
284
|
+
"bun.lock",
|
|
285
|
+
"bun.lockb",
|
|
286
|
+
"package-lock.json",
|
|
287
|
+
"pnpm-lock.yaml",
|
|
288
|
+
"yarn.lock",
|
|
289
|
+
].find((candidate) => snapshotFile(root, candidate) !== null);
|
|
290
|
+
const lockfile = lockPath ? snapshotFile(root, lockPath) : null;
|
|
291
|
+
const dependencyTarget =
|
|
292
|
+
snapshotDirManifest(root, "node_modules") ??
|
|
293
|
+
snapshotFile(root, "node_modules");
|
|
294
|
+
if (!packageManifest || !lockPath || !lockfile || !dependencyTarget)
|
|
295
|
+
return null;
|
|
296
|
+
if (!isCurrent(root, requestSnapshot)) {
|
|
297
|
+
return {
|
|
298
|
+
snapshot: mergeBounds(
|
|
299
|
+
snapshotFiles(root, ["package.json", lockPath]),
|
|
300
|
+
{ node_modules: dependencyTarget },
|
|
301
|
+
requestSnapshot,
|
|
302
|
+
),
|
|
303
|
+
baseline: [],
|
|
304
|
+
packageManifest,
|
|
305
|
+
lockfile,
|
|
306
|
+
dependencyTarget,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const paths = new Set([
|
|
311
|
+
...Object.keys(requestSnapshot),
|
|
312
|
+
...envelope.declared.read,
|
|
313
|
+
...envelope.declared.write,
|
|
314
|
+
"package.json",
|
|
315
|
+
lockPath,
|
|
316
|
+
...envelope.verification.argv.slice(3),
|
|
317
|
+
]);
|
|
318
|
+
const entries = new Map<string, BaselineEntry>();
|
|
319
|
+
const observed: Bound = {};
|
|
320
|
+
for (const relative of [...paths].sort()) {
|
|
321
|
+
const absolute = path.resolve(root, relative);
|
|
322
|
+
if (
|
|
323
|
+
absolute === root ||
|
|
324
|
+
!absolute.startsWith(`${path.resolve(root)}${path.sep}`)
|
|
325
|
+
) {
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
const stat = lstatSync(absolute, { throwIfNoEntry: false });
|
|
329
|
+
if (!stat) {
|
|
330
|
+
observed[relative] = { kind: "absent", absent: true };
|
|
331
|
+
entries.set(relative, { path: relative, kind: "deleted" });
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if (stat.isDirectory()) {
|
|
335
|
+
if (relative === "node_modules") continue;
|
|
336
|
+
try {
|
|
337
|
+
if (!addDirectoryFiles(root, relative, entries, observed)) return null;
|
|
338
|
+
} catch {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (!stat.isFile() || stat.isSymbolicLink()) return null;
|
|
344
|
+
if (
|
|
345
|
+
!addBaselineFile(
|
|
346
|
+
root,
|
|
347
|
+
relative,
|
|
348
|
+
Boolean(stat.mode & 0o111),
|
|
349
|
+
entries,
|
|
350
|
+
observed,
|
|
351
|
+
)
|
|
352
|
+
)
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
const baseline = [...entries.values()].sort((left, right) =>
|
|
356
|
+
left.path.localeCompare(right.path),
|
|
357
|
+
);
|
|
358
|
+
const dependencyBound: Bound = {
|
|
359
|
+
node_modules: dependencyTarget,
|
|
360
|
+
};
|
|
361
|
+
return {
|
|
362
|
+
snapshot: mergeBounds(observed, dependencyBound, requestSnapshot),
|
|
363
|
+
baseline,
|
|
364
|
+
packageManifest,
|
|
365
|
+
lockfile,
|
|
366
|
+
dependencyTarget,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function cancellationError(
|
|
371
|
+
signal: AbortSignal,
|
|
372
|
+
): Extract<WrappedDispatchResult, { ok: false }> {
|
|
28
373
|
const reason = signal.reason;
|
|
29
374
|
return {
|
|
30
375
|
ok: false,
|
|
@@ -37,6 +382,144 @@ function cancellationError(signal: AbortSignal): { ok: false; error: string } {
|
|
|
37
382
|
};
|
|
38
383
|
}
|
|
39
384
|
|
|
385
|
+
type FailureClass = "transport" | "artifact" | "environment";
|
|
386
|
+
|
|
387
|
+
type InternalResultMetadata = {
|
|
388
|
+
failureKind?: RunFailureKind;
|
|
389
|
+
failureClass?: FailureClass;
|
|
390
|
+
failure?: ChildFailure | TaskFailure;
|
|
391
|
+
launchConsumed?: true;
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
type ChildDispatchResult = WrappedDispatchResult & InternalResultMetadata;
|
|
395
|
+
type InternalDispatchResult = (
|
|
396
|
+
| WrappedDispatchResult
|
|
397
|
+
| (ImplementOutcome & { usage?: Usage })
|
|
398
|
+
) &
|
|
399
|
+
InternalResultMetadata;
|
|
400
|
+
type InternalFailureResult = Extract<WrappedDispatchResult, { ok: false }> &
|
|
401
|
+
InternalResultMetadata;
|
|
402
|
+
|
|
403
|
+
interface OutcomeIdentity {
|
|
404
|
+
taskId: string;
|
|
405
|
+
requestId: string;
|
|
406
|
+
phase: ImplementationPhase;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function blockedOutcome(
|
|
410
|
+
identity: OutcomeIdentity,
|
|
411
|
+
failure: TaskFailure,
|
|
412
|
+
): ImplementOutcome {
|
|
413
|
+
return { kind: "blocked", ...identity, failure: structuredClone(failure) };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function cancelledOutcome(identity: OutcomeIdentity): ImplementOutcome {
|
|
417
|
+
return { kind: "cancelled", ...identity };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function retryOutcome(
|
|
421
|
+
identity: OutcomeIdentity,
|
|
422
|
+
scope: "worker" | "checkpoint",
|
|
423
|
+
cause: "artifact" | "stale",
|
|
424
|
+
): ImplementOutcome {
|
|
425
|
+
return {
|
|
426
|
+
kind: "retry",
|
|
427
|
+
...identity,
|
|
428
|
+
scope,
|
|
429
|
+
cause,
|
|
430
|
+
remainingAttempts: 1,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function failureClassOf(failure: ChildFailure): FailureClass | undefined {
|
|
435
|
+
switch (failure.kind) {
|
|
436
|
+
case "artifact":
|
|
437
|
+
return "artifact";
|
|
438
|
+
case "environment":
|
|
439
|
+
return "environment";
|
|
440
|
+
case "transport":
|
|
441
|
+
return "transport";
|
|
442
|
+
case "stale":
|
|
443
|
+
case "approval-boundary":
|
|
444
|
+
case "cancelled":
|
|
445
|
+
case "result-limit":
|
|
446
|
+
return undefined;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function artifactCorrectionEvidence(failure?: ChildFailure): string {
|
|
451
|
+
return failure?.kind === "artifact"
|
|
452
|
+
? `candidate:${failure.code}`
|
|
453
|
+
: "generated-artifact-rejection";
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function childFailureOf(result: InternalFailureResult): ChildFailure {
|
|
457
|
+
if (result.failure) {
|
|
458
|
+
switch (result.failure.kind) {
|
|
459
|
+
case "artifact":
|
|
460
|
+
case "stale":
|
|
461
|
+
case "environment":
|
|
462
|
+
case "approval-boundary":
|
|
463
|
+
case "cancelled":
|
|
464
|
+
case "result-limit":
|
|
465
|
+
case "transport":
|
|
466
|
+
return result.failure;
|
|
467
|
+
case "attempts-exhausted":
|
|
468
|
+
case "checkpoint-attempts-exhausted":
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (result.failureKind === "cancelled") {
|
|
473
|
+
return { kind: "cancelled", code: "cancelled" };
|
|
474
|
+
}
|
|
475
|
+
switch (result.failureClass) {
|
|
476
|
+
case "artifact":
|
|
477
|
+
return { kind: "artifact", code: "invalid-diff" };
|
|
478
|
+
case "environment":
|
|
479
|
+
return { kind: "environment", code: "root-unavailable" };
|
|
480
|
+
default:
|
|
481
|
+
return {
|
|
482
|
+
kind: "transport",
|
|
483
|
+
code:
|
|
484
|
+
result.failureKind === "timed-out" ? "timeout" : "transport-failure",
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function terminalTaskFailure(failure: ChildFailure): TaskFailure | undefined {
|
|
490
|
+
switch (failure.kind) {
|
|
491
|
+
case "approval-boundary":
|
|
492
|
+
case "environment":
|
|
493
|
+
case "result-limit":
|
|
494
|
+
return failure;
|
|
495
|
+
case "artifact":
|
|
496
|
+
case "stale":
|
|
497
|
+
case "cancelled":
|
|
498
|
+
case "transport":
|
|
499
|
+
return undefined;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function attemptFailureCause(
|
|
504
|
+
failure: ChildFailure,
|
|
505
|
+
): "artifact" | "stale" | "transport" {
|
|
506
|
+
switch (failure.kind) {
|
|
507
|
+
case "artifact":
|
|
508
|
+
case "stale":
|
|
509
|
+
case "transport":
|
|
510
|
+
return failure.kind;
|
|
511
|
+
case "approval-boundary":
|
|
512
|
+
case "environment":
|
|
513
|
+
case "result-limit":
|
|
514
|
+
case "cancelled":
|
|
515
|
+
throw new Error("non-retryable failure reached the launch budget");
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function assertNever(value: never): never {
|
|
520
|
+
throw new Error(`unhandled candidate failure: ${JSON.stringify(value)}`);
|
|
521
|
+
}
|
|
522
|
+
|
|
40
523
|
export type RunFailureKind = "failed" | "cancelled" | "timed-out";
|
|
41
524
|
export type RuntimeFailureReason =
|
|
42
525
|
| "subagent failed"
|
|
@@ -65,7 +548,7 @@ export type RuntimeActivityObserver = (
|
|
|
65
548
|
) => void | Promise<void>;
|
|
66
549
|
|
|
67
550
|
interface ScheduledRunResult {
|
|
68
|
-
dispatch:
|
|
551
|
+
dispatch: InternalDispatchResult;
|
|
69
552
|
failureKind?: RunFailureKind;
|
|
70
553
|
}
|
|
71
554
|
|
|
@@ -115,25 +598,35 @@ function isSafeBound(snapshot: unknown): snapshot is Bound {
|
|
|
115
598
|
return true;
|
|
116
599
|
}
|
|
117
600
|
|
|
118
|
-
export type
|
|
601
|
+
export type WrappedDispatchResult =
|
|
119
602
|
| {
|
|
120
603
|
ok: true;
|
|
121
604
|
action: string;
|
|
122
605
|
result?: unknown;
|
|
123
606
|
resultId?: string;
|
|
124
|
-
usage?:
|
|
607
|
+
usage?: Usage;
|
|
125
608
|
}
|
|
126
|
-
| {
|
|
609
|
+
| {
|
|
610
|
+
ok: false;
|
|
611
|
+
notReady?: true;
|
|
612
|
+
error: string;
|
|
613
|
+
failure?: ChildFailure | TaskFailure;
|
|
614
|
+
usage?: Usage;
|
|
615
|
+
};
|
|
127
616
|
|
|
128
|
-
export
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
617
|
+
export type DispatchResult =
|
|
618
|
+
| WrappedDispatchResult
|
|
619
|
+
| (ImplementOutcome & { usage?: Usage });
|
|
620
|
+
|
|
621
|
+
export function isWrappedDispatchResult(
|
|
622
|
+
result: DispatchResult,
|
|
623
|
+
): result is WrappedDispatchResult {
|
|
624
|
+
return "ok" in result;
|
|
133
625
|
}
|
|
134
626
|
|
|
135
627
|
export interface RuntimeOptions {
|
|
136
628
|
activation?: Activation;
|
|
629
|
+
parentPayloadBridge: ParentPayloadBridge;
|
|
137
630
|
}
|
|
138
631
|
|
|
139
632
|
type RunContext = Pick<ExtensionContext, "cwd" | "model" | "modelRegistry">;
|
|
@@ -144,6 +637,7 @@ interface StoredRunContext {
|
|
|
144
637
|
sequence: number;
|
|
145
638
|
runningEmitted: boolean;
|
|
146
639
|
terminalEmitted: boolean;
|
|
640
|
+
usage: UsageAggregator;
|
|
147
641
|
}
|
|
148
642
|
|
|
149
643
|
export class Runtime {
|
|
@@ -153,22 +647,74 @@ export class Runtime {
|
|
|
153
647
|
private applyTail: Promise<void> = Promise.resolve();
|
|
154
648
|
private applySeq = 0;
|
|
155
649
|
private batchSeq = 0;
|
|
650
|
+
private readonly parentPayloadBridge: ParentPayloadBridge;
|
|
156
651
|
private readonly registry = new WorkerRegistry();
|
|
652
|
+
private readonly taskRecords = new WeakMap<RequestEnvelope, TaskRecord>();
|
|
157
653
|
private readonly runContexts = new WeakMap<
|
|
158
654
|
RequestEnvelope,
|
|
159
655
|
StoredRunContext
|
|
160
656
|
>();
|
|
161
657
|
private readonly scheduler: Scheduler<ScheduledRunResult>;
|
|
162
658
|
|
|
163
|
-
constructor(opts: RuntimeOptions
|
|
659
|
+
constructor(opts: RuntimeOptions) {
|
|
164
660
|
this.activation = opts.activation ?? new Activation();
|
|
661
|
+
this.parentPayloadBridge = opts.parentPayloadBridge;
|
|
165
662
|
this.scheduler = new Scheduler({
|
|
166
663
|
limit: LIMITS.maxActiveChildSessions,
|
|
167
664
|
execute: async (request, signal) => {
|
|
168
665
|
const context = this.runContexts.get(request);
|
|
169
666
|
if (!context) throw new Error("scheduled run context is unavailable");
|
|
667
|
+
const task = this.taskRecords.get(request);
|
|
668
|
+
let candidateRollback:
|
|
669
|
+
| {
|
|
670
|
+
resultId: string;
|
|
671
|
+
requestId: string;
|
|
672
|
+
phase: ImplementationPhase;
|
|
673
|
+
launchIndex: 0 | 1;
|
|
674
|
+
priorState: TaskRecord["state"];
|
|
675
|
+
}
|
|
676
|
+
| undefined;
|
|
677
|
+
const rollback = () => {
|
|
678
|
+
const pending = candidateRollback;
|
|
679
|
+
if (!pending) return;
|
|
680
|
+
this.results.discard(pending.resultId);
|
|
681
|
+
if (
|
|
682
|
+
task?.state.kind === "candidate-pending" &&
|
|
683
|
+
task.state.resultId === pending.resultId &&
|
|
684
|
+
task.state.originRequestId === pending.requestId &&
|
|
685
|
+
task.state.phase === pending.phase &&
|
|
686
|
+
task.state.launchIndex === pending.launchIndex
|
|
687
|
+
) {
|
|
688
|
+
task.state = structuredClone(pending.priorState);
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
signal.addEventListener("abort", rollback, { once: true });
|
|
170
692
|
this.notify(request, context, "running");
|
|
171
|
-
const result = await this.runScheduled(request, context
|
|
693
|
+
const result = await this.runScheduled(request, context, signal);
|
|
694
|
+
if (!isWrappedDispatchResult(result) && result.kind === "candidate") {
|
|
695
|
+
if (!task) throw new Error("candidate TaskRecord is unavailable");
|
|
696
|
+
if (task.state.kind !== "ready") {
|
|
697
|
+
throw new Error("candidate TaskRecord is not ready");
|
|
698
|
+
}
|
|
699
|
+
candidateRollback = {
|
|
700
|
+
resultId: result.resultId,
|
|
701
|
+
requestId: result.requestId,
|
|
702
|
+
phase: result.phase,
|
|
703
|
+
launchIndex: task.state.launchIndex,
|
|
704
|
+
priorState: structuredClone(task.state),
|
|
705
|
+
};
|
|
706
|
+
if (signal.aborted) {
|
|
707
|
+
rollback();
|
|
708
|
+
} else {
|
|
709
|
+
try {
|
|
710
|
+
this.rememberRetainedResult(result, request, task);
|
|
711
|
+
} catch (error) {
|
|
712
|
+
this.results.discard(result.resultId);
|
|
713
|
+
throw error;
|
|
714
|
+
}
|
|
715
|
+
if (signal.aborted) rollback();
|
|
716
|
+
}
|
|
717
|
+
}
|
|
172
718
|
return {
|
|
173
719
|
dispatch: result,
|
|
174
720
|
...(result.failureKind === undefined
|
|
@@ -181,13 +727,19 @@ export class Runtime {
|
|
|
181
727
|
|
|
182
728
|
async execute(
|
|
183
729
|
action: string,
|
|
184
|
-
params: {
|
|
730
|
+
params: {
|
|
731
|
+
request?: unknown;
|
|
732
|
+
resultId?: string;
|
|
733
|
+
requestId?: string;
|
|
734
|
+
rejection?: unknown;
|
|
735
|
+
agentsCheckpoint?: unknown;
|
|
736
|
+
},
|
|
185
737
|
ctx?: RunContext,
|
|
186
738
|
signal?: AbortSignal,
|
|
187
739
|
observer?: RuntimeActivityObserver,
|
|
188
740
|
): Promise<DispatchResult> {
|
|
189
741
|
if (!(ACTIONS as readonly string[]).includes(action)) {
|
|
190
|
-
|
|
742
|
+
throw new Error(`unknown action: ${String(action)}`);
|
|
191
743
|
}
|
|
192
744
|
if (!this.activation.isActive()) {
|
|
193
745
|
return { ok: false, notReady: true, error: "dispatcher is not active" };
|
|
@@ -195,14 +747,56 @@ export class Runtime {
|
|
|
195
747
|
if (action === "run")
|
|
196
748
|
return this.run(params.request, ctx, signal, observer);
|
|
197
749
|
if (action === "apply") {
|
|
198
|
-
if (!ctx
|
|
199
|
-
|
|
200
|
-
|
|
750
|
+
if (!ctx) throw new Error("apply requires context");
|
|
751
|
+
if (params.agentsCheckpoint !== undefined) {
|
|
752
|
+
if (params.resultId !== undefined) {
|
|
753
|
+
throw new Error("apply accepts either resultId or AGENTS checkpoint");
|
|
754
|
+
}
|
|
755
|
+
const validation = validateAgentsCheckpointAttempt(
|
|
756
|
+
params.agentsCheckpoint,
|
|
757
|
+
);
|
|
758
|
+
if (!validation.ok) {
|
|
759
|
+
throw new Error(`Implement protocol error: ${validation.reason}`);
|
|
760
|
+
}
|
|
761
|
+
return this.enqueueAgentsCheckpoint(ctx.cwd, validation.value, signal);
|
|
762
|
+
}
|
|
763
|
+
if (!params.resultId) {
|
|
764
|
+
throw new Error("apply requires resultId");
|
|
765
|
+
}
|
|
766
|
+
const retained = this.results.get(params.resultId);
|
|
767
|
+
if (
|
|
768
|
+
params.requestId !== undefined ||
|
|
769
|
+
(retained && this.results.hasCandidateIdentity(params.resultId))
|
|
770
|
+
) {
|
|
771
|
+
const validation = validateImplementApplyOperation(params);
|
|
772
|
+
if (!validation.ok) {
|
|
773
|
+
throw new Error(`Implement protocol error: ${validation.reason}`);
|
|
774
|
+
}
|
|
775
|
+
if (!retained || !this.results.hasCandidateIdentity(params.resultId)) {
|
|
776
|
+
throw new Error("retained Implement result not found");
|
|
777
|
+
}
|
|
778
|
+
return this.enqueueApply(ctx.cwd, validation.value, signal);
|
|
779
|
+
}
|
|
780
|
+
return this.enqueueApply(ctx.cwd, { resultId: params.resultId }, signal);
|
|
201
781
|
}
|
|
202
782
|
if (action === "discard") {
|
|
203
|
-
if (!params.resultId)
|
|
204
|
-
|
|
205
|
-
|
|
783
|
+
if (!params.resultId) throw new Error("discard requires resultId");
|
|
784
|
+
const retained = this.results.get(params.resultId);
|
|
785
|
+
if (
|
|
786
|
+
params.requestId !== undefined ||
|
|
787
|
+
params.rejection !== undefined ||
|
|
788
|
+
(retained && this.results.hasCandidateIdentity(params.resultId))
|
|
789
|
+
) {
|
|
790
|
+
const validation = validateImplementDiscardOperation(params);
|
|
791
|
+
if (!validation.ok) {
|
|
792
|
+
throw new Error(`Implement protocol error: ${validation.reason}`);
|
|
793
|
+
}
|
|
794
|
+
if (!retained || !this.results.hasCandidateIdentity(params.resultId)) {
|
|
795
|
+
throw new Error("retained Implement result not found");
|
|
796
|
+
}
|
|
797
|
+
return this.enqueueDiscard(validation.value);
|
|
798
|
+
}
|
|
799
|
+
return this.discardRetainedCandidate(params.resultId)
|
|
206
800
|
? { ok: true, action }
|
|
207
801
|
: { ok: false, error: "retained result not found" };
|
|
208
802
|
}
|
|
@@ -214,23 +808,493 @@ export class Runtime {
|
|
|
214
808
|
return { ok: true, action: "finish" };
|
|
215
809
|
}
|
|
216
810
|
|
|
217
|
-
private
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
811
|
+
private enqueueAgentsCheckpoint(
|
|
812
|
+
root: string,
|
|
813
|
+
attempt: AgentsCheckpointAttempt,
|
|
814
|
+
signal?: AbortSignal,
|
|
815
|
+
): Promise<DispatchResult> {
|
|
816
|
+
const sequence = ++this.applySeq;
|
|
817
|
+
return this.enqueueParentApply(() =>
|
|
818
|
+
this.performAgentsCheckpoint(root, attempt, signal),
|
|
819
|
+
).then((result) => {
|
|
820
|
+
if (
|
|
821
|
+
isWrappedDispatchResult(result) ||
|
|
822
|
+
result.kind !== "completed" ||
|
|
823
|
+
!result.result
|
|
824
|
+
) {
|
|
825
|
+
return result;
|
|
826
|
+
}
|
|
827
|
+
return {
|
|
828
|
+
...result,
|
|
829
|
+
result: { ...result.result, sequence },
|
|
830
|
+
};
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
private async performAgentsCheckpoint(
|
|
835
|
+
root: string,
|
|
836
|
+
attempt: AgentsCheckpointAttempt,
|
|
837
|
+
signal?: AbortSignal,
|
|
838
|
+
): Promise<DispatchResult> {
|
|
839
|
+
const workspaceRoot = canonicalWorkspaceRoot(root);
|
|
840
|
+
if (!workspaceRoot) {
|
|
841
|
+
throw new Error("AGENTS checkpoint workspace is unavailable");
|
|
842
|
+
}
|
|
843
|
+
const worker = this.registry.get(
|
|
844
|
+
taskRecordKey(workspaceRoot, attempt.changeId, attempt.taskId),
|
|
845
|
+
);
|
|
846
|
+
if (!worker) {
|
|
847
|
+
if (
|
|
848
|
+
this.registry
|
|
849
|
+
.values()
|
|
850
|
+
.some((record) => record.workspaceRoot === workspaceRoot)
|
|
851
|
+
) {
|
|
852
|
+
throw new Error("AGENTS checkpoint identity mismatch");
|
|
853
|
+
}
|
|
854
|
+
throw new Error("logical Worker is unavailable for AGENTS checkpoint");
|
|
855
|
+
}
|
|
856
|
+
const agents = worker.boundary.agents;
|
|
857
|
+
if (agents.impact === "none" || !agents.target || !agents.managedOnly) {
|
|
858
|
+
throw new Error("AGENTS checkpoint contract mismatch");
|
|
859
|
+
}
|
|
860
|
+
if (worker.state.kind !== "agents-checkpoint-pending") {
|
|
861
|
+
throw new Error("AGENTS checkpoint is not pending");
|
|
862
|
+
}
|
|
863
|
+
if (
|
|
864
|
+
Object.keys(attempt.snapshot as Record<string, unknown>).length !== 1 ||
|
|
865
|
+
!Object.hasOwn(attempt.snapshot as object, agents.target)
|
|
866
|
+
) {
|
|
867
|
+
throw new Error("AGENTS checkpoint target mismatch");
|
|
868
|
+
}
|
|
869
|
+
const request: AgentsCheckpointRequest = {
|
|
870
|
+
stage: "abel-implement",
|
|
871
|
+
taskId: worker.boundary.taskId,
|
|
872
|
+
agentsImpact: agents.impact,
|
|
873
|
+
agentsTarget: agents.target,
|
|
874
|
+
agentsManagedOnly: true,
|
|
875
|
+
stableCheckpoint: true,
|
|
876
|
+
snapshot: attempt.snapshot,
|
|
877
|
+
diff: attempt.diff,
|
|
878
|
+
};
|
|
879
|
+
const result = await applyAgentsCheckpoint(workspaceRoot, request, signal);
|
|
880
|
+
if (result.ok) {
|
|
881
|
+
const finalPhase = worker.state.finalPhase;
|
|
882
|
+
worker.state = {
|
|
883
|
+
kind: "completed",
|
|
884
|
+
finalPhase,
|
|
885
|
+
};
|
|
886
|
+
const { ok: _ok, ...checkpoint } = result;
|
|
887
|
+
return {
|
|
888
|
+
kind: "completed",
|
|
889
|
+
requestId: attempt.requestId,
|
|
890
|
+
taskId: attempt.taskId,
|
|
891
|
+
finalPhase,
|
|
892
|
+
result: checkpoint,
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
return this.presentCheckpointFailure(attempt, worker, result.failure);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
private presentCheckpointFailure(
|
|
899
|
+
attempt: AgentsCheckpointAttempt,
|
|
900
|
+
worker: TaskRecord,
|
|
901
|
+
failure: CandidateFailure,
|
|
902
|
+
): DispatchResult {
|
|
903
|
+
if (worker.state.kind !== "agents-checkpoint-pending") {
|
|
904
|
+
throw new Error("AGENTS checkpoint is not pending");
|
|
905
|
+
}
|
|
906
|
+
const phase = worker.state.finalPhase;
|
|
907
|
+
const identity = {
|
|
908
|
+
taskId: attempt.taskId,
|
|
909
|
+
requestId: attempt.requestId,
|
|
910
|
+
phase,
|
|
911
|
+
};
|
|
912
|
+
switch (failure.kind) {
|
|
913
|
+
case "artifact": {
|
|
914
|
+
if (worker.state.attemptIndex === 1) {
|
|
915
|
+
const taskFailure: TaskFailure = {
|
|
916
|
+
kind: "checkpoint-attempts-exhausted",
|
|
917
|
+
cause: "artifact",
|
|
918
|
+
};
|
|
919
|
+
worker.state = {
|
|
920
|
+
kind: "blocked",
|
|
921
|
+
phase,
|
|
922
|
+
failure: structuredClone(taskFailure),
|
|
923
|
+
};
|
|
924
|
+
return blockedOutcome(identity, taskFailure);
|
|
925
|
+
}
|
|
926
|
+
worker.state = {
|
|
927
|
+
kind: "agents-checkpoint-pending",
|
|
928
|
+
finalPhase: phase,
|
|
929
|
+
attemptIndex: 1,
|
|
930
|
+
};
|
|
931
|
+
return retryOutcome(identity, "checkpoint", "artifact");
|
|
932
|
+
}
|
|
933
|
+
case "stale": {
|
|
934
|
+
if (worker.state.attemptIndex === 1) {
|
|
935
|
+
const taskFailure: TaskFailure = {
|
|
936
|
+
kind: "checkpoint-attempts-exhausted",
|
|
937
|
+
cause: "stale",
|
|
938
|
+
};
|
|
939
|
+
worker.state = {
|
|
940
|
+
kind: "blocked",
|
|
941
|
+
phase,
|
|
942
|
+
failure: structuredClone(taskFailure),
|
|
943
|
+
};
|
|
944
|
+
return blockedOutcome(identity, taskFailure);
|
|
945
|
+
}
|
|
946
|
+
worker.state = {
|
|
947
|
+
kind: "agents-checkpoint-pending",
|
|
948
|
+
finalPhase: phase,
|
|
949
|
+
attemptIndex: 1,
|
|
950
|
+
};
|
|
951
|
+
return retryOutcome(identity, "checkpoint", "stale");
|
|
952
|
+
}
|
|
953
|
+
case "environment": {
|
|
954
|
+
const taskFailure: TaskFailure = failure;
|
|
955
|
+
worker.state = {
|
|
956
|
+
kind: "blocked",
|
|
957
|
+
phase,
|
|
958
|
+
failure: structuredClone(taskFailure),
|
|
959
|
+
};
|
|
960
|
+
return blockedOutcome(identity, taskFailure);
|
|
961
|
+
}
|
|
962
|
+
case "approval-boundary": {
|
|
963
|
+
const taskFailure: TaskFailure = failure;
|
|
964
|
+
worker.state = {
|
|
965
|
+
kind: "blocked",
|
|
966
|
+
phase,
|
|
967
|
+
failure: structuredClone(taskFailure),
|
|
968
|
+
};
|
|
969
|
+
return blockedOutcome(identity, taskFailure);
|
|
970
|
+
}
|
|
971
|
+
case "cancelled":
|
|
972
|
+
return cancelledOutcome(identity);
|
|
973
|
+
case "result-limit": {
|
|
974
|
+
const taskFailure: TaskFailure = failure;
|
|
975
|
+
worker.state = {
|
|
976
|
+
kind: "blocked",
|
|
977
|
+
phase,
|
|
978
|
+
failure: structuredClone(taskFailure),
|
|
979
|
+
};
|
|
980
|
+
return blockedOutcome(identity, taskFailure);
|
|
981
|
+
}
|
|
982
|
+
default:
|
|
983
|
+
return assertNever(failure);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
private resolveRetainedCandidate(
|
|
988
|
+
resultId: string,
|
|
989
|
+
operationRoot?: string,
|
|
990
|
+
): { retained: BoundRetainedResult; worker: TaskRecord } | undefined {
|
|
991
|
+
const candidate = this.results.get(resultId);
|
|
992
|
+
if (!candidate) return undefined;
|
|
993
|
+
if (!this.results.hasCandidateIdentity(resultId)) return undefined;
|
|
994
|
+
if (
|
|
995
|
+
candidate.stage !== "abel-implement" ||
|
|
996
|
+
candidate.canonicalRoot === undefined ||
|
|
997
|
+
candidate.changeId === undefined ||
|
|
998
|
+
candidate.taskId === undefined ||
|
|
999
|
+
candidate.originRequestId === undefined ||
|
|
1000
|
+
candidate.phase === undefined ||
|
|
1001
|
+
candidate.launchIndex === undefined
|
|
1002
|
+
) {
|
|
1003
|
+
throw new Error("retained candidate identity mismatch");
|
|
1004
|
+
}
|
|
1005
|
+
if (
|
|
1006
|
+
operationRoot !== undefined &&
|
|
1007
|
+
canonicalWorkspaceRoot(operationRoot) !== candidate.canonicalRoot
|
|
1008
|
+
) {
|
|
1009
|
+
throw new Error("retained candidate identity mismatch");
|
|
1010
|
+
}
|
|
1011
|
+
const worker = this.registry.get(
|
|
1012
|
+
taskRecordKey(
|
|
1013
|
+
candidate.canonicalRoot,
|
|
1014
|
+
candidate.changeId,
|
|
1015
|
+
candidate.taskId,
|
|
1016
|
+
),
|
|
221
1017
|
);
|
|
1018
|
+
if (!worker) throw new Error("retained candidate identity mismatch");
|
|
1019
|
+
if (
|
|
1020
|
+
worker.state.kind !== "candidate-pending" ||
|
|
1021
|
+
worker.state.resultId !== resultId ||
|
|
1022
|
+
worker.state.originRequestId !== candidate.originRequestId ||
|
|
1023
|
+
worker.state.phase !== candidate.phase ||
|
|
1024
|
+
worker.state.launchIndex !== candidate.launchIndex
|
|
1025
|
+
) {
|
|
1026
|
+
throw new Error("retained candidate identity mismatch");
|
|
1027
|
+
}
|
|
1028
|
+
const phase = worker.boundary.phases[worker.state.phase];
|
|
1029
|
+
if (!phase || !isSafeBound(candidate.snapshot)) {
|
|
1030
|
+
throw new Error("retained candidate identity mismatch");
|
|
1031
|
+
}
|
|
1032
|
+
const expected: RetainedCandidateFacts = {
|
|
1033
|
+
stage: "abel-implement",
|
|
1034
|
+
canonicalRoot: worker.workspaceRoot,
|
|
1035
|
+
root: worker.workspaceRoot,
|
|
1036
|
+
changeId: worker.boundary.changeId,
|
|
1037
|
+
taskId: worker.boundary.taskId,
|
|
1038
|
+
originRequestId: worker.state.originRequestId,
|
|
1039
|
+
phase: worker.state.phase,
|
|
1040
|
+
launchIndex: worker.state.launchIndex,
|
|
1041
|
+
writeSet: [...phase.write],
|
|
1042
|
+
approvedDependencies: [...worker.boundary.approvedDependencies],
|
|
1043
|
+
snapshot: candidate.snapshot,
|
|
1044
|
+
};
|
|
1045
|
+
return {
|
|
1046
|
+
retained: this.results.resolveIdentity(resultId, expected),
|
|
1047
|
+
worker,
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
private discardRetainedCandidate(resultId: string): boolean {
|
|
1052
|
+
return this.results.discard(resultId);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
private enqueueApply(
|
|
1056
|
+
root: string,
|
|
1057
|
+
operation: ImplementApplyOperation | { resultId: string },
|
|
1058
|
+
signal?: AbortSignal,
|
|
1059
|
+
): Promise<DispatchResult> {
|
|
1060
|
+
const id = operation.resultId;
|
|
1061
|
+
const resolved = this.resolveRetainedCandidate(id, root);
|
|
1062
|
+
const requestId = "requestId" in operation ? operation.requestId : null;
|
|
1063
|
+
if (requestId !== null && !resolved) {
|
|
1064
|
+
throw new Error("retained Implement result not found");
|
|
1065
|
+
}
|
|
1066
|
+
if (resolved && requestId === null) {
|
|
1067
|
+
throw new Error("Implement apply operation identity is missing");
|
|
1068
|
+
}
|
|
1069
|
+
const applyRoot = resolved?.retained.root ?? root;
|
|
1070
|
+
const run = this.enqueueParentApply(() =>
|
|
1071
|
+
applyRetainedPatch({ root: applyRoot, id, store: this.results, signal }),
|
|
1072
|
+
);
|
|
1073
|
+
const seq = ++this.applySeq;
|
|
1074
|
+
return run.then((result) => {
|
|
1075
|
+
if (result.ok) {
|
|
1076
|
+
const applied = { ...result.result, sequence: seq };
|
|
1077
|
+
if (resolved) {
|
|
1078
|
+
if (requestId === null)
|
|
1079
|
+
throw new Error("Implement apply operation identity is missing");
|
|
1080
|
+
const { retained: identity, worker } = resolved;
|
|
1081
|
+
if (worker.state.kind === "candidate-pending") {
|
|
1082
|
+
const readyPhase = nextDeclaredPhase(
|
|
1083
|
+
worker.boundary,
|
|
1084
|
+
identity.phase,
|
|
1085
|
+
);
|
|
1086
|
+
if (readyPhase) {
|
|
1087
|
+
worker.state = {
|
|
1088
|
+
kind: "ready",
|
|
1089
|
+
phase: readyPhase,
|
|
1090
|
+
launchIndex: 0,
|
|
1091
|
+
};
|
|
1092
|
+
return {
|
|
1093
|
+
kind: "applied",
|
|
1094
|
+
requestId,
|
|
1095
|
+
taskId: identity.taskId,
|
|
1096
|
+
phase: identity.phase as "red" | "green",
|
|
1097
|
+
readyPhase,
|
|
1098
|
+
result: applied,
|
|
1099
|
+
} satisfies ImplementOutcome;
|
|
1100
|
+
} else if (worker.boundary.agents.impact === "none") {
|
|
1101
|
+
worker.state = {
|
|
1102
|
+
kind: "completed",
|
|
1103
|
+
finalPhase: identity.phase as "green" | "refactor",
|
|
1104
|
+
};
|
|
1105
|
+
return {
|
|
1106
|
+
kind: "completed",
|
|
1107
|
+
requestId,
|
|
1108
|
+
taskId: identity.taskId,
|
|
1109
|
+
finalPhase: identity.phase as "green" | "refactor",
|
|
1110
|
+
result: applied,
|
|
1111
|
+
} satisfies ImplementOutcome;
|
|
1112
|
+
} else {
|
|
1113
|
+
worker.state = {
|
|
1114
|
+
kind: "agents-checkpoint-pending",
|
|
1115
|
+
finalPhase: identity.phase as "green" | "refactor",
|
|
1116
|
+
attemptIndex: 0,
|
|
1117
|
+
};
|
|
1118
|
+
return {
|
|
1119
|
+
kind: "checkpoint-required",
|
|
1120
|
+
requestId,
|
|
1121
|
+
taskId: identity.taskId,
|
|
1122
|
+
finalPhase: identity.phase as "green" | "refactor",
|
|
1123
|
+
result: applied,
|
|
1124
|
+
} satisfies ImplementOutcome;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
throw new Error("retained candidate is not pending");
|
|
1128
|
+
}
|
|
1129
|
+
return {
|
|
1130
|
+
ok: true,
|
|
1131
|
+
action: "apply",
|
|
1132
|
+
result: applied,
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
if (!resolved) {
|
|
1136
|
+
return {
|
|
1137
|
+
ok: false,
|
|
1138
|
+
error: "candidate application failed",
|
|
1139
|
+
failure: result.failure,
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
if (requestId === null)
|
|
1143
|
+
throw new Error("Implement apply operation identity is missing");
|
|
1144
|
+
return this.presentApplyFailure(
|
|
1145
|
+
requestId,
|
|
1146
|
+
id,
|
|
1147
|
+
resolved.retained,
|
|
1148
|
+
resolved.worker,
|
|
1149
|
+
result.failure,
|
|
1150
|
+
);
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
private enqueueDiscard(
|
|
1155
|
+
operation: ImplementDiscardOperation,
|
|
1156
|
+
): Promise<DispatchResult> {
|
|
1157
|
+
return this.enqueueParentApply(async () => {
|
|
1158
|
+
const resolved = this.resolveRetainedCandidate(operation.resultId);
|
|
1159
|
+
if (!resolved || !this.results.discard(operation.resultId)) {
|
|
1160
|
+
throw new Error("retained Implement result not found");
|
|
1161
|
+
}
|
|
1162
|
+
const { retained: identity, worker } = resolved;
|
|
1163
|
+
const outcomeIdentity = {
|
|
1164
|
+
requestId: operation.requestId,
|
|
1165
|
+
taskId: identity.taskId,
|
|
1166
|
+
phase: identity.phase,
|
|
1167
|
+
};
|
|
1168
|
+
const rejection = operation.rejection;
|
|
1169
|
+
if (rejection.kind === "approval-boundary") {
|
|
1170
|
+
worker.state = {
|
|
1171
|
+
kind: "blocked",
|
|
1172
|
+
phase: identity.phase,
|
|
1173
|
+
failure: structuredClone(rejection),
|
|
1174
|
+
};
|
|
1175
|
+
return blockedOutcome(outcomeIdentity, rejection);
|
|
1176
|
+
}
|
|
1177
|
+
if (identity.launchIndex === 0) {
|
|
1178
|
+
worker.state = {
|
|
1179
|
+
kind: "ready",
|
|
1180
|
+
phase: identity.phase,
|
|
1181
|
+
launchIndex: 1,
|
|
1182
|
+
correction: structuredClone(rejection),
|
|
1183
|
+
};
|
|
1184
|
+
return retryOutcome(outcomeIdentity, "worker", "artifact");
|
|
1185
|
+
}
|
|
1186
|
+
const failure: TaskFailure = {
|
|
1187
|
+
kind: "attempts-exhausted",
|
|
1188
|
+
cause: "artifact",
|
|
1189
|
+
};
|
|
1190
|
+
worker.state = {
|
|
1191
|
+
kind: "blocked",
|
|
1192
|
+
phase: identity.phase,
|
|
1193
|
+
failure: structuredClone(failure),
|
|
1194
|
+
};
|
|
1195
|
+
return blockedOutcome(outcomeIdentity, failure);
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
private enqueueParentApply<T>(operation: () => Promise<T>): Promise<T> {
|
|
1200
|
+
const run = this.applyTail.then(operation, operation);
|
|
222
1201
|
this.applyTail = run.then(
|
|
223
1202
|
() => undefined,
|
|
224
1203
|
() => undefined,
|
|
225
1204
|
);
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
1205
|
+
return run;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
private presentApplyFailure(
|
|
1209
|
+
requestId: string,
|
|
1210
|
+
resultId: string,
|
|
1211
|
+
identity: BoundRetainedResult,
|
|
1212
|
+
worker: TaskRecord,
|
|
1213
|
+
failure: CandidateFailure,
|
|
1214
|
+
): DispatchResult {
|
|
1215
|
+
const outcomeIdentity = {
|
|
1216
|
+
taskId: identity.taskId,
|
|
1217
|
+
requestId,
|
|
1218
|
+
phase: identity.phase,
|
|
1219
|
+
};
|
|
1220
|
+
switch (failure.kind) {
|
|
1221
|
+
case "environment": {
|
|
1222
|
+
this.results.discard(resultId);
|
|
1223
|
+
worker.state = {
|
|
1224
|
+
kind: "blocked",
|
|
1225
|
+
phase: identity.phase,
|
|
1226
|
+
failure: structuredClone(failure),
|
|
1227
|
+
};
|
|
1228
|
+
return blockedOutcome(outcomeIdentity, failure);
|
|
1229
|
+
}
|
|
1230
|
+
case "approval-boundary": {
|
|
1231
|
+
this.results.discard(resultId);
|
|
1232
|
+
worker.state = {
|
|
1233
|
+
kind: "blocked",
|
|
1234
|
+
phase: identity.phase,
|
|
1235
|
+
failure: structuredClone(failure),
|
|
1236
|
+
};
|
|
1237
|
+
return blockedOutcome(outcomeIdentity, failure);
|
|
1238
|
+
}
|
|
1239
|
+
case "stale": {
|
|
1240
|
+
this.results.discard(resultId);
|
|
1241
|
+
if (identity.launchIndex === 0) {
|
|
1242
|
+
worker.state = {
|
|
1243
|
+
kind: "ready",
|
|
1244
|
+
phase: identity.phase,
|
|
1245
|
+
launchIndex: 1,
|
|
1246
|
+
correction: structuredClone(failure),
|
|
1247
|
+
};
|
|
1248
|
+
} else {
|
|
1249
|
+
const taskFailure: TaskFailure = {
|
|
1250
|
+
kind: "attempts-exhausted",
|
|
1251
|
+
cause: "stale",
|
|
1252
|
+
};
|
|
1253
|
+
worker.state = {
|
|
1254
|
+
kind: "blocked",
|
|
1255
|
+
phase: identity.phase,
|
|
1256
|
+
failure: structuredClone(taskFailure),
|
|
1257
|
+
};
|
|
1258
|
+
return blockedOutcome(outcomeIdentity, taskFailure);
|
|
1259
|
+
}
|
|
1260
|
+
return retryOutcome(outcomeIdentity, "worker", "stale");
|
|
1261
|
+
}
|
|
1262
|
+
case "artifact": {
|
|
1263
|
+
this.results.discard(resultId);
|
|
1264
|
+
const correctionAvailable = identity.launchIndex === 0;
|
|
1265
|
+
if (!correctionAvailable) {
|
|
1266
|
+
const taskFailure: TaskFailure = {
|
|
1267
|
+
kind: "attempts-exhausted",
|
|
1268
|
+
cause: "artifact",
|
|
1269
|
+
};
|
|
1270
|
+
worker.state = {
|
|
1271
|
+
kind: "blocked",
|
|
1272
|
+
phase: identity.phase,
|
|
1273
|
+
failure: structuredClone(taskFailure),
|
|
1274
|
+
};
|
|
1275
|
+
return blockedOutcome(outcomeIdentity, taskFailure);
|
|
1276
|
+
}
|
|
1277
|
+
worker.state = {
|
|
1278
|
+
kind: "ready",
|
|
1279
|
+
phase: identity.phase,
|
|
1280
|
+
launchIndex: 1,
|
|
1281
|
+
correction: structuredClone(failure),
|
|
1282
|
+
};
|
|
1283
|
+
return retryOutcome(outcomeIdentity, "worker", "artifact");
|
|
1284
|
+
}
|
|
1285
|
+
case "cancelled":
|
|
1286
|
+
return cancelledOutcome(outcomeIdentity);
|
|
1287
|
+
case "result-limit":
|
|
1288
|
+
this.results.discard(resultId);
|
|
1289
|
+
worker.state = {
|
|
1290
|
+
kind: "blocked",
|
|
1291
|
+
phase: identity.phase,
|
|
1292
|
+
failure: structuredClone(failure),
|
|
1293
|
+
};
|
|
1294
|
+
return blockedOutcome(outcomeIdentity, failure);
|
|
1295
|
+
default:
|
|
1296
|
+
return assertNever(failure);
|
|
1297
|
+
}
|
|
234
1298
|
}
|
|
235
1299
|
|
|
236
1300
|
private async dispatchChild(
|
|
@@ -238,22 +1302,28 @@ export class Runtime {
|
|
|
238
1302
|
envelope: RequestEnvelope,
|
|
239
1303
|
ctx: RunContext,
|
|
240
1304
|
signal: AbortSignal,
|
|
241
|
-
|
|
1305
|
+
artifactRejection?: string,
|
|
1306
|
+
): Promise<ChildDispatchResult> {
|
|
242
1307
|
if (signal.aborted)
|
|
243
1308
|
return {
|
|
244
1309
|
...cancellationError(signal),
|
|
245
1310
|
failureKind: "cancelled",
|
|
246
1311
|
};
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
1312
|
+
const phase = await runtimeFromContext(
|
|
1313
|
+
ctx,
|
|
1314
|
+
this.parentPayloadBridge,
|
|
1315
|
+
signal,
|
|
1316
|
+
);
|
|
1317
|
+
if (!phase.ok) {
|
|
251
1318
|
return {
|
|
252
1319
|
ok: false,
|
|
253
|
-
error:
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
1320
|
+
error: phase.error,
|
|
1321
|
+
failure: phase.failure,
|
|
1322
|
+
failureKind:
|
|
1323
|
+
phase.failure.kind === "cancelled" ? "cancelled" : "failed",
|
|
1324
|
+
...(phase.failure.kind === "cancelled"
|
|
1325
|
+
? {}
|
|
1326
|
+
: { failureClass: phase.failure.kind }),
|
|
257
1327
|
};
|
|
258
1328
|
}
|
|
259
1329
|
if (signal.aborted)
|
|
@@ -264,11 +1334,28 @@ export class Runtime {
|
|
|
264
1334
|
const roots = envelope.roots.map(
|
|
265
1335
|
(root) => new URL(root, `file://${ctx.cwd}/`).pathname,
|
|
266
1336
|
);
|
|
1337
|
+
const phaseContract = {
|
|
1338
|
+
taskId: envelope.taskId ?? envelope.id,
|
|
1339
|
+
requestId: envelope.id,
|
|
1340
|
+
phase: envelope.phase,
|
|
1341
|
+
readSet: [...envelope.declared.read],
|
|
1342
|
+
writeSet: [...envelope.declared.write],
|
|
1343
|
+
verification: envelope.verification ?? null,
|
|
1344
|
+
agentsImpact: envelope.agentsImpact ?? "none",
|
|
1345
|
+
agentsTarget: envelope.agentsTarget ?? null,
|
|
1346
|
+
agentsManagedOnly: true,
|
|
1347
|
+
agentsWriteAllowed: false,
|
|
1348
|
+
impactClosure: envelope.impactClosure ?? null,
|
|
1349
|
+
};
|
|
267
1350
|
const systemPrompt = [
|
|
268
1351
|
agent.content,
|
|
269
1352
|
envelope.objective,
|
|
270
1353
|
envelope.context.agents,
|
|
271
1354
|
envelope.context.contract,
|
|
1355
|
+
`<phase-contract>${JSON.stringify(phaseContract)}</phase-contract>`,
|
|
1356
|
+
...(artifactRejection
|
|
1357
|
+
? [`<artifact-rejection>${artifactRejection}</artifact-rejection>`]
|
|
1358
|
+
: []),
|
|
272
1359
|
].join("\n\n");
|
|
273
1360
|
const child = await runChildSession({
|
|
274
1361
|
cwd: ctx.cwd,
|
|
@@ -276,10 +1363,14 @@ export class Runtime {
|
|
|
276
1363
|
model: phase.model,
|
|
277
1364
|
systemPrompt,
|
|
278
1365
|
requestId: envelope.id,
|
|
1366
|
+
taskId: envelope.taskId,
|
|
279
1367
|
role: envelope.role,
|
|
280
1368
|
phase: envelope.phase,
|
|
281
1369
|
output: envelope.output as "evidence" | "diff",
|
|
282
1370
|
roots,
|
|
1371
|
+
allowedPaths: [
|
|
1372
|
+
...new Set([...envelope.declared.read, ...envelope.declared.write]),
|
|
1373
|
+
],
|
|
283
1374
|
timeoutMs: LIMITS.phaseTimeoutMs,
|
|
284
1375
|
signal,
|
|
285
1376
|
});
|
|
@@ -287,8 +1378,15 @@ export class Runtime {
|
|
|
287
1378
|
return {
|
|
288
1379
|
...cancellationError(signal),
|
|
289
1380
|
failureKind: "cancelled",
|
|
1381
|
+
usage: child.usage,
|
|
1382
|
+
};
|
|
1383
|
+
if (!child.ok) {
|
|
1384
|
+
return {
|
|
1385
|
+
...child,
|
|
1386
|
+
failureKind: child.failureKind,
|
|
1387
|
+
failureClass: failureClassOf(child.failure),
|
|
290
1388
|
};
|
|
291
|
-
|
|
1389
|
+
}
|
|
292
1390
|
if (envelope.output === "evidence") {
|
|
293
1391
|
return {
|
|
294
1392
|
ok: true,
|
|
@@ -298,17 +1396,72 @@ export class Runtime {
|
|
|
298
1396
|
};
|
|
299
1397
|
}
|
|
300
1398
|
const diff = child.result as DiffResult;
|
|
1399
|
+
const requestSnapshot = isSafeBound(envelope.snapshot)
|
|
1400
|
+
? envelope.snapshot
|
|
1401
|
+
: {};
|
|
1402
|
+
let prepared: PreparedPreflight | null;
|
|
1403
|
+
try {
|
|
1404
|
+
prepared = preparePreflight(ctx.cwd, envelope, requestSnapshot);
|
|
1405
|
+
} catch (error) {
|
|
1406
|
+
if (!isFileSystemFailure(error)) throw error;
|
|
1407
|
+
return {
|
|
1408
|
+
ok: false,
|
|
1409
|
+
error: "candidate preflight inputs are unavailable",
|
|
1410
|
+
failure: { kind: "environment", code: "root-unavailable" },
|
|
1411
|
+
failureKind: "failed",
|
|
1412
|
+
failureClass: "environment",
|
|
1413
|
+
launchConsumed: true,
|
|
1414
|
+
usage: child.usage,
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
if (envelope.verification && !prepared) {
|
|
1418
|
+
return {
|
|
1419
|
+
ok: false,
|
|
1420
|
+
error: "candidate preflight inputs are unavailable",
|
|
1421
|
+
failure: { kind: "environment", code: "root-unavailable" },
|
|
1422
|
+
failureKind: "failed",
|
|
1423
|
+
failureClass: "environment",
|
|
1424
|
+
launchConsumed: true,
|
|
1425
|
+
usage: child.usage,
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
const task = this.taskRecords.get(envelope);
|
|
1429
|
+
const retainedRoot = task?.workspaceRoot ?? ctx.cwd;
|
|
1430
|
+
const retainedSnapshot =
|
|
1431
|
+
prepared?.snapshot ??
|
|
1432
|
+
(isSafeBound(envelope.snapshot)
|
|
1433
|
+
? mergeBounds(
|
|
1434
|
+
snapshotFiles(retainedRoot, envelope.declared.write),
|
|
1435
|
+
envelope.snapshot,
|
|
1436
|
+
)
|
|
1437
|
+
: undefined);
|
|
301
1438
|
const resultId = this.results.retain({
|
|
302
1439
|
diff: diff.diff,
|
|
303
1440
|
writeSet: envelope.declared.write,
|
|
304
|
-
|
|
1441
|
+
approvedDependencies: envelope.approvedDependencies ?? [],
|
|
1442
|
+
root: retainedRoot,
|
|
1443
|
+
...(task
|
|
1444
|
+
? {
|
|
1445
|
+
stage: "abel-implement" as const,
|
|
1446
|
+
canonicalRoot: task.workspaceRoot,
|
|
1447
|
+
changeId: task.boundary.changeId,
|
|
1448
|
+
taskId: task.boundary.taskId,
|
|
1449
|
+
originRequestId: envelope.id,
|
|
1450
|
+
phase: taskPhase(task),
|
|
1451
|
+
launchIndex: taskLaunchIndex(task),
|
|
1452
|
+
}
|
|
1453
|
+
: {}),
|
|
1454
|
+
...(prepared
|
|
1455
|
+
? {
|
|
1456
|
+
baseline: prepared.baseline,
|
|
1457
|
+
verification: envelope.verification,
|
|
1458
|
+
packageManifest: prepared.packageManifest,
|
|
1459
|
+
lockfile: prepared.lockfile,
|
|
1460
|
+
dependencyTarget: prepared.dependencyTarget,
|
|
1461
|
+
}
|
|
1462
|
+
: {}),
|
|
1463
|
+
...(retainedSnapshot ? { snapshot: retainedSnapshot } : {}),
|
|
305
1464
|
});
|
|
306
|
-
if (isSafeBound(envelope.snapshot)) {
|
|
307
|
-
const retained = this.results.get(resultId);
|
|
308
|
-
if (retained) {
|
|
309
|
-
retained.snapshot = mergeBounds(retained.snapshot, envelope.snapshot);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
1465
|
return {
|
|
313
1466
|
ok: true,
|
|
314
1467
|
action: "run",
|
|
@@ -324,28 +1477,136 @@ export class Runtime {
|
|
|
324
1477
|
signal?: AbortSignal,
|
|
325
1478
|
observer?: RuntimeActivityObserver,
|
|
326
1479
|
): Promise<DispatchResult> {
|
|
327
|
-
|
|
328
|
-
if (
|
|
1480
|
+
const implementRequest = targetsImplement(request);
|
|
1481
|
+
if (!ctx) {
|
|
1482
|
+
if (implementRequest) throw new Error("run requires extension context");
|
|
1483
|
+
return { ok: false, error: "run requires extension context" };
|
|
1484
|
+
}
|
|
329
1485
|
const validation = validateRequestEnvelope(request);
|
|
330
|
-
if (!validation.ok)
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
1486
|
+
if (!validation.ok) {
|
|
1487
|
+
if (implementRequest) {
|
|
1488
|
+
throw new Error(`Implement protocol error: ${validation.reason}`);
|
|
1489
|
+
}
|
|
1490
|
+
return { ok: false, error: validation.reason };
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
let envelope: RequestEnvelope;
|
|
1494
|
+
if (isImplementRunRequest(validation.value)) {
|
|
1495
|
+
const runRequest = validation.value;
|
|
1496
|
+
const attempt = runRequest.attempt;
|
|
1497
|
+
const workspaceRoot = canonicalWorkspaceRoot(ctx.cwd);
|
|
1498
|
+
if (
|
|
1499
|
+
runRequest.kind === "open-task" &&
|
|
1500
|
+
workspaceRoot &&
|
|
1501
|
+
this.registry.has(
|
|
1502
|
+
taskRecordKey(workspaceRoot, attempt.changeId, attempt.taskId),
|
|
1503
|
+
)
|
|
1504
|
+
) {
|
|
1505
|
+
throw new Error("duplicate task open protocol error");
|
|
1506
|
+
}
|
|
1507
|
+
if (runRequest.kind === "open-task" && signal?.aborted) {
|
|
1508
|
+
return cancelledOutcome({
|
|
1509
|
+
taskId: attempt.taskId,
|
|
1510
|
+
requestId: attempt.requestId,
|
|
1511
|
+
phase: attempt.phase,
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
if (!workspaceRoot) {
|
|
1515
|
+
throw new Error("implementation workspace is unavailable");
|
|
1516
|
+
}
|
|
1517
|
+
const key = taskRecordKey(
|
|
1518
|
+
workspaceRoot,
|
|
1519
|
+
attempt.changeId,
|
|
1520
|
+
attempt.taskId,
|
|
1521
|
+
);
|
|
1522
|
+
if (
|
|
1523
|
+
runRequest.kind === "open-task" &&
|
|
1524
|
+
this.registry
|
|
1525
|
+
.values()
|
|
1526
|
+
.some(
|
|
1527
|
+
(record) =>
|
|
1528
|
+
record.workspaceRoot === workspaceRoot &&
|
|
1529
|
+
record.state.kind !== "blocked" &&
|
|
1530
|
+
record.state.kind !== "completed" &&
|
|
1531
|
+
declarationsConflict(
|
|
1532
|
+
taskConflictOf(runRequest.boundary),
|
|
1533
|
+
record.conflict,
|
|
1534
|
+
),
|
|
1535
|
+
)
|
|
1536
|
+
) {
|
|
1537
|
+
return {
|
|
1538
|
+
kind: "deferred",
|
|
1539
|
+
taskId: attempt.taskId,
|
|
1540
|
+
requestId: attempt.requestId,
|
|
1541
|
+
reason: "task-conflict",
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
if (!ctx.model) {
|
|
1545
|
+
throw new Error("implementation model is unavailable");
|
|
1546
|
+
}
|
|
1547
|
+
const identity = workerIdentity(ctx.model);
|
|
1548
|
+
let record: TaskRecord;
|
|
1549
|
+
if (runRequest.kind === "open-task") {
|
|
1550
|
+
record = this.registry.open(
|
|
1551
|
+
runRequest.boundary,
|
|
1552
|
+
identity,
|
|
1553
|
+
workspaceRoot,
|
|
1554
|
+
attempt,
|
|
1555
|
+
);
|
|
1556
|
+
} else {
|
|
1557
|
+
const existing = this.registry.get(key);
|
|
1558
|
+
if (!existing) {
|
|
1559
|
+
throw new Error("task identity mismatch or task is not open");
|
|
1560
|
+
}
|
|
1561
|
+
if (existing.workerIdentity !== identity) {
|
|
1562
|
+
throw new Error("provider/model identity mismatch");
|
|
1563
|
+
}
|
|
1564
|
+
const attemptReason = validatePhaseAttemptAgainstBoundary(
|
|
1565
|
+
existing.boundary,
|
|
1566
|
+
attempt,
|
|
1567
|
+
);
|
|
1568
|
+
if (attemptReason !== null) throw new Error(attemptReason);
|
|
1569
|
+
assertAttemptAllowed(existing, attempt);
|
|
1570
|
+
record = existing;
|
|
1571
|
+
}
|
|
1572
|
+
if (record.state.kind === "blocked") {
|
|
1573
|
+
return blockedOutcome(
|
|
1574
|
+
{
|
|
1575
|
+
taskId: record.boundary.taskId,
|
|
1576
|
+
requestId: attempt.requestId,
|
|
1577
|
+
phase: record.state.phase,
|
|
1578
|
+
},
|
|
1579
|
+
record.state.failure,
|
|
1580
|
+
);
|
|
1581
|
+
}
|
|
1582
|
+
if (record.state.kind === "completed") {
|
|
1583
|
+
return {
|
|
1584
|
+
kind: "completed",
|
|
1585
|
+
taskId: record.boundary.taskId,
|
|
1586
|
+
requestId: attempt.requestId,
|
|
1587
|
+
finalPhase: record.state.finalPhase,
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
if (signal?.aborted) {
|
|
1591
|
+
return cancelledOutcome({
|
|
1592
|
+
taskId: record.boundary.taskId,
|
|
1593
|
+
requestId: attempt.requestId,
|
|
1594
|
+
phase: taskPhase(record),
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1597
|
+
envelope = deriveImplementEnvelope(record, attempt);
|
|
1598
|
+
this.taskRecords.set(envelope, record);
|
|
1599
|
+
} else {
|
|
1600
|
+
if (signal?.aborted) return cancellationError(signal);
|
|
1601
|
+
envelope = cloneLegacyEnvelope(validation.value);
|
|
1602
|
+
}
|
|
343
1603
|
const context: StoredRunContext = {
|
|
344
1604
|
ctx,
|
|
345
1605
|
observer,
|
|
346
1606
|
sequence: ++this.batchSeq,
|
|
347
1607
|
runningEmitted: false,
|
|
348
1608
|
terminalEmitted: false,
|
|
1609
|
+
usage: new UsageAggregator(),
|
|
349
1610
|
};
|
|
350
1611
|
this.runContexts.set(envelope, context);
|
|
351
1612
|
this.notify(envelope, context, "queued");
|
|
@@ -363,23 +1624,51 @@ export class Runtime {
|
|
|
363
1624
|
try {
|
|
364
1625
|
const outcome = await batch.result(envelope.id);
|
|
365
1626
|
if (outcome.status === "succeeded" && outcome.value) {
|
|
366
|
-
const internalResult = outcome.value.dispatch
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
1627
|
+
const internalResult = outcome.value.dispatch;
|
|
1628
|
+
const {
|
|
1629
|
+
failureKind,
|
|
1630
|
+
failureClass: _failureClass,
|
|
1631
|
+
launchConsumed: _launchConsumed,
|
|
1632
|
+
...publicResult
|
|
1633
|
+
} = internalResult;
|
|
1634
|
+
const dispatch = publicResult as DispatchResult;
|
|
1635
|
+
if (isWrappedDispatchResult(dispatch)) {
|
|
1636
|
+
if (!dispatch.ok) {
|
|
1637
|
+
const state: RuntimeActivityState = failureKind ?? "failed";
|
|
1638
|
+
this.notify(envelope, context, state);
|
|
1639
|
+
return dispatch;
|
|
1640
|
+
}
|
|
371
1641
|
this.notify(envelope, context, "completed");
|
|
372
|
-
return
|
|
1642
|
+
return dispatch;
|
|
373
1643
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
1644
|
+
this.notify(
|
|
1645
|
+
envelope,
|
|
1646
|
+
context,
|
|
1647
|
+
dispatch.kind === "cancelled" ? "cancelled" : "completed",
|
|
1648
|
+
);
|
|
1649
|
+
return dispatch;
|
|
377
1650
|
}
|
|
378
1651
|
const state: RuntimeActivityState =
|
|
379
1652
|
outcome.status === "cancelled" ? "cancelled" : "failed";
|
|
1653
|
+
const task = this.taskRecords.get(envelope);
|
|
1654
|
+
if (task) {
|
|
1655
|
+
this.notify(envelope, context, state);
|
|
1656
|
+
if (outcome.status === "cancelled") {
|
|
1657
|
+
return {
|
|
1658
|
+
...cancelledOutcome({
|
|
1659
|
+
taskId: task.boundary.taskId,
|
|
1660
|
+
requestId: envelope.id,
|
|
1661
|
+
phase: taskPhase(task),
|
|
1662
|
+
}),
|
|
1663
|
+
...this.usageMetadata(context),
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
throw new Error(outcome.error ?? `scheduled run ${outcome.status}`);
|
|
1667
|
+
}
|
|
380
1668
|
const terminal = {
|
|
381
1669
|
ok: false as const,
|
|
382
1670
|
error: outcome.error ?? `scheduled run ${outcome.status}`,
|
|
1671
|
+
...this.usageMetadata(context),
|
|
383
1672
|
};
|
|
384
1673
|
this.notify(envelope, context, state);
|
|
385
1674
|
return terminal;
|
|
@@ -432,115 +1721,256 @@ export class Runtime {
|
|
|
432
1721
|
|
|
433
1722
|
private async runScheduled(
|
|
434
1723
|
envelope: RequestEnvelope,
|
|
435
|
-
|
|
1724
|
+
context: StoredRunContext,
|
|
436
1725
|
signal: AbortSignal,
|
|
437
|
-
): Promise<
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
1726
|
+
): Promise<InternalDispatchResult> {
|
|
1727
|
+
const ctx = context.ctx;
|
|
1728
|
+
const task = this.taskRecords.get(envelope);
|
|
1729
|
+
if (!task && envelope.stage === "abel-implement") {
|
|
1730
|
+
throw new Error("implementation TaskRecord is unavailable");
|
|
1731
|
+
}
|
|
1732
|
+
if (signal.aborted) {
|
|
1733
|
+
if (task) {
|
|
1734
|
+
return cancelledOutcome({
|
|
1735
|
+
taskId: task.boundary.taskId,
|
|
1736
|
+
requestId: envelope.id,
|
|
1737
|
+
phase: taskPhase(task),
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
return { ...cancellationError(signal), failureKind: "cancelled" };
|
|
1741
|
+
}
|
|
443
1742
|
const agent = loadAgentDefinitions().find(
|
|
444
1743
|
(item) => item.role === envelope.role,
|
|
445
1744
|
);
|
|
446
|
-
if (!agent)
|
|
1745
|
+
if (!agent) {
|
|
1746
|
+
if (task) throw new Error("implementation agent definition is missing");
|
|
447
1747
|
return {
|
|
448
1748
|
ok: false,
|
|
449
|
-
error: "
|
|
1749
|
+
error: "agent definition is unavailable",
|
|
1750
|
+
failure: { kind: "environment", code: "root-unavailable" },
|
|
450
1751
|
failureKind: "failed",
|
|
1752
|
+
failureClass: "environment",
|
|
451
1753
|
};
|
|
452
|
-
|
|
1754
|
+
}
|
|
1755
|
+
if (!ctx.model) {
|
|
1756
|
+
if (task) throw new Error("implementation model is unavailable");
|
|
453
1757
|
return {
|
|
454
1758
|
ok: false,
|
|
455
|
-
error: "
|
|
1759
|
+
error: "phase runtime is unavailable",
|
|
1760
|
+
failure: {
|
|
1761
|
+
kind: "environment",
|
|
1762
|
+
code: "sandbox-runtime-unavailable",
|
|
1763
|
+
},
|
|
456
1764
|
failureKind: "failed",
|
|
1765
|
+
failureClass: "environment",
|
|
457
1766
|
};
|
|
458
|
-
const identity = workerIdentity(ctx.model);
|
|
459
|
-
const contract = contractOf(envelope);
|
|
460
|
-
const existing = this.registry.get(envelope.id);
|
|
461
|
-
if (existing) {
|
|
462
|
-
if (existing.identity !== identity) {
|
|
463
|
-
return {
|
|
464
|
-
ok: false,
|
|
465
|
-
error:
|
|
466
|
-
"blocked: logical Worker pinned provider/model identity differs",
|
|
467
|
-
failureKind: "failed",
|
|
468
|
-
};
|
|
469
|
-
}
|
|
470
|
-
if (!sameContract(existing.contract, contract)) {
|
|
471
|
-
return {
|
|
472
|
-
ok: false,
|
|
473
|
-
error:
|
|
474
|
-
"blocked: logical Worker contract changed for a recovery scope",
|
|
475
|
-
failureKind: "failed",
|
|
476
|
-
};
|
|
477
|
-
}
|
|
478
|
-
if (existing.redispatchUsed) {
|
|
479
|
-
return {
|
|
480
|
-
ok: false,
|
|
481
|
-
error: "blocked: mechanical redispatch is already exhausted",
|
|
482
|
-
failureKind: "failed",
|
|
483
|
-
};
|
|
484
|
-
}
|
|
485
|
-
} else {
|
|
486
|
-
this.registry.pin(contract, identity);
|
|
487
1767
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
1768
|
+
if (!task) {
|
|
1769
|
+
const result = await this.dispatchChild(agent, envelope, ctx, signal);
|
|
1770
|
+
this.captureUsage(context, "launch:0", result.usage);
|
|
1771
|
+
return this.withUsage(context, result);
|
|
1772
|
+
}
|
|
1773
|
+
if (task.workerIdentity !== workerIdentity(ctx.model)) {
|
|
1774
|
+
throw new Error("provider/model identity mismatch");
|
|
1775
|
+
}
|
|
1776
|
+
if (task.state.kind !== "ready") {
|
|
1777
|
+
throw new Error("implementation task is not launchable");
|
|
1778
|
+
}
|
|
1779
|
+
const phase = task.state.phase;
|
|
1780
|
+
const launchIndex = task.state.launchIndex;
|
|
1781
|
+
const correction = task.state.correction;
|
|
1782
|
+
const artifactRejection =
|
|
1783
|
+
correction?.kind === "artifact"
|
|
1784
|
+
? artifactCorrectionEvidence(correction)
|
|
1785
|
+
: undefined;
|
|
1786
|
+
const identity: OutcomeIdentity = {
|
|
1787
|
+
taskId: task.boundary.taskId,
|
|
1788
|
+
requestId: envelope.id,
|
|
1789
|
+
phase,
|
|
1790
|
+
};
|
|
1791
|
+
const block = (failure: TaskFailure): InternalDispatchResult => {
|
|
1792
|
+
task.state = {
|
|
1793
|
+
kind: "blocked",
|
|
1794
|
+
phase,
|
|
1795
|
+
failure: structuredClone(failure),
|
|
1796
|
+
};
|
|
1797
|
+
return blockedOutcome(identity, failure);
|
|
1798
|
+
};
|
|
1799
|
+
const candidate = (
|
|
1800
|
+
result: Extract<ChildDispatchResult, { ok: true }>,
|
|
1801
|
+
): InternalDispatchResult => {
|
|
1802
|
+
if (typeof result.resultId !== "string" || result.result === undefined) {
|
|
1803
|
+
throw new Error("implementation candidate result is incomplete");
|
|
1804
|
+
}
|
|
491
1805
|
return {
|
|
492
|
-
|
|
493
|
-
|
|
1806
|
+
kind: "candidate",
|
|
1807
|
+
...identity,
|
|
1808
|
+
resultId: result.resultId,
|
|
1809
|
+
result: result.result as DiffResult,
|
|
494
1810
|
};
|
|
1811
|
+
};
|
|
1812
|
+
const finish = (result: InternalDispatchResult) =>
|
|
1813
|
+
this.withUsage(context, result);
|
|
1814
|
+
|
|
1815
|
+
const first = await this.dispatchChild(
|
|
1816
|
+
agent,
|
|
1817
|
+
envelope,
|
|
1818
|
+
ctx,
|
|
1819
|
+
signal,
|
|
1820
|
+
artifactRejection,
|
|
1821
|
+
);
|
|
1822
|
+
this.captureUsage(context, "launch:0", first.usage);
|
|
1823
|
+
if (signal.aborted) {
|
|
1824
|
+
if (first.ok && typeof first.resultId === "string") {
|
|
1825
|
+
this.results.discard(first.resultId);
|
|
1826
|
+
}
|
|
1827
|
+
return finish(cancelledOutcome(identity));
|
|
1828
|
+
}
|
|
495
1829
|
if (first.ok) {
|
|
496
|
-
|
|
497
|
-
return first;
|
|
1830
|
+
return finish(candidate(first));
|
|
498
1831
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
error: "blocked: mechanical redispatch failed and is exhausted",
|
|
503
|
-
failureKind: first.failureKind ?? "failed",
|
|
504
|
-
};
|
|
1832
|
+
const firstFailure = childFailureOf(first);
|
|
1833
|
+
if (firstFailure.kind === "cancelled") {
|
|
1834
|
+
return finish(cancelledOutcome(identity));
|
|
505
1835
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
1836
|
+
const firstTerminal = terminalTaskFailure(firstFailure);
|
|
1837
|
+
if (firstTerminal) return finish(block(firstTerminal));
|
|
1838
|
+
if (launchIndex === 1) {
|
|
1839
|
+
return finish(
|
|
1840
|
+
block({
|
|
1841
|
+
kind: "attempts-exhausted",
|
|
1842
|
+
cause: attemptFailureCause(firstFailure),
|
|
1843
|
+
}),
|
|
1844
|
+
);
|
|
1845
|
+
}
|
|
1846
|
+
if (firstFailure.kind === "artifact" || firstFailure.kind === "stale") {
|
|
1847
|
+
task.state = {
|
|
1848
|
+
kind: "ready",
|
|
1849
|
+
phase,
|
|
1850
|
+
launchIndex: 1,
|
|
1851
|
+
correction: structuredClone(firstFailure),
|
|
511
1852
|
};
|
|
1853
|
+
return finish(retryOutcome(identity, "worker", firstFailure.kind));
|
|
512
1854
|
}
|
|
513
|
-
|
|
1855
|
+
|
|
1856
|
+
task.state = {
|
|
1857
|
+
kind: "ready",
|
|
1858
|
+
phase,
|
|
1859
|
+
launchIndex: 1,
|
|
1860
|
+
};
|
|
514
1861
|
const second = await this.dispatchChild(agent, envelope, ctx, signal);
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
1862
|
+
this.captureUsage(context, "launch:1", second.usage);
|
|
1863
|
+
if (signal.aborted) {
|
|
1864
|
+
if (second.ok && typeof second.resultId === "string") {
|
|
1865
|
+
this.results.discard(second.resultId);
|
|
1866
|
+
}
|
|
1867
|
+
return finish(cancelledOutcome(identity));
|
|
1868
|
+
}
|
|
1869
|
+
if (second.ok) {
|
|
1870
|
+
return finish(candidate(second));
|
|
1871
|
+
}
|
|
1872
|
+
const secondFailure = childFailureOf(second);
|
|
1873
|
+
if (secondFailure.kind === "cancelled") {
|
|
1874
|
+
return finish(cancelledOutcome(identity));
|
|
1875
|
+
}
|
|
1876
|
+
const secondTerminal = terminalTaskFailure(secondFailure);
|
|
1877
|
+
if (secondTerminal) return finish(block(secondTerminal));
|
|
1878
|
+
return finish(
|
|
1879
|
+
block({
|
|
1880
|
+
kind: "attempts-exhausted",
|
|
1881
|
+
cause: attemptFailureCause(secondFailure),
|
|
1882
|
+
}),
|
|
1883
|
+
);
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
private captureUsage(
|
|
1887
|
+
context: StoredRunContext,
|
|
1888
|
+
id: string,
|
|
1889
|
+
usage: Usage | undefined,
|
|
1890
|
+
): void {
|
|
1891
|
+
if (usage) context.usage.add(id, usage);
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
private usageMetadata(context: StoredRunContext): { usage?: Usage } {
|
|
1895
|
+
return context.usage.hasUsage() ? { usage: context.usage.total() } : {};
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
private withUsage<T extends InternalDispatchResult>(
|
|
1899
|
+
context: StoredRunContext,
|
|
1900
|
+
result: T,
|
|
1901
|
+
): T {
|
|
1902
|
+
return {
|
|
1903
|
+
...result,
|
|
1904
|
+
...this.usageMetadata(context),
|
|
1905
|
+
};
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
private rememberRetainedResult(
|
|
1909
|
+
result: Extract<ImplementOutcome, { kind: "candidate" }>,
|
|
1910
|
+
envelope: RequestEnvelope,
|
|
1911
|
+
task: TaskRecord,
|
|
1912
|
+
): void {
|
|
1913
|
+
if (task.state.kind !== "ready") {
|
|
1914
|
+
throw new Error("retained candidate identity mismatch");
|
|
1915
|
+
}
|
|
1916
|
+
const phaseState = task.state;
|
|
1917
|
+
if (
|
|
1918
|
+
result.requestId !== envelope.id ||
|
|
1919
|
+
result.taskId !== task.boundary.taskId ||
|
|
1920
|
+
result.phase !== phaseState.phase
|
|
1921
|
+
) {
|
|
1922
|
+
throw new Error("retained candidate identity mismatch");
|
|
526
1923
|
}
|
|
527
|
-
|
|
1924
|
+
const phase = task.boundary.phases[phaseState.phase];
|
|
1925
|
+
if (!phase || !isSafeBound(envelope.snapshot)) {
|
|
1926
|
+
throw new Error("retained candidate identity mismatch");
|
|
1927
|
+
}
|
|
1928
|
+
const identity: RetainedCandidateIdentity = {
|
|
1929
|
+
stage: "abel-implement",
|
|
1930
|
+
canonicalRoot: task.workspaceRoot,
|
|
1931
|
+
root: task.workspaceRoot,
|
|
1932
|
+
changeId: task.boundary.changeId,
|
|
1933
|
+
taskId: task.boundary.taskId,
|
|
1934
|
+
originRequestId: envelope.id,
|
|
1935
|
+
phase: phaseState.phase,
|
|
1936
|
+
launchIndex: phaseState.launchIndex,
|
|
1937
|
+
};
|
|
1938
|
+
if (!this.results.get(result.resultId)) {
|
|
1939
|
+
throw new Error("retained candidate is unavailable");
|
|
1940
|
+
}
|
|
1941
|
+
this.results.bindIdentity(result.resultId, identity);
|
|
1942
|
+
this.results.resolveIdentity(result.resultId, {
|
|
1943
|
+
...identity,
|
|
1944
|
+
writeSet: [...phase.write],
|
|
1945
|
+
approvedDependencies: [...task.boundary.approvedDependencies],
|
|
1946
|
+
snapshot: envelope.snapshot,
|
|
1947
|
+
});
|
|
1948
|
+
task.state = {
|
|
1949
|
+
kind: "candidate-pending",
|
|
1950
|
+
phase: phaseState.phase,
|
|
1951
|
+
launchIndex: phaseState.launchIndex,
|
|
1952
|
+
originRequestId: envelope.id,
|
|
1953
|
+
resultId: result.resultId,
|
|
1954
|
+
};
|
|
528
1955
|
}
|
|
529
1956
|
|
|
530
1957
|
validateRequest(
|
|
531
1958
|
envelope: unknown,
|
|
532
|
-
): { ok: true; value:
|
|
1959
|
+
): { ok: true; value: RunRequest } | { ok: false; reason: string } {
|
|
533
1960
|
return validateRequestEnvelope(envelope);
|
|
534
1961
|
}
|
|
535
1962
|
|
|
536
1963
|
async drain(): Promise<void> {
|
|
537
|
-
|
|
1964
|
+
this.activation.drain();
|
|
1965
|
+
const scheduled = this.scheduler.cancelAll();
|
|
1966
|
+
const applied = this.applyTail;
|
|
1967
|
+
await Promise.all([scheduled, applied]);
|
|
1968
|
+
this.parentPayloadBridge?.clear();
|
|
538
1969
|
drainStage({
|
|
539
1970
|
results: this.results,
|
|
540
1971
|
registry: this.registry,
|
|
541
1972
|
activation: this.activation,
|
|
542
1973
|
});
|
|
543
|
-
await settled;
|
|
544
1974
|
}
|
|
545
1975
|
|
|
546
1976
|
get state(): ActivationState {
|