@abelxiaoxing/cadence 1.0.0 → 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/README.md +50 -22
- package/package.json +3 -1
- package/prompts/abel-design.md +1 -0
- package/prompts/abel-diagnose.md +1 -0
- package/prompts/abel-implement.md +14 -2
- package/skills/abel-workflow/SKILL.md +7 -1
- package/src/candidate-preflight.ts +1163 -0
- package/src/child-session.ts +98 -10
- package/src/contracts.ts +379 -48
- package/src/file-snapshot.ts +62 -8
- package/src/index.ts +191 -14
- package/src/parent-payload-bridge.ts +442 -0
- package/src/parent-provider.ts +124 -16
- package/src/patch.ts +73 -3
- package/src/result-store.ts +40 -3
- package/src/runtime.ts +1013 -60
- package/src/scheduler.ts +47 -10
- package/src/scoped-tools.ts +27 -6
- package/src/subagent-activity.ts +747 -0
- package/src/submit-tool.ts +3 -1
- package/src/worker.ts +136 -26
package/src/runtime.ts
CHANGED
|
@@ -1,28 +1,302 @@
|
|
|
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";
|
|
33
|
+
import { Scheduler } from "./scheduler.ts";
|
|
17
34
|
import {
|
|
18
35
|
contractOf,
|
|
36
|
+
type LogicalWorker,
|
|
19
37
|
sameContract,
|
|
38
|
+
samePhaseContract,
|
|
20
39
|
WorkerRegistry,
|
|
21
40
|
workerIdentity,
|
|
22
41
|
} from "./worker.ts";
|
|
23
42
|
|
|
24
43
|
const SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
25
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
|
+
|
|
197
|
+
function cancellationError(signal: AbortSignal): { ok: false; error: string } {
|
|
198
|
+
const reason = signal.reason;
|
|
199
|
+
return {
|
|
200
|
+
ok: false,
|
|
201
|
+
error:
|
|
202
|
+
reason instanceof Error
|
|
203
|
+
? reason.message
|
|
204
|
+
: typeof reason === "string"
|
|
205
|
+
? reason
|
|
206
|
+
: "child phase cancelled",
|
|
207
|
+
};
|
|
208
|
+
}
|
|
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
|
+
|
|
268
|
+
export type RunFailureKind = "failed" | "cancelled" | "timed-out";
|
|
269
|
+
export type RuntimeFailureReason =
|
|
270
|
+
| "subagent failed"
|
|
271
|
+
| "subagent cancelled"
|
|
272
|
+
| "phase timed out";
|
|
273
|
+
export type RuntimeActivityState =
|
|
274
|
+
| "queued"
|
|
275
|
+
| "running"
|
|
276
|
+
| "completed"
|
|
277
|
+
| "failed"
|
|
278
|
+
| "cancelled"
|
|
279
|
+
| "timed-out";
|
|
280
|
+
|
|
281
|
+
export interface RuntimeActivityEvent {
|
|
282
|
+
state: RuntimeActivityState;
|
|
283
|
+
requestId: string;
|
|
284
|
+
role: string;
|
|
285
|
+
phase: string;
|
|
286
|
+
objective: string;
|
|
287
|
+
sequence: number;
|
|
288
|
+
failureReason?: RuntimeFailureReason;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export type RuntimeActivityObserver = (
|
|
292
|
+
event: RuntimeActivityEvent,
|
|
293
|
+
) => void | Promise<void>;
|
|
294
|
+
|
|
295
|
+
interface ScheduledRunResult {
|
|
296
|
+
dispatch: DispatchResult;
|
|
297
|
+
failureKind?: RunFailureKind;
|
|
298
|
+
}
|
|
299
|
+
|
|
26
300
|
function isSafeBound(snapshot: unknown): snapshot is Bound {
|
|
27
301
|
if (
|
|
28
302
|
typeof snapshot !== "object" ||
|
|
@@ -77,7 +351,12 @@ export type DispatchResult =
|
|
|
77
351
|
resultId?: string;
|
|
78
352
|
usage?: unknown;
|
|
79
353
|
}
|
|
80
|
-
| {
|
|
354
|
+
| {
|
|
355
|
+
ok: false;
|
|
356
|
+
notReady?: true;
|
|
357
|
+
error: string;
|
|
358
|
+
recovery?: RecoveryRecord;
|
|
359
|
+
};
|
|
81
360
|
|
|
82
361
|
export interface ApplyResult {
|
|
83
362
|
targets: string[];
|
|
@@ -88,26 +367,61 @@ export interface ApplyResult {
|
|
|
88
367
|
|
|
89
368
|
export interface RuntimeOptions {
|
|
90
369
|
activation?: Activation;
|
|
370
|
+
parentPayloadBridge: ParentPayloadBridge;
|
|
91
371
|
}
|
|
92
372
|
|
|
93
373
|
type RunContext = Pick<ExtensionContext, "cwd" | "model" | "modelRegistry">;
|
|
94
374
|
|
|
375
|
+
interface StoredRunContext {
|
|
376
|
+
ctx: RunContext;
|
|
377
|
+
observer?: RuntimeActivityObserver;
|
|
378
|
+
sequence: number;
|
|
379
|
+
runningEmitted: boolean;
|
|
380
|
+
terminalEmitted: boolean;
|
|
381
|
+
}
|
|
382
|
+
|
|
95
383
|
export class Runtime {
|
|
96
384
|
readonly activation: Activation;
|
|
97
385
|
readonly limits = LIMITS;
|
|
98
386
|
readonly results = new ResultStore();
|
|
99
387
|
private applyTail: Promise<void> = Promise.resolve();
|
|
100
388
|
private applySeq = 0;
|
|
389
|
+
private batchSeq = 0;
|
|
390
|
+
private readonly parentPayloadBridge: ParentPayloadBridge;
|
|
101
391
|
private readonly registry = new WorkerRegistry();
|
|
392
|
+
private readonly retainedRuns = new Map<string, RetainedRunIdentity>();
|
|
393
|
+
private readonly runContexts = new WeakMap<
|
|
394
|
+
RequestEnvelope,
|
|
395
|
+
StoredRunContext
|
|
396
|
+
>();
|
|
397
|
+
private readonly scheduler: Scheduler<ScheduledRunResult>;
|
|
102
398
|
|
|
103
|
-
constructor(opts: RuntimeOptions
|
|
399
|
+
constructor(opts: RuntimeOptions) {
|
|
104
400
|
this.activation = opts.activation ?? new Activation();
|
|
401
|
+
this.parentPayloadBridge = opts.parentPayloadBridge;
|
|
402
|
+
this.scheduler = new Scheduler({
|
|
403
|
+
limit: LIMITS.maxActiveChildSessions,
|
|
404
|
+
execute: async (request, signal) => {
|
|
405
|
+
const context = this.runContexts.get(request);
|
|
406
|
+
if (!context) throw new Error("scheduled run context is unavailable");
|
|
407
|
+
this.notify(request, context, "running");
|
|
408
|
+
const result = await this.runScheduled(request, context.ctx, signal);
|
|
409
|
+
return {
|
|
410
|
+
dispatch: result,
|
|
411
|
+
...(result.failureKind === undefined
|
|
412
|
+
? {}
|
|
413
|
+
: { failureKind: result.failureKind }),
|
|
414
|
+
};
|
|
415
|
+
},
|
|
416
|
+
});
|
|
105
417
|
}
|
|
106
418
|
|
|
107
419
|
async execute(
|
|
108
420
|
action: string,
|
|
109
421
|
params: { request?: unknown; resultId?: string },
|
|
110
422
|
ctx?: RunContext,
|
|
423
|
+
signal?: AbortSignal,
|
|
424
|
+
observer?: RuntimeActivityObserver,
|
|
111
425
|
): Promise<DispatchResult> {
|
|
112
426
|
if (!(ACTIONS as readonly string[]).includes(action)) {
|
|
113
427
|
return { ok: false, error: `unknown action: ${String(action)}` };
|
|
@@ -115,32 +429,58 @@ export class Runtime {
|
|
|
115
429
|
if (!this.activation.isActive()) {
|
|
116
430
|
return { ok: false, notReady: true, error: "dispatcher is not active" };
|
|
117
431
|
}
|
|
118
|
-
if (action === "run")
|
|
432
|
+
if (action === "run")
|
|
433
|
+
return this.run(params.request, ctx, signal, observer);
|
|
119
434
|
if (action === "apply") {
|
|
120
435
|
if (!ctx || !params.resultId)
|
|
121
436
|
return { ok: false, error: "apply requires context and resultId" };
|
|
122
|
-
return this.enqueueApply(ctx.cwd, params.resultId);
|
|
437
|
+
return this.enqueueApply(ctx.cwd, params.resultId, signal);
|
|
123
438
|
}
|
|
124
439
|
if (action === "discard") {
|
|
125
440
|
if (!params.resultId)
|
|
126
441
|
return { ok: false, error: "discard requires resultId" };
|
|
127
|
-
return this.
|
|
442
|
+
return this.discardRetainedCandidate(params.resultId)
|
|
128
443
|
? { ok: true, action }
|
|
129
444
|
: { ok: false, error: "retained result not found" };
|
|
130
445
|
}
|
|
131
|
-
if (action === "cancel")
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
});
|
|
446
|
+
if (action === "cancel") {
|
|
447
|
+
await this.scheduler.cancelAll();
|
|
448
|
+
return { ok: true, action };
|
|
449
|
+
}
|
|
450
|
+
await this.drain();
|
|
137
451
|
return { ok: true, action: "finish" };
|
|
138
452
|
}
|
|
139
453
|
|
|
140
|
-
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);
|
|
141
481
|
const run = this.applyTail.then(
|
|
142
|
-
() => applyRetainedPatch({ root, id, store: this.results }),
|
|
143
|
-
() => applyRetainedPatch({ root, id, store: this.results }),
|
|
482
|
+
() => applyRetainedPatch({ root, id, store: this.results, signal }),
|
|
483
|
+
() => applyRetainedPatch({ root, id, store: this.results, signal }),
|
|
144
484
|
);
|
|
145
485
|
this.applyTail = run.then(
|
|
146
486
|
() => undefined,
|
|
@@ -148,11 +488,143 @@ export class Runtime {
|
|
|
148
488
|
);
|
|
149
489
|
const seq = ++this.applySeq;
|
|
150
490
|
return run.then(
|
|
151
|
-
(result) =>
|
|
152
|
-
result.ok
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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",
|
|
156
628
|
);
|
|
157
629
|
}
|
|
158
630
|
|
|
@@ -160,21 +632,59 @@ export class Runtime {
|
|
|
160
632
|
agent: { role: string; content: string },
|
|
161
633
|
envelope: RequestEnvelope,
|
|
162
634
|
ctx: RunContext,
|
|
163
|
-
|
|
635
|
+
signal: AbortSignal,
|
|
636
|
+
artifactRejection?: string,
|
|
637
|
+
): Promise<InternalDispatchResult> {
|
|
638
|
+
if (signal.aborted)
|
|
639
|
+
return {
|
|
640
|
+
...cancellationError(signal),
|
|
641
|
+
failureKind: "cancelled",
|
|
642
|
+
};
|
|
164
643
|
let phase: Awaited<ReturnType<typeof runtimeFromContext>>;
|
|
165
644
|
try {
|
|
166
|
-
phase = await runtimeFromContext(ctx);
|
|
645
|
+
phase = await runtimeFromContext(ctx, this.parentPayloadBridge, signal);
|
|
167
646
|
} catch (error) {
|
|
168
|
-
|
|
647
|
+
const bridgeUnavailable =
|
|
648
|
+
error instanceof Error &&
|
|
649
|
+
error.message === "parent payload bridge is unavailable";
|
|
650
|
+
return {
|
|
651
|
+
ok: false,
|
|
652
|
+
error: signal.aborted
|
|
653
|
+
? cancellationError(signal).error
|
|
654
|
+
: (error as Error).message,
|
|
655
|
+
failureKind: signal.aborted ? "cancelled" : "failed",
|
|
656
|
+
failureClass: signal.aborted
|
|
657
|
+
? undefined
|
|
658
|
+
: bridgeUnavailable
|
|
659
|
+
? "transport"
|
|
660
|
+
: "environment",
|
|
661
|
+
};
|
|
169
662
|
}
|
|
663
|
+
if (signal.aborted)
|
|
664
|
+
return {
|
|
665
|
+
...cancellationError(signal),
|
|
666
|
+
failureKind: "cancelled",
|
|
667
|
+
};
|
|
170
668
|
const roots = envelope.roots.map(
|
|
171
669
|
(root) => new URL(root, `file://${ctx.cwd}/`).pathname,
|
|
172
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
|
+
};
|
|
173
679
|
const systemPrompt = [
|
|
174
680
|
agent.content,
|
|
175
681
|
envelope.objective,
|
|
176
682
|
envelope.context.agents,
|
|
177
683
|
envelope.context.contract,
|
|
684
|
+
`<phase-contract>${JSON.stringify(phaseContract)}</phase-contract>`,
|
|
685
|
+
...(artifactRejection
|
|
686
|
+
? [`<artifact-rejection>${artifactRejection}</artifact-rejection>`]
|
|
687
|
+
: []),
|
|
178
688
|
].join("\n\n");
|
|
179
689
|
const child = await runChildSession({
|
|
180
690
|
cwd: ctx.cwd,
|
|
@@ -182,13 +692,39 @@ export class Runtime {
|
|
|
182
692
|
model: phase.model,
|
|
183
693
|
systemPrompt,
|
|
184
694
|
requestId: envelope.id,
|
|
695
|
+
taskId: envelope.taskId,
|
|
185
696
|
role: envelope.role,
|
|
186
697
|
phase: envelope.phase,
|
|
187
698
|
output: envelope.output as "evidence" | "diff",
|
|
188
699
|
roots,
|
|
700
|
+
allowedPaths: [
|
|
701
|
+
...new Set([...envelope.declared.read, ...envelope.declared.write]),
|
|
702
|
+
],
|
|
189
703
|
timeoutMs: LIMITS.phaseTimeoutMs,
|
|
704
|
+
signal,
|
|
190
705
|
});
|
|
191
|
-
if (
|
|
706
|
+
if (signal.aborted)
|
|
707
|
+
return {
|
|
708
|
+
...cancellationError(signal),
|
|
709
|
+
failureKind: "cancelled",
|
|
710
|
+
};
|
|
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
|
+
}
|
|
192
728
|
if (envelope.output === "evidence") {
|
|
193
729
|
return {
|
|
194
730
|
ok: true,
|
|
@@ -198,12 +734,46 @@ export class Runtime {
|
|
|
198
734
|
};
|
|
199
735
|
}
|
|
200
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
|
+
}
|
|
201
761
|
const resultId = this.results.retain({
|
|
202
762
|
diff: diff.diff,
|
|
203
763
|
writeSet: envelope.declared.write,
|
|
204
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
|
+
: {}),
|
|
205
775
|
});
|
|
206
|
-
if (isSafeBound(envelope.snapshot)) {
|
|
776
|
+
if (!prepared && isSafeBound(envelope.snapshot)) {
|
|
207
777
|
const retained = this.results.get(resultId);
|
|
208
778
|
if (retained) {
|
|
209
779
|
retained.snapshot = mergeBounds(retained.snapshot, envelope.snapshot);
|
|
@@ -221,68 +791,441 @@ export class Runtime {
|
|
|
221
791
|
private async run(
|
|
222
792
|
request: unknown,
|
|
223
793
|
ctx?: RunContext,
|
|
794
|
+
signal?: AbortSignal,
|
|
795
|
+
observer?: RuntimeActivityObserver,
|
|
224
796
|
): Promise<DispatchResult> {
|
|
225
797
|
if (!ctx) return { ok: false, error: "run requires extension context" };
|
|
798
|
+
if (signal?.aborted) return cancellationError(signal);
|
|
226
799
|
const validation = validateRequestEnvelope(request);
|
|
227
800
|
if (!validation.ok) return { ok: false, error: validation.reason };
|
|
228
|
-
const envelope =
|
|
801
|
+
const envelope: RequestEnvelope = {
|
|
802
|
+
...validation.value,
|
|
803
|
+
taskId: validation.value.taskId ?? validation.value.id,
|
|
804
|
+
roots: [...validation.value.roots],
|
|
805
|
+
context: { ...validation.value.context },
|
|
806
|
+
declared: {
|
|
807
|
+
...validation.value.declared,
|
|
808
|
+
read: [...validation.value.declared.read],
|
|
809
|
+
write: [...validation.value.declared.write],
|
|
810
|
+
conflicts: [...validation.value.declared.conflicts],
|
|
811
|
+
resources: [...validation.value.declared.resources],
|
|
812
|
+
},
|
|
813
|
+
...(validation.value.verification === undefined
|
|
814
|
+
? {}
|
|
815
|
+
: {
|
|
816
|
+
verification: {
|
|
817
|
+
...validation.value.verification,
|
|
818
|
+
argv: [...validation.value.verification.argv],
|
|
819
|
+
},
|
|
820
|
+
}),
|
|
821
|
+
};
|
|
822
|
+
const context: StoredRunContext = {
|
|
823
|
+
ctx,
|
|
824
|
+
observer,
|
|
825
|
+
sequence: ++this.batchSeq,
|
|
826
|
+
runningEmitted: false,
|
|
827
|
+
terminalEmitted: false,
|
|
828
|
+
};
|
|
829
|
+
this.runContexts.set(envelope, context);
|
|
830
|
+
this.notify(envelope, context, "queued");
|
|
831
|
+
const batchId = `runtime-${context.sequence}`;
|
|
832
|
+
const batch = this.scheduler.schedule(batchId, [
|
|
833
|
+
{ request: envelope, prerequisites: [] },
|
|
834
|
+
]);
|
|
835
|
+
const cancelBatch = () =>
|
|
836
|
+
this.scheduler.cancel(
|
|
837
|
+
batchId,
|
|
838
|
+
signal?.reason ?? new Error("tool call cancelled"),
|
|
839
|
+
);
|
|
840
|
+
if (signal?.aborted) cancelBatch();
|
|
841
|
+
else signal?.addEventListener("abort", cancelBatch, { once: true });
|
|
842
|
+
try {
|
|
843
|
+
const outcome = await batch.result(envelope.id);
|
|
844
|
+
if (outcome.status === "succeeded" && outcome.value) {
|
|
845
|
+
const internalResult = outcome.value.dispatch as DispatchResult & {
|
|
846
|
+
failureKind?: RunFailureKind;
|
|
847
|
+
failureClass?: FailureClass;
|
|
848
|
+
};
|
|
849
|
+
const {
|
|
850
|
+
failureKind,
|
|
851
|
+
failureClass: _failureClass,
|
|
852
|
+
...publicResult
|
|
853
|
+
} = internalResult;
|
|
854
|
+
if (publicResult.ok) {
|
|
855
|
+
this.notify(envelope, context, "completed");
|
|
856
|
+
return publicResult;
|
|
857
|
+
}
|
|
858
|
+
const state: RuntimeActivityState = failureKind ?? "failed";
|
|
859
|
+
this.notify(envelope, context, state);
|
|
860
|
+
return publicResult;
|
|
861
|
+
}
|
|
862
|
+
const state: RuntimeActivityState =
|
|
863
|
+
outcome.status === "cancelled" ? "cancelled" : "failed";
|
|
864
|
+
const terminal = {
|
|
865
|
+
ok: false as const,
|
|
866
|
+
error: outcome.error ?? `scheduled run ${outcome.status}`,
|
|
867
|
+
};
|
|
868
|
+
this.notify(envelope, context, state);
|
|
869
|
+
return terminal;
|
|
870
|
+
} finally {
|
|
871
|
+
signal?.removeEventListener("abort", cancelBatch);
|
|
872
|
+
this.runContexts.delete(envelope);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
private notify(
|
|
877
|
+
envelope: RequestEnvelope,
|
|
878
|
+
context: StoredRunContext,
|
|
879
|
+
state: RuntimeActivityState,
|
|
880
|
+
): void {
|
|
881
|
+
if (!context.observer || context.terminalEmitted) return;
|
|
882
|
+
if (state === "running") {
|
|
883
|
+
if (context.runningEmitted) return;
|
|
884
|
+
context.runningEmitted = true;
|
|
885
|
+
}
|
|
886
|
+
if (
|
|
887
|
+
state === "completed" ||
|
|
888
|
+
state === "failed" ||
|
|
889
|
+
state === "cancelled" ||
|
|
890
|
+
state === "timed-out"
|
|
891
|
+
) {
|
|
892
|
+
context.terminalEmitted = true;
|
|
893
|
+
}
|
|
894
|
+
const event: RuntimeActivityEvent = {
|
|
895
|
+
state,
|
|
896
|
+
requestId: envelope.id,
|
|
897
|
+
role: envelope.role,
|
|
898
|
+
phase: envelope.phase,
|
|
899
|
+
objective: envelope.objective,
|
|
900
|
+
sequence: context.sequence,
|
|
901
|
+
...(state === "cancelled"
|
|
902
|
+
? { failureReason: "subagent cancelled" as const }
|
|
903
|
+
: state === "timed-out"
|
|
904
|
+
? { failureReason: "phase timed out" as const }
|
|
905
|
+
: state === "failed"
|
|
906
|
+
? { failureReason: "subagent failed" as const }
|
|
907
|
+
: {}),
|
|
908
|
+
};
|
|
909
|
+
try {
|
|
910
|
+
const result = context.observer(event);
|
|
911
|
+
if (result instanceof Promise) void result.catch(() => undefined);
|
|
912
|
+
} catch {
|
|
913
|
+
// Presentation observers cannot affect orchestration.
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
private async runScheduled(
|
|
918
|
+
envelope: RequestEnvelope,
|
|
919
|
+
ctx: RunContext,
|
|
920
|
+
signal: AbortSignal,
|
|
921
|
+
): Promise<InternalDispatchResult> {
|
|
922
|
+
if (signal.aborted)
|
|
923
|
+
return {
|
|
924
|
+
...cancellationError(signal),
|
|
925
|
+
failureKind: "cancelled",
|
|
926
|
+
};
|
|
927
|
+
const taskId = envelope.taskId ?? envelope.id;
|
|
229
928
|
const agent = loadAgentDefinitions().find(
|
|
230
929
|
(item) => item.role === envelope.role,
|
|
231
930
|
);
|
|
232
|
-
if (!agent)
|
|
931
|
+
if (!agent)
|
|
932
|
+
return {
|
|
933
|
+
...recoveryFailure(
|
|
934
|
+
envelope,
|
|
935
|
+
"environment-blocked",
|
|
936
|
+
"repair-environment",
|
|
937
|
+
0,
|
|
938
|
+
),
|
|
939
|
+
};
|
|
233
940
|
if (!ctx.model)
|
|
234
|
-
return
|
|
941
|
+
return recoveryFailure(
|
|
942
|
+
envelope,
|
|
943
|
+
"environment-blocked",
|
|
944
|
+
"repair-environment",
|
|
945
|
+
0,
|
|
946
|
+
"failed",
|
|
947
|
+
"phase runtime is unavailable",
|
|
948
|
+
);
|
|
235
949
|
const identity = workerIdentity(ctx.model);
|
|
236
950
|
const contract = contractOf(envelope);
|
|
237
|
-
const existing = this.registry.get(
|
|
951
|
+
const existing = this.registry.get(taskId);
|
|
238
952
|
if (existing) {
|
|
239
953
|
if (existing.identity !== identity) {
|
|
240
|
-
return
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
954
|
+
return recoveryFailure(
|
|
955
|
+
envelope,
|
|
956
|
+
"environment-blocked",
|
|
957
|
+
"repair-environment",
|
|
958
|
+
existing.currentPhase.correctionIndex,
|
|
959
|
+
"failed",
|
|
960
|
+
"pinned provider/model identity differs",
|
|
961
|
+
);
|
|
245
962
|
}
|
|
246
|
-
if (!sameContract(existing.
|
|
247
|
-
return
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
+
);
|
|
252
985
|
}
|
|
253
|
-
if (
|
|
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
|
+
);
|
|
1056
|
+
}
|
|
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") {
|
|
254
1064
|
return {
|
|
255
1065
|
ok: false,
|
|
256
|
-
error: "
|
|
1066
|
+
error: "current phase candidate is not applied",
|
|
1067
|
+
failureKind: "failed",
|
|
257
1068
|
};
|
|
258
1069
|
}
|
|
259
|
-
|
|
260
|
-
|
|
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
|
+
};
|
|
261
1104
|
}
|
|
262
|
-
|
|
263
|
-
const first = await this.dispatchChild(
|
|
1105
|
+
|
|
1106
|
+
const first = await this.dispatchChild(
|
|
1107
|
+
agent,
|
|
1108
|
+
envelope,
|
|
1109
|
+
ctx,
|
|
1110
|
+
signal,
|
|
1111
|
+
artifactRejection,
|
|
1112
|
+
);
|
|
1113
|
+
if (signal.aborted)
|
|
1114
|
+
return {
|
|
1115
|
+
...cancellationError(signal),
|
|
1116
|
+
failureKind: "cancelled",
|
|
1117
|
+
};
|
|
264
1118
|
if (first.ok) {
|
|
265
|
-
|
|
1119
|
+
this.rememberRetainedResult(first, envelope, pinned);
|
|
266
1120
|
return first;
|
|
267
1121
|
}
|
|
268
|
-
if (
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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",
|
|
272
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
|
+
);
|
|
273
1151
|
}
|
|
274
|
-
|
|
275
|
-
|
|
1152
|
+
|
|
1153
|
+
if (first.failureClass === "artifact") {
|
|
1154
|
+
pinned.state = {
|
|
1155
|
+
kind: "artifact-correction-pending",
|
|
1156
|
+
rejection: normalizedArtifactRejection(first.error),
|
|
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
|
+
);
|
|
276
1166
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
1167
|
+
|
|
1168
|
+
pinned = this.registry.setCurrentPhase(taskId, contract, 1) ?? pinned;
|
|
1169
|
+
const second = await this.dispatchChild(agent, envelope, ctx, signal);
|
|
1170
|
+
if (signal.aborted)
|
|
280
1171
|
return {
|
|
281
|
-
|
|
282
|
-
|
|
1172
|
+
...cancellationError(signal),
|
|
1173
|
+
failureKind: "cancelled",
|
|
283
1174
|
};
|
|
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
|
+
);
|
|
1189
|
+
}
|
|
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;
|
|
284
1212
|
}
|
|
285
|
-
|
|
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" };
|
|
286
1229
|
}
|
|
287
1230
|
|
|
288
1231
|
validateRequest(
|
|
@@ -291,15 +1234,25 @@ export class Runtime {
|
|
|
291
1234
|
return validateRequestEnvelope(envelope);
|
|
292
1235
|
}
|
|
293
1236
|
|
|
294
|
-
drain(): void {
|
|
1237
|
+
async drain(): Promise<void> {
|
|
1238
|
+
this.parentPayloadBridge?.clear();
|
|
1239
|
+
const settled = this.scheduler.cancelAll();
|
|
1240
|
+
this.retainedRuns.clear();
|
|
295
1241
|
drainStage({
|
|
296
1242
|
results: this.results,
|
|
297
1243
|
registry: this.registry,
|
|
298
1244
|
activation: this.activation,
|
|
299
1245
|
});
|
|
1246
|
+
await settled;
|
|
300
1247
|
}
|
|
301
1248
|
|
|
302
1249
|
get state(): ActivationState {
|
|
303
1250
|
return this.activation.state;
|
|
304
1251
|
}
|
|
305
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
|
+
}
|