@jungjaehoon/mama-core 2.4.0 → 4.0.0

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 (374) hide show
  1. package/README.md +76 -239
  2. package/db/migrations/030-case-first-memory-substrate.sql +13 -98
  3. package/db/migrations/034-add-connector-event-scope-columns.sql +9 -13
  4. package/db/migrations/039-add-connector-event-operator-ingest-seq.sql +9 -95
  5. package/db/migrations/062-refresh-connector-event-sequences.sql +9 -113
  6. package/db/migrations/063-refresh-legacy-connector-event-updates.sql +9 -32
  7. package/db/migrations/067-add-connector-event-source-entity-id.sql +9 -13
  8. package/db/migrations/068-tool-trace-diagnostics.sql +13 -0
  9. package/db/migrations/069-create-registry-nodes.sql +52 -0
  10. package/db/migrations/070-record-identity.sql +35 -0
  11. package/db/migrations/071-service-operation-origins.sql +187 -0
  12. package/db/migrations/072-observation-versions.sql +29 -0
  13. package/db/migrations/073-registry-corrections.sql +45 -0
  14. package/db/migrations/074-work-graph-ref-kinds.sql +39 -0
  15. package/db/migrations/075-agent-judgments.sql +58 -0
  16. package/db/migrations/076-scoped-checkpoint-bindings.sql +15 -0
  17. package/db/migrations/077-legacy-record-kind.sql +6 -0
  18. package/db/migrations/078-source-ingest-commands.sql +18 -0
  19. package/db/migrations/079-twin-edge-relations.sql +43 -0
  20. package/db/migrations/080-restore-duration-days.sql +7 -0
  21. package/db/migrations/081-mailbox.sql +61 -0
  22. package/db/migrations/082-commitments-row-id.sql +12 -0
  23. package/db/migrations/083-observation-is-the-evidence.sql +49 -0
  24. package/db/migrations/084-scope-kind-is-not-the-cores-to-list.sql +34 -0
  25. package/db/migrations/085-a-migration-has-a-source.sql +37 -0
  26. package/db/migrations/086-the-mailbox-is-generic.sql +110 -0
  27. package/db/migrations/087-mailbox-payload.sql +5 -0
  28. package/db/migrations/088-native-input-journal.sql +14 -0
  29. package/db/migrations/089-native-turn-receipt-lookup.sql +3 -0
  30. package/db/migrations/090-model-run-native-inputs.sql +29 -0
  31. package/db/migrations/091-native-turn-results.sql +11 -0
  32. package/db/migrations/092-restore-decision-fts.sql +34 -0
  33. package/db/migrations/093-native-turn-primary-kind.sql +14 -0
  34. package/db/migrations/094-commitment-run-provenance.sql +9 -0
  35. package/db/migrations/095-model-run-native-input-identity.sql +39 -0
  36. package/db/migrations/096-drop-connector-event-index-fts.sql +9 -0
  37. package/db/migrations/097-native-input-json-validity.sql +37 -0
  38. package/db/migrations/098-workflow-memory-kind.sql +105 -0
  39. package/dist/action-contracts.d.ts +212 -0
  40. package/dist/action-contracts.js +10 -0
  41. package/dist/api/catalog.d.ts +118 -0
  42. package/dist/api/catalog.js +1756 -0
  43. package/dist/api/dispatch.d.ts +76 -0
  44. package/dist/api/dispatch.js +372 -0
  45. package/dist/canonicalize.d.ts +2 -2
  46. package/dist/canonicalize.js +3 -3
  47. package/dist/client/client.d.ts +33 -0
  48. package/dist/client/client.js +123 -0
  49. package/dist/client/ipc.d.ts +81 -0
  50. package/dist/client/ipc.js +314 -0
  51. package/dist/db-adapter/base-adapter.d.ts +6 -66
  52. package/dist/db-adapter/base-adapter.js +6 -6
  53. package/dist/db-adapter/index.d.ts +5 -5
  54. package/dist/db-adapter/index.js +2 -5
  55. package/dist/db-adapter/node-sqlite-adapter.d.ts +87 -15
  56. package/dist/db-adapter/node-sqlite-adapter.js +2045 -568
  57. package/dist/db-manager.d.ts +53 -97
  58. package/dist/db-manager.js +121 -603
  59. package/dist/embedding/embedder.d.ts +132 -0
  60. package/dist/embedding/embedder.js +299 -0
  61. package/dist/identity/principal-repository.d.ts +2 -2
  62. package/dist/index.d.ts +43 -58
  63. package/dist/index.js +99 -145
  64. package/dist/{context-compile/channel-grant.d.ts → knowledge/access.d.ts} +12 -17
  65. package/dist/knowledge/access.js +663 -0
  66. package/dist/knowledge/case-errors.d.ts +31 -0
  67. package/dist/knowledge/case-errors.js +40 -0
  68. package/dist/{cases/search-rollup.d.ts → knowledge/case-search-rollup.d.ts} +5 -4
  69. package/dist/{cases/search-rollup.js → knowledge/case-search-rollup.js} +6 -8
  70. package/dist/{cases/store.d.ts → knowledge/case-store.d.ts} +3 -6
  71. package/dist/{cases/store.js → knowledge/case-store.js} +7 -42
  72. package/dist/{cases/timeline-range.d.ts → knowledge/case-timeline-range.d.ts} +3 -3
  73. package/dist/{cases/timeline-range.js → knowledge/case-timeline-range.js} +47 -25
  74. package/dist/{cases/types.d.ts → knowledge/case-types.d.ts} +8 -2
  75. package/dist/{cases/types.js → knowledge/case-types.js} +1 -1
  76. package/dist/knowledge/commitments.d.ts +188 -0
  77. package/dist/knowledge/commitments.js +370 -0
  78. package/dist/knowledge/decision-edges.d.ts +70 -0
  79. package/dist/knowledge/decision-edges.js +123 -0
  80. package/dist/{agent-graph/types.d.ts → knowledge/graph-query.d.ts} +80 -70
  81. package/dist/knowledge/graph-query.js +1585 -0
  82. package/dist/knowledge/identity.d.ts +16 -0
  83. package/dist/knowledge/identity.js +22 -0
  84. package/dist/knowledge/index.d.ts +47 -0
  85. package/dist/knowledge/index.js +90 -0
  86. package/dist/knowledge/judgments.d.ts +118 -0
  87. package/dist/knowledge/judgments.js +808 -0
  88. package/dist/knowledge/observations.d.ts +160 -0
  89. package/dist/knowledge/observations.js +449 -0
  90. package/dist/knowledge/question-type.d.ts +34 -0
  91. package/dist/knowledge/question-type.js +70 -0
  92. package/dist/{search → knowledge}/ranker-features.js +1 -1
  93. package/dist/{search → knowledge}/search-quality.d.ts +0 -1
  94. package/dist/knowledge/search.d.ts +38 -0
  95. package/dist/knowledge/search.js +91 -0
  96. package/dist/knowledge/source-ingest.d.ts +65 -0
  97. package/dist/knowledge/source-ingest.js +174 -0
  98. package/dist/{edges/types.d.ts → knowledge/twin-edge-types.d.ts} +31 -4
  99. package/dist/{edges/types.js → knowledge/twin-edge-types.js} +13 -2
  100. package/dist/knowledge/work-dates.d.ts +49 -0
  101. package/dist/knowledge/work-dates.js +120 -0
  102. package/dist/mama-api.d.ts +116 -778
  103. package/dist/mama-api.js +313 -2457
  104. package/dist/memory/api.d.ts +264 -24
  105. package/dist/memory/api.js +1633 -843
  106. package/dist/memory/bootstrap-builder.d.ts +2 -1
  107. package/dist/memory/bootstrap-builder.js +5 -5
  108. package/dist/memory/channel-summary-state-store.d.ts +3 -2
  109. package/dist/memory/channel-summary-state-store.js +53 -51
  110. package/dist/memory/channel-summary-store.d.ts +5 -3
  111. package/dist/memory/channel-summary-store.js +3 -8
  112. package/dist/memory/dashboard-read.d.ts +46 -0
  113. package/dist/memory/dashboard-read.js +94 -0
  114. package/dist/memory/event-store.d.ts +3 -3
  115. package/dist/memory/event-store.js +3 -10
  116. package/dist/memory/finding-store.d.ts +3 -2
  117. package/dist/memory/finding-store.js +2 -7
  118. package/dist/memory/graph-read.d.ts +52 -0
  119. package/dist/memory/graph-read.js +130 -0
  120. package/dist/memory/judgment-types.d.ts +394 -0
  121. package/dist/memory/judgment-types.js +16 -0
  122. package/dist/memory/provenance-audit.d.ts +5 -2
  123. package/dist/memory/provenance-audit.js +8 -14
  124. package/dist/memory/provenance-live.d.ts +141 -0
  125. package/dist/memory/provenance-live.js +409 -0
  126. package/dist/memory/provenance-query.d.ts +5 -4
  127. package/dist/memory/provenance-query.js +21 -29
  128. package/dist/memory/provenance-resolver.d.ts +191 -0
  129. package/dist/memory/provenance-resolver.js +155 -0
  130. package/dist/memory/provenance.d.ts +1 -11
  131. package/dist/memory/provenance.js +2 -33
  132. package/dist/memory/recall-sanitize.d.ts +55 -0
  133. package/dist/memory/recall-sanitize.js +144 -0
  134. package/dist/memory/secret-filter.d.ts +32 -0
  135. package/dist/memory/secret-filter.js +126 -0
  136. package/dist/memory/truth-store.d.ts +2 -1
  137. package/dist/memory/truth-store.js +1 -4
  138. package/dist/memory/types.d.ts +74 -24
  139. package/dist/memory/types.js +75 -5
  140. package/dist/memory/write-adapters.d.ts +98 -0
  141. package/dist/memory/write-adapters.js +242 -0
  142. package/dist/operations/owner-action-effects.d.ts +53 -0
  143. package/dist/operations/owner-action-effects.js +96 -0
  144. package/dist/provenance/source-ref.d.ts +1 -1
  145. package/dist/provenance/source-ref.js +0 -2
  146. package/dist/registry/corrections.d.ts +31 -0
  147. package/dist/registry/corrections.js +332 -0
  148. package/dist/registry/record-identity.d.ts +49 -0
  149. package/dist/registry/record-identity.js +133 -0
  150. package/dist/registry/store.d.ts +101 -0
  151. package/dist/registry/store.js +395 -0
  152. package/dist/registry/types.d.ts +28 -0
  153. package/dist/registry/types.js +5 -0
  154. package/dist/relevance-scorer.js +2 -2
  155. package/dist/runtime/agent-event-bus.d.ts +50 -0
  156. package/dist/runtime/agent-event-bus.js +101 -0
  157. package/dist/runtime/drivers/claude-cli-wrapper.d.ts +155 -0
  158. package/dist/runtime/drivers/claude-cli-wrapper.js +392 -0
  159. package/dist/runtime/drivers/cli-arg-redaction.d.ts +2 -0
  160. package/dist/runtime/drivers/cli-arg-redaction.js +18 -0
  161. package/dist/runtime/drivers/cli-secret-redaction.d.ts +17 -0
  162. package/dist/runtime/drivers/cli-secret-redaction.js +126 -0
  163. package/dist/runtime/drivers/codex-app-server-process.d.ts +277 -0
  164. package/dist/runtime/drivers/codex-app-server-process.js +2342 -0
  165. package/dist/runtime/drivers/codex-auxiliary-tools.d.ts +17 -0
  166. package/dist/runtime/drivers/codex-auxiliary-tools.js +267 -0
  167. package/dist/runtime/drivers/codex-home.d.ts +20 -0
  168. package/dist/runtime/drivers/codex-home.js +674 -0
  169. package/dist/runtime/drivers/codex-thread-registry.d.ts +63 -0
  170. package/dist/runtime/drivers/codex-thread-registry.js +433 -0
  171. package/dist/runtime/drivers/persistent-cli-adapter.d.ts +160 -0
  172. package/dist/runtime/drivers/persistent-cli-adapter.js +396 -0
  173. package/dist/runtime/drivers/persistent-cli-process.d.ts +424 -0
  174. package/dist/runtime/drivers/persistent-cli-process.js +1543 -0
  175. package/dist/runtime/drivers/types.d.ts +456 -0
  176. package/dist/runtime/drivers/types.js +110 -0
  177. package/dist/runtime/mailbox.d.ts +223 -0
  178. package/dist/runtime/mailbox.js +582 -0
  179. package/dist/runtime/model-run-store.d.ts +28 -0
  180. package/dist/{model-runs/store.js → runtime/model-run-store.js} +81 -41
  181. package/dist/runtime/model-run-types.d.ts +132 -0
  182. package/dist/{model-runs/types.js → runtime/model-run-types.js} +1 -1
  183. package/dist/runtime/native-effect-observer.d.ts +22 -0
  184. package/dist/runtime/native-effect-observer.js +103 -0
  185. package/dist/runtime/native-input-journal.d.ts +46 -0
  186. package/dist/runtime/native-input-journal.js +308 -0
  187. package/dist/runtime/native-prompt.d.ts +118 -0
  188. package/dist/runtime/native-prompt.js +496 -0
  189. package/dist/runtime/native-session.d.ts +10 -0
  190. package/dist/{agent-graph/index.js → runtime/native-session.js} +9 -6
  191. package/dist/runtime/native-tool-trace-observer.d.ts +5 -0
  192. package/dist/runtime/native-tool-trace-observer.js +65 -0
  193. package/dist/runtime/native-turn.d.ts +272 -0
  194. package/dist/runtime/native-turn.js +1041 -0
  195. package/dist/runtime/operations.d.ts +236 -0
  196. package/dist/runtime/operations.js +565 -0
  197. package/dist/runtime/post-tool-handler.d.ts +74 -0
  198. package/dist/runtime/post-tool-handler.js +137 -0
  199. package/dist/runtime/prompt-layers.d.ts +126 -0
  200. package/dist/runtime/prompt-layers.js +239 -0
  201. package/dist/runtime/runtime-process.d.ts +102 -0
  202. package/dist/runtime/runtime-process.js +206 -0
  203. package/dist/runtime/runtime.d.ts +278 -0
  204. package/dist/runtime/runtime.js +563 -0
  205. package/dist/runtime/session-pool.d.ts +176 -0
  206. package/dist/runtime/session-pool.js +369 -0
  207. package/dist/runtime/stimulus-payload.d.ts +5 -0
  208. package/dist/runtime/stimulus-payload.js +50 -0
  209. package/dist/runtime/subagent-bridge.d.ts +24 -0
  210. package/dist/{agent-graph/types.js → runtime/subagent-bridge.js} +1 -1
  211. package/dist/runtime/text-completion.d.ts +22 -0
  212. package/dist/{agent-situation/types.js → runtime/text-completion.js} +1 -1
  213. package/dist/runtime/token-estimator.d.ts +25 -0
  214. package/dist/runtime/token-estimator.js +75 -0
  215. package/dist/runtime/tool-trace-store.d.ts +22 -0
  216. package/dist/runtime/tool-trace-store.js +242 -0
  217. package/dist/runtime/trace-summary.d.ts +5 -0
  218. package/dist/runtime/trace-summary.js +74 -0
  219. package/dist/runtime/turn-text.d.ts +23 -0
  220. package/dist/runtime/turn-text.js +41 -0
  221. package/dist/storage/database.d.ts +67 -0
  222. package/dist/storage/database.js +158 -0
  223. package/package.json +20 -34
  224. package/dist/agent-graph/alias-write.d.ts +0 -3
  225. package/dist/agent-graph/alias-write.js +0 -234
  226. package/dist/agent-graph/entity-resolve.d.ts +0 -3
  227. package/dist/agent-graph/entity-resolve.js +0 -271
  228. package/dist/agent-graph/errors.d.ts +0 -4
  229. package/dist/agent-graph/errors.js +0 -11
  230. package/dist/agent-graph/graph-query.d.ts +0 -5
  231. package/dist/agent-graph/graph-query.js +0 -360
  232. package/dist/agent-graph/index.d.ts +0 -6
  233. package/dist/agent-situation/builder.d.ts +0 -8
  234. package/dist/agent-situation/builder.js +0 -239
  235. package/dist/agent-situation/cache-key.d.ts +0 -4
  236. package/dist/agent-situation/cache-key.js +0 -64
  237. package/dist/agent-situation/index.d.ts +0 -7
  238. package/dist/agent-situation/index.js +0 -28
  239. package/dist/agent-situation/packet-store.d.ts +0 -27
  240. package/dist/agent-situation/packet-store.js +0 -228
  241. package/dist/agent-situation/ranking-policy.d.ts +0 -18
  242. package/dist/agent-situation/ranking-policy.js +0 -75
  243. package/dist/agent-situation/source-readers.d.ts +0 -52
  244. package/dist/agent-situation/source-readers.js +0 -300
  245. package/dist/agent-situation/types.d.ts +0 -195
  246. package/dist/cases/wiki-page-index.d.ts +0 -37
  247. package/dist/cases/wiki-page-index.js +0 -201
  248. package/dist/config-loader.d.ts +0 -68
  249. package/dist/config-loader.js +0 -239
  250. package/dist/connectors/event-index.d.ts +0 -24
  251. package/dist/connectors/event-index.js +0 -245
  252. package/dist/connectors/raw-query.d.ts +0 -40
  253. package/dist/connectors/raw-query.js +0 -412
  254. package/dist/connectors/types.d.ts +0 -117
  255. package/dist/connectors/types.js +0 -3
  256. package/dist/context-compile/boundary-defaults.d.ts +0 -12
  257. package/dist/context-compile/boundary-defaults.js +0 -105
  258. package/dist/context-compile/channel-grant.js +0 -66
  259. package/dist/context-compile/compiler-policy.d.ts +0 -39
  260. package/dist/context-compile/compiler-policy.js +0 -147
  261. package/dist/context-compile/compiler.d.ts +0 -18
  262. package/dist/context-compile/compiler.js +0 -404
  263. package/dist/context-compile/index.d.ts +0 -10
  264. package/dist/context-compile/index.js +0 -26
  265. package/dist/context-compile/packet-store.d.ts +0 -8
  266. package/dist/context-compile/packet-store.js +0 -239
  267. package/dist/context-compile/ref.d.ts +0 -7
  268. package/dist/context-compile/ref.js +0 -102
  269. package/dist/context-compile/source-readers.d.ts +0 -70
  270. package/dist/context-compile/source-readers.js +0 -840
  271. package/dist/context-compile/types.d.ts +0 -155
  272. package/dist/context-compile/types.js +0 -11
  273. package/dist/context-compile/visibility.d.ts +0 -21
  274. package/dist/context-compile/visibility.js +0 -224
  275. package/dist/decision-tracker.d.ts +0 -206
  276. package/dist/decision-tracker.js +0 -481
  277. package/dist/edges/ref-validation.d.ts +0 -7
  278. package/dist/edges/ref-validation.js +0 -321
  279. package/dist/edges/store.d.ts +0 -8
  280. package/dist/edges/store.js +0 -129
  281. package/dist/embedding-client.d.ts +0 -28
  282. package/dist/embedding-client.js +0 -53
  283. package/dist/embedding-server/index.d.ts +0 -67
  284. package/dist/embedding-server/index.js +0 -405
  285. package/dist/embedding-server/mobile/auth.d.ts +0 -44
  286. package/dist/embedding-server/mobile/auth.js +0 -150
  287. package/dist/embedding-server/mobile/daemon.d.ts +0 -129
  288. package/dist/embedding-server/mobile/daemon.js +0 -313
  289. package/dist/embedding-server/mobile/output-parser.d.ts +0 -115
  290. package/dist/embedding-server/mobile/output-parser.js +0 -241
  291. package/dist/embedding-server/mobile/session-api.d.ts +0 -57
  292. package/dist/embedding-server/mobile/session-api.js +0 -261
  293. package/dist/embedding-server/mobile/session-manager.d.ts +0 -138
  294. package/dist/embedding-server/mobile/session-manager.js +0 -378
  295. package/dist/embedding-server/mobile/websocket-handler.d.ts +0 -137
  296. package/dist/embedding-server/mobile/websocket-handler.js +0 -486
  297. package/dist/embeddings.d.ts +0 -68
  298. package/dist/embeddings.js +0 -244
  299. package/dist/entities/audit-metrics.d.ts +0 -68
  300. package/dist/entities/audit-metrics.js +0 -124
  301. package/dist/entities/candidate-generator.d.ts +0 -8
  302. package/dist/entities/candidate-generator.js +0 -24
  303. package/dist/entities/entity-linked-decision-counts.d.ts +0 -2
  304. package/dist/entities/entity-linked-decision-counts.js +0 -29
  305. package/dist/entities/entity-list.d.ts +0 -22
  306. package/dist/entities/entity-list.js +0 -3
  307. package/dist/entities/entity-orphan-list.d.ts +0 -17
  308. package/dist/entities/entity-orphan-list.js +0 -3
  309. package/dist/entities/entity-search.d.ts +0 -20
  310. package/dist/entities/entity-search.js +0 -3
  311. package/dist/entities/errors.d.ts +0 -55
  312. package/dist/entities/errors.js +0 -100
  313. package/dist/entities/lineage-store.d.ts +0 -28
  314. package/dist/entities/lineage-store.js +0 -204
  315. package/dist/entities/normalization.d.ts +0 -14
  316. package/dist/entities/normalization.js +0 -63
  317. package/dist/entities/policy-store.d.ts +0 -8
  318. package/dist/entities/policy-store.js +0 -75
  319. package/dist/entities/policy-types.d.ts +0 -72
  320. package/dist/entities/policy-types.js +0 -59
  321. package/dist/entities/projection.d.ts +0 -8
  322. package/dist/entities/projection.js +0 -82
  323. package/dist/entities/provenance-query.d.ts +0 -32
  324. package/dist/entities/provenance-query.js +0 -3
  325. package/dist/entities/read-identity.d.ts +0 -31
  326. package/dist/entities/read-identity.js +0 -264
  327. package/dist/entities/recall-bridge.d.ts +0 -5
  328. package/dist/entities/recall-bridge.js +0 -171
  329. package/dist/entities/resolution-engine.d.ts +0 -8
  330. package/dist/entities/resolution-engine.js +0 -3
  331. package/dist/entities/rollback-preview.d.ts +0 -35
  332. package/dist/entities/rollback-preview.js +0 -3
  333. package/dist/entities/source-locator.d.ts +0 -3
  334. package/dist/entities/source-locator.js +0 -32
  335. package/dist/entities/store.d.ts +0 -69
  336. package/dist/entities/store.js +0 -544
  337. package/dist/entities/types.d.ts +0 -166
  338. package/dist/entities/types.js +0 -30
  339. package/dist/memory/extraction-prompt.d.ts +0 -4
  340. package/dist/memory/extraction-prompt.js +0 -89
  341. package/dist/memory/scope-backfill.d.ts +0 -14
  342. package/dist/memory/scope-backfill.js +0 -3
  343. package/dist/memory/scope-store.d.ts +0 -4
  344. package/dist/memory/scope-store.js +0 -9
  345. package/dist/memory-store.d.ts +0 -87
  346. package/dist/memory-store.js +0 -78
  347. package/dist/model-runs/store.d.ts +0 -13
  348. package/dist/model-runs/tool-trace-store.d.ts +0 -4
  349. package/dist/model-runs/tool-trace-store.js +0 -118
  350. package/dist/model-runs/types.d.ts +0 -72
  351. package/dist/notification-manager.d.ts +0 -7
  352. package/dist/notification-manager.js +0 -12
  353. package/dist/outcome-tracker.d.ts +0 -151
  354. package/dist/outcome-tracker.js +0 -271
  355. package/dist/query-intent.d.ts +0 -23
  356. package/dist/query-intent.js +0 -114
  357. package/dist/runtime/trusted-provenance.d.ts +0 -3
  358. package/dist/runtime/trusted-provenance.js +0 -6
  359. package/dist/search/question-type.d.ts +0 -5
  360. package/dist/search/question-type.js +0 -41
  361. package/dist/test-utils.d.ts +0 -80
  362. package/dist/test-utils.js +0 -218
  363. package/dist/tier-validator.d.ts +0 -43
  364. package/dist/tier-validator.js +0 -159
  365. package/dist/time-formatter.d.ts +0 -25
  366. package/dist/time-formatter.js +0 -93
  367. /package/dist/{search → knowledge}/feedback-store.d.ts +0 -0
  368. /package/dist/{search → knowledge}/feedback-store.js +0 -0
  369. /package/dist/{search → knowledge}/ranker-features.d.ts +0 -0
  370. /package/dist/{search → knowledge}/ranker-rescore.d.ts +0 -0
  371. /package/dist/{search → knowledge}/ranker-rescore.js +0 -0
  372. /package/dist/{search → knowledge}/ranker-trainer.d.ts +0 -0
  373. /package/dist/{search → knowledge}/ranker-trainer.js +0 -0
  374. /package/dist/{search → knowledge}/search-quality.js +0 -0
@@ -8,17 +8,184 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
8
8
  return (mod && mod.__esModule) ? mod : { "default": mod };
9
9
  };
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
- exports.NodeSQLiteAdapter = void 0;
11
+ exports.NodeSQLiteAdapter = exports.CORE_MIGRATION_SOURCE = void 0;
12
12
  const path_1 = __importDefault(require("path"));
13
- const os_1 = __importDefault(require("os"));
14
13
  const fs_1 = __importDefault(require("fs"));
15
- const base_adapter_js_1 = require("./base-adapter.js");
16
14
  const node_sqlite_statement_js_1 = require("./node-sqlite-statement.js");
17
15
  const debug_logger_js_1 = require("../debug-logger.js");
18
- const embeddings_js_1 = require("../embeddings.js");
19
- const LEGACY_DB_PATH = path_1.default.join(os_1.default.homedir(), '.spinelift', 'memories.db');
20
- const DEFAULT_DB_PATH = path_1.default.join(os_1.default.homedir(), '.claude', 'mama-memory.db');
16
+ const embedder_js_1 = require("../embedding/embedder.js");
21
17
  const SQLITE_IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
18
+ // Migration 071 origin invariant, kept in ONE place so the DDL the rebuild
19
+ // writes and the shape verification that admits a table cannot drift. A row is
20
+ // valid only with a model-only origin or a complete operation/actor pair.
21
+ const TOOL_TRACES_ORIGIN_CHECK = `CHECK (
22
+ (model_run_id IS NOT NULL AND operation_id IS NULL AND actor_principal_id IS NULL)
23
+ OR (
24
+ operation_id IS NOT NULL AND length(trim(operation_id)) > 0
25
+ AND actor_principal_id IS NOT NULL AND length(trim(actor_principal_id)) > 0
26
+ )
27
+ )`;
28
+ const TOOL_TRACES_MODEL_FK = 'FOREIGN KEY (model_run_id) REFERENCES model_runs(model_run_id)';
29
+ const WORKFLOW_KIND_MIGRATION_COLUMNS = [
30
+ 'id',
31
+ 'topic',
32
+ 'decision',
33
+ 'reasoning',
34
+ 'outcome',
35
+ 'failure_reason',
36
+ 'limitation',
37
+ 'user_involvement',
38
+ 'session_id',
39
+ 'supersedes',
40
+ 'superseded_by',
41
+ 'refined_from',
42
+ 'confidence',
43
+ 'created_at',
44
+ 'updated_at',
45
+ 'needs_validation',
46
+ 'validation_attempts',
47
+ 'last_validated_at',
48
+ 'usage_count',
49
+ 'trust_context',
50
+ 'usage_success',
51
+ 'usage_failure',
52
+ 'time_saved',
53
+ 'evidence',
54
+ 'alternatives',
55
+ 'risks',
56
+ 'event_date',
57
+ 'kind',
58
+ 'status',
59
+ 'summary',
60
+ 'is_static',
61
+ 'event_datetime',
62
+ 'agent_id',
63
+ 'model_run_id',
64
+ 'envelope_hash',
65
+ 'gateway_call_id',
66
+ 'source_refs_json',
67
+ 'provenance_json',
68
+ 'item_id',
69
+ 'record_kind',
70
+ 'payload_json',
71
+ 'applies_from',
72
+ 'applies_until',
73
+ 'duration_days',
74
+ ];
75
+ function normalizeSqlText(sql) {
76
+ return sql.replace(/\s+/g, '').toLowerCase();
77
+ }
78
+ /**
79
+ * Split a CREATE TABLE body (the text between the outermost parentheses) into
80
+ * its top-level column and table-constraint clauses. Respects nested
81
+ * parentheses (CHECK expressions), and single/double/backtick/bracket quoted
82
+ * identifiers and string literals. This is a targeted parser for reconstructing
83
+ * ONE known table, not a general SQL engine.
84
+ */
85
+ function splitCreateTableClauses(createSql) {
86
+ const open = createSql.indexOf('(');
87
+ const close = createSql.lastIndexOf(')');
88
+ if (open < 0 || close <= open) {
89
+ return [];
90
+ }
91
+ const body = createSql.slice(open + 1, close);
92
+ const clauses = [];
93
+ let depth = 0;
94
+ let current = '';
95
+ let quote = null;
96
+ for (let i = 0; i < body.length; i++) {
97
+ const ch = body[i];
98
+ if (quote) {
99
+ current += ch;
100
+ if (ch === quote) {
101
+ // Doubled quote is an escaped quote, not a close (SQLite identifier/string rule).
102
+ if (quote !== ']' && body[i + 1] === quote) {
103
+ current += body[++i];
104
+ }
105
+ else {
106
+ quote = null;
107
+ }
108
+ }
109
+ continue;
110
+ }
111
+ if (ch === "'" || ch === '"' || ch === '`') {
112
+ quote = ch;
113
+ current += ch;
114
+ continue;
115
+ }
116
+ if (ch === '[') {
117
+ quote = ']';
118
+ current += ch;
119
+ continue;
120
+ }
121
+ if (ch === '(') {
122
+ depth++;
123
+ current += ch;
124
+ continue;
125
+ }
126
+ if (ch === ')') {
127
+ depth--;
128
+ current += ch;
129
+ continue;
130
+ }
131
+ if (ch === ',' && depth === 0) {
132
+ if (current.trim()) {
133
+ clauses.push(current.trim());
134
+ }
135
+ current = '';
136
+ continue;
137
+ }
138
+ current += ch;
139
+ }
140
+ if (current.trim()) {
141
+ clauses.push(current.trim());
142
+ }
143
+ return clauses;
144
+ }
145
+ const TABLE_CONSTRAINT_KEYWORDS = new Set(['constraint', 'primary', 'unique', 'check', 'foreign']);
146
+ function clauseIsTableConstraint(clause) {
147
+ const firstToken = clause.trimStart().split(/[\s(]/, 1)[0].toLowerCase();
148
+ return TABLE_CONSTRAINT_KEYWORDS.has(firstToken);
149
+ }
150
+ /**
151
+ * Quote a decoded identifier for safe emission in SQL. Any column/table name we
152
+ * recovered from a CREATE TABLE clause or sqlite_master (possibly containing a
153
+ * space, an embedded double quote, or a reserved word) is re-quoted with SQLite
154
+ * double-quote rules: wrap in double quotes and double any embedded double quote.
155
+ * clauseColumnName strips quoting, so the INSERT/SELECT column list MUST re-quote
156
+ * or a name like `runtime note` becomes two bare tokens and breaks the migration.
157
+ */
158
+ function quoteSqlIdentifier(name) {
159
+ return `"${name.replace(/"/g, '""')}"`;
160
+ }
161
+ /** Column name of a column-definition clause, stripping quotes/brackets. */
162
+ function clauseColumnName(clause) {
163
+ const trimmed = clause.trimStart();
164
+ const first = trimmed[0];
165
+ if (first === '"' || first === '`' || first === "'") {
166
+ let name = '';
167
+ for (let i = 1; i < trimmed.length; i++) {
168
+ if (trimmed[i] === first) {
169
+ if (trimmed[i + 1] === first) {
170
+ name += first;
171
+ i++;
172
+ }
173
+ else {
174
+ break;
175
+ }
176
+ }
177
+ else {
178
+ name += trimmed[i];
179
+ }
180
+ }
181
+ return name;
182
+ }
183
+ if (first === '[') {
184
+ const end = trimmed.indexOf(']');
185
+ return end > 0 ? trimmed.slice(1, end) : trimmed.slice(1);
186
+ }
187
+ return trimmed.split(/[\s(]/, 1)[0];
188
+ }
22
189
  // Prefer better-sqlite3 (includes FTS5) over node:sqlite (lacks FTS5)
23
190
  let BetterSQLite3 = null;
24
191
  let DatabaseSync = null;
@@ -82,12 +249,15 @@ class NodeSQLiteConnection {
82
249
  return this.connected;
83
250
  }
84
251
  }
85
- class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
252
+ /** The core is one source among others; it is only first because others build on it. */
253
+ exports.CORE_MIGRATION_SOURCE = 'core';
254
+ class NodeSQLiteAdapter {
255
+ transactionDepth = 0;
86
256
  config;
87
257
  db = null;
88
- _vectorSearchEnabled = true;
89
258
  vectorCache = new Map();
90
259
  topicCache = new Map();
260
+ kindCache = new Map();
91
261
  // Effective status (status, falling back to outcome) per decision rowid. Used as a
92
262
  // search-time optimization only - recallMemory's post-filter stays the authority
93
263
  // (this cache can lag a status UPDATE until the next reloadVectorCache).
@@ -95,23 +265,26 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
95
265
  decisionsHasStatusColumns = false;
96
266
  decisionsColumnInfoChecked = false;
97
267
  constructor(config = {}) {
98
- super();
99
268
  this.config = config;
100
269
  }
101
- get vectorSearchEnabled() {
102
- return this._vectorSearchEnabled;
103
- }
104
270
  getDbPath() {
105
271
  const envPath = process.env.MAMA_DB_PATH || process.env.MAMA_DATABASE_PATH;
106
272
  const configPath = this.config.dbPath;
107
- let expandedEnvPath = envPath;
108
- if (envPath) {
109
- expandedEnvPath = envPath.replace(/\$\{HOME\}/g, os_1.default.homedir()).replace(/^~/, os_1.default.homedir());
110
- }
111
- const targetPath = configPath || expandedEnvPath || DEFAULT_DB_PATH;
112
- if (!configPath && !envPath && fs_1.default.existsSync(LEGACY_DB_PATH)) {
113
- (0, debug_logger_js_1.info)('[node-sqlite-adapter] Found legacy database at ~/.spinelift/memories.db, using it for backward compatibility');
114
- return LEGACY_DB_PATH;
273
+ const targetPath = configPath || envPath;
274
+ // The adapter used to expand `~` and `${HOME}` here, which made a shared library
275
+ // the thing that decides what home means. Whoever set the variable knows; this
276
+ // says so rather than guessing on their behalf.
277
+ if (targetPath &&
278
+ (targetPath === '~' || targetPath.startsWith('~/') || targetPath.includes('${HOME}'))) {
279
+ throw new Error(`Database path is not resolved (${targetPath}). ` +
280
+ 'Expand it where the home directory is known and pass the result.');
281
+ }
282
+ if (!targetPath) {
283
+ // There used to be a default here: ~/.claude/mama-memory.db. A shared core that
284
+ // knows where one product keeps its database is not shared, and a caller who
285
+ // forgot to say got a database it never named -- which is worse than an error,
286
+ // because it works.
287
+ throw new Error('No database path. Pass dbPath, or set MAMA_DB_PATH before opening the database.');
115
288
  }
116
289
  return targetPath;
117
290
  }
@@ -155,19 +328,25 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
155
328
  if (!this.isConnected()) {
156
329
  throw new Error('Database not connected');
157
330
  }
158
- if (!this.decisionsColumnInfoChecked) {
159
- this.refreshDecisionColumnInfo();
160
- }
331
+ const decisionCols = this.refreshDecisionColumnInfo();
332
+ const kindSelect = decisionCols.has('kind') ? 'kind' : 'NULL AS kind';
161
333
  const cacheSelect = this.decisionsHasStatusColumns
162
- ? 'SELECT topic, status, outcome FROM decisions WHERE rowid = ?'
163
- : 'SELECT topic, NULL AS status, NULL AS outcome FROM decisions WHERE rowid = ?';
334
+ ? `SELECT topic, ${kindSelect}, status, outcome FROM decisions WHERE rowid = ?`
335
+ : `SELECT topic, ${kindSelect}, NULL AS status, NULL AS outcome FROM decisions WHERE rowid = ?`;
164
336
  const row = this.prepare(cacheSelect).get(rowid);
165
337
  if (!row) {
166
338
  this.statusCache.delete(rowid);
167
339
  this.topicCache.delete(rowid);
340
+ this.kindCache.delete(rowid);
168
341
  return;
169
342
  }
170
343
  this.topicCache.set(rowid, row.topic);
344
+ if (row.kind) {
345
+ this.kindCache.set(rowid, row.kind);
346
+ }
347
+ else {
348
+ this.kindCache.delete(rowid);
349
+ }
171
350
  const effectiveStatus = row.status || row.outcome;
172
351
  if (effectiveStatus) {
173
352
  this.statusCache.set(rowid, effectiveStatus);
@@ -195,6 +374,7 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
195
374
  if (tableCheck.length === 0) {
196
375
  this.vectorCache.clear();
197
376
  this.topicCache.clear();
377
+ this.kindCache.clear();
198
378
  this.statusCache.clear();
199
379
  return;
200
380
  }
@@ -215,15 +395,19 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
215
395
  // A missing column just leaves statusCache empty; the api-layer post-filter
216
396
  // remains the authority.
217
397
  this.topicCache.clear();
398
+ this.kindCache.clear();
218
399
  this.statusCache.clear();
219
400
  const decisionCols = this.refreshDecisionColumnInfo();
401
+ const kindSelect = decisionCols.has('kind') ? 'kind' : 'NULL AS kind';
220
402
  const statusSelect = decisionCols.has('status') ? 'status' : 'NULL AS status';
221
403
  const outcomeSelect = decisionCols.has('outcome') ? 'outcome' : 'NULL AS outcome';
222
404
  const topicRows = this.db
223
- .prepare(`SELECT rowid, topic, ${statusSelect}, ${outcomeSelect} FROM decisions`)
405
+ .prepare(`SELECT rowid, topic, ${kindSelect}, ${statusSelect}, ${outcomeSelect} FROM decisions`)
224
406
  .all();
225
407
  for (const row of topicRows) {
226
408
  this.topicCache.set(row.rowid, row.topic);
409
+ if (row.kind)
410
+ this.kindCache.set(row.rowid, row.kind);
227
411
  const effectiveStatus = row.status || row.outcome;
228
412
  if (effectiveStatus) {
229
413
  this.statusCache.set(row.rowid, effectiveStatus);
@@ -233,7 +417,7 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
233
417
  const elapsed = Date.now() - start;
234
418
  (0, debug_logger_js_1.info)(`[node-sqlite-adapter] Vector cache loaded: ${count} embeddings in ${elapsed}ms`);
235
419
  if (count > CACHE_WARN_THRESHOLD) {
236
- (0, debug_logger_js_1.warn)(`[node-sqlite-adapter] Vector cache holds ${count} embeddings — consider LRU eviction or on-demand loading for large datasets`);
420
+ (0, debug_logger_js_1.warn)(`[node-sqlite-adapter] Vector cache holds ${count} embeddings -- consider LRU eviction or on-demand loading for large datasets`);
237
421
  }
238
422
  }
239
423
  disconnect() {
@@ -259,30 +443,68 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
259
443
  this.db.exec(sql);
260
444
  }
261
445
  transaction(fn) {
446
+ return this.runTransaction(fn, 'deferred');
447
+ }
448
+ transactionImmediate(fn) {
449
+ return this.runTransaction(fn, 'immediate');
450
+ }
451
+ runTransaction(fn, mode) {
262
452
  if (!this.isConnected()) {
263
453
  throw new Error('Database not connected');
264
454
  }
265
- this.exec('BEGIN TRANSACTION');
455
+ const depth = this.transactionDepth;
456
+ const vectorSnapshot = new Map(this.vectorCache);
457
+ const topicSnapshot = new Map(this.topicCache);
458
+ const kindSnapshot = new Map(this.kindCache);
459
+ const statusSnapshot = new Map(this.statusCache);
460
+ const savepoint = `mama_nested_${depth}`;
461
+ this.exec(depth === 0
462
+ ? mode === 'immediate'
463
+ ? 'BEGIN IMMEDIATE'
464
+ : 'BEGIN TRANSACTION'
465
+ : `SAVEPOINT ${savepoint}`);
466
+ this.transactionDepth += 1;
266
467
  try {
267
468
  const result = fn();
268
469
  if (((typeof result === 'object' && result !== null) || typeof result === 'function') &&
269
470
  typeof result.then === 'function') {
270
471
  throw new Error('DatabaseAdapter.transaction() callbacks must be synchronous');
271
472
  }
272
- this.exec('COMMIT');
473
+ this.exec(depth === 0 ? 'COMMIT' : `RELEASE SAVEPOINT ${savepoint}`);
474
+ this.transactionDepth = depth;
273
475
  return result;
274
476
  }
275
477
  catch (error) {
478
+ this.vectorCache = vectorSnapshot;
479
+ this.topicCache = topicSnapshot;
480
+ this.kindCache = kindSnapshot;
481
+ this.statusCache = statusSnapshot;
482
+ this.transactionDepth = depth;
483
+ let cleanupError;
276
484
  try {
277
- this.exec('ROLLBACK');
485
+ this.exec(depth === 0 ? 'ROLLBACK' : `ROLLBACK TO SAVEPOINT ${savepoint}`);
486
+ if (depth > 0) {
487
+ this.exec(`RELEASE SAVEPOINT ${savepoint}`);
488
+ }
489
+ }
490
+ catch (rollbackError) {
491
+ cleanupError = rollbackError;
278
492
  }
279
- catch {
280
- // Preserve the original transaction failure when rollback also fails.
493
+ if (cleanupError !== undefined) {
494
+ const failures = [error, cleanupError];
495
+ try {
496
+ this.disconnect();
497
+ }
498
+ catch (disconnectError) {
499
+ failures.push(disconnectError);
500
+ this.db = null;
501
+ }
502
+ throw new AggregateError(failures, 'Transaction settlement and rollback cleanup both failed; adapter disconnected');
281
503
  }
282
504
  throw error;
283
505
  }
284
506
  }
285
- vectorSearch(embedding, limit = 5, topicPrefix, excludeStatuses) {
507
+ vectorSearch(embedding, limit = 5, topicPrefix, excludeStatuses, kind) {
286
508
  if (!this.isConnected()) {
287
509
  throw new Error('Database not connected');
288
510
  }
@@ -300,6 +522,13 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
300
522
  if (!topic || !topic.startsWith(topicPrefix))
301
523
  continue;
302
524
  }
525
+ // Pre-filter by memory kind so unrelated records cannot consume the top-K
526
+ // candidate slots before the recall layer applies its requested kind.
527
+ if (kind !== undefined &&
528
+ (Array.isArray(kind)
529
+ ? !kind.some((value) => value === this.kindCache.get(rowid))
530
+ : this.kindCache.get(rowid) !== kind))
531
+ continue;
303
532
  // Pre-filter by effective status so superseded history does not occupy
304
533
  // top-K slots (the api-layer post-filter remains the authority)
305
534
  if (excluded) {
@@ -307,7 +536,7 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
307
536
  if (status && excluded.has(status))
308
537
  continue;
309
538
  }
310
- const similarity = (0, embeddings_js_1.cosineSimilarity)(candidate, queryVector);
539
+ const similarity = (0, embedder_js_1.cosineSimilarity)(candidate, queryVector);
311
540
  if (bestMatches.length < effectiveLimit) {
312
541
  bestMatches.push({ rowid, similarity, distance: 1 - similarity });
313
542
  if (bestMatches.length === effectiveLimit) {
@@ -349,24 +578,93 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
349
578
  const result = this.prepare('SELECT last_insert_rowid() as rowid').get();
350
579
  return result.rowid;
351
580
  }
352
- runMigrations(migrationsDir) {
581
+ /**
582
+ * Run migrations from one source, or from several in order.
583
+ *
584
+ * A migration belongs to whoever wrote it, and its identity is (source,
585
+ * version). One author could key on the number alone; a second cannot, because
586
+ * the next number is already spoken for. Without this there is nowhere for a
587
+ * package's own table to live except inside the core.
588
+ *
589
+ * The core's source runs first and the rest follow, so a later source can
590
+ * reference what the core declared. A plain directory means the core's own.
591
+ */
592
+ /**
593
+ * Record that one source's migration ran. Before (source, version) there was
594
+ * one author, so the column did not exist; a database that has not reached 085
595
+ * yet still has the old shape and is still the core's.
596
+ */
597
+ stampMigration(sourceName, version) {
598
+ if (this.tableColumns('schema_version').has('source')) {
599
+ this.prepare('INSERT OR IGNORE INTO schema_version (source, version) VALUES (?, ?)').run(sourceName, version);
600
+ return;
601
+ }
602
+ this.prepare('INSERT OR IGNORE INTO schema_version (version) VALUES (?)').run(version);
603
+ }
604
+ runMigrations(sources) {
605
+ const list = typeof sources === 'string' ? [{ name: exports.CORE_MIGRATION_SOURCE, dir: sources }] : sources;
606
+ for (const source of list) {
607
+ this.runMigrationSource(source.dir, source.name);
608
+ }
609
+ }
610
+ runMigrationSource(migrationsDir, sourceName) {
353
611
  if (!this.isConnected()) {
354
612
  throw new Error('Database not connected');
355
613
  }
614
+ const isCore = sourceName === exports.CORE_MIGRATION_SOURCE;
356
615
  const tables = this.prepare(`
357
616
  SELECT name FROM sqlite_master
358
617
  WHERE type='table' AND name='schema_version'
359
618
  `).all();
360
619
  let currentVersion = 0;
361
620
  if (tables.length > 0) {
362
- const version = this.prepare('SELECT MAX(version) as version FROM schema_version').get();
621
+ // Progress is per source. A shared MAX() would let one author's numbering
622
+ // declare another author's migrations already applied.
623
+ const hasSource = this.tableColumns('schema_version').has('source');
624
+ const version = (hasSource
625
+ ? this.prepare('SELECT MAX(version) as version FROM schema_version WHERE source = ?').get(sourceName)
626
+ : this.prepare('SELECT MAX(version) as version FROM schema_version').get());
363
627
  currentVersion = version?.version || 0;
364
628
  }
365
- (0, debug_logger_js_1.info)(`[node-sqlite-adapter] Current schema version: ${currentVersion}`);
629
+ (0, debug_logger_js_1.info)(`[node-sqlite-adapter] Current schema version (${sourceName}): ${currentVersion}`);
366
630
  const migrationFiles = fs_1.default
367
631
  .readdirSync(migrationsDir)
368
632
  .filter((file) => file.endsWith('.sql'))
369
633
  .sort();
634
+ if (isCore &&
635
+ currentVersion >= 72 &&
636
+ fs_1.default.existsSync(path_1.default.join(migrationsDir, '072-observation-versions.sql')) &&
637
+ this.needsObservationVersionsRepair072()) {
638
+ this.recoverObservationVersionsMigration072();
639
+ }
640
+ if (isCore &&
641
+ currentVersion >= 73 &&
642
+ this.tableExists('registry_nodes') &&
643
+ fs_1.default.existsSync(path_1.default.join(migrationsDir, '073-registry-corrections.sql')) &&
644
+ this.needsRegistryCorrectionsRepair073()) {
645
+ this.recoverRegistryCorrectionsMigration073(migrationsDir);
646
+ }
647
+ if (isCore &&
648
+ currentVersion >= 74 &&
649
+ this.tableExists('twin_edges') &&
650
+ fs_1.default.existsSync(path_1.default.join(migrationsDir, '074-work-graph-ref-kinds.sql')) &&
651
+ this.needsWorkGraphRefsRepair074()) {
652
+ this.recoverWorkGraphRefsMigration074();
653
+ }
654
+ if (isCore &&
655
+ currentVersion >= 77 &&
656
+ this.tableExists('decisions') &&
657
+ fs_1.default.existsSync(path_1.default.join(migrationsDir, '077-legacy-record-kind.sql')) &&
658
+ !this.legacyRecordKindShape077()) {
659
+ this.recoverLegacyRecordKindMigration077();
660
+ }
661
+ if (isCore &&
662
+ currentVersion >= 79 &&
663
+ this.tableExists('twin_edges') &&
664
+ fs_1.default.existsSync(path_1.default.join(migrationsDir, '079-twin-edge-relations.sql')) &&
665
+ this.needsTwinEdgeRelationsRepair079()) {
666
+ this.recoverTwinEdgeRelationsMigration079();
667
+ }
370
668
  for (const file of migrationFiles) {
371
669
  const versionMatch = file.match(/^(\d+)-/);
372
670
  if (!versionMatch) {
@@ -376,6 +674,100 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
376
674
  if (version <= currentVersion) {
377
675
  continue;
378
676
  }
677
+ if (isCore && version === 98) {
678
+ const decisionColumns = this.tableColumns('decisions');
679
+ if (!this.tableExists('decisions_fts') ||
680
+ !WORKFLOW_KIND_MIGRATION_COLUMNS.every((column) => decisionColumns.has(column))) {
681
+ (0, debug_logger_js_1.warn)(`[node-sqlite-adapter] Migration ${file} deferred: the full decisions projection is not present`);
682
+ continue;
683
+ }
684
+ this.rebuildWorkflowMemoryKind098(migrationsDir);
685
+ this.stampMigration(sourceName, version);
686
+ (0, debug_logger_js_1.info)(`[node-sqlite-adapter] Migration ${file} reconciled successfully`);
687
+ continue;
688
+ }
689
+ // Runtime MetricsStore may already own project_id/channel_id. Reconcile
690
+ // this additive migration atomically instead of accepting a duplicate skip.
691
+ if (isCore && version === 68) {
692
+ if (!this.tableExists('tool_traces')) {
693
+ // Legacy version ledgers can have skipped 033; the structural repair
694
+ // below creates its table before reconciling 068. Do not stamp it yet.
695
+ continue;
696
+ }
697
+ this.recoverToolTraceDiagnosticsMigration068();
698
+ (0, debug_logger_js_1.info)(`[node-sqlite-adapter] Migration ${file} reconciled successfully`);
699
+ continue;
700
+ }
701
+ // Relaxing tool_traces.model_run_id to nullable and adding the operation
702
+ // origin CHECK needs a table rebuild that also preserves any extra runtime
703
+ // columns/indexes a live database carries - more than the static SQL file
704
+ // can express. Reconcile with a dynamic rebuild instead of exec'ing the SQL.
705
+ if (isCore && version === 71) {
706
+ if (!this.tableExists('tool_traces')) {
707
+ // A legacy ledger may have skipped 033; the structural repair below
708
+ // creates the table first, then this rebuild reconciles it. Do not
709
+ // stamp 71 yet.
710
+ continue;
711
+ }
712
+ this.recoverServiceOperationOriginsMigration071();
713
+ (0, debug_logger_js_1.info)(`[node-sqlite-adapter] Migration ${file} reconciled successfully`);
714
+ continue;
715
+ }
716
+ if (isCore && version === 72) {
717
+ // `observation_versions` is the core's own table. This used to skip the
718
+ // migration entirely when `connector_event_index` was absent -- back when
719
+ // the connector index WAS the substrate -- so a consumer without the
720
+ // connector package silently got no observations at all, and every later
721
+ // migration touching them was swallowed as "no such table".
722
+ if (!this.tableExists('observation_versions')) {
723
+ this.exec(fs_1.default.readFileSync(path_1.default.join(migrationsDir, file), 'utf8'));
724
+ this.stampMigration(sourceName, version);
725
+ (0, debug_logger_js_1.info)(`[node-sqlite-adapter] Migration ${file} applied`);
726
+ continue;
727
+ }
728
+ this.recoverObservationVersionsMigration072();
729
+ (0, debug_logger_js_1.info)(`[node-sqlite-adapter] Migration ${file} reconciled successfully`);
730
+ continue;
731
+ }
732
+ if (isCore && version === 73) {
733
+ this.recoverRegistryCorrectionsMigration073(migrationsDir);
734
+ (0, debug_logger_js_1.info)(`[node-sqlite-adapter] Migration ${file} reconciled successfully`);
735
+ continue;
736
+ }
737
+ if (isCore && version === 74) {
738
+ if (!this.tableExists('twin_edges')) {
739
+ continue;
740
+ }
741
+ this.recoverWorkGraphRefsMigration074();
742
+ (0, debug_logger_js_1.info)(`[node-sqlite-adapter] Migration ${file} reconciled successfully`);
743
+ continue;
744
+ }
745
+ if (isCore && version === 77) {
746
+ this.recoverLegacyRecordKindMigration077();
747
+ (0, debug_logger_js_1.info)(`[node-sqlite-adapter] Migration ${file} reconciled successfully`);
748
+ continue;
749
+ }
750
+ // Extending the twin_edges relation CHECK needs the same dynamic rebuild
751
+ // as 074 so custom columns, indexes, triggers, and FK children survive;
752
+ // the static SQL file cannot express that. Reconcile instead of exec'ing.
753
+ if (isCore && version === 79) {
754
+ if (!this.tableExists('twin_edges')) {
755
+ continue;
756
+ }
757
+ this.recoverTwinEdgeRelationsMigration079();
758
+ (0, debug_logger_js_1.info)(`[node-sqlite-adapter] Migration ${file} reconciled successfully`);
759
+ continue;
760
+ }
761
+ if (isCore && version === 92) {
762
+ // Legacy high-version stores may carry only a skeletal decisions table.
763
+ // An external-content FTS rebuild cannot read columns that do not exist;
764
+ // leave this migration unstamped until the canonical table is present.
765
+ const columns = this.tableColumns('decisions');
766
+ if (!['topic', 'decision', 'reasoning'].every((column) => columns.has(column))) {
767
+ (0, debug_logger_js_1.warn)(`[node-sqlite-adapter] Migration ${file} deferred: decisions content is incomplete`);
768
+ continue;
769
+ }
770
+ }
379
771
  const migrationPath = path_1.default.join(migrationsDir, file);
380
772
  const migrationSQL = fs_1.default.readFileSync(migrationPath, 'utf8');
381
773
  (0, debug_logger_js_1.info)(`[node-sqlite-adapter] Applying migration: ${file}`);
@@ -383,7 +775,7 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
383
775
  this.exec('BEGIN TRANSACTION');
384
776
  this.exec(migrationSQL);
385
777
  this.exec('COMMIT');
386
- this.prepare('INSERT OR IGNORE INTO schema_version (version) VALUES (?)').run(version);
778
+ this.stampMigration(sourceName, version);
387
779
  (0, debug_logger_js_1.info)(`[node-sqlite-adapter] Migration ${file} applied successfully`);
388
780
  }
389
781
  catch (err) {
@@ -394,43 +786,9 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
394
786
  (0, debug_logger_js_1.info)(`[node-sqlite-adapter] Migration ${file} recovered successfully`);
395
787
  continue;
396
788
  }
397
- if (message.includes('duplicate column') && version === 34) {
398
- this.recoverConnectorEventScopeMigration034();
399
- (0, debug_logger_js_1.info)(`[node-sqlite-adapter] Migration ${file} recovered successfully`);
400
- continue;
401
- }
402
- if (message.includes('duplicate column') && version === 39) {
403
- this.recoverConnectorEventOperatorSeqMigration039();
404
- (0, debug_logger_js_1.info)(`[node-sqlite-adapter] Migration ${file} recovered successfully`);
405
- continue;
406
- }
407
- if (message.includes('duplicate column') && version === 62) {
408
- this.recoverConnectorEventSequencesMigration062();
409
- (0, debug_logger_js_1.info)(`[node-sqlite-adapter] Migration ${file} recovered successfully`);
410
- continue;
411
- }
412
- if (message.includes('duplicate column') && version === 67) {
413
- this.recoverConnectorEventSourceEntityIdMigration067();
414
- (0, debug_logger_js_1.info)(`[node-sqlite-adapter] Migration ${file} recovered successfully`);
415
- continue;
416
- }
417
- if (message.includes('no such column') && version === 62) {
418
- (0, debug_logger_js_1.warn)(`[node-sqlite-adapter] Migration ${file} deferred until connector structure recovery (${message})`);
419
- continue;
420
- }
421
- if (version === 63 &&
422
- (message.includes('no such column') || message.includes('no such table'))) {
423
- (0, debug_logger_js_1.warn)(`[node-sqlite-adapter] Migration ${file} deferred until connector structure recovery (${message})`);
424
- continue;
425
- }
426
- if (version === 67 &&
427
- (message.includes('no such column') || message.includes('no such table'))) {
428
- (0, debug_logger_js_1.warn)(`[node-sqlite-adapter] Migration ${file} deferred until connector structure recovery (${message})`);
429
- continue;
430
- }
431
789
  if (message.includes('duplicate column')) {
432
790
  (0, debug_logger_js_1.warn)(`[node-sqlite-adapter] Migration ${file} skipped (duplicate column - already applied)`);
433
- this.prepare('INSERT OR IGNORE INTO schema_version (version) VALUES (?)').run(version);
791
+ this.stampMigration(sourceName, version);
434
792
  continue;
435
793
  }
436
794
  if (message.includes('no such table')) {
@@ -440,14 +798,16 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
440
798
  throw new Error(`Migration ${file} failed: ${message}`);
441
799
  }
442
800
  (0, debug_logger_js_1.warn)(`[node-sqlite-adapter] Migration ${file} skipped: ALTER TABLE on non-existent table (${message})`);
443
- this.prepare('INSERT OR IGNORE INTO schema_version (version) VALUES (?)').run(version);
801
+ this.stampMigration(sourceName, version);
444
802
  continue;
445
803
  }
446
804
  (0, debug_logger_js_1.error)(`[node-sqlite-adapter] Migration ${file} failed:`, err);
447
805
  throw new Error(`Migration ${file} failed: ${message}`);
448
806
  }
449
807
  }
450
- this.repairSkippedFeatureMigrations(migrationsDir);
808
+ if (isCore) {
809
+ this.repairSkippedFeatureMigrations(migrationsDir);
810
+ }
451
811
  const embeddingsTables = this.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='embeddings'`).all();
452
812
  if (embeddingsTables.length === 0) {
453
813
  throw new Error('Embeddings table is missing after migrations. Add the required embeddings migration.');
@@ -455,6 +815,7 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
455
815
  this.migrateFromVssMemories();
456
816
  }
457
817
  repairSkippedFeatureMigrations(migrationsDir) {
818
+ this.repairRegistryIdentityStructures(migrationsDir);
458
819
  if (this.tableExists('decisions')) {
459
820
  const decisionColumns = this.tableColumns('decisions');
460
821
  const hasMissingMemoryProvenanceColumn = [
@@ -479,66 +840,61 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
479
840
  if (!this.tableExists('model_runs') || !this.tableExists('tool_traces')) {
480
841
  this.applyRepairMigration(migrationsDir, '033-create-model-runs-and-tool-traces.sql', 'model run provenance');
481
842
  }
482
- if (this.tableExists('connector_event_index')) {
483
- const connectorColumns = this.tableColumns('connector_event_index');
484
- const hasMissingConnectorScopeColumn = [
485
- 'source_cursor',
486
- 'tenant_id',
487
- 'project_id',
488
- 'memory_scope_kind',
489
- 'memory_scope_id',
490
- ].some((column) => !connectorColumns.has(column));
491
- if (hasMissingConnectorScopeColumn) {
492
- this.recoverConnectorEventScopeMigration034();
493
- (0, debug_logger_js_1.info)('[node-sqlite-adapter] Repaired skipped connector event scope migration');
494
- }
495
- const connectorColumnsAfterScopeRepair = this.tableColumns('connector_event_index');
496
- const hasMissingOperatorSeqFeature = !connectorColumnsAfterScopeRepair.has('operator_ingest_seq') ||
497
- !this.tableExists('connector_event_index_operator_seq_cursors') ||
498
- !this.indexExists('idx_connector_event_index_operator_scope_seq') ||
499
- !this.indexExists('idx_connector_event_index_operator_cursor_order') ||
500
- !this.triggerExists('trg_connector_event_index_operator_ingest_seq_ai') ||
501
- !this.triggerExists('trg_connector_event_index_operator_ingest_seq_explicit_ai') ||
502
- !this.schemaVersionExists(39);
503
- if (hasMissingOperatorSeqFeature) {
504
- this.recoverConnectorEventOperatorSeqMigration039();
505
- (0, debug_logger_js_1.info)('[node-sqlite-adapter] Repaired skipped connector event operator sequence migration');
506
- }
507
- const connectorColumnsAfterOperatorSeqRepair = this.tableColumns('connector_event_index');
508
- if (connectorColumnsAfterOperatorSeqRepair.has('source_timestamp_ms')) {
509
- const hasMissingObservationSeqFeature = !connectorColumnsAfterOperatorSeqRepair.has('operator_observation_seq') ||
510
- !this.tableExists('connector_event_index_observation_cursors') ||
511
- !this.indexExists('idx_connector_event_index_observation_seq') ||
512
- !this.triggerExists('trg_connector_event_index_operator_ingest_seq_au') ||
513
- !this.triggerExists('trg_connector_event_index_observation_seq_ai') ||
514
- !this.triggerExists('trg_connector_event_index_observation_seq_au') ||
515
- !this.triggerExists('trg_connector_event_index_observation_seq_explicit_ai') ||
516
- !this.schemaVersionExists(62);
517
- if (hasMissingObservationSeqFeature) {
518
- this.recoverConnectorEventSequencesMigration062();
519
- (0, debug_logger_js_1.info)('[node-sqlite-adapter] Repaired skipped connector event observation sequence migration');
520
- }
521
- const hasMissingLegacyRefreshFeature = !this.triggerExists('trg_connector_event_index_legacy_content_refresh_au') ||
522
- !this.schemaVersionExists(63);
523
- if (hasMissingLegacyRefreshFeature) {
524
- this.recoverConnectorEventLegacyRefreshMigration063();
525
- (0, debug_logger_js_1.info)('[node-sqlite-adapter] Repaired skipped legacy connector event refresh migration');
526
- }
527
- }
528
- const hasMissingSourceEntityIdFeature = !this.tableColumns('connector_event_index').has('source_entity_id') ||
529
- !this.indexExists('idx_connector_event_source_entity') ||
530
- !this.schemaVersionExists(67);
531
- if (hasMissingSourceEntityIdFeature) {
532
- this.recoverConnectorEventSourceEntityIdMigration067();
533
- (0, debug_logger_js_1.info)('[node-sqlite-adapter] Repaired skipped connector event source_entity_id migration');
534
- }
843
+ // Also repairs databases already stamped 68 by the former generic duplicate
844
+ // skip, which rolled back the new columns before advancing schema_version.
845
+ // Only enter the recovery transaction when something is actually missing --
846
+ // a complete database must not take the write path on every runMigrations().
847
+ if (fs_1.default.existsSync(path_1.default.join(migrationsDir, '068-tool-trace-diagnostics.sql')) &&
848
+ this.needsToolTraceDiagnosticsRepair068()) {
849
+ this.recoverToolTraceDiagnosticsMigration068();
850
+ }
851
+ // Runs after the 068 repair so tool_traces already carries the diagnostic
852
+ // columns. Only enters the rebuild when the operation-origin shape is
853
+ // actually missing - a complete database must not rebuild on every open.
854
+ if (fs_1.default.existsSync(path_1.default.join(migrationsDir, '071-service-operation-origins.sql')) &&
855
+ this.tableExists('tool_traces') &&
856
+ this.needsServiceOperationOriginsRepair071()) {
857
+ this.recoverServiceOperationOriginsMigration071();
858
+ }
859
+ if (fs_1.default.existsSync(path_1.default.join(migrationsDir, '072-observation-versions.sql')) &&
860
+ this.needsObservationVersionsRepair072()) {
861
+ this.recoverObservationVersionsMigration072();
862
+ (0, debug_logger_js_1.info)('[node-sqlite-adapter] Repaired skipped observation versions migration');
863
+ }
864
+ if (fs_1.default.existsSync(path_1.default.join(migrationsDir, '073-registry-corrections.sql')) &&
865
+ this.needsRegistryCorrectionsRepair073()) {
866
+ this.recoverRegistryCorrectionsMigration073(migrationsDir);
867
+ (0, debug_logger_js_1.info)('[node-sqlite-adapter] Repaired skipped registry corrections migration');
868
+ }
869
+ if (fs_1.default.existsSync(path_1.default.join(migrationsDir, '074-work-graph-ref-kinds.sql')) &&
870
+ this.tableExists('twin_edges') &&
871
+ this.needsWorkGraphRefsRepair074()) {
872
+ this.recoverWorkGraphRefsMigration074();
873
+ (0, debug_logger_js_1.info)('[node-sqlite-adapter] Repaired skipped work graph ref migration');
535
874
  }
536
875
  if (!this.tableExists('twin_edges')) {
537
876
  this.applyRepairMigration(migrationsDir, '035-create-twin-edges.sql', 'twin edge ledger');
538
877
  }
539
- if (!this.tableExists('agent_situation_packets') ||
540
- !this.tableExists('agent_situation_refresh_leases')) {
541
- this.applyRepairMigration(migrationsDir, '036-create-agent-situation-packets.sql', 'agent situation packet cache');
878
+ if (fs_1.default.existsSync(path_1.default.join(migrationsDir, '072-observation-versions.sql')) &&
879
+ this.needsObservationVersionsRepair072()) {
880
+ this.recoverObservationVersionsMigration072();
881
+ }
882
+ if (fs_1.default.existsSync(path_1.default.join(migrationsDir, '073-registry-corrections.sql')) &&
883
+ this.needsRegistryCorrectionsRepair073()) {
884
+ this.recoverRegistryCorrectionsMigration073(migrationsDir);
885
+ }
886
+ if (fs_1.default.existsSync(path_1.default.join(migrationsDir, '074-work-graph-ref-kinds.sql')) &&
887
+ this.needsWorkGraphRefsRepair074()) {
888
+ this.recoverWorkGraphRefsMigration074();
889
+ }
890
+ // A database stamped >= 79 whose twin_edges was rebuilt from 035/074 above
891
+ // still carries the pre-079 edge_type CHECK; extend it here so the missing
892
+ // relation list is repaired in the same pass.
893
+ if (fs_1.default.existsSync(path_1.default.join(migrationsDir, '079-twin-edge-relations.sql')) &&
894
+ this.tableExists('twin_edges') &&
895
+ this.needsTwinEdgeRelationsRepair079()) {
896
+ this.recoverTwinEdgeRelationsMigration079();
897
+ (0, debug_logger_js_1.info)('[node-sqlite-adapter] Repaired skipped twin edge relations migration');
542
898
  }
543
899
  if (!this.tableExists('context_packets')) {
544
900
  this.applyRepairMigration(migrationsDir, '037-create-context-packets.sql', 'context packet store');
@@ -571,6 +927,391 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
571
927
  }
572
928
  this.assertMigration041Complete();
573
929
  }
930
+ repairRegistryIdentityStructures(migrationsDir) {
931
+ const columnsMatch = (table, expected) => {
932
+ if (!this.tableExists(table)) {
933
+ return false;
934
+ }
935
+ const rows = this.prepare(`PRAGMA table_info("${table}")`).all();
936
+ return (rows.length === expected.length &&
937
+ expected.every(([name, type, notnull, dflt, pk]) => {
938
+ const row = rows.find((candidate) => candidate.name === name);
939
+ return Boolean(row &&
940
+ row.type.toUpperCase() === type &&
941
+ row.notnull === notnull &&
942
+ row.pk === pk &&
943
+ row.dflt_value === dflt);
944
+ }));
945
+ };
946
+ const fkMatch = (table, expected) => {
947
+ if (!this.tableExists(table)) {
948
+ return false;
949
+ }
950
+ const rows = this.prepare(`PRAGMA foreign_key_list("${table}")`).all();
951
+ return expected.every(([from, target, to, onDelete]) => rows.some((row) => row.from === from &&
952
+ row.table === target &&
953
+ row.to === to &&
954
+ row.on_delete.toUpperCase() === onDelete));
955
+ };
956
+ const indexMatch = (name, columns) => {
957
+ if (!this.indexExists(name)) {
958
+ return false;
959
+ }
960
+ const rows = this.prepare(`PRAGMA index_info("${name}")`).all();
961
+ return (rows.length === columns.length &&
962
+ rows.sort((a, b) => a.seqno - b.seqno).every((row, index) => row.name === columns[index]));
963
+ };
964
+ const registrySql = this.tableExists('registry_nodes')
965
+ ? this.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'registry_nodes'").get()
966
+ : undefined;
967
+ const aliasSql = this.tableExists('registry_aliases')
968
+ ? String(this.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'registry_aliases'").get()?.sql ?? '')
969
+ : '';
970
+ const scopeSql = this.tableExists('registry_scope_bindings')
971
+ ? String(this.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'registry_scope_bindings'").get()?.sql ?? '')
972
+ : '';
973
+ const registryMalformed = Boolean(registrySql) &&
974
+ (!columnsMatch('registry_nodes', [
975
+ ['id', 'TEXT', 0, null, 1],
976
+ ['kind', 'TEXT', 1, null, 0],
977
+ ['name', 'TEXT', 1, null, 0],
978
+ ['parent_id', 'TEXT', 0, null, 0],
979
+ ['merged_into', 'TEXT', 0, null, 0],
980
+ ['merge_reason', 'TEXT', 0, null, 0],
981
+ ['note', 'TEXT', 0, null, 0],
982
+ ['created_at', 'INTEGER', 1, null, 0],
983
+ ['updated_at', 'INTEGER', 1, null, 0],
984
+ ]) ||
985
+ !columnsMatch('registry_aliases', [
986
+ ['node_id', 'TEXT', 1, null, 0],
987
+ ['kind', 'TEXT', 1, null, 1],
988
+ ['alias', 'TEXT', 1, null, 2],
989
+ ['alias_display', 'TEXT', 1, null, 0],
990
+ ['scope_kind', 'TEXT', 1, null, 3],
991
+ ['scope_id', 'TEXT', 1, null, 4],
992
+ ['created_at', 'INTEGER', 1, null, 0],
993
+ ]) ||
994
+ !columnsMatch('registry_scope_bindings', [
995
+ ['node_id', 'TEXT', 1, null, 1],
996
+ ['scope_kind', 'TEXT', 1, null, 2],
997
+ ['scope_id', 'TEXT', 1, null, 3],
998
+ ]) ||
999
+ !fkMatch('registry_nodes', [
1000
+ ['parent_id', 'registry_nodes', 'id', 'SET NULL'],
1001
+ ['merged_into', 'registry_nodes', 'id', 'SET NULL'],
1002
+ ]) ||
1003
+ !fkMatch('registry_aliases', [['node_id', 'registry_nodes', 'id', 'CASCADE']]) ||
1004
+ !fkMatch('registry_scope_bindings', [['node_id', 'registry_nodes', 'id', 'CASCADE']]) ||
1005
+ !normalizeSqlText(String(registrySql.sql ?? '')).includes(normalizeSqlText("CHECK (kind IN ('item', 'person', 'client'))")) ||
1006
+ !normalizeSqlText(aliasSql).includes(normalizeSqlText("CHECK (kind IN ('item', 'person', 'client'))")) ||
1007
+ !normalizeSqlText(scopeSql).includes(normalizeSqlText("CHECK (scope_kind IN ('global', 'user', 'channel', 'project'))")) ||
1008
+ !indexMatch('idx_registry_nodes_kind', ['kind', 'merged_into']) ||
1009
+ !indexMatch('idx_registry_nodes_parent', ['parent_id']) ||
1010
+ !indexMatch('idx_registry_aliases_node', ['node_id']) ||
1011
+ !indexMatch('idx_registry_aliases_scope', ['kind', 'alias', 'scope_kind', 'scope_id']) ||
1012
+ !indexMatch('idx_registry_scope_lookup', ['scope_kind', 'scope_id', 'node_id']));
1013
+ if (registryMalformed) {
1014
+ this.rebuildRegistry069(migrationsDir);
1015
+ }
1016
+ const registryStructureMissing = !this.tableExists('registry_nodes') ||
1017
+ !this.tableExists('registry_aliases') ||
1018
+ !this.tableExists('registry_scope_bindings') ||
1019
+ !this.indexExists('idx_registry_nodes_kind') ||
1020
+ !this.indexExists('idx_registry_nodes_parent') ||
1021
+ !this.indexExists('idx_registry_aliases_node') ||
1022
+ !this.indexExists('idx_registry_scope_lookup');
1023
+ if (registryStructureMissing) {
1024
+ this.applyRepairMigration(migrationsDir, '069-create-registry-nodes.sql', 'registry nodes and aliases');
1025
+ }
1026
+ if (!this.tableExists('decisions')) {
1027
+ return;
1028
+ }
1029
+ if (this.tableColumns('decisions').has('item_id')) {
1030
+ const itemColumn = this.prepare('PRAGMA table_info("decisions")').all().find((column) => column.name === 'item_id');
1031
+ if (!itemColumn ||
1032
+ itemColumn.type.toUpperCase() !== 'TEXT' ||
1033
+ itemColumn.notnull !== 0 ||
1034
+ itemColumn.pk !== 0 ||
1035
+ itemColumn.dflt_value !== null) {
1036
+ throw new Error('Migration 070 cannot safely repair noncanonical decisions.item_id');
1037
+ }
1038
+ }
1039
+ const actorSql = this.tableExists('record_actors')
1040
+ ? this.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'record_actors'").get()
1041
+ : undefined;
1042
+ const actorMalformed = Boolean(actorSql) &&
1043
+ (!columnsMatch('record_actors', [
1044
+ ['record_id', 'TEXT', 1, null, 1],
1045
+ ['person_id', 'TEXT', 1, null, 2],
1046
+ ['role', 'TEXT', 1, null, 3],
1047
+ ['position', 'INTEGER', 1, '0', 0],
1048
+ ['created_at', 'INTEGER', 1, null, 0],
1049
+ ]) ||
1050
+ !fkMatch('record_actors', [['record_id', 'decisions', 'id', 'CASCADE']]) ||
1051
+ !indexMatch('idx_record_actors_person', ['person_id', 'record_id']));
1052
+ if (actorMalformed) {
1053
+ this.rebuildRecordActors070();
1054
+ }
1055
+ const needsRecordIdentityRepair = !this.tableColumns('decisions').has('item_id') ||
1056
+ !this.tableExists('record_actors') ||
1057
+ !this.indexExists('idx_decisions_item') ||
1058
+ !this.indexExists('idx_record_actors_person');
1059
+ if (!needsRecordIdentityRepair) {
1060
+ return;
1061
+ }
1062
+ this.exec('BEGIN TRANSACTION');
1063
+ try {
1064
+ if (!this.tableColumns('decisions').has('item_id')) {
1065
+ this.exec('ALTER TABLE decisions ADD COLUMN item_id TEXT');
1066
+ }
1067
+ this.exec('CREATE INDEX IF NOT EXISTS idx_decisions_item ON decisions(item_id)');
1068
+ this.exec(`CREATE TABLE IF NOT EXISTS record_actors (
1069
+ record_id TEXT NOT NULL REFERENCES decisions(id) ON DELETE CASCADE,
1070
+ person_id TEXT NOT NULL,
1071
+ role TEXT NOT NULL,
1072
+ position INTEGER NOT NULL DEFAULT 0,
1073
+ created_at INTEGER NOT NULL,
1074
+ PRIMARY KEY (record_id, person_id, role)
1075
+ )`);
1076
+ this.exec('CREATE INDEX IF NOT EXISTS idx_record_actors_person ON record_actors(person_id, record_id)');
1077
+ this.prepare('INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)').run(70, 'Record identity bindings');
1078
+ this.exec('COMMIT');
1079
+ (0, debug_logger_js_1.info)('[node-sqlite-adapter] Repaired skipped record identity migration');
1080
+ }
1081
+ catch (repairError) {
1082
+ this.exec('ROLLBACK');
1083
+ throw repairError;
1084
+ }
1085
+ }
1086
+ rebuildRegistry069(migrationsDir) {
1087
+ const originalTableSql = (table) => String(this.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name = ?").get(table)?.sql ?? '');
1088
+ const extraConstraints = (table, canonicalFragments) => {
1089
+ const clauses = splitCreateTableClauses(originalTableSql(table));
1090
+ if (clauses.length === 0) {
1091
+ throw new Error(`Migration 069 cannot parse ${table} definition`);
1092
+ }
1093
+ for (const clause of clauses.filter((candidate) => !clauseIsTableConstraint(candidate))) {
1094
+ const normalized = normalizeSqlText(clause);
1095
+ const hasInlineConstraint = ['check(', 'unique', 'collate', 'references'].some((token) => normalized.includes(token));
1096
+ const columnName = clauseColumnName(clause);
1097
+ const managedConstraint = (columnName === 'kind' && normalized.includes('check(')) ||
1098
+ (columnName === 'scope_kind' && normalized.includes('check(')) ||
1099
+ (['parent_id', 'merged_into', 'node_id'].includes(columnName) &&
1100
+ normalized.includes('references'));
1101
+ if (hasInlineConstraint &&
1102
+ !managedConstraint &&
1103
+ !canonicalFragments.some((fragment) => normalized.includes(normalizeSqlText(fragment)))) {
1104
+ throw new Error(`Migration 069 cannot safely preserve inline constraint in ${table}`);
1105
+ }
1106
+ }
1107
+ return clauses.filter((clause) => {
1108
+ if (!clauseIsTableConstraint(clause)) {
1109
+ return false;
1110
+ }
1111
+ const normalized = normalizeSqlText(clause);
1112
+ return !canonicalFragments.some((fragment) => normalized.includes(normalizeSqlText(fragment)));
1113
+ });
1114
+ };
1115
+ const nodeExtras = extraConstraints('registry_nodes', [
1116
+ "CHECK (kind IN ('item', 'person', 'client'))",
1117
+ 'FOREIGN KEY (parent_id) REFERENCES registry_nodes(id)',
1118
+ 'FOREIGN KEY (merged_into) REFERENCES registry_nodes(id)',
1119
+ 'REFERENCES registry_nodes(id) ON DELETE SET NULL',
1120
+ ]);
1121
+ const aliasExtras = this.tableExists('registry_aliases')
1122
+ ? extraConstraints('registry_aliases', [
1123
+ "CHECK (kind IN ('item', 'person', 'client'))",
1124
+ 'PRIMARY KEY (kind, alias, scope_kind, scope_id)',
1125
+ // The pre-scope shape, which is exactly what this rebuild exists to
1126
+ // convert. Listing only the post-migration spelling made every database
1127
+ // written before alias scoping look like it carried a constraint this
1128
+ // code does not understand, so the rebuild preserved it and emitted a
1129
+ // CREATE TABLE with two PRIMARY KEY clauses. The daemon could not open
1130
+ // its own older database.
1131
+ 'PRIMARY KEY (kind, alias)',
1132
+ 'FOREIGN KEY (node_id) REFERENCES registry_nodes(id)',
1133
+ 'REFERENCES registry_nodes(id) ON DELETE CASCADE',
1134
+ ])
1135
+ : [];
1136
+ const scopeExtras = this.tableExists('registry_scope_bindings')
1137
+ ? extraConstraints('registry_scope_bindings', [
1138
+ "CHECK (scope_kind IN ('global', 'user', 'channel', 'project'))",
1139
+ 'PRIMARY KEY (node_id, scope_kind, scope_id)',
1140
+ 'FOREIGN KEY (node_id) REFERENCES registry_nodes(id)',
1141
+ 'REFERENCES registry_nodes(id) ON DELETE CASCADE',
1142
+ ])
1143
+ : [];
1144
+ const nodes = this.prepare('SELECT * FROM registry_nodes').all();
1145
+ const aliases = this.tableExists('registry_aliases')
1146
+ ? this.prepare('SELECT * FROM registry_aliases').all()
1147
+ : [];
1148
+ const scopes = this.tableExists('registry_scope_bindings')
1149
+ ? this.prepare('SELECT * FROM registry_scope_bindings').all()
1150
+ : [];
1151
+ const nodeIds = new Set();
1152
+ for (const row of nodes) {
1153
+ if (typeof row.id !== 'string' ||
1154
+ !row.id ||
1155
+ !['item', 'person', 'client'].includes(String(row.kind)) ||
1156
+ typeof row.name !== 'string' ||
1157
+ !row.name.trim()) {
1158
+ throw new Error('Migration 069 source row violates registry node contract');
1159
+ }
1160
+ nodeIds.add(row.id);
1161
+ }
1162
+ const aliasKeys = new Set();
1163
+ for (const row of aliases) {
1164
+ const key = `${String(row.kind)}:${String(row.alias)}:${String(row.scope_kind ?? 'global')}:${String(row.scope_id ?? '*')}`;
1165
+ if (!nodeIds.has(String(row.node_id)) ||
1166
+ !['item', 'person', 'client'].includes(String(row.kind)) ||
1167
+ typeof row.alias !== 'string' ||
1168
+ !row.alias ||
1169
+ aliasKeys.has(key)) {
1170
+ throw new Error('Migration 069 source row violates registry alias contract');
1171
+ }
1172
+ aliasKeys.add(key);
1173
+ }
1174
+ const customObjects = this.prepare(`SELECT sql FROM sqlite_master
1175
+ WHERE type IN ('index', 'trigger')
1176
+ AND tbl_name IN ('registry_nodes', 'registry_aliases', 'registry_scope_bindings')
1177
+ AND sql IS NOT NULL
1178
+ AND name NOT IN (
1179
+ 'idx_registry_nodes_kind', 'idx_registry_nodes_parent',
1180
+ 'idx_registry_aliases_node', 'idx_registry_aliases_scope',
1181
+ 'idx_registry_scope_lookup'
1182
+ )`).all();
1183
+ let migrationSQL = fs_1.default.readFileSync(path_1.default.join(migrationsDir, '069-create-registry-nodes.sql'), 'utf8');
1184
+ const inject = (table, extras) => {
1185
+ if (extras.length === 0) {
1186
+ return;
1187
+ }
1188
+ const pattern = new RegExp(`(CREATE TABLE IF NOT EXISTS ${table} \\([\\s\\S]*?)(\\n\\);)`);
1189
+ if (!pattern.test(migrationSQL)) {
1190
+ throw new Error(`Migration 069 cannot preserve ${table} constraints`);
1191
+ }
1192
+ migrationSQL = migrationSQL.replace(pattern, `$1,\n ${extras.join(',\n ')}$2`);
1193
+ };
1194
+ inject('registry_nodes', nodeExtras);
1195
+ inject('registry_aliases', aliasExtras);
1196
+ inject('registry_scope_bindings', scopeExtras);
1197
+ const previousForeignKeys = this.readForeignKeysEnabled();
1198
+ this.exec('PRAGMA foreign_keys = OFF');
1199
+ if (this.readForeignKeysEnabled()) {
1200
+ throw new Error('Migration 069 could not disable foreign_keys before rebuild');
1201
+ }
1202
+ try {
1203
+ this.transaction(() => {
1204
+ this.exec('DROP TABLE IF EXISTS registry_scope_bindings');
1205
+ this.exec('DROP TABLE IF EXISTS registry_aliases');
1206
+ this.exec('DROP TABLE registry_nodes');
1207
+ this.exec(migrationSQL);
1208
+ const insertNode = this.prepare(`INSERT INTO registry_nodes
1209
+ (id, kind, name, parent_id, merged_into, merge_reason, note, created_at, updated_at)
1210
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`);
1211
+ for (const row of nodes) {
1212
+ insertNode.run(row.id, row.kind, row.name, row.parent_id ?? null, row.merged_into ?? null, row.merge_reason ?? null, row.note ?? null, typeof row.created_at === 'number' ? row.created_at : 0, typeof row.updated_at === 'number'
1213
+ ? row.updated_at
1214
+ : typeof row.created_at === 'number'
1215
+ ? row.created_at
1216
+ : 0);
1217
+ }
1218
+ const insertAlias = this.prepare(`INSERT INTO registry_aliases
1219
+ (node_id, kind, alias, alias_display, scope_kind, scope_id, created_at)
1220
+ VALUES (?, ?, ?, ?, ?, ?, ?)`);
1221
+ for (const row of aliases) {
1222
+ insertAlias.run(row.node_id, row.kind, row.alias, typeof row.alias_display === 'string' ? row.alias_display : row.alias, typeof row.scope_kind === 'string' ? row.scope_kind : 'global', typeof row.scope_id === 'string' ? row.scope_id : '*', typeof row.created_at === 'number' ? row.created_at : 0);
1223
+ }
1224
+ const insertScope = this.prepare('INSERT INTO registry_scope_bindings (node_id, scope_kind, scope_id) VALUES (?, ?, ?)');
1225
+ for (const row of scopes) {
1226
+ insertScope.run(row.node_id, row.scope_kind, row.scope_id);
1227
+ }
1228
+ for (const object of customObjects) {
1229
+ this.exec(object.sql);
1230
+ }
1231
+ const violations = this.prepare('PRAGMA foreign_key_check').all();
1232
+ if (violations.length > 0) {
1233
+ throw new Error('Migration 069 repair left foreign key violations');
1234
+ }
1235
+ });
1236
+ }
1237
+ finally {
1238
+ this.exec(`PRAGMA foreign_keys = ${previousForeignKeys ? 'ON' : 'OFF'}`);
1239
+ }
1240
+ }
1241
+ rebuildRecordActors070() {
1242
+ const originalSql = String(this.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='record_actors'").get()?.sql ?? '');
1243
+ const actorExtras = splitCreateTableClauses(originalSql).filter((clause) => {
1244
+ if (!clauseIsTableConstraint(clause)) {
1245
+ return false;
1246
+ }
1247
+ const normalized = normalizeSqlText(clause);
1248
+ return ![
1249
+ 'PRIMARY KEY (record_id, person_id, role)',
1250
+ 'FOREIGN KEY (record_id) REFERENCES decisions(id)',
1251
+ ].some((fragment) => normalized.includes(normalizeSqlText(fragment)));
1252
+ });
1253
+ for (const clause of splitCreateTableClauses(originalSql).filter((candidate) => !clauseIsTableConstraint(candidate))) {
1254
+ const normalized = normalizeSqlText(clause);
1255
+ const hasInlineConstraint = ['check(', 'unique', 'collate', 'references'].some((token) => normalized.includes(token));
1256
+ const canonicalReference = normalized.includes('record_idtextnotnullreferencesdecisions(id)ondeletecascade');
1257
+ if (hasInlineConstraint && !canonicalReference) {
1258
+ throw new Error('Migration 070 cannot safely preserve inline record_actors constraint');
1259
+ }
1260
+ }
1261
+ const rows = this.prepare('SELECT * FROM record_actors').all();
1262
+ const keys = new Set();
1263
+ for (const row of rows) {
1264
+ const key = `${String(row.record_id)}:${String(row.person_id)}:${String(row.role)}`;
1265
+ const decision = this.prepare('SELECT 1 FROM decisions WHERE id = ?').get(row.record_id);
1266
+ if (!decision ||
1267
+ typeof row.person_id !== 'string' ||
1268
+ !row.person_id ||
1269
+ typeof row.role !== 'string' ||
1270
+ !row.role ||
1271
+ keys.has(key)) {
1272
+ throw new Error('Migration 070 source row violates record actor contract');
1273
+ }
1274
+ keys.add(key);
1275
+ }
1276
+ const customObjects = this.prepare(`SELECT sql FROM sqlite_master
1277
+ WHERE type IN ('index', 'trigger') AND tbl_name = 'record_actors'
1278
+ AND sql IS NOT NULL AND name != 'idx_record_actors_person'`).all();
1279
+ const previousForeignKeys = this.readForeignKeysEnabled();
1280
+ this.exec('PRAGMA foreign_keys = OFF');
1281
+ if (this.readForeignKeysEnabled()) {
1282
+ throw new Error('Migration 070 could not disable foreign_keys before rebuild');
1283
+ }
1284
+ try {
1285
+ this.transaction(() => {
1286
+ this.exec('DROP TABLE record_actors');
1287
+ this.exec(`CREATE TABLE record_actors (
1288
+ record_id TEXT NOT NULL REFERENCES decisions(id) ON DELETE CASCADE,
1289
+ person_id TEXT NOT NULL,
1290
+ role TEXT NOT NULL,
1291
+ position INTEGER NOT NULL DEFAULT 0,
1292
+ created_at INTEGER NOT NULL,
1293
+ PRIMARY KEY (record_id, person_id, role)
1294
+ ${actorExtras.length > 0 ? `, ${actorExtras.join(', ')}` : ''}
1295
+ )`);
1296
+ this.exec('CREATE INDEX idx_record_actors_person ON record_actors(person_id, record_id)');
1297
+ const insert = this.prepare(`INSERT INTO record_actors
1298
+ (record_id, person_id, role, position, created_at) VALUES (?, ?, ?, ?, ?)`);
1299
+ for (const row of rows) {
1300
+ insert.run(row.record_id, row.person_id, row.role, typeof row.position === 'number' ? row.position : 0, typeof row.created_at === 'number' ? row.created_at : 0);
1301
+ }
1302
+ for (const object of customObjects) {
1303
+ this.exec(object.sql);
1304
+ }
1305
+ const violations = this.prepare('PRAGMA foreign_key_check').all();
1306
+ if (violations.length > 0) {
1307
+ throw new Error('Migration 070 repair left foreign key violations');
1308
+ }
1309
+ });
1310
+ }
1311
+ finally {
1312
+ this.exec(`PRAGMA foreign_keys = ${previousForeignKeys ? 'ON' : 'OFF'}`);
1313
+ }
1314
+ }
574
1315
  applyRepairMigration(migrationsDir, fileName, label) {
575
1316
  const migrationPath = path_1.default.join(migrationsDir, fileName);
576
1317
  if (!fs_1.default.existsSync(migrationPath)) {
@@ -589,6 +1330,29 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
589
1330
  throw new Error(`Repair migration ${fileName} failed: ${message}`);
590
1331
  }
591
1332
  }
1333
+ rebuildWorkflowMemoryKind098(migrationsDir) {
1334
+ const migrationPath = path_1.default.join(migrationsDir, '098-workflow-memory-kind.sql');
1335
+ if (!fs_1.default.existsSync(migrationPath)) {
1336
+ throw new Error('Migration 098 workflow-kind rebuild SQL is missing');
1337
+ }
1338
+ const previousForeignKeys = this.readForeignKeysEnabled();
1339
+ this.exec('PRAGMA foreign_keys = OFF');
1340
+ if (this.readForeignKeysEnabled()) {
1341
+ throw new Error('Migration 098 could not disable foreign_keys before rebuilding decisions');
1342
+ }
1343
+ try {
1344
+ this.transaction(() => {
1345
+ this.exec(fs_1.default.readFileSync(migrationPath, 'utf8'));
1346
+ });
1347
+ const violations = this.prepare('PRAGMA foreign_key_check').all();
1348
+ if (violations.length > 0) {
1349
+ throw new Error('Migration 098 left foreign key violations');
1350
+ }
1351
+ }
1352
+ finally {
1353
+ this.exec(`PRAGMA foreign_keys = ${previousForeignKeys ? 'ON' : 'OFF'}`);
1354
+ }
1355
+ }
592
1356
  operatorMemoryCommitIntentTableSql() {
593
1357
  const tableDefinition = this.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name = 'operator_memory_commit_intents'").get();
594
1358
  return tableDefinition?.sql ?? '';
@@ -647,6 +1411,1149 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
647
1411
  throw new Error('Migration 041 recovery failed: incompatible operator_memory_commit_intents table definition missing claim invariant');
648
1412
  }
649
1413
  }
1414
+ needsToolTraceDiagnosticsRepair068() {
1415
+ if (!this.tableExists('tool_traces')) {
1416
+ // The table itself is missing: let the recovery helper fail loudly.
1417
+ return true;
1418
+ }
1419
+ const columns = this.tableColumns('tool_traces');
1420
+ const hasMissingColumn = [
1421
+ 'diagnostic_json',
1422
+ 'evidence_json',
1423
+ 'catalog_revision',
1424
+ 'owner_scope',
1425
+ 'project_id',
1426
+ 'channel_id',
1427
+ ].some((column) => !columns.has(column));
1428
+ const hasMissingIndex = [
1429
+ 'idx_tool_traces_scope_recency',
1430
+ 'idx_tool_traces_channel_recency',
1431
+ ].some((indexName) => !this.indexExists(indexName));
1432
+ return hasMissingColumn || hasMissingIndex || !this.schemaVersionExists(68);
1433
+ }
1434
+ recoverToolTraceDiagnosticsMigration068() {
1435
+ this.transaction(() => {
1436
+ if (!this.tableExists('tool_traces')) {
1437
+ throw new Error('Migration 068 recovery failed: missing table tool_traces');
1438
+ }
1439
+ const expectedColumns = [
1440
+ 'diagnostic_json',
1441
+ 'evidence_json',
1442
+ 'catalog_revision',
1443
+ 'owner_scope',
1444
+ 'project_id',
1445
+ 'channel_id',
1446
+ ];
1447
+ const columns = this.tableColumns('tool_traces');
1448
+ for (const column of expectedColumns) {
1449
+ if (!columns.has(column)) {
1450
+ this.exec(`ALTER TABLE tool_traces ADD COLUMN ${column} TEXT`);
1451
+ }
1452
+ }
1453
+ this.exec(`
1454
+ CREATE INDEX IF NOT EXISTS idx_tool_traces_scope_recency
1455
+ ON tool_traces(owner_scope, project_id, created_at DESC, trace_id DESC);
1456
+ CREATE INDEX IF NOT EXISTS idx_tool_traces_channel_recency
1457
+ ON tool_traces(owner_scope, project_id, channel_id, created_at DESC, trace_id DESC);
1458
+ `);
1459
+ const actualColumns = this.tableColumns('tool_traces');
1460
+ for (const column of expectedColumns) {
1461
+ if (!actualColumns.has(column)) {
1462
+ throw new Error(`Migration 068 recovery failed: missing tool_traces.${column}`);
1463
+ }
1464
+ }
1465
+ for (const index of ['idx_tool_traces_scope_recency', 'idx_tool_traces_channel_recency']) {
1466
+ if (!this.indexExists(index)) {
1467
+ throw new Error(`Migration 068 recovery failed: missing index ${index}`);
1468
+ }
1469
+ }
1470
+ this.prepare('INSERT OR IGNORE INTO schema_version (version) VALUES (?)').run(68);
1471
+ });
1472
+ }
1473
+ toolTracesTableSql() {
1474
+ const row = this.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='tool_traces'").get();
1475
+ return row?.sql ?? '';
1476
+ }
1477
+ tableSql(name) {
1478
+ const row = this.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name = ?").get(name);
1479
+ if (!row?.sql) {
1480
+ throw new Error(`Missing CREATE TABLE definition for ${name}`);
1481
+ }
1482
+ return row.sql;
1483
+ }
1484
+ storedObjectsForTable(table, excludedIndexes = new Set()) {
1485
+ const indexes = this.prepare("SELECT name, sql FROM sqlite_master WHERE type='index' AND tbl_name = ? AND sql IS NOT NULL").all(table)
1486
+ .filter((row) => !excludedIndexes.has(row.name))
1487
+ .map((row) => row.sql);
1488
+ const triggers = this.prepare("SELECT sql FROM sqlite_master WHERE type='trigger' AND tbl_name = ? AND sql IS NOT NULL").all(table).map((row) => row.sql);
1489
+ return { indexes, triggers };
1490
+ }
1491
+ tablesInForeignKeyGraph(parent) {
1492
+ const graph = new Set([parent]);
1493
+ const tables = this.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").all();
1494
+ for (const { name } of tables) {
1495
+ const refs = this.prepare('SELECT "table" AS ref FROM pragma_foreign_key_list(?)').all(name);
1496
+ if (refs.some((ref) => ref.ref === parent)) {
1497
+ graph.add(name);
1498
+ }
1499
+ }
1500
+ return [...graph];
1501
+ }
1502
+ observationVersionsShape072() {
1503
+ if (!this.tableExists('observation_versions')) {
1504
+ return false;
1505
+ }
1506
+ const required = [
1507
+ 'observation_id',
1508
+ 'source',
1509
+ 'source_id',
1510
+ 'producer_version_id',
1511
+ 'body',
1512
+ 'body_location_json',
1513
+ 'author',
1514
+ 'source_at',
1515
+ 'observed_at',
1516
+ 'content_hash',
1517
+ 'metadata_json',
1518
+ 'scope_json',
1519
+ ];
1520
+ const columns = this.tableColumns('observation_versions');
1521
+ const clauses = splitCreateTableClauses(this.tableSql('observation_versions'));
1522
+ const columnClauses = new Map(clauses
1523
+ .filter((clause) => !clauseIsTableConstraint(clause))
1524
+ .map((clause) => [clauseColumnName(clause), normalizeSqlText(clause)]));
1525
+ const bodyXor = normalizeSqlText('CHECK ((body IS NOT NULL AND body_location_json IS NULL) OR ' +
1526
+ '(body IS NULL AND body_location_json IS NOT NULL))');
1527
+ const columnInfo = new Map(this.prepare('PRAGMA table_info(observation_versions)').all().map((column) => [column.name, column]));
1528
+ // Whether any index points AT an observation is that index's own business.
1529
+ // This migration declares the evidence; it does not declare its readers.
1530
+ return (required.every((column) => columns.has(column)) &&
1531
+ required.every((column) => {
1532
+ const expected = ['source_at', 'observed_at'].includes(column) ? 'INTEGER' : 'TEXT';
1533
+ return columnInfo.get(column)?.type.toUpperCase() === expected;
1534
+ }) &&
1535
+ columnInfo.get('observation_id')?.pk === 1 &&
1536
+ ['source', 'source_id', 'observed_at', 'content_hash', 'metadata_json', 'scope_json'].every((column) => columnInfo.get(column)?.notnull === 1) &&
1537
+ columnClauses.get('source') ===
1538
+ normalizeSqlText('source TEXT NOT NULL CHECK (length(trim(source)) > 0)') &&
1539
+ columnClauses.get('source_id') ===
1540
+ normalizeSqlText('source_id TEXT NOT NULL CHECK (length(trim(source_id)) > 0)') &&
1541
+ columnClauses.get('content_hash') ===
1542
+ normalizeSqlText('content_hash TEXT NOT NULL CHECK (length(trim(content_hash)) > 0)') &&
1543
+ clauses.some((clause) => normalizeSqlText(clause) === bodyXor) &&
1544
+ this.indexExists('observation_source_versions') &&
1545
+ normalizeSqlText(this.indexSql('observation_source_versions')).includes('onobservation_versions(source,source_id,observed_at,observation_id)'));
1546
+ }
1547
+ needsObservationVersionsRepair072() {
1548
+ return !this.observationVersionsShape072() || !this.schemaVersionExists(72);
1549
+ }
1550
+ recoverObservationVersionsMigration072() {
1551
+ if (this.observationVersionsShape072()) {
1552
+ this.transaction(() => {
1553
+ this.prepare('INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)').run(72, 'Immutable connector and owner observation versions');
1554
+ });
1555
+ return;
1556
+ }
1557
+ const canonical = new Map([
1558
+ ['observation_id', 'observation_id TEXT PRIMARY KEY'],
1559
+ ['source', 'source TEXT NOT NULL CHECK (length(trim(source)) > 0)'],
1560
+ ['source_id', 'source_id TEXT NOT NULL CHECK (length(trim(source_id)) > 0)'],
1561
+ ['producer_version_id', 'producer_version_id TEXT'],
1562
+ ['body', 'body TEXT'],
1563
+ ['body_location_json', 'body_location_json TEXT'],
1564
+ ['author', 'author TEXT'],
1565
+ ['source_at', 'source_at INTEGER'],
1566
+ ['observed_at', 'observed_at INTEGER NOT NULL'],
1567
+ ['content_hash', 'content_hash TEXT NOT NULL CHECK (length(trim(content_hash)) > 0)'],
1568
+ ['metadata_json', 'metadata_json TEXT NOT NULL'],
1569
+ ['scope_json', 'scope_json TEXT NOT NULL'],
1570
+ ]);
1571
+ let existingNames = [];
1572
+ let columnDefs = [...canonical.values()];
1573
+ let constraints = [
1574
+ 'CHECK ((body IS NOT NULL AND body_location_json IS NULL) OR ' +
1575
+ '(body IS NULL AND body_location_json IS NOT NULL))',
1576
+ ];
1577
+ const bodyXor = normalizeSqlText(constraints[0]);
1578
+ let objects = { indexes: [], triggers: [] };
1579
+ if (this.tableExists('observation_versions')) {
1580
+ const clauses = splitCreateTableClauses(this.tableSql('observation_versions'));
1581
+ if (clauses.length === 0) {
1582
+ throw new Error('Migration 072 recovery failed: unreadable observation_versions');
1583
+ }
1584
+ const columns = clauses.filter((clause) => !clauseIsTableConstraint(clause));
1585
+ existingNames = columns.map(clauseColumnName);
1586
+ const missing = [...canonical.keys()].filter((name) => !existingNames.includes(name));
1587
+ const count = this.prepare('SELECT COUNT(*) AS count FROM observation_versions').get().count;
1588
+ if (missing.length > 0 && count > 0) {
1589
+ throw new Error(`Migration 072 cannot preserve populated rows missing columns: ${missing.join(', ')}`);
1590
+ }
1591
+ for (const clause of columns) {
1592
+ const name = clauseColumnName(clause);
1593
+ if (!canonical.has(name)) {
1594
+ continue;
1595
+ }
1596
+ const normalized = normalizeSqlText(clause);
1597
+ const expected = normalizeSqlText(canonical.get(name));
1598
+ const legacyExpected = normalizeSqlText(name === 'observation_id'
1599
+ ? 'observation_id TEXT PRIMARY KEY'
1600
+ : [
1601
+ 'source',
1602
+ 'source_id',
1603
+ 'observed_at',
1604
+ 'content_hash',
1605
+ 'metadata_json',
1606
+ 'scope_json',
1607
+ ].includes(name)
1608
+ ? `${name} ${['observed_at', 'source_at'].includes(name) ? 'INTEGER' : 'TEXT'} NOT NULL`
1609
+ : `${name} ${name === 'source_at' ? 'INTEGER' : 'TEXT'}`);
1610
+ if (normalized !== expected &&
1611
+ normalized !== legacyExpected &&
1612
+ /collate|generated|references|\bunique\b|\bcheck\b/i.test(clause)) {
1613
+ throw new Error(`Migration 072 cannot safely preserve inline constraint on ${name}`);
1614
+ }
1615
+ }
1616
+ const extras = columns.filter((clause) => !canonical.has(clauseColumnName(clause)));
1617
+ columnDefs = [...canonical.values(), ...extras];
1618
+ const customConstraints = clauses.filter((clause) => clauseIsTableConstraint(clause) &&
1619
+ !(normalizeSqlText(clause).includes('bodyisnotnullandbody_location_jsonisnull') &&
1620
+ normalizeSqlText(clause).includes('bodyisnullandbody_location_jsonisnotnull')));
1621
+ for (const constraint of clauses.filter(clauseIsTableConstraint)) {
1622
+ const normalizedConstraint = normalizeSqlText(constraint);
1623
+ if ((normalizedConstraint.includes('body') ||
1624
+ normalizedConstraint.includes('body_location_json')) &&
1625
+ normalizedConstraint !== bodyXor) {
1626
+ throw new Error('Migration 072 cannot safely preserve conflicting body CHECK');
1627
+ }
1628
+ }
1629
+ constraints = [...constraints, ...customConstraints];
1630
+ objects = this.storedObjectsForTable('observation_versions', new Set(['observation_source_versions']));
1631
+ }
1632
+ const previousForeignKeys = this.readForeignKeysEnabled();
1633
+ this.exec('PRAGMA foreign_keys = OFF');
1634
+ if (this.readForeignKeysEnabled()) {
1635
+ throw new Error('Migration 072 recovery failed: could not disable foreign_keys');
1636
+ }
1637
+ try {
1638
+ this.transaction(() => {
1639
+ if (this.tableExists('observation_versions')) {
1640
+ this.exec(`CREATE TABLE observation_versions_072_new (\n ${[...columnDefs, ...constraints].join(',\n ')}\n)`);
1641
+ if (existingNames.length > 0) {
1642
+ const columns = existingNames.map(quoteSqlIdentifier).join(', ');
1643
+ this.exec(`INSERT INTO observation_versions_072_new (${columns}) SELECT ${columns} FROM observation_versions`);
1644
+ }
1645
+ this.exec('DROP TABLE observation_versions');
1646
+ this.exec('ALTER TABLE observation_versions_072_new RENAME TO observation_versions');
1647
+ }
1648
+ else {
1649
+ this.exec(`CREATE TABLE observation_versions (\n ${[...columnDefs, ...constraints].join(',\n ')}\n)`);
1650
+ }
1651
+ for (const sql of objects.indexes) {
1652
+ this.exec(sql);
1653
+ }
1654
+ this.exec('CREATE INDEX observation_source_versions ON observation_versions(source, source_id, observed_at, observation_id)');
1655
+ for (const sql of objects.triggers) {
1656
+ this.exec(sql);
1657
+ }
1658
+ if (!this.observationVersionsShape072()) {
1659
+ throw new Error('Migration 072 recovery failed: incomplete observation shape');
1660
+ }
1661
+ for (const table of this.tablesInForeignKeyGraph('observation_versions')) {
1662
+ if (this.prepare('SELECT 1 FROM pragma_foreign_key_check(?)').all(table).length > 0) {
1663
+ throw new Error(`Migration 072 recovery failed: foreign key violations after rebuild (${table})`);
1664
+ }
1665
+ }
1666
+ this.prepare('INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)').run(72, 'Immutable connector and owner observation versions');
1667
+ });
1668
+ }
1669
+ finally {
1670
+ this.exec(`PRAGMA foreign_keys = ${previousForeignKeys ? 'ON' : 'OFF'}`);
1671
+ }
1672
+ }
1673
+ registryCorrectionsShape073() {
1674
+ const expected = new Map([
1675
+ ['registry_identity_state', ['singleton', 'revision']],
1676
+ [
1677
+ 'registry_corrections',
1678
+ [
1679
+ 'command_id',
1680
+ 'operation',
1681
+ 'expected_revision',
1682
+ 'committed_revision',
1683
+ 'principal_id',
1684
+ 'agent_id',
1685
+ 'origin',
1686
+ 'payload_json',
1687
+ 'reason',
1688
+ 'evidence_json',
1689
+ 'scope_json',
1690
+ 'receipt_json',
1691
+ 'created_at',
1692
+ ],
1693
+ ],
1694
+ [
1695
+ 'registry_ref_assignments',
1696
+ [
1697
+ 'command_id',
1698
+ 'edge_id',
1699
+ 'endpoint',
1700
+ 'original_kind',
1701
+ 'original_id',
1702
+ 'resolved_node_id',
1703
+ 'committed_revision',
1704
+ 'created_at',
1705
+ ],
1706
+ ],
1707
+ ]);
1708
+ if (!([...expected].every(([table, columns]) => this.tableExists(table) &&
1709
+ columns.every((column) => this.tableColumns(table).has(column))) &&
1710
+ this.indexExists('idx_registry_ref_assignments_current') &&
1711
+ normalizeSqlText(this.indexSql('idx_registry_ref_assignments_current')).includes('onregistry_ref_assignments(edge_id,endpoint,committed_revisiondesc)'))) {
1712
+ return false;
1713
+ }
1714
+ const stateSql = normalizeSqlText(this.tableSql('registry_identity_state'));
1715
+ const correctionsSql = normalizeSqlText(this.tableSql('registry_corrections'));
1716
+ const assignmentsSql = normalizeSqlText(this.tableSql('registry_ref_assignments'));
1717
+ const correctionClauses = splitCreateTableClauses(this.tableSql('registry_corrections'));
1718
+ const aggregateAuthorityCheck = normalizeSqlText(`CHECK (
1719
+ (origin = 'trusted' AND principal_id IS NOT NULL AND length(trim(principal_id)) > 0
1720
+ AND agent_id IS NOT NULL AND length(trim(agent_id)) > 0)
1721
+ OR (origin = 'legacy_unattributed' AND principal_id IS NULL AND agent_id IS NULL)
1722
+ )`);
1723
+ const stateRows = this.prepare('SELECT singleton, revision FROM registry_identity_state ORDER BY singleton').all();
1724
+ const correctionInfo = new Map(this.prepare('PRAGMA table_info(registry_corrections)').all().map((column) => [column.name, column]));
1725
+ const assignmentInfo = new Map(this.prepare('PRAGMA table_info(registry_ref_assignments)').all().map((column) => [column.name, column]));
1726
+ const assignmentFks = this.prepare('SELECT "from" AS from_col, "table" AS target, "to" AS to_col, on_delete FROM pragma_foreign_key_list(?)').all('registry_ref_assignments');
1727
+ return (stateSql.includes('singletonintegerprimarykeycheck(singleton=1)') &&
1728
+ stateSql.includes('revisionintegernotnullcheck(revision>=0)') &&
1729
+ stateRows.length === 1 &&
1730
+ stateRows[0]?.singleton === 1 &&
1731
+ Number.isSafeInteger(stateRows[0]?.revision) &&
1732
+ (stateRows[0]?.revision ?? -1) >= 0 &&
1733
+ correctionInfo.get('command_id')?.pk === 1 &&
1734
+ [
1735
+ 'command_id',
1736
+ 'operation',
1737
+ 'expected_revision',
1738
+ 'committed_revision',
1739
+ 'principal_id',
1740
+ 'agent_id',
1741
+ 'origin',
1742
+ 'payload_json',
1743
+ 'reason',
1744
+ 'evidence_json',
1745
+ 'scope_json',
1746
+ 'receipt_json',
1747
+ 'created_at',
1748
+ ].every((name) => {
1749
+ const column = correctionInfo.get(name);
1750
+ return ['expected_revision', 'committed_revision', 'created_at'].includes(name)
1751
+ ? column?.type.toUpperCase() === 'INTEGER'
1752
+ : column?.type.toUpperCase() === 'TEXT';
1753
+ }) &&
1754
+ [
1755
+ 'operation',
1756
+ 'expected_revision',
1757
+ 'committed_revision',
1758
+ 'origin',
1759
+ 'payload_json',
1760
+ 'reason',
1761
+ 'evidence_json',
1762
+ 'scope_json',
1763
+ 'receipt_json',
1764
+ 'created_at',
1765
+ ].every((column) => correctionInfo.get(column)?.notnull === 1) &&
1766
+ correctionsSql.includes("operationin('add_alias','merge','split','assign_refs')") &&
1767
+ correctionsSql.includes('expected_revision>=0') &&
1768
+ correctionsSql.includes('committed_revisionintegernotnullunique') &&
1769
+ correctionsSql.includes('committed_revision>expected_revision') &&
1770
+ correctionsSql.includes("originin('trusted','legacy_unattributed')") &&
1771
+ correctionsSql.includes('length(trim(reason))>0') &&
1772
+ correctionClauses.some((clause) => normalizeSqlText(clause) === aggregateAuthorityCheck) &&
1773
+ assignmentInfo.get('command_id')?.pk === 1 &&
1774
+ [
1775
+ 'command_id',
1776
+ 'edge_id',
1777
+ 'endpoint',
1778
+ 'original_kind',
1779
+ 'original_id',
1780
+ 'resolved_node_id',
1781
+ 'committed_revision',
1782
+ 'created_at',
1783
+ ].every((name) => {
1784
+ const column = assignmentInfo.get(name);
1785
+ return ['committed_revision', 'created_at'].includes(name)
1786
+ ? column?.type.toUpperCase() === 'INTEGER'
1787
+ : column?.type.toUpperCase() === 'TEXT';
1788
+ }) &&
1789
+ assignmentInfo.get('edge_id')?.pk === 2 &&
1790
+ assignmentInfo.get('endpoint')?.pk === 3 &&
1791
+ [
1792
+ 'command_id',
1793
+ 'edge_id',
1794
+ 'endpoint',
1795
+ 'original_kind',
1796
+ 'original_id',
1797
+ 'committed_revision',
1798
+ 'created_at',
1799
+ ].every((column) => assignmentInfo.get(column)?.notnull === 1) &&
1800
+ assignmentInfo.get('resolved_node_id')?.notnull === 0 &&
1801
+ assignmentsSql.includes("endpointin('from','to')") &&
1802
+ assignmentFks.some((fk) => fk.from_col === 'command_id' &&
1803
+ fk.target === 'registry_corrections' &&
1804
+ fk.to_col === 'command_id' &&
1805
+ fk.on_delete.toUpperCase() === 'CASCADE') &&
1806
+ assignmentFks.some((fk) => fk.from_col === 'resolved_node_id' && fk.target === 'registry_nodes' && fk.to_col === 'id'));
1807
+ }
1808
+ needsRegistryCorrectionsRepair073() {
1809
+ return !this.registryCorrectionsShape073() || !this.schemaVersionExists(73);
1810
+ }
1811
+ recoverRegistryCorrectionsMigration073(migrationsDir) {
1812
+ const tables = ['registry_identity_state', 'registry_corrections', 'registry_ref_assignments'];
1813
+ const malformed = tables.filter((table) => this.tableExists(table) &&
1814
+ !(table === 'registry_identity_state'
1815
+ ? ['singleton', 'revision']
1816
+ : table === 'registry_corrections'
1817
+ ? [
1818
+ 'command_id',
1819
+ 'operation',
1820
+ 'expected_revision',
1821
+ 'committed_revision',
1822
+ 'principal_id',
1823
+ 'agent_id',
1824
+ 'origin',
1825
+ 'payload_json',
1826
+ 'reason',
1827
+ 'evidence_json',
1828
+ 'scope_json',
1829
+ 'receipt_json',
1830
+ 'created_at',
1831
+ ]
1832
+ : [
1833
+ 'command_id',
1834
+ 'edge_id',
1835
+ 'endpoint',
1836
+ 'original_kind',
1837
+ 'original_id',
1838
+ 'resolved_node_id',
1839
+ 'committed_revision',
1840
+ 'created_at',
1841
+ ]).every((column) => this.tableColumns(table).has(column)));
1842
+ const allTablesPresent = tables.every((table) => this.tableExists(table));
1843
+ for (const table of tables) {
1844
+ if (!this.tableExists(table)) {
1845
+ continue;
1846
+ }
1847
+ const requiredColumns = table === 'registry_identity_state'
1848
+ ? ['singleton', 'revision']
1849
+ : table === 'registry_corrections'
1850
+ ? [
1851
+ 'command_id',
1852
+ 'operation',
1853
+ 'expected_revision',
1854
+ 'committed_revision',
1855
+ 'principal_id',
1856
+ 'agent_id',
1857
+ 'origin',
1858
+ 'payload_json',
1859
+ 'reason',
1860
+ 'evidence_json',
1861
+ 'scope_json',
1862
+ 'receipt_json',
1863
+ 'created_at',
1864
+ ]
1865
+ : [
1866
+ 'command_id',
1867
+ 'edge_id',
1868
+ 'endpoint',
1869
+ 'original_kind',
1870
+ 'original_id',
1871
+ 'resolved_node_id',
1872
+ 'committed_revision',
1873
+ 'created_at',
1874
+ ];
1875
+ if (allTablesPresent &&
1876
+ requiredColumns.every((column) => this.tableColumns(table).has(column)) &&
1877
+ !this.registryCorrectionsShape073()) {
1878
+ throw new Error(`Migration 073 cannot safely repair structurally incompatible table ${table}`);
1879
+ }
1880
+ }
1881
+ for (const table of malformed) {
1882
+ const count = this.prepare(`SELECT COUNT(*) AS count FROM ${quoteSqlIdentifier(table)}`).get().count;
1883
+ if (count > 0) {
1884
+ throw new Error(`Migration 073 cannot safely repair populated incompatible table ${table}`);
1885
+ }
1886
+ const excluded = table === 'registry_ref_assignments'
1887
+ ? new Set(['idx_registry_ref_assignments_current'])
1888
+ : new Set();
1889
+ const objects = this.storedObjectsForTable(table, excluded);
1890
+ if (objects.indexes.length > 0 ||
1891
+ objects.triggers.length > 0 ||
1892
+ this.tablesInForeignKeyGraph(table).length > 1 ||
1893
+ /\b(?:check|unique|references)\b/i.test(this.tableSql(table))) {
1894
+ throw new Error(`Migration 073 cannot safely replace incompatible table ${table}`);
1895
+ }
1896
+ }
1897
+ const sql = fs_1.default.readFileSync(path_1.default.join(migrationsDir, '073-registry-corrections.sql'), 'utf8');
1898
+ this.transaction(() => {
1899
+ if (malformed.includes('registry_ref_assignments')) {
1900
+ this.exec('DROP TABLE registry_ref_assignments');
1901
+ }
1902
+ if (malformed.includes('registry_corrections')) {
1903
+ this.exec('DROP TABLE registry_corrections');
1904
+ }
1905
+ if (malformed.includes('registry_identity_state')) {
1906
+ this.exec('DROP TABLE registry_identity_state');
1907
+ }
1908
+ this.exec(sql);
1909
+ if (!this.registryCorrectionsShape073()) {
1910
+ throw new Error('Migration 073 recovery failed: incomplete correction shape');
1911
+ }
1912
+ this.prepare('INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)').run(73, 'Atomic scoped registry correction history');
1913
+ });
1914
+ }
1915
+ workGraphRefsShape074() {
1916
+ if (!this.tableExists('twin_edges')) {
1917
+ return false;
1918
+ }
1919
+ const clauses = splitCreateTableClauses(this.tableSql('twin_edges'));
1920
+ const columnClauses = new Map(clauses
1921
+ .filter((clause) => !clauseIsTableConstraint(clause))
1922
+ .map((clause) => [clauseColumnName(clause), normalizeSqlText(clause)]));
1923
+ // 079 extends the relation list with 'refines','contradicts','amends'; a
1924
+ // table already at that shape satisfies the 074 endpoint contract too.
1925
+ const edgeTypeClause = clauses.find((clause) => !clauseIsTableConstraint(clause) && clauseColumnName(clause) === 'edge_type');
1926
+ const edgeTypeOk = edgeTypeClause !== undefined &&
1927
+ [
1928
+ "edge_type TEXT NOT NULL CHECK (edge_type IN ('supersedes','builds_on','debates','synthesizes','mentions','derived_from','case_member','alias_of','next_action_for','blocks'))",
1929
+ "edge_type TEXT NOT NULL CHECK (edge_type IN ('supersedes','refines','contradicts','builds_on','debates','synthesizes','mentions','derived_from','case_member','alias_of','next_action_for','blocks','amends'))",
1930
+ ].some((definition) => normalizeSqlText(edgeTypeClause) === normalizeSqlText(definition));
1931
+ const exactColumns = new Map([
1932
+ [
1933
+ 'confidence',
1934
+ 'confidence REAL NOT NULL DEFAULT 1.0 CHECK (confidence >= 0.0 AND confidence <= 1.0)',
1935
+ ],
1936
+ ['source', "source TEXT NOT NULL CHECK (source IN ('agent','human','code'))"],
1937
+ ['content_hash', 'content_hash BLOB NOT NULL CHECK(length(content_hash)=32)'],
1938
+ ]);
1939
+ const required = [
1940
+ 'edge_id',
1941
+ 'edge_type',
1942
+ 'subject_kind',
1943
+ 'subject_id',
1944
+ 'object_kind',
1945
+ 'object_id',
1946
+ 'relation_attrs_json',
1947
+ 'confidence',
1948
+ 'source',
1949
+ 'agent_id',
1950
+ 'model_run_id',
1951
+ 'envelope_hash',
1952
+ 'human_actor_id',
1953
+ 'human_actor_role',
1954
+ 'authority_scope_json',
1955
+ 'reason_classification',
1956
+ 'reason_text',
1957
+ 'evidence_refs_json',
1958
+ 'request_idempotency_key',
1959
+ 'edge_idempotency_key',
1960
+ 'content_hash',
1961
+ 'created_at',
1962
+ ];
1963
+ const info = new Map(this.prepare('PRAGMA table_info(twin_edges)').all().map((column) => [column.name, column]));
1964
+ return (required.every((column) => info.has(column)) &&
1965
+ required.every((column) => {
1966
+ const expected = column === 'confidence'
1967
+ ? 'REAL'
1968
+ : column === 'created_at'
1969
+ ? 'INTEGER'
1970
+ : column === 'content_hash'
1971
+ ? 'BLOB'
1972
+ : 'TEXT';
1973
+ return info.get(column)?.type.toUpperCase() === expected;
1974
+ }) &&
1975
+ info.get('edge_id')?.pk === 1 &&
1976
+ [
1977
+ 'edge_type',
1978
+ 'subject_kind',
1979
+ 'subject_id',
1980
+ 'object_kind',
1981
+ 'object_id',
1982
+ 'confidence',
1983
+ 'source',
1984
+ 'content_hash',
1985
+ 'created_at',
1986
+ ].every((column) => info.get(column)?.notnull === 1) &&
1987
+ edgeTypeOk &&
1988
+ [...exactColumns].every(([name, definition]) => columnClauses.get(name) === normalizeSqlText(definition)) &&
1989
+ clauses.some((clause) => !clauseIsTableConstraint(clause) &&
1990
+ clauseColumnName(clause) === 'subject_kind' &&
1991
+ normalizeSqlText(clause) ===
1992
+ normalizeSqlText("subject_kind TEXT NOT NULL CHECK (subject_kind IN ('memory','case','entity','report','edge','registry','observation'))")) &&
1993
+ clauses.some((clause) => !clauseIsTableConstraint(clause) &&
1994
+ clauseColumnName(clause) === 'object_kind' &&
1995
+ normalizeSqlText(clause) ===
1996
+ normalizeSqlText("object_kind TEXT NOT NULL CHECK (object_kind IN ('memory','case','entity','report','edge','raw','registry','observation'))")) &&
1997
+ !clauses.some((clause) => clauseIsTableConstraint(clause) && /\b(?:subject_kind|object_kind)\b/i.test(clause)) &&
1998
+ this.indexExists('idx_twin_edges_subject') &&
1999
+ this.indexExists('idx_twin_edges_object') &&
2000
+ this.indexExists('idx_twin_edges_model_run_id') &&
2001
+ this.indexExists('idx_twin_edges_request_idempotency') &&
2002
+ this.indexExists('ux_twin_edges_model_run_edge_idempotency') &&
2003
+ normalizeSqlText(this.indexSql('idx_twin_edges_subject')).includes('ontwin_edges(subject_kind,subject_id,created_atdesc)') &&
2004
+ normalizeSqlText(this.indexSql('idx_twin_edges_object')).includes('ontwin_edges(object_kind,object_id,created_atdesc)') &&
2005
+ normalizeSqlText(this.indexSql('idx_twin_edges_model_run_id')).includes('ontwin_edges(model_run_id,created_atdesc)') &&
2006
+ normalizeSqlText(this.indexSql('idx_twin_edges_request_idempotency')).includes('ontwin_edges(model_run_id,request_idempotency_key,created_atdesc)wheremodel_run_idisnotnullandrequest_idempotency_keyisnotnull') &&
2007
+ normalizeSqlText(this.indexSql('ux_twin_edges_model_run_edge_idempotency')).includes('uniqueindexux_twin_edges_model_run_edge_idempotencyontwin_edges(model_run_id,edge_idempotency_key)wheremodel_run_idisnotnullandedge_idempotency_keyisnotnull'));
2008
+ }
2009
+ needsWorkGraphRefsRepair074() {
2010
+ return !this.workGraphRefsShape074() || !this.schemaVersionExists(74);
2011
+ }
2012
+ recoverWorkGraphRefsMigration074() {
2013
+ if (!this.tableExists('twin_edges')) {
2014
+ throw new Error('Migration 074 recovery failed: missing twin_edges');
2015
+ }
2016
+ if (this.workGraphRefsShape074()) {
2017
+ this.transaction(() => {
2018
+ this.prepare('INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)').run(74, 'Registry and observation work graph references');
2019
+ });
2020
+ return;
2021
+ }
2022
+ const clauses = splitCreateTableClauses(this.tableSql('twin_edges'));
2023
+ if (clauses.length === 0) {
2024
+ throw new Error('Migration 074 recovery failed: unreadable twin_edges');
2025
+ }
2026
+ const existingColumns = [];
2027
+ const columns = [];
2028
+ const constraints = [];
2029
+ const subjectDef = "subject_kind TEXT NOT NULL CHECK (subject_kind IN ('memory','case','entity','report','edge','registry','observation'))";
2030
+ const objectDef = "object_kind TEXT NOT NULL CHECK (object_kind IN ('memory','case','entity','report','edge','raw','registry','observation'))";
2031
+ const legacySubjectDef = normalizeSqlText("subject_kind TEXT NOT NULL CHECK (subject_kind IN ('memory','case','entity','report','edge'))");
2032
+ const legacyObjectDef = normalizeSqlText("object_kind TEXT NOT NULL CHECK (object_kind IN ('memory','case','entity','report','edge','raw'))");
2033
+ const exactProtectedColumns = new Map([
2034
+ [
2035
+ 'confidence',
2036
+ 'confidence REAL NOT NULL DEFAULT 1.0 CHECK (confidence >= 0.0 AND confidence <= 1.0)',
2037
+ ],
2038
+ ['source', "source TEXT NOT NULL CHECK (source IN ('agent','human','code'))"],
2039
+ ['content_hash', 'content_hash BLOB NOT NULL CHECK(length(content_hash)=32)'],
2040
+ ]);
2041
+ // The edge_type CHECK was extended by migration 079; a table carrying the
2042
+ // newer relation list is preserved as-is instead of being rebuilt down.
2043
+ const allowedEdgeTypeDefs = new Set([
2044
+ "edge_type TEXT NOT NULL CHECK (edge_type IN ('supersedes','builds_on','debates','synthesizes','mentions','derived_from','case_member','alias_of','next_action_for','blocks'))",
2045
+ "edge_type TEXT NOT NULL CHECK (edge_type IN ('supersedes','refines','contradicts','builds_on','debates','synthesizes','mentions','derived_from','case_member','alias_of','next_action_for','blocks','amends'))",
2046
+ ].map(normalizeSqlText));
2047
+ for (const clause of clauses) {
2048
+ if (clauseIsTableConstraint(clause)) {
2049
+ if (/\b(?:subject_kind|object_kind)\b/i.test(clause)) {
2050
+ throw new Error('Migration 074 cannot safely preserve conflicting endpoint CHECK');
2051
+ }
2052
+ constraints.push(clause);
2053
+ continue;
2054
+ }
2055
+ const name = clauseColumnName(clause);
2056
+ existingColumns.push(name);
2057
+ if (name === 'subject_kind') {
2058
+ const normalizedClause = normalizeSqlText(clause);
2059
+ if (normalizedClause !== legacySubjectDef &&
2060
+ normalizedClause !== normalizeSqlText(subjectDef)) {
2061
+ throw new Error('Migration 074 cannot safely preserve inline constraint on subject_kind');
2062
+ }
2063
+ columns.push(subjectDef);
2064
+ }
2065
+ else if (name === 'object_kind') {
2066
+ const normalizedClause = normalizeSqlText(clause);
2067
+ if (normalizedClause !== legacyObjectDef &&
2068
+ normalizedClause !== normalizeSqlText(objectDef)) {
2069
+ throw new Error('Migration 074 cannot safely preserve inline constraint on object_kind');
2070
+ }
2071
+ columns.push(objectDef);
2072
+ }
2073
+ else if (name === 'edge_type') {
2074
+ if (!allowedEdgeTypeDefs.has(normalizeSqlText(clause))) {
2075
+ throw new Error('Migration 074 cannot safely preserve inline constraint on edge_type');
2076
+ }
2077
+ columns.push(clause);
2078
+ }
2079
+ else if (exactProtectedColumns.has(name)) {
2080
+ if (normalizeSqlText(clause) !== normalizeSqlText(exactProtectedColumns.get(name))) {
2081
+ throw new Error(`Migration 074 cannot safely preserve inline constraint on ${name}`);
2082
+ }
2083
+ columns.push(clause);
2084
+ }
2085
+ else {
2086
+ columns.push(clause);
2087
+ }
2088
+ }
2089
+ const objects = this.storedObjectsForTable('twin_edges', new Set([
2090
+ 'idx_twin_edges_subject',
2091
+ 'idx_twin_edges_object',
2092
+ 'idx_twin_edges_model_run_id',
2093
+ 'idx_twin_edges_request_idempotency',
2094
+ 'ux_twin_edges_model_run_edge_idempotency',
2095
+ ]));
2096
+ const previousForeignKeys = this.readForeignKeysEnabled();
2097
+ this.exec('PRAGMA foreign_keys = OFF');
2098
+ if (this.readForeignKeysEnabled()) {
2099
+ throw new Error('Migration 074 recovery failed: could not disable foreign_keys');
2100
+ }
2101
+ try {
2102
+ this.transaction(() => {
2103
+ this.exec(`CREATE TABLE twin_edges_074_new (\n ${[...columns, ...constraints].join(',\n ')}\n)`);
2104
+ const names = existingColumns.map(quoteSqlIdentifier).join(', ');
2105
+ this.exec(`INSERT INTO twin_edges_074_new (${names}) SELECT ${names} FROM twin_edges`);
2106
+ this.exec('DROP TABLE twin_edges');
2107
+ this.exec('ALTER TABLE twin_edges_074_new RENAME TO twin_edges');
2108
+ for (const sql of objects.indexes) {
2109
+ this.exec(sql);
2110
+ }
2111
+ this.exec(`
2112
+ CREATE INDEX idx_twin_edges_subject
2113
+ ON twin_edges(subject_kind, subject_id, created_at DESC);
2114
+ CREATE INDEX idx_twin_edges_object
2115
+ ON twin_edges(object_kind, object_id, created_at DESC);
2116
+ CREATE INDEX idx_twin_edges_model_run_id
2117
+ ON twin_edges(model_run_id, created_at DESC);
2118
+ CREATE INDEX idx_twin_edges_request_idempotency
2119
+ ON twin_edges(model_run_id, request_idempotency_key, created_at DESC)
2120
+ WHERE model_run_id IS NOT NULL AND request_idempotency_key IS NOT NULL;
2121
+ CREATE UNIQUE INDEX ux_twin_edges_model_run_edge_idempotency
2122
+ ON twin_edges(model_run_id, edge_idempotency_key)
2123
+ WHERE model_run_id IS NOT NULL AND edge_idempotency_key IS NOT NULL;
2124
+ `);
2125
+ for (const sql of objects.triggers) {
2126
+ this.exec(sql);
2127
+ }
2128
+ if (!this.workGraphRefsShape074()) {
2129
+ throw new Error('Migration 074 recovery failed: incomplete work graph shape');
2130
+ }
2131
+ for (const table of this.tablesInForeignKeyGraph('twin_edges')) {
2132
+ if (this.prepare('SELECT 1 FROM pragma_foreign_key_check(?)').all(table).length > 0) {
2133
+ throw new Error(`Migration 074 recovery failed: foreign key violations after rebuild (${table})`);
2134
+ }
2135
+ }
2136
+ this.prepare('INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)').run(74, 'Registry and observation work graph references');
2137
+ });
2138
+ }
2139
+ finally {
2140
+ this.exec(`PRAGMA foreign_keys = ${previousForeignKeys ? 'ON' : 'OFF'}`);
2141
+ }
2142
+ }
2143
+ /**
2144
+ * 079 endpoint shape: the 074 work-graph shape plus the extended edge_type
2145
+ * relation list ('refines','contradicts','amends') used by authored record
2146
+ * links. `workGraphRefsShape074` already accepts this edge_type variant, so
2147
+ * the extra check here is the only difference.
2148
+ */
2149
+ twinEdgeRelationsShape079() {
2150
+ if (!this.tableExists('twin_edges')) {
2151
+ return false;
2152
+ }
2153
+ const clauses = splitCreateTableClauses(this.tableSql('twin_edges'));
2154
+ const edgeTypeClause = clauses.find((clause) => !clauseIsTableConstraint(clause) && clauseColumnName(clause) === 'edge_type');
2155
+ return (this.workGraphRefsShape074() &&
2156
+ edgeTypeClause !== undefined &&
2157
+ normalizeSqlText(edgeTypeClause) ===
2158
+ normalizeSqlText("edge_type TEXT NOT NULL CHECK (edge_type IN ('supersedes','refines','contradicts','builds_on','debates','synthesizes','mentions','derived_from','case_member','alias_of','next_action_for','blocks','amends'))") &&
2159
+ !clauses.some((clause) => clauseIsTableConstraint(clause) && /\bedge_type\b/i.test(clause)));
2160
+ }
2161
+ needsTwinEdgeRelationsRepair079() {
2162
+ return !this.twinEdgeRelationsShape079() || !this.schemaVersionExists(79);
2163
+ }
2164
+ /**
2165
+ * Rebuild twin_edges with the extended relation CHECK while preserving every
2166
+ * existing column (including runtime-added custom columns), index, trigger,
2167
+ * and foreign-key child - the same contract recoverWorkGraphRefsMigration074
2168
+ * honours. Only the edge_type clause is rewritten; a table already carrying
2169
+ * the extended list is just stamped.
2170
+ */
2171
+ recoverTwinEdgeRelationsMigration079() {
2172
+ if (!this.tableExists('twin_edges')) {
2173
+ throw new Error('Migration 079 recovery failed: missing twin_edges');
2174
+ }
2175
+ if (this.twinEdgeRelationsShape079()) {
2176
+ this.transaction(() => {
2177
+ this.prepare('INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)').run(79, 'Twin edge relations for judgment links (refines/contradicts/amends)');
2178
+ });
2179
+ return;
2180
+ }
2181
+ const clauses = splitCreateTableClauses(this.tableSql('twin_edges'));
2182
+ if (clauses.length === 0) {
2183
+ throw new Error('Migration 079 recovery failed: unreadable twin_edges');
2184
+ }
2185
+ const edgeTypeDef = "edge_type TEXT NOT NULL CHECK (edge_type IN ('supersedes','refines','contradicts','builds_on','debates','synthesizes','mentions','derived_from','case_member','alias_of','next_action_for','blocks','amends'))";
2186
+ const allowedEdgeTypeDefs = new Set([
2187
+ "edge_type TEXT NOT NULL CHECK (edge_type IN ('supersedes','builds_on','debates','synthesizes','mentions','derived_from','case_member','alias_of','next_action_for','blocks'))",
2188
+ edgeTypeDef,
2189
+ ].map(normalizeSqlText));
2190
+ const existingColumns = [];
2191
+ const columns = [];
2192
+ const constraints = [];
2193
+ for (const clause of clauses) {
2194
+ if (clauseIsTableConstraint(clause)) {
2195
+ if (/\bedge_type\b/i.test(clause)) {
2196
+ throw new Error('Migration 079 cannot safely preserve conflicting edge_type CHECK');
2197
+ }
2198
+ constraints.push(clause);
2199
+ continue;
2200
+ }
2201
+ const name = clauseColumnName(clause);
2202
+ existingColumns.push(name);
2203
+ if (name === 'edge_type') {
2204
+ if (!allowedEdgeTypeDefs.has(normalizeSqlText(clause))) {
2205
+ throw new Error('Migration 079 cannot safely preserve inline constraint on edge_type');
2206
+ }
2207
+ columns.push(edgeTypeDef);
2208
+ }
2209
+ else {
2210
+ columns.push(clause);
2211
+ }
2212
+ }
2213
+ const objects = this.storedObjectsForTable('twin_edges', new Set([
2214
+ 'idx_twin_edges_subject',
2215
+ 'idx_twin_edges_object',
2216
+ 'idx_twin_edges_model_run_id',
2217
+ 'idx_twin_edges_request_idempotency',
2218
+ 'ux_twin_edges_model_run_edge_idempotency',
2219
+ ]));
2220
+ const previousForeignKeys = this.readForeignKeysEnabled();
2221
+ this.exec('PRAGMA foreign_keys = OFF');
2222
+ if (this.readForeignKeysEnabled()) {
2223
+ throw new Error('Migration 079 recovery failed: could not disable foreign_keys');
2224
+ }
2225
+ try {
2226
+ this.transaction(() => {
2227
+ this.exec(`CREATE TABLE twin_edges_079_new (\n ${[...columns, ...constraints].join(',\n ')}\n)`);
2228
+ // Generated columns (table_xinfo hidden: 2 = VIRTUAL, 3 = STORED) refuse
2229
+ // direct writes; the rebuilt table recomputes them from its own clause.
2230
+ const generatedColumns = new Set(this.prepare('SELECT name FROM pragma_table_xinfo(?) WHERE hidden IN (2, 3)').all('twin_edges').map((row) => row.name));
2231
+ const names = existingColumns
2232
+ .filter((name) => !generatedColumns.has(name))
2233
+ .map(quoteSqlIdentifier)
2234
+ .join(', ');
2235
+ this.exec(`INSERT INTO twin_edges_079_new (${names}) SELECT ${names} FROM twin_edges`);
2236
+ this.exec('DROP TABLE twin_edges');
2237
+ this.exec('ALTER TABLE twin_edges_079_new RENAME TO twin_edges');
2238
+ for (const sql of objects.indexes) {
2239
+ this.exec(sql);
2240
+ }
2241
+ this.exec(`
2242
+ CREATE INDEX idx_twin_edges_subject
2243
+ ON twin_edges(subject_kind, subject_id, created_at DESC);
2244
+ CREATE INDEX idx_twin_edges_object
2245
+ ON twin_edges(object_kind, object_id, created_at DESC);
2246
+ CREATE INDEX idx_twin_edges_model_run_id
2247
+ ON twin_edges(model_run_id, created_at DESC);
2248
+ CREATE INDEX idx_twin_edges_request_idempotency
2249
+ ON twin_edges(model_run_id, request_idempotency_key, created_at DESC)
2250
+ WHERE model_run_id IS NOT NULL AND request_idempotency_key IS NOT NULL;
2251
+ CREATE UNIQUE INDEX ux_twin_edges_model_run_edge_idempotency
2252
+ ON twin_edges(model_run_id, edge_idempotency_key)
2253
+ WHERE model_run_id IS NOT NULL AND edge_idempotency_key IS NOT NULL;
2254
+ `);
2255
+ for (const sql of objects.triggers) {
2256
+ this.exec(sql);
2257
+ }
2258
+ if (!this.twinEdgeRelationsShape079()) {
2259
+ throw new Error('Migration 079 recovery failed: incomplete twin edge relations shape');
2260
+ }
2261
+ for (const table of this.tablesInForeignKeyGraph('twin_edges')) {
2262
+ if (this.prepare('SELECT 1 FROM pragma_foreign_key_check(?)').all(table).length > 0) {
2263
+ throw new Error(`Migration 079 recovery failed: foreign key violations after rebuild (${table})`);
2264
+ }
2265
+ }
2266
+ this.prepare('INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)').run(79, 'Twin edge relations for judgment links (refines/contradicts/amends)');
2267
+ });
2268
+ }
2269
+ finally {
2270
+ this.exec(`PRAGMA foreign_keys = ${previousForeignKeys ? 'ON' : 'OFF'}`);
2271
+ }
2272
+ }
2273
+ legacyRecordKindShape077() {
2274
+ if (!this.tableExists('decisions')) {
2275
+ return false;
2276
+ }
2277
+ const clauses = splitCreateTableClauses(this.tableSql('decisions'));
2278
+ const recordKindClause = clauses.find((clause) => !clauseIsTableConstraint(clause) && clauseColumnName(clause) === 'record_kind');
2279
+ return Boolean(recordKindClause &&
2280
+ normalizeSqlText(recordKindClause) ===
2281
+ normalizeSqlText("record_kind TEXT NOT NULL DEFAULT 'legacy' CHECK (record_kind IN ('legacy', 'judgment', 'commitment'))") &&
2282
+ !clauses.some((clause) => clauseIsTableConstraint(clause) && /\brecord_kind\b/i.test(clause)));
2283
+ }
2284
+ recoverLegacyRecordKindMigration077() {
2285
+ if (!this.tableExists('decisions')) {
2286
+ throw new Error('Migration 077 recovery failed: missing decisions');
2287
+ }
2288
+ if (this.legacyRecordKindShape077()) {
2289
+ this.transaction(() => {
2290
+ this.prepare('INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)').run(77, 'Distinguish legacy memory records from agent judgments');
2291
+ });
2292
+ return;
2293
+ }
2294
+ const clauses = splitCreateTableClauses(this.tableSql('decisions'));
2295
+ if (clauses.length === 0) {
2296
+ throw new Error('Migration 077 recovery failed: unreadable decisions');
2297
+ }
2298
+ const existingColumns = [];
2299
+ const columns = [];
2300
+ const constraints = [];
2301
+ let hasRecordKind = false;
2302
+ for (const clause of clauses) {
2303
+ if (clauseIsTableConstraint(clause)) {
2304
+ if (/\brecord_kind\b/i.test(clause)) {
2305
+ throw new Error('Migration 077 cannot safely preserve conflicting record_kind CHECK');
2306
+ }
2307
+ constraints.push(clause);
2308
+ continue;
2309
+ }
2310
+ const name = clauseColumnName(clause);
2311
+ existingColumns.push(name);
2312
+ if (name === 'record_kind') {
2313
+ hasRecordKind = true;
2314
+ columns.push("record_kind TEXT NOT NULL DEFAULT 'legacy' CHECK (record_kind IN ('legacy', 'judgment', 'commitment'))");
2315
+ }
2316
+ else {
2317
+ columns.push(clause);
2318
+ }
2319
+ }
2320
+ if (!hasRecordKind) {
2321
+ throw new Error('Migration 077 recovery failed: record_kind column is missing');
2322
+ }
2323
+ const objects = this.storedObjectsForTable('decisions');
2324
+ const previousForeignKeys = this.readForeignKeysEnabled();
2325
+ this.exec('PRAGMA foreign_keys = OFF');
2326
+ if (this.readForeignKeysEnabled()) {
2327
+ throw new Error('Migration 077 recovery failed: could not disable foreign_keys');
2328
+ }
2329
+ try {
2330
+ this.transaction(() => {
2331
+ this.exec(`CREATE TABLE decisions_077_new (\n ${[...columns, ...constraints].join(',\n ')}\n)`);
2332
+ const names = existingColumns.map(quoteSqlIdentifier).join(', ');
2333
+ const source = existingColumns
2334
+ .map((name) => name === 'record_kind'
2335
+ ? `CASE WHEN ${this.tableExists('judgment_commands') ? 'EXISTS (SELECT 1 FROM judgment_commands jc WHERE jc.record_id = decisions.id)' : '0'} THEN record_kind ELSE 'legacy' END`
2336
+ : quoteSqlIdentifier(name))
2337
+ .join(', ');
2338
+ this.exec(`INSERT INTO decisions_077_new (${names}) SELECT ${source} FROM decisions`);
2339
+ this.exec('DROP TABLE decisions');
2340
+ this.exec('ALTER TABLE decisions_077_new RENAME TO decisions');
2341
+ for (const sql of objects.indexes) {
2342
+ this.exec(sql);
2343
+ }
2344
+ for (const sql of objects.triggers) {
2345
+ this.exec(sql);
2346
+ }
2347
+ if (!this.legacyRecordKindShape077()) {
2348
+ throw new Error('Migration 077 recovery failed: incomplete record_kind shape');
2349
+ }
2350
+ for (const table of this.tablesInForeignKeyGraph('decisions')) {
2351
+ if (this.prepare('SELECT 1 FROM pragma_foreign_key_check(?)').all(table).length > 0) {
2352
+ throw new Error(`Migration 077 recovery failed: foreign key violations after rebuild (${table})`);
2353
+ }
2354
+ }
2355
+ this.prepare('INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)').run(77, 'Distinguish legacy memory records from agent judgments');
2356
+ });
2357
+ }
2358
+ finally {
2359
+ this.exec(`PRAGMA foreign_keys = ${previousForeignKeys ? 'ON' : 'OFF'}`);
2360
+ }
2361
+ }
2362
+ toolTracesHasOperationOriginShape() {
2363
+ const columns = this.tableColumns('tool_traces');
2364
+ if (!columns.has('operation_id') || !columns.has('actor_principal_id')) {
2365
+ return false;
2366
+ }
2367
+ const modelRunColumn = this.prepare('PRAGMA table_info(tool_traces)').all().find((column) => column.name === 'model_run_id');
2368
+ if (!modelRunColumn || modelRunColumn.notnull !== 0) {
2369
+ return false;
2370
+ }
2371
+ const normalized = normalizeSqlText(this.toolTracesTableSql());
2372
+ // The FULL origin CHECK must be present, not merely a prefix: a weakened
2373
+ // `CHECK (model_run_id IS NOT NULL OR 1)` would let a both-null-origin row
2374
+ // exist, so a table carrying it is NOT migrated and must be repaired.
2375
+ if (!normalized.includes(normalizeSqlText(TOOL_TRACES_ORIGIN_CHECK))) {
2376
+ return false;
2377
+ }
2378
+ // The model_runs FK must survive so a legacy/causal model reference is real.
2379
+ if (!normalized.includes(normalizeSqlText(TOOL_TRACES_MODEL_FK))) {
2380
+ return false;
2381
+ }
2382
+ // The origin lookup indexes are part of the shape this migration guarantees.
2383
+ return (this.indexExists('idx_tool_traces_operation_id') &&
2384
+ this.indexExists('idx_tool_traces_actor_principal'));
2385
+ }
2386
+ needsServiceOperationOriginsRepair071() {
2387
+ if (!this.tableExists('tool_traces')) {
2388
+ // The table itself is missing: let the recovery helper fail loudly.
2389
+ return true;
2390
+ }
2391
+ return !this.toolTracesHasOperationOriginShape() || !this.schemaVersionExists(71);
2392
+ }
2393
+ readForeignKeysEnabled() {
2394
+ const row = this.prepare('PRAGMA foreign_keys').get();
2395
+ return Boolean(row?.foreign_keys);
2396
+ }
2397
+ /**
2398
+ * tool_traces plus every table that declares a FK referencing it. This is the
2399
+ * relevant FK graph for the 071 rebuild: tool_traces' own model_runs reference
2400
+ * and any child (e.g. an ON DELETE CASCADE table) whose rows the rebuild must
2401
+ * have preserved. A valid child whose table name needs quoting (a space, an
2402
+ * embedded quote, a reserved word) is inspected correctly too: the name is fed
2403
+ * to the table-valued pragma as a bound argument, not interpolated -- so no
2404
+ * referencing child is silently skipped by an identifier regex.
2405
+ */
2406
+ tablesInToolTracesFkGraph() {
2407
+ const graph = new Set(['tool_traces']);
2408
+ const tables = this.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").all();
2409
+ for (const { name } of tables) {
2410
+ const fkList = this.prepare('SELECT "table" AS ref FROM pragma_foreign_key_list(?)').all(name);
2411
+ if (fkList.some((fk) => fk.ref === 'tool_traces')) {
2412
+ graph.add(name);
2413
+ }
2414
+ }
2415
+ return [...graph];
2416
+ }
2417
+ recoverServiceOperationOriginsMigration071() {
2418
+ if (!this.tableExists('tool_traces')) {
2419
+ throw new Error('Migration 071 recovery failed: missing table tool_traces');
2420
+ }
2421
+ // Idempotent: a database already carrying the full operation-origin shape
2422
+ // only needs its schema_version stamped. Never infer health from version.
2423
+ if (this.toolTracesHasOperationOriginShape()) {
2424
+ this.transaction(() => {
2425
+ this.prepare('INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)').run(71, 'Service operation-origin tool traces');
2426
+ });
2427
+ return;
2428
+ }
2429
+ // The 071 rebuild recreates the 068 scope indexes, which reference
2430
+ // owner_scope/project_id/channel_id. A legacy ledger stamped 68 but missing
2431
+ // those diagnostic columns (the main loop skips version 68 when it is
2432
+ // already stamped, so the in-loop 071 branch reaches here before the
2433
+ // end-of-run repair) would abort the rebuild with "no such column". Repair
2434
+ // the real 068 prerequisite first, reusing the same structural dependency
2435
+ // the late repair path already orders (068 before 071). This runs before
2436
+ // the FK toggle/transaction below, so no nested transaction is introduced,
2437
+ // and it is a no-op on a healthy database.
2438
+ if (this.needsToolTraceDiagnosticsRepair068()) {
2439
+ this.recoverToolTraceDiagnosticsMigration068();
2440
+ }
2441
+ // FK enforcement MUST be toggled OUTSIDE a transaction (SQLite silently
2442
+ // ignores the pragma while one is open). Disabling it before BEGIN is what
2443
+ // stops the DROP TABLE below from CASCADE-deleting real child rows; the
2444
+ // whole-graph foreign_key_check inside the transaction is the honest
2445
+ // preservation proof, never a post-deletion recount. Restore in finally.
2446
+ const previousForeignKeys = this.readForeignKeysEnabled();
2447
+ this.exec('PRAGMA foreign_keys = OFF');
2448
+ if (this.readForeignKeysEnabled()) {
2449
+ // A pragma that did not take effect means a transaction is already open;
2450
+ // proceeding would let CASCADE run. Refuse rather than pretend it worked.
2451
+ throw new Error('Migration 071 recovery failed: could not disable foreign_keys (transaction already open?)');
2452
+ }
2453
+ try {
2454
+ this.transaction(() => {
2455
+ // Preserve the COMPLETE existing table definition. Every column keeps its
2456
+ // exact declared type, NOT NULL/DEFAULT/CHECK/UNIQUE/COLLATE and quoted
2457
+ // name; only model_run_id is relaxed to nullable and the two origin
2458
+ // columns + origin CHECK are added. Reconstructing from name/type alone
2459
+ // would silently drop constraints and defaults a live database carries.
2460
+ const originalSql = this.toolTracesTableSql();
2461
+ const clauses = splitCreateTableClauses(originalSql);
2462
+ if (clauses.length === 0) {
2463
+ throw new Error('Migration 071 recovery failed: could not read tool_traces definition');
2464
+ }
2465
+ const existingColumnNames = [];
2466
+ const columnDefs = [];
2467
+ const constraintDefs = [];
2468
+ for (const clause of clauses) {
2469
+ if (clauseIsTableConstraint(clause)) {
2470
+ constraintDefs.push(clause);
2471
+ continue;
2472
+ }
2473
+ const name = clauseColumnName(clause);
2474
+ existingColumnNames.push(name);
2475
+ if (name === 'model_run_id') {
2476
+ // Relax the single NOT NULL so an operation-origin row (model NULL)
2477
+ // is allowed; everything else about the column is preserved.
2478
+ columnDefs.push(clause.replace(/\s+not\s+null\b/i, ''));
2479
+ }
2480
+ else {
2481
+ columnDefs.push(clause);
2482
+ }
2483
+ }
2484
+ const present = new Set(existingColumnNames);
2485
+ if (!present.has('operation_id')) {
2486
+ columnDefs.push('operation_id TEXT');
2487
+ }
2488
+ if (!present.has('actor_principal_id')) {
2489
+ columnDefs.push('actor_principal_id TEXT');
2490
+ }
2491
+ const constraints = [...constraintDefs];
2492
+ const hasModelFk = constraints.some((clause) => normalizeSqlText(clause).includes('referencesmodel_runs(model_run_id)'));
2493
+ if (!hasModelFk) {
2494
+ constraints.push(TOOL_TRACES_MODEL_FK);
2495
+ }
2496
+ constraints.push(TOOL_TRACES_ORIGIN_CHECK);
2497
+ // Preserve every custom/existing trigger on the old table; DROP TABLE
2498
+ // drops them, and RENAME does not carry them, so recreate from DDL.
2499
+ const triggerSqls = this.prepare("SELECT sql FROM sqlite_master WHERE type='trigger' AND tbl_name='tool_traces' AND sql IS NOT NULL").all().map((row) => row.sql);
2500
+ // Preserve every non-auto index on the old table (033/061/068 plus any
2501
+ // runtime-created ones) by recreating them from their stored DDL.
2502
+ const indexSqls = this.prepare("SELECT sql FROM sqlite_master WHERE type='index' AND tbl_name='tool_traces' AND sql IS NOT NULL").all().map((row) => row.sql);
2503
+ this.exec(`CREATE TABLE tool_traces_071_new (\n ${[...columnDefs, ...constraints].join(',\n ')}\n)`);
2504
+ // Copy every column the old table actually carries (canonical, runtime
2505
+ // extras, and any already-present operation_id/actor_principal_id values
2506
+ // from a partially-repaired shape - never dropping real origin values).
2507
+ // Each name is re-quoted: clauseColumnName decoded it, so a quoted name
2508
+ // carrying a space/embedded-quote/reserved word must be emitted quoted or
2509
+ // it would parse as several bare tokens and abort the rebuild.
2510
+ const columnList = existingColumnNames.map(quoteSqlIdentifier).join(', ');
2511
+ this.exec(`INSERT INTO tool_traces_071_new (${columnList}) SELECT ${columnList} FROM tool_traces`);
2512
+ this.exec('DROP TABLE tool_traces');
2513
+ this.exec('ALTER TABLE tool_traces_071_new RENAME TO tool_traces');
2514
+ for (const sql of indexSqls) {
2515
+ this.exec(sql.replace(/^CREATE\s+INDEX/i, 'CREATE INDEX IF NOT EXISTS'));
2516
+ }
2517
+ this.exec(`
2518
+ CREATE INDEX IF NOT EXISTS idx_tool_traces_model_run_id
2519
+ ON tool_traces(model_run_id, created_at DESC);
2520
+ CREATE INDEX IF NOT EXISTS idx_tool_traces_gateway_call_id
2521
+ ON tool_traces(gateway_call_id);
2522
+ CREATE INDEX IF NOT EXISTS idx_tool_traces_scope_recency
2523
+ ON tool_traces(owner_scope, project_id, created_at DESC, trace_id DESC);
2524
+ CREATE INDEX IF NOT EXISTS idx_tool_traces_channel_recency
2525
+ ON tool_traces(owner_scope, project_id, channel_id, created_at DESC, trace_id DESC);
2526
+ CREATE INDEX IF NOT EXISTS idx_tool_traces_operation_id
2527
+ ON tool_traces(operation_id, created_at DESC);
2528
+ CREATE INDEX IF NOT EXISTS idx_tool_traces_actor_principal
2529
+ ON tool_traces(actor_principal_id, created_at DESC);
2530
+ `);
2531
+ for (const sql of triggerSqls) {
2532
+ this.exec(sql);
2533
+ }
2534
+ if (!this.toolTracesHasOperationOriginShape()) {
2535
+ throw new Error('Migration 071 recovery failed: operation-origin shape missing after rebuild');
2536
+ }
2537
+ // Validate the RELEVANT FK graph, not merely tool_traces' own FK: check
2538
+ // tool_traces (its model_runs reference - catches a dangling row) AND
2539
+ // every table that references tool_traces (a CASCADE child that the
2540
+ // rebuild must have preserved). Any violation aborts the whole
2541
+ // transaction, so nothing partial or unattributed is ever committed.
2542
+ // Unrelated tables are deliberately NOT checked - a pre-existing
2543
+ // violation elsewhere must not block this upgrade.
2544
+ for (const table of this.tablesInToolTracesFkGraph()) {
2545
+ const violations = this.prepare('SELECT 1 FROM pragma_foreign_key_check(?)').all(table);
2546
+ if (violations.length > 0) {
2547
+ throw new Error(`Migration 071 recovery failed: foreign key violations after rebuild (${table})`);
2548
+ }
2549
+ }
2550
+ this.prepare('INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)').run(71, 'Service operation-origin tool traces');
2551
+ });
2552
+ }
2553
+ finally {
2554
+ this.exec(`PRAGMA foreign_keys = ${previousForeignKeys ? 'ON' : 'OFF'}`);
2555
+ }
2556
+ }
650
2557
  recoverMemoryProvenanceMigration032() {
651
2558
  this.transaction(() => {
652
2559
  const expectedDecisionColumns = [
@@ -713,443 +2620,6 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
713
2620
  }
714
2621
  }
715
2622
  }
716
- recoverConnectorEventScopeMigration034() {
717
- this.transaction(() => {
718
- const expectedColumns = [
719
- 'source_cursor',
720
- 'tenant_id',
721
- 'project_id',
722
- 'memory_scope_kind',
723
- 'memory_scope_id',
724
- ];
725
- const columns = this.tableColumns('connector_event_index');
726
- for (const column of expectedColumns) {
727
- if (!columns.has(column)) {
728
- this.exec(`ALTER TABLE connector_event_index ADD COLUMN ${column} TEXT`);
729
- columns.add(column);
730
- }
731
- }
732
- this.exec(`
733
- CREATE INDEX IF NOT EXISTS idx_connector_event_scope
734
- ON connector_event_index(tenant_id, project_id, memory_scope_kind, memory_scope_id)
735
- `);
736
- this.exec(`
737
- CREATE INDEX IF NOT EXISTS idx_connector_event_source_cursor
738
- ON connector_event_index(source_connector, source_cursor)
739
- `);
740
- this.assertMigration034Complete();
741
- this.prepare('INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)').run(34, 'Add connector event scope columns');
742
- });
743
- }
744
- assertMigration034Complete() {
745
- const columns = this.tableColumns('connector_event_index');
746
- for (const column of [
747
- 'source_cursor',
748
- 'tenant_id',
749
- 'project_id',
750
- 'memory_scope_kind',
751
- 'memory_scope_id',
752
- ]) {
753
- if (!columns.has(column)) {
754
- throw new Error(`Migration 034 recovery failed: missing connector_event_index.${column}`);
755
- }
756
- }
757
- for (const indexName of ['idx_connector_event_scope', 'idx_connector_event_source_cursor']) {
758
- if (!this.indexExists(indexName)) {
759
- throw new Error(`Migration 034 recovery failed: missing index ${indexName}`);
760
- }
761
- }
762
- }
763
- recoverConnectorEventSourceEntityIdMigration067() {
764
- this.transaction(() => {
765
- const columns = this.tableColumns('connector_event_index');
766
- if (!columns.has('source_entity_id')) {
767
- this.exec('ALTER TABLE connector_event_index ADD COLUMN source_entity_id TEXT');
768
- }
769
- // Backfill only when metadata_json exists; a legacy/minimal table rebuilt by the repair path
770
- // may not have it yet. New rows carry source_entity_id at write time regardless.
771
- if (columns.has('metadata_json')) {
772
- this.exec(`
773
- UPDATE connector_event_index
774
- SET source_entity_id = json_extract(metadata_json, '$.sourceEntityId')
775
- WHERE source_entity_id IS NULL
776
- AND metadata_json IS NOT NULL
777
- AND json_extract(metadata_json, '$.sourceEntityId') IS NOT NULL
778
- `);
779
- }
780
- this.exec(`
781
- CREATE INDEX IF NOT EXISTS idx_connector_event_source_entity
782
- ON connector_event_index(source_connector, source_entity_id)
783
- `);
784
- this.assertMigration067Complete();
785
- this.prepare('INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)').run(67, 'Add connector_event_index source_entity_id');
786
- });
787
- }
788
- assertMigration067Complete() {
789
- const columns = this.tableColumns('connector_event_index');
790
- if (!columns.has('source_entity_id')) {
791
- throw new Error('Migration 067 recovery failed: missing connector_event_index.source_entity_id');
792
- }
793
- if (!this.indexExists('idx_connector_event_source_entity')) {
794
- throw new Error('Migration 067 recovery failed: missing index idx_connector_event_source_entity');
795
- }
796
- }
797
- recoverConnectorEventOperatorSeqMigration039() {
798
- this.transaction(() => {
799
- const columns = this.tableColumns('connector_event_index');
800
- if (!columns.has('operator_ingest_seq')) {
801
- this.exec(`
802
- ALTER TABLE connector_event_index
803
- ADD COLUMN operator_ingest_seq INTEGER CHECK (
804
- operator_ingest_seq IS NULL OR operator_ingest_seq >= 1
805
- )
806
- `);
807
- }
808
- this.exec(`
809
- CREATE TABLE IF NOT EXISTS connector_event_index_operator_seq_cursors (
810
- source_connector TEXT NOT NULL,
811
- channel TEXT NOT NULL DEFAULT '',
812
- next_seq INTEGER NOT NULL CHECK (next_seq >= 1),
813
- PRIMARY KEY (source_connector, channel)
814
- )
815
- `);
816
- this.exec(`
817
- WITH ranked_events AS (
818
- SELECT
819
- event_index_id,
820
- ROW_NUMBER() OVER (
821
- PARTITION BY source_connector, COALESCE(channel, '')
822
- ORDER BY rowid ASC
823
- ) AS operator_seq
824
- FROM connector_event_index
825
- )
826
- UPDATE connector_event_index
827
- SET operator_ingest_seq = (
828
- SELECT operator_seq
829
- FROM ranked_events
830
- WHERE ranked_events.event_index_id = connector_event_index.event_index_id
831
- )
832
- WHERE operator_ingest_seq IS NULL
833
- `);
834
- this.exec(`
835
- INSERT OR IGNORE INTO connector_event_index_operator_seq_cursors (
836
- source_connector,
837
- channel,
838
- next_seq
839
- )
840
- SELECT
841
- source_connector,
842
- COALESCE(channel, ''),
843
- COALESCE(MAX(operator_ingest_seq), 0) + 1
844
- FROM connector_event_index
845
- GROUP BY source_connector, COALESCE(channel, '')
846
- `);
847
- this.exec(`
848
- CREATE UNIQUE INDEX IF NOT EXISTS idx_connector_event_index_operator_scope_seq
849
- ON connector_event_index(source_connector, COALESCE(channel, ''), operator_ingest_seq)
850
- WHERE operator_ingest_seq IS NOT NULL
851
- `);
852
- this.exec(`
853
- CREATE INDEX IF NOT EXISTS idx_connector_event_index_operator_cursor_order
854
- ON connector_event_index(source_connector, channel, operator_ingest_seq)
855
- `);
856
- this.exec(`
857
- CREATE TRIGGER IF NOT EXISTS trg_connector_event_index_operator_ingest_seq_ai
858
- AFTER INSERT ON connector_event_index
859
- WHEN NEW.operator_ingest_seq IS NULL
860
- BEGIN
861
- INSERT OR IGNORE INTO connector_event_index_operator_seq_cursors (
862
- source_connector,
863
- channel,
864
- next_seq
865
- )
866
- VALUES (NEW.source_connector, COALESCE(NEW.channel, ''), 1);
867
-
868
- UPDATE connector_event_index
869
- SET operator_ingest_seq = (
870
- SELECT next_seq
871
- FROM connector_event_index_operator_seq_cursors
872
- WHERE source_connector = NEW.source_connector
873
- AND channel = COALESCE(NEW.channel, '')
874
- )
875
- WHERE event_index_id = NEW.event_index_id;
876
-
877
- UPDATE connector_event_index_operator_seq_cursors
878
- SET next_seq = next_seq + 1
879
- WHERE source_connector = NEW.source_connector
880
- AND channel = COALESCE(NEW.channel, '');
881
- END
882
- `);
883
- this.exec(`
884
- CREATE TRIGGER IF NOT EXISTS trg_connector_event_index_operator_ingest_seq_explicit_ai
885
- AFTER INSERT ON connector_event_index
886
- WHEN NEW.operator_ingest_seq IS NOT NULL
887
- BEGIN
888
- INSERT OR IGNORE INTO connector_event_index_operator_seq_cursors (
889
- source_connector,
890
- channel,
891
- next_seq
892
- )
893
- VALUES (NEW.source_connector, COALESCE(NEW.channel, ''), 1);
894
-
895
- UPDATE connector_event_index_operator_seq_cursors
896
- SET next_seq = CASE
897
- WHEN next_seq <= NEW.operator_ingest_seq THEN NEW.operator_ingest_seq + 1
898
- ELSE next_seq
899
- END
900
- WHERE source_connector = NEW.source_connector
901
- AND channel = COALESCE(NEW.channel, '');
902
- END
903
- `);
904
- this.assertMigration039Complete();
905
- this.prepare('INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)').run(39, 'Add connector event operator ingest sequence');
906
- });
907
- }
908
- assertMigration039Complete() {
909
- const columns = this.tableColumns('connector_event_index');
910
- if (!columns.has('operator_ingest_seq')) {
911
- throw new Error('Migration 039 recovery failed: missing connector_event_index.operator_ingest_seq');
912
- }
913
- if (!this.tableExists('connector_event_index_operator_seq_cursors')) {
914
- throw new Error('Migration 039 recovery failed: missing connector_event_index_operator_seq_cursors');
915
- }
916
- for (const indexName of [
917
- 'idx_connector_event_index_operator_scope_seq',
918
- 'idx_connector_event_index_operator_cursor_order',
919
- ]) {
920
- if (!this.indexExists(indexName)) {
921
- throw new Error(`Migration 039 recovery failed: missing index ${indexName}`);
922
- }
923
- }
924
- for (const triggerName of [
925
- 'trg_connector_event_index_operator_ingest_seq_ai',
926
- 'trg_connector_event_index_operator_ingest_seq_explicit_ai',
927
- ]) {
928
- if (!this.triggerExists(triggerName)) {
929
- throw new Error(`Migration 039 recovery failed: missing trigger ${triggerName}`);
930
- }
931
- }
932
- }
933
- recoverConnectorEventSequencesMigration062() {
934
- this.transaction(() => {
935
- const columns = this.tableColumns('connector_event_index');
936
- if (!columns.has('operator_observation_seq')) {
937
- this.exec(`
938
- ALTER TABLE connector_event_index
939
- ADD COLUMN operator_observation_seq INTEGER CHECK (
940
- operator_observation_seq IS NULL OR operator_observation_seq >= 1
941
- )
942
- `);
943
- }
944
- this.exec(`
945
- CREATE TABLE IF NOT EXISTS connector_event_index_observation_cursors (
946
- source_connector TEXT PRIMARY KEY,
947
- next_seq INTEGER NOT NULL CHECK (next_seq >= 1)
948
- )
949
- `);
950
- this.exec(`
951
- WITH existing_max AS (
952
- SELECT source_connector, MAX(operator_observation_seq) AS max_seq
953
- FROM connector_event_index
954
- WHERE operator_observation_seq IS NOT NULL
955
- GROUP BY source_connector
956
- ), ranked_nulls AS (
957
- SELECT event_index_id,
958
- source_connector,
959
- ROW_NUMBER() OVER (
960
- PARTITION BY source_connector
961
- ORDER BY source_timestamp_ms, rowid
962
- ) AS seq
963
- FROM connector_event_index
964
- WHERE operator_observation_seq IS NULL
965
- )
966
- UPDATE connector_event_index
967
- SET operator_observation_seq = (
968
- SELECT COALESCE(existing_max.max_seq, 0) + ranked_nulls.seq
969
- FROM ranked_nulls
970
- LEFT JOIN existing_max
971
- ON existing_max.source_connector = ranked_nulls.source_connector
972
- WHERE ranked_nulls.event_index_id = connector_event_index.event_index_id
973
- )
974
- WHERE operator_observation_seq IS NULL
975
- `);
976
- this.exec(`
977
- INSERT INTO connector_event_index_observation_cursors (source_connector, next_seq)
978
- SELECT source_connector, MAX(operator_observation_seq) + 1
979
- FROM connector_event_index
980
- GROUP BY source_connector
981
- ON CONFLICT(source_connector) DO UPDATE SET next_seq = CASE
982
- WHEN connector_event_index_observation_cursors.next_seq < excluded.next_seq
983
- THEN excluded.next_seq
984
- ELSE connector_event_index_observation_cursors.next_seq
985
- END
986
- `);
987
- this.exec(`
988
- CREATE UNIQUE INDEX IF NOT EXISTS idx_connector_event_index_observation_seq
989
- ON connector_event_index(source_connector, operator_observation_seq)
990
- WHERE operator_observation_seq IS NOT NULL
991
- `);
992
- this.exec(`
993
- CREATE TRIGGER IF NOT EXISTS trg_connector_event_index_operator_ingest_seq_au
994
- AFTER UPDATE OF operator_ingest_seq ON connector_event_index
995
- WHEN NEW.operator_ingest_seq IS NULL AND OLD.operator_ingest_seq IS NOT NULL
996
- BEGIN
997
- INSERT INTO connector_event_index_operator_seq_cursors
998
- (source_connector, channel, next_seq)
999
- VALUES (NEW.source_connector, COALESCE(NEW.channel, ''), 1)
1000
- ON CONFLICT(source_connector, channel) DO NOTHING;
1001
- UPDATE connector_event_index
1002
- SET operator_ingest_seq = (
1003
- SELECT next_seq FROM connector_event_index_operator_seq_cursors
1004
- WHERE source_connector = NEW.source_connector
1005
- AND channel = COALESCE(NEW.channel, '')
1006
- )
1007
- WHERE event_index_id = NEW.event_index_id;
1008
- UPDATE connector_event_index_operator_seq_cursors
1009
- SET next_seq = next_seq + 1
1010
- WHERE source_connector = NEW.source_connector
1011
- AND channel = COALESCE(NEW.channel, '');
1012
- END
1013
- `);
1014
- this.exec(`
1015
- CREATE TRIGGER IF NOT EXISTS trg_connector_event_index_observation_seq_ai
1016
- AFTER INSERT ON connector_event_index
1017
- WHEN NEW.operator_observation_seq IS NULL
1018
- BEGIN
1019
- INSERT INTO connector_event_index_observation_cursors
1020
- (source_connector, next_seq)
1021
- VALUES (NEW.source_connector, 1)
1022
- ON CONFLICT(source_connector) DO NOTHING;
1023
- UPDATE connector_event_index
1024
- SET operator_observation_seq = (
1025
- SELECT next_seq FROM connector_event_index_observation_cursors
1026
- WHERE source_connector = NEW.source_connector
1027
- )
1028
- WHERE event_index_id = NEW.event_index_id;
1029
- UPDATE connector_event_index_observation_cursors
1030
- SET next_seq = next_seq + 1
1031
- WHERE source_connector = NEW.source_connector;
1032
- END
1033
- `);
1034
- this.exec(`
1035
- CREATE TRIGGER IF NOT EXISTS trg_connector_event_index_observation_seq_au
1036
- AFTER UPDATE OF operator_observation_seq ON connector_event_index
1037
- WHEN NEW.operator_observation_seq IS NULL AND OLD.operator_observation_seq IS NOT NULL
1038
- BEGIN
1039
- INSERT INTO connector_event_index_observation_cursors
1040
- (source_connector, next_seq)
1041
- VALUES (NEW.source_connector, 1)
1042
- ON CONFLICT(source_connector) DO NOTHING;
1043
- UPDATE connector_event_index
1044
- SET operator_observation_seq = (
1045
- SELECT next_seq FROM connector_event_index_observation_cursors
1046
- WHERE source_connector = NEW.source_connector
1047
- )
1048
- WHERE event_index_id = NEW.event_index_id;
1049
- UPDATE connector_event_index_observation_cursors
1050
- SET next_seq = next_seq + 1
1051
- WHERE source_connector = NEW.source_connector;
1052
- END
1053
- `);
1054
- this.exec(`
1055
- CREATE TRIGGER IF NOT EXISTS trg_connector_event_index_observation_seq_explicit_ai
1056
- AFTER INSERT ON connector_event_index
1057
- WHEN NEW.operator_observation_seq IS NOT NULL
1058
- BEGIN
1059
- INSERT INTO connector_event_index_observation_cursors
1060
- (source_connector, next_seq)
1061
- VALUES (NEW.source_connector, 1)
1062
- ON CONFLICT(source_connector) DO NOTHING;
1063
- UPDATE connector_event_index_observation_cursors
1064
- SET next_seq = CASE
1065
- WHEN next_seq <= NEW.operator_observation_seq THEN NEW.operator_observation_seq + 1
1066
- ELSE next_seq
1067
- END
1068
- WHERE source_connector = NEW.source_connector;
1069
- END
1070
- `);
1071
- this.assertMigration062Complete();
1072
- this.prepare('INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)').run(62, 'Refresh connector event delivery and observation sequences');
1073
- });
1074
- }
1075
- assertMigration062Complete() {
1076
- const columns = this.tableColumns('connector_event_index');
1077
- if (!columns.has('operator_observation_seq')) {
1078
- throw new Error('Migration 062 recovery failed: missing connector_event_index.operator_observation_seq');
1079
- }
1080
- if (!this.tableExists('connector_event_index_observation_cursors')) {
1081
- throw new Error('Migration 062 recovery failed: missing connector_event_index_observation_cursors');
1082
- }
1083
- const cursorColumns = this.tableColumns('connector_event_index_observation_cursors');
1084
- for (const columnName of ['source_connector', 'next_seq']) {
1085
- if (!cursorColumns.has(columnName)) {
1086
- throw new Error(`Migration 062 recovery failed: missing connector_event_index_observation_cursors.${columnName}`);
1087
- }
1088
- }
1089
- if (!this.indexExists('idx_connector_event_index_observation_seq')) {
1090
- throw new Error('Migration 062 recovery failed: missing index idx_connector_event_index_observation_seq');
1091
- }
1092
- for (const triggerName of [
1093
- 'trg_connector_event_index_operator_ingest_seq_au',
1094
- 'trg_connector_event_index_observation_seq_ai',
1095
- 'trg_connector_event_index_observation_seq_au',
1096
- 'trg_connector_event_index_observation_seq_explicit_ai',
1097
- ]) {
1098
- if (!this.triggerExists(triggerName)) {
1099
- throw new Error(`Migration 062 recovery failed: missing trigger ${triggerName}`);
1100
- }
1101
- }
1102
- }
1103
- recoverConnectorEventLegacyRefreshMigration063() {
1104
- this.transaction(() => {
1105
- this.assertMigration062Complete();
1106
- const connectorColumns = this.tableColumns('connector_event_index');
1107
- for (const columnName of [
1108
- 'content_hash',
1109
- 'metadata_json',
1110
- 'source_timestamp_ms',
1111
- 'source_type',
1112
- 'channel',
1113
- 'operator_ingest_seq',
1114
- 'operator_observation_seq',
1115
- ]) {
1116
- if (!connectorColumns.has(columnName)) {
1117
- throw new Error(`Migration 063 recovery failed: missing connector_event_index.${columnName}`);
1118
- }
1119
- }
1120
- this.exec(`
1121
- CREATE TRIGGER IF NOT EXISTS trg_connector_event_index_legacy_content_refresh_au
1122
- AFTER UPDATE OF content_hash, metadata_json, source_timestamp_ms, source_type, channel
1123
- ON connector_event_index
1124
- WHEN (
1125
- OLD.content_hash IS NOT NEW.content_hash
1126
- OR OLD.metadata_json IS NOT NEW.metadata_json
1127
- OR OLD.source_timestamp_ms IS NOT NEW.source_timestamp_ms
1128
- OR OLD.source_type IS NOT NEW.source_type
1129
- OR OLD.channel IS NOT NEW.channel
1130
- ) AND (
1131
- NEW.operator_ingest_seq IS OLD.operator_ingest_seq
1132
- OR NEW.operator_observation_seq IS OLD.operator_observation_seq
1133
- )
1134
- BEGIN
1135
- UPDATE connector_event_index
1136
- SET operator_ingest_seq = CASE
1137
- WHEN NEW.operator_ingest_seq IS OLD.operator_ingest_seq THEN NULL
1138
- ELSE NEW.operator_ingest_seq
1139
- END,
1140
- operator_observation_seq = CASE
1141
- WHEN NEW.operator_observation_seq IS OLD.operator_observation_seq THEN NULL
1142
- ELSE NEW.operator_observation_seq
1143
- END
1144
- WHERE event_index_id = NEW.event_index_id;
1145
- END
1146
- `);
1147
- if (!this.triggerExists('trg_connector_event_index_legacy_content_refresh_au')) {
1148
- throw new Error('Migration 063 recovery failed: missing trigger trg_connector_event_index_legacy_content_refresh_au');
1149
- }
1150
- this.prepare('INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)').run(63, 'Refresh sequences for legacy connector event updates');
1151
- });
1152
- }
1153
2623
  tableColumns(tableName) {
1154
2624
  if (!SQLITE_IDENTIFIER_PATTERN.test(tableName)) {
1155
2625
  throw new Error('Invalid SQLite table identifier');
@@ -1168,6 +2638,13 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
1168
2638
  const row = this.prepare("SELECT name FROM sqlite_master WHERE type='index' AND name = ?").get(indexName);
1169
2639
  return row?.name === indexName;
1170
2640
  }
2641
+ indexSql(indexName) {
2642
+ if (!SQLITE_IDENTIFIER_PATTERN.test(indexName)) {
2643
+ throw new Error('Invalid SQLite index identifier');
2644
+ }
2645
+ const row = this.prepare("SELECT sql FROM sqlite_master WHERE type='index' AND name = ?").get(indexName);
2646
+ return row?.sql ?? '';
2647
+ }
1171
2648
  triggerExists(triggerName) {
1172
2649
  const row = this.prepare("SELECT name FROM sqlite_master WHERE type='trigger' AND name = ?").get(triggerName);
1173
2650
  return row?.name === triggerName;