@deksden-com/dd-flow-cli 0.9.0-beta.124 → 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 (113) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +10 -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/harness-runtime/bin/dd-agy.mjs +3 -2
  14. package/dist/harness-runtime/bin/dd-codex.mjs +3 -2
  15. package/dist/harness-runtime/bin/dd-droid.mjs +3 -2
  16. package/dist/harness-runtime/bin/dd-grok.mjs +16 -6
  17. package/dist/harness-runtime/bin/dd-opencode.mjs +3 -2
  18. package/dist/harness-runtime/bin/dd-zcode.mjs +20 -16
  19. package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
  20. package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
  21. package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
  22. package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
  23. package/dist/harness-runtime/lib/daemon-operations.mjs +60 -13
  24. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +151 -72
  25. package/dist/harness-runtime/lib/dd-agy.mjs +11 -23
  26. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +9 -14
  27. package/dist/harness-runtime/lib/dd-codex.mjs +252 -80
  28. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +14 -19
  29. package/dist/harness-runtime/lib/dd-droid.mjs +117 -31
  30. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +56 -30
  31. package/dist/harness-runtime/lib/dd-grok.mjs +15 -13
  32. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +18 -27
  33. package/dist/harness-runtime/lib/dd-opencode.mjs +44 -14
  34. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +56 -53
  35. package/dist/harness-runtime/lib/dd-zcode.mjs +200 -111
  36. package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
  37. package/dist/harness-runtime/lib/lease-renewal.mjs +23 -0
  38. package/dist/harness-runtime/lib/lifecycle-command.d.mts +3 -3
  39. package/dist/harness-runtime/lib/lifecycle-command.mjs +74 -20
  40. package/dist/harness-runtime/lib/managed-daemon.d.mts +10 -1
  41. package/dist/harness-runtime/lib/managed-daemon.mjs +228 -40
  42. package/dist/harness-runtime/lib/native-children.d.mts +13 -0
  43. package/dist/harness-runtime/lib/native-children.mjs +78 -0
  44. package/dist/harness-runtime/lib/native-hook-command.d.mts +4 -0
  45. package/dist/harness-runtime/lib/native-hook-command.mjs +115 -11
  46. package/dist/harness-runtime/lib/native-streams.mjs +45 -0
  47. package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
  48. package/dist/harness-runtime/lib/observation-clock.mjs +73 -17
  49. package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
  50. package/dist/harness-runtime/lib/operation-errors.mjs +105 -8
  51. package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
  52. package/dist/harness-runtime/lib/operation-progress.mjs +112 -0
  53. package/dist/harness-runtime/lib/process-json.mjs +136 -38
  54. package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
  55. package/dist/harness-runtime/lib/tool-observations.mjs +5 -2
  56. package/dist/harness-runtime/lib/zcode-usage.mjs +3 -2
  57. package/dist/runtime/context.js +8 -4
  58. package/dist/schemas/run-control-receipt.schema.json +19 -0
  59. package/dist/services/code-checks.js +79 -9
  60. package/dist/services/codex-capacity.js +117 -41
  61. package/dist/services/codex-hook-delivery.js +2 -2
  62. package/dist/services/compatibility-preflight.js +4 -1
  63. package/dist/services/controller-fanout.js +9 -76
  64. package/dist/services/engines.js +70 -20
  65. package/dist/services/eval-snapshots.js +179 -40
  66. package/dist/services/external-work-launch.js +13 -8
  67. package/dist/services/harness-adapter.js +175 -11
  68. package/dist/services/hooks.js +65 -21
  69. package/dist/services/lifecycle-invocations.js +404 -32
  70. package/dist/services/managed-lease-monitor.js +128 -0
  71. package/dist/services/managed-processes.js +103 -47
  72. package/dist/services/merge-server.js +58 -21
  73. package/dist/services/recovery-observation-budget.js +67 -7
  74. package/dist/services/recovery-snapshot-database.js +7 -4
  75. package/dist/services/run-control-receipt.js +1 -1
  76. package/dist/services/run-control-worker.js +177 -118
  77. package/dist/services/run-control.js +99 -8
  78. package/dist/services/run-controller-adapter.js +169 -46
  79. package/dist/services/run-controller-capture.js +14 -2
  80. package/dist/services/run-controller-process.js +11 -4
  81. package/dist/services/run-controller-state.js +68 -7
  82. package/dist/services/run-controller.js +166 -94
  83. package/dist/services/run-fork.js +8 -3
  84. package/dist/services/run-recovery-guard.js +75 -0
  85. package/dist/services/run-recovery-runtime.js +1 -56
  86. package/dist/services/run-recovery.js +17 -13
  87. package/dist/services/runs.js +39 -6
  88. package/dist/services/runtime-budget.js +32 -15
  89. package/dist/services/runtime-scope-control.js +8 -0
  90. package/dist/services/runtime-scope-resume.js +30 -8
  91. package/dist/services/runtime-scope-worker.js +118 -73
  92. package/dist/services/runtime-service.js +6 -7
  93. package/dist/services/scope-writer-cleanup.js +3 -2
  94. package/dist/services/stage-context.js +135 -0
  95. package/dist/services/stage-lifecycle.js +3 -15
  96. package/dist/services/stage-pause.js +3 -2
  97. package/dist/services/usage.js +14 -3
  98. package/dist/services/vnext-code-review.js +10 -9
  99. package/dist/services/vnext-code.js +10 -8
  100. package/dist/services/vnext-contracts.js +10 -1
  101. package/dist/services/vnext-execution-profile.js +75 -2
  102. package/dist/services/vnext-fanout.js +5 -0
  103. package/dist/services/vnext-merge.js +10 -9
  104. package/dist/services/vnext-plan-review.js +21 -17
  105. package/dist/services/vnext-plan.js +9 -7
  106. package/dist/services/vnext-protocolize.js +15 -9
  107. package/dist/services/vnext-specify.js +6 -6
  108. package/dist/services/work-registry.js +34 -4
  109. package/dist/services/workspace-bootstrap.js +14 -3
  110. package/dist/shared/errors.js +11 -6
  111. package/dist/storage/agent-output.js +26 -0
  112. package/dist/storage/database.js +80 -21
  113. package/package.json +7 -4
@@ -2,6 +2,34 @@ import { setTimeout as delay } from "node:timers/promises";
2
2
  import crypto from "node:crypto";
3
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
35
  export function assertCapacityOwnerAdmission(context, record, operationId, binding) {
@@ -96,6 +124,20 @@ export async function continueCodexCapacity(input) {
96
124
  const promptSha = crypto.createHash("sha256").update(text).digest("hex");
97
125
  let ordinal = parsed.ordinal + 1;
98
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
+ };
99
141
  const refusals = [...(input.retainedFailures?.() ?? [])];
100
142
  if (parsed.ordinal > history.length)
101
143
  conflict("Continuation has no complete retained predecessor chain");
@@ -132,7 +174,7 @@ export async function continueCodexCapacity(input) {
132
174
  throw error;
133
175
  let observed;
134
176
  try {
135
- observed = await input.inspectOperation(intent.operation_id);
177
+ observed = await observe(() => input.inspectOperation(intent.operation_id), intent.operation_id);
136
178
  }
137
179
  catch (cause) {
138
180
  let code = cause;
@@ -142,28 +184,51 @@ export async function continueCodexCapacity(input) {
142
184
  throw cause;
143
185
  break;
144
186
  }
145
- if (observed.operation_id !== intent.operation_id || observed.session_id !== input.sessionId || observed.operation !== "session.prompt" || observed.primary_error)
146
- 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
+ }
147
210
  if (observed.state === "completed" && observed.result && typeof observed.result === "object") {
148
211
  input.signal?.throwIfAborted();
149
212
  input.assertCurrent();
150
- const boundary = await input.decision?.() ?? "continue";
213
+ const boundary = await observe(async () => await input.decision?.() ?? "continue", intent.operation_id);
151
214
  input.signal?.throwIfAborted();
152
215
  input.assertCurrent();
153
216
  if (boundary === "reconciliation_blocked")
154
217
  throw new AppError("capacity_continuation_blocked", "Retained completion cannot be reconciled with its owner boundary", 1);
155
218
  return boundary === "paused" || boundary === "completed" ? { provider_session_id: input.sessionId, capacity_decision: boundary } : observed.result;
156
219
  }
220
+ if (observed.state === "failed" && observed.error && !terminalCodexOverload(observed.error, input.sessionId))
221
+ throw observed.error;
157
222
  if (observed.state !== "failed" || !terminalCodexOverload(observed.error, input.sessionId))
158
- 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 });
159
224
  operationId = intent.operation_id;
160
225
  error = observed.error;
161
226
  ordinal += 1;
162
227
  }
163
- const decide = async () => {
228
+ const decide = async (observedOperationId = operationId) => {
164
229
  input.signal?.throwIfAborted();
165
230
  input.assertCurrent();
166
- const decision = await input.decision?.() ?? "continue";
231
+ const decision = await observe(async () => await input.decision?.() ?? "continue", observedOperationId);
167
232
  input.signal?.throwIfAborted();
168
233
  input.assertCurrent();
169
234
  if (decision === "reconciliation_blocked")
@@ -183,45 +248,30 @@ export async function continueCodexCapacity(input) {
183
248
  const completed = await decide();
184
249
  if (completed)
185
250
  return completed;
186
- const prior = await input.inspectOperation(operationId);
251
+ const prior = await observe(() => input.inspectOperation(operationId));
187
252
  const failure = prior.error;
188
253
  const confirmed = normalizeCodexFailure(failure, input.sessionId);
189
254
  const turnId = confirmed?.turn_id;
190
255
  input.signal?.throwIfAborted();
191
256
  input.assertCurrent();
257
+ if (prior.primary_error)
258
+ throw prior.primary_error;
192
259
  if (prior.operation_id !== operationId || prior.operation !== "session.prompt" || prior.session_id !== input.sessionId
193
260
  || prior.state !== "failed" || prior.primary_error || !turnId || !terminalCodexOverload(failure, input.sessionId, turnId)) {
194
- if (ordinal === 1)
195
- return null;
196
- throw error;
261
+ throw blocked("Retained predecessor does not prove the exact terminal overloaded Turn");
197
262
  }
198
- const settled = async () => {
263
+ const readiness = async () => {
199
264
  input.signal?.throwIfAborted();
200
265
  input.assertCurrent();
201
- const seen = await input.inspectSession(turnId);
266
+ const seen = await observe(() => input.inspectSession(turnId));
202
267
  input.signal?.throwIfAborted();
203
268
  input.assertCurrent();
204
- if (seen.terminal_turn_id !== undefined && seen.terminal_turn_id !== turnId)
205
- return false;
206
- const items = seen.terminal_turn_id === turnId && seen.terminal_status === "failed" ? seen.native_turn_items : confirmed?.native_turn_items;
207
- if (!nativeItemsSettled(items))
208
- return false;
209
- const settlement = seen.settlement;
210
- const ids = Array.isArray(seen.tree_session_ids) ? seen.tree_session_ids : [];
211
- const observations = Array.isArray(seen.tree_observations) ? seen.tree_observations : [];
212
- const treeProven = input.allowSettledDescendants
213
- ? ids.length > 0 && ids[0] === input.sessionId && observations.length === ids.length
214
- && ids.every(id => observations.some(item => item.provider_session_id === id && item.settled === true))
215
- && Array.isArray(seen.unsettled_sessions) && seen.unsettled_sessions.length === 0
216
- : ids.length === 1 && ids[0] === input.sessionId;
217
- return seen.provider_session_id === input.sessionId && seen.settled === true && treeProven
218
- && (!settlement || settlement.state === "not_required" || settlement.state === "settled" && settlement.operation_ids?.includes(operationId) === true);
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);
219
271
  };
220
- if (!await settled()) {
221
- if (ordinal === 1)
222
- return null;
223
- throw error;
224
- }
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");
225
275
  const native = normalizeCodexFailure(failure, input.sessionId, turnId);
226
276
  let refusal = refusals.find(item => item.ordinal === ordinal - 1);
227
277
  if (refusal && (refusal.turn_id !== turnId || refusal.session_id !== input.sessionId || refusal.policy !== CAPACITY_POLICY))
@@ -239,27 +289,52 @@ export async function continueCodexCapacity(input) {
239
289
  conflict("Retained continuation has no original failure time");
240
290
  const observedAt = typeof native.observed_at === "string" && Number.isFinite(Date.parse(native.observed_at)) ? native.observed_at : new Date().toISOString();
241
291
  refusal = refusalObservation(failure, ordinal - 1, observedAt);
242
- input.retainFailure?.(operationId, refusal);
292
+ if (!input.atomicIntent)
293
+ input.retainFailure?.(operationId, refusal);
243
294
  refusals.push(refusal);
244
295
  }
245
296
  const burst = overloadBurst(refusals);
246
- if (burst)
297
+ if (burst) {
298
+ if (input.atomicIntent)
299
+ input.retainFailure?.(operationId, refusal);
247
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 });
301
+ }
248
302
  const nextId = `${rootId}:capacity:${ordinal}:session.prompt`;
249
303
  const existing = history.find(item => item.operation_id === nextId);
250
304
  if (existing && (existing.predecessor !== operationId || existing.turn_id !== turnId || existing.prompt_sha256 !== promptSha || existing.ordinal !== ordinal))
251
305
  throw error;
252
306
  const wait = existing ? Math.max(0, Date.parse(existing.not_before) - Date.now()) : capacityDelay(failure, ordinal);
253
- if (!Number.isFinite(wait) || Date.now() + wait > 8.64e15 || input.deadline !== undefined && Date.now() + wait >= input.deadline)
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);
254
310
  throw new AppError("capacity_owner_deadline_exceeded", "Continuation wait exceeds its original owner deadline", 1, { primary_error: failure, deadline: input.deadline ?? null });
311
+ }
255
312
  if (!existing) {
256
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 };
257
314
  input.retainIntent(intent);
258
315
  history.push(intent);
259
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;
260
335
  // Node timers truncate large delays. Bounded slices preserve cancellation
261
336
  // and owner checks; monotonic elapsed time does not renew a Retry-After.
262
- const until = performance.now() + wait;
337
+ const until = performance.now() + Math.max(0, Date.parse(history.find(item => item.operation_id === nextId).not_before) - Date.now());
263
338
  while (performance.now() < until) {
264
339
  await delay(Math.min(250, Math.max(0, until - performance.now())), undefined, { signal: input.signal });
265
340
  const boundary = await decide();
@@ -269,19 +344,20 @@ export async function continueCodexCapacity(input) {
269
344
  const afterWait = await decide();
270
345
  if (afterWait)
271
346
  return afterWait;
272
- if (!await settled())
273
- throw new AppError("capacity_continuation_blocked", "Native predecessor settlement changed during capacity wait", 1, { primary_error: failure });
347
+ const refreshedBoundary = await waitForOwnedChildren(await readiness());
348
+ if (refreshedBoundary)
349
+ return refreshedBoundary;
274
350
  const beforeDispatch = await decide();
275
351
  if (beforeDispatch)
276
352
  return beforeDispatch;
277
353
  try {
278
- return await input.prompt(nextId, text, ordinal);
354
+ return await observe(() => input.prompt(nextId, text, ordinal), nextId);
279
355
  }
280
356
  catch (nextError) {
281
357
  // A task can complete/pause while the daemon queues for its permit.
282
358
  // Admission then refuses the stale prompt; accept that owner decision
283
359
  // before treating the transport's rejection as another provider failure.
284
- const finished = await decide();
360
+ const finished = await decide(nextId);
285
361
  if (finished)
286
362
  return finished;
287
363
  error = nextError;
@@ -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,81 +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
- ...(text(value, ["terminal_observed_at"]) ? { terminal_observed_at: text(value, ["terminal_observed_at"]) } : {}),
142
- ...(value.provider_error !== undefined ? { provider_error: value.provider_error } : {}),
143
- ...(value.native_turn_items && typeof value.native_turn_items === "object" ? { native_turn_items: value.native_turn_items } : {}) });
144
- };
145
- for (const value of list(receipt.descendants))
146
- add(value, "adapter.descendants");
147
- for (const value of list(receipt.children))
148
- add(value, "opencode.session.children");
149
- const subagents = object(object(receipt.evidence).subagents ?? receipt.subagents);
150
- for (const value of list(subagents.running))
151
- add(value, "zcode/session/subagents", rootSessionId, "running");
152
- for (const value of list(subagents.completed))
153
- add(value, "zcode/session/subagents", rootSessionId, "completed");
154
- for (const value of list(object(subagents.ended).items)) {
155
- const status = String(object(value).status ?? "unknown");
156
- if (!["success", "failed", "cancelled", "lost"].includes(status))
157
- throw new AppError("native_child_outcome_unavailable", "ZCode ended child has no recognized native outcome", 1, { child: value });
158
- add(value, "zcode/session/subagents", rootSessionId);
112
+ try {
113
+ return normalizeNativeChildren(receipt, rootSessionId);
159
114
  }
160
- const unique = new Map();
161
- const rank = { unknown: 0, running: 1, settled_by_root: 2, completed: 3, failed: 3, cancelled: 3 };
162
- for (const child of candidates) {
163
- const previous = unique.get(child.session_id);
164
- if (previous && previous.parent_session_id !== child.parent_session_id) {
165
- // Keep the contradictory parent visible to the existing provenance gate,
166
- // independently of input order or which source happened to say success.
167
- const foreign = [previous, child].filter(item => item.parent_session_id !== rootSessionId).sort((a, b) => a.parent_session_id.localeCompare(b.parent_session_id))[0];
168
- unique.set(child.session_id, { ...foreign, provenance: "parent_mismatch" });
169
- continue;
170
- }
171
- if (previous && rank[previous.status] === 3 && rank[child.status] === 3 && previous.status !== child.status)
172
- throw new AppError("native_child_outcome_conflict", "Native child has contradictory terminal outcomes", 1, { session_id: child.session_id, observations: [previous, child] });
173
- if (previous && previous.status === child.status && previous.terminal_turn_id && child.terminal_turn_id && previous.terminal_turn_id !== child.terminal_turn_id)
174
- throw new AppError("native_child_turn_conflict", "Native child has contradictory terminal Turn identities", 1, { session_id: child.session_id, observations: [previous, child] });
175
- if (previous && previous.status === child.status) {
176
- unique.set(child.session_id, { ...previous, ...child,
177
- ...(child.terminal_turn_id ?? previous.terminal_turn_id ? { terminal_turn_id: child.terminal_turn_id ?? previous.terminal_turn_id } : {}),
178
- ...(child.provider_error ?? previous.provider_error ? { provider_error: child.provider_error ?? previous.provider_error } : {}),
179
- ...(child.native_turn_items ?? previous.native_turn_items ? { native_turn_items: child.native_turn_items ?? previous.native_turn_items } : {}) });
180
- continue;
181
- }
182
- if (!previous || rank[child.status] > rank[previous.status])
183
- 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);
184
118
  }
185
- return [...unique.values()].sort((left, right) => left.session_id.localeCompare(right.session_id));
186
119
  }
@@ -82,7 +82,7 @@ export function installCurrentEngine(context, input = {}) {
82
82
  packageVersion: build.version,
83
83
  integrityChecksum: stagedChecksum
84
84
  }) : null;
85
- if (duplicate) {
85
+ if (duplicate && manifestDiagnostics(duplicate, { checkEntrypoint: false }).length === 0) {
86
86
  fs.rmSync(tmp, { recursive: true, force: true });
87
87
  return {
88
88
  ok: true,
@@ -141,7 +141,7 @@ export function doctorEngines(context, input = {}) {
141
141
  const installed = readInstalledManifests(root);
142
142
  const checks = installed.map(manifest => {
143
143
  const diagnostics = manifestDiagnostics(manifest);
144
- return { engine: manifestSummary(manifest, diagnostics), status: diagnostics.length ? "corrupt" : "ok", diagnostics };
144
+ return { engine: manifestSummary(manifest, diagnostics), status: diagnostics.includes("entrypoint_probe_unavailable") ? "unverified" : diagnostics.length ? "corrupt" : "ok", diagnostics };
145
145
  });
146
146
  const classification = input.classification ?? classifyCliOperation(["engine", "doctor"], input.env ?? context.env);
147
147
  const selection = selectEngine(context, input, {}, classification);
@@ -223,6 +223,8 @@ export function bindRunEngine(context, input, prepared = prepareRunEngineBind(co
223
223
  encoding: "utf8",
224
224
  timeout: 30_000
225
225
  });
226
+ if (probe.error || probe.signal)
227
+ throw new AppError("run_engine_probe_unavailable", "Selected engine observation could not complete; compatibility is unknown", 1, { run_id: located.run_id, cause: probe.error?.message, signal: probe.signal });
226
228
  if (probe.status !== 0) {
227
229
  throw new AppError("run_engine_probe_failed", "Selected engine cannot read the RUN authority", 1, {
228
230
  run_id: located.run_id,
@@ -291,6 +293,8 @@ export function upgradeForkedRunEngine(context, input) {
291
293
  env: { ...process.env, ...context.env, DD_FLOW_HOME: context.ddFlowHome, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: manifest.snapshot_root },
292
294
  encoding: "utf8", timeout: 30_000
293
295
  });
296
+ if (probe.error || probe.signal)
297
+ throw new AppError("fork_engine_probe_unavailable", "Selected engine observation could not complete; compatibility is unknown", 1, { run_id: located.run_id, cause: probe.error?.message, signal: probe.signal });
294
298
  if (probe.status !== 0)
295
299
  throw new AppError("fork_engine_probe_failed", "Selected engine cannot read the forked RUN", 1, { run_id: located.run_id, engine: manifestSummary(manifest), stderr: probe.stderr.slice(0, 2000) });
296
300
  const binding = { schema_id: runEngineBindingSchemaId, run_id: located.run_id, project_root: projectRoot, bound_at: context.now(), source: "fork_upgrade", reason: requiredText(input.reason, "reason"), engine: requested, probe: { status: "passed", command: `dd-flow ${probeArgs.join(" ")}` } };
@@ -429,7 +433,11 @@ export function selectEngine(context, input, options = {}, classification = inpu
429
433
  const requiredRange = stringValue(compatibility?.engine?.version_range) ?? (classification.mode === "mb_upgrade" ? null : `>=${build.version} <9999.0.0`);
430
434
  const recommended = stringValue(compatibility?.engine?.recommended_version) ?? build.version;
431
435
  const installHintTemplate = stringValue(compatibility?.engine?.old_engine_install_hint) ?? `npx ${packageName}@<version> engine install`;
432
- const manifests = readInstalledManifests(engineStoreRoot(context.ddFlowHome))
436
+ // An engine-mode caller already executes this module's build. Another
437
+ // installed engine cannot establish compatibility for its current writes.
438
+ // Keep full snapshot validation in routing/binding, not lease maintenance.
439
+ const executingCurrent = options.allowCurrentInProcess && isEngineMode(input.env ?? process.env);
440
+ const manifests = executingCurrent ? [] : readInstalledManifests(engineStoreRoot(context.ddFlowHome))
433
441
  .filter((manifest) => manifest.package_name === packageName)
434
442
  .filter((manifest) => requiredRange !== null && satisfiesRange(manifest.package_version, requiredRange))
435
443
  .filter((manifest) => manifestHealthy(manifest))
@@ -447,7 +455,7 @@ export function selectEngine(context, input, options = {}, classification = inpu
447
455
  requiredRange !== null &&
448
456
  packageName === build.package_name &&
449
457
  satisfiesRange(build.version, requiredRange)
450
- ? currentBuildManifest(context, build)
458
+ ? currentBuildManifest(context, build, options.verifyCurrentContent !== false)
451
459
  : null);
452
460
  const diagnostics = [
453
461
  ...targetBlockers,
@@ -465,9 +473,38 @@ export function selectEngine(context, input, options = {}, classification = inpu
465
473
  diagnostics
466
474
  };
467
475
  }
468
- function currentBuildManifest(context, build) {
476
+ function currentBuildManifest(context, build, verifyContent) {
469
477
  const packageRoot = findPackageRoot(path.dirname(sourceFile)) ?? process.cwd();
470
- return buildManifest(context, build.package_name, build.version, packageRoot, packageRoot, packageRoot);
478
+ const manifestFile = path.join(packageRoot, "engine.json");
479
+ const installedLayout = path.basename(path.dirname(path.dirname(packageRoot))) === "engines"
480
+ && path.basename(path.dirname(packageRoot)) === path.basename(path.dirname(engineVersionRoot(context.ddFlowHome, build.package_name, build.version)));
481
+ if (fs.existsSync(manifestFile) || installedLayout) {
482
+ const installed = readManifest(manifestFile);
483
+ // Use the executing module's physical root, never an environment alias.
484
+ // A damaged installed manifest must not become a development fallback.
485
+ if (!installed || installed.package_name !== build.package_name || installed.package_version !== build.version
486
+ || installed.engine_version !== build.version || installed.engine_id !== "dd-flow-engine"
487
+ || typeof installed.snapshot_root !== "string" || !fs.existsSync(installed.snapshot_root)
488
+ || fs.realpathSync(installed.snapshot_root) !== fs.realpathSync(packageRoot)
489
+ || typeof installed.entrypoint !== "string" || installed.integrity?.source !== "package_snapshot"
490
+ || typeof installed.integrity?.checksum !== "string" || !/^[a-f0-9]{64}$/.test(installed.integrity.checksum)
491
+ || (installed.schema_registry && (!Array.isArray(installed.schema_registry.entries)
492
+ || installed.schema_registry.entries.some(entry => !entry || typeof entry.path !== "string"
493
+ || typeof entry.id !== "string" || typeof entry.name !== "string" || !/^[a-f0-9]{64}$/.test(entry.checksum ?? ""))))) {
494
+ throw new AppError("unhealthy_engine", "Executing engine snapshot manifest does not match its build", 1, { manifest: manifestFile });
495
+ }
496
+ const diagnostics = manifestDiagnostics(installed, { checkEntrypoint: false, verifyContent });
497
+ if (diagnostics.length)
498
+ throw new AppError("unhealthy_engine", "Executing engine snapshot is unhealthy", 1, { manifest: manifestFile, diagnostics });
499
+ return installed;
500
+ }
501
+ // Only an unmaterialized package has no snapshot proof. Canon and
502
+ // package-manager aliases are expected in development checkouts.
503
+ const manifest = buildManifest(context, build.package_name, build.version, packageRoot, packageRoot, packageRoot, null);
504
+ // An ephemeral routing choice has no snapshot proof. Hash bytes only when
505
+ // materializing/validating an installed snapshot, never on every CLI call.
506
+ manifest.integrity.mode = "degraded_local_development";
507
+ return manifest;
471
508
  }
472
509
  export function compatibilityContexts(context, input = {}) {
473
510
  const env = input.env ?? process.env;
@@ -614,7 +651,7 @@ function option(parsed, key) {
614
651
  function positional(parsed, index) {
615
652
  return parsed.positional[index] ?? null;
616
653
  }
617
- function buildManifest(context, packageName, version, packageRoot, snapshotRoot, checksumRoot) {
654
+ function buildManifest(context, packageName, version, packageRoot, snapshotRoot, checksumRoot, checksum = engineSnapshotChecksum(checksumRoot)) {
618
655
  return {
619
656
  schema_id: engineManifestSchemaId,
620
657
  package_name: packageName,
@@ -635,7 +672,7 @@ function buildManifest(context, packageName, version, packageRoot, snapshotRoot,
635
672
  schema_registry: buildSchemaRegistry(checksumRoot),
636
673
  integrity: {
637
674
  source: "package_snapshot",
638
- checksum: engineSnapshotChecksum(checksumRoot),
675
+ checksum,
639
676
  mode: "full_content"
640
677
  }
641
678
  };
@@ -703,19 +740,23 @@ export function prepareForkEngine(engineVersion, integrityChecksum) {
703
740
  throw new AppError("harness_runtime_unbuilt", "Build the CLI before preparing a fork", 1);
704
741
  const files = packageSnapshotFiles(packageRoot);
705
742
  if (integrityChecksum !== undefined) {
706
- const checksum = crypto.createHash("sha256");
707
- for (const [relative, source] of [...files].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
708
- if (path.basename(relative) === "engine.json")
709
- continue;
710
- checksum.update(relative);
711
- checksum.update("\0");
712
- checksum.update(fs.readFileSync(source));
713
- checksum.update("\0");
714
- }
715
- if (checksum.digest("hex") !== integrityChecksum)
743
+ const checksum = snapshotFilesChecksum(files);
744
+ if (checksum !== integrityChecksum)
716
745
  throw new AppError("fork_engine_unavailable", "Requested engine checksum differs from the invoking CLI artifact", 2, { requested: integrityChecksum });
717
746
  }
718
747
  }
748
+ function snapshotFilesChecksum(files) {
749
+ const checksum = crypto.createHash("sha256");
750
+ for (const [relative, source] of [...files].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
751
+ if (path.basename(relative) === "engine.json")
752
+ continue;
753
+ checksum.update(relative);
754
+ checksum.update("\0");
755
+ checksum.update(fs.readFileSync(source));
756
+ checksum.update("\0");
757
+ }
758
+ return checksum.digest("hex");
759
+ }
719
760
  function readProductionDependencies(packageRoot) {
720
761
  try {
721
762
  const manifest = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
@@ -795,6 +836,9 @@ function readInstalledManifests(root) {
795
836
  if (!fs.statSync(packageRoot).isDirectory())
796
837
  continue;
797
838
  for (const versionDir of fs.readdirSync(packageRoot)) {
839
+ // Failed staging, activation backups and locks are not installations.
840
+ if (versionDir.startsWith(".") || /\.previous-\d+-\d+$/.test(versionDir) || versionDir.endsWith(".lock"))
841
+ continue;
798
842
  const manifest = readManifest(path.join(packageRoot, versionDir, "engine.json"));
799
843
  if (manifest)
800
844
  manifests.push(manifest);
@@ -847,7 +891,7 @@ function manifestDiagnostics(manifest, options = {}) {
847
891
  if (!manifest.integrity || manifest.integrity.mode !== "full_content" || !manifest.integrity.checksum) {
848
892
  diagnostics.push("content_checksum_missing");
849
893
  }
850
- else if (fs.existsSync(snapshotRoot)) {
894
+ else if (options.verifyContent !== false && fs.existsSync(snapshotRoot)) {
851
895
  try {
852
896
  if (engineSnapshotChecksum(snapshotRoot) !== manifest.integrity.checksum)
853
897
  diagnostics.push("content_checksum_mismatch");
@@ -861,6 +905,8 @@ function manifestDiagnostics(manifest, options = {}) {
861
905
  const schemaPath = safeResolveSnapshotPath(manifest, entry.path);
862
906
  if (!schemaPath || !fs.existsSync(schemaPath))
863
907
  diagnostics.push(`schema_registry_missing:${entry.name}`);
908
+ else if (options.verifyContent === false)
909
+ continue;
864
910
  else if (fileChecksum(schemaPath) !== entry.checksum)
865
911
  diagnostics.push(`schema_registry_checksum_mismatch:${entry.name}`);
866
912
  else {
@@ -887,7 +933,9 @@ function manifestDiagnostics(manifest, options = {}) {
887
933
  stdio: ["ignore", "pipe", "pipe"],
888
934
  timeout: 15_000
889
935
  });
890
- if (health.status !== 0)
936
+ if (health.error || health.signal)
937
+ diagnostics.push("entrypoint_probe_unavailable");
938
+ else if (health.status !== 0)
891
939
  diagnostics.push("entrypoint_dependency_load_failed");
892
940
  }
893
941
  return diagnostics;
@@ -1072,6 +1120,8 @@ function collectFiles(root, current, files) {
1072
1120
  if (entry.name === "engine.json")
1073
1121
  continue;
1074
1122
  const full = path.join(current, entry.name);
1123
+ if (entry.isSymbolicLink())
1124
+ throw new AppError("engine_integrity_symlink", "Pinned engine snapshot contains an unhashed symbolic link", 1, { path: path.relative(root, full) });
1075
1125
  if (entry.isDirectory())
1076
1126
  collectFiles(root, full, files);
1077
1127
  else if (entry.isFile())