@deksden-com/dd-flow-cli 0.9.0-beta.152 → 0.9.0-beta.156

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 (227) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/build-info.json +8 -8
  3. package/dist/cli/command-inputs.js +33 -3
  4. package/dist/cli/command-metadata.js +73 -2
  5. package/dist/cli/delivery.js +108 -0
  6. package/dist/cli/diagnostic-invocation.js +23 -7
  7. package/dist/cli/diagnostic.js +14 -3
  8. package/dist/cli/help.js +117 -1
  9. package/dist/cli/hook-ingress.js +36 -49
  10. package/dist/cli/interactive-invocation.js +33 -0
  11. package/dist/cli/interactive.js +114 -0
  12. package/dist/cli/memory-fix-invocation.js +52 -0
  13. package/dist/cli/memory-fix.js +100 -0
  14. package/dist/cli/memory-flow-admission.js +7 -3
  15. package/dist/cli/memory-init.js +5 -4
  16. package/dist/cli/native-hook-ingress.js +5 -1
  17. package/dist/cli/native-hook-preparation.js +31 -4
  18. package/dist/cli/run-cli.js +274 -19
  19. package/dist/cli.js +17 -3
  20. package/dist/domain/delivery-flows.js +8 -0
  21. package/dist/domain/distill-flow.js +20 -0
  22. package/dist/domain/interactive-flow.js +1 -0
  23. package/dist/domain/memory-fix-flow.js +2 -0
  24. package/dist/domain/review-fix-flow.js +2 -0
  25. package/dist/domain/source-snapshot-flows.js +4 -0
  26. package/dist/harness-runtime/bin/dd-agy.mjs +18 -3
  27. package/dist/harness-runtime/bin/dd-grok.mjs +15 -5
  28. package/dist/harness-runtime/bin/dd-zcode.mjs +15 -7
  29. package/dist/harness-runtime/lib/agy-error-boundary.mjs +18 -10
  30. package/dist/harness-runtime/lib/agy-native-inspection.mjs +12 -3
  31. package/dist/harness-runtime/lib/agy-native-inventory.mjs +19 -0
  32. package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +13 -6
  33. package/dist/harness-runtime/lib/agy-transcript-stream.mjs +54 -0
  34. package/dist/harness-runtime/lib/daemon-observation.d.mts +1 -0
  35. package/dist/harness-runtime/lib/daemon-observation.mjs +12 -4
  36. package/dist/harness-runtime/lib/daemon-operations.mjs +20 -5
  37. package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +1 -0
  38. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +371 -114
  39. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +43 -3
  40. package/dist/harness-runtime/lib/dd-codex.mjs +36 -4
  41. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +25 -11
  42. package/dist/harness-runtime/lib/dd-droid.mjs +124 -11
  43. package/dist/harness-runtime/lib/dd-grok-daemon.d.mts +1 -0
  44. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +137 -25
  45. package/dist/harness-runtime/lib/dd-grok.mjs +4 -2
  46. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +115 -17
  47. package/dist/harness-runtime/lib/dd-opencode.mjs +10 -7
  48. package/dist/harness-runtime/lib/dd-zcode-daemon.d.mts +1 -0
  49. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +115 -22
  50. package/dist/harness-runtime/lib/dd-zcode.mjs +75 -36
  51. package/dist/harness-runtime/lib/delegation-instructions.d.mts +2 -1
  52. package/dist/harness-runtime/lib/delegation-instructions.mjs +15 -5
  53. package/dist/harness-runtime/lib/delivery-command.mjs +84 -0
  54. package/dist/harness-runtime/lib/driver-recovery.mjs +0 -19
  55. package/dist/harness-runtime/lib/droid-observation.mjs +9 -5
  56. package/dist/harness-runtime/lib/lifecycle-contract.d.mts +1 -0
  57. package/dist/harness-runtime/lib/lifecycle-contract.mjs +2 -1
  58. package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -2
  59. package/dist/harness-runtime/lib/native-hook-command.mjs +63 -8
  60. package/dist/harness-runtime/lib/native-streams.mjs +3 -10
  61. package/dist/harness-runtime/lib/opencode-plugin.mjs +36 -9
  62. package/dist/harness-runtime/lib/operation-errors.mjs +13 -0
  63. package/dist/harness-runtime/lib/operation-progress.mjs +6 -2
  64. package/dist/harness-runtime/lib/process-json.mjs +4 -5
  65. package/dist/harness-runtime/lib/tool-observations.mjs +20 -10
  66. package/dist/harness-runtime/lib/zcode-native-hook-ownership.mjs +109 -0
  67. package/dist/schemas/code-work-result.schema.json +2 -1
  68. package/dist/schemas/delivery-stage-result.schema.json +286 -0
  69. package/dist/schemas/distill-stage-result.schema.json +667 -0
  70. package/dist/schemas/distill-work-result.schema.json +428 -0
  71. package/dist/schemas/execution-bundle.schema.json +6 -2
  72. package/dist/schemas/interactive-stage-result.schema.json +47 -0
  73. package/dist/schemas/interactive-work-result.schema.json +63 -0
  74. package/dist/schemas/mb-fix-stage-result.schema.json +93 -0
  75. package/dist/schemas/project-flow-pack-manifest.schema.json +2 -2
  76. package/dist/schemas/review-fix-stage-result.schema.json +210 -0
  77. package/dist/schemas/run-control-receipt.schema.json +1 -0
  78. package/dist/services/assigned-lifecycle.js +382 -0
  79. package/dist/services/check-runner-evidence.js +45 -8
  80. package/dist/services/cli-operation-classifier.js +6 -1
  81. package/dist/services/code-checks.js +46 -29
  82. package/dist/services/codex-hook-delivery.js +11 -2
  83. package/dist/services/controller-child-repair.js +95 -0
  84. package/dist/services/controller-execution.js +4 -1
  85. package/dist/services/controller-fanout.js +19 -9
  86. package/dist/services/controller-memory-worker.js +6 -4
  87. package/dist/services/controller-result-authority.js +122 -0
  88. package/dist/services/dashboard.js +4 -2
  89. package/dist/services/delivery-access.js +123 -0
  90. package/dist/services/delivery-approval.js +68 -0
  91. package/dist/services/delivery-artifact.js +86 -0
  92. package/dist/services/delivery-checks.js +178 -0
  93. package/dist/services/delivery-execution.js +181 -0
  94. package/dist/services/delivery-intents.js +212 -0
  95. package/dist/services/delivery-operation.js +81 -0
  96. package/dist/services/delivery-policy.js +91 -0
  97. package/dist/services/delivery-procedures.js +43 -0
  98. package/dist/services/delivery-request.js +99 -0
  99. package/dist/services/delivery-result.js +144 -0
  100. package/dist/services/delivery-stage-commands.js +26 -0
  101. package/dist/services/delivery-stage-entry.js +56 -0
  102. package/dist/services/delivery-stage-handlers.js +135 -0
  103. package/dist/services/delivery-stage-results.js +94 -0
  104. package/dist/services/descriptor-coordinator-session.js +3 -1
  105. package/dist/services/descriptor-run-start.js +24 -4
  106. package/dist/services/descriptor-stage-coordinator.js +78 -10
  107. package/dist/services/descriptor-stage-settlement.js +59 -14
  108. package/dist/services/diagnostic-repair-intake.js +123 -0
  109. package/dist/services/diagnostic-repair-source-intake.js +102 -0
  110. package/dist/services/diagnostic-report.js +30 -0
  111. package/dist/services/diagnostic-run-bundle.js +48 -9
  112. package/dist/services/diagnostic-snapshot.js +2 -2
  113. package/dist/services/diagnostic-stage-handlers.js +3 -2
  114. package/dist/services/diagnostic-stage-results.js +26 -0
  115. package/dist/services/diagnostic-start.js +56 -6
  116. package/dist/services/diagnostic-work-collection.js +3 -3
  117. package/dist/services/distill-document-publication.js +280 -0
  118. package/dist/services/distill-promotion.js +197 -0
  119. package/dist/services/distill-publication-custody.js +162 -0
  120. package/dist/services/distill-stage-handlers.js +249 -0
  121. package/dist/services/distill-stage-results.js +105 -0
  122. package/dist/services/distill-user-inputs.js +4 -0
  123. package/dist/services/distill-work-batch.js +42 -0
  124. package/dist/services/distill-work-collection.js +74 -0
  125. package/dist/services/distill-work-registration.js +93 -0
  126. package/dist/services/distill-work-results.js +98 -0
  127. package/dist/services/eval-snapshots.js +17 -7
  128. package/dist/services/external-work-launch.js +8 -2
  129. package/dist/services/flow-catalog.js +24 -0
  130. package/dist/services/frozen-engine.js +19 -2
  131. package/dist/services/generic-execution.js +4 -3
  132. package/dist/services/harness-adapter.js +2 -14
  133. package/dist/services/hook-memory-routing.js +1 -1
  134. package/dist/services/hooks.js +31 -6
  135. package/dist/services/hybrid-contracts.js +6 -1
  136. package/dist/services/installed-flow-source.js +37 -2
  137. package/dist/services/interactive-attached-session.js +215 -0
  138. package/dist/services/interactive-baseline-custody.js +54 -0
  139. package/dist/services/interactive-before-versions.js +148 -0
  140. package/dist/services/interactive-bundle-data.js +85 -0
  141. package/dist/services/interactive-checks.js +179 -0
  142. package/dist/services/interactive-execution.js +33 -0
  143. package/dist/services/interactive-finish-integration.js +236 -0
  144. package/dist/services/interactive-git-baseline.js +230 -0
  145. package/dist/services/interactive-git-dispatch.js +278 -0
  146. package/dist/services/interactive-input-delivery.js +82 -0
  147. package/dist/services/interactive-mutation-scope.js +231 -0
  148. package/dist/services/interactive-request.js +287 -0
  149. package/dist/services/interactive-run-bundle.js +109 -0
  150. package/dist/services/interactive-stage-entry.js +41 -0
  151. package/dist/services/interactive-stage-handlers.js +289 -0
  152. package/dist/services/interactive-stage-observation.js +136 -0
  153. package/dist/services/interactive-stage-results.js +111 -0
  154. package/dist/services/interactive-user-inputs.js +142 -0
  155. package/dist/services/interactive-work-batch.js +43 -0
  156. package/dist/services/interactive-work-collection.js +119 -0
  157. package/dist/services/interactive-work-completion.js +180 -0
  158. package/dist/services/interactive-work-context.js +249 -0
  159. package/dist/services/interactive-work-history.js +112 -0
  160. package/dist/services/interactive-work-registration.js +124 -0
  161. package/dist/services/interactive-work-results.js +115 -0
  162. package/dist/services/interactive-work-start.js +155 -0
  163. package/dist/services/lifecycle-invocations.js +250 -33
  164. package/dist/services/managed-daemon-binding.js +52 -1
  165. package/dist/services/memory-compose-handler.js +2 -22
  166. package/dist/services/memory-finish-integration.js +58 -17
  167. package/dist/services/memory-fix-finish-integration.js +129 -0
  168. package/dist/services/memory-fix-git-dispatch.js +139 -0
  169. package/dist/services/memory-fix-intake-custody.js +11 -0
  170. package/dist/services/memory-fix-intake-settlement.js +127 -0
  171. package/dist/services/memory-fix-publication.js +196 -0
  172. package/dist/services/memory-fix-repair-admission.js +116 -0
  173. package/dist/services/memory-fix-request.js +141 -0
  174. package/dist/services/memory-fix-run-bundle.js +83 -0
  175. package/dist/services/memory-fix-scope.js +78 -0
  176. package/dist/services/memory-fix-sources.js +85 -0
  177. package/dist/services/memory-fix-stage-entry.js +24 -0
  178. package/dist/services/memory-fix-stage-handlers.js +282 -0
  179. package/dist/services/memory-fix-stage-results.js +106 -0
  180. package/dist/services/memory-fix-workspace.js +127 -0
  181. package/dist/services/memory-flow-user-inputs.js +88 -0
  182. package/dist/services/memory-init-contexts.js +55 -5
  183. package/dist/services/memory-init-controller-start.js +20 -4
  184. package/dist/services/memory-layer.js +7 -3
  185. package/dist/services/memory-native-review-copy.js +27 -3
  186. package/dist/services/memory-repair-document-delta.js +1 -0
  187. package/dist/services/project-flow-source.js +195 -0
  188. package/dist/services/project-git-integration.js +92 -0
  189. package/dist/services/repair-intents.js +2 -7
  190. package/dist/services/retained-native-hook.js +274 -0
  191. package/dist/services/review-fix-handoff.js +82 -0
  192. package/dist/services/review-fix-intake-custody.js +17 -0
  193. package/dist/services/review-fix-stage-entry.js +43 -0
  194. package/dist/services/review-fix-stage-handlers.js +176 -0
  195. package/dist/services/review-fix-stage-results.js +86 -0
  196. package/dist/services/run-control-worker.js +42 -1
  197. package/dist/services/run-control.js +40 -5
  198. package/dist/services/run-controller-adapter.js +37 -3
  199. package/dist/services/run-controller-process.js +25 -4
  200. package/dist/services/run-controller-recovery.js +46 -6
  201. package/dist/services/run-controller-state.js +3 -2
  202. package/dist/services/run-controller.js +257 -44
  203. package/dist/services/run-recovery.js +31 -2
  204. package/dist/services/run-timeline.js +59 -0
  205. package/dist/services/runs.js +18 -31
  206. package/dist/services/sessions.js +2 -1
  207. package/dist/services/stage-handlers.js +55 -3
  208. package/dist/services/stage-lifecycle.js +4 -3
  209. package/dist/services/stage-pause.js +26 -9
  210. package/dist/services/stage-report-renderer.js +2 -6
  211. package/dist/services/technical-notes.js +31 -0
  212. package/dist/services/verification-matrix.js +1 -1
  213. package/dist/services/version-status.js +1 -1
  214. package/dist/services/vnext-code-review.js +74 -5
  215. package/dist/services/vnext-code.js +8 -32
  216. package/dist/services/vnext-fanout.js +53 -8
  217. package/dist/services/vnext-merge.js +10 -5
  218. package/dist/services/vnext-plan-review.js +1 -1
  219. package/dist/services/vnext-plan.js +1 -1
  220. package/dist/services/vnext-protocolize.js +1 -1
  221. package/dist/services/vnext-specify.js +10 -5
  222. package/dist/services/work-registry.js +255 -69
  223. package/dist/services/worktrees.js +7 -4
  224. package/dist/storage/database.js +2 -1
  225. package/dist/verification-components.json +82 -0
  226. package/package.json +3 -2
  227. package/tools/verification-jobs.mjs +35 -10
package/dist/cli.js CHANGED
@@ -53,12 +53,26 @@ if (args[0] === "upgrade" || args[0] === "flow" && args[1] === "mb-upgrade") {
53
53
  const { runMemoryUpgradeCli } = await import("./cli/memory-upgrade.js");
54
54
  process.exit(await runMemoryUpgradeCli(args, process.env));
55
55
  }
56
- if (args[0] === "review" || args[0] === "flow" && ["mb-audit", "project-review"].includes(args[1] ?? "")) {
56
+ if (args[0] === "review" || args[0] === "flow" && ["mb-audit", "project-review", "mb-distill", "review-fix"].includes(args[1] ?? "")) {
57
57
  publishSetup('ingress_entered');
58
58
  const { runDiagnosticCli } = await import("./cli/diagnostic.js");
59
59
  publishSetup('imports_ready');
60
60
  process.exit(await runDiagnosticCli(args, process.env));
61
61
  }
62
+ if (args[0] === "flow" && args[1] === "mb-fix") {
63
+ publishSetup('ingress_entered');
64
+ const { runMemoryFixCli } = await import("./cli/memory-fix.js");
65
+ publishSetup('imports_ready');
66
+ process.exit(await runMemoryFixCli(args, process.env));
67
+ }
68
+ if (args[0] === "flow" && args[1] === "interactive") {
69
+ const { runInteractiveCli } = await import("./cli/interactive.js");
70
+ process.exit(await runInteractiveCli(args, process.env));
71
+ }
72
+ if (args[0] === "flow" && ["release", "deploy", "publish"].includes(args[1] ?? "")) {
73
+ const { runDeliveryCli } = await import("./cli/delivery.js");
74
+ process.exit(await runDeliveryCli(args, process.env));
75
+ }
62
76
  if (args.slice(0, 3).join(" ") === "runtime storage admit") {
63
77
  const { runMemoryStorageAdmissionCli } = await import("./cli/memory-storage-admission.js");
64
78
  process.exit(runMemoryStorageAdmissionCli(args));
@@ -75,10 +89,10 @@ if (args.slice(0, 3).join(" ") === "runtime flow admit") {
75
89
  const { runMemoryFlowAdmissionCli } = await import("./cli/memory-flow-admission.js");
76
90
  process.exit(await runMemoryFlowAdmissionCli(args));
77
91
  }
78
- const managedHook = Boolean(process.env.DD_FLOW_CODEX_STATE_DIR) && args.slice(0, 3).join(" ") === "codex hook handle";
92
+ const codexHook = args.slice(0, 3).join(" ") === "codex hook handle";
79
93
  const exitCode = isRuntimeMaintenanceIngress(args, process.env)
80
94
  ? await runRuntimeMaintenanceIngress(args)
81
- : managedHook
95
+ : codexHook
82
96
  ? await (await import("./cli/hook-ingress.js")).runHookIngress(args)
83
97
  : isNativeHookIngress(args)
84
98
  ? await (await import("./cli/native-hook-ingress.js")).runNativeHookIngress(args)
@@ -0,0 +1,8 @@
1
+ export const deliveryFlowStages = {
2
+ release: ["preflight", "version-decision", "prepare", "verify", "publish", "finish"],
3
+ deploy: ["preflight", "target-decision", "prepare", "deploy", "verify", "finish"],
4
+ publish: ["preflight", "target-decision", "prepare", "verify", "publish", "finish"]
5
+ };
6
+ export function isDeliveryFlow(value) {
7
+ return typeof value === "string" && Object.hasOwn(deliveryFlowStages, value);
8
+ }
@@ -0,0 +1,20 @@
1
+ /** Distillation's semantic catalog, not executable handler registration. */
2
+ export const distillStages = ["preflight", "extract", "review", "promote", "verify", "finish"];
3
+ const domain = [
4
+ "structure-navigation", "delivery-protocols", "deferrals-closure", "scenarios-evidence", "ui-guides",
5
+ "system-adr-contracts", "engineering-code-standards", "operations-release", "agent-workflow-prompts", "templates-metadata"
6
+ ];
7
+ const process = [
8
+ "user-intent-intake", "project-grounding", "flow-routing", "planning-decomposition", "execution-orchestration",
9
+ "implementation-context-coding", "debugging-failure-recovery", "review-verification-testing", "evidence-acceptance-closure",
10
+ "delivery-release-operations", "memory-documentation-lifecycle", "memory-freshness-self-improvement"
11
+ ];
12
+ export const distillAspects = Object.freeze([
13
+ ...domain.map((name, index) => aspect("domain", name, index)),
14
+ ...process.map((name, index) => aspect("process", name, index))
15
+ ]);
16
+ function aspect(lens, name, index) {
17
+ const number = String(index + 1).padStart(2, "0");
18
+ return Object.freeze({ id: `${lens}-${number}`, lens, name,
19
+ criterion: `support/dd-flow/mb-distill/${lens === "domain" ? "aspects" : "process-aspects"}/${number}-${name}.md` });
20
+ }
@@ -0,0 +1 @@
1
+ export const interactiveStages = ["preflight", "interactive", "consolidation", "hardening", "review", "integration"];
@@ -0,0 +1,2 @@
1
+ /** Semantic contract only; executable admission belongs to registered handlers. */
2
+ export const memoryFixStages = ["intake", "repair", "verify", "review", "finish"];
@@ -0,0 +1,2 @@
1
+ /** Scope intake only. Handoff never allocates or authorizes an SDLC RUN. */
2
+ export const reviewFixStages = ["intake", "scope-review", "handoff"];
@@ -0,0 +1,4 @@
1
+ import { isDiagnosticFlow } from "./diagnostic-flows.js";
2
+ export function isSourceSnapshotFlow(id) {
3
+ return id === "mb-distill" || id === "review-fix" || isDiagnosticFlow(id);
4
+ }
@@ -17,22 +17,37 @@ async function hook(options) {
17
17
  try {
18
18
  const observationMs = budget.remaining("observation");
19
19
  const identity = await callDaemon(options["state-dir"], "hook.observe", { event, payload, daemonId: options["daemon-id"], deadlineAt: budget.deadlineAt }, observationMs, requestId);
20
+ if (identity.fastAck === true) {
21
+ const binding = identity.nativeHookBinding, args = payload.toolCall?.args ?? {};
22
+ if (event !== "PreToolUse" || binding?.native_admission !== "assigned_work_start" || binding.source_harness !== "antigravity-cli"
23
+ || binding.daemon_id !== options["daemon-id"] || binding.provider_session_id !== payload.conversationId
24
+ || binding.native_command !== (args.command ?? args.cmd ?? args.CommandLine)) throw Object.assign(new Error("Deferred native callback binding differs"), { code: "native_hook_unproven" });
25
+ return { decision: "allow" };
26
+ }
20
27
  if (identity.stale) return event === "PreToolUse" ? { decision: "deny", reason: "agy_stale_hook" } : {};
21
- if (event === "PreToolUse" || event === "PostToolUse") {
28
+ if (!identity.observationOnly && (event === "PreToolUse" || event === "PostToolUse")) {
22
29
  const phase = event === "PreToolUse" ? "before" : "after", args = payload.toolCall?.args ?? {};
23
30
  const normalized = { schema_id: "dd-flow/agy-tool-event@1", event_id: identity.event_id, phase, daemon_id: identity.daemon_id, launch_cwd: identity.launch_cwd, conversation_id: payload.conversationId, parent_conversation_id: identity.parent_conversation_id, execution_num: payload.executionNum, step_index: payload.stepIdx, tool: payload.toolCall?.name, input: args, workspace_paths: payload.workspacePaths, transcript_path: payload.transcriptPath, model: payload.modelName, profile: identity.profile, ...(phase === "after" ? { error: payload.error ?? "" } : {}) };
24
31
  await invokeNativeHook({ bin: options["dd-flow-bin"] ?? identity.dd_flow_bin,
25
32
  args: ["agy", "event", "handle", "--project-root", options["project-root"] ?? identity.project_root, "--json"],
26
33
  payload: normalized, home: options["dd-flow-home"] ?? identity.dd_flow_home, timeoutMs: budget.remaining("admission") });
27
34
  }
35
+ // A successful callback can retire its exact observation request. This is
36
+ // best-effort housekeeping: lost IPC neither denies the native tool nor
37
+ // invents an execution result; the daemon retains the request if uncertain.
38
+ try { await callDaemon(options["state-dir"], "hook.complete", { daemonId: options["daemon-id"], requestId }, Math.min(500, budget.remaining("retirement"))); }
39
+ catch { /* The request remains available for a late failure receipt. */ }
28
40
  return event === "PreToolUse" ? { decision: "allow" } : {};
29
41
  } catch (error) {
30
42
  // A failed socket cannot be used to report its own failure. The daemon
31
43
  // reads only receipts matching request IDs it actually accepted this turn.
32
44
  const correctable = event === "PreToolUse" && error.code === "lifecycle_shell_syntax_invalid" && error.details?.effect === "no_effect"
33
45
  && error.details?.recoverable === true && error.details?.disposition === "pre_cli_shell_refusal";
34
- try { await budget.run(() => writeJsonAtomic(path.join(options["state-dir"], "hook-failures", `${requestId}.json`), { request_id: requestId, observed_at: new Date().toISOString(), code: error.code ?? "agy_hook_rejected", message: error.message,
35
- details: { ...error.details, effect: correctable ? "no_effect" : "unknown", ...(correctable ? { native_pre_cli_refusal: true } : {}) } }, () => budget.remaining("failure_receipt_commit")), "failure_receipt"); }
46
+ // The operation deadline may already be exhausted. It cannot be reused to
47
+ // persist the failure caused by that deadline: the daemon consumes only a
48
+ // receipt for a request it actually registered for its current turn.
49
+ try { await writeJsonAtomic(path.join(options["state-dir"], "hook-failures", `${requestId}.json`), { request_id: requestId, observed_at: new Date().toISOString(), code: error.code ?? "agy_hook_rejected", message: error.message,
50
+ details: { ...error.details, effect: correctable ? "no_effect" : "unknown", ...(correctable ? { native_pre_cli_refusal: true } : {}) } }); }
36
51
  catch (storage) { process.stderr.write(`${JSON.stringify({ code: error.code, receipt_error: storage.message })}\n`); }
37
52
  throw error;
38
53
  }
@@ -10,15 +10,25 @@ function parse(argv) { const positional = []; const options = {}; for (let index
10
10
  function common(options, operation) { return { bin: options["grok-bin"], journal: options.journal, stateDir: options["state-dir"], cwd: options.cwd, sessionId: options["session-id"], model: options.model, reasoning: options.reasoning, mode: options.mode ?? "bypassPermissions", authPath: options["auth-path"], authMethodId: options["auth-method-id"], ddFlowBin: options["dd-flow-bin"], ddFlowHome: options["dd-flow-home"], projectRoot: options["project-root"], sessionArchive: options["session-archive"], noFlow: options["no-flow"] === true, ...adapterTimeoutOptions(options, operation) }; }
11
11
  async function hook(options) {
12
12
  const budget = hookBudget();
13
- const stdin = await readNativeHookInput(process.stdin, budget); const payload = JSON.parse(stdin || "{}"); const identity = await callDaemon(options["state-dir"], "hook.resolve", { sessionId: payload.sessionId ?? payload.session_id, payload }, budget.remaining("identity"));
13
+ const stdin = await readNativeHookInput(process.stdin, budget); const payload = JSON.parse(stdin || "{}"); const identity = await callDaemon(options["state-dir"], "hook.resolve", { sessionId: payload.sessionId ?? payload.session_id, payload, hookDeadlineAt: budget.deadlineAt }, budget.remaining("identity"));
14
+ const binding = identity.nativeHookBinding, input = payload.tool_input ?? payload.toolInput;
15
+ // Only the daemon's exact, retained child delivery can use this short ACK;
16
+ // the issued CLI command still verifies its assignment and materializes it.
17
+ if (identity.fastAck === true && binding?.source_harness === "grok-acp" && binding.request_id
18
+ && binding.provider_session_id === (payload.sessionId ?? payload.session_id)
19
+ && binding.provider_session_id !== binding.root_provider_session_id
20
+ && binding.native_command === (input?.command ?? input?.cmd)) return { decision: "continue" };
14
21
  const enriched = { ...payload, _meta: { ...(payload._meta ?? {}), ddGrok: identity } }; const args = ["grok", "event", "handle", "--project-root", options["project-root"], "--json"]; const command = options["dd-flow-bin"];
15
22
  try { return await invokeNativeHook({ bin: command, args, home: options["dd-flow-home"], payload: enriched, timeoutMs: budget.remaining("admission") }); }
16
23
  catch (error) {
17
24
  if (identity.nativeHookBinding) error.details = { ...error.details, native_hook_binding: identity.nativeHookBinding };
18
- try {
19
- await writeNativeHookFailure(options["state-dir"], identity.nativeHookBinding, error, budget);
20
- if (identity.nativeHookBinding) await callDaemon(options["state-dir"], "hook.failed", { requestId: identity.nativeHookBinding.request_id }, budget.remaining("failure_notification"));
21
- } catch (storage) { process.stderr.write(`${JSON.stringify({ code: error.code, receipt_error: storage.message })}\n`); }
25
+ let storageError;
26
+ try { await writeNativeHookFailure(options["state-dir"], identity.nativeHookBinding, error); }
27
+ catch (storage) { storageError = storage; process.stderr.write(`${JSON.stringify({ code: error.code, receipt_error: storage.message })}\n`); }
28
+ if (identity.nativeHookBinding && storageError?.code !== "native_hook_identity_conflict") try {
29
+ await callDaemon(options["state-dir"], "hook.failed", { requestId: identity.nativeHookBinding.request_id,
30
+ ...(storageError ? { binding: identity.nativeHookBinding, failure: errorRecord(error) } : {}) }, storageError ? 5_000 : budget.remaining("failure_notification"));
31
+ } catch { /* The durable receipt is polled by the owner or retained for reconciliation. */ }
22
32
  throw error;
23
33
  }
24
34
  }
@@ -34,18 +34,26 @@ function common(options, operation) {
34
34
  async function hook(options) {
35
35
  const budget = hookBudget();
36
36
  const payload = JSON.parse((await readNativeHookInput(process.stdin, budget)) || "{}");
37
- const identity = await callDaemon(options["state-dir"], "hook.resolve", { sessionId: payload.session_id ?? payload.sessionId, payload }, budget.remaining("identity"));
37
+ const identity = await callDaemon(options["state-dir"], "hook.resolve", { sessionId: payload.session_id ?? payload.sessionId, payload, hookDeadlineAt: budget.deadlineAt }, budget.remaining("identity"));
38
+ const binding = identity.nativeHookBinding, input = payload.tool_input ?? payload.toolInput;
39
+ // This only acknowledges an already retained child delivery. The actual
40
+ // Work CLI still materializes and validates its issued invocation via CAS.
41
+ if (identity.fastAck === true && binding?.source_harness === "zcode-acp" && binding.request_id
42
+ && binding.provider_session_id === (payload.session_id ?? payload.sessionId)
43
+ && binding.provider_session_id !== binding.root_provider_session_id
44
+ && binding.native_command === (input?.command ?? input?.cmd)) return {};
38
45
  const command = options["dd-flow-bin"]; const args = ["zcode", "hook", "handle", "--project-root", options["project-root"], "--json"];
39
46
 
40
47
  try { return await invokeNativeHook({ bin: command, args, home: options["dd-flow-home"], timeoutMs: budget.remaining("admission"), payload: { ...payload, _meta: { ...(payload._meta ?? {}), ddZcode: identity } } }); }
41
48
  catch (error) {
42
49
  if (identity.nativeHookBinding) error.details = { ...error.details, native_hook_binding: identity.nativeHookBinding };
43
- try {
44
- // Evidence shares the original deadline; an expired admission cannot
45
- // start a new independent failure-report window.
46
- await writeNativeHookFailure(options["state-dir"], identity.nativeHookBinding, error, budget);
47
- if (identity.nativeHookBinding) await callDaemon(options["state-dir"], "hook.failed", { requestId: identity.nativeHookBinding.request_id }, budget.remaining("failure_notification"));
48
- } catch (storage) { process.stderr.write(`${JSON.stringify({ code: error.code, receipt_error: storage.message })}\n`); }
50
+ let storageError;
51
+ try { await writeNativeHookFailure(options["state-dir"], identity.nativeHookBinding, error); }
52
+ catch (storage) { storageError = storage; process.stderr.write(`${JSON.stringify({ code: error.code, receipt_error: storage.message })}\n`); }
53
+ if (identity.nativeHookBinding && storageError?.code !== "native_hook_identity_conflict") try {
54
+ await callDaemon(options["state-dir"], "hook.failed", { requestId: identity.nativeHookBinding.request_id,
55
+ ...(storageError ? { binding: identity.nativeHookBinding, failure: errorRecord(error) } : {}) }, storageError ? 5_000 : budget.remaining("failure_notification"));
56
+ } catch { /* The durable receipt is polled by the owner or retained for reconciliation. */ }
49
57
  throw error;
50
58
  }
51
59
  }
@@ -6,14 +6,22 @@ import { agyTerminalEnvelope, ownedAgyOutput } from "./agy-terminal-envelope.mjs
6
6
  export function recoveredAgyErrorBoundary(text, error, floor = -1, source = null, sessionId = null) {
7
7
  let rows;
8
8
  try { rows = text.trim().split("\n").filter(Boolean).map(JSON.parse); } catch { return null; }
9
+ const reducer = createAgyErrorBoundary(error, floor, source, sessionId);
10
+ for (const row of rows) reducer.add(row);
11
+ return reducer.finish();
12
+ }
13
+
14
+ /** Stateful equivalent for an owned, validated JSONL snapshot. */
15
+ export function createAgyErrorBoundary(error, floor = -1, source = null, sessionId = null) {
9
16
  let previous = -1, lastError = null, boundary = null, invocation = null, call = null;
10
- const launches = new Map();
11
- for (const row of rows) {
12
- if (!Number.isInteger(row.step_index) || row.step_index <= previous) return null;
17
+ const launches = new Map(); let invalid = false;
18
+ function add(row) {
19
+ if (invalid) return;
20
+ if (!Number.isInteger(row.step_index) || row.step_index <= previous) { invalid = true; return; }
13
21
  previous = row.step_index;
14
- if (row.step_index <= (floor ?? -1)) continue;
15
- if (row.type === "USER_INPUT") { lastError = null; boundary = null; invocation = null; call = null; launches.clear(); continue; }
16
- if (row.type === "ERROR_MESSAGE") { lastError = row; boundary = null; invocation = null; continue; }
22
+ if (row.step_index <= (floor ?? -1)) return;
23
+ if (row.type === "USER_INPUT") { lastError = null; boundary = null; invocation = null; call = null; launches.clear(); return; }
24
+ if (row.type === "ERROR_MESSAGE") { lastError = row; boundary = null; invocation = null; return; }
17
25
  if (Array.isArray(row.tool_calls)) {
18
26
  invocation = null;
19
27
  call = row.tool_calls.length === 1 ? row.tool_calls[0] : null;
@@ -29,7 +37,7 @@ export function recoveredAgyErrorBoundary(text, error, floor = -1, source = null
29
37
  if (row.type === 'GENERIC' && row.status === 'RUNNING') {
30
38
  const task = row.content?.match(/^Created At: [^\n]+\nTool is running as a background task with task id: ([\w-]+\/task-\d+)\n/);
31
39
  if (task && invocation && (!sessionId || task[1].startsWith(`${sessionId}/`))) launches.set(task[1], launches.has(task[1]) ? null : { invocation, step: row.step_index });
32
- call = null; invocation = null; continue;
40
+ call = null; invocation = null; return;
33
41
  }
34
42
  const envelope = agyTerminalEnvelope(row, call);
35
43
  let launchStep = null;
@@ -38,11 +46,11 @@ export function recoveredAgyErrorBoundary(text, error, floor = -1, source = null
38
46
  invocation = launch && launch.step < row.step_index ? launch.invocation : null;
39
47
  launchStep = launch?.step ?? null;
40
48
  }
41
- if (!lastError || envelope?.exit_code !== 0) continue;
49
+ if (!lastError || envelope?.exit_code !== 0) return;
42
50
  const retained = envelope.output_reference && source && sessionId ? ownedAgyOutput(envelope.output_reference, source, sessionId, envelope.task_id) : null;
43
51
  const full = retained ? agyTerminalEnvelope({ type: 'GENERIC', status: 'DONE', content: retained.text }) : null;
44
52
  const output = (retained ? full?.exit_code === 0 ? full.output : null : envelope.output)?.replaceAll("\r", "");
45
- if (!output) continue;
53
+ if (!output) return;
46
54
  for (const match of output.matchAll(/^\s*\{/gm)) {
47
55
  let receipt;
48
56
  const candidate = output.slice(match.index).trim();
@@ -74,6 +82,6 @@ export function recoveredAgyErrorBoundary(text, error, floor = -1, source = null
74
82
  }
75
83
  }
76
84
  }
77
- return boundary && lastError?.error === error ? boundary : null;
85
+ return { add, finish: () => !invalid && boundary && lastError?.error === error ? boundary : null };
78
86
  }
79
87
 
@@ -160,14 +160,23 @@ async function agyNativeRequest({ endpoint, sessionId, cwd, remainingMs, signal,
160
160
  if (!/^127\.0\.0\.1:\d+$/.test(endpoint?.target ?? '') || Number(endpoint.target.split(':')[1]) < 1 || Number(endpoint.target.split(':')[1]) > 65535) throw invalid('native_endpoint_nonloopback');
161
161
  if (!Number.isFinite(remainingMs) || remainingMs < 1) throw invalid('observation_budget_exhausted');
162
162
  const deadline = Date.now() + remainingMs;
163
- const client = new Client(endpoint.target, grpc.credentials.createInsecure(), { 'grpc.max_receive_message_length': 1024 * 1024, 'grpc.max_send_message_length': 64 * 1024, 'grpc.enable_retries': 0 });
163
+ const client = new Client(endpoint.target, grpc.credentials.createInsecure(), { 'grpc.max_receive_message_length': -1, 'grpc.max_send_message_length': 64 * 1024, 'grpc.enable_retries': 0 });
164
164
  let active;
165
165
  const headers = new grpc.Metadata();
166
166
  if (typeof csrfToken === 'string' && csrfToken) headers.set('x-codeium-csrf-token', csrfToken);
167
167
  const abort = () => active?.cancel();
168
+ // Native error details may contain request or authentication material. Keep
169
+ // the method and numeric status for diagnosis without copying those bytes.
170
+ const grpcError = (name, error) => {
171
+ const code = Number.isInteger(error?.code) ? error.code : null;
172
+ const failure = invalid(`grpc_${code ?? 'unknown'}`);
173
+ failure.details.grpc_method = agyNativeService[name].path;
174
+ failure.details.grpc_code = code;
175
+ return failure;
176
+ };
168
177
  const unary = (name, request) => new Promise((resolve, reject) => {
169
178
  if (signal?.aborted) return reject(invalid('inspection_cancelled'));
170
- active = client[name](request, headers, { deadline }, (error, value) => error ? reject(invalid(`grpc_${error.code ?? 'unknown'}`)) : value ? resolve(value) : reject(invalid('grpc_message_missing')));
179
+ active = client[name](request, headers, { deadline }, (error, value) => error ? reject(grpcError(name, error)) : value ? resolve(value) : reject(invalid('grpc_message_missing')));
171
180
  });
172
181
  signal?.addEventListener('abort', abort, { once: true });
173
182
  try {
@@ -191,7 +200,7 @@ async function agyNativeRequest({ endpoint, sessionId, cwd, remainingMs, signal,
191
200
  const snapshot = JSON.parse(JSON.stringify(update, (key, value) => key.startsWith('$') ? undefined : value));
192
201
  done = true; resolve({ ...snapshot, observed_at: new Date().toISOString(), method: agyNativeService.state.path }); active.cancel();
193
202
  });
194
- active.on('error', error => { if (!done) { done = true; reject(invalid(`grpc_${error.code ?? 'unknown'}`)); } });
203
+ active.on('error', error => { if (!done) { done = true; reject(grpcError('state', error)); } });
195
204
  active.on('end', () => { if (!done) { done = true; reject(invalid('native_state_snapshot_missing')); } });
196
205
  });
197
206
  }
@@ -0,0 +1,19 @@
1
+ // A new StreamAgentStateUpdates subscription starts with a full snapshot.
2
+ // An omitted protobuf update is an empty list; a partial indexed list is not.
3
+ export const agyNativeFullList = (update, key) => !update ||
4
+ Array.isArray(update[key]) && update.totalLength === update[key].length &&
5
+ update.indices?.length === update.totalLength && update.indices.every((value, offset) => value === offset);
6
+
7
+ export function agyNativeInventory(snapshot, sessionId) {
8
+ if (snapshot?.conversationId !== sessionId || !snapshot.trajectoryId || snapshot.error ||
9
+ snapshot.method !== '/exa.language_server_pb.LanguageServerService/StreamAgentStateUpdates' ||
10
+ !agyNativeFullList(snapshot.backgroundTasksUpdate, 'backgroundTasks') ||
11
+ !agyNativeFullList(snapshot.pendingAgentMessagesUpdate, 'pendingAgentMessages'))
12
+ throw Object.assign(new Error('AGY native task inventory is incomplete or unbound'), { code: 'native_resources_unproven' });
13
+ const tasks = snapshot.backgroundTasksUpdate?.backgroundTasks?.map(item => item.taskSnapshot) ?? [];
14
+ if (tasks.some(task => !task || typeof task.taskId !== 'string' || !task.taskId.startsWith(`${sessionId}/task-`) || !/\/task-\d+$/.test(task.taskId) || !task.toolName) ||
15
+ new Set(tasks.map(task => task.taskId)).size !== tasks.length)
16
+ throw Object.assign(new Error('AGY native task identity is unproven'), { code: 'native_resources_unproven' });
17
+ return { tasks, messages: snapshot.pendingAgentMessagesUpdate?.pendingAgentMessages ?? [],
18
+ idle: snapshot.fullyIdle === true && snapshot.status === 1 && snapshot.executorLoopStatus === 1 && snapshot.hasActiveChildren !== true };
19
+ }
@@ -2,6 +2,7 @@ import { constants, openSync, closeSync, fstatSync, lstatSync, readSync, realpat
2
2
  import { fileURLToPath } from 'node:url';
3
3
  import path from 'node:path';
4
4
  import { createHash } from 'node:crypto';
5
+ import { StringDecoder } from 'node:string_decoder';
5
6
 
6
7
  const taskId = '[\\w-]+/task-\\d+';
7
8
 
@@ -18,16 +19,22 @@ export function ownedAgyOutput(reference, source, sessionId, ownerTask = null) {
18
19
  if (!number || ![`steps/${number}/output.txt`, `tasks/task-${number}.log`].includes(relative)) return null;
19
20
  }
20
21
  const before = lstatSync(file);
21
- if (!before.isFile() || before.size > 2 * 1024 * 1024) return null;
22
+ if (!before.isFile()) return null;
22
23
  fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
23
24
  const stat = fstatSync(fd);
24
25
  if (!stat.isFile() || stat.ino !== before.ino || stat.dev !== before.dev) return null;
25
- const bytes = Buffer.alloc(stat.size);
26
- let offset = 0;
27
- while (offset < bytes.length) { const count = readSync(fd, bytes, offset, bytes.length - offset, offset); if (!count) return null; offset += count; }
26
+ const bytes = Buffer.allocUnsafe(64 * 1024), decoder = new StringDecoder('utf8'), hash = createHash('sha256');
27
+ let offset = 0, text = '';
28
+ while (offset < stat.size) {
29
+ const count = readSync(fd, bytes, 0, Math.min(bytes.length, stat.size - offset), offset); if (!count) return null;
30
+ const chunk = bytes.subarray(0, count); hash.update(chunk); text += decoder.write(chunk); offset += count;
31
+ }
32
+ text += decoder.end();
28
33
  const after = fstatSync(fd);
29
- if (stat.size !== after.size || stat.mtimeMs !== after.mtimeMs) return null;
30
- return { text: bytes.toString('utf8'), sha256: createHash('sha256').update(bytes).digest('hex'), locator: reference };
34
+ const finalPath = lstatSync(file);
35
+ if (!finalPath.isFile() || finalPath.dev !== stat.dev || finalPath.ino !== stat.ino || realpathSync(file) !== file
36
+ || stat.size !== after.size || stat.mtimeMs !== after.mtimeMs || stat.ctimeMs !== after.ctimeMs) return null;
37
+ return { text, sha256: hash.digest('hex'), locator: reference };
31
38
  } catch { return null; }
32
39
  finally { if (fd !== undefined) closeSync(fd); }
33
40
  }
@@ -0,0 +1,54 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { StringDecoder } from 'node:string_decoder';
3
+
4
+ // The caller opens and validates the owned no-symlink handle and must discard
5
+ // tentative callback results if this function rejects. One complete JSONL
6
+ // record, rather than the whole transcript, is the largest retained value.
7
+ export async function readAgyTranscriptSnapshot(handle, { locator, assertIdentity, onRecord, onProgress }) {
8
+ if (typeof assertIdentity !== 'function' || typeof onRecord !== 'function') throw new TypeError('snapshot requires owner validation and a record consumer');
9
+ const original = await handle.stat();
10
+ const unchanged = async () => {
11
+ const current = await handle.stat();
12
+ if (!current.isFile() || current.dev !== original.dev || current.ino !== original.ino || current.size !== original.size
13
+ || current.mtimeMs !== original.mtimeMs || current.ctimeMs !== original.ctimeMs) throw new Error('agy_transcript_snapshot_changed');
14
+ await assertIdentity(current);
15
+ };
16
+ if (!original.isFile()) throw new Error('agy_transcript_snapshot_changed');
17
+ await unchanged();
18
+ const buffer = Buffer.allocUnsafe(64 * 1024), digest = createHash('sha256');
19
+ let offset = 0, complete = 0, completeHash = digest.copy();
20
+ while (offset < original.size) {
21
+ const { bytesRead } = await handle.read(buffer, 0, Math.min(buffer.length, original.size - offset), offset);
22
+ if (!bytesRead) throw new Error('agy_transcript_snapshot_changed');
23
+ const chunk = buffer.subarray(0, bytesRead), lastNewline = chunk.lastIndexOf(10);
24
+ if (lastNewline >= 0) {
25
+ digest.update(chunk.subarray(0, lastNewline + 1));
26
+ complete = offset + lastNewline + 1; completeHash = digest.copy();
27
+ digest.update(chunk.subarray(lastNewline + 1));
28
+ } else digest.update(chunk);
29
+ offset += bytesRead;
30
+ await onProgress?.(`digest:${original.dev}:${original.ino}:${original.size}:${original.mtimeMs}:${offset}`);
31
+ }
32
+ const source = { locator, byte_start: 0, byte_end: complete, sha256: completeHash.digest('hex') };
33
+ await unchanged();
34
+ const decoder = new StringDecoder('utf8'); let pending = '', line = 0;
35
+ offset = 0;
36
+ while (offset < complete) {
37
+ const { bytesRead } = await handle.read(buffer, 0, Math.min(buffer.length, complete - offset), offset);
38
+ if (!bytesRead) throw new Error('agy_transcript_snapshot_changed');
39
+ pending += decoder.write(buffer.subarray(0, bytesRead)); offset += bytesRead;
40
+ await onProgress?.(`records:${source.sha256}:${offset}`);
41
+ let newline;
42
+ while ((newline = pending.indexOf('\n')) >= 0) {
43
+ const text = pending.slice(0, newline); pending = pending.slice(newline + 1); line += 1;
44
+ if (!text.trim()) continue;
45
+ let record;
46
+ try { record = JSON.parse(text); } catch { throw new Error('agy_transcript_invalid_record'); }
47
+ await onRecord(record, { ...source, line });
48
+ }
49
+ }
50
+ pending += decoder.end();
51
+ if (pending) throw new Error('agy_transcript_snapshot_changed');
52
+ await unchanged();
53
+ return { source, partial_tail: complete !== original.size };
54
+ }
@@ -1,2 +1,3 @@
1
1
  export function recoverDaemonReply(root: string, id: string, options?: { timeoutMs?: number | null; signal?: AbortSignal; binding?: Record<string, unknown>; onProgress?: (value: Record<string, unknown>) => void; onOutcome?: (value: Record<string, unknown>) => void; onPendingSettlement?: (value: Record<string, unknown>) => Promise<void>; observationClock?: import("./observation-clock.mjs").ObservationClock }): Promise<Record<string, unknown>>;
2
2
  export function retainedDaemonReply(root: string, id: string, binding?: Record<string, unknown>): Promise<{ ready: boolean; outcome: unknown; value?: unknown; settlement?: Record<string, unknown> | null; reason?: string } | null>;
3
+ export function withDaemonProgressObserver<T>(observer: (value: Record<string, unknown>) => void, action: () => Promise<T>): Promise<T>;
@@ -1,13 +1,17 @@
1
1
  import net from 'node:net';
2
+ import { AsyncLocalStorage } from 'node:async_hooks';
3
+ import { randomUUID } from 'node:crypto';
2
4
  import { readFile } from 'node:fs/promises';
3
5
  import path from 'node:path';
4
6
  import { hashJson, writeJsonAtomic } from './runner-events.mjs';
5
7
  import { ObservationClock } from './observation-clock.mjs';
6
8
  import { canonicalNativeOperation, nativeOperationWait, validateDaemonWait } from './adapter-timeouts.mjs';
7
- import { assertNativeLine, retainNativeReplyBuffer } from './native-streams.mjs';
9
+ import { assertNativeLine } from './native-streams.mjs';
8
10
  import { operationDirectory, readOperationRecord, readOperationProgress, readOperationWaiting, suspendOperationClock } from './operation-progress.mjs';
9
11
 
10
12
  const productive = new Set(['session.create', 'session.prompt', 'session.start', 'session.fork', 'session.resume']);
13
+ const progressObserver = new AsyncLocalStorage();
14
+ export const withDaemonProgressObserver = (observer, action) => progressObserver.run(observer, action);
11
15
  const fault = (code, message, details) => Object.assign(new Error(message), { code, retryable: false, details });
12
16
  export async function retainedDaemonReply(root, id, binding = {}) {
13
17
  const requested = await readOperationRecord(root, id, 'requested.json');
@@ -84,9 +88,12 @@ export async function callDaemonObserved({ stateDir, socket, request, responseSc
84
88
  validateDaemonWait(harness, request.operation, timeoutMs);
85
89
  assertNativeLine(JSON.stringify(request), 'request');
86
90
  const durable = productive.has(request.operation);
91
+ const verification = request.operation === 'hook.verify' && request.params?.binding?.native_admission === 'assigned_work_start';
92
+ if (verification) request = { ...request, id: randomUUID() }; // Read-only proof is fresh, never a replay of prior authority.
93
+ const observable = durable || verification, onProgress = progressObserver.getStore();
87
94
  const binding = { operation: request.operation, params_sha256: hashJson({ operation: request.operation, params: request.params ?? {} }) };
88
95
  let initialDaemon;
89
- if (durable) try {
96
+ if (observable) try {
90
97
  const state = JSON.parse(await readFile(path.join(stateDir,'daemon.json'),'utf8'));
91
98
  initialDaemon = state.daemon_id ?? state.config?.daemonId ?? null;
92
99
  binding.owner_id = state.config?.runtime_owner?.owner_id ?? null;
@@ -119,12 +126,13 @@ export async function callDaemonObserved({ stateDir, socket, request, responseSc
119
126
  if (done || polling) return pollFlight; polling = true;
120
127
  pollFlight = (async () => {
121
128
  try {
122
- const progress = durable ? await readOperationProgress(stateDir, request.id, { ...binding, ...(frozenDaemon === undefined ? {} : { daemon_id: frozenDaemon }) }) : null;
129
+ const progress = observable ? await readOperationProgress(stateDir, request.id, { ...binding, ...(frozenDaemon === undefined ? {} : { daemon_id: frozenDaemon }) }) : null;
123
130
  if (done) return;
124
131
  if (progress && progress.sequence > lastSequence) {
125
132
  frozenDaemon ??= progress.daemon_id; lastSequence = progress.sequence;
126
133
  binding.generation ??= progress.generation;
127
134
  latestProgress = {cursor:progress.sequence,observed_at:Date.parse(progress.observed_at)};
135
+ if (verification && progress.phase === 'verification_progress') onProgress?.(progress);
128
136
  process.stderr.write(`${JSON.stringify({ kind: 'operation_progress', ...progress })}\n`);
129
137
  }
130
138
  if (durable && disconnected) {
@@ -148,7 +156,7 @@ export async function callDaemonObserved({ stateDir, socket, request, responseSc
148
156
  client.on('end', () => lost(fault('daemon_connection_closed', 'daemon closed before reply')));
149
157
  client.on('close', () => lost(fault('daemon_connection_closed', 'daemon closed before reply')));
150
158
  client.on('data', chunk => { void (async () => {
151
- buffer += chunk; if (!retainNativeReplyBuffer(buffer, finish)) return;
159
+ buffer += chunk;
152
160
  const at = buffer.indexOf('\n'); if (at < 0) return;
153
161
  try {
154
162
  const response = JSON.parse(buffer.slice(0, at));
@@ -4,7 +4,7 @@ import { mkdir, readFile, readdir, link, unlink, open } from "node:fs/promises";
4
4
  import { randomUUID } from "node:crypto";
5
5
  import path from "node:path";
6
6
  import { hashJson, writeJsonAtomic } from "./runner-events.mjs";
7
- import { errorRecord, isObservationLoss, isProvenShellCorrection, reportedError } from "./operation-errors.mjs";
7
+ import { errorRecord, isNativeHookObservationLoss, isObservationLoss, isProvenShellCorrection, reportedError } from "./operation-errors.mjs";
8
8
  import { assertDaemonOwnership, budgetObservationScope, releaseBudgetObservation, completeBudgetOperation, stopProcessGroup } from "./managed-daemon.mjs";
9
9
  import { processSnapshot } from "./process-snapshot.mjs";
10
10
  import { NATIVE_SHUTDOWN_CONTRACT, NATIVE_SHUTDOWN_PHASES } from "./native-children.mjs";
@@ -90,6 +90,12 @@ export async function completeDaemonShutdown(root, state, persist, prepare, phas
90
90
  commitDaemonShutdown(root);
91
91
  if (reconcileResources && !shutdown.phases.owned_resources) {
92
92
  const revision = state.native_execution_revision, generation = dispatchGenerations.get(root) ?? shutdown.generation;
93
+ // A fresh attempt cannot reuse a previous inventory while reconciliation
94
+ // is in flight or after the new attempt fails.
95
+ if (shutdown.phases.native_resources_reconciled) {
96
+ shutdown = { ...shutdown, phases: { ...shutdown.phases, native_resources_reconciled: false } };
97
+ await persist({ shutdown });
98
+ }
93
99
  const resources = await reconcileResources();
94
100
  const prior = shutdown.result?.native_resources;
95
101
  if ((state.daemon_id ?? state.config?.daemonId) !== daemonId || state.native_execution_revision !== revision
@@ -98,7 +104,9 @@ export async function completeDaemonShutdown(root, state, persist, prepare, phas
98
104
  || resources.generation !== prior?.generation || resources.revision !== prior?.revision || !Array.isArray(resources.entries)
99
105
  || resources.entries.some(item => item.owner_daemon_id !== daemonId || item.generation !== resources.generation))
100
106
  throw Object.assign(new Error('native resource reconciliation lost its owner/execution fence'), { code: 'native_resources_unproven' });
101
- shutdown = { ...shutdown, result: { ...shutdown.result, native_resources: resources } };
107
+ shutdown = { ...shutdown, phases: { ...shutdown.phases, native_resources_reconciled: true },
108
+ native_resource_reconciliation: { daemon_id: daemonId, dispatch_generation: generation, native_revision: revision },
109
+ result: { ...shutdown.result, native_resources: resources } };
102
110
  await persist({ shutdown });
103
111
  }
104
112
  for (const [phase, action] of phases) {
@@ -249,8 +257,12 @@ export async function inspectDaemonOperation(root, id) {
249
257
  catch (error) { hookDiagnostics.push({ code: "native_hook_receipt_invalid", message: error.message, request_id: binding.request_id, effect: "unknown", receipt_bound: false }); continue; }
250
258
  if (failure?.request_id !== binding.request_id || typeof failure.code !== "string" || typeof failure.message !== "string") continue;
251
259
  if (binding.command_sha256 && JSON.stringify(failure.native_hook_binding) !== JSON.stringify(binding)) continue;
252
- const correction = isProvenShellCorrection(failure);
253
- (correction ? hookCorrections : hookFailures).push({ ...errorRecord(failure), observed_at: failure.observed_at ?? null, details: { ...failure.details, native_hook_binding: binding, effect: correction ? "no_effect" : "unknown" } });
260
+ // AGY can observe a callback while a newer prompt is active without
261
+ // knowing the native creator. Keep its original refusal as diagnostic,
262
+ // never as this operation's primary failure or automatic correction.
263
+ const unconfirmedCreator = binding.native_creator === "unconfirmed";
264
+ const correction = !unconfirmedCreator && isProvenShellCorrection(failure);
265
+ (unconfirmedCreator ? hookDiagnostics : correction ? hookCorrections : isNativeHookObservationLoss(failure) ? hookDiagnostics : hookFailures).push({ ...errorRecord(failure), observed_at: failure.observed_at ?? null, details: { ...failure.details, native_hook_binding: binding, effect: correction ? "no_effect" : "unknown" } });
254
266
  }
255
267
  hookFailures.sort((a, b) => String(a.observed_at ?? "").localeCompare(String(b.observed_at ?? "")));
256
268
  // Admission failure is causal evidence, not proof that the native tree stopped.
@@ -342,6 +354,8 @@ export async function durableDaemonDispatch(root, request, action, observe) {
342
354
  return settlement.state === "not_required" || !result || typeof result !== "object" || Array.isArray(result) ? result : { ...result, settlement };
343
355
  }
344
356
  const control = ["daemon.stop", "session.cancel"].includes(request.operation);
357
+ if (request.operation === 'hook.verify' && request.params?.binding?.native_admission === 'assigned_work_start')
358
+ return await journalDispatch(root, request, () => withOperationProgress(root, request.id, action), false);
345
359
  if (!productive.has(request.operation) && !control) return await action();
346
360
  const generation = dispatchGenerations.get(root) ?? 0;
347
361
  // Once retirement is committed there is no productive dispatch to cancel.
@@ -397,7 +411,7 @@ export async function durableDaemonStart(options, action) {
397
411
  return await journalDispatch(root, request, () => action(options));
398
412
  }
399
413
 
400
- async function journalDispatch(root, request, action) {
414
+ async function journalDispatch(root, request, action, allowReplay = true) {
401
415
  const directory = location(root, request.id), fingerprint = hashJson({ operation: request.operation, params: request.params ?? {} });
402
416
  await mkdir(directory, { recursive: true, mode: 0o700 });
403
417
  // Publish a complete request atomically and exclusively. Writing directly with
@@ -411,6 +425,7 @@ async function journalDispatch(root, request, action) {
411
425
  await link(temporary, path.join(directory, "requested.json"));
412
426
  } catch (error) {
413
427
  if (error.code !== "EEXIST") throw error;
428
+ if (!allowReplay) throw Object.assign(new Error('Native verification requires a fresh observation'), { code: 'operation_conflict' });
414
429
  const saved = await inspectDaemonOperation(root, request.id);
415
430
  if (saved.params_sha256 !== fingerprint || (saved.invocation_sha256 ?? null) !== (request.invocation_sha256 ?? null)) throw Object.assign(new Error("operation id was reused with different parameters or invocation"), { code: "operation_conflict", details: { operation_id: request.id } });
416
431
  if (saved.state === "completed") return saved.result;
@@ -4,3 +4,4 @@ export declare function qualifyAgyHooks(projectRoot: string, options?: { stateDi
4
4
  workspaceHooksSha256: string;
5
5
  workspaceHooksInstalled: string;
6
6
  }>;
7
+ export declare function callDaemon(stateDir: string, operation: string, params?: Record<string, unknown>, timeoutMs?: number): Promise<Record<string, unknown>>;