@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/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 { runChildSession } from "./child-session.ts";
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 { type Bound, mergeBounds } from "./file-snapshot.ts";
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 { ResultStore } from "./result-store.ts";
17
- import { Scheduler } from "./scheduler.ts";
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
- contractOf,
20
- sameContract,
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 cancellationError(signal: AbortSignal): { ok: false; error: string } {
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: DispatchResult;
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 DispatchResult =
601
+ export type WrappedDispatchResult =
119
602
  | {
120
603
  ok: true;
121
604
  action: string;
122
605
  result?: unknown;
123
606
  resultId?: string;
124
- usage?: unknown;
607
+ usage?: Usage;
125
608
  }
126
- | { ok: false; notReady?: true; error: string };
609
+ | {
610
+ ok: false;
611
+ notReady?: true;
612
+ error: string;
613
+ failure?: ChildFailure | TaskFailure;
614
+ usage?: Usage;
615
+ };
127
616
 
128
- export interface ApplyResult {
129
- targets: string[];
130
- checkExitCode: 0;
131
- applyExitCode: 0;
132
- sequence?: number;
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.ctx, signal);
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: { request?: unknown; resultId?: string },
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
- return { ok: false, error: `unknown action: ${String(action)}` };
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 || !params.resultId)
199
- return { ok: false, error: "apply requires context and resultId" };
200
- return this.enqueueApply(ctx.cwd, params.resultId);
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
- return { ok: false, error: "discard requires resultId" };
205
- return this.results.discard(params.resultId)
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 enqueueApply(root: string, id: string): Promise<DispatchResult> {
218
- const run = this.applyTail.then(
219
- () => applyRetainedPatch({ root, id, store: this.results }),
220
- () => applyRetainedPatch({ root, id, store: this.results }),
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
- const seq = ++this.applySeq;
227
- return run.then(
228
- (result) =>
229
- result.ok
230
- ? { ok: true, action: "apply", result: { ...result, sequence: seq } }
231
- : result,
232
- (error) => ({ ok: false, error: (error as Error).message }),
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
- ): Promise<DispatchResult & { failureKind?: RunFailureKind }> {
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
- let phase: Awaited<ReturnType<typeof runtimeFromContext>>;
248
- try {
249
- phase = await runtimeFromContext(ctx, signal);
250
- } catch (error) {
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: signal.aborted
254
- ? cancellationError(signal).error
255
- : (error as Error).message,
256
- failureKind: signal.aborted ? "cancelled" : "failed",
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
- if (!child.ok) return { ...child, failureKind: child.failureKind };
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
- root: ctx.cwd,
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
- if (!ctx) return { ok: false, error: "run requires extension context" };
328
- if (signal?.aborted) return cancellationError(signal);
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) return { ok: false, error: validation.reason };
331
- const envelope: RequestEnvelope = {
332
- ...validation.value,
333
- roots: [...validation.value.roots],
334
- context: { ...validation.value.context },
335
- declared: {
336
- ...validation.value.declared,
337
- read: [...validation.value.declared.read],
338
- write: [...validation.value.declared.write],
339
- conflicts: [...validation.value.declared.conflicts],
340
- resources: [...validation.value.declared.resources],
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 as DispatchResult & {
367
- failureKind?: RunFailureKind;
368
- };
369
- const { failureKind, ...publicResult } = internalResult;
370
- if (publicResult.ok) {
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 publicResult;
1642
+ return dispatch;
373
1643
  }
374
- const state: RuntimeActivityState = failureKind ?? "failed";
375
- this.notify(envelope, context, state);
376
- return publicResult;
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
- ctx: RunContext,
1724
+ context: StoredRunContext,
436
1725
  signal: AbortSignal,
437
- ): Promise<DispatchResult & { failureKind?: RunFailureKind }> {
438
- if (signal.aborted)
439
- return {
440
- ...cancellationError(signal),
441
- failureKind: "cancelled",
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: "package Agent role not found",
1749
+ error: "agent definition is unavailable",
1750
+ failure: { kind: "environment", code: "root-unavailable" },
450
1751
  failureKind: "failed",
1752
+ failureClass: "environment",
451
1753
  };
452
- if (!ctx.model)
1754
+ }
1755
+ if (!ctx.model) {
1756
+ if (task) throw new Error("implementation model is unavailable");
453
1757
  return {
454
1758
  ok: false,
455
- error: "run requires a model identity",
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
- const pinned = this.registry.get(envelope.id);
489
- const first = await this.dispatchChild(agent, envelope, ctx, signal);
490
- if (signal.aborted)
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
- ...cancellationError(signal),
493
- failureKind: "cancelled",
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
- if (pinned) pinned.redispatchUsed = false;
497
- return first;
1830
+ return finish(candidate(first));
498
1831
  }
499
- if (pinned?.redispatchUsed) {
500
- return {
501
- ok: false,
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
- if (!pinned) {
507
- return {
508
- ok: false,
509
- error: "blocked: logical Worker is not pinned",
510
- failureKind: "failed",
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
- pinned.redispatchUsed = true;
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
- if (signal.aborted)
516
- return {
517
- ...cancellationError(signal),
518
- failureKind: "cancelled",
519
- };
520
- if (!second.ok) {
521
- return {
522
- ok: false,
523
- error: "blocked: identical mechanical redispatch failed again",
524
- failureKind: second.failureKind ?? first.failureKind ?? "failed",
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
- return second;
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: RequestEnvelope } | { ok: false; reason: string } {
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
- const settled = this.scheduler.cancelAll();
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 {