@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
@@ -0,0 +1,424 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createHash, randomUUID } from "node:crypto";
4
+ import { DatabaseSync } from "node:sqlite";
5
+ import { AppError } from "../shared/errors.js";
6
+ import { PreparedStoreWriterMigration, assertStoreWriterDrain } from "./writer-migration.js";
7
+ import { publishImmutableInput } from "./immutable-input.js";
8
+ import { beginStorageMaintenance, beginStorageRecovery, completeStorageMaintenance, importGuardPath } from "./import-guard.js";
9
+ import { processTreeIsAlive } from "../services/managed-processes.js";
10
+ import { STORE_WRITER_VERSION } from "./writer-contract.js";
11
+ import { PreparedStoreWriterRestore } from "./writer-restore.js";
12
+ import { acquireInstallationLock } from "../services/installation-lock.js";
13
+ import { withStoreCleanup } from "./native-cleanup.js";
14
+ import { bootstrapMemoryUpgradeSchema } from "./memory-upgrade-schema-bootstrap.js";
15
+ const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
16
+ function refuse(reason) {
17
+ throw new AppError("paired_storage_migration_invalid", "Reconcile the exact paired migration; recovery is a separate operation", 1, { reason, effect: "unknown", next_action: "reconcile_exact_request" });
18
+ }
19
+ function canonical(file) {
20
+ if (!path.isAbsolute(file) || path.join(fs.realpathSync(path.dirname(file)), path.basename(file)) !== file)
21
+ refuse("noncanonical_path");
22
+ return file;
23
+ }
24
+ function children(intent) {
25
+ return intent.stores.map(store => {
26
+ const bytes = Buffer.from(store.input.base64, "base64");
27
+ if (bytes.toString("base64") !== store.input.base64)
28
+ refuse("invalid_child_bytes");
29
+ const value = JSON.parse(bytes.toString("utf8"));
30
+ if (value.database !== store.database || value.backup !== store.backup || value.request_id !== `${intent.request_id}.${store.kind}`)
31
+ refuse("foreign_child_scope");
32
+ return PreparedStoreWriterMigration.restoreInput(store.input.path, bytes.toString("utf8"), store.input.sha256);
33
+ });
34
+ }
35
+ /** Existing logical drain plus actual physical owner observation. Never kills
36
+ * a process or infers drain from expired/terminal metadata alone. */
37
+ export function observeDrainedMigrationStores(stores) {
38
+ const observations = [];
39
+ for (const store of stores) {
40
+ const db = new DatabaseSync(store.database, { readOnly: true });
41
+ try {
42
+ db.exec("BEGIN");
43
+ assertStoreWriterDrain(db);
44
+ let processes = 0;
45
+ if (db.prepare("SELECT 1 FROM sqlite_schema WHERE type='table' AND name='managed_processes'").get())
46
+ for (const row of db.prepare("SELECT * FROM managed_processes").all()) {
47
+ processes++;
48
+ if (processTreeIsAlive(row))
49
+ refuse("physical_owner_alive");
50
+ }
51
+ observations.push({ database: store.database, unsettled_writers: 0, live_processes: 0, registered_processes: processes });
52
+ }
53
+ finally {
54
+ db.close();
55
+ }
56
+ }
57
+ return observations;
58
+ }
59
+ const drained = observeDrainedMigrationStores;
60
+ const evidenceKinds = ["writer-drain", "storage-backup", "storage-plan", "storage-apply", "storage-verification"];
61
+ function schemaStep(intent) { return intent.bootstrap_schema ? { bootstrap_schema: intent.bootstrap_schema } : {}; }
62
+ function plannedStores(intent) {
63
+ return intent.stores.map(store => ({ kind: store.kind, database: store.database, backup: store.backup,
64
+ source_request_sha256: store.input.sha256, ...schemaStep(intent) }));
65
+ }
66
+ /** Two existing one-store migrations under the existing shared admission fence.
67
+ * Default is writer-only. Memory upgrade may explicitly bind the executing
68
+ * engine's fixed additive bootstrap; it runs under the same retained permits,
69
+ * backup and old-data verification, never as a readonly admission side effect. */
70
+ export class PreparedPairedWriterMigration {
71
+ sha256;
72
+ #file;
73
+ #intent;
74
+ #children;
75
+ constructor(file, intent) {
76
+ this.#file = file;
77
+ this.#intent = structuredClone(intent);
78
+ this.#children = children(intent);
79
+ const bytes = JSON.stringify(intent);
80
+ if (Buffer.byteLength(bytes) > 16 * 1024 * 1024)
81
+ refuse("intent_exceeds_bound");
82
+ this.sha256 = hash(bytes);
83
+ Object.freeze(this);
84
+ }
85
+ static prepare(input) {
86
+ if (input.bootstrapSchema !== undefined && input.bootstrapSchema !== "memory-upgrade@1")
87
+ refuse("unsupported_schema_bootstrap");
88
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,85}$/.test(input.requestId) || !/^[a-f0-9]{64}$/.test(input.selectionSha256))
89
+ refuse("invalid_identity");
90
+ const file = canonical(input.requestFile);
91
+ if (fs.existsSync(file) || fs.existsSync(file + ".result.json"))
92
+ refuse("restore_original_request");
93
+ for (const root of [input.projectRoot, input.workspaceRoot, input.runHome, input.resourceHome])
94
+ if (fs.realpathSync(root) !== root || !fs.statSync(root).isDirectory())
95
+ refuse("noncanonical_scope");
96
+ const stores = ["run", "resource"].map(kind => {
97
+ const home = kind === "run" ? input.runHome : input.resourceHome;
98
+ const database = path.join(home, kind === "run" ? "db.sqlite" : "runtime.sqlite"), backup = `${file}.${kind}.backup.sqlite`;
99
+ const child = PreparedStoreWriterMigration.prepare({ requestId: `${input.requestId}.${kind}`, requestFile: `${file}.${kind}.json`, database, backup });
100
+ return { kind, home, database, backup, input: child.inputArtifact() };
101
+ });
102
+ const occupied = stores.flatMap(store => [store.database, store.database + "-wal", store.database + "-shm", store.database + "-journal", importGuardPath(store.home), importGuardPath(store.home) + ".lock"]);
103
+ if (stores.some(store => occupied.includes(store.backup) || occupied.includes(store.input.path)) || occupied.includes(file) || occupied.includes(file + ".result.json"))
104
+ refuse("path_alias");
105
+ if (stores.every(store => JSON.parse(Buffer.from(store.input.base64, "base64").toString("utf8")).source.writer_version === STORE_WRITER_VERSION))
106
+ refuse("content_only_route_required");
107
+ drained(stores);
108
+ return new PreparedPairedWriterMigration(file, { schema_id: "dd-flow/paired-writer-migration@1", request_id: input.requestId,
109
+ permit_id: randomUUID(), selection_sha256: input.selectionSha256, project_root: input.projectRoot, workspace_root: input.workspaceRoot, stores,
110
+ ...(input.bootstrapSchema ? { bootstrap_schema: input.bootstrapSchema } : {}) });
111
+ }
112
+ static restore(file, expectedSha256) {
113
+ canonical(file);
114
+ const stat = fs.lstatSync(file);
115
+ if (!stat.isFile() || stat.nlink !== 1 || stat.size > 16 * 1024 * 1024)
116
+ refuse("invalid_intent_file");
117
+ const bytes = fs.readFileSync(file, "utf8");
118
+ if (!/^[a-f0-9]{64}$/.test(expectedSha256) || hash(bytes) !== expectedSha256)
119
+ refuse("intent_changed");
120
+ const intent = JSON.parse(bytes);
121
+ if (Object.keys(intent).sort().join() !== ["permit_id", "project_root", "request_id", "schema_id", "selection_sha256", "stores", "workspace_root",
122
+ ...(intent.bootstrap_schema !== undefined ? ["bootstrap_schema"] : [])].sort().join()
123
+ || intent.bootstrap_schema !== undefined && intent.bootstrap_schema !== "memory-upgrade@1"
124
+ || intent.schema_id !== "dd-flow/paired-writer-migration@1" || !/^[0-9a-f-]{36}$/.test(intent.permit_id)
125
+ || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,85}$/.test(intent.request_id) || !/^[a-f0-9]{64}$/.test(intent.selection_sha256)
126
+ || !Array.isArray(intent.stores) || intent.stores.length !== 2 || intent.stores[0]?.kind !== "run" || intent.stores[1]?.kind !== "resource")
127
+ refuse("invalid_intent");
128
+ for (const store of intent.stores) {
129
+ if (Object.keys(store).sort().join() !== "backup,database,home,input,kind" || Object.keys(store.input).sort().join() !== "base64,path,sha256"
130
+ || fs.realpathSync(store.home) !== store.home || !fs.statSync(store.home).isDirectory()
131
+ || store.database !== path.join(store.home, store.kind === "run" ? "db.sqlite" : "runtime.sqlite")
132
+ || store.backup !== `${file}.${store.kind}.backup.sqlite` || store.input.path !== `${file}.${store.kind}.json`)
133
+ refuse("foreign_store_binding");
134
+ }
135
+ const prepared = new PreparedPairedWriterMigration(file, intent);
136
+ if (prepared.sha256 !== expectedSha256)
137
+ refuse("intent_changed");
138
+ return prepared;
139
+ }
140
+ retain() {
141
+ canonical(this.#file);
142
+ // Parent first: a crash before child publication can be reconstructed from
143
+ // these independently retained exact bytes without selecting new sources.
144
+ publishImmutableInput(this.#file, Buffer.from(JSON.stringify(this.#intent)), "paired_migration_request_conflict");
145
+ for (const child of this.#children)
146
+ child.retain();
147
+ return { request_file: this.#file, sha256: this.sha256, selection_sha256: this.#intent.selection_sha256 };
148
+ }
149
+ reconcile() {
150
+ // Physical outcomes only: "applied" does not itself release admission.
151
+ const outcomes = this.#children.map(child => child.reconcile());
152
+ const count = outcomes.filter(outcome => outcome.status === "applied").length;
153
+ return { status: outcomes.some(outcome => outcome.status === "unknown") ? "unknown" : count === 2 ? "applied" : count ? "partial" : "not_applied", outcomes };
154
+ }
155
+ recoveryMaterial() { return { file: this.#file, sha256: this.sha256, intent: structuredClone(this.#intent), children: [...this.#children] }; }
156
+ apply() {
157
+ const lease = acquireInstallationLock(this.#file + ".lock", this.sha256);
158
+ return withStoreCleanup(() => this.applyLocked(lease.assertOwnership), () => lease.release());
159
+ }
160
+ applyLocked(assertOwnership) {
161
+ assertOwnership();
162
+ if (fs.existsSync(this.#file + ".recovery.json"))
163
+ refuse("recovery_selected");
164
+ for (const store of this.#intent.stores)
165
+ canonical(store.database);
166
+ this.retain();
167
+ const before = this.reconcile();
168
+ if (before.status === "unknown")
169
+ refuse("unknown_child_outcome");
170
+ const resultFile = this.#file + ".result.json";
171
+ if (fs.existsSync(resultFile) && before.status !== "applied")
172
+ refuse("completed_migration_changed");
173
+ const scope = { projectRoot: this.#intent.project_root, workspaceRoot: this.#intent.workspace_root, sourceManifestSha256: this.sha256, importId: this.#intent.permit_id };
174
+ const homes = [...new Set(this.#intent.stores.map(store => store.home))];
175
+ for (const home of homes)
176
+ beginStorageMaintenance({ home, ...scope });
177
+ let evidence;
178
+ if (!fs.existsSync(resultFile)) {
179
+ const drain = drained(this.#intent.stores);
180
+ for (const child of this.#children) {
181
+ assertOwnership();
182
+ drained(this.#intent.stores);
183
+ child.backup();
184
+ }
185
+ const backups = this.#children.map(child => child.backupFacts());
186
+ for (const child of this.#children) {
187
+ assertOwnership();
188
+ drained(this.#intent.stores);
189
+ child.apply();
190
+ }
191
+ if (this.#intent.bootstrap_schema) {
192
+ assertOwnership();
193
+ drained(this.#intent.stores);
194
+ bootstrapMemoryUpgradeSchema(this.#intent.stores[0].home, this.#intent.stores[1].home, this.#intent.permit_id);
195
+ }
196
+ const verification = this.#children.map(child => ({ ...child.verifyAppliedData(), ...schemaStep(this.#intent) }));
197
+ const applied = this.reconcile();
198
+ if (applied.status !== "applied")
199
+ refuse("verification_incomplete");
200
+ const plans = plannedStores(this.#intent);
201
+ const stores = plans.map((store, i) => ({ ...store,
202
+ receipt_sha256: hash(JSON.stringify(applied.outcomes[i].receipt)) }));
203
+ const payloads = [drain, backups, plans, stores, verification];
204
+ evidence = evidenceKinds.map((kind, i) => {
205
+ const bytes = Buffer.from(JSON.stringify({ schema_id: "dd-flow/paired-storage-evidence@1", kind, request_sha256: this.sha256,
206
+ selection_sha256: this.#intent.selection_sha256, stores: payloads[i] }));
207
+ return { kind, path: `receipts/${kind}.json`, sha256: hash(bytes), base64: bytes.toString("base64") };
208
+ });
209
+ }
210
+ else
211
+ evidence = this.readEvidence().evidence;
212
+ const after = this.reconcile();
213
+ if (after.status !== "applied")
214
+ refuse("verification_incomplete");
215
+ const receipt = { schema_id: "dd-flow/paired-writer-migration-result@2", request_sha256: this.sha256,
216
+ selection_sha256: this.#intent.selection_sha256, stores: this.#intent.stores.map((store, i) => ({ kind: store.kind, database: store.database, backup: store.backup,
217
+ receipt_sha256: hash(JSON.stringify(after.outcomes[i].receipt)) })), evidence };
218
+ publishImmutableInput(resultFile, Buffer.from(JSON.stringify(receipt)), "paired_migration_result_conflict");
219
+ for (const home of homes)
220
+ completeStorageMaintenance({ home, ...scope });
221
+ return receipt;
222
+ }
223
+ /** Historical evidence is verified against original request/backup custody,
224
+ * not re-issued from today's mutable runtime data after bootstrap writes. */
225
+ readEvidence() {
226
+ if (fs.existsSync(this.#file + ".recovery.json"))
227
+ refuse("recovery_selected");
228
+ const file = this.#file + ".result.json", stat = fs.lstatSync(file);
229
+ if (!stat.isFile() || stat.nlink !== 1 || stat.size > 1024 * 1024)
230
+ refuse("invalid_migration_evidence_file");
231
+ const bytes = fs.readFileSync(file), receipt = JSON.parse(bytes.toString("utf8")), observed = this.reconcile();
232
+ if (observed.status !== "applied" || receipt.schema_id !== "dd-flow/paired-writer-migration-result@2" || receipt.request_sha256 !== this.sha256
233
+ || receipt.selection_sha256 !== this.#intent.selection_sha256 || !Array.isArray(receipt.evidence) || receipt.evidence.length !== evidenceKinds.length
234
+ || JSON.stringify(receipt.stores) !== JSON.stringify(this.#intent.stores.map((store, i) => ({ kind: store.kind, database: store.database, backup: store.backup,
235
+ receipt_sha256: hash(JSON.stringify(observed.outcomes[i].receipt)) }))))
236
+ refuse("migration_evidence_custody_changed");
237
+ const evidence = receipt.evidence;
238
+ for (let i = 0; i < evidence.length; i++) {
239
+ const entry = evidence[i], raw = Buffer.from(entry.base64, "base64"), value = JSON.parse(raw.toString("utf8"));
240
+ if (entry.kind !== evidenceKinds[i] || entry.path !== `receipts/${entry.kind}.json` || raw.toString("base64") !== entry.base64 || hash(raw) !== entry.sha256
241
+ || value.schema_id !== "dd-flow/paired-storage-evidence@1" || value.kind !== entry.kind || value.request_sha256 !== this.sha256
242
+ || value.selection_sha256 !== this.#intent.selection_sha256 || !Array.isArray(value.stores) || value.stores.length !== 2
243
+ || value.stores.some((store, index) => store.database !== this.#intent.stores[index].database))
244
+ refuse("migration_evidence_changed");
245
+ }
246
+ const backups = this.#children.map(child => child.backupFacts());
247
+ const facts = evidence.map(entry => JSON.parse(Buffer.from(entry.base64, "base64").toString("utf8")).stores);
248
+ if (JSON.stringify(facts[1]) !== JSON.stringify(backups))
249
+ refuse("backup_evidence_changed");
250
+ const plans = plannedStores(this.#intent);
251
+ const applies = plans.map((store, i) => ({ ...store, receipt_sha256: hash(JSON.stringify(observed.outcomes[i].receipt)) }));
252
+ const verification = backups.map(store => ({ database: store.database, source_request_sha256: store.source_request_sha256,
253
+ writer_version: STORE_WRITER_VERSION, source_data_sha256: store.source_data_sha256, integrity: "ok", foreign_key_violations: 0, ...schemaStep(this.#intent) }));
254
+ if (JSON.stringify(facts[2]) !== JSON.stringify(plans) || JSON.stringify(facts[3]) !== JSON.stringify(applies)
255
+ || JSON.stringify(facts[4]) !== JSON.stringify(verification)
256
+ || facts[0].some((store) => store.unsettled_writers !== 0 || store.live_processes !== 0 || !Number.isSafeInteger(store.registered_processes) || store.registered_processes < 0))
257
+ refuse("migration_evidence_changed");
258
+ return { reference: { path: file, sha256: hash(bytes) }, evidence: structuredClone(evidence) };
259
+ }
260
+ }
261
+ function assertOriginal(child, store) {
262
+ child.recoveryReference();
263
+ const db = new DatabaseSync(store.database, { readOnly: true });
264
+ try {
265
+ db.exec("BEGIN");
266
+ child.assertOriginal(db);
267
+ }
268
+ finally {
269
+ db.close();
270
+ }
271
+ }
272
+ /** Separate, explicitly selected recovery. A never-applied child is verified
273
+ * against its original state, not restored from a nonexistent backup. */
274
+ export class PreparedPairedWriterRecovery {
275
+ sha256;
276
+ #file;
277
+ #migration;
278
+ #intent;
279
+ #restores;
280
+ constructor(migration, file, intent) {
281
+ this.#file = file;
282
+ this.#migration = migration;
283
+ this.#intent = structuredClone(intent);
284
+ const material = migration.recoveryMaterial();
285
+ this.#restores = intent.stores.map((store, i) => {
286
+ if (Object.keys(store).sort().join() !== "input,kind" || store.kind !== material.intent.stores[i].kind)
287
+ refuse("recovery_child_scope_changed");
288
+ if (store.input === null)
289
+ return null;
290
+ if (!store.input || Object.keys(store.input).sort().join() !== "base64,path,sha256")
291
+ refuse("recovery_child_scope_changed");
292
+ const bytes = Buffer.from(store.input.base64, "base64");
293
+ if (store.input.path !== `${file}.${store.kind}.json` || bytes.toString("base64") !== store.input.base64
294
+ || JSON.parse(bytes.toString("utf8")).recovery_id !== `${intent.recovery_id}.${store.kind}`)
295
+ refuse("recovery_child_scope_changed");
296
+ return PreparedStoreWriterRestore.restoreInput(material.children[i], store.input.path, bytes.toString("utf8"), store.input.sha256);
297
+ });
298
+ const bytes = JSON.stringify(intent);
299
+ if (Buffer.byteLength(bytes) > 128 * 1024)
300
+ refuse("recovery_intent_exceeds_bound");
301
+ this.sha256 = hash(bytes);
302
+ Object.freeze(this);
303
+ }
304
+ migrationReference() { return { ...this.#intent.migration }; }
305
+ static prepare(migration, recoveryId) {
306
+ if (!(migration instanceof PreparedPairedWriterMigration) || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,85}$/.test(recoveryId))
307
+ refuse("invalid_recovery_identity");
308
+ const material = migration.recoveryMaterial(), file = material.file + ".recovery.json";
309
+ const lease = acquireInstallationLock(material.file + ".lock", material.sha256);
310
+ return withStoreCleanup(() => {
311
+ lease.assertOwnership();
312
+ if (fs.existsSync(file) || fs.existsSync(file + ".result.json"))
313
+ refuse("restore_original_recovery");
314
+ migration.retain();
315
+ drained(material.intent.stores);
316
+ const stores = material.intent.stores.map((store, i) => {
317
+ const child = material.children[i];
318
+ try {
319
+ assertOriginal(child, store);
320
+ return { kind: store.kind, input: null };
321
+ }
322
+ catch (error) {
323
+ if (!(error instanceof AppError) || error.code !== "store_migration_outcome_unknown" || error.details.reason !== "source_changed")
324
+ throw error;
325
+ }
326
+ if (!fs.existsSync(store.backup))
327
+ refuse("changed_store_has_no_backup");
328
+ const restore = PreparedStoreWriterRestore.prepare(child, { recoveryId: `${recoveryId}.${store.kind}`, requestFile: `${file}.${store.kind}.json` });
329
+ return { kind: store.kind, input: restore.inputArtifact() };
330
+ });
331
+ return new PreparedPairedWriterRecovery(migration, file, { schema_id: "dd-flow/paired-writer-recovery@1", recovery_id: recoveryId,
332
+ migration: { path: material.file, sha256: material.sha256 }, stores });
333
+ }, () => lease.release());
334
+ }
335
+ static restore(file, expectedSha256) {
336
+ canonical(file);
337
+ const stat = fs.lstatSync(file);
338
+ if (!stat.isFile() || stat.nlink !== 1 || stat.size > 128 * 1024)
339
+ refuse("invalid_recovery_intent");
340
+ const bytes = fs.readFileSync(file, "utf8");
341
+ if (hash(bytes) !== expectedSha256)
342
+ refuse("recovery_intent_changed");
343
+ const intent = JSON.parse(bytes);
344
+ if (Object.keys(intent).sort().join() !== "migration,recovery_id,schema_id,stores" || intent.schema_id !== "dd-flow/paired-writer-recovery@1"
345
+ || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,85}$/.test(intent.recovery_id) || Object.keys(intent.migration).sort().join() !== "path,sha256"
346
+ || file !== intent.migration.path + ".recovery.json"
347
+ || !Array.isArray(intent.stores) || intent.stores.length !== 2)
348
+ refuse("invalid_recovery_intent");
349
+ const migration = PreparedPairedWriterMigration.restore(intent.migration.path, intent.migration.sha256);
350
+ const prepared = new PreparedPairedWriterRecovery(migration, file, intent);
351
+ if (prepared.sha256 !== expectedSha256)
352
+ refuse("recovery_intent_changed");
353
+ return prepared;
354
+ }
355
+ retainLocked() {
356
+ canonical(this.#file);
357
+ this.#migration.retain();
358
+ const material = this.#migration.recoveryMaterial();
359
+ drained(material.intent.stores);
360
+ for (let i = 0; i < this.#restores.length; i++) {
361
+ const restore = this.#restores[i];
362
+ if (restore)
363
+ restore.revalidatePrepared();
364
+ else
365
+ assertOriginal(material.children[i], material.intent.stores[i]);
366
+ }
367
+ publishImmutableInput(this.#file, Buffer.from(JSON.stringify(this.#intent)), "paired_recovery_request_conflict");
368
+ // Selecting recovery fences the drained stores before returning its prepared
369
+ // apply command; it is not a promise to restore while new writers keep running.
370
+ for (const home of new Set(material.intent.stores.map(store => store.home)))
371
+ beginStorageRecovery({ home,
372
+ projectRoot: material.intent.project_root, workspaceRoot: material.intent.workspace_root, importId: material.intent.permit_id,
373
+ originalSha256: material.sha256, recoverySha256: this.sha256 });
374
+ for (const restore of this.#restores)
375
+ restore?.retain();
376
+ return { request_file: this.#file, sha256: this.sha256, migration_sha256: material.sha256 };
377
+ }
378
+ retain() {
379
+ const material = this.#migration.recoveryMaterial(), lease = acquireInstallationLock(material.file + ".lock", material.sha256);
380
+ return withStoreCleanup(() => { lease.assertOwnership(); return this.retainLocked(); }, () => lease.release());
381
+ }
382
+ /** Read-only physical outcome, not proof that admission was reopened. */
383
+ reconcile() {
384
+ const material = this.#migration.recoveryMaterial();
385
+ const outcomes = this.#restores.map((restore, i) => {
386
+ if (restore)
387
+ return { kind: material.intent.stores[i].kind, ...restore.reconcile() };
388
+ try {
389
+ assertOriginal(material.children[i], material.intent.stores[i]);
390
+ return { kind: material.intent.stores[i].kind, status: "restored" };
391
+ }
392
+ catch (error) {
393
+ return { kind: material.intent.stores[i].kind, status: "unknown", reason: error instanceof AppError ? String(error.details.reason ?? error.code) : "source_unavailable" };
394
+ }
395
+ });
396
+ return { status: outcomes.some(outcome => outcome.status === "unknown") ? "unknown" : outcomes.every(outcome => outcome.status === "restored") ? "restored" : "not_restored", outcomes };
397
+ }
398
+ apply() {
399
+ const material = this.#migration.recoveryMaterial(), lease = acquireInstallationLock(material.file + ".lock", material.sha256);
400
+ return withStoreCleanup(() => {
401
+ lease.assertOwnership();
402
+ this.retainLocked();
403
+ const scope = { projectRoot: material.intent.project_root, workspaceRoot: material.intent.workspace_root, importId: material.intent.permit_id };
404
+ const homes = [...new Set(material.intent.stores.map(store => store.home))];
405
+ const stores = this.#restores.map((restore, i) => {
406
+ lease.assertOwnership();
407
+ drained(material.intent.stores);
408
+ if (!restore) {
409
+ assertOriginal(material.children[i], material.intent.stores[i]);
410
+ return { kind: material.intent.stores[i].kind, status: "unchanged", migration_sha256: material.children[i].sha256 };
411
+ }
412
+ const receipt = restore.apply();
413
+ if (restore.reconcile().status !== "restored")
414
+ refuse("recovery_readback_failed");
415
+ return { kind: material.intent.stores[i].kind, status: "restored", receipt_sha256: hash(JSON.stringify(receipt)) };
416
+ });
417
+ const receipt = { schema_id: "dd-flow/paired-writer-recovery-result@1", request_sha256: this.sha256, migration_sha256: material.sha256, stores };
418
+ publishImmutableInput(this.#file + ".result.json", Buffer.from(JSON.stringify(receipt)), "paired_recovery_result_conflict");
419
+ for (const home of homes)
420
+ completeStorageMaintenance({ home, ...scope, sourceManifestSha256: this.sha256 });
421
+ return receipt;
422
+ }, () => lease.release());
423
+ }
424
+ }
@@ -4,6 +4,9 @@ import { rollbackStoreTransaction } from "./native-cleanup.js";
4
4
  export const STORE_WRITER_VERSION = 2;
5
5
  const version = STORE_WRITER_VERSION;
6
6
  const table = "dd_flow_writer_contract";
7
+ const admissions = new WeakMap();
8
+ /** Change admission without adopting a different writer version after cutover. */
9
+ export function bindStoreWriterAdmission(db, admission) { admissions.set(db, admission); }
7
10
  const quote = (name) => `"${name.replaceAll('"', '""')}"`;
8
11
  /** The executing writer's fence definition, also used for readonly admission. */
9
12
  export function storeWriterTriggerSql(name, action) {
@@ -38,19 +41,23 @@ export function executeStoreSchema(db, sql, initialize = false) {
38
41
  }
39
42
  }
40
43
  /** Registration is connection-local and never changes historical input bytes. */
41
- export function registerStoreWriter(db) {
44
+ export function registerStoreWriter(db, admission) {
42
45
  const current = readStoreWriterContract(db);
43
46
  if (current !== null && current !== version)
44
47
  throw new AppError("store_writer_incompatible", "This CLI cannot write the store's required contract", 1, { required: current, supported: version });
45
- db.function("dd_flow_writer_version", () => version);
48
+ if (admission)
49
+ admissions.set(db, admission);
50
+ db.function("dd_flow_writer_version", () => { admissions.get(db)?.(); return version; });
46
51
  }
47
52
  /** Health maintenance uses only verified legacy lifecycle columns and never
48
53
  * migrates schema. Keep the retained writer fence, including version 1. */
49
- export function registerReconciliationWriter(db) {
54
+ export function registerReconciliationWriter(db, admission) {
50
55
  const current = readStoreWriterContract(db);
51
56
  if (current !== null && current !== 1 && current !== version)
52
57
  throw new AppError("store_writer_incompatible", "Unsupported reconciliation writer contract", 1, { required: current, supported: [1, version] });
53
- db.function("dd_flow_writer_version", () => current ?? version);
58
+ if (admission)
59
+ admissions.set(db, admission);
60
+ db.function("dd_flow_writer_version", () => { admissions.get(db)?.(); return current ?? version; });
54
61
  }
55
62
  export function readStoreWriterContract(db) {
56
63
  if (!db.prepare("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = ?").get(table))