@abelxiaoxing/cadence 1.0.1 → 1.0.2
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/package.json +1 -1
- package/prompts/abel-implement.md +13 -2
- package/skills/abel-workflow/SKILL.md +7 -1
- package/src/candidate-preflight.ts +1163 -0
- package/src/child-session.ts +46 -5
- package/src/contracts.ts +379 -48
- package/src/file-snapshot.ts +62 -8
- package/src/index.ts +55 -10
- package/src/parent-payload-bridge.ts +442 -0
- package/src/parent-provider.ts +91 -12
- package/src/patch.ts +73 -3
- package/src/result-store.ts +40 -3
- package/src/runtime.ts +775 -66
- package/src/scheduler.ts +1 -0
- package/src/scoped-tools.ts +27 -6
- package/src/submit-tool.ts +3 -1
- package/src/worker.ts +136 -26
package/src/runtime.ts
CHANGED
|
@@ -1,29 +1,199 @@
|
|
|
1
|
+
import { lstatSync, readdirSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
1
3
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
4
|
import { Activation, type ActivationState } from "./activation.ts";
|
|
3
5
|
import { loadAgentDefinitions } from "./agent-registry.ts";
|
|
6
|
+
import type { BaselineEntry } from "./candidate-preflight.ts";
|
|
4
7
|
import { runChildSession } from "./child-session.ts";
|
|
5
8
|
import {
|
|
6
9
|
ACTIONS,
|
|
7
10
|
type DiffResult,
|
|
8
11
|
LIMITS,
|
|
12
|
+
type RecoveryCode,
|
|
13
|
+
type RecoveryNext,
|
|
14
|
+
type RecoveryRecord,
|
|
9
15
|
type RequestEnvelope,
|
|
10
16
|
validateRequestEnvelope,
|
|
11
17
|
} from "./contracts.ts";
|
|
12
18
|
import { drainStage } from "./drain.ts";
|
|
13
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
type Bound,
|
|
21
|
+
type DirBound,
|
|
22
|
+
type FileBound,
|
|
23
|
+
isCurrent,
|
|
24
|
+
mergeBounds,
|
|
25
|
+
snapshotDirManifest,
|
|
26
|
+
snapshotFile,
|
|
27
|
+
snapshotFiles,
|
|
28
|
+
} from "./file-snapshot.ts";
|
|
29
|
+
import type { ParentPayloadBridge } from "./parent-payload-bridge.ts";
|
|
14
30
|
import { runtimeFromContext } from "./parent-provider.ts";
|
|
15
31
|
import { applyRetainedPatch } from "./patch.ts";
|
|
16
32
|
import { ResultStore } from "./result-store.ts";
|
|
17
33
|
import { Scheduler } from "./scheduler.ts";
|
|
18
34
|
import {
|
|
19
35
|
contractOf,
|
|
36
|
+
type LogicalWorker,
|
|
20
37
|
sameContract,
|
|
38
|
+
samePhaseContract,
|
|
21
39
|
WorkerRegistry,
|
|
22
40
|
workerIdentity,
|
|
23
41
|
} from "./worker.ts";
|
|
24
42
|
|
|
25
43
|
const SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
26
44
|
|
|
45
|
+
interface PreparedPreflight {
|
|
46
|
+
snapshot: Bound;
|
|
47
|
+
baseline: BaselineEntry[];
|
|
48
|
+
packageManifest: FileBound;
|
|
49
|
+
lockfile: FileBound;
|
|
50
|
+
dependencyTarget: FileBound | DirBound;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function addBaselineFile(
|
|
54
|
+
root: string,
|
|
55
|
+
relative: string,
|
|
56
|
+
executable: boolean,
|
|
57
|
+
entries: Map<string, BaselineEntry>,
|
|
58
|
+
observed: Bound,
|
|
59
|
+
): boolean {
|
|
60
|
+
const bound = snapshotFile(root, relative);
|
|
61
|
+
if (!bound) return false;
|
|
62
|
+
observed[relative] = bound;
|
|
63
|
+
entries.set(relative, {
|
|
64
|
+
path: relative,
|
|
65
|
+
kind: "file",
|
|
66
|
+
sha256: bound.sha256,
|
|
67
|
+
bytes: bound.bytes,
|
|
68
|
+
executable,
|
|
69
|
+
});
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function addDirectoryFiles(
|
|
74
|
+
root: string,
|
|
75
|
+
relative: string,
|
|
76
|
+
entries: Map<string, BaselineEntry>,
|
|
77
|
+
observed: Bound,
|
|
78
|
+
): boolean {
|
|
79
|
+
const directory = path.resolve(root, relative);
|
|
80
|
+
for (const name of readdirSync(directory).sort()) {
|
|
81
|
+
const child = path.posix.join(relative, name);
|
|
82
|
+
const stat = lstatSync(path.resolve(root, child));
|
|
83
|
+
if (stat.isSymbolicLink()) return false;
|
|
84
|
+
if (stat.isDirectory()) {
|
|
85
|
+
if (!addDirectoryFiles(root, child, entries, observed)) return false;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (!stat.isFile()) return false;
|
|
89
|
+
if (
|
|
90
|
+
!addBaselineFile(
|
|
91
|
+
root,
|
|
92
|
+
child,
|
|
93
|
+
Boolean(stat.mode & 0o111),
|
|
94
|
+
entries,
|
|
95
|
+
observed,
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function preparePreflight(
|
|
104
|
+
root: string,
|
|
105
|
+
envelope: RequestEnvelope,
|
|
106
|
+
requestSnapshot: Bound,
|
|
107
|
+
): PreparedPreflight | null {
|
|
108
|
+
if (!envelope.verification) return null;
|
|
109
|
+
const packageManifest = snapshotFile(root, "package.json");
|
|
110
|
+
const lockPath = [
|
|
111
|
+
"bun.lock",
|
|
112
|
+
"bun.lockb",
|
|
113
|
+
"package-lock.json",
|
|
114
|
+
"pnpm-lock.yaml",
|
|
115
|
+
"yarn.lock",
|
|
116
|
+
].find((candidate) => snapshotFile(root, candidate) !== null);
|
|
117
|
+
const lockfile = lockPath ? snapshotFile(root, lockPath) : null;
|
|
118
|
+
const dependencyTarget =
|
|
119
|
+
snapshotDirManifest(root, "node_modules") ??
|
|
120
|
+
snapshotFile(root, "node_modules");
|
|
121
|
+
if (!packageManifest || !lockPath || !lockfile || !dependencyTarget)
|
|
122
|
+
return null;
|
|
123
|
+
if (!isCurrent(root, requestSnapshot)) {
|
|
124
|
+
return {
|
|
125
|
+
snapshot: mergeBounds(
|
|
126
|
+
snapshotFiles(root, ["package.json", lockPath]),
|
|
127
|
+
{ node_modules: dependencyTarget },
|
|
128
|
+
requestSnapshot,
|
|
129
|
+
),
|
|
130
|
+
baseline: [],
|
|
131
|
+
packageManifest,
|
|
132
|
+
lockfile,
|
|
133
|
+
dependencyTarget,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const paths = new Set([
|
|
138
|
+
...Object.keys(requestSnapshot),
|
|
139
|
+
...envelope.declared.read,
|
|
140
|
+
...envelope.declared.write,
|
|
141
|
+
"package.json",
|
|
142
|
+
lockPath,
|
|
143
|
+
...envelope.verification.argv.slice(3),
|
|
144
|
+
]);
|
|
145
|
+
const entries = new Map<string, BaselineEntry>();
|
|
146
|
+
const observed: Bound = {};
|
|
147
|
+
for (const relative of [...paths].sort()) {
|
|
148
|
+
const absolute = path.resolve(root, relative);
|
|
149
|
+
if (
|
|
150
|
+
absolute === root ||
|
|
151
|
+
!absolute.startsWith(`${path.resolve(root)}${path.sep}`)
|
|
152
|
+
) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const stat = lstatSync(absolute, { throwIfNoEntry: false });
|
|
156
|
+
if (!stat) {
|
|
157
|
+
observed[relative] = { kind: "absent", absent: true };
|
|
158
|
+
entries.set(relative, { path: relative, kind: "deleted" });
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (stat.isDirectory()) {
|
|
162
|
+
if (relative === "node_modules") continue;
|
|
163
|
+
try {
|
|
164
|
+
if (!addDirectoryFiles(root, relative, entries, observed)) return null;
|
|
165
|
+
} catch {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (!stat.isFile() || stat.isSymbolicLink()) return null;
|
|
171
|
+
if (
|
|
172
|
+
!addBaselineFile(
|
|
173
|
+
root,
|
|
174
|
+
relative,
|
|
175
|
+
Boolean(stat.mode & 0o111),
|
|
176
|
+
entries,
|
|
177
|
+
observed,
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
const baseline = [...entries.values()].sort((left, right) =>
|
|
183
|
+
left.path.localeCompare(right.path),
|
|
184
|
+
);
|
|
185
|
+
const dependencyBound: Bound = {
|
|
186
|
+
node_modules: dependencyTarget,
|
|
187
|
+
};
|
|
188
|
+
return {
|
|
189
|
+
snapshot: mergeBounds(observed, dependencyBound, requestSnapshot),
|
|
190
|
+
baseline,
|
|
191
|
+
packageManifest,
|
|
192
|
+
lockfile,
|
|
193
|
+
dependencyTarget,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
27
197
|
function cancellationError(signal: AbortSignal): { ok: false; error: string } {
|
|
28
198
|
const reason = signal.reason;
|
|
29
199
|
return {
|
|
@@ -37,6 +207,64 @@ function cancellationError(signal: AbortSignal): { ok: false; error: string } {
|
|
|
37
207
|
};
|
|
38
208
|
}
|
|
39
209
|
|
|
210
|
+
type FailureClass = "transport" | "artifact" | "environment";
|
|
211
|
+
|
|
212
|
+
type InternalDispatchResult = DispatchResult & {
|
|
213
|
+
failureKind?: RunFailureKind;
|
|
214
|
+
failureClass?: FailureClass;
|
|
215
|
+
launchConsumed?: true;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
interface RecoveryIdentity {
|
|
219
|
+
taskId?: string;
|
|
220
|
+
id: string;
|
|
221
|
+
phase: string;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function recoveryFailure(
|
|
225
|
+
identity: RecoveryIdentity,
|
|
226
|
+
code: RecoveryCode,
|
|
227
|
+
next: RecoveryNext,
|
|
228
|
+
launchIndex: 0 | 1,
|
|
229
|
+
failureKind: RunFailureKind = "failed",
|
|
230
|
+
detail = "branch recovery is required",
|
|
231
|
+
): InternalDispatchResult {
|
|
232
|
+
const recovery: RecoveryRecord = {
|
|
233
|
+
code,
|
|
234
|
+
taskId: identity.taskId ?? identity.id,
|
|
235
|
+
requestId: identity.id,
|
|
236
|
+
phase: identity.phase,
|
|
237
|
+
launchIndex,
|
|
238
|
+
branchBlocked: true,
|
|
239
|
+
dependentsBlocked: true,
|
|
240
|
+
partialResultUsable: false,
|
|
241
|
+
independentResultsPreserved: true,
|
|
242
|
+
next,
|
|
243
|
+
};
|
|
244
|
+
return {
|
|
245
|
+
ok: false,
|
|
246
|
+
error: `${code}: ${detail}`,
|
|
247
|
+
recovery,
|
|
248
|
+
failureKind,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function normalizedArtifactRejection(error: string): string {
|
|
253
|
+
const preflight = /candidate preflight rejected: artifact:([a-z0-9-]+)/i.exec(
|
|
254
|
+
error,
|
|
255
|
+
);
|
|
256
|
+
return preflight
|
|
257
|
+
? `candidate-preflight:${preflight[1].toLowerCase()}`
|
|
258
|
+
: "generated-artifact-rejection";
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
interface RetainedRunIdentity {
|
|
262
|
+
taskId: string;
|
|
263
|
+
requestId: string;
|
|
264
|
+
phase: string;
|
|
265
|
+
launchIndex: 0 | 1;
|
|
266
|
+
}
|
|
267
|
+
|
|
40
268
|
export type RunFailureKind = "failed" | "cancelled" | "timed-out";
|
|
41
269
|
export type RuntimeFailureReason =
|
|
42
270
|
| "subagent failed"
|
|
@@ -123,7 +351,12 @@ export type DispatchResult =
|
|
|
123
351
|
resultId?: string;
|
|
124
352
|
usage?: unknown;
|
|
125
353
|
}
|
|
126
|
-
| {
|
|
354
|
+
| {
|
|
355
|
+
ok: false;
|
|
356
|
+
notReady?: true;
|
|
357
|
+
error: string;
|
|
358
|
+
recovery?: RecoveryRecord;
|
|
359
|
+
};
|
|
127
360
|
|
|
128
361
|
export interface ApplyResult {
|
|
129
362
|
targets: string[];
|
|
@@ -134,6 +367,7 @@ export interface ApplyResult {
|
|
|
134
367
|
|
|
135
368
|
export interface RuntimeOptions {
|
|
136
369
|
activation?: Activation;
|
|
370
|
+
parentPayloadBridge: ParentPayloadBridge;
|
|
137
371
|
}
|
|
138
372
|
|
|
139
373
|
type RunContext = Pick<ExtensionContext, "cwd" | "model" | "modelRegistry">;
|
|
@@ -153,15 +387,18 @@ export class Runtime {
|
|
|
153
387
|
private applyTail: Promise<void> = Promise.resolve();
|
|
154
388
|
private applySeq = 0;
|
|
155
389
|
private batchSeq = 0;
|
|
390
|
+
private readonly parentPayloadBridge: ParentPayloadBridge;
|
|
156
391
|
private readonly registry = new WorkerRegistry();
|
|
392
|
+
private readonly retainedRuns = new Map<string, RetainedRunIdentity>();
|
|
157
393
|
private readonly runContexts = new WeakMap<
|
|
158
394
|
RequestEnvelope,
|
|
159
395
|
StoredRunContext
|
|
160
396
|
>();
|
|
161
397
|
private readonly scheduler: Scheduler<ScheduledRunResult>;
|
|
162
398
|
|
|
163
|
-
constructor(opts: RuntimeOptions
|
|
399
|
+
constructor(opts: RuntimeOptions) {
|
|
164
400
|
this.activation = opts.activation ?? new Activation();
|
|
401
|
+
this.parentPayloadBridge = opts.parentPayloadBridge;
|
|
165
402
|
this.scheduler = new Scheduler({
|
|
166
403
|
limit: LIMITS.maxActiveChildSessions,
|
|
167
404
|
execute: async (request, signal) => {
|
|
@@ -197,12 +434,12 @@ export class Runtime {
|
|
|
197
434
|
if (action === "apply") {
|
|
198
435
|
if (!ctx || !params.resultId)
|
|
199
436
|
return { ok: false, error: "apply requires context and resultId" };
|
|
200
|
-
return this.enqueueApply(ctx.cwd, params.resultId);
|
|
437
|
+
return this.enqueueApply(ctx.cwd, params.resultId, signal);
|
|
201
438
|
}
|
|
202
439
|
if (action === "discard") {
|
|
203
440
|
if (!params.resultId)
|
|
204
441
|
return { ok: false, error: "discard requires resultId" };
|
|
205
|
-
return this.
|
|
442
|
+
return this.discardRetainedCandidate(params.resultId)
|
|
206
443
|
? { ok: true, action }
|
|
207
444
|
: { ok: false, error: "retained result not found" };
|
|
208
445
|
}
|
|
@@ -214,10 +451,36 @@ export class Runtime {
|
|
|
214
451
|
return { ok: true, action: "finish" };
|
|
215
452
|
}
|
|
216
453
|
|
|
217
|
-
private
|
|
454
|
+
private discardRetainedCandidate(resultId: string): boolean {
|
|
455
|
+
const identity = this.retainedRuns.get(resultId);
|
|
456
|
+
if (!this.results.discard(resultId)) return false;
|
|
457
|
+
this.retainedRuns.delete(resultId);
|
|
458
|
+
if (!identity) return true;
|
|
459
|
+
const worker = this.registry.get(identity.taskId);
|
|
460
|
+
if (
|
|
461
|
+
worker?.currentPhase.requestId !== identity.requestId ||
|
|
462
|
+
worker.currentPhase.phase !== identity.phase
|
|
463
|
+
) {
|
|
464
|
+
return true;
|
|
465
|
+
}
|
|
466
|
+
if (identity.launchIndex === 0) {
|
|
467
|
+
worker.currentPhase.correctionIndex = 1;
|
|
468
|
+
worker.state = { kind: "ready" };
|
|
469
|
+
} else {
|
|
470
|
+
worker.state = { kind: "blocked", reason: "mechanical" };
|
|
471
|
+
}
|
|
472
|
+
return true;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
private enqueueApply(
|
|
476
|
+
root: string,
|
|
477
|
+
id: string,
|
|
478
|
+
signal?: AbortSignal,
|
|
479
|
+
): Promise<DispatchResult> {
|
|
480
|
+
const retainedIdentity = this.retainedRuns.get(id);
|
|
218
481
|
const run = this.applyTail.then(
|
|
219
|
-
() => applyRetainedPatch({ root, id, store: this.results }),
|
|
220
|
-
() => applyRetainedPatch({ root, id, store: this.results }),
|
|
482
|
+
() => applyRetainedPatch({ root, id, store: this.results, signal }),
|
|
483
|
+
() => applyRetainedPatch({ root, id, store: this.results, signal }),
|
|
221
484
|
);
|
|
222
485
|
this.applyTail = run.then(
|
|
223
486
|
() => undefined,
|
|
@@ -225,11 +488,143 @@ export class Runtime {
|
|
|
225
488
|
);
|
|
226
489
|
const seq = ++this.applySeq;
|
|
227
490
|
return run.then(
|
|
228
|
-
(result) =>
|
|
229
|
-
result.ok
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
491
|
+
(result) => {
|
|
492
|
+
if (result.ok) {
|
|
493
|
+
this.retainedRuns.delete(id);
|
|
494
|
+
if (retainedIdentity) {
|
|
495
|
+
const worker = this.registry.get(retainedIdentity.taskId);
|
|
496
|
+
if (
|
|
497
|
+
worker?.currentPhase.requestId === retainedIdentity.requestId &&
|
|
498
|
+
worker.currentPhase.phase === retainedIdentity.phase
|
|
499
|
+
) {
|
|
500
|
+
worker.state = { kind: "phase-applied" };
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return {
|
|
504
|
+
ok: true,
|
|
505
|
+
action: "apply",
|
|
506
|
+
result: { ...result, sequence: seq },
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
return this.presentApplyFailure(
|
|
510
|
+
id,
|
|
511
|
+
retainedIdentity,
|
|
512
|
+
result.error,
|
|
513
|
+
signal,
|
|
514
|
+
);
|
|
515
|
+
},
|
|
516
|
+
(error) =>
|
|
517
|
+
this.presentApplyFailure(
|
|
518
|
+
id,
|
|
519
|
+
retainedIdentity,
|
|
520
|
+
(error as Error).message,
|
|
521
|
+
signal,
|
|
522
|
+
),
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
private presentApplyFailure(
|
|
527
|
+
resultId: string,
|
|
528
|
+
identity: RetainedRunIdentity | undefined,
|
|
529
|
+
error: string,
|
|
530
|
+
signal?: AbortSignal,
|
|
531
|
+
): DispatchResult {
|
|
532
|
+
if (signal?.aborted) return cancellationError(signal);
|
|
533
|
+
if (!identity) return { ok: false, error };
|
|
534
|
+
if (/cancelled/i.test(error)) {
|
|
535
|
+
return { ok: false, error: "candidate application cancelled" };
|
|
536
|
+
}
|
|
537
|
+
if (/candidate preflight rejected: environment:/i.test(error)) {
|
|
538
|
+
return recoveryFailure(
|
|
539
|
+
{
|
|
540
|
+
taskId: identity.taskId,
|
|
541
|
+
id: identity.requestId,
|
|
542
|
+
phase: identity.phase,
|
|
543
|
+
},
|
|
544
|
+
"environment-blocked",
|
|
545
|
+
"repair-environment",
|
|
546
|
+
identity.launchIndex,
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
const worker = this.registry.get(identity.taskId);
|
|
550
|
+
if (/candidate preflight rejected: design:/i.test(error)) {
|
|
551
|
+
this.results.discard(resultId);
|
|
552
|
+
this.retainedRuns.delete(resultId);
|
|
553
|
+
if (worker) {
|
|
554
|
+
worker.state = { kind: "ready" };
|
|
555
|
+
}
|
|
556
|
+
return recoveryFailure(
|
|
557
|
+
{
|
|
558
|
+
taskId: identity.taskId,
|
|
559
|
+
id: identity.requestId,
|
|
560
|
+
phase: identity.phase,
|
|
561
|
+
},
|
|
562
|
+
"design-required",
|
|
563
|
+
"return-to-design",
|
|
564
|
+
identity.launchIndex,
|
|
565
|
+
"failed",
|
|
566
|
+
"approved Red verification contract is invalid",
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
if (
|
|
570
|
+
/^(?:candidate preflight rejected: stale:|stale file snapshot)/i.test(
|
|
571
|
+
error,
|
|
572
|
+
)
|
|
573
|
+
) {
|
|
574
|
+
this.results.discard(resultId);
|
|
575
|
+
this.retainedRuns.delete(resultId);
|
|
576
|
+
if (worker) {
|
|
577
|
+
if (identity.launchIndex === 0) {
|
|
578
|
+
worker.state = { kind: "stale-redispatch-pending" };
|
|
579
|
+
} else {
|
|
580
|
+
worker.state = { kind: "blocked", reason: "mechanical" };
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
if (identity.launchIndex === 1) {
|
|
584
|
+
return recoveryFailure(
|
|
585
|
+
{
|
|
586
|
+
taskId: identity.taskId,
|
|
587
|
+
id: identity.requestId,
|
|
588
|
+
phase: identity.phase,
|
|
589
|
+
},
|
|
590
|
+
"mechanical-redispatch-exhausted",
|
|
591
|
+
"finish-unaffected",
|
|
592
|
+
1,
|
|
593
|
+
"failed",
|
|
594
|
+
"phase launch budget is exhausted",
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
return { ok: false, error };
|
|
598
|
+
}
|
|
599
|
+
if (/^retained result (?:not found|root mismatch)/i.test(error)) {
|
|
600
|
+
return { ok: false, error };
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
this.results.discard(resultId);
|
|
604
|
+
this.retainedRuns.delete(resultId);
|
|
605
|
+
const correctionAvailable = identity.launchIndex === 0;
|
|
606
|
+
if (worker) {
|
|
607
|
+
worker.currentPhase.correctionIndex = identity.launchIndex;
|
|
608
|
+
worker.state = correctionAvailable
|
|
609
|
+
? {
|
|
610
|
+
kind: "artifact-correction-pending",
|
|
611
|
+
rejection: normalizedArtifactRejection(error),
|
|
612
|
+
}
|
|
613
|
+
: { kind: "blocked", reason: "artifact" };
|
|
614
|
+
}
|
|
615
|
+
return recoveryFailure(
|
|
616
|
+
{
|
|
617
|
+
taskId: identity.taskId,
|
|
618
|
+
id: identity.requestId,
|
|
619
|
+
phase: identity.phase,
|
|
620
|
+
},
|
|
621
|
+
"implementation-artifact-delivery-blocked",
|
|
622
|
+
correctionAvailable ? "correct-artifact" : "finish-unaffected",
|
|
623
|
+
identity.launchIndex,
|
|
624
|
+
"failed",
|
|
625
|
+
correctionAvailable
|
|
626
|
+
? "generated implementation artifact requires bounded correction"
|
|
627
|
+
: "artifact correction launch budget is exhausted",
|
|
233
628
|
);
|
|
234
629
|
}
|
|
235
630
|
|
|
@@ -238,7 +633,8 @@ export class Runtime {
|
|
|
238
633
|
envelope: RequestEnvelope,
|
|
239
634
|
ctx: RunContext,
|
|
240
635
|
signal: AbortSignal,
|
|
241
|
-
|
|
636
|
+
artifactRejection?: string,
|
|
637
|
+
): Promise<InternalDispatchResult> {
|
|
242
638
|
if (signal.aborted)
|
|
243
639
|
return {
|
|
244
640
|
...cancellationError(signal),
|
|
@@ -246,14 +642,22 @@ export class Runtime {
|
|
|
246
642
|
};
|
|
247
643
|
let phase: Awaited<ReturnType<typeof runtimeFromContext>>;
|
|
248
644
|
try {
|
|
249
|
-
phase = await runtimeFromContext(ctx, signal);
|
|
645
|
+
phase = await runtimeFromContext(ctx, this.parentPayloadBridge, signal);
|
|
250
646
|
} catch (error) {
|
|
647
|
+
const bridgeUnavailable =
|
|
648
|
+
error instanceof Error &&
|
|
649
|
+
error.message === "parent payload bridge is unavailable";
|
|
251
650
|
return {
|
|
252
651
|
ok: false,
|
|
253
652
|
error: signal.aborted
|
|
254
653
|
? cancellationError(signal).error
|
|
255
654
|
: (error as Error).message,
|
|
256
655
|
failureKind: signal.aborted ? "cancelled" : "failed",
|
|
656
|
+
failureClass: signal.aborted
|
|
657
|
+
? undefined
|
|
658
|
+
: bridgeUnavailable
|
|
659
|
+
? "transport"
|
|
660
|
+
: "environment",
|
|
257
661
|
};
|
|
258
662
|
}
|
|
259
663
|
if (signal.aborted)
|
|
@@ -264,11 +668,23 @@ export class Runtime {
|
|
|
264
668
|
const roots = envelope.roots.map(
|
|
265
669
|
(root) => new URL(root, `file://${ctx.cwd}/`).pathname,
|
|
266
670
|
);
|
|
671
|
+
const phaseContract = {
|
|
672
|
+
taskId: envelope.taskId ?? envelope.id,
|
|
673
|
+
requestId: envelope.id,
|
|
674
|
+
phase: envelope.phase,
|
|
675
|
+
readSet: [...envelope.declared.read],
|
|
676
|
+
writeSet: [...envelope.declared.write],
|
|
677
|
+
verification: envelope.verification ?? null,
|
|
678
|
+
};
|
|
267
679
|
const systemPrompt = [
|
|
268
680
|
agent.content,
|
|
269
681
|
envelope.objective,
|
|
270
682
|
envelope.context.agents,
|
|
271
683
|
envelope.context.contract,
|
|
684
|
+
`<phase-contract>${JSON.stringify(phaseContract)}</phase-contract>`,
|
|
685
|
+
...(artifactRejection
|
|
686
|
+
? [`<artifact-rejection>${artifactRejection}</artifact-rejection>`]
|
|
687
|
+
: []),
|
|
272
688
|
].join("\n\n");
|
|
273
689
|
const child = await runChildSession({
|
|
274
690
|
cwd: ctx.cwd,
|
|
@@ -276,10 +692,14 @@ export class Runtime {
|
|
|
276
692
|
model: phase.model,
|
|
277
693
|
systemPrompt,
|
|
278
694
|
requestId: envelope.id,
|
|
695
|
+
taskId: envelope.taskId,
|
|
279
696
|
role: envelope.role,
|
|
280
697
|
phase: envelope.phase,
|
|
281
698
|
output: envelope.output as "evidence" | "diff",
|
|
282
699
|
roots,
|
|
700
|
+
allowedPaths: [
|
|
701
|
+
...new Set([...envelope.declared.read, ...envelope.declared.write]),
|
|
702
|
+
],
|
|
283
703
|
timeoutMs: LIMITS.phaseTimeoutMs,
|
|
284
704
|
signal,
|
|
285
705
|
});
|
|
@@ -288,7 +708,23 @@ export class Runtime {
|
|
|
288
708
|
...cancellationError(signal),
|
|
289
709
|
failureKind: "cancelled",
|
|
290
710
|
};
|
|
291
|
-
if (!child.ok)
|
|
711
|
+
if (!child.ok) {
|
|
712
|
+
const generatedArtifact =
|
|
713
|
+
child.classification.attempts > 0 ||
|
|
714
|
+
child.classification.finalCategory === "text-only" ||
|
|
715
|
+
child.classification.finalCategory === "mixed" ||
|
|
716
|
+
child.classification.finalCategory === "multiple-submit";
|
|
717
|
+
return {
|
|
718
|
+
...child,
|
|
719
|
+
failureKind: child.failureKind,
|
|
720
|
+
failureClass:
|
|
721
|
+
child.failureKind === "timed-out" || child.transportFailure
|
|
722
|
+
? "transport"
|
|
723
|
+
: generatedArtifact
|
|
724
|
+
? "artifact"
|
|
725
|
+
: "transport",
|
|
726
|
+
};
|
|
727
|
+
}
|
|
292
728
|
if (envelope.output === "evidence") {
|
|
293
729
|
return {
|
|
294
730
|
ok: true,
|
|
@@ -298,12 +734,46 @@ export class Runtime {
|
|
|
298
734
|
};
|
|
299
735
|
}
|
|
300
736
|
const diff = child.result as DiffResult;
|
|
737
|
+
const requestSnapshot = isSafeBound(envelope.snapshot)
|
|
738
|
+
? envelope.snapshot
|
|
739
|
+
: {};
|
|
740
|
+
let prepared: PreparedPreflight | null;
|
|
741
|
+
try {
|
|
742
|
+
prepared = preparePreflight(ctx.cwd, envelope, requestSnapshot);
|
|
743
|
+
} catch {
|
|
744
|
+
return {
|
|
745
|
+
ok: false,
|
|
746
|
+
error: "candidate preflight inputs are unavailable",
|
|
747
|
+
failureKind: "failed",
|
|
748
|
+
failureClass: "environment",
|
|
749
|
+
launchConsumed: true,
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
if (envelope.verification && !prepared) {
|
|
753
|
+
return {
|
|
754
|
+
ok: false,
|
|
755
|
+
error: "candidate preflight inputs are unavailable",
|
|
756
|
+
failureKind: "failed",
|
|
757
|
+
failureClass: "environment",
|
|
758
|
+
launchConsumed: true,
|
|
759
|
+
};
|
|
760
|
+
}
|
|
301
761
|
const resultId = this.results.retain({
|
|
302
762
|
diff: diff.diff,
|
|
303
763
|
writeSet: envelope.declared.write,
|
|
304
764
|
root: ctx.cwd,
|
|
765
|
+
...(prepared
|
|
766
|
+
? {
|
|
767
|
+
snapshot: prepared.snapshot,
|
|
768
|
+
baseline: prepared.baseline,
|
|
769
|
+
verification: envelope.verification,
|
|
770
|
+
packageManifest: prepared.packageManifest,
|
|
771
|
+
lockfile: prepared.lockfile,
|
|
772
|
+
dependencyTarget: prepared.dependencyTarget,
|
|
773
|
+
}
|
|
774
|
+
: {}),
|
|
305
775
|
});
|
|
306
|
-
if (isSafeBound(envelope.snapshot)) {
|
|
776
|
+
if (!prepared && isSafeBound(envelope.snapshot)) {
|
|
307
777
|
const retained = this.results.get(resultId);
|
|
308
778
|
if (retained) {
|
|
309
779
|
retained.snapshot = mergeBounds(retained.snapshot, envelope.snapshot);
|
|
@@ -330,6 +800,7 @@ export class Runtime {
|
|
|
330
800
|
if (!validation.ok) return { ok: false, error: validation.reason };
|
|
331
801
|
const envelope: RequestEnvelope = {
|
|
332
802
|
...validation.value,
|
|
803
|
+
taskId: validation.value.taskId ?? validation.value.id,
|
|
333
804
|
roots: [...validation.value.roots],
|
|
334
805
|
context: { ...validation.value.context },
|
|
335
806
|
declared: {
|
|
@@ -339,6 +810,14 @@ export class Runtime {
|
|
|
339
810
|
conflicts: [...validation.value.declared.conflicts],
|
|
340
811
|
resources: [...validation.value.declared.resources],
|
|
341
812
|
},
|
|
813
|
+
...(validation.value.verification === undefined
|
|
814
|
+
? {}
|
|
815
|
+
: {
|
|
816
|
+
verification: {
|
|
817
|
+
...validation.value.verification,
|
|
818
|
+
argv: [...validation.value.verification.argv],
|
|
819
|
+
},
|
|
820
|
+
}),
|
|
342
821
|
};
|
|
343
822
|
const context: StoredRunContext = {
|
|
344
823
|
ctx,
|
|
@@ -365,8 +844,13 @@ export class Runtime {
|
|
|
365
844
|
if (outcome.status === "succeeded" && outcome.value) {
|
|
366
845
|
const internalResult = outcome.value.dispatch as DispatchResult & {
|
|
367
846
|
failureKind?: RunFailureKind;
|
|
847
|
+
failureClass?: FailureClass;
|
|
368
848
|
};
|
|
369
|
-
const {
|
|
849
|
+
const {
|
|
850
|
+
failureKind,
|
|
851
|
+
failureClass: _failureClass,
|
|
852
|
+
...publicResult
|
|
853
|
+
} = internalResult;
|
|
370
854
|
if (publicResult.ok) {
|
|
371
855
|
this.notify(envelope, context, "completed");
|
|
372
856
|
return publicResult;
|
|
@@ -434,97 +918,314 @@ export class Runtime {
|
|
|
434
918
|
envelope: RequestEnvelope,
|
|
435
919
|
ctx: RunContext,
|
|
436
920
|
signal: AbortSignal,
|
|
437
|
-
): Promise<
|
|
921
|
+
): Promise<InternalDispatchResult> {
|
|
438
922
|
if (signal.aborted)
|
|
439
923
|
return {
|
|
440
924
|
...cancellationError(signal),
|
|
441
925
|
failureKind: "cancelled",
|
|
442
926
|
};
|
|
927
|
+
const taskId = envelope.taskId ?? envelope.id;
|
|
443
928
|
const agent = loadAgentDefinitions().find(
|
|
444
929
|
(item) => item.role === envelope.role,
|
|
445
930
|
);
|
|
446
931
|
if (!agent)
|
|
447
932
|
return {
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
933
|
+
...recoveryFailure(
|
|
934
|
+
envelope,
|
|
935
|
+
"environment-blocked",
|
|
936
|
+
"repair-environment",
|
|
937
|
+
0,
|
|
938
|
+
),
|
|
451
939
|
};
|
|
452
940
|
if (!ctx.model)
|
|
453
|
-
return
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
941
|
+
return recoveryFailure(
|
|
942
|
+
envelope,
|
|
943
|
+
"environment-blocked",
|
|
944
|
+
"repair-environment",
|
|
945
|
+
0,
|
|
946
|
+
"failed",
|
|
947
|
+
"phase runtime is unavailable",
|
|
948
|
+
);
|
|
458
949
|
const identity = workerIdentity(ctx.model);
|
|
459
950
|
const contract = contractOf(envelope);
|
|
460
|
-
const existing = this.registry.get(
|
|
951
|
+
const existing = this.registry.get(taskId);
|
|
461
952
|
if (existing) {
|
|
462
953
|
if (existing.identity !== identity) {
|
|
463
|
-
return
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
954
|
+
return recoveryFailure(
|
|
955
|
+
envelope,
|
|
956
|
+
"environment-blocked",
|
|
957
|
+
"repair-environment",
|
|
958
|
+
existing.currentPhase.correctionIndex,
|
|
959
|
+
"failed",
|
|
960
|
+
"pinned provider/model identity differs",
|
|
961
|
+
);
|
|
469
962
|
}
|
|
470
|
-
if (!sameContract(existing.
|
|
471
|
-
return
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
963
|
+
if (!sameContract(existing.taskContract, contract)) {
|
|
964
|
+
return recoveryFailure(
|
|
965
|
+
envelope,
|
|
966
|
+
"design-required",
|
|
967
|
+
"return-to-design",
|
|
968
|
+
existing.currentPhase.correctionIndex,
|
|
969
|
+
"failed",
|
|
970
|
+
"immutable task contract changed",
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
if (
|
|
974
|
+
existing.state.kind === "blocked" &&
|
|
975
|
+
existing.state.reason === "artifact"
|
|
976
|
+
) {
|
|
977
|
+
return recoveryFailure(
|
|
978
|
+
envelope,
|
|
979
|
+
"implementation-artifact-delivery-blocked",
|
|
980
|
+
"finish-unaffected",
|
|
981
|
+
1,
|
|
982
|
+
"failed",
|
|
983
|
+
"artifact correction launch budget is exhausted",
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
if (
|
|
987
|
+
existing.state.kind === "blocked" &&
|
|
988
|
+
existing.state.reason === "mechanical"
|
|
989
|
+
) {
|
|
990
|
+
return recoveryFailure(
|
|
991
|
+
envelope,
|
|
992
|
+
"mechanical-redispatch-exhausted",
|
|
993
|
+
"finish-unaffected",
|
|
994
|
+
1,
|
|
995
|
+
"failed",
|
|
996
|
+
"phase launch budget is exhausted",
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
} else {
|
|
1000
|
+
this.registry.pin(contract, identity);
|
|
1001
|
+
}
|
|
1002
|
+
let pinned = this.registry.get(taskId);
|
|
1003
|
+
if (!pinned) {
|
|
1004
|
+
return recoveryFailure(
|
|
1005
|
+
envelope,
|
|
1006
|
+
"environment-blocked",
|
|
1007
|
+
"repair-environment",
|
|
1008
|
+
0,
|
|
1009
|
+
"failed",
|
|
1010
|
+
"logical Worker could not be pinned",
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
const correctingArtifact =
|
|
1015
|
+
pinned.state.kind === "artifact-correction-pending";
|
|
1016
|
+
const redispatchingStale = pinned.state.kind === "stale-redispatch-pending";
|
|
1017
|
+
let artifactRejection: string | undefined;
|
|
1018
|
+
const changedPhase =
|
|
1019
|
+
pinned.currentPhase.requestId !== envelope.id ||
|
|
1020
|
+
pinned.currentPhase.phase !== envelope.phase;
|
|
1021
|
+
if (pinned.state.kind === "candidate-pending") {
|
|
1022
|
+
return {
|
|
1023
|
+
ok: false,
|
|
1024
|
+
error: "current phase candidate is awaiting parent apply",
|
|
1025
|
+
failureKind: "failed",
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
if (redispatchingStale) {
|
|
1029
|
+
if (
|
|
1030
|
+
envelope.id !== pinned.currentPhase.requestId ||
|
|
1031
|
+
!samePhaseContract(pinned.currentPhase, contract.currentPhase)
|
|
1032
|
+
) {
|
|
1033
|
+
return recoveryFailure(
|
|
1034
|
+
envelope,
|
|
1035
|
+
"design-required",
|
|
1036
|
+
"return-to-design",
|
|
1037
|
+
pinned.currentPhase.correctionIndex,
|
|
1038
|
+
"failed",
|
|
1039
|
+
"stale redispatch changed the current phase contract",
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
pinned = this.registry.setCurrentPhase(taskId, contract, 1) ?? pinned;
|
|
1043
|
+
} else if (correctingArtifact) {
|
|
1044
|
+
if (
|
|
1045
|
+
envelope.id === pinned.currentPhase.requestId ||
|
|
1046
|
+
!samePhaseContract(pinned.currentPhase, contract.currentPhase)
|
|
1047
|
+
) {
|
|
1048
|
+
return recoveryFailure(
|
|
1049
|
+
envelope,
|
|
1050
|
+
"design-required",
|
|
1051
|
+
"return-to-design",
|
|
1052
|
+
pinned.currentPhase.correctionIndex,
|
|
1053
|
+
"failed",
|
|
1054
|
+
"artifact correction changed the current phase contract",
|
|
1055
|
+
);
|
|
477
1056
|
}
|
|
478
|
-
|
|
1057
|
+
artifactRejection =
|
|
1058
|
+
pinned.state.kind === "artifact-correction-pending"
|
|
1059
|
+
? pinned.state.rejection
|
|
1060
|
+
: undefined;
|
|
1061
|
+
pinned = this.registry.setCurrentPhase(taskId, contract, 1) ?? pinned;
|
|
1062
|
+
} else if (changedPhase) {
|
|
1063
|
+
if (pinned.state.kind !== "phase-applied") {
|
|
479
1064
|
return {
|
|
480
1065
|
ok: false,
|
|
481
|
-
error: "
|
|
1066
|
+
error: "current phase candidate is not applied",
|
|
482
1067
|
failureKind: "failed",
|
|
483
1068
|
};
|
|
484
1069
|
}
|
|
485
|
-
|
|
486
|
-
|
|
1070
|
+
const sameAppliedPhase = pinned.currentPhase.phase === envelope.phase;
|
|
1071
|
+
if (
|
|
1072
|
+
sameAppliedPhase &&
|
|
1073
|
+
!samePhaseContract(pinned.currentPhase, contract.currentPhase)
|
|
1074
|
+
) {
|
|
1075
|
+
return recoveryFailure(
|
|
1076
|
+
envelope,
|
|
1077
|
+
"design-required",
|
|
1078
|
+
"return-to-design",
|
|
1079
|
+
pinned.currentPhase.correctionIndex,
|
|
1080
|
+
"failed",
|
|
1081
|
+
"same-phase correction changed the current phase contract",
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1084
|
+
if (
|
|
1085
|
+
!sameAppliedPhase &&
|
|
1086
|
+
!isNextImplementationPhase(pinned.currentPhase.phase, envelope.phase)
|
|
1087
|
+
) {
|
|
1088
|
+
return recoveryFailure(
|
|
1089
|
+
envelope,
|
|
1090
|
+
"design-required",
|
|
1091
|
+
"return-to-design",
|
|
1092
|
+
pinned.currentPhase.correctionIndex,
|
|
1093
|
+
"failed",
|
|
1094
|
+
"invalid implementation phase transition",
|
|
1095
|
+
);
|
|
1096
|
+
}
|
|
1097
|
+
pinned = this.registry.setCurrentPhase(taskId, contract, 0) ?? pinned;
|
|
1098
|
+
} else if (pinned.state.kind === "phase-applied") {
|
|
1099
|
+
return {
|
|
1100
|
+
ok: false,
|
|
1101
|
+
error: "current phase candidate is already applied",
|
|
1102
|
+
failureKind: "failed",
|
|
1103
|
+
};
|
|
487
1104
|
}
|
|
488
|
-
|
|
489
|
-
const first = await this.dispatchChild(
|
|
1105
|
+
|
|
1106
|
+
const first = await this.dispatchChild(
|
|
1107
|
+
agent,
|
|
1108
|
+
envelope,
|
|
1109
|
+
ctx,
|
|
1110
|
+
signal,
|
|
1111
|
+
artifactRejection,
|
|
1112
|
+
);
|
|
490
1113
|
if (signal.aborted)
|
|
491
1114
|
return {
|
|
492
1115
|
...cancellationError(signal),
|
|
493
1116
|
failureKind: "cancelled",
|
|
494
1117
|
};
|
|
495
1118
|
if (first.ok) {
|
|
496
|
-
|
|
1119
|
+
this.rememberRetainedResult(first, envelope, pinned);
|
|
497
1120
|
return first;
|
|
498
1121
|
}
|
|
499
|
-
if (
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
1122
|
+
if (first.failureKind === "cancelled") return first;
|
|
1123
|
+
if (first.failureClass === "environment") {
|
|
1124
|
+
const launchIndex = pinned.currentPhase.correctionIndex;
|
|
1125
|
+
if (first.launchConsumed) this.consumeWorkerLaunch(pinned);
|
|
1126
|
+
return recoveryFailure(
|
|
1127
|
+
envelope,
|
|
1128
|
+
"environment-blocked",
|
|
1129
|
+
"repair-environment",
|
|
1130
|
+
launchIndex,
|
|
1131
|
+
first.failureKind,
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
if (pinned.currentPhase.correctionIndex === 1) {
|
|
1135
|
+
const artifactBlocked =
|
|
1136
|
+
correctingArtifact || first.failureClass === "artifact";
|
|
1137
|
+
pinned.state = {
|
|
1138
|
+
kind: "blocked",
|
|
1139
|
+
reason: artifactBlocked ? "artifact" : "mechanical",
|
|
504
1140
|
};
|
|
1141
|
+
return recoveryFailure(
|
|
1142
|
+
envelope,
|
|
1143
|
+
artifactBlocked
|
|
1144
|
+
? "implementation-artifact-delivery-blocked"
|
|
1145
|
+
: "mechanical-redispatch-exhausted",
|
|
1146
|
+
"finish-unaffected",
|
|
1147
|
+
1,
|
|
1148
|
+
first.failureKind,
|
|
1149
|
+
"phase launch budget is exhausted",
|
|
1150
|
+
);
|
|
505
1151
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
1152
|
+
|
|
1153
|
+
if (first.failureClass === "artifact") {
|
|
1154
|
+
pinned.state = {
|
|
1155
|
+
kind: "artifact-correction-pending",
|
|
1156
|
+
rejection: normalizedArtifactRejection(first.error),
|
|
511
1157
|
};
|
|
1158
|
+
return recoveryFailure(
|
|
1159
|
+
envelope,
|
|
1160
|
+
"implementation-artifact-delivery-blocked",
|
|
1161
|
+
"correct-artifact",
|
|
1162
|
+
0,
|
|
1163
|
+
first.failureKind,
|
|
1164
|
+
"generated implementation artifact requires bounded correction",
|
|
1165
|
+
);
|
|
512
1166
|
}
|
|
513
|
-
|
|
1167
|
+
|
|
1168
|
+
pinned = this.registry.setCurrentPhase(taskId, contract, 1) ?? pinned;
|
|
514
1169
|
const second = await this.dispatchChild(agent, envelope, ctx, signal);
|
|
515
1170
|
if (signal.aborted)
|
|
516
1171
|
return {
|
|
517
1172
|
...cancellationError(signal),
|
|
518
1173
|
failureKind: "cancelled",
|
|
519
1174
|
};
|
|
520
|
-
if (
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
1175
|
+
if (second.ok) {
|
|
1176
|
+
this.rememberRetainedResult(second, envelope, pinned);
|
|
1177
|
+
return second;
|
|
1178
|
+
}
|
|
1179
|
+
if (second.failureKind === "cancelled") return second;
|
|
1180
|
+
if (second.failureClass === "environment") {
|
|
1181
|
+
if (second.launchConsumed) this.consumeWorkerLaunch(pinned);
|
|
1182
|
+
return recoveryFailure(
|
|
1183
|
+
envelope,
|
|
1184
|
+
"environment-blocked",
|
|
1185
|
+
"repair-environment",
|
|
1186
|
+
1,
|
|
1187
|
+
second.failureKind,
|
|
1188
|
+
);
|
|
526
1189
|
}
|
|
527
|
-
|
|
1190
|
+
const artifactBlocked = second.failureClass === "artifact";
|
|
1191
|
+
pinned.state = {
|
|
1192
|
+
kind: "blocked",
|
|
1193
|
+
reason: artifactBlocked ? "artifact" : "mechanical",
|
|
1194
|
+
};
|
|
1195
|
+
return recoveryFailure(
|
|
1196
|
+
envelope,
|
|
1197
|
+
artifactBlocked
|
|
1198
|
+
? "implementation-artifact-delivery-blocked"
|
|
1199
|
+
: "mechanical-redispatch-exhausted",
|
|
1200
|
+
"finish-unaffected",
|
|
1201
|
+
1,
|
|
1202
|
+
second.failureKind ?? first.failureKind,
|
|
1203
|
+
"phase launch budget is exhausted",
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
private consumeWorkerLaunch(worker: LogicalWorker): void {
|
|
1208
|
+
if (worker.currentPhase.correctionIndex === 0) {
|
|
1209
|
+
worker.currentPhase.correctionIndex = 1;
|
|
1210
|
+
worker.state = { kind: "ready" };
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
worker.state = { kind: "blocked", reason: "mechanical" };
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
private rememberRetainedResult(
|
|
1217
|
+
result: InternalDispatchResult,
|
|
1218
|
+
envelope: RequestEnvelope,
|
|
1219
|
+
worker: LogicalWorker,
|
|
1220
|
+
): void {
|
|
1221
|
+
if (!result.ok || typeof result.resultId !== "string") return;
|
|
1222
|
+
this.retainedRuns.set(result.resultId, {
|
|
1223
|
+
taskId: envelope.taskId ?? envelope.id,
|
|
1224
|
+
requestId: envelope.id,
|
|
1225
|
+
phase: envelope.phase,
|
|
1226
|
+
launchIndex: worker.currentPhase.correctionIndex,
|
|
1227
|
+
});
|
|
1228
|
+
worker.state = { kind: "candidate-pending" };
|
|
528
1229
|
}
|
|
529
1230
|
|
|
530
1231
|
validateRequest(
|
|
@@ -534,7 +1235,9 @@ export class Runtime {
|
|
|
534
1235
|
}
|
|
535
1236
|
|
|
536
1237
|
async drain(): Promise<void> {
|
|
1238
|
+
this.parentPayloadBridge?.clear();
|
|
537
1239
|
const settled = this.scheduler.cancelAll();
|
|
1240
|
+
this.retainedRuns.clear();
|
|
538
1241
|
drainStage({
|
|
539
1242
|
results: this.results,
|
|
540
1243
|
registry: this.registry,
|
|
@@ -547,3 +1250,9 @@ export class Runtime {
|
|
|
547
1250
|
return this.activation.state;
|
|
548
1251
|
}
|
|
549
1252
|
}
|
|
1253
|
+
|
|
1254
|
+
function isNextImplementationPhase(current: string, next: string): boolean {
|
|
1255
|
+
const phases = ["red", "green", "refactor"];
|
|
1256
|
+
const index = phases.indexOf(current);
|
|
1257
|
+
return index >= 0 && phases[index + 1] === next;
|
|
1258
|
+
}
|