@deksden-com/dd-flow-cli 0.9.0-beta.144 → 0.9.0-beta.149

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 (254) hide show
  1. package/CHANGELOG.md +130 -0
  2. package/README.md +13 -8
  3. package/dist/build-info.json +7 -7
  4. package/dist/cli/command-inputs.js +13 -3
  5. package/dist/cli/command-metadata.js +51 -16
  6. package/dist/cli/diagnostic-invocation.js +46 -0
  7. package/dist/cli/diagnostic.js +120 -0
  8. package/dist/cli/help.js +67 -1
  9. package/dist/cli/hook-ingress.js +27 -3
  10. package/dist/cli/memory-flow-admission.js +9 -3
  11. package/dist/cli/memory-init-invocation.js +10 -3
  12. package/dist/cli/memory-init.js +77 -68
  13. package/dist/cli/memory-storage-admission.js +3 -2
  14. package/dist/cli/memory-storage-qualification.js +105 -0
  15. package/dist/cli/memory-storage-transition.js +123 -0
  16. package/dist/cli/memory-upgrade.js +189 -0
  17. package/dist/cli/native-hook-ingress.js +14 -4
  18. package/dist/cli/native-hook-preparation.js +55 -0
  19. package/dist/cli/response-file.js +2 -1
  20. package/dist/cli/run-cli.js +79 -40
  21. package/dist/cli/setup.js +2 -1
  22. package/dist/cli.js +18 -0
  23. package/dist/domain/diagnostic-aspects.js +103 -0
  24. package/dist/domain/diagnostic-flows.js +8 -0
  25. package/dist/domain/stage-handler-identities.js +8 -0
  26. package/dist/harness-runtime/bin/dd-agy.mjs +3 -3
  27. package/dist/harness-runtime/bin/dd-grok.mjs +3 -3
  28. package/dist/harness-runtime/bin/dd-zcode.mjs +5 -5
  29. package/dist/harness-runtime/lib/agy-error-boundary.mjs +58 -0
  30. package/dist/harness-runtime/lib/agy-native-inspection.mjs +216 -0
  31. package/dist/harness-runtime/lib/agy-native_pb.js +117 -0
  32. package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
  33. package/dist/harness-runtime/lib/daemon-operations.mjs +84 -10
  34. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +366 -52
  35. package/dist/harness-runtime/lib/dd-agy.mjs +17 -3
  36. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +27 -13
  37. package/dist/harness-runtime/lib/dd-codex.mjs +4 -0
  38. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +4 -3
  39. package/dist/harness-runtime/lib/dd-droid.mjs +71 -29
  40. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +73 -16
  41. package/dist/harness-runtime/lib/dd-grok.mjs +6 -3
  42. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +66 -20
  43. package/dist/harness-runtime/lib/dd-opencode.mjs +30 -3
  44. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +7 -6
  45. package/dist/harness-runtime/lib/dd-zcode.mjs +44 -14
  46. package/dist/harness-runtime/lib/delegation-instructions.mjs +1 -1
  47. package/dist/harness-runtime/lib/driver-recovery.mjs +9 -8
  48. package/dist/harness-runtime/lib/entity-references.d.mts +2 -0
  49. package/dist/harness-runtime/lib/entity-references.mjs +9 -0
  50. package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -1
  51. package/dist/harness-runtime/lib/managed-daemon.mjs +24 -9
  52. package/dist/harness-runtime/lib/native-children.d.mts +8 -1
  53. package/dist/harness-runtime/lib/native-children.mjs +114 -8
  54. package/dist/harness-runtime/lib/native-hook-command.mjs +4 -4
  55. package/dist/harness-runtime/lib/native-request-proof.mjs +1 -1
  56. package/dist/harness-runtime/lib/observation-clock.d.mts +2 -2
  57. package/dist/harness-runtime/lib/observation-clock.mjs +7 -4
  58. package/dist/harness-runtime/lib/opencode-plugin.mjs +3 -3
  59. package/dist/harness-runtime/lib/operation-errors.mjs +1 -1
  60. package/dist/harness-runtime/lib/provider-overload-policy.d.mts +5 -3
  61. package/dist/harness-runtime/lib/provider-overload-policy.mjs +20 -7
  62. package/dist/harness-runtime/lib/runner-events.mjs +3 -1
  63. package/dist/harness-runtime/lib/setup-progress.mjs +8 -6
  64. package/dist/harness-runtime/lib/tool-observations.mjs +1 -1
  65. package/dist/schemas/diagnostic-stage-result.schema.json +354 -0
  66. package/dist/schemas/diagnostic-work-result.schema.json +296 -0
  67. package/dist/schemas/execution-bundle.schema.json +7 -1
  68. package/dist/schemas/mb-upgrade-stage-result.schema.json +863 -0
  69. package/dist/schemas/memory-repair-work-result.schema.json +9 -2
  70. package/dist/schemas/project-flow-pack-manifest.schema.json +1 -1
  71. package/dist/schemas/release-impact.schema.json +3 -3
  72. package/dist/schemas/stage-start-response.schema.json +4 -0
  73. package/dist/services/accepted-intent.js +13 -1
  74. package/dist/services/canon.js +43 -7
  75. package/dist/services/check-environment.js +21 -0
  76. package/dist/services/check-runner-evidence.js +267 -15
  77. package/dist/services/cli-operation-classifier.js +1 -1
  78. package/dist/services/cli-upgrade-allowlist.js +1 -1
  79. package/dist/services/code-checks.js +34 -19
  80. package/dist/services/compatibility-preflight.js +5 -2
  81. package/dist/services/controller-execution.js +10 -4
  82. package/dist/services/controller-fanout.js +34 -10
  83. package/dist/services/controller-memory-task.js +45 -0
  84. package/dist/services/controller-memory-worker.js +49 -0
  85. package/dist/services/dashboard.js +2 -2
  86. package/dist/services/descriptor-run-settlement.js +5 -3
  87. package/dist/services/descriptor-run-start.js +46 -20
  88. package/dist/services/descriptor-stage-coordinator.js +66 -13
  89. package/dist/services/descriptor-stage-settlement.js +2 -1
  90. package/dist/services/descriptor-stage-start.js +40 -4
  91. package/dist/services/diagnostic-evidence.js +73 -0
  92. package/dist/services/diagnostic-report-publication.js +72 -0
  93. package/dist/services/diagnostic-report.js +67 -0
  94. package/dist/services/diagnostic-run-bundle.js +74 -0
  95. package/dist/services/diagnostic-snapshot.js +159 -0
  96. package/dist/services/diagnostic-stage-handlers.js +108 -0
  97. package/dist/services/diagnostic-stage-results.js +132 -0
  98. package/dist/services/diagnostic-start.js +84 -0
  99. package/dist/services/diagnostic-work-batch.js +34 -0
  100. package/dist/services/diagnostic-work-collection.js +97 -0
  101. package/dist/services/diagnostic-work-registration.js +101 -0
  102. package/dist/services/diagnostic-work-results.js +93 -0
  103. package/dist/services/engines.js +64 -14
  104. package/dist/services/eval-snapshots.js +186 -119
  105. package/dist/services/execution-bundles.js +19 -7
  106. package/dist/services/external-work-launch.js +5 -3
  107. package/dist/services/flow-catalog.js +13 -4
  108. package/dist/services/frozen-engine.js +148 -11
  109. package/dist/services/hook-memory-routing.js +188 -0
  110. package/dist/services/hook-recovery-guard.js +14 -0
  111. package/dist/services/hook-session-observation.js +24 -0
  112. package/dist/services/hooks.js +27 -8
  113. package/dist/services/hybrid-contracts.js +4 -13
  114. package/dist/services/hybrid-digest.js +14 -0
  115. package/dist/services/installed-flow-source.js +47 -4
  116. package/dist/services/lifecycle-invocations.js +179 -38
  117. package/dist/services/memory-accepted-composition.js +5 -4
  118. package/dist/services/memory-completed-correction-wave.js +23 -15
  119. package/dist/services/memory-compose-handler.js +163 -21
  120. package/dist/services/memory-composition-acceptance.js +186 -10
  121. package/dist/services/memory-composition-graph.js +42 -11
  122. package/dist/services/memory-composition-registration.js +23 -8
  123. package/dist/services/memory-composition-work-custody.js +14 -9
  124. package/dist/services/memory-composition-work-results.js +15 -3
  125. package/dist/services/memory-corrected-review-batch.js +10 -3
  126. package/dist/services/memory-corrected-review-findings.js +4 -2
  127. package/dist/services/memory-corrected-review-inputs.js +10 -5
  128. package/dist/services/memory-corrected-review-receipt.js +17 -9
  129. package/dist/services/memory-corrected-review-registration.js +3 -2
  130. package/dist/services/memory-correction-check-receipt.js +20 -4
  131. package/dist/services/memory-custody-snapshot.js +36 -0
  132. package/dist/services/memory-finish-evidence.js +44 -22
  133. package/dist/services/memory-finish-handler.js +37 -23
  134. package/dist/services/memory-finish-integration.js +218 -48
  135. package/dist/services/memory-finish-readiness.js +66 -19
  136. package/dist/services/memory-git-integration.js +87 -18
  137. package/dist/services/memory-historical-corrected-review.js +21 -15
  138. package/dist/services/memory-historical-original-review.js +22 -9
  139. package/dist/services/memory-historical-repair-registration.js +14 -8
  140. package/dist/services/memory-historical-second-review.js +30 -11
  141. package/dist/services/memory-init-contexts.js +44 -2
  142. package/dist/services/memory-init-controller-start.js +27 -8
  143. package/dist/services/memory-init-git-policy.js +50 -21
  144. package/dist/services/memory-init-lint-capability.js +24 -6
  145. package/dist/services/memory-init-native-capacity.js +17 -4
  146. package/dist/services/memory-init-preflight.js +21 -17
  147. package/dist/services/memory-init-request-candidate.js +73 -17
  148. package/dist/services/memory-init-request-input.js +23 -7
  149. package/dist/services/memory-init-run-bundle.js +49 -12
  150. package/dist/services/memory-init-storage.js +17 -7
  151. package/dist/services/memory-init-worktrunk-capability.js +3 -7
  152. package/dist/services/memory-layer.js +71 -6
  153. package/dist/services/memory-lint-config.js +111 -2
  154. package/dist/services/memory-native-review-copy.js +5 -67
  155. package/dist/services/memory-normal-flow-entry.js +12 -0
  156. package/dist/services/memory-ordinary-defaults.js +22 -5
  157. package/dist/services/memory-preflight-handler.js +15 -0
  158. package/dist/services/memory-repair-current-view.js +1 -1
  159. package/dist/services/memory-repair-document-delta.js +70 -33
  160. package/dist/services/memory-repair-history.js +12 -7
  161. package/dist/services/memory-repair-physical-binding.js +1 -119
  162. package/dist/services/memory-repair-publication-plan.js +5 -3
  163. package/dist/services/memory-repair-publication-source.js +1 -1
  164. package/dist/services/memory-repair-publication.js +3 -2
  165. package/dist/services/memory-repair-registration.js +2 -1
  166. package/dist/services/memory-repair-work-baseline.js +4 -2
  167. package/dist/services/memory-repair-work-completion.js +32 -12
  168. package/dist/services/memory-repair-work-results.js +21 -2
  169. package/dist/services/memory-repair-workspace.js +11 -2
  170. package/dist/services/memory-repair-writer-copy.js +2 -1
  171. package/dist/services/memory-result-binding.js +78 -12
  172. package/dist/services/memory-retained-review-authority.js +17 -8
  173. package/dist/services/memory-retained-second-baseline.js +1 -1
  174. package/dist/services/memory-retained-second-repairs.js +12 -6
  175. package/dist/services/memory-review-correction-plan.js +47 -12
  176. package/dist/services/memory-review-handler.js +17 -17
  177. package/dist/services/memory-review-inputs.js +55 -9
  178. package/dist/services/memory-review-limits.js +10 -0
  179. package/dist/services/memory-review-receipt.js +2 -2
  180. package/dist/services/memory-review-registration.js +13 -4
  181. package/dist/services/memory-review-work-custody.js +3 -1
  182. package/dist/services/memory-review-work-input.js +26 -7
  183. package/dist/services/memory-review-work-results.js +26 -4
  184. package/dist/services/memory-second-corrected-review-inputs.js +5 -3
  185. package/dist/services/memory-second-correction-plan.js +2 -1
  186. package/dist/services/memory-second-repair-launch.js +4 -4
  187. package/dist/services/memory-second-repair-source.js +1 -1
  188. package/dist/services/memory-stage-dispatch.js +38 -1
  189. package/dist/services/memory-stage-results.js +113 -22
  190. package/dist/services/memory-tool-probe.js +25 -0
  191. package/dist/services/memory-upgrade-analysis-handler.js +243 -0
  192. package/dist/services/memory-upgrade-bootstrap-readiness.js +251 -0
  193. package/dist/services/memory-upgrade-evidence.js +100 -0
  194. package/dist/services/memory-upgrade-inventory.js +442 -0
  195. package/dist/services/memory-upgrade-publication-custody.js +73 -0
  196. package/dist/services/memory-upgrade-publication.js +413 -0
  197. package/dist/services/memory-upgrade-selection.js +430 -0
  198. package/dist/services/memory-upgrade-start.js +60 -0
  199. package/dist/services/memory-upgrade-storage-readiness.js +340 -0
  200. package/dist/services/memory-verification-acceptance.js +38 -17
  201. package/dist/services/memory-verification-checks.js +92 -27
  202. package/dist/services/memory-verification-receipt.js +6 -5
  203. package/dist/services/memory-verify-handler.js +15 -13
  204. package/dist/services/memory-work-result-json.js +12 -0
  205. package/dist/services/memory-work-result-preparation.js +5 -4
  206. package/dist/services/merge-server.js +4 -2
  207. package/dist/services/migrations.js +127 -98
  208. package/dist/services/native-daemon-history.js +2 -1
  209. package/dist/services/physical-startup.js +45 -0
  210. package/dist/services/project-registry.js +97 -0
  211. package/dist/services/project-summary.js +1 -1
  212. package/dist/services/projects.js +3 -93
  213. package/dist/services/prompt-delivery.js +71 -0
  214. package/dist/services/provider-capacity.js +112 -46
  215. package/dist/services/recovery-snapshot-database.js +32 -4
  216. package/dist/services/release-cache.js +8 -0
  217. package/dist/services/repair-intents.js +1 -12
  218. package/dist/services/repair-receipt.js +13 -0
  219. package/dist/services/run-control-worker.js +35 -18
  220. package/dist/services/run-control.js +2 -1
  221. package/dist/services/run-controller-adapter.js +2 -1
  222. package/dist/services/run-controller-process.js +4 -7
  223. package/dist/services/run-controller-recovery.js +8 -2
  224. package/dist/services/run-controller.js +215 -94
  225. package/dist/services/run-fork.js +1 -1
  226. package/dist/services/run-recovery-guard.js +30 -0
  227. package/dist/services/run-recovery.js +11 -44
  228. package/dist/services/run-start-custody.js +17 -5
  229. package/dist/services/runs.js +6 -1
  230. package/dist/services/runtime-scope-resume.js +12 -6
  231. package/dist/services/runtime-scope-stop.js +3 -2
  232. package/dist/services/runtime-scope-worker.js +46 -19
  233. package/dist/services/runtime-service.js +2 -1
  234. package/dist/services/sessions.js +3 -24
  235. package/dist/services/setup.js +63 -13
  236. package/dist/services/stage-handlers.js +22 -7
  237. package/dist/services/stage-pause.js +111 -44
  238. package/dist/services/version-status.js +1 -1
  239. package/dist/services/vnext-code.js +2 -2
  240. package/dist/services/vnext-fanout.js +51 -9
  241. package/dist/services/vnext-plan-review.js +3 -5
  242. package/dist/services/work-registry.js +117 -39
  243. package/dist/services/workspace-bootstrap.js +2 -1
  244. package/dist/services/workspace-files.js +1 -1
  245. package/dist/shared/entity-references.js +1 -8
  246. package/dist/storage/data-preservation.js +65 -0
  247. package/dist/storage/database.js +53 -19
  248. package/dist/storage/import-guard.js +84 -27
  249. package/dist/storage/memory-upgrade-schema-bootstrap.js +38 -0
  250. package/dist/storage/paired-writer-migration.js +424 -0
  251. package/dist/storage/writer-contract.js +11 -4
  252. package/dist/storage/writer-migration.js +313 -52
  253. package/dist/storage/writer-restore.js +217 -0
  254. package/package.json +13 -6
@@ -16,7 +16,7 @@ import { reviewExecutionCopyHash, reviewSourceFilter, reviewSourceHash, reviewSo
16
16
  import { workspaceGitFiles } from "./workspace-files.js";
17
17
  import { qualifyAgyHooks } from "../harness-runtime/lib/dd-agy-daemon.mjs";
18
18
  import { lifecycleRetryInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
19
- import { continueProviderCapacity } from "./provider-capacity.js";
19
+ import { continueProviderCapacity, ownedProviderBoundary } from "./provider-capacity.js";
20
20
  import { errorRecord } from "./run-controller-state.js";
21
21
  import { readFlowRunExecutionBundle } from "./runs.js";
22
22
  import { stageWorkIds } from "./stage-work-graph.js";
@@ -79,7 +79,7 @@ export async function launchExternalWork(context, input) {
79
79
  catch { /* Legacy non-memory payload handling remains owned by the Work registry. */ }
80
80
  const repairWave = payload?.kind === "memory-repair" ? payload.memory_repair?.wave : undefined;
81
81
  if (payload?.kind === "memory-repair" && (payload.read_only !== false || ![1, 2].includes(repairWave ?? 0)
82
- || bundle?.descriptor.descriptor.id !== "mb-init" || stage !== "review")) {
82
+ || (bundle?.descriptor.descriptor.id !== "mb-init" && bundle?.descriptor.descriptor.id !== "mb-upgrade") || stage !== "review")) {
83
83
  throw new AppError("memory_repair_launch_invalid", "Repair dispatch requires its registered memory review wave", 2, { phase: "prepare", effect: "no_effect" });
84
84
  }
85
85
  const repairScope = { projectRoot: run.project_root, runId: work.run_id, workId: work.work_id };
@@ -214,7 +214,9 @@ export async function launchExternalWork(context, input) {
214
214
  if (reviewCopy && reviewSourceHash(run.workspace_root, reviewInputs, reviewBase) !== reviewCopy.source_sha256)
215
215
  throw new AppError("read_only_input_changed", "Authoritative review workspace changed during capacity wait", 2);
216
216
  },
217
- decision: () => {
217
+ decision: (boundary) => {
218
+ if (boundary && (!stage || !ownedProviderBoundary(context, { projectId: work.project_id, runId: work.run_id, stage, sessionId, generation, workId: work.work_id, scope: "work" }, boundary)))
219
+ return "reconciliation_blocked";
218
220
  const status = context.db.get("SELECT status FROM works WHERE work_id = ?", [work.work_id])?.status;
219
221
  return status === "completed" ? "completed" : status === "paused" ? "paused" : "continue";
220
222
  },
@@ -1,6 +1,7 @@
1
1
  import { vnextStages } from "../domain/stage-catalog.js";
2
- import { sdlcHandlerIdentities, memoryInitHandlerIdentities } from "../domain/stage-handler-identities.js";
2
+ import { sdlcHandlerIdentities, memoryInitHandlerIdentities, memoryUpgradeHandlerIdentities } from "../domain/stage-handler-identities.js";
3
3
  import { AppError } from "../shared/errors.js";
4
+ import { diagnosticFlowStages } from "../domain/diagnostic-flows.js";
4
5
  /** Engine-owned catalog, not descriptor discovery or permission to execute a RUN. */
5
6
  export function listBundledFlows() {
6
7
  return [
@@ -8,9 +9,17 @@ export function listBundledFlows() {
8
9
  stages: vnextStages.map(stage => ({ id: stage.id, directory: stage.directory,
9
10
  entry_handler: sdlcHandlerIdentities[stage.id].entryId, finish_handler: sdlcHandlerIdentities[stage.id].finishId })),
10
11
  edges: vnextStages.flatMap(stage => stage.next.map((to, index) => ({ from: stage.id, to, on: index ? "not-applicable" : "accepted" }))) },
11
- { id: "mb-init", handler_status: "registered", entry_stage: "preflight", terminal_stages: ["finish"],
12
- stages: Object.values(memoryInitHandlerIdentities).map((handler, index) => ({ id: handler.stage, directory: `${String(index + 1).padStart(2, "0")}-${handler.stage}`, entry_handler: handler.entryId, finish_handler: handler.finishId })),
13
- edges: ["preflight", "discover", "compose", "verify", "review"].flatMap((from, index) => [{ from, to: ["discover", "compose", "verify", "review", "finish"][index], on: "accepted" }, ...(from === "verify" ? [{ from, to: "review", on: "needs_repair" }] : [])]) }
12
+ ...[{ id: "mb-init", handlers: memoryInitHandlerIdentities }, { id: "mb-upgrade", handlers: memoryUpgradeHandlerIdentities }].map(({ id, handlers }) => {
13
+ const stages = Object.values(handlers);
14
+ return { id, handler_status: "registered", entry_stage: "preflight", terminal_stages: ["finish"],
15
+ stages: stages.map((handler, index) => ({ id: handler.stage, directory: `${String(index + 1).padStart(2, "0")}-${handler.stage}`, entry_handler: handler.entryId, finish_handler: handler.finishId })),
16
+ edges: stages.slice(0, -1).flatMap((handler, index) => [{ from: handler.stage, to: stages[index + 1].stage, on: "accepted" },
17
+ ...(handler.stage === "verify" ? [{ from: handler.stage, to: "review", on: "needs_repair" }] : [])]) };
18
+ }),
19
+ ...Object.entries(diagnosticFlowStages).map(([id, stages]) => ({ id, handler_status: "registered", entry_stage: "preflight", terminal_stages: ["report"],
20
+ stages: stages.map((stage, index) => ({ id: stage, directory: `${String(index + 1).padStart(2, "0")}-${stage}`,
21
+ entry_handler: "diagnostic-stage-entry", finish_handler: "diagnostic-stage-finish" })),
22
+ edges: stages.slice(0, -1).map((from, index) => ({ from, to: stages[index + 1], on: "accepted" })) }))
14
23
  ];
15
24
  }
16
25
  export function showBundledFlow(id) {
@@ -11,8 +11,10 @@ import { hybridDigest } from "./hybrid-contracts.js";
11
11
  import { isMemoryInitResourceStorageFacts } from "./memory-init-resource-storage.js";
12
12
  import { memoryInitStartupInputSchemas } from "./memory-init-startup-custody.js";
13
13
  import { SETUP_STARTUP_MS } from "../harness-runtime/lib/setup-progress.mjs";
14
+ import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
14
15
  function storageProbeRefuse(reason, probe) {
15
- throw new AppError("frozen_engine_storage_incompatible", "Selected frozen engine cannot admit memory-init storage", 1, { reason, effect: "no_effect", ...(probe ? { probe } : {}) });
16
+ const summary = probe ? ` (${probe.error_code ?? probe.signal ?? `exit ${probe.status}`})` : "";
17
+ throw new AppError("frozen_engine_storage_incompatible", `Selected frozen engine cannot admit memory-flow storage${summary}`, 1, { reason, effect: "no_effect", ...(probe ? { probe } : {}) });
16
18
  }
17
19
  function probeFailure(result) {
18
20
  const code = result.error?.code ?? null;
@@ -75,6 +77,37 @@ export class AdmittedFrozenEngine {
75
77
  refuse("invalid_selection_identity");
76
78
  return AdmittedFrozenEngine.fromIdentity(identity, expectedManifestSha256);
77
79
  }
80
+ /** Physical relocation preserves the original manifest as provenance. It
81
+ * cannot authorize a different executable or different semantic metadata. */
82
+ static admitRelocation(binding, retained, expectedSha256, target) {
83
+ if (hash(retained) !== expectedSha256 || !shape("run-engine-binding", binding)
84
+ || !shape("run-engine-binding", target) || binding.run_id !== target.run_id)
85
+ refuse("invalid_relocation_identity");
86
+ const value = json(retained);
87
+ if (!shape("engine-manifest", value))
88
+ refuse("invalid_manifest_shape");
89
+ const original = value;
90
+ if (original.snapshot_root !== binding.engine.snapshot_root
91
+ || original.package_name !== binding.engine.package_name || original.package_version !== binding.engine.package_version
92
+ || original.engine_version !== binding.engine.engine_version || original.integrity.checksum !== binding.engine.integrity_checksum
93
+ || Object.entries(binding.engine).some(([key, value]) => key !== "snapshot_root" && value !== target.engine[key])) {
94
+ refuse("foreign_relocation_identity");
95
+ }
96
+ try {
97
+ const bytes = readBoundedReleaseFile(path.join(target.engine.snapshot_root, "engine.json"), 1024 * 1024);
98
+ const selected = AdmittedFrozenEngine.admit(target, hash(bytes));
99
+ const semantic = (manifest) => Object.fromEntries(Object.entries(manifest)
100
+ .filter(([key]) => !["snapshot_root", "package_root", "installed_at", "install_source"].includes(key)));
101
+ if (hybridDigest(semantic(original)) !== hybridDigest(semantic(selected.manifest())))
102
+ refuse("relocated_manifest_changed");
103
+ return selected;
104
+ }
105
+ catch (error) {
106
+ if (error instanceof AppError && error.code === "frozen_engine_invalid")
107
+ throw error;
108
+ return refuse("snapshot_unavailable_or_unreadable");
109
+ }
110
+ }
78
111
  static fromIdentity(identity, expectedManifestSha256) {
79
112
  try {
80
113
  const root = path.resolve(identity.snapshot_root);
@@ -119,8 +152,17 @@ export class AdmittedFrozenEngine {
119
152
  /** Qualify actual descriptor handlers in this immutable executable, not the
120
153
  * invoking CLI catalog. No storage or productive operation is admitted. */
121
154
  admitMemoryInitFlow(source, startupInputSchema) {
122
- const fail = (reason, probe) => {
123
- throw new AppError("frozen_engine_flow_incompatible", "Selected frozen engine cannot admit memory-init handlers", 1, { reason, phase: "prepare", effect: "no_effect", ...(probe ? { probe } : {}) });
155
+ return this.#admitMemoryFlow(source, "mb-init", startupInputSchema);
156
+ }
157
+ admitMemoryUpgradeFlow(source) {
158
+ return this.#admitMemoryFlow(source, "mb-upgrade");
159
+ }
160
+ admitDiagnosticFlow(source, flowId) {
161
+ return this.#admitMemoryFlow(source, flowId);
162
+ }
163
+ #admitMemoryFlow(source, flowId, startupInputSchema) {
164
+ const fail = (reason, details = {}) => {
165
+ throw new AppError("frozen_engine_flow_incompatible", `Selected frozen engine cannot admit ${flowId} handlers`, 1, { ...details, reason, phase: "prepare", effect: "no_effect" });
124
166
  };
125
167
  const revalidate = () => {
126
168
  try {
@@ -146,18 +188,25 @@ export class AdmittedFrozenEngine {
146
188
  ...(startupInputSchema === undefined ? {} : { startup_input_schema: startupInputSchema }) }));
147
189
  if (input.length > 64 * 1024 * 1024)
148
190
  fail("unbounded_source");
149
- const expected = { schema_id: startupInputSchema === undefined ? "dd-flow/memory-init-engine-flow@1" : "dd-flow/memory-init-engine-flow@2",
191
+ const flow = source.resolveRegisteredFlow();
192
+ if (flow.descriptor.id !== flowId)
193
+ fail("foreign_flow_source");
194
+ const expected = { schema_id: isDiagnosticFlow(flowId) ? "dd-flow/diagnostic-engine-flow@1" : flowId === "mb-upgrade" ? "dd-flow/memory-upgrade-engine-flow@1"
195
+ : startupInputSchema === undefined ? "dd-flow/memory-init-engine-flow@1" : "dd-flow/memory-init-engine-flow@2",
150
196
  ...(startupInputSchema === undefined ? {} : { startup_input_schema: startupInputSchema }),
151
- flow_id: "mb-init", source_sha256: source.sha256,
152
- descriptor_sha256: source.descriptorReference().sha256, stages: source.resolveRegisteredFlow().descriptor.stages.map(stage => stage.id) };
197
+ flow_id: flowId, source_sha256: source.sha256,
198
+ descriptor_sha256: source.descriptorReference().sha256, stages: flow.descriptor.stages.map(stage => stage.id) };
153
199
  revalidate();
154
200
  const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !["NODE_OPTIONS", "NODE_PATH"].includes(key) && !key.startsWith("DD_FLOW_")));
155
- const result = spawnSync(this.nodeExecutable, [this.entrypoint, "runtime", "flow", "admit", "--operation", "memory-init", "--json"], { input, cwd: this.#manifest.snapshot_root, env: { ...env, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: this.#manifest.snapshot_root },
201
+ const started = performance.now();
202
+ const result = spawnSync(this.nodeExecutable, [this.entrypoint, "runtime", "flow", "admit", "--operation", isDiagnosticFlow(flowId) ? flowId : flowId === "mb-init" ? "memory-init" : "memory-upgrade", "--json"], { input, cwd: this.#manifest.snapshot_root, env: { ...env, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: this.#manifest.snapshot_root },
156
203
  // This is cold startup of a selected CLI, not an already-loaded handler.
157
204
  // Admission is bounded local validation; no provider operation is started.
158
205
  timeout: SETUP_STARTUP_MS, maxBuffer: 1024 * 1024, shell: false, windowsHide: true });
159
206
  if (result.error || result.signal || result.status !== 0)
160
- fail("selected_engine_probe_failed", probeFailure(result));
207
+ fail("selected_engine_probe_failed", {
208
+ ...probeFailure(result), operation: flowId, elapsed_ms: Math.round(performance.now() - started), probe: probeFailure(result)
209
+ });
161
210
  revalidate();
162
211
  let response;
163
212
  try {
@@ -178,6 +227,91 @@ export class AdmittedFrozenEngine {
178
227
  * This qualifies storage connections only, not provider or flow readiness.
179
228
  */
180
229
  admitMemoryInitStorage(home, resourceHome) {
230
+ return this.admitMemoryStorage(home, resourceHome, "memory-init");
231
+ }
232
+ /** A distinct operation: old engines cannot accidentally claim support for
233
+ * upgrade request custody merely because init storage admission succeeds. */
234
+ admitMemoryUpgradeStorage(home, resourceHome) {
235
+ return this.admitMemoryStorage(home, resourceHome, "memory-upgrade");
236
+ }
237
+ /** Execute storage transition in this exact snapshot before AppContext opens
238
+ * a writer. A timeout or malformed acknowledgement never proves no effect. */
239
+ qualifyMemoryUpgradeStorage(home, resourceHome) {
240
+ const intact = () => hash(readBoundedReleaseFile(path.join(this.#manifest.snapshot_root, "engine.json"), 1024 * 1024)) === this.manifestSha256
241
+ && engineSnapshotChecksum(this.#manifest.snapshot_root) === this.#manifest.integrity.checksum;
242
+ if (!intact())
243
+ refuse("engine_changed_before_storage_qualification");
244
+ const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !["NODE_OPTIONS", "NODE_PATH"].includes(key)));
245
+ const reply = spawnSync(this.nodeExecutable, [this.entrypoint, "runtime", "storage", "qualify", "--operation", "memory-upgrade", "--json"], { env: { ...env, DD_FLOW_HOME: home, DD_FLOW_RESOURCE_HOME: resourceHome, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: this.#manifest.snapshot_root },
246
+ encoding: "utf8", timeout: 120000, maxBuffer: 1024 * 1024, shell: false, windowsHide: true });
247
+ if (!intact())
248
+ refuse("engine_changed_during_storage_qualification");
249
+ if (reply.error || reply.signal || reply.status !== 0)
250
+ storageProbeRefuse("selected_engine_qualification_failed", { status: reply.status, signal: reply.signal });
251
+ let response;
252
+ try {
253
+ response = JSON.parse(reply.stdout);
254
+ }
255
+ catch {
256
+ return storageProbeRefuse("invalid_qualification_json");
257
+ }
258
+ const facts = response?.facts;
259
+ if (response?.ok !== true || response.action !== "runtime_storage_qualify" || facts?.schema_id !== "dd-flow/memory-upgrade-storage-capability@1"
260
+ || facts.operation !== "memory-upgrade" || !["verified-no-op", "migration"].includes(facts.route) || facts.productive_admission !== false
261
+ || hybridDigest(facts.source_stores) !== hybridDigest([path.join(home, "db.sqlite"), path.join(resourceHome, "runtime.sqlite")])
262
+ || response.sha256 !== hybridDigest(facts))
263
+ storageProbeRefuse("invalid_qualification_facts");
264
+ return { facts, sha256: response.sha256, engine_manifest_sha256: this.manifestSha256, engine_sha256: this.#manifest.integrity.checksum };
265
+ }
266
+ transitionMemoryUpgradeStorage(input) {
267
+ const intact = () => hash(readBoundedReleaseFile(path.join(this.#manifest.snapshot_root, "engine.json"), 1024 * 1024)) === this.manifestSha256
268
+ && engineSnapshotChecksum(this.#manifest.snapshot_root) === this.#manifest.integrity.checksum;
269
+ if (!intact())
270
+ refuse("engine_changed_before_storage_transition");
271
+ const args = ["runtime", "storage", "transition", "--action", input.action, "--project-root", input.scope.projectRoot,
272
+ "--workspace-root", input.scope.workspaceRoot, "--request-id", input.scope.requestId, "--selection-sha256", input.selectionSha256,
273
+ ...(input.expectedSha256 ? ["--expected-sha256", input.expectedSha256] : []), ...(input.recoveryId ? ["--recovery-id", input.recoveryId] : []),
274
+ ...(input.offline ? ["--offline"] : []), "--json"];
275
+ const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !["NODE_OPTIONS", "NODE_PATH"].includes(key)));
276
+ const reply = spawnSync(this.nodeExecutable, [this.entrypoint, ...args], { env: { ...env, DD_FLOW_HOME: input.home, DD_FLOW_RESOURCE_HOME: input.resourceHome,
277
+ DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: this.#manifest.snapshot_root }, encoding: "utf8", timeout: 120000, maxBuffer: 1024 * 1024, shell: false, windowsHide: true });
278
+ const fail = (reason, details = {}) => {
279
+ throw new AppError("frozen_engine_storage_transition_failed", "Observe or reconcile the original selected-engine storage request", 1, { reason, effect: "unknown", ...details });
280
+ };
281
+ let unchanged = false;
282
+ try {
283
+ unchanged = intact();
284
+ }
285
+ catch { /* Committed effects remain unknown if code custody is lost. */ }
286
+ if (!unchanged)
287
+ fail("engine_changed_during_storage_transition");
288
+ if (reply.error || reply.signal || reply.status !== 0) {
289
+ let child = {};
290
+ try {
291
+ child = JSON.parse(reply.stderr);
292
+ }
293
+ catch { /* Unknown acknowledgements retain unknown effect. */ }
294
+ fail("selected_engine_transition_failed", { status: reply.status, signal: reply.signal, error_code: reply.error?.code ?? null,
295
+ child_code: child.error?.code ?? null, child_reason: child.error?.details?.reason ?? null, effect: child.error?.details?.effect === "no_effect" ? "no_effect" : "unknown" });
296
+ }
297
+ let response;
298
+ try {
299
+ response = JSON.parse(reply.stdout);
300
+ }
301
+ catch {
302
+ return fail("invalid_transition_json");
303
+ }
304
+ if (!response || typeof response !== "object" || Array.isArray(response))
305
+ fail("invalid_transition_response");
306
+ const selected = response.engine;
307
+ if (response.ok !== true || response.action !== "runtime_storage_transition" || response.operation !== input.action
308
+ || !response.scope || typeof response.scope !== "object" || Array.isArray(response.scope)
309
+ || hybridDigest(response.scope) !== hybridDigest(input.scope) || response.selection_sha256 !== input.selectionSha256
310
+ || selected?.manifest_sha256 !== this.manifestSha256 || selected.sha256 !== this.#manifest.integrity.checksum)
311
+ fail("foreign_transition_acknowledgement");
312
+ return response;
313
+ }
314
+ admitMemoryStorage(home, resourceHome, operation) {
181
315
  for (const root of [home, resourceHome]) {
182
316
  if (!path.isAbsolute(root) || path.resolve(root) !== root || fs.realpathSync(root) !== root)
183
317
  storageProbeRefuse("noncanonical_storage_home");
@@ -189,10 +323,13 @@ export class AdmittedFrozenEngine {
189
323
  };
190
324
  revalidateEngine();
191
325
  const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !["NODE_OPTIONS", "NODE_PATH"].includes(key)));
192
- const result = spawnSync(this.nodeExecutable, [this.entrypoint, "runtime", "storage", "admit", "--operation", "memory-init", "--json"], { env: { ...env, DD_FLOW_HOME: home, DD_FLOW_RESOURCE_HOME: resourceHome, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: this.#manifest.snapshot_root },
326
+ const started = performance.now();
327
+ const result = spawnSync(this.nodeExecutable, [this.entrypoint, "runtime", "storage", "admit", "--operation", operation, "--json"], { env: { ...env, DD_FLOW_HOME: home, DD_FLOW_RESOURCE_HOME: resourceHome, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: this.#manifest.snapshot_root },
193
328
  timeout: SETUP_STARTUP_MS, maxBuffer: 1024 * 1024, shell: false, windowsHide: true });
194
329
  if (result.error || result.signal || result.status !== 0)
195
- storageProbeRefuse("selected_engine_probe_failed", probeFailure(result));
330
+ storageProbeRefuse("selected_engine_probe_failed", {
331
+ ...probeFailure(result), operation, elapsed_ms: Math.round(performance.now() - started)
332
+ });
196
333
  revalidateEngine();
197
334
  let response;
198
335
  try {
@@ -206,7 +343,7 @@ export class AdmittedFrozenEngine {
206
343
  storageProbeRefuse("invalid_probe_response");
207
344
  const facts = response.facts, run = facts?.run;
208
345
  if (!facts || Object.keys(facts).sort().join() !== "operation,outcome,resource,route,run,schema_id"
209
- || facts.schema_id !== "dd-flow/memory-init-engine-storage@1" || facts.operation !== "memory-init"
346
+ || facts.schema_id !== `dd-flow/${operation}-engine-storage@1` || facts.operation !== operation
210
347
  || facts.route !== "verified-no-op" || facts.outcome !== "no-op" || response.sha256 !== hybridDigest(facts)
211
348
  || !isMemoryInitResourceStorageFacts(facts.resource) || facts.resource.store_path !== path.join(resourceHome, "runtime.sqlite")
212
349
  || !run || Object.keys(run).sort().join() !== "executing_writer_version,operation,outcome,route,schema_id,schema_sha256,store_dev,store_ino,store_kind,store_path"
@@ -0,0 +1,188 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { AppError } from "../shared/errors.js";
5
+ import { hybridDigest } from "./hybrid-digest.js";
6
+ import { assertRunMutationAllowed } from "./run-recovery-guard.js";
7
+ function repairRefuse(reason) { throw new AppError("memory_repair_native_binding_invalid", "Native repair requires its actual direct-child Hook and independently owned copy", 1, { reason, effect: "no_effect" }); }
8
+ function reviewRefuse(reason) { throw new AppError("memory_native_review_copy_invalid", "Native Memory review requires an independently owned immutable execution copy", 1, { reason, effect: "no_effect" }); }
9
+ export function memoryReviewPhysicalDestination(runRoot, relative) {
10
+ if (fs.realpathSync(runRoot) !== path.resolve(runRoot) || !fs.lstatSync(runRoot).isDirectory())
11
+ reviewRefuse("noncanonical_run_root");
12
+ let current = runRoot;
13
+ for (const part of relative.split("/")) {
14
+ if (!part || part === "." || part === "..")
15
+ reviewRefuse("invalid_copy_path");
16
+ current = path.join(current, part);
17
+ const stat = fs.lstatSync(current, { throwIfNoEntry: false });
18
+ if (stat && !stat.isDirectory())
19
+ reviewRefuse("linked_or_non_directory_copy_path");
20
+ if (!stat)
21
+ break;
22
+ }
23
+ }
24
+ /** Hook ingress selects only an independently prepared repair copy. This
25
+ * establishes a command's assigned directory, not claim/repair authority. */
26
+ export function memoryRepairAssignedHookCwd(context, input) {
27
+ const work = context.db.get("SELECT payload_json,result_schema,status FROM works WHERE project_id=? AND run_id=? AND work_id=?", [input.projectId, input.runId, input.workId]);
28
+ let payload;
29
+ try {
30
+ payload = JSON.parse(work?.payload_json ?? "null");
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ if (payload?.kind !== "memory-repair" && work?.result_schema !== "dd-flow/memory-repair-work-result@1")
36
+ return null;
37
+ const run = context.db.get("SELECT r.run_root,r.run_dir,r.flow_kind,p.root AS project_root FROM runs r JOIN projects p ON p.id=r.project_id WHERE r.project_id=? AND r.id=?", [input.projectId, input.runId]);
38
+ if (!run || (run.flow_kind !== "mb-init" && run.flow_kind !== "mb-upgrade") || run.project_root !== input.projectRoot || payload.read_only !== false
39
+ || ![1, 2].includes(payload.memory_repair?.wave ?? 0) || work?.result_schema !== "dd-flow/memory-repair-work-result@1")
40
+ repairRefuse("registered_memory_repair_required");
41
+ const generation = assertRunMutationAllowed(context, input.projectId, input.runId), runRoot = run.run_root ?? run.run_dir;
42
+ const external = context.db.get("SELECT 1 FROM sqlite_schema WHERE type='table' AND name='work_launches'")
43
+ ? context.db.get("SELECT operation_id,status,profile_json,profile_hash,provider_session_id FROM work_launches WHERE work_id=?", [input.workId]) : undefined;
44
+ const native = !external;
45
+ const admitted = (root) => {
46
+ if (native && input.observedCwd !== root && input.literalExecutionCwd !== root)
47
+ throw new AppError("memory_repair_native_copy_route_unavailable", "Native repair requires a genuinely observed child execution directory inside its owned writer copy", 1, { phase: "prepare", effect: "no_effect", recoverable: false, expected: root, actual: input.observedCwd ?? null,
48
+ next_action: "Use only a documented native child/tool capability that establishes this exact directory; otherwise stop and select external delegation in a new RUN." });
49
+ return { root, native };
50
+ };
51
+ if (work.status === "created") {
52
+ // Routing consumes registered copy custody only. Actual Work start owns
53
+ // physical source/copy/profile admission after the durable native Hook.
54
+ const rows = (eventType) => context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type=? AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.work_id')=? AND json_extract(payload_json,'$.generation')=? LIMIT 2", [input.projectId, eventType, input.runId, input.workId, generation]);
55
+ const intents = rows("memory.repair_writer_copy_intent"), copies = rows("memory.repair_writer_copy_ready");
56
+ if (intents.length !== 1 || copies.length !== 1 || Buffer.byteLength(intents[0].payload_json) > 1024 * 1024
57
+ || Buffer.byteLength(copies[0].payload_json) > 12 * 1024 * 1024)
58
+ repairRefuse("owned_ready_copy_required");
59
+ try {
60
+ const intent = JSON.parse(intents[0].payload_json), ready = JSON.parse(copies[0].payload_json);
61
+ const workspace_manifest_sha256 = ready.workspace_manifest_sha256, readyIntent = { ...ready };
62
+ delete readyIntent.workspace_manifest_sha256;
63
+ delete readyIntent.workspace_manifest_base64;
64
+ const relative = `works/${input.workId}/writer-workspaces/generation-${generation}`;
65
+ if (intent.schema_id !== "dd-flow/memory-repair-writer-copy-intent@1" || intent.project_id !== input.projectId || intent.run_id !== input.runId
66
+ || intent.work_id !== input.workId || intent.generation !== generation || intent.run_root !== runRoot || !path.isAbsolute(runRoot)
67
+ || intent.copy_relative !== relative || !/^[a-f0-9]{64}$/.test(intent.baseline_registration_sha256)
68
+ || !/^[a-f0-9]{64}$/.test(workspace_manifest_sha256) || hybridDigest(readyIntent) !== hybridDigest(intent))
69
+ repairRefuse("owned_ready_copy_required");
70
+ if (external) {
71
+ if (external.provider_session_id !== input.providerSessionId)
72
+ repairRefuse("foreign_physical_session");
73
+ const profile = JSON.parse(external.profile_json), metadata = profile.memory_repair_copy;
74
+ if (profile.delegation?.mode !== "external" || hybridDigest(metadata) !== hybridDigest({ root: relative, copy_intent_sha256: hybridDigest(intent),
75
+ workspace_manifest_sha256, baseline_registration_sha256: intent.baseline_registration_sha256 }))
76
+ repairRefuse("native_copy_cwd_mismatch");
77
+ }
78
+ if (fs.realpathSync(runRoot) !== path.resolve(runRoot))
79
+ repairRefuse("owned_ready_copy_required");
80
+ let destination = runRoot;
81
+ for (const part of relative.split('/')) {
82
+ destination = path.join(destination, part);
83
+ if (!fs.lstatSync(destination).isDirectory())
84
+ repairRefuse("owned_ready_copy_required");
85
+ }
86
+ return admitted(destination);
87
+ }
88
+ catch (error) {
89
+ if (error instanceof AppError)
90
+ throw error;
91
+ return repairRefuse("owned_ready_copy_required");
92
+ }
93
+ }
94
+ // Running writers retain their committed directory, not today's source bytes.
95
+ let binding = external;
96
+ let nativeStart;
97
+ if (!binding) {
98
+ const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='memory.repair_native_binding' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.work_id')=? LIMIT 2", [input.projectId, input.runId, input.workId]);
99
+ if (rows.length !== 1 || Buffer.byteLength(rows[0].payload_json) > 1024 * 1024)
100
+ repairRefuse("native_registered_start_changed");
101
+ try {
102
+ const retained = JSON.parse(rows[0].payload_json);
103
+ if (retained.schema_id !== "dd-flow/memory-repair-native-binding@1" || retained.project_id !== input.projectId || retained.run_id !== input.runId || retained.work_id !== input.workId
104
+ || typeof retained.work_session_id !== "string" || typeof retained.hook_event_key !== "string")
105
+ repairRefuse("native_registered_start_changed");
106
+ binding = retained.binding;
107
+ nativeStart = retained;
108
+ }
109
+ catch (error) {
110
+ if (error instanceof AppError)
111
+ throw error;
112
+ return repairRefuse("native_registered_start_changed");
113
+ }
114
+ }
115
+ const starts = context.db.all("SELECT ws.id,ws.hook_event_id,ws.start_receipt_json FROM work_sessions ws JOIN sessions s ON s.project_id=? AND s.session_id=ws.session_id WHERE ws.work_id=? AND ws.status='running' AND s.run_id=? AND s.worker_id=? AND s.provider_session_id=? LIMIT 2", [input.projectId, input.workId, input.runId, input.workId, input.providerSessionId]);
116
+ if (work.status !== "running" || !binding || binding.provider_session_id !== input.providerSessionId || starts.length !== 1)
117
+ repairRefuse("native_registered_start_changed");
118
+ if (nativeStart && (nativeStart.work_session_id !== starts[0].id || nativeStart.hook_event_key !== starts[0].hook_event_id
119
+ || binding.operation_id !== `native-hook:${nativeStart.hook_event_key}`
120
+ || !context.db.get("SELECT 1 FROM hook_events WHERE project_id=? AND event_key=? AND status='claimed' AND provider_session_id=?", [input.projectId, nativeStart.hook_event_key, input.providerSessionId])))
121
+ repairRefuse("native_registered_start_changed");
122
+ try {
123
+ const text = starts[0].start_receipt_json;
124
+ if (typeof text !== "string" || Buffer.byteLength(text) > 16 * 1024 * 1024)
125
+ repairRefuse("native_registered_start_changed");
126
+ const start = JSON.parse(text), packet = JSON.parse(start.context), profile = JSON.parse(binding.profile_json), metadata = profile.memory_repair_copy;
127
+ if (profile.delegation?.mode !== (native ? "native" : "external") || start.version !== 2 || createHash("sha256").update(start.context).digest("hex") !== start.context_sha256
128
+ || typeof metadata?.root !== "string" || metadata.root !== `works/${input.workId}/writer-workspaces/generation-${start.generation}`
129
+ || packet.work_id !== input.workId || packet.run_id !== input.runId || packet.workspace_root !== path.join(runRoot, metadata.root)
130
+ || packet.memory_repair_input?.provider_session_id !== input.providerSessionId
131
+ || packet.memory_repair_input?.launch_operation_id !== binding.operation_id)
132
+ repairRefuse("native_registered_start_changed");
133
+ return admitted(packet.workspace_root);
134
+ }
135
+ catch (error) {
136
+ if (error instanceof AppError)
137
+ throw error;
138
+ return repairRefuse("native_registered_start_changed");
139
+ }
140
+ }
141
+ /** Routing is not fresh source admission: an already claimed reviewer retains
142
+ * its committed private directory even after a later correction changes MB. */
143
+ export function memoryNativeReviewAssignedHookCwd(context, input) {
144
+ const work = context.db.get("SELECT status,payload_json FROM works WHERE project_id=? AND run_id=? AND work_id=?", [input.projectId, input.runId, input.workId]);
145
+ const payload = JSON.parse(work?.payload_json ?? "null");
146
+ if (payload?.kind !== "memory-review" || payload.read_only !== true)
147
+ return null;
148
+ const ready = context.db.get("SELECT 1 FROM audit_events WHERE project_id=? AND event_type='memory.native_review_copy_ready' AND json_extract(payload_json,'$.intent.run_id')=? AND json_extract(payload_json,'$.intent.work_id')=?", [input.projectId, input.runId, input.workId]);
149
+ if (!ready)
150
+ return null;
151
+ if (work?.status === "created") {
152
+ // Hook ingress routes an already allocated copy. Source admission belongs
153
+ // to the actual work-start owner, not a second physical graph in the Hook.
154
+ const generation = assertRunMutationAllowed(context, input.projectId, input.runId);
155
+ const run = context.db.get("SELECT run_root,run_dir FROM runs WHERE project_id=? AND id=?", [input.projectId, input.runId]);
156
+ if (!run)
157
+ reviewRefuse("owned_run_required");
158
+ const rows = (event) => context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type=? AND COALESCE(json_extract(payload_json,'$.run_id'),json_extract(payload_json,'$.intent.run_id'))=? AND COALESCE(json_extract(payload_json,'$.work_id'),json_extract(payload_json,'$.intent.work_id'))=? AND COALESCE(json_extract(payload_json,'$.generation'),json_extract(payload_json,'$.intent.generation'))=? LIMIT 2", [input.projectId, event, input.runId, input.workId, generation]);
159
+ const intents = rows("memory.native_review_copy_intent"), copies = rows("memory.native_review_copy_ready");
160
+ if (intents.length !== 1 || copies.length !== 1)
161
+ reviewRefuse("registered_copy_required");
162
+ const intent = JSON.parse(intents[0].payload_json), copy = JSON.parse(copies[0].payload_json);
163
+ const relative = `works/${input.workId}/native-review-workspaces/generation-${generation}`, runRoot = run.run_root ?? run.run_dir;
164
+ if (intent.schema_id !== "dd-flow/memory-native-review-copy@1" || intent.project_id !== input.projectId || intent.run_id !== input.runId
165
+ || intent.work_id !== input.workId || intent.generation !== generation || intent.root !== relative
166
+ || !/^[a-f0-9]{64}$/.test(intent.input_sha256) || !/^[a-f0-9]{64}$/.test(intent.source_sha256)
167
+ || hybridDigest(copy.intent) !== hybridDigest(intent) || !/^[a-f0-9]{64}$/.test(copy.sha256))
168
+ reviewRefuse("registered_copy_changed");
169
+ memoryReviewPhysicalDestination(runRoot, relative);
170
+ const root = path.join(runRoot, relative);
171
+ if (!fs.lstatSync(root, { throwIfNoEntry: false })?.isDirectory())
172
+ reviewRefuse("registered_copy_required");
173
+ return root;
174
+ }
175
+ const rows = context.db.all("SELECT ws.start_receipt_json,r.run_root,r.run_dir,s.cwd FROM work_sessions ws JOIN works w ON w.work_id=ws.work_id JOIN runs r ON r.project_id=w.project_id AND r.id=w.run_id JOIN sessions s ON s.project_id=w.project_id AND s.session_id=ws.session_id WHERE w.project_id=? AND w.run_id=? AND w.work_id=? AND s.provider_session_id=? AND s.worker_id=w.work_id AND EXISTS(SELECT 1 FROM audit_events a WHERE a.project_id=w.project_id AND a.event_type='memory.native_review_binding' AND json_extract(a.payload_json,'$.work_session_id')=ws.id AND json_extract(a.payload_json,'$.run_id')=w.run_id AND json_extract(a.payload_json,'$.work_id')=w.work_id) LIMIT 2", [input.projectId, input.runId, input.workId, input.providerSessionId]);
176
+ if (rows.length !== 1)
177
+ reviewRefuse("registered_session_required");
178
+ const row = rows[0];
179
+ if (typeof row.start_receipt_json !== "string" || Buffer.byteLength(row.start_receipt_json) > 16 * 1024 * 1024)
180
+ reviewRefuse("registered_copy_required");
181
+ const receipt = JSON.parse(row.start_receipt_json), packet = JSON.parse(receipt.context);
182
+ const relative = `works/${input.workId}/native-review-workspaces/generation-${receipt.generation}`, root = path.join(row.run_root ?? row.run_dir, relative);
183
+ if (receipt.version !== 2 || createHash("sha256").update(receipt.context).digest("hex") !== receipt.context_sha256
184
+ || packet.work_id !== input.workId || packet.run_id !== input.runId || packet.workspace_root !== root
185
+ || packet.review_copy?.root !== relative || row.cwd !== root)
186
+ reviewRefuse("registered_copy_changed");
187
+ return root;
188
+ }
@@ -0,0 +1,14 @@
1
+ import { AppError } from "../shared/errors.js";
2
+ import { assertRunMutationAllowed } from "./run-recovery-guard.js";
3
+ /** A recovered RUN never grants the retired process a new execution segment. */
4
+ export function assertRecoveryHookCurrent(context, projectId, runId, identity) {
5
+ const generation = assertRunMutationAllowed(context, projectId, runId);
6
+ if (!generation)
7
+ return;
8
+ const current = identity.daemonId && context.db.get("SELECT b.recovery_id FROM run_recovery_bindings b JOIN run_recovery_guards g ON g.recovery_id = b.recovery_id WHERE g.project_id = ? AND g.run_id = ? AND g.generation = ? AND g.status = 'recovered' AND b.status = 'accepted' AND b.daemon_id = ? LIMIT 1", [projectId, runId, generation, identity.daemonId]);
9
+ if (current)
10
+ return;
11
+ const retired = identity.daemonId && (context.db.get(`SELECT ws.id FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id JOIN hook_events he ON he.project_id = w.project_id AND he.event_key = ws.hook_event_id WHERE w.project_id = ? AND w.run_id = ? AND he.daemon_id = ? LIMIT 1`, [projectId, runId, identity.daemonId]) || context.db.get("SELECT b.recovery_id FROM run_recovery_bindings b JOIN run_recovery_guards g ON g.recovery_id = b.recovery_id WHERE g.project_id = ? AND g.run_id = ? AND b.daemon_id = ? AND (g.generation <> ? OR b.status = 'abandoned') LIMIT 1", [projectId, runId, identity.daemonId, generation]));
12
+ if (!identity.daemonId || retired)
13
+ throw new AppError("recovery_daemon_stale", "Lifecycle receipt belongs to an unidentified or retired recovery daemon", 1, { run_id: runId, generation, daemon_id: identity.daemonId });
14
+ }
@@ -0,0 +1,24 @@
1
+ export function recordFlowSessionObservation(context, input) {
2
+ const duplicate = context.db.get("SELECT id FROM flow_session_segments WHERE project_id = ? AND event_key = ?", [input.projectId, input.eventKey]);
3
+ if (duplicate)
4
+ return { changed: false, segment_id: duplicate.id };
5
+ const open = context.db.get(`SELECT id, run_id, protocol_id FROM flow_session_segments
6
+ WHERE project_id = ? AND session_id = ? AND ended_at IS NULL ORDER BY started_at DESC, id DESC LIMIT 1`, [input.projectId, input.sessionId]);
7
+ const now = context.now();
8
+ const runId = input.runId === undefined ? open?.run_id ?? null : input.runId;
9
+ const protocolId = input.protocolId === undefined ? open?.protocol_id ?? null : input.protocolId;
10
+ if (open && open.run_id === runId && open.protocol_id === protocolId) {
11
+ return { changed: false, segment_id: open.id };
12
+ }
13
+ if (open)
14
+ context.db.run("UPDATE flow_session_segments SET ended_at = ? WHERE id = ? AND ended_at IS NULL", [now, open.id]);
15
+ const result = context.db.run(`INSERT INTO flow_session_segments
16
+ (project_id, session_id, run_id, protocol_id, started_at, ended_at, cwd, tool_name, event_key)
17
+ VALUES (?, ?, ?, ?, ?, NULL, ?, ?, ?)`, [input.projectId, input.sessionId, runId, protocolId, now, input.cwd ?? null, input.toolName ?? null, input.eventKey]);
18
+ return { changed: true, segment_id: Number(result.lastInsertRowid) };
19
+ }
20
+ export function activeFlowSessionsForProject(context, projectId) {
21
+ return context.db.all(`SELECT * FROM sessions
22
+ WHERE project_id = ? AND status IN ('pending', 'active', 'waiting_user', 'blocked', 'stopping')
23
+ ORDER BY updated_at DESC`, [projectId]);
24
+ }
@@ -9,15 +9,15 @@ import { parseJsonObject } from "../shared/json.js";
9
9
  import { canonicalPath, ensureDir, resolveProjectRoot } from "../storage/paths.js";
10
10
  import { resolveWorkReference } from "../storage/work-references.js";
11
11
  import { appendAudit } from "./audit.js";
12
- import { memoryRepairAssignedHookCwd } from "./memory-repair-physical-binding.js";
13
- import { memoryNativeReviewAssignedHookCwd } from "./memory-native-review-copy.js";
12
+ import { memoryRepairAssignedHookCwd, memoryNativeReviewAssignedHookCwd } from "./hook-memory-routing.js";
14
13
  import { prepareTextFile, prepareJsonText, prepareError } from "../cli/input-preparation.js";
14
+ import { nativeHookNonParticipation } from "../cli/native-hook-preparation.js";
15
15
  import { nativeSessionIdentity, storageSessionId } from "./session-identity.js";
16
- import { assertRecoveryHookCurrent } from "./run-recovery.js";
16
+ import { assertRecoveryHookCurrent } from "./hook-recovery-guard.js";
17
17
  import { observeLifecycleInvocation, observedLifecycleInvocation, managedNativeInvocationScope } from "./lifecycle-invocations.js";
18
18
  import { commandHasOption, commandOption, commandPosition, shellSuffixOffset, parseLifecycleCommand, lifecycleCommandFromPayload, continuationCommand } from "./lifecycle-command.js";
19
- import { registerProject, requireProjectByRoot } from "./projects.js";
20
- import { activeFlowSessionsForProject, recordFlowSessionObservation } from "./sessions.js";
19
+ import { registerProject, requireProjectByRoot } from "./project-registry.js";
20
+ import { activeFlowSessionsForProject, recordFlowSessionObservation } from "./hook-session-observation.js";
21
21
  const defaultProfileName = "default";
22
22
  const maxSanitizedSummaryLength = 1600;
23
23
  function lifecycleObservationResult(receipt) {
@@ -496,6 +496,9 @@ function codexNativeParentId(payload, childId) {
496
496
  /** Qualified ACP admission binds public commands to an exact issued assignment.
497
497
  * Its provenance is an observation, never a synchronous PreToolUse assertion. */
498
498
  export function handleZcodeEvent(context, input) {
499
+ const unrelated = nativeHookNonParticipation(["zcode", "event"], input.stdin);
500
+ if (unrelated)
501
+ return unrelated;
499
502
  const notification = parseJsonObject(input.stdin || "{}", "zcode ACP notification");
500
503
  const params = objectRecord(notification.params), update = objectRecord(params.update);
501
504
  if (notification.method !== "session/update" || update.sessionUpdate !== "tool_call")
@@ -523,6 +526,9 @@ export function handleZcodeEvent(context, input) {
523
526
  * admission; the native hook must not create a competing receipt for them.
524
527
  */
525
528
  export function handleZcodeHook(context, input) {
529
+ const unrelated = nativeHookNonParticipation(["zcode", "hook"], input.stdin);
530
+ if (unrelated)
531
+ return unrelated;
526
532
  return recordZcodeLifecycle(context, input, undefined, true);
527
533
  }
528
534
  function recordZcodeLifecycle(context, input, invocationId, nativePreTool = false) {
@@ -631,6 +637,9 @@ function recordZcodeLifecycle(context, input, invocationId, nativePreTool = fals
631
637
  }
632
638
  /** Convert a Grok Build PreToolUse hook into a trusted lifecycle receipt. */
633
639
  export function handleGrokEvent(context, input) {
640
+ const unrelated = nativeHookNonParticipation(["grok", "event"], input.stdin);
641
+ if (unrelated)
642
+ return unrelated;
634
643
  const hook = parseJsonObject(input.stdin || "{}", "Grok Build hook event");
635
644
  const eventName = stringValue(hook.hook_event_name) ?? stringValue(hook.hookEventName);
636
645
  if (eventName && eventName !== "PreToolUse" && eventName !== "pre_tool_use") {
@@ -703,6 +712,9 @@ export function handleGrokEvent(context, input) {
703
712
  }
704
713
  /** Convert a controlled OpenCode tool hook into the shared lifecycle receipt. */
705
714
  export function handleOpenCodeEvent(context, input) {
715
+ const unrelated = nativeHookNonParticipation(["opencode", "event"], input.stdin);
716
+ if (unrelated)
717
+ return unrelated;
706
718
  const event = parseJsonObject(input.stdin || "{}", "OpenCode tool event");
707
719
  if (event.schema_id !== "dd-flow/opencode-tool-event@1")
708
720
  throw new AppError("unsupported_opencode_event_schema", "Unsupported OpenCode event schema", 1);
@@ -710,6 +722,9 @@ export function handleOpenCodeEvent(context, input) {
710
722
  }
711
723
  /** Accept adapter-observed Factory identity, never a model-supplied Session ID. */
712
724
  export function handleDroidEvent(context, input) {
725
+ const unrelated = nativeHookNonParticipation(["droid", "event"], input.stdin);
726
+ if (unrelated)
727
+ return unrelated;
713
728
  const event = parseJsonObject(input.stdin || "{}", "Droid tool event");
714
729
  if (event.schema_id !== "dd-flow/droid-tool-event@1")
715
730
  throw new AppError("unsupported_droid_event_schema", "Unsupported Droid event schema", 1);
@@ -935,6 +950,9 @@ function handleControlledToolEvent(context, input, event, harness) {
935
950
  }
936
951
  /** Convert an Antigravity CLI tool hook into the shared lifecycle receipt. */
937
952
  export function handleAgyEvent(context, input) {
953
+ const unrelated = nativeHookNonParticipation(["agy", "event"], input.stdin);
954
+ if (unrelated)
955
+ return unrelated;
938
956
  const event = parseJsonObject(input.stdin || "{}", "Antigravity tool event");
939
957
  if (event.schema_id !== "dd-flow/agy-tool-event@1")
940
958
  throw new AppError("unsupported_agy_event_schema", "Unsupported Antigravity event schema", 1);
@@ -1493,9 +1511,10 @@ function hookEntry(matcher, command, statusMessage) {
1493
1511
  }
1494
1512
  function commandHook(command, statusMessage) {
1495
1513
  // Hook admission is synchronous (the CLI must see a committed receipt), but
1496
- // the old 5s budget was shorter than a cold CLI/store open under fan-out.
1497
- // Startup owns migrations; this budget only covers the bounded hot path.
1498
- return { type: "command", command, timeout: 30, statusMessage };
1514
+ // Admission keeps its own shared 20s deadline. Leave separate headroom for
1515
+ // the shell, cold CLI imports and bounded subprocess cleanup: the host must
1516
+ // not kill the hook before it can report its typed admission failure.
1517
+ return { type: "command", command, timeout: 60, statusMessage };
1499
1518
  }
1500
1519
  function hookCommand(event) {
1501
1520
  const args = `codex hook handle --event ${event} --json`;