@deksden-com/dd-flow-cli 0.9.0-beta.123 → 0.9.0-beta.127

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 (122) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +33 -1
  3. package/dist/build-info.json +7 -7
  4. package/dist/cli/argument-values.js +38 -0
  5. package/dist/cli/command-inputs.js +4 -3
  6. package/dist/cli/help.js +1 -0
  7. package/dist/cli/hook-ingress.js +1 -1
  8. package/dist/cli/run-cli.js +126 -193
  9. package/dist/cli/runtime-maintenance-diagnostics.js +46 -0
  10. package/dist/cli/runtime-maintenance-ingress.js +51 -0
  11. package/dist/cli/runtime-process.js +128 -0
  12. package/dist/cli.js +8 -5
  13. package/dist/domain/stage-catalog.js +1 -1
  14. package/dist/harness-runtime/bin/dd-agy.mjs +3 -2
  15. package/dist/harness-runtime/bin/dd-codex.mjs +3 -2
  16. package/dist/harness-runtime/bin/dd-droid.mjs +3 -2
  17. package/dist/harness-runtime/bin/dd-grok.mjs +16 -6
  18. package/dist/harness-runtime/bin/dd-opencode.mjs +3 -2
  19. package/dist/harness-runtime/bin/dd-zcode.mjs +20 -16
  20. package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
  21. package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
  22. package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +10 -0
  23. package/dist/harness-runtime/lib/codex-capacity-policy.mjs +89 -0
  24. package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
  25. package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
  26. package/dist/harness-runtime/lib/daemon-operations.mjs +159 -15
  27. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +215 -86
  28. package/dist/harness-runtime/lib/dd-agy.mjs +11 -13
  29. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +47 -23
  30. package/dist/harness-runtime/lib/dd-codex.mjs +257 -99
  31. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +32 -30
  32. package/dist/harness-runtime/lib/dd-droid.mjs +117 -31
  33. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +96 -38
  34. package/dist/harness-runtime/lib/dd-grok.mjs +15 -13
  35. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +54 -20
  36. package/dist/harness-runtime/lib/dd-opencode.mjs +44 -14
  37. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +81 -76
  38. package/dist/harness-runtime/lib/dd-zcode.mjs +204 -116
  39. package/dist/harness-runtime/lib/driver-recovery.mjs +12 -2
  40. package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
  41. package/dist/harness-runtime/lib/lease-renewal.mjs +23 -0
  42. package/dist/harness-runtime/lib/lifecycle-command.d.mts +3 -3
  43. package/dist/harness-runtime/lib/lifecycle-command.mjs +74 -20
  44. package/dist/harness-runtime/lib/managed-daemon.d.mts +11 -0
  45. package/dist/harness-runtime/lib/managed-daemon.mjs +321 -65
  46. package/dist/harness-runtime/lib/native-children.d.mts +13 -0
  47. package/dist/harness-runtime/lib/native-children.mjs +78 -0
  48. package/dist/harness-runtime/lib/native-hook-command.d.mts +4 -0
  49. package/dist/harness-runtime/lib/native-hook-command.mjs +115 -11
  50. package/dist/harness-runtime/lib/native-streams.mjs +45 -0
  51. package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
  52. package/dist/harness-runtime/lib/observation-clock.mjs +73 -17
  53. package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
  54. package/dist/harness-runtime/lib/operation-errors.mjs +105 -8
  55. package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
  56. package/dist/harness-runtime/lib/operation-progress.mjs +112 -0
  57. package/dist/harness-runtime/lib/process-json.mjs +136 -38
  58. package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
  59. package/dist/harness-runtime/lib/tool-observations.mjs +5 -2
  60. package/dist/harness-runtime/lib/zcode-usage.mjs +3 -2
  61. package/dist/runtime/context.js +8 -4
  62. package/dist/schemas/flow-run.schema.json +1 -0
  63. package/dist/schemas/plan-review-decision.schema.json +3 -3
  64. package/dist/schemas/run-control-receipt.schema.json +19 -0
  65. package/dist/services/code-checks.js +93 -27
  66. package/dist/services/codex-capacity.js +251 -74
  67. package/dist/services/codex-hook-delivery.js +2 -2
  68. package/dist/services/compatibility-preflight.js +4 -1
  69. package/dist/services/controller-fanout.js +9 -75
  70. package/dist/services/engines.js +70 -20
  71. package/dist/services/eval-snapshots.js +179 -40
  72. package/dist/services/external-work-launch.js +21 -9
  73. package/dist/services/harness-adapter.js +195 -25
  74. package/dist/services/hooks.js +65 -21
  75. package/dist/services/lifecycle-invocations.js +404 -32
  76. package/dist/services/managed-lease-monitor.js +128 -0
  77. package/dist/services/managed-processes.js +117 -53
  78. package/dist/services/merge-server.js +66 -23
  79. package/dist/services/recovery-observation-budget.js +67 -7
  80. package/dist/services/recovery-snapshot-database.js +7 -4
  81. package/dist/services/review-deferral.js +23 -0
  82. package/dist/services/run-control-receipt.js +1 -1
  83. package/dist/services/run-control-worker.js +177 -118
  84. package/dist/services/run-control.js +99 -8
  85. package/dist/services/run-controller-adapter.js +177 -51
  86. package/dist/services/run-controller-capture.js +14 -2
  87. package/dist/services/run-controller-process.js +11 -4
  88. package/dist/services/run-controller-recovery.js +17 -0
  89. package/dist/services/run-controller-state.js +68 -7
  90. package/dist/services/run-controller.js +207 -107
  91. package/dist/services/run-fork.js +8 -3
  92. package/dist/services/run-recovery-guard.js +75 -0
  93. package/dist/services/run-recovery-runtime.js +21 -30
  94. package/dist/services/run-recovery.js +17 -13
  95. package/dist/services/runs.js +40 -7
  96. package/dist/services/runtime-budget.js +32 -15
  97. package/dist/services/runtime-scope-control.js +8 -0
  98. package/dist/services/runtime-scope-resume.js +30 -8
  99. package/dist/services/runtime-scope-worker.js +118 -73
  100. package/dist/services/runtime-service.js +6 -7
  101. package/dist/services/scope-writer-cleanup.js +3 -2
  102. package/dist/services/stage-context.js +135 -0
  103. package/dist/services/stage-lifecycle.js +3 -15
  104. package/dist/services/stage-pause.js +3 -2
  105. package/dist/services/usage.js +14 -3
  106. package/dist/services/verification-matrix.js +30 -1
  107. package/dist/services/vnext-code-review.js +26 -13
  108. package/dist/services/vnext-code.js +10 -8
  109. package/dist/services/vnext-contracts.js +10 -1
  110. package/dist/services/vnext-execution-profile.js +75 -2
  111. package/dist/services/vnext-fanout.js +5 -0
  112. package/dist/services/vnext-merge.js +30 -11
  113. package/dist/services/vnext-plan-review.js +102 -35
  114. package/dist/services/vnext-plan.js +9 -7
  115. package/dist/services/vnext-protocolize.js +15 -9
  116. package/dist/services/vnext-specify.js +6 -6
  117. package/dist/services/work-registry.js +43 -6
  118. package/dist/services/workspace-bootstrap.js +14 -3
  119. package/dist/shared/errors.js +11 -6
  120. package/dist/storage/agent-output.js +26 -0
  121. package/dist/storage/database.js +80 -21
  122. package/package.json +7 -4
@@ -1,16 +1,85 @@
1
1
  import { setTimeout as delay } from "node:timers/promises";
2
2
  import crypto from "node:crypto";
3
- import { terminalCodexOverload } from "../harness-runtime/lib/dd-codex.mjs";
3
+ import { CAPACITY_POLICY, capacityBackoff, nativeItemsSettled, normalizeCodexFailure, overloadBurst, parseCapacityOperation, refusalObservation, terminalCodexOverload } from "../harness-runtime/lib/codex-capacity-policy.mjs";
4
4
  import { AppError } from "../shared/errors.js";
5
+ import { isObservationLoss } from "../harness-runtime/lib/operation-errors.mjs";
6
+ export function eligibleCodexChildOverload(child, parentSessionId) {
7
+ return typeof child.session_id === "string" && child.session_id !== parentSessionId && child.parent_session_id === parentSessionId && child.provenance !== "parent_mismatch" && child.status === "failed" && typeof child.terminal_turn_id === "string" && nativeItemsSettled(child.native_turn_items)
8
+ && terminalCodexOverload({ code: "turn_interrupted", details: { provider_session_id: child.session_id, turn_id: child.terminal_turn_id, native_turn_id: child.terminal_turn_id, terminal_status: "failed", provider_error: child.provider_error } }, child.session_id, child.terminal_turn_id);
9
+ }
10
+ /** A terminal root is not a settled tree. Only the daemon's exact owned tree
11
+ * with a settled root and positively live descendants permits waiting. */
12
+ export function capacityReadiness(seen, sessionId, operationId, turnId, items, allowDescendants = false) {
13
+ if (seen.provider_session_id !== sessionId || seen.terminal_turn_id !== undefined && seen.terminal_turn_id !== turnId || !nativeItemsSettled(items))
14
+ return "unproven";
15
+ const ids = Array.isArray(seen.tree_session_ids) ? seen.tree_session_ids : [];
16
+ if (!ids.length || ids[0] !== sessionId || ids.some(id => typeof id !== "string" || !id) || new Set(ids).size !== ids.length)
17
+ return "unproven";
18
+ const observations = Array.isArray(seen.tree_observations) ? seen.tree_observations : [];
19
+ const exactTree = observations.length === ids.length && ids.every(id => observations.filter(item => item.provider_session_id === id).length === 1);
20
+ const settlement = seen.settlement;
21
+ const budgetSettled = !settlement || settlement.state === "not_required" || settlement.state === "settled" && settlement.operation_ids?.includes(operationId) === true;
22
+ const treeSettled = allowDescendants ? exactTree && observations.every(item => item.settled === true) && Array.isArray(seen.unsettled_sessions) && seen.unsettled_sessions.length === 0 : ids.length === 1;
23
+ if (seen.settled === true && treeSettled && budgetSettled)
24
+ return "ready";
25
+ if (!allowDescendants || seen.settled !== false || !exactTree || ids.length < 2 || observations[0]?.provider_session_id !== sessionId || observations[0].settled !== true
26
+ || settlement && settlement.state !== "not_required" && !(settlement.state === "pending" && settlement.reason === "tree_unsettled"))
27
+ return "unproven";
28
+ const live = observations.filter(item => item.settled === false);
29
+ const unsettled = seen.unsettled_sessions;
30
+ const knownLive = live.length > 0 && live.every(item => item.provider_session_id !== sessionId && ["active", "running", "inProgress"].includes(String(typeof item.status === "object" && item.status ? item.status.type : item.status)));
31
+ return knownLive && observations.every(item => typeof item.settled === "boolean") && Array.isArray(unsettled) && unsettled.length === live.length && live.every(item => unsettled.includes(item.provider_session_id)) ? "owned_children_live" : "unproven";
32
+ }
5
33
  /** Permit acquisition must still belong to the original task, not merely an
6
34
  * open RUN. Controls/inspection and ordinary successor prompts are unaffected. */
7
- export function assertCapacityOwnerAdmission(context, record, operationId) {
35
+ export function assertCapacityOwnerAdmission(context, record, operationId, binding) {
8
36
  if (!operationId || !record.project_id || !record.run_id)
9
37
  return;
10
- const root = operationId.replace(/:capacity:[12]:session\.prompt$/, "");
11
- if (root === operationId && !/:child:[a-f0-9]+:followup:[12]$/.test(root))
38
+ const root = parseCapacityOperation(operationId).root;
39
+ const childOrdinal = /:child:[a-f0-9]+:followup:(\d+)$/.exec(root)?.[1];
40
+ if (childOrdinal && (!Number.isSafeInteger(Number(childOrdinal)) || Number(childOrdinal) < 1 || String(Number(childOrdinal)) !== childOrdinal))
41
+ throw new AppError("capacity_continuation_conflict", "Invalid native child continuation ordinal", 1);
42
+ if (root === operationId && !childOrdinal)
12
43
  return;
13
44
  const reject = () => { throw new AppError("capacity_owner_boundary_changed", "Queued continuation no longer belongs to an unfinished task; reconcile its durable state", 1, { operation_id: operationId, work_id: record.work_id, run_id: record.run_id }); };
45
+ const metadata = JSON.parse(record.metadata_json);
46
+ const hasTable = (name) => Boolean(context.db.get("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?", [name]));
47
+ const parsed = parseCapacityOperation(operationId);
48
+ if (parsed.ordinal > 0 && !["judge", "probe"].includes(metadata.role ?? "")) {
49
+ if (!binding?.nativeSessionId || !binding.promptSha256)
50
+ reject();
51
+ let intent;
52
+ const predecessor = parsed.ordinal === 1 ? root : `${root}:capacity:${parsed.ordinal - 1}:session.prompt`;
53
+ if (hasTable("run_controller_operations")) {
54
+ const saved = context.db.get("SELECT receipt_json FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND generation = ? AND status = 'failed'", [predecessor, record.owner_id, metadata.run_generation ?? 0]);
55
+ if (saved)
56
+ intent = JSON.parse(saved.receipt_json).capacity_intent;
57
+ }
58
+ if (!intent && record.work_id)
59
+ for (const table of ["work_launches", "merge_requests"]) {
60
+ if (!hasTable(table))
61
+ continue;
62
+ const rows = table === "work_launches"
63
+ ? context.db.all("SELECT l.adapter_receipt_json FROM work_launches l JOIN works w ON w.work_id = l.work_id WHERE l.work_id = ? AND w.project_id = ? AND w.run_id = ?", [record.work_id, record.project_id, record.run_id])
64
+ : context.db.all("SELECT adapter_receipt_json FROM merge_requests WHERE executor_work_id = ? AND project_id = ? AND run_id = ?", [record.work_id, record.project_id, record.run_id]);
65
+ for (const saved of rows) {
66
+ const retained = JSON.parse(saved.adapter_receipt_json ?? "null");
67
+ intent = retained?.capacity_intents?.find((item) => item.operation_id === operationId);
68
+ if (intent)
69
+ break;
70
+ }
71
+ if (intent)
72
+ break;
73
+ }
74
+ if (!intent || intent.policy !== CAPACITY_POLICY || intent.root_operation_id !== root || intent.ordinal !== parsed.ordinal
75
+ || intent.predecessor !== predecessor || intent.operation_id !== operationId || intent.refusal?.turn_id !== intent.turn_id
76
+ || intent.refusal?.ordinal !== parsed.ordinal - 1 || intent.refusal?.session_id !== intent.session_id
77
+ || !Number.isFinite(Date.parse(intent.not_before)) || Date.parse(intent.not_before) > Date.parse(context.now())
78
+ || intent.deadline !== null && (typeof intent.deadline !== "number" || intent.deadline <= Date.parse(context.now()))
79
+ || binding?.nativeSessionId !== undefined && intent.session_id !== binding.nativeSessionId
80
+ || binding?.promptSha256 !== undefined && intent.prompt_sha256 !== binding.promptSha256)
81
+ reject();
82
+ }
14
83
  if (context.db.get("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'run_recovery_bindings'")) {
15
84
  if (context.db.get("SELECT 1 FROM run_recovery_bindings b JOIN run_recovery_guards g ON g.recovery_id = b.recovery_id WHERE g.project_id = ? AND g.run_id = ? AND g.status = 'recovered' AND b.status = 'accepted' AND json_extract(b.runtime_dispatch_json, '$.operation_id') = ?", [record.project_id, record.run_id, root]))
16
85
  reject();
@@ -22,7 +91,7 @@ export function assertCapacityOwnerAdmission(context, record, operationId) {
22
91
  reject();
23
92
  // MERGE owns the Stage/request boundary: finishing its Work does not
24
93
  // necessarily commit stage finish yet. External workers own only Work.
25
- const mergeOwner = JSON.parse(record.metadata_json).role === "merge";
94
+ const mergeOwner = metadata.role === "merge";
26
95
  if (!work || !(merge && mergeOwner ? ["created", "running", "completed"] : ["created", "running"]).includes(work.status))
27
96
  reject();
28
97
  }
@@ -37,70 +106,140 @@ export function assertCapacityOwnerAdmission(context, record, operationId) {
37
106
  if (state.stage && (index.current_stage && index.current_stage !== state.stage || index.stage_runs?.some(stage => stage.stage === state.stage && ["done", "skipped"].includes(stage.status))))
38
107
  reject();
39
108
  const child = Object.entries(state.child_capacity ?? {}).find(([, intent]) => intent.operation_id === root)?.[0];
109
+ if (childOrdinal && !child)
110
+ reject();
40
111
  if (child && context.db.get("SELECT 1 FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id JOIN sessions s ON s.session_id = ws.session_id WHERE w.project_id = ? AND w.run_id = ? AND s.provider_session_id = ? AND w.status <> 'running'", [record.project_id, record.run_id, child]))
41
112
  reject();
42
113
  }
43
114
  export function capacityDelay(error, ordinal, now = Date.now()) {
44
- const native = error?.details?.provider_error;
45
- const headers = native?.headers;
46
- const header = headers && Object.entries(headers).find(([key]) => key.toLowerCase() === "retry-after")?.[1];
47
- const value = header ?? native?.retryAfter ?? native?.retry_after;
48
- const seconds = typeof value === "number" || typeof value === "string" && /^\d+(\.\d+)?$/.test(value.trim()) ? Number(value) * 1000 : typeof value === "string" ? Date.parse(value) - now : NaN;
49
- return Math.max(ordinal === 1 ? 5_000 : 15_000, Number.isFinite(seconds) ? Math.max(0, seconds) : 0);
115
+ return capacityBackoff(error, ordinal, now);
50
116
  }
51
117
  /** Continue a confirmed terminal Turn, never replay its completed effects. */
52
118
  export async function continueCodexCapacity(input) {
53
119
  let { error, operationId } = input;
54
- const history = input.retainedIntents?.() ?? [];
55
- const rootId = input.operationId.replace(/:capacity:[12]:session\.prompt$/, "");
120
+ const history = [...(input.retainedIntents?.() ?? [])];
121
+ const parsed = parseCapacityOperation(input.operationId);
122
+ const rootId = parsed.root;
56
123
  const text = input.continuationText ?? "Continue the same assigned task in this Session. Check current durable RUN, Work and Stage state first. Never repeat an observed lifecycle command, accepted HITL answer, check or merge apply; use only an exact issued successor. Stop at the assigned boundary.";
57
124
  const promptSha = crypto.createHash("sha256").update(text).digest("hex");
58
- let ordinal = Number(/:capacity:([12]):session\.prompt$/.exec(operationId)?.[1] ?? 0) + 1;
125
+ let ordinal = parsed.ordinal + 1;
126
+ const conflict = (message) => { throw new AppError("capacity_continuation_conflict", message, 1, { operation_id: operationId, root_operation_id: rootId, session_id: input.sessionId }); };
127
+ const blocked = (message) => new AppError("capacity_continuation_blocked", message, 1, { capacity_uncertainty: true, operation_id: operationId, root_operation_id: rootId, provider_session_id: input.sessionId, primary_error: error });
128
+ const observe = async (action, observedOperationId = operationId) => {
129
+ try {
130
+ return await action();
131
+ }
132
+ catch (cause) {
133
+ let primary = cause;
134
+ for (let depth = 0; depth < 4 && primary?.code === "harness_adapter_failed"; depth++)
135
+ primary = (primary.cause ?? primary.details?.cause);
136
+ if (isObservationLoss(primary))
137
+ throw new AppError("operation_observation_lost", "Capacity native observation was lost; do not cancel or resend", 1, { capacity_uncertainty: true, root_operation_id: rootId, operation_id: observedOperationId, provider_session_id: input.sessionId, predecessor_error: error, cause });
138
+ throw cause;
139
+ }
140
+ };
141
+ const refusals = [...(input.retainedFailures?.() ?? [])];
142
+ if (parsed.ordinal > history.length)
143
+ conflict("Continuation has no complete retained predecessor chain");
144
+ for (let index = 0; index < history.length; index++) {
145
+ const intent = history[index];
146
+ const expected = index + 1;
147
+ if (intent.ordinal !== expected || intent.operation_id !== `${rootId}:capacity:${expected}:session.prompt`
148
+ || intent.predecessor !== (expected === 1 ? rootId : `${rootId}:capacity:${expected - 1}:session.prompt`)
149
+ || intent.prompt_sha256 !== promptSha || intent.policy !== CAPACITY_POLICY || intent.root_operation_id !== rootId || intent.session_id !== input.sessionId
150
+ || intent.deadline !== (input.deadline ?? null) || !intent.refusal || intent.refusal.ordinal !== expected - 1 || intent.refusal.turn_id !== intent.turn_id
151
+ || !Number.isFinite(Date.parse(intent.not_before)))
152
+ conflict("Retained capacity chain is incomplete or belongs to another policy/task");
153
+ const previous = refusals.find(item => item.ordinal === intent.refusal.ordinal);
154
+ if (previous && JSON.stringify(previous) !== JSON.stringify(intent.refusal))
155
+ conflict("Retained failure observation changed");
156
+ if (!previous)
157
+ refusals.push(intent.refusal);
158
+ }
159
+ refusals.sort((a, b) => a.ordinal - b.ordinal);
160
+ for (let index = 0; index < refusals.length; index++) {
161
+ const refusal = refusals[index];
162
+ if (refusal.ordinal !== index || refusal.policy !== CAPACITY_POLICY || refusal.session_id !== input.sessionId || !refusal.turn_id
163
+ || refusals.slice(0, index).some(item => item.turn_id === refusal.turn_id) || !Number.isFinite(Date.parse(refusal.first_observed_at)))
164
+ conflict("Retained refusal chain is invalid");
165
+ }
59
166
  // Reconcile a retained dispatch before preparing another. Native absence is
60
167
  // the only safe way to reuse a prepared intent; a lost reply is not absence.
61
168
  for (const intent of history) {
62
- if (intent.predecessor !== operationId || intent.ordinal !== ordinal || intent.operation_id !== `${rootId}:capacity:${ordinal}:session.prompt`)
169
+ if (intent.ordinal < ordinal)
63
170
  continue;
171
+ if (intent.predecessor !== operationId || intent.ordinal !== ordinal || intent.operation_id !== `${rootId}:capacity:${ordinal}:session.prompt`)
172
+ conflict("Retained successor skips its predecessor");
64
173
  if (intent.prompt_sha256 !== promptSha || !intent.turn_id || !Number.isFinite(Date.parse(intent.not_before)))
65
174
  throw error;
66
175
  let observed;
67
176
  try {
68
- observed = await input.inspectOperation(intent.operation_id);
177
+ observed = await observe(() => input.inspectOperation(intent.operation_id), intent.operation_id);
69
178
  }
70
179
  catch (cause) {
71
180
  let code = cause;
72
181
  for (let depth = 0; depth < 4 && code?.code === "harness_adapter_failed"; depth++)
73
182
  code = code.details?.cause;
74
183
  if (code?.code !== "operation_not_found")
75
- throw error;
184
+ throw cause;
76
185
  break;
77
186
  }
78
- if (observed.operation_id !== intent.operation_id || observed.session_id !== input.sessionId || observed.operation !== "session.prompt" || observed.primary_error)
79
- throw error;
187
+ if (observed.operation_id !== intent.operation_id || observed.session_id !== input.sessionId || observed.operation !== "session.prompt")
188
+ conflict("Retained native successor identity changed");
189
+ if (observed.primary_error)
190
+ throw observed.primary_error;
191
+ while (["prepared", "dispatching", "running", "awaiting_provider", "outcome_unknown"].includes(String(observed.state))) {
192
+ input.signal?.throwIfAborted();
193
+ input.assertCurrent();
194
+ const boundary = await observe(async () => await input.decision?.() ?? "continue", intent.operation_id);
195
+ if (boundary === "completed" || boundary === "paused")
196
+ return { provider_session_id: input.sessionId, capacity_decision: boundary };
197
+ if (boundary === "reconciliation_blocked")
198
+ throw new AppError("capacity_continuation_blocked", "Retained native successor has an unresolved owner boundary", 1, { operation_id: intent.operation_id, primary_error: error });
199
+ if (input.deadline !== undefined && Date.now() >= input.deadline)
200
+ throw new AppError("capacity_owner_deadline_exceeded", "Retained successor exhausted its original deadline", 1);
201
+ await delay(250, undefined, { signal: input.signal });
202
+ observed = await observe(() => input.inspectOperation(intent.operation_id), intent.operation_id);
203
+ input.signal?.throwIfAborted();
204
+ input.assertCurrent();
205
+ if (observed.operation_id !== intent.operation_id || observed.session_id !== input.sessionId || observed.operation !== "session.prompt")
206
+ conflict("Retained native successor identity changed");
207
+ if (observed.primary_error)
208
+ throw observed.primary_error;
209
+ }
80
210
  if (observed.state === "completed" && observed.result && typeof observed.result === "object") {
81
211
  input.signal?.throwIfAborted();
82
212
  input.assertCurrent();
83
- return observed.result;
213
+ const boundary = await observe(async () => await input.decision?.() ?? "continue", intent.operation_id);
214
+ input.signal?.throwIfAborted();
215
+ input.assertCurrent();
216
+ if (boundary === "reconciliation_blocked")
217
+ throw new AppError("capacity_continuation_blocked", "Retained completion cannot be reconciled with its owner boundary", 1);
218
+ return boundary === "paused" || boundary === "completed" ? { provider_session_id: input.sessionId, capacity_decision: boundary } : observed.result;
84
219
  }
220
+ if (observed.state === "failed" && observed.error && !terminalCodexOverload(observed.error, input.sessionId))
221
+ throw observed.error;
85
222
  if (observed.state !== "failed" || !terminalCodexOverload(observed.error, input.sessionId))
86
- throw error;
223
+ throw new AppError("operation_observation_lost", "Retained successor outcome is not proven; do not resend", 1, { operation_id: intent.operation_id, primary_error: error, observed });
87
224
  operationId = intent.operation_id;
88
225
  error = observed.error;
89
226
  ordinal += 1;
90
227
  }
91
- const decide = async () => {
228
+ const decide = async (observedOperationId = operationId) => {
92
229
  input.signal?.throwIfAborted();
93
230
  input.assertCurrent();
94
- const decision = await input.decision?.() ?? "continue";
231
+ const decision = await observe(async () => await input.decision?.() ?? "continue", observedOperationId);
95
232
  input.signal?.throwIfAborted();
96
233
  input.assertCurrent();
97
234
  if (decision === "reconciliation_blocked")
98
- throw error;
235
+ throw new AppError("capacity_continuation_blocked", "Native refusal cannot be reconciled with its durable owner boundary", 1, { primary_error: error, root_operation_id: rootId, provider_session_id: input.sessionId });
99
236
  if (decision === "continue" && input.deadline !== undefined && Date.now() >= input.deadline)
100
- throw error;
237
+ throw new AppError("capacity_owner_deadline_exceeded", "Continuation exhausted its original owner deadline", 1, { primary_error: error, deadline: input.deadline, root_operation_id: rootId, provider_session_id: input.sessionId });
101
238
  return decision === "continue" ? null : { provider_session_id: input.sessionId, capacity_decision: decision };
102
239
  };
103
- for (; ordinal <= 2; ordinal += 1) {
240
+ for (;; ordinal += 1) {
241
+ if (!Number.isSafeInteger(ordinal))
242
+ conflict("Continuation ordinal overflow");
104
243
  if (!terminalCodexOverload(error, input.sessionId)) {
105
244
  if (ordinal === 1)
106
245
  return null;
@@ -109,82 +248,120 @@ export async function continueCodexCapacity(input) {
109
248
  const completed = await decide();
110
249
  if (completed)
111
250
  return completed;
112
- let prior;
113
- try {
114
- prior = await input.inspectOperation(operationId);
115
- }
116
- catch {
117
- if (ordinal === 1)
118
- return null;
119
- throw error;
120
- }
251
+ const prior = await observe(() => input.inspectOperation(operationId));
121
252
  const failure = prior.error;
122
- const turnId = failure?.details?.turn_id;
253
+ const confirmed = normalizeCodexFailure(failure, input.sessionId);
254
+ const turnId = confirmed?.turn_id;
255
+ input.signal?.throwIfAborted();
256
+ input.assertCurrent();
257
+ if (prior.primary_error)
258
+ throw prior.primary_error;
123
259
  if (prior.operation_id !== operationId || prior.operation !== "session.prompt" || prior.session_id !== input.sessionId
124
260
  || prior.state !== "failed" || prior.primary_error || !turnId || !terminalCodexOverload(failure, input.sessionId, turnId)) {
125
- if (ordinal === 1)
126
- return null;
127
- throw error;
261
+ throw blocked("Retained predecessor does not prove the exact terminal overloaded Turn");
128
262
  }
129
- const settled = async () => {
130
- const seen = await input.inspectSession(turnId);
131
- const items = seen.terminal_turn_id === turnId && seen.terminal_status === "failed" ? seen.native_turn_items : failure.details?.native_turn_items;
132
- if (items?.observed !== true || typeof items.possible_effects !== "boolean" || items.pending === true || items.possible_effects === true && items.pending !== false)
133
- return false;
134
- const settlement = seen.settlement;
135
- const ids = Array.isArray(seen.tree_session_ids) ? seen.tree_session_ids : [];
136
- const observations = Array.isArray(seen.tree_observations) ? seen.tree_observations : [];
137
- const treeProven = input.allowSettledDescendants
138
- ? ids.length > 0 && ids[0] === input.sessionId && observations.length === ids.length
139
- && ids.every(id => observations.some(item => item.provider_session_id === id && item.settled === true))
140
- && Array.isArray(seen.unsettled_sessions) && seen.unsettled_sessions.length === 0
141
- : ids.length === 1 && ids[0] === input.sessionId;
142
- return seen.provider_session_id === input.sessionId && seen.settled === true && treeProven
143
- && (!settlement || settlement.state === "not_required" || settlement.state === "settled" && settlement.operation_ids?.includes(operationId) === true);
263
+ const readiness = async () => {
264
+ input.signal?.throwIfAborted();
265
+ input.assertCurrent();
266
+ const seen = await observe(() => input.inspectSession(turnId));
267
+ input.signal?.throwIfAborted();
268
+ input.assertCurrent();
269
+ const items = (seen.terminal_turn_id === turnId && seen.terminal_status === "failed" ? seen.native_turn_items : confirmed?.native_turn_items);
270
+ return capacityReadiness(seen, input.sessionId, operationId, turnId, items, input.allowSettledDescendants);
144
271
  };
145
- try {
146
- if (!await settled()) {
147
- if (ordinal === 1)
148
- return null;
149
- throw error;
150
- }
272
+ const ready = await readiness();
273
+ if (ready === "unproven")
274
+ throw blocked("Native predecessor readiness is unproven; do not cancel or replay to manufacture proof");
275
+ const native = normalizeCodexFailure(failure, input.sessionId, turnId);
276
+ let refusal = refusals.find(item => item.ordinal === ordinal - 1);
277
+ if (refusal && (refusal.turn_id !== turnId || refusal.session_id !== input.sessionId || refusal.policy !== CAPACITY_POLICY))
278
+ conflict("Failure ordinal identifies a different native Turn");
279
+ if (refusal) {
280
+ const expected = refusalObservation(failure, ordinal - 1, refusal.first_observed_at);
281
+ if (refusal.native_terminal_at !== expected.native_terminal_at || refusal.native_terminal_basis !== expected.native_terminal_basis)
282
+ conflict("Retained refusal time differs from its native receipt");
151
283
  }
152
- catch {
153
- if (ordinal === 1)
154
- return null;
155
- throw error;
284
+ if (!refusal) {
285
+ if (refusals.some(item => item.turn_id === turnId))
286
+ conflict("A cached native Turn cannot be a second refusal");
287
+ // A cached legacy failure cannot be assigned a new time during reattach.
288
+ if (ordinal > 1 && parsed.ordinal > 0 && !native.observed_at)
289
+ conflict("Retained continuation has no original failure time");
290
+ const observedAt = typeof native.observed_at === "string" && Number.isFinite(Date.parse(native.observed_at)) ? native.observed_at : new Date().toISOString();
291
+ refusal = refusalObservation(failure, ordinal - 1, observedAt);
292
+ if (!input.atomicIntent)
293
+ input.retainFailure?.(operationId, refusal);
294
+ refusals.push(refusal);
295
+ }
296
+ const burst = overloadBurst(refusals);
297
+ if (burst) {
298
+ if (input.atomicIntent)
299
+ input.retainFailure?.(operationId, refusal);
300
+ throw new AppError("provider_overload_burst", "Two consecutive continuation Turns were overloaded within two minutes", 1, { ...burst, root_operation_id: rootId, provider_session_id: input.sessionId, continuation_count: ordinal - 1, primary_error: failure });
156
301
  }
157
302
  const nextId = `${rootId}:capacity:${ordinal}:session.prompt`;
158
303
  const existing = history.find(item => item.operation_id === nextId);
159
304
  if (existing && (existing.predecessor !== operationId || existing.turn_id !== turnId || existing.prompt_sha256 !== promptSha || existing.ordinal !== ordinal))
160
305
  throw error;
161
306
  const wait = existing ? Math.max(0, Date.parse(existing.not_before) - Date.now()) : capacityDelay(failure, ordinal);
162
- if (!Number.isFinite(wait) || wait > 30_000 || input.deadline !== undefined && Date.now() + wait >= input.deadline)
163
- throw error;
164
- if (!existing)
165
- input.retainIntent({ predecessor: operationId, operation_id: nextId, turn_id: turnId, ordinal, prompt_sha256: promptSha, not_before: new Date(Date.now() + wait).toISOString() });
166
- await delay(wait, undefined, { signal: input.signal });
307
+ if (!Number.isFinite(wait) || Date.now() + wait > 8.64e15 || input.deadline !== undefined && Date.now() + wait >= input.deadline) {
308
+ if (input.atomicIntent)
309
+ input.retainFailure?.(operationId, refusal);
310
+ throw new AppError("capacity_owner_deadline_exceeded", "Continuation wait exceeds its original owner deadline", 1, { primary_error: failure, deadline: input.deadline ?? null });
311
+ }
312
+ if (!existing) {
313
+ const intent = { predecessor: operationId, operation_id: nextId, turn_id: turnId, ordinal, prompt_sha256: promptSha, not_before: new Date(Date.now() + wait).toISOString(), policy: CAPACITY_POLICY, root_operation_id: rootId, session_id: input.sessionId, deadline: input.deadline ?? null, refusal };
314
+ input.retainIntent(intent);
315
+ history.push(intent);
316
+ }
317
+ // Intent/refusal are durable before the first wait; no permit or writer is
318
+ // held here, and live siblings are never cancelled as a settlement proof.
319
+ const waitForOwnedChildren = async (initial) => {
320
+ let current = initial;
321
+ while (current === "owned_children_live") {
322
+ await delay(250, undefined, { signal: input.signal });
323
+ const boundary = await decide();
324
+ if (boundary)
325
+ return boundary;
326
+ current = await readiness();
327
+ }
328
+ if (current !== "ready")
329
+ throw blocked("Owned native child readiness became unproven");
330
+ return null;
331
+ };
332
+ const childBoundary = await waitForOwnedChildren(ready);
333
+ if (childBoundary)
334
+ return childBoundary;
335
+ // Node timers truncate large delays. Bounded slices preserve cancellation
336
+ // and owner checks; monotonic elapsed time does not renew a Retry-After.
337
+ const until = performance.now() + Math.max(0, Date.parse(history.find(item => item.operation_id === nextId).not_before) - Date.now());
338
+ while (performance.now() < until) {
339
+ await delay(Math.min(250, Math.max(0, until - performance.now())), undefined, { signal: input.signal });
340
+ const boundary = await decide();
341
+ if (boundary)
342
+ return boundary;
343
+ }
167
344
  const afterWait = await decide();
168
345
  if (afterWait)
169
346
  return afterWait;
170
- if (!await settled().catch(() => false))
171
- throw error;
347
+ const refreshedBoundary = await waitForOwnedChildren(await readiness());
348
+ if (refreshedBoundary)
349
+ return refreshedBoundary;
172
350
  const beforeDispatch = await decide();
173
351
  if (beforeDispatch)
174
352
  return beforeDispatch;
175
353
  try {
176
- return await input.prompt(nextId, text, ordinal);
354
+ return await observe(() => input.prompt(nextId, text, ordinal), nextId);
177
355
  }
178
356
  catch (nextError) {
179
357
  // A task can complete/pause while the daemon queues for its permit.
180
358
  // Admission then refuses the stale prompt; accept that owner decision
181
359
  // before treating the transport's rejection as another provider failure.
182
- const finished = await decide();
360
+ const finished = await decide(nextId);
183
361
  if (finished)
184
362
  return finished;
185
363
  error = nextError;
186
364
  operationId = nextId;
187
365
  }
188
366
  }
189
- throw error;
190
367
  }
@@ -5,8 +5,8 @@ import { commandOption, lifecycleCommandFromPayload, parseLifecycleCommand, cont
5
5
  /** The hook asks the existing daemon about native evidence; it cannot mint it. */
6
6
  export async function verifyCodexHookDelivery(env, payload) {
7
7
  const command = lifecycleCommandFromPayload(payload);
8
- const parsed = command ? parseLifecycleCommand(command) : { kind: "none" };
9
- if (parsed.kind === "none" && !parsed.unresolved && !(command && continuationCommand(command)))
8
+ const parsed = command ? parseLifecycleCommand(command) : { kind: "none", participation: "none" };
9
+ if (parsed.kind === "none" && !["lifecycle", "auxiliary"].includes(parsed.participation) && !(command && continuationCommand(command)))
10
10
  return undefined;
11
11
  const managed = (parsed.kind !== "none" && commandOption(parsed.invocation, "invocation-id")) || env.DD_FLOW_DAEMON_ID;
12
12
  if (!managed)
@@ -11,7 +11,10 @@ export function preflightCliCompatibility(context, args, env = process.env, reso
11
11
  if (!projectRoot) {
12
12
  return { ok: true, classification, compatibility: null };
13
13
  }
14
- const selection = selectEngine(context, { projectRoot, env }, { allowCurrentInProcess: true }, classification);
14
+ // Compatibility checks the executing build/manifest identity. Routing and
15
+ // binding validate snapshot bytes before dispatch; maintenance must not
16
+ // rehash the dependency closure or spawn health probes on every lease ACK.
17
+ const selection = selectEngine(context, { projectRoot, env }, { allowCurrentInProcess: true, verifyCurrentContent: false }, classification);
15
18
  const compatibility = compatibilityReport(selection, classification);
16
19
  if (classification.mode === "read_only_diagnostics" || classification.mode === "mb_upgrade") {
17
20
  return { ok: true, classification, compatibility };
@@ -1,4 +1,5 @@
1
- import { AppError } from "../shared/errors.js";
1
+ import { AppError, errorRecord } from "../shared/errors.js";
2
+ import { normalizeNativeChildren } from "../harness-runtime/lib/native-children.mjs";
2
3
  import { getVnextFanoutStatus, observeVnextNativeChildren, reconcileVnextFanout, subagentCapacityKey } from "./vnext-fanout.js";
3
4
  import { getFlowRunVariables } from "./runs.js";
4
5
  import { dispatchVnextPlanReview } from "./vnext-plan-review.js";
@@ -57,7 +58,7 @@ export async function nextControllerFanout(context, input) {
57
58
  throw new AppError("stage_graph_compatibility_required", "The retained graph cannot be dispatched by this controller; use the RUN's pinned engine", 1, { stage: input.stage, run_id: input.runId, graph: fanout });
58
59
  if (children.length) {
59
60
  const reconciled = await reconcileVnextFanout(context, { ...input, observations });
60
- const issues = reconciled.issues.filter(issue => issue.code !== "child_check_in_progress");
61
+ const issues = reconciled.issues.filter(issue => !["child_check_in_progress", "child_capacity_pending"].includes(issue.code));
61
62
  if (issues.length)
62
63
  throw new AppError("fanout_reconciliation_required", "Native child outcome requires reconciliation, not replay", 1, { issues });
63
64
  if (children.some(child => child.status === "running" || child.status === "unknown"))
@@ -106,80 +107,13 @@ export async function nextControllerFanout(context, input) {
106
107
  throw new AppError("fanout_graph_stalled", "Created Work has no ready or running dependency path", 1);
107
108
  return prompt("semantic_completion", semanticCompletionPrompt(input.stage, fanout.works.completed));
108
109
  }
109
- /** Normalize provider-observed topology only; never parse assistant prose. */
110
+ /** Shared pinned runtime normalization; preserve structured errors at the TS boundary. */
110
111
  export function controllerNativeChildren(receipt, rootSessionId) {
111
- const candidates = [];
112
- const object = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
113
- const list = (value) => Array.isArray(value) ? value : [];
114
- const text = (value, keys) => { for (const key of keys)
115
- if (typeof value[key] === "string" && value[key])
116
- return value[key]; return null; };
117
- const settledTree = new Set(list(receipt.tree_observations)
118
- .filter(value => object(value).settled === true)
119
- .map(value => text(object(value), ["provider_session_id", "session_id"]))
120
- .filter((id) => id !== null));
121
- const add = (raw, source, scopedParent = null, fallback = "unknown") => {
122
- const value = object(raw);
123
- const id = text(value, ["provider_session_id", "session_id", "sessionId", "childSessionId", "conversation_id", "conversationId", "id", "subagentId"]);
124
- const parent = text(value, ["parent_provider_session_id", "parent_session_id", "parentSessionId", "parentID", "parentId"]) ?? scopedParent;
125
- if (!id)
126
- return;
127
- const outcomeError = object(value.outcome_error);
128
- if (typeof outcomeError.code === "string")
129
- throw new AppError(outcomeError.code, String(outcomeError.message ?? "Native child outcome is unconfirmed"), 1, { ...object(outcomeError.details), session_id: id, source });
130
- const state = String(value.status ?? value.state ?? fallback).toLowerCase();
131
- if (state === "lost")
132
- throw new AppError("native_child_outcome_lost", "Native child ended without a recoverable outcome", 1, { session_id: id, parent_session_id: parent, source, native_status: state });
133
- // Provider idle means no current activity, not a Work result. Only an
134
- // explicit terminal result or a separately proven root settlement may be
135
- // terminal here; unknown states stay unknown for reconciliation.
136
- const status = ["completed", "complete", "done", "success", "succeeded"].includes(state) ? "completed" : state === "settled_by_root" ? "settled_by_root" : ["failed", "error"].includes(state) ? "failed" : ["cancelled", "canceled", "interrupted"].includes(state) ? "cancelled" : ["running", "active", "pending"].includes(state) ? "running" : fallback;
137
- const rootChild = parent === rootSessionId;
138
- const settled = rootChild && status === "unknown" && settledTree.has(id) ? "settled_by_root" : status;
139
- candidates.push({ session_id: id, parent_session_id: parent ?? "", status: settled, source, ...(rootChild ? {} : { provenance: "parent_mismatch" }),
140
- ...(text(value, ["terminal_turn_id"]) ? { terminal_turn_id: text(value, ["terminal_turn_id"]) } : {}),
141
- ...(value.provider_error !== undefined ? { provider_error: value.provider_error } : {}),
142
- ...(value.native_turn_items && typeof value.native_turn_items === "object" ? { native_turn_items: value.native_turn_items } : {}) });
143
- };
144
- for (const value of list(receipt.descendants))
145
- add(value, "adapter.descendants");
146
- for (const value of list(receipt.children))
147
- add(value, "opencode.session.children");
148
- const subagents = object(object(receipt.evidence).subagents ?? receipt.subagents);
149
- for (const value of list(subagents.running))
150
- add(value, "zcode/session/subagents", rootSessionId, "running");
151
- for (const value of list(subagents.completed))
152
- add(value, "zcode/session/subagents", rootSessionId, "completed");
153
- for (const value of list(object(subagents.ended).items)) {
154
- const status = String(object(value).status ?? "unknown");
155
- if (!["success", "failed", "cancelled", "lost"].includes(status))
156
- throw new AppError("native_child_outcome_unavailable", "ZCode ended child has no recognized native outcome", 1, { child: value });
157
- add(value, "zcode/session/subagents", rootSessionId);
112
+ try {
113
+ return normalizeNativeChildren(receipt, rootSessionId);
158
114
  }
159
- const unique = new Map();
160
- const rank = { unknown: 0, running: 1, settled_by_root: 2, completed: 3, failed: 3, cancelled: 3 };
161
- for (const child of candidates) {
162
- const previous = unique.get(child.session_id);
163
- if (previous && previous.parent_session_id !== child.parent_session_id) {
164
- // Keep the contradictory parent visible to the existing provenance gate,
165
- // independently of input order or which source happened to say success.
166
- const foreign = [previous, child].filter(item => item.parent_session_id !== rootSessionId).sort((a, b) => a.parent_session_id.localeCompare(b.parent_session_id))[0];
167
- unique.set(child.session_id, { ...foreign, provenance: "parent_mismatch" });
168
- continue;
169
- }
170
- if (previous && rank[previous.status] === 3 && rank[child.status] === 3 && previous.status !== child.status)
171
- throw new AppError("native_child_outcome_conflict", "Native child has contradictory terminal outcomes", 1, { session_id: child.session_id, observations: [previous, child] });
172
- if (previous && previous.status === child.status && previous.terminal_turn_id && child.terminal_turn_id && previous.terminal_turn_id !== child.terminal_turn_id)
173
- throw new AppError("native_child_turn_conflict", "Native child has contradictory terminal Turn identities", 1, { session_id: child.session_id, observations: [previous, child] });
174
- if (previous && previous.status === child.status) {
175
- unique.set(child.session_id, { ...previous, ...child,
176
- ...(child.terminal_turn_id ?? previous.terminal_turn_id ? { terminal_turn_id: child.terminal_turn_id ?? previous.terminal_turn_id } : {}),
177
- ...(child.provider_error ?? previous.provider_error ? { provider_error: child.provider_error ?? previous.provider_error } : {}),
178
- ...(child.native_turn_items ?? previous.native_turn_items ? { native_turn_items: child.native_turn_items ?? previous.native_turn_items } : {}) });
179
- continue;
180
- }
181
- if (!previous || rank[child.status] > rank[previous.status])
182
- unique.set(child.session_id, child);
115
+ catch (error) {
116
+ const record = errorRecord(error);
117
+ throw Object.assign(new AppError(record.code, record.message, record.exitCode, record.details), record);
183
118
  }
184
- return [...unique.values()].sort((left, right) => left.session_id.localeCompare(right.session_id));
185
119
  }