@deksden-com/dd-flow-cli 0.9.0-beta.143 → 0.9.0-beta.145

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 (197) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +13 -8
  3. package/dist/build-info.json +7 -7
  4. package/dist/cli/command-inputs.js +9 -3
  5. package/dist/cli/command-metadata.js +33 -14
  6. package/dist/cli/help.js +32 -1
  7. package/dist/cli/hook-ingress.js +27 -3
  8. package/dist/cli/memory-flow-admission.js +7 -3
  9. package/dist/cli/memory-init-invocation.js +10 -3
  10. package/dist/cli/memory-init.js +74 -68
  11. package/dist/cli/memory-storage-admission.js +3 -2
  12. package/dist/cli/memory-storage-qualification.js +105 -0
  13. package/dist/cli/memory-storage-transition.js +123 -0
  14. package/dist/cli/memory-upgrade.js +189 -0
  15. package/dist/cli/native-hook-ingress.js +14 -4
  16. package/dist/cli/native-hook-preparation.js +55 -0
  17. package/dist/cli/response-file.js +2 -1
  18. package/dist/cli/run-cli.js +70 -32
  19. package/dist/cli/setup.js +2 -1
  20. package/dist/cli.js +12 -0
  21. package/dist/domain/stage-handler-identities.js +8 -0
  22. package/dist/harness-runtime/bin/dd-agy.mjs +3 -3
  23. package/dist/harness-runtime/bin/dd-grok.mjs +3 -3
  24. package/dist/harness-runtime/bin/dd-zcode.mjs +5 -5
  25. package/dist/harness-runtime/lib/agy-error-boundary.mjs +58 -0
  26. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +62 -20
  27. package/dist/harness-runtime/lib/dd-agy.mjs +17 -3
  28. package/dist/harness-runtime/lib/dd-droid.mjs +1 -1
  29. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +70 -13
  30. package/dist/harness-runtime/lib/dd-grok.mjs +6 -3
  31. package/dist/harness-runtime/lib/dd-zcode.mjs +44 -14
  32. package/dist/harness-runtime/lib/delegation-instructions.mjs +1 -1
  33. package/dist/harness-runtime/lib/entity-references.d.mts +2 -0
  34. package/dist/harness-runtime/lib/entity-references.mjs +9 -0
  35. package/dist/harness-runtime/lib/managed-daemon.mjs +13 -6
  36. package/dist/harness-runtime/lib/native-hook-command.mjs +4 -4
  37. package/dist/harness-runtime/lib/opencode-plugin.mjs +3 -3
  38. package/dist/harness-runtime/lib/operation-errors.mjs +1 -1
  39. package/dist/harness-runtime/lib/provider-overload-policy.d.mts +4 -2
  40. package/dist/harness-runtime/lib/provider-overload-policy.mjs +19 -6
  41. package/dist/harness-runtime/lib/runner-events.mjs +3 -1
  42. package/dist/harness-runtime/lib/setup-progress.mjs +8 -6
  43. package/dist/schemas/mb-upgrade-stage-result.schema.json +863 -0
  44. package/dist/schemas/memory-repair-work-result.schema.json +9 -2
  45. package/dist/schemas/release-impact.schema.json +3 -3
  46. package/dist/schemas/stage-start-response.schema.json +4 -0
  47. package/dist/services/accepted-intent.js +13 -1
  48. package/dist/services/canon.js +43 -7
  49. package/dist/services/cli-operation-classifier.js +1 -1
  50. package/dist/services/cli-upgrade-allowlist.js +1 -1
  51. package/dist/services/code-checks.js +11 -4
  52. package/dist/services/compatibility-preflight.js +5 -2
  53. package/dist/services/controller-fanout.js +21 -4
  54. package/dist/services/controller-memory-task.js +45 -0
  55. package/dist/services/controller-memory-worker.js +48 -0
  56. package/dist/services/dashboard.js +2 -2
  57. package/dist/services/descriptor-run-start.js +26 -19
  58. package/dist/services/descriptor-stage-coordinator.js +30 -10
  59. package/dist/services/descriptor-stage-start.js +40 -4
  60. package/dist/services/engines.js +64 -14
  61. package/dist/services/eval-snapshots.js +104 -93
  62. package/dist/services/external-work-launch.js +5 -3
  63. package/dist/services/flow-catalog.js +8 -4
  64. package/dist/services/frozen-engine.js +113 -11
  65. package/dist/services/hook-memory-routing.js +188 -0
  66. package/dist/services/hook-recovery-guard.js +14 -0
  67. package/dist/services/hook-session-observation.js +24 -0
  68. package/dist/services/hooks.js +27 -8
  69. package/dist/services/hybrid-contracts.js +2 -13
  70. package/dist/services/hybrid-digest.js +14 -0
  71. package/dist/services/installed-flow-source.js +25 -4
  72. package/dist/services/lifecycle-invocations.js +33 -5
  73. package/dist/services/memory-accepted-composition.js +5 -4
  74. package/dist/services/memory-completed-correction-wave.js +23 -15
  75. package/dist/services/memory-compose-handler.js +163 -21
  76. package/dist/services/memory-composition-acceptance.js +186 -10
  77. package/dist/services/memory-composition-graph.js +42 -11
  78. package/dist/services/memory-composition-registration.js +23 -8
  79. package/dist/services/memory-composition-work-custody.js +14 -9
  80. package/dist/services/memory-composition-work-results.js +15 -3
  81. package/dist/services/memory-corrected-review-batch.js +10 -3
  82. package/dist/services/memory-corrected-review-findings.js +4 -2
  83. package/dist/services/memory-corrected-review-inputs.js +10 -5
  84. package/dist/services/memory-corrected-review-receipt.js +17 -9
  85. package/dist/services/memory-corrected-review-registration.js +3 -2
  86. package/dist/services/memory-correction-check-receipt.js +20 -4
  87. package/dist/services/memory-custody-snapshot.js +36 -0
  88. package/dist/services/memory-finish-evidence.js +44 -22
  89. package/dist/services/memory-finish-handler.js +37 -23
  90. package/dist/services/memory-finish-integration.js +218 -48
  91. package/dist/services/memory-finish-readiness.js +66 -19
  92. package/dist/services/memory-git-integration.js +87 -18
  93. package/dist/services/memory-historical-corrected-review.js +21 -15
  94. package/dist/services/memory-historical-original-review.js +22 -9
  95. package/dist/services/memory-historical-repair-registration.js +14 -8
  96. package/dist/services/memory-historical-second-review.js +30 -11
  97. package/dist/services/memory-init-contexts.js +28 -2
  98. package/dist/services/memory-init-controller-start.js +19 -8
  99. package/dist/services/memory-init-git-policy.js +50 -21
  100. package/dist/services/memory-init-lint-capability.js +21 -3
  101. package/dist/services/memory-init-native-capacity.js +13 -0
  102. package/dist/services/memory-init-preflight.js +11 -1
  103. package/dist/services/memory-init-request-candidate.js +73 -17
  104. package/dist/services/memory-init-request-input.js +23 -7
  105. package/dist/services/memory-init-run-bundle.js +49 -12
  106. package/dist/services/memory-init-storage.js +17 -7
  107. package/dist/services/memory-layer.js +68 -4
  108. package/dist/services/memory-lint-config.js +111 -2
  109. package/dist/services/memory-native-review-copy.js +5 -67
  110. package/dist/services/memory-normal-flow-entry.js +12 -0
  111. package/dist/services/memory-ordinary-defaults.js +22 -5
  112. package/dist/services/memory-preflight-handler.js +15 -0
  113. package/dist/services/memory-repair-current-view.js +1 -1
  114. package/dist/services/memory-repair-document-delta.js +70 -33
  115. package/dist/services/memory-repair-history.js +12 -7
  116. package/dist/services/memory-repair-physical-binding.js +1 -119
  117. package/dist/services/memory-repair-publication-plan.js +5 -3
  118. package/dist/services/memory-repair-publication-source.js +1 -1
  119. package/dist/services/memory-repair-publication.js +3 -2
  120. package/dist/services/memory-repair-registration.js +2 -1
  121. package/dist/services/memory-repair-work-baseline.js +4 -2
  122. package/dist/services/memory-repair-work-completion.js +32 -12
  123. package/dist/services/memory-repair-work-results.js +21 -2
  124. package/dist/services/memory-repair-workspace.js +11 -2
  125. package/dist/services/memory-repair-writer-copy.js +2 -1
  126. package/dist/services/memory-result-binding.js +78 -12
  127. package/dist/services/memory-retained-review-authority.js +17 -8
  128. package/dist/services/memory-retained-second-baseline.js +1 -1
  129. package/dist/services/memory-retained-second-repairs.js +12 -6
  130. package/dist/services/memory-review-correction-plan.js +47 -12
  131. package/dist/services/memory-review-handler.js +17 -17
  132. package/dist/services/memory-review-inputs.js +55 -9
  133. package/dist/services/memory-review-limits.js +10 -0
  134. package/dist/services/memory-review-receipt.js +2 -2
  135. package/dist/services/memory-review-registration.js +13 -4
  136. package/dist/services/memory-review-work-custody.js +3 -1
  137. package/dist/services/memory-review-work-input.js +26 -7
  138. package/dist/services/memory-review-work-results.js +26 -4
  139. package/dist/services/memory-second-corrected-review-inputs.js +5 -3
  140. package/dist/services/memory-second-correction-plan.js +2 -1
  141. package/dist/services/memory-second-repair-launch.js +4 -4
  142. package/dist/services/memory-second-repair-source.js +1 -1
  143. package/dist/services/memory-stage-dispatch.js +38 -1
  144. package/dist/services/memory-stage-results.js +113 -22
  145. package/dist/services/memory-upgrade-analysis-handler.js +243 -0
  146. package/dist/services/memory-upgrade-bootstrap-readiness.js +251 -0
  147. package/dist/services/memory-upgrade-evidence.js +100 -0
  148. package/dist/services/memory-upgrade-inventory.js +442 -0
  149. package/dist/services/memory-upgrade-publication-custody.js +73 -0
  150. package/dist/services/memory-upgrade-publication.js +413 -0
  151. package/dist/services/memory-upgrade-selection.js +430 -0
  152. package/dist/services/memory-upgrade-start.js +60 -0
  153. package/dist/services/memory-upgrade-storage-readiness.js +340 -0
  154. package/dist/services/memory-verification-acceptance.js +38 -17
  155. package/dist/services/memory-verification-checks.js +77 -23
  156. package/dist/services/memory-verification-receipt.js +6 -5
  157. package/dist/services/memory-verify-handler.js +15 -13
  158. package/dist/services/memory-work-result-json.js +12 -0
  159. package/dist/services/memory-work-result-preparation.js +5 -4
  160. package/dist/services/merge-server.js +4 -2
  161. package/dist/services/migrations.js +127 -98
  162. package/dist/services/physical-startup.js +45 -0
  163. package/dist/services/project-registry.js +97 -0
  164. package/dist/services/project-summary.js +1 -1
  165. package/dist/services/projects.js +3 -93
  166. package/dist/services/prompt-delivery.js +71 -0
  167. package/dist/services/provider-capacity.js +74 -9
  168. package/dist/services/recovery-snapshot-database.js +32 -4
  169. package/dist/services/repair-intents.js +1 -12
  170. package/dist/services/repair-receipt.js +13 -0
  171. package/dist/services/run-control-worker.js +35 -18
  172. package/dist/services/run-controller-process.js +2 -6
  173. package/dist/services/run-controller-recovery.js +6 -1
  174. package/dist/services/run-controller.js +131 -80
  175. package/dist/services/run-recovery-guard.js +30 -0
  176. package/dist/services/run-recovery.js +11 -44
  177. package/dist/services/run-start-custody.js +17 -5
  178. package/dist/services/runtime-scope-worker.js +46 -19
  179. package/dist/services/sessions.js +3 -24
  180. package/dist/services/setup.js +63 -13
  181. package/dist/services/stage-handlers.js +9 -7
  182. package/dist/services/stage-pause.js +108 -44
  183. package/dist/services/version-status.js +1 -1
  184. package/dist/services/vnext-fanout.js +21 -9
  185. package/dist/services/vnext-plan-review.js +3 -5
  186. package/dist/services/work-registry.js +80 -33
  187. package/dist/services/workspace-files.js +1 -1
  188. package/dist/shared/entity-references.js +1 -8
  189. package/dist/storage/data-preservation.js +65 -0
  190. package/dist/storage/database.js +53 -19
  191. package/dist/storage/import-guard.js +84 -27
  192. package/dist/storage/memory-upgrade-schema-bootstrap.js +38 -0
  193. package/dist/storage/paired-writer-migration.js +424 -0
  194. package/dist/storage/writer-contract.js +11 -4
  195. package/dist/storage/writer-migration.js +313 -52
  196. package/dist/storage/writer-restore.js +217 -0
  197. package/package.json +8 -4
@@ -60,7 +60,7 @@ export function delegationContract(harness) {
60
60
  return `dd-flow/delegation/${key}@${["codex", "opencode"].includes(key) ? 2 : 1}`;
61
61
  }
62
62
 
63
- export const lifecycleCommandCompletionInstruction = "Run runtime-issued lifecycle CLI calls in the foreground, never with a native background or detached-shell option. A shell tool may yield before the lifecycle process exits. Retain and poll its exact process/session handle to completion, including inside a tool wrapper; empty interim output is not a CLI result. If a wrapper returns 'Script running with cell ID …', use that wrapper's wait tool to finish the same cell before expecting an inner shell session handle; do not execute the command again. Read final stdout or response_file before deciding continuation, and never reissue the command while it is pending.";
63
+ export const lifecycleCommandCompletionInstruction = "Run runtime-issued lifecycle CLI calls in the foreground, never with a native background or detached-shell option. A shell tool may yield before the lifecycle process exits. Retain and poll its exact process/session handle to completion, including inside a tool wrapper; empty interim output is not a CLI result. If a wrapper returns 'Script running with cell ID …', use that wrapper's wait tool to finish the same cell before expecting an inner shell session handle; do not execute the command again. Read final stdout or response_file before deciding continuation, and never reissue the command while it is pending. When the result provides prompt_delivery, read its complete plain Markdown file at prompt_delivery.path before working; it contains the current instructions, unlike the historical prompt_path. If delivery publication is pending, stop for reconciliation without repeating the accepted command.";
64
64
 
65
65
  export const lifecycleRetryInstruction = `${lifecycleCommandCompletionInstruction} A failure is not permission to replay a lifecycle command. For proven effect=no_effect with recoverable=true and retry_command, correct the input and execute that exact issued successor. A check failure can retain receipts while leaving Work running: correct the evidenced cause first, then use the CLI's exact retry_command for an intentional fresh check attempt; never replay a check merely because Work is unsettled. If a successful CODE finish returns outcome=verification_required, inspect the retained receipts, update its verification file, and use only next.finish_command: the Stage is still running and the previous invocation ID is consumed. If continuation.kind=repair_required, or a successful result has outcome=repair_required and a repair Work ID, end this Turn for the registered repair child; use any retry_command only after that repair settles and verification is refreshed. Unknown effects require reconciliation, never replay. A committed effect without an explicit continuation is not retryable. A native shell launch refusal before the CLI starts permits correcting only that shell-form error and retrying the same exact command. Otherwise report the primary error and stop; never invent IDs, call hooks directly, or run status as a substitute.`;
66
66
 
@@ -0,0 +1,2 @@
1
+ export function shortEntityReference(id: string): string;
2
+ export function entityReferenceVariants(id: string): string[];
@@ -0,0 +1,9 @@
1
+ /** Stable model-facing reference for persisted project, RUN and Work IDs. */
2
+ export function shortEntityReference(id) {
3
+ return /^(?:PRJ|RUN|WRK)-\d{3,}(?:-|$)/.exec(id)?.[0]?.replace(/-$/, "") ?? id;
4
+ }
5
+
6
+ /** Accepted spellings of one already-resolved entity. Wrong full slugs stay distinct. */
7
+ export function entityReferenceVariants(id) {
8
+ return [...new Set([id, shortEntityReference(id)])];
9
+ }
@@ -409,21 +409,28 @@ export async function closeManagedBridge(config, state, bridge) {
409
409
  await finishDaemonProcess(config, state.provider_process, "stopped", "daemon_stopped");
410
410
  }
411
411
 
412
- export function scheduleDaemonProcessHeartbeat(config, record) {
412
+ export function scheduleDaemonProcessHeartbeat(config, record, { signal } = {}) {
413
413
  if (!record?.id || !record.lease_token) return;
414
414
  const key = leaseKey(config, record);
415
- if (leases.has(key)) return;
416
- const lease = { pending: null, error: null, timer: null, lost: false, closing: false, expiresAt: Date.parse(record.lease_expires_at ?? ""), renewal: createRenewalState({ leaseExpiresAt: record.lease_expires_at }), stateDir: path.resolve(config.runtime_owner?.state_dir ?? config.stateDir ?? config.cwd), config, record };
415
+ if (leases.has(key)) {
416
+ if (signal && leases.get(key).signal !== signal) throw Object.assign(new Error("Heartbeat cancellation owner differs from the retained lease"), { code: "process_maintenance_signal_conflict" });
417
+ return;
418
+ }
419
+ const lease = { pending: null, error: null, timer: null, lost: false, closing: false, signal, expiresAt: Date.parse(record.lease_expires_at ?? ""), renewal: createRenewalState({ leaseExpiresAt: record.lease_expires_at }), stateDir: path.resolve(config.runtime_owner?.state_dir ?? config.stateDir ?? config.cwd), config, record };
417
420
  leases.set(key, lease);
418
421
  // Retry no-effect contention inside the same authority episode while native
419
422
  // work is active; scheduling ticks are not ownership evidence.
420
423
  lease.timer = setInterval(() => reconfirmLease(lease, () => {}).catch(() => {}), 60_000);
421
424
  lease.timer.unref();
425
+ if (signal) {
426
+ signal.addEventListener("abort", () => clearInterval(lease.timer), { once: true });
427
+ if (signal.aborted) clearInterval(lease.timer);
428
+ }
422
429
  }
423
430
 
424
- export async function heartbeatDaemonProcess(config, record) {
431
+ export async function heartbeatDaemonProcess(config, record, { signal } = {}) {
425
432
  if (!record?.id || !record.lease_token) return;
426
- scheduleDaemonProcessHeartbeat(config, record);
433
+ scheduleDaemonProcessHeartbeat(config, record, { signal });
427
434
  const lease = leases.get(leaseKey(config, record));
428
435
  if (lease.closing) throw Object.assign(new Error("Managed process is closing"), { code: "process_closing" });
429
436
  if (lease.lost) throw lease.error;
@@ -435,7 +442,7 @@ export async function heartbeatDaemonProcess(config, record) {
435
442
  lease.pending = (async () => {
436
443
  try {
437
444
  lease.renewal.uncertainSince ??= performance.now();
438
- const result = await runtimeProcess(config, "heartbeat", { id: record.id, "lease-token": record.lease_token, "lease-ms": 300_000 }, { timeoutMs: renewalRemaining(lease.renewal) });
445
+ const result = await runtimeProcess(config, "heartbeat", { id: record.id, "lease-token": record.lease_token, "lease-ms": 300_000 }, { timeoutMs: renewalRemaining(lease.renewal), signal: lease.signal });
439
446
  if (result?.ok === false) {
440
447
  lease.lost = true; clearInterval(lease.timer);
441
448
  throw Object.assign(new Error("managed process lease is no longer owned; new productive operations are blocked"), { code: "process_lease_lost", details: { process_id: record.id } });
@@ -37,7 +37,7 @@ export async function writeNativeHookFailure(stateDir, binding, error, budget) {
37
37
  if (!binding?.request_id || !/^[A-Za-z0-9-]{1,128}$/.test(binding.request_id)) return;
38
38
  await budget.run(() => writeJsonAtomic(path.join(stateDir, "hook-failures", `${binding.request_id}.json`), {
39
39
  ...errorRecord(error), request_id: binding.request_id, observed_at: new Date().toISOString(), native_hook_binding: binding,
40
- }), "failure_receipt");
40
+ }, () => budget.remaining("failure_receipt_commit")), "failure_receipt");
41
41
  }
42
42
 
43
43
  /** Read only requests accepted for this exact live operation, never scan a mailbox. */
@@ -74,7 +74,7 @@ export function hookBudget(timeoutMs = 25_000, deadlineAt = process.env.DD_FLOW_
74
74
  return { deadlineAt: Math.floor(Date.now() + duration), remaining, async run(action, phase) {
75
75
  const timeoutMs = remaining(phase);
76
76
  let timer;
77
- try { return await Promise.race([Promise.resolve().then(action), new Promise((_, reject) => { timer = setTimeout(() => reject(Object.assign(new Error(`Native hook deadline exceeded during ${phase}`), { code: "native_hook_timeout", details: { phase, effect: "unknown" } })), timeoutMs); })]); }
77
+ try { return await Promise.race([Promise.resolve().then(() => { remaining(phase); return action(); }), new Promise((_, reject) => { timer = setTimeout(() => reject(Object.assign(new Error(`Native hook deadline exceeded during ${phase}`), { code: "native_hook_timeout", details: { phase, effect: "unknown" } })), timeoutMs); })]); }
78
78
  finally { clearTimeout(timer); }
79
79
  } };
80
80
  }
@@ -241,8 +241,8 @@ function hookDiagnostics(stderr) {
241
241
  }).slice(-16);
242
242
  }
243
243
 
244
- export async function invokeNativeHook({ bin, args, nodeBin, payload, home, cwd, env, timeoutMs = 15_000 }) {
245
- const output = await runBoundedCommand({ bin, args, nodeBin, input: `${JSON.stringify(payload)}\n`, cwd, env: { ...env, DD_FLOW_HOME: home }, timeoutMs });
244
+ export async function invokeNativeHook({ bin, args, nodeBin, payload, home, cwd, env, timeoutMs = 25_000 }) {
245
+ const output = await runBoundedCommand({ bin, args, nodeBin, input: `${JSON.stringify(payload)}\n`, cwd, env: { ...env, DD_FLOW_HOME: home }, timeoutMs, cleanupWithinBudget: true });
246
246
  let result;
247
247
  try { result = JSON.parse(output); } catch { throw Object.assign(new Error("Invalid native hook JSON response"), { code: "native_hook_response_invalid" }); }
248
248
  if (result?.ok === false && typeof result.error?.code === "string") throw Object.assign(new Error(result.error.message ?? "Native hook failed"), { code: result.error.code, details: result.error.details });
@@ -33,16 +33,16 @@ export function makeOpenCodePlugin(binding, invoke = invokeNativeHook, resolve =
33
33
  try {
34
34
  const result = await invoke({ bin: binding.ddFlowBin, nodeBin: binding.nodeBin,
35
35
  args: ["opencode", "event", "handle", "--project-root", binding.projectRoot, "--json"],
36
- payload: { ...payload, native_hook_binding: identity.nativeHookBinding }, home: binding.ddFlowHome, timeoutMs: budget.remaining("admission", 15000) });
36
+ payload: { ...payload, native_hook_binding: identity.nativeHookBinding }, home: binding.ddFlowHome, timeoutMs: budget.remaining("admission") });
37
37
  await budget.run(() => resolve(binding.stateDir, "hook.verify", { binding: identity.nativeHookBinding }, budget.remaining("generation")), "generation");
38
38
  if (result?.permissionDecision === "deny" || result?.hookSpecificOutput?.permissionDecision === "deny") throw Object.assign(new Error("dd-flow denied native shell admission"), { code: "native_hook_rejected" });
39
39
  return result;
40
40
  } catch (error) {
41
41
  error.details = { ...error.details, native_hook_binding: identity.nativeHookBinding, ...(payload.phase === "after" ? { effect: "unknown", tool_outcome: payload.outcome } : {}) };
42
- try { await writeNativeHookFailure(binding.stateDir, identity.nativeHookBinding, error, hookBudget(1_000, null)); await resolve(binding.stateDir, "hook.failed", {}, 1_000); }
42
+ try { await writeNativeHookFailure(binding.stateDir, identity.nativeHookBinding, error, budget); await resolve(binding.stateDir, "hook.failed", {}, budget.remaining("failure_notification")); }
43
43
  catch (storage) {
44
44
  error.details.receipt_error = { code: storage.code ?? "hook_receipt_write_failed", message: storage.message };
45
- try { await resolve(binding.stateDir, "hook.failed", { binding: identity.nativeHookBinding, failure: errorRecord(error) }, 1_000); }
45
+ try { await resolve(binding.stateDir, "hook.failed", { binding: identity.nativeHookBinding, failure: errorRecord(error) }, budget.remaining("failure_notification")); }
46
46
  catch (notify) { error.details.receipt_error.notification_error = notify.code ?? "hook_notification_failed"; }
47
47
  }
48
48
  throw error;
@@ -35,7 +35,7 @@ function boundedText(value, budget, limit = 16000) {
35
35
  // Lifecycle controls are not optional diagnostics. Reserve them before bulk
36
36
  // messages/details, including nested typed causes, without copying authority
37
37
  // that was truncated into a different command or continuation receipt.
38
- const operationalDetailKeys = ["phase", "effect", "recoverable", "retryable_no_effect", "handoff_required", "storage_retry", "repeated_without_progress", "cleanup_unconfirmed", "native_dispatch_started", "disposition", "cli_admission_barrier", "native_pre_cli_refusal", "issued_invocation_id", "invocation_id", "event_key", "refusal_event_key", "hook_event_id", "work_id", "hook_retry_signature", "rejection_signature", "retry_command", "repair_command", "continuation", "retry_instruction", "recovery_instruction", "authority_truncated"];
38
+ const operationalDetailKeys = ["phase", "effect", "recoverable", "retryable_no_effect", "handoff_required", "storage_retry", "repeated_without_progress", "cleanup_unconfirmed", "native_dispatch_started", "disposition", "cli_admission_barrier", "native_pre_cli_refusal", "issued_invocation_id", "invocation_id", "event_key", "refusal_event_key", "hook_event_id", "work_id", "hook_retry_signature", "rejection_signature", "retry_command", "repair_command", "continuation", "retry_instruction", "correction_instruction", "recovery_instruction", "authority_truncated"];
39
39
  function operationalDetails(value, budget) {
40
40
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
41
41
  const entries = [];
@@ -3,9 +3,11 @@ export const MAX_OVERLOAD_RETRIES: 2;
3
3
  export const MAX_RETRY_AFTER_MS: 30000;
4
4
  export function classifyProviderError(native: unknown): string;
5
5
  export function safeProviderError(native: unknown): Record<string, unknown>;
6
- export type CapacityRefusal = { policy: string; ordinal: number; session_id: string; turn_id: string; first_observed_at: string; native_terminal_at?: string; native_terminal_basis?: string };
6
+ export type RequestProof = { kind: "native_turn"; session_id: string; turn_id: string } | { kind: "stream_request"; daemon_instance: string; connection_id: string; operation_id: string; turn_generation: number; session_id: string; outbound_position: number; terminal_position: number };
7
+ export function streamRequestKey(proof: unknown): string | null;
8
+ export type CapacityRefusal = { policy: string; ordinal: number; session_id: string; turn_id: string; request_key?: string; request_proof?: RequestProof; first_observed_at: string; native_terminal_at?: string; native_terminal_basis?: string };
7
9
  export function parseCapacityOperation(id: string): { root: string; ordinal: number };
8
- export function normalizeProviderFailure(error: unknown, sessionId?: string, turnId?: string): null | { code: string; session_id: string; turn_id: string; native_error: Record<string, unknown>; observed_at: unknown; terminal_at: unknown; native_turn_items: unknown; primary_error: { details?: Record<string, unknown> } };
10
+ export function normalizeProviderFailure(error: unknown, sessionId?: string, turnId?: string): null | { code: string; session_id: string; turn_id: string; request_key: string; request_proof: RequestProof; native_error: Record<string, unknown>; observed_at: unknown; terminal_at: unknown; native_turn_items: unknown; primary_error: { details?: Record<string, unknown> } };
9
11
  export function terminalProviderOverload(error: unknown, sessionId?: string, turnId?: string): boolean;
10
12
  export function nativeItemsSettled(items: unknown): boolean;
11
13
  export function capacityBackoff(error: unknown, ordinal: number, now?: number, random?: () => number): number;
@@ -11,10 +11,10 @@ export function classifyProviderError(native) {
11
11
  const code = [native.code, native.type, native.codexErrorInfo?.code ?? native.codexErrorInfo].filter(value => typeof value === "string").join(" ");
12
12
  const message = String(native.message ?? "");
13
13
  const text = `${code} ${message}`;
14
- const status = Number(native.status ?? native.statusCode ?? native.status_code ?? native.http_status);
14
+ const status = Number(native.status ?? native.statusCode ?? native.status_code ?? native.http_status ?? /\(code (\d{3})\)/i.exec(message)?.[1]);
15
15
  if (status === 402 || /quota|usageLimitExceeded|sessionBudgetExceeded|billing|payment.required|balance.exhausted|insufficient.credit|resource_exhausted.*limit/i.test(text)) return "provider_quota_exhausted";
16
16
  if (status === 401 || status === 403 || /provider_auth_failed|invalid.api.key|authentication|unauthorized|eligib|forbidden|access.denied|rejected.this.account/i.test(text)) return "provider_auth_failed";
17
- if (/provider_capacity_transient|serverOverloaded|model.?capacity|at capacity|server.overload|model.overload|server.?busy/i.test(text) || /(?:^|\s)overloaded_error(?:\s|$)/i.test(code) || status === 503 || status === 529) return "provider_capacity_transient";
17
+ if (/provider_capacity_transient|serverOverloaded|model.?capacity|at capacity|no capacity available for model|server.overload|model.overload|server.?busy/i.test(text) || /(?:^|\s)overloaded_error(?:\s|$)/i.test(code) || status === 503 || status === 529) return "provider_capacity_transient";
18
18
  if (/rateLimitExceeded|rate.limit/i.test(text) && (native.retryAfter !== undefined || native.retry_after !== undefined || Object.keys(native.headers ?? {}).some(key => key.toLowerCase() === "retry-after"))) return "provider_rate_limited";
19
19
  return status === 429 || /provider_limit_unknown|\b429\b|rateLimitExceeded|rate.limit/i.test(text) ? "provider_limit_unknown" : "provider_failed";
20
20
  }
@@ -45,6 +45,15 @@ export function parseCapacityOperation(id) {
45
45
  return { root: match[1], ordinal };
46
46
  }
47
47
 
48
+ /** Local ordered-stream identity is not a native Turn ID. */
49
+ export function streamRequestKey(proof) {
50
+ if (proof?.kind !== "stream_request" || ![proof.daemon_instance, proof.connection_id, proof.operation_id, proof.session_id].every(value => typeof value === "string" && value)
51
+ || !Number.isSafeInteger(proof.turn_generation) || proof.turn_generation < 1
52
+ || !Number.isSafeInteger(proof.outbound_position) || proof.outbound_position < 1
53
+ || !Number.isSafeInteger(proof.terminal_position) || proof.terminal_position <= proof.outbound_position) return null;
54
+ return JSON.stringify(["stream_request", proof.daemon_instance, proof.connection_id, proof.operation_id, proof.session_id, proof.turn_generation, proof.outbound_position, proof.terminal_position]);
55
+ }
56
+
48
57
  export function normalizeProviderFailure(error, expectedSessionId, expectedTurnId) {
49
58
  let current = error;
50
59
  for (let depth = 0; depth < 4 && current && typeof current === "object"; depth++) {
@@ -52,14 +61,17 @@ export function normalizeProviderFailure(error, expectedSessionId, expectedTurnI
52
61
  // refusal. Never classify transport loss or an active native retry here.
53
62
  if (current.code === "provider_capacity_transient" || current.code === "provider_rate_limited") {
54
63
  const details = current.details ?? {};
64
+ const streamKey = details.native_source === "agy.result" && details.turn_id === undefined && details.native_turn_id === undefined ? streamRequestKey(details.request_proof) : null;
65
+ const turnKey = streamKey ?? details.turn_id;
55
66
  if (!["opencode.v2.session.execution", "agy.result", "acp.error_response", "droid.agent_turn_outcome", "grok.subagent.event"].includes(details.native_source) || details.terminal_status !== "failed"
56
67
  || details.will_retry === true || details.native_retry_active === true || details.provider_error?.willRetry === true
57
68
  || details.provider_error && !["provider_capacity_transient", "provider_rate_limited"].includes(classifyProviderError(details.provider_error))
58
69
  || typeof details.provider_session_id !== "string" || !details.provider_session_id
59
- || typeof details.turn_id !== "string" || !details.turn_id || details.native_turn_id !== details.turn_id
70
+ || typeof turnKey !== "string" || !turnKey || (streamKey ? details.request_proof.session_id !== details.provider_session_id || details.native_turn_id !== undefined : details.native_turn_id !== details.turn_id)
60
71
  || expectedSessionId !== undefined && details.provider_session_id !== expectedSessionId
61
- || expectedTurnId !== undefined && details.turn_id !== expectedTurnId) return null;
62
- return { code: "serverOverloaded", session_id: details.provider_session_id, turn_id: details.turn_id,
72
+ || expectedTurnId !== undefined && turnKey !== expectedTurnId) return null;
73
+ return { code: "serverOverloaded", session_id: details.provider_session_id, turn_id: turnKey, request_key: turnKey,
74
+ request_proof: streamKey ? details.request_proof : { kind: "native_turn", session_id: details.provider_session_id, turn_id: details.turn_id },
63
75
  native_error: details.provider_error ?? { type: details.native_error_type, status: details.http_status }, observed_at: details.observed_at ?? null,
64
76
  terminal_at: null, terminal_basis: null, native_turn_items: details.native_turn_items ?? null, primary_error: current };
65
77
  }
@@ -70,7 +82,7 @@ export function normalizeProviderFailure(error, expectedSessionId, expectedTurnI
70
82
  || expectedSessionId !== undefined && details.provider_session_id !== expectedSessionId
71
83
  || typeof details.turn_id !== "string" || !details.turn_id || details.native_turn_id !== details.turn_id
72
84
  || expectedTurnId !== undefined && details.turn_id !== expectedTurnId || typeof code !== "string") return null;
73
- return { code, session_id: details.provider_session_id, turn_id: details.turn_id, native_error: native,
85
+ return { code, session_id: details.provider_session_id, turn_id: details.turn_id, request_key: details.turn_id, request_proof: { kind: "native_turn", session_id: details.provider_session_id, turn_id: details.turn_id }, native_error: native,
74
86
  observed_at: details.observed_at ?? null, terminal_at: details.terminal_at ?? null, terminal_basis: details.terminal_basis ?? null,
75
87
  native_turn_items: details.native_turn_items ?? null, primary_error: current };
76
88
  }
@@ -118,6 +130,7 @@ export function refusalObservation(error, ordinal, firstObservedAt) {
118
130
  throw policyError("capacity_continuation_conflict", "Native refusal observation time is invalid or changed");
119
131
  const native = failure.terminal_at && Date.parse(failure.terminal_at);
120
132
  return { policy: CAPACITY_POLICY, ordinal, session_id: failure.session_id, turn_id: failure.turn_id,
133
+ request_key: failure.request_key, request_proof: failure.request_proof,
121
134
  first_observed_at: firstObservedAt, ...(Number.isFinite(native) && typeof failure.terminal_basis === "string" && failure.terminal_basis ? { native_terminal_at: failure.terminal_at, native_terminal_basis: failure.terminal_basis } : {}) };
122
135
  }
123
136
 
@@ -22,13 +22,15 @@ export function sha256(value) {
22
22
 
23
23
  export function hashJson(value) { return sha256(canonicalJson(value)); }
24
24
 
25
- export async function writeJsonAtomic(file, value) {
25
+ export async function writeJsonAtomic(file, value, beforeCommit = () => {}) {
26
+ beforeCommit();
26
27
  await mkdir(path.dirname(file), { recursive: true });
27
28
  const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
28
29
  try {
29
30
  const payload = await open(temporary, "wx", 0o600);
30
31
  try { await payload.writeFile(`${JSON.stringify(value, null, 2)}\n`); await payload.sync(); }
31
32
  finally { await payload.close(); }
33
+ beforeCommit();
32
34
  await rename(temporary, file);
33
35
  const directory = await open(path.dirname(file), "r");
34
36
  try { await directory.sync(); }
@@ -1,19 +1,21 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
3
4
  export const SETUP_STARTUP_MS = 120_000;
4
5
  export const SETUP_QUIET_MS = 30_000;
5
6
  export const setupPhases = new Set(['ingress_entered', 'imports_ready', 'probe_started', 'probe_completed', 'dependency_check_completed', 'transfer_progress', 'identity_digest_progress', 'identity_digest_completed', 'native_initialized', 'ready']);
6
7
  export function setupReporter(operationId = process.env.DD_EVAL_OPERATION_ID) {
7
8
  if (!operationId) return () => {};
8
- let sequence = 0;
9
+ let sequence = 0; const producerId = randomUUID();
9
10
  return (phase, step = null) => {
10
11
  if (!setupPhases.has(phase)) throw new Error('Invalid setup phase');
11
- process.stderr.write(JSON.stringify({ kind: 'setup_phase', operation_id: operationId, phase, step, sequence: ++sequence, observed_at: new Date().toISOString() }) + '\n');
12
+ process.stderr.write(JSON.stringify({ kind: 'setup_phase', operation_id: operationId, producer_id: producerId, phase, step, sequence: ++sequence, observed_at: new Date().toISOString() }) + '\n');
12
13
  };
13
14
  }
14
15
  export const publishSetup = setupReporter();
15
16
  export function isSetupPhase(value) {
16
17
  return value?.kind === 'setup_phase' && typeof value.operation_id === 'string' && value.operation_id.length > 0 && setupPhases.has(value.phase)
18
+ && (!Object.hasOwn(value, 'producer_id') || typeof value.producer_id === 'string' && value.producer_id.length > 0 && value.producer_id.length <= 128)
17
19
  && Number.isSafeInteger(value.sequence) && value.sequence > 0 && value.sequence <= 1_000_000
18
20
  && (value.step === null || typeof value.step === 'string' && value.step.length <= 4096 || Number.isSafeInteger(value.step) && value.step >= 0)
19
21
  && typeof value.observed_at === 'string' && Number.isFinite(Date.parse(value.observed_at))
@@ -36,13 +38,13 @@ export function startupObserver(stateDir, daemonId) {
36
38
  };
37
39
  }
38
40
  export function setupObserver(operationId) {
39
- const startedAt = Date.now(); let sequence = 0; const markers = new Set();
41
+ const startedAt = Date.now(); const sequences = new Map(), markers = new Set();
40
42
  return value => {
41
43
  if (typeof operationId !== 'string' || markers.size >= 4096 || !isSetupPhase(value) || value.operation_id !== operationId
42
- || value.sequence <= sequence || Date.parse(value.observed_at) < startedAt || Date.parse(value.observed_at) > Date.now()) return false;
43
- const marker = JSON.stringify([value.phase, value.step]);
44
+ || value.sequence <= (sequences.get(value.producer_id ?? null) ?? 0) || Date.parse(value.observed_at) < startedAt || Date.parse(value.observed_at) > Date.now()) return false;
45
+ const marker = JSON.stringify([value.producer_id ?? null, value.phase, value.step]);
44
46
  if (markers.has(marker)) return false;
45
- markers.add(marker); sequence = value.sequence; return true;
47
+ markers.add(marker); sequences.set(value.producer_id ?? null, value.sequence); return true;
46
48
  };
47
49
  }
48
50
  export function observationDetails(clock, phase, pid) {