@tea-agent/loop-agent 0.41.0 → 0.42.0-next.1

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.
Files changed (162) hide show
  1. package/CHANGELOG.md +102 -14
  2. package/dist/adapters/context-transfer/optional-pi-handoff.js +31 -0
  3. package/dist/adapters/context-transfer/pi-session.js +61 -0
  4. package/dist/build-stamp.json +3 -3
  5. package/dist/cli/command-definitions.js +12 -0
  6. package/dist/cli/program.js +23 -1
  7. package/dist/commands/dag-artifact.js +284 -0
  8. package/dist/commands/dag-context.js +184 -0
  9. package/dist/commands/dag-rerun.js +206 -1
  10. package/dist/executors/dag-pi-executor.js +39 -12
  11. package/dist/executors/pi-sdk-executor.js +3 -0
  12. package/dist/{worker → infrastructure}/console/app-data.js +4 -0
  13. package/dist/infrastructure/console/artifact-revision-store.js +430 -0
  14. package/dist/infrastructure/console/context-export-store.js +160 -0
  15. package/dist/infrastructure/console/dir-lock.js +132 -0
  16. package/dist/{worker → infrastructure}/console/operation-store.js +28 -0
  17. package/dist/infrastructure/harness/artifact-store.js +10 -1
  18. package/dist/infrastructure/harness/atomic-write.js +12 -2
  19. package/dist/shared/context-transfer/artifact-revision.js +172 -0
  20. package/dist/shared/context-transfer.js +418 -0
  21. package/dist/shared/operator/capabilities.js +14 -0
  22. package/dist/shared/operator/safe-run-summary.js +1 -0
  23. package/dist/shared/path-safety.js +93 -0
  24. package/dist/shared/preview.js +28 -4
  25. package/dist/task/config-types.js +2 -1
  26. package/dist/task/task-demand-routing.js +3 -0
  27. package/dist/worker/console/chat/chat-event-store.js +2 -2
  28. package/dist/worker/console/chat/resource-preferences-store.js +1 -1
  29. package/dist/worker/console/chat/routes.js +1 -1
  30. package/dist/worker/console/chat/session-store.js +1 -1
  31. package/dist/worker/console/chat/user-questions.js +1 -1
  32. package/dist/worker/console/console-update-runtime.js +1 -1
  33. package/dist/worker/console/context-transfer-diagnostics.js +198 -0
  34. package/dist/worker/console/dag-confirmation.js +1 -1
  35. package/dist/worker/console/dag-execution-receipt.js +1 -1
  36. package/dist/worker/console/doctor.js +1 -1
  37. package/dist/worker/console/draft-store.js +1 -1
  38. package/dist/worker/console/human-gate-token.js +1 -1
  39. package/dist/worker/console/index.js +3 -3
  40. package/dist/worker/console/interview/assessment.js +1 -1
  41. package/dist/worker/console/interview/session.js +1 -1
  42. package/dist/worker/console/operation-runner.js +45 -4
  43. package/dist/worker/console/operation-sse.js +1 -1
  44. package/dist/worker/console/operation-wait.js +1 -1
  45. package/dist/worker/console/operator-actions.js +104 -3
  46. package/dist/worker/console/operator-surface-health.js +1 -1
  47. package/dist/worker/console/operator-user-error.js +169 -0
  48. package/dist/worker/console/pi-plugins.js +57 -0
  49. package/dist/worker/console/routes.js +637 -8
  50. package/dist/worker/console/security.js +35 -0
  51. package/dist/worker/console/server.js +6 -3
  52. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DwVJxUxz.js → abnfDiagram-N423BO3Z-DZZ8m3PO.js} +1 -1
  53. package/dist/worker/console/static/assets/{arc-DqfCcXdu.js → arc-D6PvaVd-.js} +1 -1
  54. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-qMVtggRv.js → architectureDiagram-T3A2C74G-B_OTOiI8.js} +1 -1
  55. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-ECrV38dh.js → blockDiagram-VBNYF7ZC-Bv6rqHBg.js} +1 -1
  56. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DBaaLqQv.js → c4Diagram-5PPSVZJV-B8eHr0oz.js} +1 -1
  57. package/dist/worker/console/static/assets/channel-BU5gOilw.js +1 -0
  58. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BvfNHkAz.js → chunk-2GRJ4B5K-DYwR0im2.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-C5StiIbc.js → chunk-2Q5K7J3B-D2WPGqXt.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-HabGiJkS.js → chunk-5RXB4S5H-CQISJ_I7.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BiTg127i.js → chunk-5VM5RSS4-C0o2Du1e.js} +1 -1
  62. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-CZsXBPuR.js → chunk-6Q2QTUOP-4f8kr-U7.js} +1 -1
  63. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-D-r1MxLK.js → chunk-GF5L2VYU-D_OWvXzX.js} +1 -1
  64. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-DQxb9plr.js → chunk-JWPE2WC7-CasdPz5X.js} +1 -1
  65. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DMndGvCT.js → chunk-KBJHAD2P-Cj8lRrla.js} +1 -1
  66. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-ByL4BgOJ.js → chunk-RYQCIY6F-CUvD1FSd.js} +1 -1
  67. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BVe1b1gL.js → chunk-XXDRQBXY-CqLw_eqB.js} +1 -1
  68. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DIzKJGHr.js +1 -0
  69. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DIzKJGHr.js +1 -0
  70. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BAyGzN8j.js → cose-bilkent-JH36ORCC-ku-WqJPx.js} +1 -1
  71. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-C5yVYU7E.js → cynefin-VYW2F7L2-Bq_PYDhl.js} +1 -1
  72. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-x-MwsEm9.js → cynefinDiagram-MW4NZA55-BiGZV641.js} +1 -1
  73. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CT5Tle5F.js → dagre-VZM6K2ZE-C4JRlglF.js} +1 -1
  74. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BARERhlS.js → diagram-7IWD3JNH-B0dNeWYG.js} +1 -1
  75. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BwfUz0VM.js → diagram-B4RE2ZJO-Cj35YvYM.js} +1 -1
  76. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-Mk4r2WvJ.js → diagram-LBJQPF4R-uzQoJ2-8.js} +1 -1
  77. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-KUrxuBs0.js → diagram-Q27KOJAE-D1a-Buoz.js} +1 -1
  78. package/dist/worker/console/static/assets/{diagram-UB23O5K3-CjZOceZn.js → diagram-UB23O5K3-XjRrLRSs.js} +1 -1
  79. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-rnwUCiEI.js → ebnfDiagram-BXEA7PRR-Da_O24cW.js} +1 -1
  80. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DM4j23FM.js → erDiagram-JOGREHBK-BLJ8jrYU.js} +1 -1
  81. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DD5HdDEs.js → flowDiagram-UKHOOZJN-B9GrLjM0.js} +1 -1
  82. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CIAGSF47.js → ganttDiagram-PKOTCBZU-CeJ0TqiK.js} +1 -1
  83. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DbwFbVEL.js → gitGraphDiagram-DS77QQ5N-Bm2eNNFX.js} +1 -1
  84. package/dist/worker/console/static/assets/index-H9rFJiGL.css +1 -0
  85. package/dist/worker/console/static/assets/{index-Bih9w9h9.js → index-xwu9GxEc.js} +80 -80
  86. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-B-Pb0T66.js → infoDiagram-6WML65LV-DB26i3d8.js} +1 -1
  87. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-B9Unk-YX.js → ishikawaDiagram-WSZJBQD7-BOGRyeZT.js} +1 -1
  88. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-C2F3JkPt.js → journeyDiagram-NVQOT4AX-CZRoBO_6.js} +1 -1
  89. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Bl8-Xcoz.js → kanban-definition-27J2QSJJ-qlWhJyoB.js} +1 -1
  90. package/dist/worker/console/static/assets/{linear-yK4uZmC5.js → linear-CfUiDDB3.js} +1 -1
  91. package/dist/worker/console/static/assets/{mermaid.core-Bpa6PmuE.js → mermaid.core-BQe6fpqj.js} +5 -5
  92. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BfGRoCSe.js → mindmap-definition-FAOFIHXS-zFzWHw64.js} +1 -1
  93. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-Bz6_HtST.js → pegDiagram-VL7TDLO6-DFGUqjZO.js} +1 -1
  94. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-i0Umbsby.js → pieDiagram-7S7Q4E2Y-BB5i0l8Z.js} +1 -1
  95. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DaVQBobe.js → quadrantDiagram-CIZ2JOQS-obbVZ_X5.js} +1 -1
  96. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-DMTGnSq7.js → railroadDiagram-AXF67PYL-BwdaNzc1.js} +1 -1
  97. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-2ao_DaSq.js → requirementDiagram-LRYGKXZP-CkTJaugh.js} +1 -1
  98. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-2SG1lTB9.js → sankeyDiagram-W5VNT64P-BwJ-hIgq.js} +1 -1
  99. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BQYOLybR.js → sequenceDiagram-SI44F4Z6-BzSgmm7w.js} +1 -1
  100. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-C8g3yjuI.js → sizeCapture-X5ZJPWSS-Bn3oj5Q1.js} +1 -1
  101. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-q_F8nAQG.js → stateDiagram-OKZ733FA-dxhGtL8K.js} +1 -1
  102. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cld2qK9v.js +1 -0
  103. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-8LrWpH2s.js → swimlanes-SLNWSIFB-BLpEInya.js} +2 -2
  104. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-TIHpiT7w.js +8 -0
  105. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-BQoY3qzl.js → timeline-definition-Z64GVDOM-ChJGYcXy.js} +1 -1
  106. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BDu1UjSp.js → vennDiagram-T6HMQDX7-DK-qjRer.js} +1 -1
  107. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-C48M7Y0a.js → wardleyDiagram-T6FBY63Y-3qeaWAg-.js} +1 -1
  108. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-YU6FTl_F.js → xychartDiagram-ELKLHX3M-CootlyP9.js} +1 -1
  109. package/dist/worker/console/static/index.html +7 -2
  110. package/dist/worker/console/static-src/app/useOperatorActions.js +3 -2
  111. package/dist/worker/console/static-src/shell/console-update-reload.js +25 -0
  112. package/dist/worker/console/workspace-context.js +28 -2
  113. package/dist/worker/console/workspace-registry.js +2 -2
  114. package/dist/worker/continuation/worker-continuation.js +278 -0
  115. package/dist/worker/observe/health.js +1 -1
  116. package/dist/worker/observe/node-transparency.js +572 -0
  117. package/dist/worker/observe/routes.js +51 -1
  118. package/dist/worker/observe/static/api.js +69 -5
  119. package/dist/worker/observe/static/dag-context-reason-labels.d.ts +9 -0
  120. package/dist/worker/observe/static/dag-context-reason-labels.js +120 -0
  121. package/dist/worker/observe/static/state.js +3 -1
  122. package/dist/worker/observe/static/styles.css +1037 -126
  123. package/dist/worker/observe/static/views/dag-inspector.js +1215 -58
  124. package/dist/worker/observe/static/views/session-timeline.js +75 -10
  125. package/dist/worker/outcomes/adapters.js +19 -0
  126. package/dist/worker/outcomes/types.js +1 -0
  127. package/dist/worker/pool/attempt-lease.js +97 -66
  128. package/dist/worker/pool/begin-attempt-with-lease.js +1 -0
  129. package/dist/worker/pool/reconcile.js +46 -1
  130. package/dist/worker/pool/run-store.js +2 -0
  131. package/dist/worker/pool/state-projection.js +2 -0
  132. package/dist/worker/task-spec/workflow-routing.js +8 -3
  133. package/dist/workflows/dag/artifact-bindings.js +149 -0
  134. package/dist/workflows/dag/artifact-revision-schema-registry.js +31 -0
  135. package/dist/workflows/dag/budget-enforcement.js +10 -0
  136. package/dist/workflows/dag/context-receipt.js +305 -0
  137. package/dist/workflows/dag/context-transfer/context-bundle.js +423 -0
  138. package/dist/workflows/dag/context-transfer/operator-actions.js +61 -0
  139. package/dist/workflows/dag/context-transfer/renderers.js +93 -0
  140. package/dist/workflows/dag/frontend-writer-rollback.js +32 -0
  141. package/dist/workflows/dag/init-hybrid.js +146 -0
  142. package/dist/workflows/dag/node-execution.js +112 -27
  143. package/dist/workflows/dag/path-safety.js +1 -0
  144. package/dist/workflows/dag/prompt.js +29 -4
  145. package/dist/workflows/dag/rerun-plan.js +555 -1
  146. package/dist/workflows/dag/rerun-run.js +601 -15
  147. package/dist/workflows/dag/runner.js +20 -1
  148. package/dist/workflows/dag/skill-snapshot.js +17 -0
  149. package/dist/workflows/dag/types.js +183 -7
  150. package/docs/architecture/runtime-boundaries.md +6 -6
  151. package/docs/architecture/worker-and-feature.md +1 -1
  152. package/docs/templates/agent-dag.schema.json +32 -0
  153. package/docs/templates/product-line/task.yaml +1 -1
  154. package/package.json +3 -1
  155. package/skills/loop-agent/references/command-reference.md +6 -0
  156. package/dist/worker/console/static/assets/channel-CL1NRAmS.js +0 -1
  157. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-PBGzzjZA.js +0 -1
  158. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-PBGzzjZA.js +0 -1
  159. package/dist/worker/console/static/assets/index-BfleX-qj.css +0 -1
  160. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DrxGBDJu.js +0 -1
  161. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BhpDxzAf.js +0 -8
  162. /package/dist/{worker → infrastructure}/console/repo-fingerprint.js +0 -0
@@ -1,23 +1,56 @@
1
- import { createHash } from "node:crypto";
2
- import { access, copyFile, mkdir, readdir, readFile } from "node:fs/promises";
1
+ import { createHash, timingSafeEqual } from "node:crypto";
2
+ import { access, copyFile, lstat, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
+ import { z } from "zod";
4
5
  import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
5
6
  import { composeDagPromptOverridePrompt, isDagRerunPromptOverrideEnabled, } from "../../shared/dag-prompt-override.js";
6
7
  import { resolveRunningControllerIdentity } from "../../shared/package-metadata.js";
7
8
  import { readControllerIdentityArtifact, captureControllerIdentity } from "./controller-identity.js";
8
- import { DAG_RUNS_DIR, getDagRunDir, isDagNonExecutionLifecycle, locateDagRun, readDagRunSpec, readDagRunState, } from "./lifecycle.js";
9
+ import { DAG_RUNS_DIR, getDagRunDir, isDagNonExecutionLifecycle, locateDagRun, readDagRunSpec, readDagRunState, transferDagRunDir, } from "./lifecycle.js";
9
10
  import { buildDagRunId, runDagContinuation, } from "./runner.js";
10
- import { computePlanHash, evaluateDagRerunPlan, evaluateLiveDagRerunBindings, } from "./rerun-plan.js";
11
+ import { computePlanHash, evaluateArtifactOverrideRerunPlan, evaluateDagRerunPlan, evaluateLiveDagRerunBindings, } from "./rerun-plan.js";
11
12
  import { prepareActiveRunDir, writeNodeRecord, writeRunSpec, writeRunState, } from "./run-store.js";
12
13
  import { cloneParentSkillSnapshotForContinuation, readSkillSnapshot, } from "./skill-snapshot.js";
13
14
  import { topoSortToRanks } from "./topo.js";
14
15
  import { captureWorkspaceCheckpoint, readWorkspaceCheckpoint, WORKSPACE_CHECKPOINT_START_REL, WORKSPACE_CHECKPOINT_TERMINAL_REL, writeWorkspaceCheckpoint, } from "./workspace-checkpoint.js";
16
+ import { captureWriterBaseline, readJournalFile, recordWriterAttemptFromCurrent, restoreJournal, writeJournalFile, } from "./frontend-writer-rollback.js";
15
17
  export const RERUN_PLAN_REL_PATH = ".runtime/rerun-plan.json";
16
18
  export const IMPORT_MANIFEST_REL_PATH = ".runtime/import-manifest.json";
17
19
  export const WORKER_ASSOCIATION_REL_PATH = ".runtime/worker-association.json";
20
+ export const CONTINUATION_WRITER_ROLLBACK_REL_PATH = ".runtime/continuation-writer-rollback.json";
21
+ export const CONTINUATION_WRITER_TRANSACTION_REL_PATH = ".runtime/continuation-writer-transaction.json";
18
22
  function sha256Hex(content) {
19
23
  return createHash("sha256").update(content).digest("hex");
20
24
  }
25
+ function constantTimeHashEqual(left, right) {
26
+ if (!/^[a-f0-9]{64}$/.test(left) || !/^[a-f0-9]{64}$/.test(right))
27
+ return false;
28
+ return timingSafeEqual(Buffer.from(left, "hex"), Buffer.from(right, "hex"));
29
+ }
30
+ export async function snapshotDagRunManifest(runDir) {
31
+ const entries = [];
32
+ const walk = async (dir) => {
33
+ for (const entry of (await readdir(dir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
34
+ const absolute = path.join(dir, entry.name);
35
+ const relative = normalizeRunRelativePath(path.relative(runDir, absolute));
36
+ if (entry.isSymbolicLink())
37
+ throw new Error(`symlink rejected in DAG run manifest: ${relative}`);
38
+ if (entry.isDirectory()) {
39
+ await walk(absolute);
40
+ continue;
41
+ }
42
+ if (!entry.isFile())
43
+ throw new Error(`non-regular DAG run entry: ${relative}`);
44
+ const stat = await lstat(absolute);
45
+ if (stat.nlink !== 1)
46
+ throw new Error(`hardlink rejected in DAG run manifest: ${relative}`);
47
+ const bytes = await readFile(absolute);
48
+ entries.push({ path: relative, sha256: sha256Hex(bytes), bytes: bytes.byteLength });
49
+ }
50
+ };
51
+ await walk(runDir);
52
+ return sha256Hex(JSON.stringify(entries));
53
+ }
21
54
  async function fileExists(filePath) {
22
55
  try {
23
56
  await access(filePath);
@@ -30,18 +63,67 @@ async function fileExists(filePath) {
30
63
  function operatorRequestPath(cwd, requestId) {
31
64
  return path.join(cwd, DAG_RUNS_DIR, ".operator-requests", `${requestId}.json`);
32
65
  }
66
+ /** Strict envelope for persisted operator-request replays (SF2). */
67
+ const operatorRequestEnvelopeSchema = z
68
+ .object({
69
+ schemaVersion: z.literal(1),
70
+ kind: z.literal("dag-rerun-from-node-request"),
71
+ requestId: z.string().min(1),
72
+ parentRunId: z.string().min(1),
73
+ newRunId: z.string().min(1),
74
+ createdAt: z.string().min(1),
75
+ result: z
76
+ .object({
77
+ ok: z.boolean(),
78
+ plan: z
79
+ .object({
80
+ parentRunId: z.string().min(1),
81
+ planHash: z.string().min(1),
82
+ })
83
+ .passthrough(),
84
+ newRunId: z.string().min(1).optional(),
85
+ })
86
+ .passthrough(),
87
+ })
88
+ .strict();
33
89
  async function readExistingOperatorRequest(cwd, requestId) {
34
90
  try {
35
91
  const raw = JSON.parse(await readFile(operatorRequestPath(cwd, requestId), "utf-8"));
36
- if (raw.result && typeof raw.result === "object") {
37
- return { ...raw.result, idempotentReplay: true };
38
- }
92
+ const parsed = operatorRequestEnvelopeSchema.safeParse(raw);
93
+ if (!parsed.success)
94
+ return undefined;
95
+ const result = parsed.data.result;
96
+ return {
97
+ ...result,
98
+ plan: result.plan,
99
+ idempotentReplay: true,
100
+ };
39
101
  }
40
102
  catch {
41
103
  // not found or unreadable
42
104
  }
43
105
  return undefined;
44
106
  }
107
+ /**
108
+ * Idempotent replay of a previously persisted operator request, if any.
109
+ * When expected context is supplied, a replay that belongs to a different
110
+ * parent run or plan is a request-id reuse conflict rather than a valid
111
+ * answer.
112
+ */
113
+ export async function readOperatorRequestReplay(cwd, requestId, expected) {
114
+ const replay = await readExistingOperatorRequest(cwd, requestId);
115
+ if (!replay)
116
+ return undefined;
117
+ if (expected?.parentRunId !== undefined &&
118
+ replay.plan?.parentRunId !== expected.parentRunId) {
119
+ throw new Error("REQUEST_ID_REUSE_CONFLICT: operator request replay belongs to a different parent run");
120
+ }
121
+ if (expected?.planHash !== undefined &&
122
+ replay.plan?.planHash !== expected.planHash) {
123
+ throw new Error("REQUEST_ID_REUSE_CONFLICT: operator request replay belongs to a different plan");
124
+ }
125
+ return replay;
126
+ }
45
127
  async function persistOperatorRequest(cwd, result, requestId) {
46
128
  await writeJsonAtomic(operatorRequestPath(cwd, requestId), {
47
129
  schemaVersion: 1,
@@ -246,6 +328,54 @@ export async function planDagRerun(input) {
246
328
  bindingStatus,
247
329
  });
248
330
  }
331
+ export async function planDagArtifactOverride(input) {
332
+ const located = await locateDagRun(input.cwd, input.parentRunId);
333
+ if (!located)
334
+ throw new Error(`dag run not found: ${input.parentRunId}`);
335
+ if (located.lifecycle !== "completed") {
336
+ throw new Error(`artifact continuation requires completed parent: ${input.parentRunId}`);
337
+ }
338
+ const parentSpec = await readDagRunSpec(located.runDir);
339
+ const parentState = await readDagRunState(located.runDir);
340
+ const currentWorkspace = await captureWorkspaceCheckpoint(input.cwd).catch(() => undefined);
341
+ const parentTerminalWorkspace = await readWorkspaceCheckpoint(located.runDir, WORKSPACE_CHECKPOINT_TERMINAL_REL);
342
+ const fingerprints = await resolveControllerFingerprints(located.runDir);
343
+ const skillSnapshotOk = await verifyParentSkillSnapshot(located.runDir, parentState, input.parentRunId);
344
+ const workerHints = await detectWorkerManagedRun(located.runDir, parentState, parentSpec);
345
+ const liveBindings = await evaluateLiveDagRerunBindings({ cwd: input.cwd, parentSpec });
346
+ return evaluateArtifactOverrideRerunPlan({
347
+ parentRunId: input.parentRunId,
348
+ parentSpec,
349
+ parentState,
350
+ artifactId: input.artifactId,
351
+ artifactOverrides: input.artifactOverrides,
352
+ stagingRevisionSha256: input.stagingRevisionSha256,
353
+ parentLifecycle: "completed",
354
+ bindings: {
355
+ ...liveBindings,
356
+ controllerIdentityMatched: fingerprints.currentControllerFingerprint === undefined ||
357
+ fingerprints.parentControllerFingerprint === undefined ||
358
+ fingerprints.currentControllerFingerprint === fingerprints.parentControllerFingerprint,
359
+ skillSnapshotVerified: skillSnapshotOk,
360
+ workspaceMatched: parentTerminalWorkspace?.fingerprint !== undefined &&
361
+ currentWorkspace?.fingerprint !== undefined &&
362
+ parentTerminalWorkspace.fingerprint === currentWorkspace.fingerprint,
363
+ },
364
+ parentTerminalWorkspaceFingerprint: parentTerminalWorkspace?.fingerprint,
365
+ currentWorkspaceFingerprint: currentWorkspace?.fingerprint,
366
+ workerAssociation: workerHints.workerManaged
367
+ ? {
368
+ kind: "worker-managed",
369
+ ...(workerHints.featureId ? { featureId: workerHints.featureId } : {}),
370
+ ...(workerHints.taskId ? { taskId: workerHints.taskId } : {}),
371
+ ...(workerHints.workerRunId ? { workerRunId: workerHints.workerRunId } : {}),
372
+ }
373
+ : { kind: "standalone" },
374
+ ...(input.workerContinuationAuthority
375
+ ? { workerContinuationAuthority: input.workerContinuationAuthority }
376
+ : {}),
377
+ });
378
+ }
249
379
  function normalizeRunRelativePath(relPath) {
250
380
  return relPath.replace(/\\/g, "/");
251
381
  }
@@ -359,6 +489,18 @@ function buildImportedNodeRecord(parentRecord, parentRunId, sourceNodeRecordSha2
359
489
  const { origin: _origin, ...rest } = parentRecord;
360
490
  void _origin;
361
491
  const imported = structuredClone(rest);
492
+ imported.declaredArtifacts = imported.declaredArtifacts?.map((artifact) => ({
493
+ ...artifact,
494
+ ...(artifact.sha256
495
+ ? {
496
+ origin: {
497
+ kind: "imported",
498
+ parentRunId,
499
+ sourceSha256: artifact.sha256,
500
+ },
501
+ }
502
+ : {}),
503
+ }));
362
504
  imported.nodeRecordPath = path.join(newRunDir, `${parentRecord.id}.json`);
363
505
  for (const key of [
364
506
  "stdoutArtifactPath",
@@ -424,25 +566,204 @@ function applyPromptOverridesToSpec(input) {
424
566
  }
425
567
  return spec;
426
568
  }
569
+ function collectMaterializedDynamicChildIds(spec, dynamicParentNodeIds) {
570
+ const parentSet = new Set(dynamicParentNodeIds);
571
+ const matchers = [];
572
+ for (const task of spec.tasks) {
573
+ if (!parentSet.has(task.id))
574
+ continue;
575
+ if (task.dynamicExpansion) {
576
+ const escaped = task.dynamicExpansion.childIdPrefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
577
+ matchers.push(new RegExp(`^${escaped}-\\d{4}$`));
578
+ }
579
+ if (task.dynamicLoopUntil) {
580
+ const escaped = task.id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
581
+ matchers.push(new RegExp(`^${escaped}-r\\d+-`));
582
+ }
583
+ }
584
+ return new Set(spec.tasks
585
+ .map((task) => task.id)
586
+ .filter((nodeId) => matchers.some((matcher) => matcher.test(nodeId))));
587
+ }
588
+ async function applyArtifactOverrideToStagedRun(input) {
589
+ const override = input.plan.artifactOverrides?.overrides[0];
590
+ if (!override)
591
+ throw new Error("artifact override missing from V2 plan");
592
+ if (override.parentArtifact.runId !== input.parentRunId) {
593
+ throw new Error("artifact override parent run drift");
594
+ }
595
+ if (!constantTimeHashEqual(sha256Hex(input.candidateBytes), override.replacement.sha256)) {
596
+ throw new Error("ARTIFACT_REVISION_DRIFT: candidate hash mismatch");
597
+ }
598
+ if (input.candidateBytes.byteLength !== override.replacement.bytes) {
599
+ throw new Error("ARTIFACT_REVISION_DRIFT: candidate byte length mismatch");
600
+ }
601
+ const relativePath = assertArtifactPathWithinParentRun(input.parentRunDir, override.parentArtifact.path);
602
+ const parentBytes = await readFile(path.join(input.parentRunDir, ...relativePath.split("/")));
603
+ if (!constantTimeHashEqual(sha256Hex(parentBytes), override.parentArtifact.sha256)) {
604
+ throw new Error("ARTIFACT_REVISION_DRIFT: parent artifact changed");
605
+ }
606
+ const destinationPath = path.join(input.newRunDir, ...relativePath.split("/"));
607
+ const resolvedRun = path.resolve(input.newRunDir);
608
+ if (!path.resolve(destinationPath).startsWith(`${resolvedRun}${path.sep}`)) {
609
+ throw new Error("ARTIFACT_REVISION_PATH_INVALID");
610
+ }
611
+ await mkdir(path.dirname(destinationPath), { recursive: true });
612
+ await writeFile(destinationPath, input.candidateBytes, { mode: 0o600 });
613
+ const producer = input.nodes[override.parentArtifact.nodeId];
614
+ if (!producer || producer.origin?.kind !== "imported") {
615
+ throw new Error("artifact producer must be imported into continuation");
616
+ }
617
+ producer.structuredArtifactPath = destinationPath;
618
+ producer.structuredArtifactSha256 = override.replacement.sha256;
619
+ producer.declaredArtifacts = producer.declaredArtifacts?.map((artifact) => artifact.path === relativePath && artifact.sha256 === override.parentArtifact.sha256
620
+ ? {
621
+ ...artifact,
622
+ sha256: override.replacement.sha256,
623
+ bytes: override.replacement.bytes,
624
+ origin: {
625
+ kind: "overridden",
626
+ parentRunId: input.parentRunId,
627
+ revisionId: override.revisionId,
628
+ supersedes: override.supersedes,
629
+ },
630
+ }
631
+ : artifact);
632
+ await writeNodeRecord(input.newRunDir, override.parentArtifact.nodeId, producer);
633
+ const manifestNode = input.manifestNodes.find((entry) => entry.nodeId === override.parentArtifact.nodeId);
634
+ if (manifestNode) {
635
+ manifestNode.importedArtifacts = manifestNode.importedArtifacts.filter((artifact) => artifact.destinationRelativePath !== relativePath);
636
+ }
637
+ return {
638
+ revisionId: override.revisionId,
639
+ producerNodeId: override.parentArtifact.nodeId,
640
+ destinationRelativePath: relativePath,
641
+ parentSha256: override.parentArtifact.sha256,
642
+ replacementSha256: override.replacement.sha256,
643
+ bytes: override.replacement.bytes,
644
+ mediaType: override.replacement.mediaType,
645
+ supersedes: override.supersedes,
646
+ };
647
+ }
648
+ async function captureContinuationWriterTransaction(input) {
649
+ const writer = input.plan.schemaVersion === 2 ? input.plan.highRiskAuthority?.writer : undefined;
650
+ if (!writer)
651
+ return;
652
+ const snapshot = await captureWriterBaseline(input.cwd, writer.writeSet);
653
+ await writeJournalFile(path.join(input.runDir, CONTINUATION_WRITER_ROLLBACK_REL_PATH), snapshot);
654
+ await writeJsonAtomic(path.join(input.runDir, CONTINUATION_WRITER_TRANSACTION_REL_PATH), {
655
+ schemaVersion: 1,
656
+ state: "prepared",
657
+ preparedAt: new Date().toISOString(),
658
+ writerNodeIds: writer.nodeIds,
659
+ writeSet: writer.writeSet,
660
+ writeSetSha256: writer.writeSetSha256,
661
+ });
662
+ }
663
+ async function finalizeContinuationWriterTransaction(input) {
664
+ const writer = input.plan.schemaVersion === 2 ? input.plan.highRiskAuthority?.writer : undefined;
665
+ if (!writer)
666
+ return { state: "not-applicable" };
667
+ const transactionPath = path.join(input.runDir, CONTINUATION_WRITER_TRANSACTION_REL_PATH);
668
+ if (input.committed) {
669
+ await writeJsonAtomic(transactionPath, {
670
+ schemaVersion: 1,
671
+ state: "committed",
672
+ committedAt: new Date().toISOString(),
673
+ writerNodeIds: writer.nodeIds,
674
+ writeSet: writer.writeSet,
675
+ writeSetSha256: writer.writeSetSha256,
676
+ });
677
+ return { state: "committed" };
678
+ }
679
+ const journalPath = path.join(input.runDir, CONTINUATION_WRITER_ROLLBACK_REL_PATH);
680
+ const baseline = await readJournalFile(journalPath);
681
+ if (!baseline) {
682
+ const reason = "continuation writer rollback journal is missing or invalid";
683
+ await writeJsonAtomic(transactionPath, {
684
+ schemaVersion: 1,
685
+ state: "blocked",
686
+ blockedAt: new Date().toISOString(),
687
+ reason,
688
+ writerNodeIds: writer.nodeIds,
689
+ writeSet: writer.writeSet,
690
+ writeSetSha256: writer.writeSetSha256,
691
+ });
692
+ return { state: "blocked", reason };
693
+ }
694
+ const recorded = await recordWriterAttemptFromCurrent(baseline, {
695
+ repoRoot: input.cwd,
696
+ });
697
+ await writeJournalFile(journalPath, recorded);
698
+ await input.onAttemptRecorded?.();
699
+ const restored = await restoreJournal(recorded, { repoRoot: input.cwd });
700
+ await writeJournalFile(journalPath, restored.journal);
701
+ if (restored.state === "blocked") {
702
+ const reason = restored.blockedReason ?? "continuation writer rollback boundary is unprovable";
703
+ await writeJsonAtomic(transactionPath, {
704
+ schemaVersion: 1,
705
+ state: "blocked",
706
+ blockedAt: new Date().toISOString(),
707
+ reason,
708
+ restoredPaths: restored.restoredPaths,
709
+ writerNodeIds: writer.nodeIds,
710
+ writeSet: writer.writeSet,
711
+ writeSetSha256: writer.writeSetSha256,
712
+ });
713
+ return { state: "blocked", reason };
714
+ }
715
+ await writeJsonAtomic(transactionPath, {
716
+ schemaVersion: 1,
717
+ state: "rolled-back",
718
+ rolledBackAt: new Date().toISOString(),
719
+ restoredPaths: restored.restoredPaths,
720
+ writerNodeIds: writer.nodeIds,
721
+ writeSet: writer.writeSet,
722
+ writeSetSha256: writer.writeSetSha256,
723
+ });
724
+ return { state: "rolled-back" };
725
+ }
427
726
  async function stageContinuationRun(input) {
428
- const specForRun = input.promptOverrides !== undefined && input.promptOverrides.length > 0
727
+ const promptAdjustedSpec = input.promptOverrides !== undefined && input.promptOverrides.length > 0
429
728
  ? applyPromptOverridesToSpec({
430
729
  spec: input.parentSpec,
431
730
  plan: input.plan,
432
731
  overrides: input.promptOverrides,
433
732
  })
434
- : input.parentSpec;
733
+ : structuredClone(input.parentSpec);
734
+ const dynamicParentNodeIds = input.plan.schemaVersion === 2
735
+ ? (input.plan.highRiskAuthority?.dynamic?.nodeIds ?? [])
736
+ : [];
737
+ const materializedDynamicChildIds = collectMaterializedDynamicChildIds(promptAdjustedSpec, dynamicParentNodeIds);
738
+ const specForRun = {
739
+ ...promptAdjustedSpec,
740
+ tasks: promptAdjustedSpec.tasks.filter((task) => !materializedDynamicChildIds.has(task.id)),
741
+ };
435
742
  const { ranks } = topoSortToRanks(specForRun);
436
- const runId = await buildDagRunId(specForRun, { cwd: input.cwd });
743
+ const workerContinuationAuthority = input.plan.schemaVersion === 2
744
+ ? input.plan.workerContinuationAuthority
745
+ : undefined;
746
+ const runId = workerContinuationAuthority?.childWorkerRunId
747
+ ?? await buildDagRunId(specForRun, { cwd: input.cwd });
748
+ if (workerContinuationAuthority && await locateDagRun(input.cwd, runId)) {
749
+ throw new Error(`WORKER_CONTINUATION_NEEDS_RECONCILE: child DAG already exists: ${runId}`);
750
+ }
437
751
  const runDir = getDagRunDir(input.cwd, "active", runId);
438
752
  await prepareActiveRunDir(runDir);
439
753
  await writeRunSpec(runDir, specForRun);
754
+ await captureContinuationWriterTransaction({
755
+ cwd: input.cwd,
756
+ runDir,
757
+ plan: input.plan,
758
+ });
440
759
  const resetSet = new Set(input.plan.resetNodeIds);
441
760
  const importedSet = new Set(input.plan.importedNodeIds);
442
761
  const tasksById = new Map(specForRun.tasks.map((task) => [task.id, task]));
443
762
  const importManifestNodes = [];
444
763
  const nodes = {};
445
764
  for (const nodeId of input.plan.importedNodeIds) {
765
+ if (materializedDynamicChildIds.has(nodeId))
766
+ continue;
446
767
  const parentRecord = input.parentState.nodes[nodeId];
447
768
  if (!parentRecord) {
448
769
  throw new Error(`parent node record missing for imported node: ${nodeId}`);
@@ -458,6 +779,8 @@ async function stageContinuationRun(input) {
458
779
  nodes[nodeId] = importedRecord;
459
780
  }
460
781
  for (const nodeId of input.plan.resetNodeIds) {
782
+ if (materializedDynamicChildIds.has(nodeId))
783
+ continue;
461
784
  const task = tasksById.get(nodeId);
462
785
  if (!task) {
463
786
  throw new Error(`reset node missing from spec: ${nodeId}`);
@@ -465,6 +788,8 @@ async function stageContinuationRun(input) {
465
788
  nodes[nodeId] = buildPendingNodeRecord(task);
466
789
  }
467
790
  for (const task of input.parentSpec.tasks) {
791
+ if (materializedDynamicChildIds.has(task.id))
792
+ continue;
468
793
  if (!resetSet.has(task.id) && !importedSet.has(task.id)) {
469
794
  throw new Error(`node ${task.id} missing from rerun plan partition`);
470
795
  }
@@ -483,6 +808,29 @@ async function stageContinuationRun(input) {
483
808
  ...(input.plan.promptOverrides
484
809
  ? { promptOverrides: input.plan.promptOverrides }
485
810
  : {}),
811
+ ...(input.plan.schemaVersion === 2 && input.plan.artifactOverrides
812
+ ? { artifactOverrides: input.plan.artifactOverrides }
813
+ : {}),
814
+ ...(workerContinuationAuthority
815
+ ? { workerContinuationAuthority }
816
+ : {}),
817
+ ...(input.plan.schemaVersion === 2 && input.plan.highRiskAuthority?.budget
818
+ ? {
819
+ budgetLineage: {
820
+ parentLedgerSha256: input.plan.highRiskAuthority.budget.parentLedgerSha256,
821
+ inheritedAt: createdAt,
822
+ inheritedConsumed: structuredClone(input.plan.highRiskAuthority.budget.inheritedConsumed),
823
+ },
824
+ }
825
+ : {}),
826
+ ...(input.plan.schemaVersion === 2 && input.plan.highRiskAuthority?.writer
827
+ ? {
828
+ writerTransaction: {
829
+ writeSetSha256: input.plan.highRiskAuthority.writer.writeSetSha256,
830
+ writerNodeIds: input.plan.highRiskAuthority.writer.nodeIds,
831
+ },
832
+ }
833
+ : {}),
486
834
  createdAt,
487
835
  };
488
836
  const state = {
@@ -504,6 +852,19 @@ async function stageContinuationRun(input) {
504
852
  ...(input.parentState.budget
505
853
  ? { budget: structuredClone(input.parentState.budget) }
506
854
  : {}),
855
+ ...(input.parentState.budgetLedger
856
+ ? { budgetLedger: structuredClone(input.parentState.budgetLedger) }
857
+ : {}),
858
+ ...(workerContinuationAuthority
859
+ ? {
860
+ workerAssociation: {
861
+ schemaVersion: 1,
862
+ workerRunId: workerContinuationAuthority.childWorkerRunId,
863
+ featureId: workerContinuationAuthority.featureId,
864
+ taskId: workerContinuationAuthority.taskId,
865
+ },
866
+ }
867
+ : {}),
507
868
  };
508
869
  if (!input.parentState.skillSnapshotRef) {
509
870
  throw new Error("parent run is missing skill snapshot ref");
@@ -514,30 +875,67 @@ async function stageContinuationRun(input) {
514
875
  parentRunId: input.parentRunId,
515
876
  newRunId: runId,
516
877
  newRunDir: runDir,
878
+ spec: specForRun,
517
879
  });
518
880
  state.controllerIdentityRef = await captureControllerIdentity({ runDir });
519
881
  try {
520
882
  const startCheckpoint = await captureWorkspaceCheckpoint(input.cwd);
883
+ const highRiskWorkspace = input.plan.schemaVersion === 2
884
+ ? input.plan.highRiskAuthority?.workspaceFingerprint
885
+ : undefined;
886
+ if (highRiskWorkspace && startCheckpoint.fingerprint !== highRiskWorkspace) {
887
+ throw new Error("WORKSPACE_DRIFT: continuation staging no longer matches the planned parent terminal workspace");
888
+ }
521
889
  await writeWorkspaceCheckpoint(runDir, WORKSPACE_CHECKPOINT_START_REL, startCheckpoint);
522
890
  }
523
891
  catch (error) {
892
+ if (input.plan.schemaVersion === 2 && input.plan.highRiskAuthority) {
893
+ throw error;
894
+ }
524
895
  console.warn(`[dag rerun] warning: failed to write workspace start checkpoint: ${error instanceof Error ? error.message : String(error)}`);
525
896
  }
897
+ const overrideManifest = input.plan.schemaVersion === 2 && input.plan.trigger === "artifact-override"
898
+ ? [await applyArtifactOverrideToStagedRun({
899
+ parentRunDir: input.parentRunDir,
900
+ newRunDir: runDir,
901
+ parentRunId: input.parentRunId,
902
+ plan: input.plan,
903
+ candidateBytes: input.artifactCandidateBytes ?? Buffer.alloc(0),
904
+ nodes,
905
+ manifestNodes: importManifestNodes,
906
+ })]
907
+ : undefined;
526
908
  const importManifest = {
527
909
  schemaVersion: 1,
528
910
  parentRunId: input.parentRunId,
529
911
  createdAt,
530
912
  nodes: importManifestNodes,
913
+ ...(overrideManifest ? { overrides: overrideManifest } : {}),
531
914
  };
532
915
  await writeJsonAtomic(path.join(runDir, IMPORT_MANIFEST_REL_PATH), importManifest);
533
916
  await writeJsonAtomic(path.join(runDir, RERUN_PLAN_REL_PATH), input.plan);
534
917
  await writeRunState(runDir, state);
535
918
  return { runId, runDir, state, spec: specForRun };
536
919
  }
920
+ async function settleStagedContinuationWithoutExecution(input) {
921
+ input.staged.state.status = "failed";
922
+ input.staged.state.finishedAt = new Date().toISOString();
923
+ input.staged.state.failureCategory = input.failureCategory;
924
+ input.staged.state.terminalReason = input.terminalReason;
925
+ await writeRunState(input.staged.runDir, input.staged.state);
926
+ return transferDagRunDir(input.staged.runDir, getDagRunDir(input.cwd, "completed", input.staged.runId));
927
+ }
537
928
  export async function executeDagRerun(input) {
538
- const existing = await readExistingOperatorRequest(input.cwd, input.requestId);
539
- if (existing) {
540
- return existing;
929
+ // Replay-first with SF2 envelope validation at the core boundary (same
930
+ // contract as executeDagArtifactOverrideRerun): an existing operator
931
+ // request is only a valid idempotent answer when it matches the requested
932
+ // parent run and plan hash; otherwise REQUEST_ID_REUSE_CONFLICT.
933
+ const replay = await readOperatorRequestReplay(input.cwd, input.requestId, {
934
+ parentRunId: input.parentRunId,
935
+ planHash: input.planHash,
936
+ });
937
+ if (replay) {
938
+ return replay;
541
939
  }
542
940
  const plan = await planDagRerun({
543
941
  cwd: input.cwd,
@@ -545,7 +943,7 @@ export async function executeDagRerun(input) {
545
943
  selectedNodeId: input.selectedNodeId,
546
944
  promptOverrides: input.promptOverrides,
547
945
  });
548
- if (plan.planHash !== input.planHash) {
946
+ if (!constantTimeHashEqual(plan.planHash, input.planHash)) {
549
947
  return {
550
948
  ok: false,
551
949
  plan,
@@ -602,10 +1000,198 @@ export async function executeDagRerun(input) {
602
1000
  summary,
603
1001
  };
604
1002
  if (await fileExists(operatorRequestPath(input.cwd, input.requestId))) {
605
- const replay = await readExistingOperatorRequest(input.cwd, input.requestId);
1003
+ const replay = await readOperatorRequestReplay(input.cwd, input.requestId, {
1004
+ parentRunId: input.parentRunId,
1005
+ planHash: input.planHash,
1006
+ });
606
1007
  if (replay)
607
1008
  return replay;
608
1009
  }
609
1010
  await persistOperatorRequest(input.cwd, result, input.requestId);
610
1011
  return result;
611
1012
  }
1013
+ export async function executeDagArtifactOverrideRerun(input) {
1014
+ // Replay-first, but with SF2 envelope validation at the core boundary: an
1015
+ // existing operator request is only an idempotent answer when it belongs to
1016
+ // the same parent run AND the same plan hash. A caller that supplies a
1017
+ // different planHash/parent (direct call or a race between CLI validation
1018
+ // and this core) must get REQUEST_ID_REUSE_CONFLICT, never an unvalidated
1019
+ // replay.
1020
+ const replay = await readOperatorRequestReplay(input.cwd, input.requestId, {
1021
+ parentRunId: input.parentRunId,
1022
+ planHash: input.planHash,
1023
+ });
1024
+ if (replay)
1025
+ return replay;
1026
+ const plan = await planDagArtifactOverride({
1027
+ cwd: input.cwd,
1028
+ parentRunId: input.parentRunId,
1029
+ artifactId: input.artifactId,
1030
+ artifactOverrides: input.artifactOverrides,
1031
+ stagingRevisionSha256: input.stagingRevisionSha256,
1032
+ ...(input.workerContinuationAuthority
1033
+ ? { workerContinuationAuthority: input.workerContinuationAuthority }
1034
+ : {}),
1035
+ });
1036
+ if (!constantTimeHashEqual(plan.planHash, input.planHash)) {
1037
+ return {
1038
+ ok: false,
1039
+ plan,
1040
+ errorCode: "RERUN_PLAN_DRIFT",
1041
+ message: "plan hash mismatch; refresh impact preview before executing",
1042
+ };
1043
+ }
1044
+ if (!plan.eligible) {
1045
+ return {
1046
+ ok: false,
1047
+ plan,
1048
+ errorCode: "INELIGIBLE",
1049
+ message: plan.blockedReasons.join(", ") || "artifact continuation is not eligible",
1050
+ };
1051
+ }
1052
+ const override = plan.artifactOverrides?.overrides[0];
1053
+ if (!override || !constantTimeHashEqual(sha256Hex(input.candidateBytes), override.replacement.sha256)) {
1054
+ return {
1055
+ ok: false,
1056
+ plan,
1057
+ errorCode: "ARTIFACT_REVISION_DRIFT",
1058
+ message: "staged revision bytes no longer match the plan",
1059
+ };
1060
+ }
1061
+ const located = await locateDagRun(input.cwd, input.parentRunId);
1062
+ if (!located || located.lifecycle !== "completed") {
1063
+ return {
1064
+ ok: false,
1065
+ plan,
1066
+ errorCode: "PARENT_LIFECYCLE",
1067
+ message: `parent run must be completed: ${input.parentRunId}`,
1068
+ };
1069
+ }
1070
+ const parentManifestBefore = await snapshotDagRunManifest(located.runDir);
1071
+ const parentSpec = await readDagRunSpec(located.runDir);
1072
+ const parentState = await readDagRunState(located.runDir);
1073
+ let staged;
1074
+ try {
1075
+ staged = await stageContinuationRun({
1076
+ cwd: input.cwd,
1077
+ parentRunDir: located.runDir,
1078
+ parentRunId: input.parentRunId,
1079
+ parentSpec,
1080
+ parentState,
1081
+ plan,
1082
+ reason: input.reason,
1083
+ requestId: input.requestId,
1084
+ artifactCandidateBytes: input.candidateBytes,
1085
+ });
1086
+ }
1087
+ catch (error) {
1088
+ const message = error instanceof Error ? error.message : String(error);
1089
+ if (message.startsWith("WORKER_CONTINUATION_NEEDS_RECONCILE:")) {
1090
+ const result = {
1091
+ ok: false,
1092
+ plan,
1093
+ newRunId: input.workerContinuationAuthority?.childWorkerRunId,
1094
+ errorCode: "WORKER_CONTINUATION_NEEDS_RECONCILE",
1095
+ message,
1096
+ };
1097
+ await persistOperatorRequest(input.cwd, result, input.requestId);
1098
+ return result;
1099
+ }
1100
+ throw error;
1101
+ }
1102
+ const parentManifestAfterStage = await snapshotDagRunManifest(located.runDir);
1103
+ if (!constantTimeHashEqual(parentManifestBefore, parentManifestAfterStage)) {
1104
+ const settledRunDir = await settleStagedContinuationWithoutExecution({
1105
+ cwd: input.cwd,
1106
+ staged,
1107
+ failureCategory: "parent-immutability-violation",
1108
+ terminalReason: "parent run changed while staging the continuation",
1109
+ });
1110
+ return {
1111
+ ok: false,
1112
+ plan,
1113
+ newRunId: staged.runId,
1114
+ runDir: settledRunDir,
1115
+ errorCode: "PARENT_IMMUTABILITY_VIOLATION",
1116
+ message: "parent run changed while staging the child; child execution was blocked",
1117
+ };
1118
+ }
1119
+ await input.onStaged?.({ runId: staged.runId, runDir: staged.runDir });
1120
+ const plannedWorkspace = plan.highRiskAuthority?.workspaceFingerprint;
1121
+ if (plannedWorkspace) {
1122
+ const beforeExecution = await captureWorkspaceCheckpoint(input.cwd).catch(() => undefined);
1123
+ if (!beforeExecution || beforeExecution.fingerprint !== plannedWorkspace) {
1124
+ const settledRunDir = await settleStagedContinuationWithoutExecution({
1125
+ cwd: input.cwd,
1126
+ staged,
1127
+ failureCategory: "workspace-drift",
1128
+ terminalReason: "workspace changed after continuation staging and before writer execution",
1129
+ });
1130
+ return {
1131
+ ok: false,
1132
+ plan,
1133
+ newRunId: staged.runId,
1134
+ runDir: settledRunDir,
1135
+ errorCode: "WORKSPACE_DRIFT",
1136
+ message: "workspace changed after staging; revision was not consumed and writer execution was blocked",
1137
+ };
1138
+ }
1139
+ }
1140
+ if (input.consumeRevision) {
1141
+ await input.consumeRevision({
1142
+ revisionId: override.revisionId,
1143
+ expectedSha256: override.replacement.sha256,
1144
+ });
1145
+ }
1146
+ const writerTransactionRef = { current: { state: "not-applicable" } };
1147
+ const summary = await runDagContinuation({
1148
+ cwd: input.cwd,
1149
+ spec: staged.spec,
1150
+ state: staged.state,
1151
+ runDir: staged.runDir,
1152
+ ...(input.maxConcurrent !== undefined ? { maxConcurrent: input.maxConcurrent } : {}),
1153
+ ...(input.executeNode ? { executeNode: input.executeNode } : {}),
1154
+ beforeTerminalArchive: async ({ runDir, state }) => {
1155
+ writerTransactionRef.current = await finalizeContinuationWriterTransaction({
1156
+ cwd: input.cwd,
1157
+ runDir,
1158
+ plan,
1159
+ committed: state.status === "finished",
1160
+ ...(input.onWriterAttemptRecorded
1161
+ ? { onAttemptRecorded: input.onWriterAttemptRecorded }
1162
+ : {}),
1163
+ });
1164
+ if (writerTransactionRef.current.state === "blocked") {
1165
+ state.status = "failed";
1166
+ state.failureCategory = "writer-rollback-blocked";
1167
+ state.terminalReason = writerTransactionRef.current.reason;
1168
+ }
1169
+ },
1170
+ });
1171
+ const writerTransaction = writerTransactionRef.current;
1172
+ const parentManifestAfterRun = await snapshotDagRunManifest(located.runDir);
1173
+ const immutable = constantTimeHashEqual(parentManifestBefore, parentManifestAfterRun);
1174
+ const result = immutable && writerTransaction.state !== "blocked"
1175
+ ? {
1176
+ ok: summary.status === "finished",
1177
+ plan,
1178
+ newRunId: summary.runId,
1179
+ runDir: summary.runDir,
1180
+ summary,
1181
+ }
1182
+ : {
1183
+ ok: false,
1184
+ plan,
1185
+ newRunId: summary.runId,
1186
+ runDir: summary.runDir,
1187
+ summary,
1188
+ errorCode: writerTransaction.state === "blocked"
1189
+ ? "WRITER_ROLLBACK_BLOCKED"
1190
+ : "PARENT_IMMUTABILITY_VIOLATION",
1191
+ message: writerTransaction.state === "blocked"
1192
+ ? writerTransaction.reason
1193
+ : "parent run manifest changed during child execution",
1194
+ };
1195
+ await persistOperatorRequest(input.cwd, result, input.requestId);
1196
+ return result;
1197
+ }