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