@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
@@ -5,42 +5,61 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.getChannelSummary = exports.upsertChannelSummary = exports.LEXICAL_STOPWORDS = void 0;
7
7
  exports.buildDecisionId = buildDecisionId;
8
+ exports.readMemoryRecordsInScopes = readMemoryRecordsInScopes;
9
+ exports.readMemoryRecordById = readMemoryRecordById;
8
10
  exports.getLexicalQueryTokens = getLexicalQueryTokens;
9
11
  exports.topicAffinityBoost = topicAffinityBoost;
10
12
  exports.loadEdgesForIds = loadEdgesForIds;
11
13
  exports.saveMemory = saveMemory;
12
- exports.saveMemoryWithTrustedProvenance = saveMemoryWithTrustedProvenance;
14
+ exports.saveJudgmentRecord = saveJudgmentRecord;
15
+ exports.boundReadScopesFor = boundReadScopesFor;
16
+ exports.saveLegacyMemory = saveLegacyMemory;
13
17
  exports.promoteMemoryStatus = promoteMemoryStatus;
18
+ exports.retireMemoryRecord = retireMemoryRecord;
14
19
  exports.buildProfile = buildProfile;
15
20
  exports.recallMemory = recallMemory;
16
21
  exports.ingestMemory = ingestMemory;
17
- exports.ingestWithTrustedProvenance = ingestWithTrustedProvenance;
18
22
  exports.evolveMemory = evolveMemory;
19
23
  exports.buildMemoryBootstrap = buildMemoryBootstrap;
20
24
  exports.createAuditAck = createAuditAck;
21
25
  exports.recordMemoryAudit = recordMemoryAudit;
22
- exports.setExtractionFn = setExtractionFn;
23
26
  exports.ingestConversation = ingestConversation;
24
- exports.ingestConversationWithTrustedProvenance = ingestConversationWithTrustedProvenance;
27
+ exports.expandWithGraphInAdapter = expandWithGraphInAdapter;
28
+ exports.suggestInAdapter = suggestInAdapter;
29
+ exports.listDecisionsInAdapter = listDecisionsInAdapter;
30
+ exports.updateOutcomeInAdapter = updateOutcomeInAdapter;
31
+ exports.readMemoryStatsInAdapter = readMemoryStatsInAdapter;
32
+ exports.saveCheckpointInAdapter = saveCheckpointInAdapter;
33
+ exports.loadCheckpointInAdapter = loadCheckpointInAdapter;
34
+ exports.listCheckpointsInAdapter = listCheckpointsInAdapter;
25
35
  const node_crypto_1 = __importDefault(require("node:crypto"));
36
+ const canonicalize_js_1 = require("../canonicalize.js");
26
37
  const db_manager_js_1 = require("../db-manager.js");
27
- const embeddings_js_1 = require("../embeddings.js");
28
- const wiki_page_index_js_1 = require("../cases/wiki-page-index.js");
38
+ const write_adapters_js_1 = require("./write-adapters.js");
39
+ const decision_formatter_js_1 = require("../decision-formatter.js");
40
+ const debug_logger_js_1 = require("../debug-logger.js");
41
+ const graph_query_js_1 = require("../knowledge/graph-query.js");
42
+ const case_search_rollup_js_1 = require("../knowledge/case-search-rollup.js");
43
+ const ranker_rescore_js_1 = require("../knowledge/ranker-rescore.js");
44
+ const ranker_features_js_1 = require("../knowledge/ranker-features.js");
45
+ const search_js_1 = require("../knowledge/search.js");
46
+ const embedder_js_1 = require("../embedding/embedder.js");
47
+ const index_js_1 = require("../knowledge/index.js");
48
+ const write_adapters_js_2 = require("./write-adapters.js");
29
49
  const profile_builder_js_1 = require("./profile-builder.js");
30
50
  const bootstrap_builder_js_1 = require("./bootstrap-builder.js");
31
51
  const evolution_engine_js_1 = require("./evolution-engine.js");
32
52
  const channel_summary_state_store_js_1 = require("./channel-summary-state-store.js");
33
- const debug_logger_js_1 = require("../debug-logger.js");
34
- const extraction_prompt_js_1 = require("./extraction-prompt.js");
53
+ const debug_logger_js_2 = require("../debug-logger.js");
54
+ const secret_filter_js_1 = require("./secret-filter.js");
35
55
  const types_js_1 = require("./types.js");
36
56
  const channel_summary_store_js_1 = require("./channel-summary-store.js");
37
57
  Object.defineProperty(exports, "getChannelSummary", { enumerable: true, get: function () { return channel_summary_store_js_1.getChannelSummary; } });
38
58
  Object.defineProperty(exports, "upsertChannelSummary", { enumerable: true, get: function () { return channel_summary_store_js_1.upsertChannelSummary; } });
39
- const recall_bridge_js_1 = require("../entities/recall-bridge.js");
40
- const read_identity_js_1 = require("../entities/read-identity.js");
41
- const search_quality_js_1 = require("../search/search-quality.js");
42
- const event_store_js_1 = require("./event-store.js");
59
+ const search_quality_js_1 = require("../knowledge/search-quality.js");
43
60
  const provenance_js_1 = require("./provenance.js");
61
+ const judgments_js_1 = require("../knowledge/judgments.js");
62
+ const record_identity_js_1 = require("../registry/record-identity.js");
44
63
  function buildDecisionId(topic) {
45
64
  // Non-ASCII topics (Korean etc.) used to collapse into bare underscore runs
46
65
  // ("decision_______<ts>") - unreadable and near-colliding. Collapse the runs
@@ -76,12 +95,35 @@ function toMemoryRecord(row, scopes, fallbackSource) {
76
95
  }
77
96
  }
78
97
  const savedSource = trustContext?.source;
98
+ let guidance = {};
99
+ if (typeof row.payload_json === 'string') {
100
+ try {
101
+ const payload = JSON.parse(row.payload_json);
102
+ if (payload.guidance &&
103
+ typeof payload.guidance === 'object' &&
104
+ !Array.isArray(payload.guidance)) {
105
+ guidance = payload.guidance;
106
+ }
107
+ }
108
+ catch {
109
+ /* malformed payloads do not become guidance */
110
+ }
111
+ }
112
+ const steps = Array.isArray(guidance.steps)
113
+ ? guidance.steps.filter((step) => typeof step === 'string')
114
+ : undefined;
115
+ const evidenceChecks = Array.isArray(guidance.evidence_checks)
116
+ ? guidance.evidence_checks.filter((check) => typeof check === 'string')
117
+ : undefined;
79
118
  return {
80
119
  id: String(row.id),
81
120
  topic: String(row.topic),
82
121
  kind: row.kind ?? 'decision',
83
122
  summary: String(row.summary ?? row.decision ?? ''),
84
123
  details: String(row.reasoning ?? row.decision ?? ''),
124
+ ...(typeof guidance.applies_when === 'string' ? { applies_when: guidance.applies_when } : {}),
125
+ ...(steps === undefined ? {} : { steps }),
126
+ ...(evidenceChecks === undefined ? {} : { evidence_checks: evidenceChecks }),
85
127
  confidence: Number(row.confidence ?? 0.5),
86
128
  status: row.status ?? 'active',
87
129
  scopes,
@@ -92,124 +134,71 @@ function toMemoryRecord(row, scopes, fallbackSource) {
92
134
  event_datetime: typeof row.event_datetime === 'number' && Number.isFinite(row.event_datetime)
93
135
  ? row.event_datetime
94
136
  : null,
137
+ outcome: row.outcome ?? null,
95
138
  };
96
139
  }
97
- function loadEventDateTimeForObservations(adapter, observationIds) {
98
- const uniqueIds = Array.from(new Set(observationIds.filter(Boolean)));
99
- if (uniqueIds.length === 0) {
100
- return null;
101
- }
102
- const placeholders = uniqueIds.map(() => '?').join(', ');
103
- const row = adapter
104
- .prepare(`
105
- SELECT MAX(COALESCE(timestamp_observed, created_at)) AS event_datetime
106
- FROM entity_observations
107
- WHERE id IN (${placeholders})
108
- `)
109
- .get(...uniqueIds);
110
- return typeof row?.event_datetime === 'number' && Number.isFinite(row.event_datetime)
111
- ? row.event_datetime
112
- : null;
140
+ function scopeBoundRowsClause(scopes, alias = 'd') {
141
+ if (scopes.length === 0)
142
+ return { sql: '0', params: [] };
143
+ const alternatives = scopes.map(() => '(s.kind = ? AND s.external_id = ?)').join(' OR ');
144
+ return {
145
+ sql: `EXISTS (
146
+ SELECT 1 FROM memory_scope_bindings b
147
+ JOIN memory_scopes s ON s.id = b.scope_id
148
+ WHERE b.memory_id = ${alias}.id AND (${alternatives})
149
+ )`,
150
+ params: scopes.flatMap((scope) => [scope.kind, scope.id]),
151
+ };
113
152
  }
114
- function getTimelineEntityKindPriority(kind) {
115
- switch (kind) {
116
- case 'work_item':
117
- return 0;
118
- case 'project':
119
- return 1;
120
- case 'organization':
121
- return 2;
122
- case 'person':
123
- return 3;
124
- default:
125
- return 99;
153
+ /** Read complete memory records whose stored scopes intersect the admitted scopes. */
154
+ async function readMemoryRecordsInScopes(adapter, scopes, options = {}) {
155
+ const scopeClause = scopeBoundRowsClause(scopes);
156
+ const conditions = [scopeClause.sql];
157
+ const params = [...scopeClause.params];
158
+ const kinds = options.kind === undefined ? [] : Array.isArray(options.kind) ? options.kind : [options.kind];
159
+ if (kinds.length > 0) {
160
+ conditions.push(`d.kind IN (${kinds.map(() => '?').join(', ')})`);
161
+ params.push(...kinds);
126
162
  }
127
- }
128
- function resolveTimelineTargetEntityIdFromObservations(adapter, observationIds) {
129
- const uniqueIds = Array.from(new Set(observationIds.filter(Boolean)));
130
- if (uniqueIds.length === 0) {
131
- return null;
163
+ const statuses = options.status === undefined
164
+ ? []
165
+ : Array.isArray(options.status)
166
+ ? options.status
167
+ : [options.status];
168
+ if (statuses.length > 0) {
169
+ conditions.push(`COALESCE(d.status, 'active') IN (${statuses.map(() => '?').join(', ')})`);
170
+ params.push(...statuses);
132
171
  }
133
- const placeholders = uniqueIds.map(() => '?').join(', ');
134
172
  const rows = adapter
135
- .prepare(`
136
- SELECT
137
- n.id AS entity_id,
138
- n.kind AS entity_kind,
139
- COUNT(*) AS matched_observations
140
- FROM entity_lineage_links l
141
- JOIN entity_nodes n
142
- ON n.id = l.canonical_entity_id
143
- WHERE l.entity_observation_id IN (${placeholders})
144
- AND l.status = 'active'
145
- AND n.status = 'active'
146
- AND n.merged_into IS NULL
147
- GROUP BY n.id, n.kind
148
- `)
149
- .all(...uniqueIds);
150
- rows.sort((left, right) => {
151
- const kindDelta = getTimelineEntityKindPriority(left.entity_kind) -
152
- getTimelineEntityKindPriority(right.entity_kind);
153
- if (kindDelta !== 0) {
154
- return kindDelta;
155
- }
156
- if (right.matched_observations !== left.matched_observations) {
157
- return right.matched_observations - left.matched_observations;
158
- }
159
- return left.entity_id.localeCompare(right.entity_id);
160
- });
161
- return rows[0]?.entity_id ?? null;
173
+ .prepare(`SELECT d.id, d.topic, d.decision, d.reasoning, d.confidence, d.created_at, d.updated_at,
174
+ d.trust_context, d.kind, d.status, d.summary, d.event_date, d.event_datetime,
175
+ d.outcome, d.payload_json
176
+ FROM decisions d
177
+ WHERE ${conditions.join(' AND ')}
178
+ ORDER BY d.created_at ASC, d.id ASC`)
179
+ .all(...params);
180
+ const scopesById = batchLoadScopes(adapter, rows.map((row) => String(row.id)));
181
+ const fallbackSource = { package: 'mama-core', source_type: 'db' };
182
+ return rows.map((row) => toMemoryRecord(row, scopesById.get(String(row.id)) ?? [], fallbackSource));
162
183
  }
163
- function buildTimelineEventForSave(adapter, memoryId, topic, entityObservationIds, timelineEvent) {
164
- if (!timelineEvent) {
184
+ /** Read one complete record only when at least one stored scope is admitted. */
185
+ async function readMemoryRecordById(adapter, memoryId, scopes) {
186
+ const id = memoryId.trim();
187
+ const scopeClause = scopeBoundRowsClause(scopes);
188
+ if (!id || scopes.length === 0)
165
189
  return null;
166
- }
167
- const resolvedEntityId = timelineEvent.entity_id ??
168
- resolveTimelineTargetEntityIdFromObservations(adapter, entityObservationIds);
169
- if (!resolvedEntityId) {
190
+ const row = adapter
191
+ .prepare(`SELECT d.id, d.topic, d.decision, d.reasoning, d.confidence, d.created_at, d.updated_at,
192
+ d.trust_context, d.kind, d.status, d.summary, d.event_date, d.event_datetime,
193
+ d.outcome, d.payload_json
194
+ FROM decisions d
195
+ WHERE d.id = ? AND ${scopeClause.sql}
196
+ LIMIT 1`)
197
+ .get(id, ...scopeClause.params);
198
+ if (!row)
170
199
  return null;
171
- }
172
- return {
173
- id: timelineEvent.id ?? `et_${node_crypto_1.default.randomUUID()}`,
174
- entity_id: resolvedEntityId,
175
- event_type: timelineEvent.event_type,
176
- role: timelineEvent.role ?? null,
177
- valid_from: timelineEvent.valid_from ?? null,
178
- valid_to: timelineEvent.valid_to ?? null,
179
- observed_at: timelineEvent.observed_at ?? null,
180
- source_ref: timelineEvent.source_ref ?? `decision:${memoryId}`,
181
- summary: timelineEvent.summary,
182
- details: timelineEvent.details ??
183
- JSON.stringify({
184
- memory_id: memoryId,
185
- topic,
186
- }),
187
- };
188
- }
189
- function insertTimelineEventForSave(adapter, event, createdAt) {
190
- adapter
191
- .prepare(`
192
- INSERT INTO entity_timeline_events (
193
- id, entity_id, event_type, role, valid_from, valid_to, observed_at,
194
- source_ref, summary, details, created_at
195
- )
196
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
197
- `)
198
- .run(event.id, event.entity_id, event.event_type, event.role, event.valid_from, event.valid_to, event.observed_at, event.source_ref, event.summary, event.details, createdAt);
199
- }
200
- function cleanupFailedMemorySave(adapter, memoryId) {
201
- const row = adapter.prepare(`SELECT rowid FROM decisions WHERE id = ?`).get(memoryId);
202
- if (!row) {
203
- return;
204
- }
205
- const cleanup = () => {
206
- adapter.prepare(`DELETE FROM embeddings WHERE rowid = ?`).run(row.rowid);
207
- adapter.prepare(`DELETE FROM decisions WHERE id = ?`).run(memoryId);
208
- };
209
- const txResult = adapter.transaction(cleanup);
210
- if (typeof txResult === 'function') {
211
- txResult();
212
- }
200
+ const recordScopes = batchLoadScopes(adapter, [id]).get(id) ?? [];
201
+ return toMemoryRecord(row, recordScopes, { package: 'mama-core', source_type: 'db' });
213
202
  }
214
203
  function batchLoadScopes(adapter, memoryIds) {
215
204
  const scopeMap = new Map();
@@ -232,28 +221,27 @@ function batchLoadScopes(adapter, memoryIds) {
232
221
  }
233
222
  return scopeMap;
234
223
  }
235
- async function loadScopedMemories(scopes) {
236
- await (0, db_manager_js_1.initDB)();
237
- const adapter = (0, db_manager_js_1.getAdapter)();
224
+ async function loadScopedMemories(adapter, scopes) {
238
225
  const fallbackSource = { package: 'mama-core', source_type: 'db' };
239
226
  let rows;
240
227
  if (scopes.length === 0) {
241
228
  rows = adapter
242
229
  .prepare(`
243
230
  SELECT id, topic, decision, reasoning, confidence, created_at, updated_at, trust_context,
244
- kind, status, summary, event_date, event_datetime
231
+ kind, status, summary, event_date, event_datetime, outcome, payload_json
245
232
  FROM decisions
246
233
  ORDER BY COALESCE(event_datetime, created_at) DESC, created_at DESC
247
234
  `)
248
235
  .all();
249
236
  }
250
237
  else {
251
- const scopeIds = await Promise.all(scopes.map((scope) => (0, db_manager_js_1.ensureMemoryScope)(scope.kind, scope.id)));
238
+ const scopeIds = await Promise.all(scopes.map((scope) => (0, db_manager_js_1.ensureMemoryScope)(adapter, scope.kind, scope.id)));
252
239
  const placeholders = scopeIds.map(() => '?').join(', ');
253
240
  rows = adapter
254
241
  .prepare(`
255
242
  SELECT DISTINCT d.id, d.topic, d.decision, d.reasoning, d.confidence, d.created_at,
256
- d.updated_at, d.trust_context, d.kind, d.status, d.summary, d.event_date, d.event_datetime
243
+ d.updated_at, d.trust_context, d.kind, d.status, d.summary, d.event_date, d.event_datetime,
244
+ d.outcome, d.payload_json
257
245
  FROM decisions d
258
246
  JOIN memory_scope_bindings msb ON msb.memory_id = d.id
259
247
  WHERE msb.scope_id IN (${placeholders})
@@ -400,12 +388,25 @@ function buildLexicalCandidates(records, query) {
400
388
  return { memory: record, score };
401
389
  })
402
390
  .filter((candidate) => candidate.score > 0)
403
- .sort((left, right) => {
404
- if (right.score !== left.score) {
405
- return right.score - left.score;
406
- }
407
- return Number(right.memory.created_at) - Number(left.memory.created_at);
408
- });
391
+ .sort(compareLexicalCandidates);
392
+ }
393
+ function lexicalEventTime(record) {
394
+ if (typeof record.event_datetime === 'number' && Number.isFinite(record.event_datetime)) {
395
+ return record.event_datetime;
396
+ }
397
+ const numericCreated = Number(record.created_at);
398
+ if (Number.isFinite(numericCreated)) {
399
+ return numericCreated;
400
+ }
401
+ const parsedCreated = Date.parse(String(record.created_at));
402
+ return Number.isFinite(parsedCreated) ? parsedCreated : 0;
403
+ }
404
+ function compareLexicalCandidates(left, right) {
405
+ // Relevance wins; equally relevant history follows event time, not import or
406
+ // commit order. FTS rowid order is not an event-time judgment.
407
+ return (right.score - left.score ||
408
+ lexicalEventTime(right.memory) - lexicalEventTime(left.memory) ||
409
+ left.memory.id.localeCompare(right.memory.id));
409
410
  }
410
411
  function lexicalScoreToConfidence(score) {
411
412
  return Math.min(0.95, 0.45 + score * 0.05);
@@ -460,11 +461,9 @@ function _mergeRecallCandidates(primary, lexical) {
460
461
  })
461
462
  .map((candidate) => candidate.memory);
462
463
  }
463
- async function loadEdgesForIds(ids) {
464
+ async function loadEdgesForIds(adapter, ids) {
464
465
  if (ids.length === 0)
465
466
  return [];
466
- await (0, db_manager_js_1.initDB)();
467
- const adapter = (0, db_manager_js_1.getAdapter)();
468
467
  const placeholders = ids.map(() => '?').join(', ');
469
468
  const rows = adapter
470
469
  .prepare(`SELECT from_id, to_id, relationship AS type, reason
@@ -479,223 +478,221 @@ async function loadEdgesForIds(ids) {
479
478
  reason: row.reason ?? undefined,
480
479
  }));
481
480
  }
482
- async function saveMemoryInternal(input, options) {
483
- await (0, db_manager_js_1.initDB)();
484
- const adapter = (0, db_manager_js_1.getAdapter)();
485
- const id = buildDecisionId(input.topic);
486
- const now = Date.now();
487
- const provenance = (0, provenance_js_1.normalizeMemoryWriteProvenance)(options);
488
- const targetStatus = input.status ?? 'active';
489
- const shouldApplyEvolution = targetStatus === 'active';
490
- // Find evolution candidates: exact topic match first, then semantic search fallback
491
- const primaryScope = input.scopes.length > 0 ? input.scopes[0] : null;
492
- const excludeIds = new Set(input.excludeIds ?? []);
493
- let existingCandidates;
494
- if (!shouldApplyEvolution) {
495
- existingCandidates = [];
496
- }
497
- else if (primaryScope) {
498
- const scopeId = await (0, db_manager_js_1.ensureMemoryScope)(primaryScope.kind, primaryScope.id);
499
- existingCandidates = adapter
500
- .prepare(`
501
- SELECT d.id, d.topic, d.summary, d.kind
502
- FROM decisions d
503
- JOIN memory_scope_bindings msb ON msb.memory_id = d.id
504
- WHERE d.topic = ? AND msb.scope_id = ?
505
- AND (d.status = 'active' OR d.status IS NULL)
506
- AND d.superseded_by IS NULL
507
- ORDER BY d.created_at DESC
508
- LIMIT 5
509
- `)
510
- .all(input.topic, scopeId).filter((c) => !excludeIds.has(c.id));
511
- }
512
- else {
513
- existingCandidates = adapter
514
- .prepare(`
515
- SELECT id, topic, summary, kind
516
- FROM decisions
517
- WHERE topic = ?
518
- AND (status = 'active' OR status IS NULL)
519
- AND superseded_by IS NULL
520
- ORDER BY created_at DESC
521
- LIMIT 5
522
- `)
523
- .all(input.topic).filter((c) => !excludeIds.has(c.id));
481
+ const GUIDANCE_MEMORY_KINDS = new Set([
482
+ 'lesson',
483
+ 'preference',
484
+ 'constraint',
485
+ 'workflow',
486
+ ]);
487
+ function oneLineGuidanceText(value) {
488
+ return value.trim().replace(/\s+/g, ' ');
489
+ }
490
+ function guidancePayloadFor(input) {
491
+ if (!GUIDANCE_MEMORY_KINDS.has(input.kind))
492
+ return undefined;
493
+ const appliesWhen = typeof input.appliesWhen === 'string' ? oneLineGuidanceText(input.appliesWhen) : '';
494
+ if (!appliesWhen) {
495
+ throw new judgments_js_1.JudgmentError('INVALID_INPUT', `memory.save kind '${input.kind}' requires an appliesWhen line`);
524
496
  }
525
- // Semantic fallback: if no exact topic match, find similar memories via vector search
526
- if (shouldApplyEvolution && existingCandidates.length === 0) {
527
- try {
528
- const queryText = `${input.topic} ${input.summary}`;
529
- const embedding = await (0, embeddings_js_1.generateEmbedding)(queryText, 'query');
530
- // Exclude superseded history so a dense supersede chain cannot fill all 3
531
- // candidate slots and hide the prior ACTIVE decision (which would break the
532
- // evolution link and create an active duplicate).
533
- const semanticResults = await (0, db_manager_js_1.vectorSearch)(embedding, 3, 0.82, undefined, Array.from(EXCLUDED_STATUSES));
534
- // Scope-filter semantic candidates when a primary scope is available
535
- let scopeFiltered = semanticResults;
536
- if (primaryScope) {
537
- const semIds = semanticResults.map((r) => String(r.id));
538
- const semScopeMap = batchLoadScopes(adapter, semIds);
539
- const scopeKey = `${primaryScope.kind}:${primaryScope.id}`;
540
- scopeFiltered = semanticResults.filter((r) => {
541
- const scopes = semScopeMap.get(String(r.id)) ?? [];
542
- return scopes.length === 0 || scopes.some((s) => `${s.kind}:${s.id}` === scopeKey);
543
- });
497
+ const guidance = { applies_when: appliesWhen };
498
+ if (input.kind === 'workflow') {
499
+ const steps = Array.isArray(input.steps) ? input.steps.map(oneLineGuidanceText) : [];
500
+ if (steps.length === 0 || steps.some((step) => step.length === 0)) {
501
+ throw new judgments_js_1.JudgmentError('INVALID_INPUT', 'memory.save kind workflow requires one or more ordered steps');
502
+ }
503
+ guidance.steps = steps;
504
+ if (input.evidenceChecks !== undefined) {
505
+ if (!Array.isArray(input.evidenceChecks)) {
506
+ throw new judgments_js_1.JudgmentError('INVALID_INPUT', 'memory.save evidenceChecks must be a list');
544
507
  }
545
- existingCandidates = scopeFiltered
546
- .filter((r) => {
547
- const status = String(r.status || '');
548
- return !status || status === 'active' || status === '';
549
- })
550
- .filter((c) => !excludeIds.has(String(c.id)))
551
- .map((r) => ({
552
- id: String(r.id),
553
- topic: String(r.topic || ''),
554
- summary: String(r.decision || ''),
555
- kind: 'fact',
556
- _semanticMatch: true,
557
- }));
558
- }
559
- catch {
560
- // Semantic search unavailable — proceed with empty candidates
508
+ const checks = input.evidenceChecks.map(oneLineGuidanceText);
509
+ if (checks.some((check) => check.length === 0)) {
510
+ throw new judgments_js_1.JudgmentError('INVALID_INPUT', 'memory.save evidenceChecks cannot contain an empty line');
511
+ }
512
+ guidance.evidence_checks = checks;
561
513
  }
562
514
  }
563
- const evolution = shouldApplyEvolution
564
- ? (0, evolution_engine_js_1.resolveMemoryEvolution)({
565
- incoming: { topic: input.topic, summary: input.summary, kind: input.kind },
566
- existing: existingCandidates.map((c) => ({
567
- ...c,
568
- kind: (c.kind || 'fact'),
569
- })),
570
- })
571
- : { edges: [] };
572
- const supersedesTarget = evolution.edges.find((edge) => edge.type === 'supersedes')?.to_id ?? null;
573
- const entityObservationIds = Array.from(new Set(input.entityObservationIds ?? []));
574
- const eventDateTime = typeof input.eventDateTime === 'number'
575
- ? input.eventDateTime
576
- : loadEventDateTimeForObservations(adapter, entityObservationIds);
577
- const timelineEvent = buildTimelineEventForSave(adapter, id, input.topic, entityObservationIds, input.timelineEvent);
578
- await (0, db_manager_js_1.insertDecisionWithEmbedding)({
579
- id,
515
+ return { guidance };
516
+ }
517
+ async function saveMemoryInternal(adapter, input, provenance, access, trustedEnvelope, legacy, commandIdOverride) {
518
+ const targetStatus = input.status ?? 'active';
519
+ const requestedScopes = input.scopes ?? [];
520
+ const payload = guidancePayloadFor(input);
521
+ const eventDateTime = typeof input.eventDateTime === 'number' ? input.eventDateTime : null;
522
+ // Fail before any write: identity references and relationship targets are
523
+ // checked against the same admitted scopes the command will carry.
524
+ (0, record_identity_js_1.validateRecordIdentityReferences)(adapter, {
525
+ itemId: input.itemId,
526
+ actors: input.actors,
527
+ scopes: input.scopes,
528
+ });
529
+ const commandId = commandIdOverride ?? `save:${buildDecisionId(input.topic)}`;
530
+ const recordId = (0, index_js_1.judgmentRecordId)(commandId);
531
+ // Relationships are persisted only when the caller names their target ids
532
+ // explicitly. Matching topic text or vector similarity is evidence for
533
+ // retrieval, not identity.
534
+ const explicitRelationships = Array.from(new Map((legacy?.relationships ?? []).flatMap((relationship) => relationship.targetIds.map((targetId) => [
535
+ `${relationship.type}:${targetId}`,
536
+ { type: relationship.type, targetId },
537
+ ]))).values());
538
+ (0, write_adapters_js_2.assertRelationshipTargetsVisible)(adapter, explicitRelationships.map((relationship) => relationship.targetId), access.scopes, trustedEnvelope);
539
+ const { links, replaces, decisionEdges, supersedeTargets } = (0, write_adapters_js_2.relationshipsToCommandFields)(explicitRelationships, { trusted: trustedEnvelope });
540
+ const authoredLinks = [...links, ...(input.links ?? [])];
541
+ const authoredReplacements = [...replaces, ...(input.replaces ?? [])];
542
+ const allSupersedeTargets = [
543
+ ...new Set([
544
+ ...supersedeTargets,
545
+ ...(input.replaces ?? []).map((replacement) => replacement.id),
546
+ ]),
547
+ ];
548
+ const embeddingDecision = {
549
+ id: recordId,
580
550
  topic: input.topic,
581
551
  decision: input.summary,
582
552
  reasoning: input.details,
583
553
  confidence: input.confidence ?? 0.5,
584
- supersedes: supersedesTarget,
585
- created_at: now,
586
- updated_at: now,
587
- trust_context: JSON.stringify({ source: input.source }),
588
554
  event_date: input.eventDate ?? null,
589
555
  event_datetime: eventDateTime,
590
- agent_id: provenance.agent_id,
591
- model_run_id: provenance.model_run_id,
592
- envelope_hash: provenance.envelope_hash,
593
- gateway_call_id: provenance.gateway_call_id,
594
- source_refs_json: JSON.stringify(provenance.source_refs),
595
- provenance_json: JSON.stringify(provenance.provenance),
596
- });
597
- // Pre-resolve scope IDs before the synchronous transaction
598
- const resolvedScopeIds = [];
599
- for (const [index, scope] of input.scopes.entries()) {
600
- const scopeId = await (0, db_manager_js_1.ensureMemoryScope)(scope.kind, scope.id);
601
- resolvedScopeIds.push({ scopeId, isPrimary: index === 0 });
602
- }
603
- // Wrap all post-insert mutations in a transaction for atomicity
604
- try {
605
- adapter.transaction(() => {
606
- adapter
607
- .prepare(`
608
- UPDATE decisions
609
- SET kind = ?, status = ?, summary = ?, is_static = ?, trust_context = ?, updated_at = ?
610
- WHERE id = ?
611
- `)
612
- .run(input.kind, targetStatus, input.summary, input.kind === 'preference' || input.kind === 'constraint' ? 1 : 0, JSON.stringify({ source: input.source }), now, id);
613
- for (const { scopeId, isPrimary } of resolvedScopeIds) {
614
- adapter
615
- .prepare(`
616
- INSERT OR REPLACE INTO memory_scope_bindings (memory_id, scope_id, is_primary)
617
- VALUES (?, ?, ?)
618
- `)
619
- .run(id, scopeId, isPrimary ? 1 : 0);
620
- }
621
- (0, event_store_js_1.insertMemoryEventInTransaction)(adapter, {
622
- event_type: 'save',
623
- actor: provenance.actor,
624
- source_turn_id: provenance.source_turn_id ?? undefined,
625
- memory_id: id,
626
- topic: input.topic,
627
- scope_refs: input.scopes,
628
- evidence_refs: provenance.source_refs,
629
- reason: buildSaveEventReason(provenance.tool_name, provenance.gateway_call_id),
630
- created_at: now,
631
- });
632
- for (const entityObservationId of entityObservationIds) {
633
- adapter
634
- .prepare(`
635
- INSERT OR IGNORE INTO decision_entity_sources (
636
- decision_id, entity_observation_id, relation_type, created_at
637
- ) VALUES (?, ?, ?, ?)
638
- `)
639
- .run(id, entityObservationId, 'support', now);
640
- }
641
- if (timelineEvent) {
642
- insertTimelineEventForSave(adapter, timelineEvent, now);
643
- }
644
- for (const edge of evolution.edges) {
645
- if (edge.type === 'supersedes') {
646
- adapter
647
- .prepare(`UPDATE decisions SET superseded_by = ?, status = 'superseded', updated_at = ? WHERE id = ?`)
648
- .run(id, now, edge.to_id);
556
+ };
557
+ const command = {
558
+ commandId,
559
+ topic: input.topic,
560
+ summary: input.summary,
561
+ reasoning: input.details,
562
+ recordKind: 'judgment',
563
+ confidence: input.confidence ?? 0.5,
564
+ eventDate: input.eventDate ?? null,
565
+ eventDatetime: eventDateTime,
566
+ outcome: legacy?.outcome ?? null,
567
+ failureReason: legacy?.failureReason ?? null,
568
+ limitation: legacy?.limitation ?? null,
569
+ sourceRefs: provenance.source_refs,
570
+ provenance: provenance.provenance,
571
+ ...(payload === undefined ? {} : { payload }),
572
+ agentId: provenance.agent_id,
573
+ modelRunId: provenance.model_run_id,
574
+ envelopeHash: provenance.envelope_hash,
575
+ gatewayCallId: provenance.gateway_call_id,
576
+ scopes: [...requestedScopes],
577
+ links: authoredLinks,
578
+ replaces: authoredReplacements,
579
+ record: {
580
+ kind: input.kind,
581
+ status: targetStatus,
582
+ summary: input.summary,
583
+ isStatic: legacy?.isStatic ?? (input.kind === 'preference' || input.kind === 'constraint' ? 1 : 0),
584
+ userInvolvement: legacy?.userInvolvement ?? null,
585
+ trustContext: JSON.stringify({ source: input.source }),
586
+ },
587
+ event: {
588
+ actor: provenance.actor,
589
+ ...(provenance.source_turn_id ? { sourceTurnId: provenance.source_turn_id } : {}),
590
+ evidenceRefs: provenance.source_refs,
591
+ reason: buildSaveEventReason(provenance.tool_name, provenance.gateway_call_id),
592
+ },
593
+ projections: {
594
+ decisionEdges,
595
+ ...(allSupersedeTargets.length > 0 ? { supersedeTargets: allSupersedeTargets } : {}),
596
+ ...(input.itemId !== undefined || input.actors !== undefined
597
+ ? {
598
+ recordIdentity: { itemId: input.itemId ?? null, actors: input.actors ?? [] },
649
599
  }
650
- adapter
651
- .prepare(`
652
- INSERT OR REPLACE INTO decision_edges (from_id, to_id, relationship, reason, weight, created_at)
653
- VALUES (?, ?, ?, ?, ?, ?)
654
- `)
655
- .run(id, edge.to_id, edge.type, edge.reason ?? null, 1.0, now);
656
- }
600
+ : {}),
601
+ },
602
+ };
603
+ let receipt;
604
+ try {
605
+ receipt = await (0, index_js_1.appendJudgment)(command, access, {
606
+ adapter,
607
+ embedder: (0, write_adapters_js_2.commandEmbedder)(embeddingDecision),
657
608
  });
658
609
  }
659
610
  catch (error) {
660
- cleanupFailedMemorySave(adapter, id);
661
611
  const rollbackError = (error instanceof Error ? error : new Error(String(error)));
662
- rollbackError.memoryId = id;
612
+ rollbackError.memoryId = recordId;
663
613
  throw rollbackError;
664
614
  }
665
- // Sync the adapter's status cache for every row whose status changed in the
666
- // post-insert transaction: the saved row itself (status is written AFTER
667
- // insertEmbedding populated the cache) and any rows just superseded. This makes
668
- // the vectorSearch pre-filter correct within this session, not only after reload.
669
- if (adapter.refreshDecisionStatusCache) {
670
- const changedIds = [
671
- id,
672
- ...evolution.edges.filter((e) => e.type === 'supersedes').map((e) => e.to_id),
673
- ];
674
- const ridStmt = adapter.prepare('SELECT rowid FROM decisions WHERE id = ?');
675
- for (const changedId of changedIds) {
676
- const ridRow = ridStmt.get(changedId);
677
- if (ridRow) {
678
- adapter.refreshDecisionStatusCache(ridRow.rowid);
679
- }
680
- }
681
- }
682
615
  return {
683
616
  success: true,
684
- id,
685
- saved_decision_id: id,
686
- timeline_event_id: timelineEvent?.id ?? null,
687
- timeline_event_ids: timelineEvent ? [timelineEvent.id] : [],
617
+ id: receipt.recordId,
618
+ saved_decision_id: receipt.recordId,
688
619
  };
689
620
  }
690
- async function saveMemory(input) {
691
- return saveMemoryInternal((0, provenance_js_1.sanitizePublicSaveMemoryInput)(input));
621
+ async function saveMemory(adapter, input) {
622
+ const clean = (0, provenance_js_1.sanitizePublicSaveMemoryInput)(input);
623
+ const provenance = (0, provenance_js_1.normalizeMemoryWriteProvenance)();
624
+ const access = (0, write_adapters_js_2.writeAccessForProvenance)(provenance, clean.scopes ?? []);
625
+ return saveMemoryInternal(adapter, clean, provenance, access, false);
626
+ }
627
+ /**
628
+ * The unified `memory.save` action path. Access IS the call authority — the
629
+ * command's scopes are checked against it inside `appendJudgment`, and the
630
+ * record's provenance is composed from it plus the session facts the host
631
+ * attests (never from caller input). `commandId` is the caller's operationId,
632
+ * so a retried call replays the original receipt.
633
+ */
634
+ async function saveJudgmentRecord(adapter, input, access, commandId, session) {
635
+ // An omitted scope request writes under the full admitted scopes — the same
636
+ // default the gateway's trusted path applied via the context packet.
637
+ const clean = (0, provenance_js_1.sanitizePublicSaveMemoryInput)({
638
+ ...input,
639
+ scopes: input.scopes ?? access.scopes,
640
+ });
641
+ const provenance = (0, provenance_js_1.normalizeMemoryWriteProvenance)({
642
+ actor: session?.actor ?? 'main_agent',
643
+ agent_id: access.agentId,
644
+ model_run_id: session?.modelRunId,
645
+ envelope_hash: session?.envelopeHash,
646
+ tool_name: session?.toolName,
647
+ gateway_call_id: session?.gatewayCallId,
648
+ context_packet_id: session?.contextPacketId,
649
+ source_turn_id: session?.sourceTurnId,
650
+ source_message_ref: session?.sourceMessageRef,
651
+ source_refs: session?.sourceRefs ? [...session.sourceRefs] : undefined,
652
+ });
653
+ return saveMemoryInternal(adapter, clean, provenance, access, true, undefined, commandId);
654
+ }
655
+ /**
656
+ * The read-side scope rule for memory actions — the same bound `appendJudgment`
657
+ * applies to writes. An omitted request reads under the full admitted scopes;
658
+ * an explicit request must be a subset of them. An empty admitted set admits an
659
+ * empty corpus: a caller with no scopes can see no scoped record (reads never
660
+ * fall back to an unbounded scan — the legacy `scopes: []` = read-all behavior
661
+ * is exactly what this bound removes).
662
+ */
663
+ function boundReadScopesFor(access, requested) {
664
+ const readable = [...access.scopes, ...(access.readScopes ?? [])];
665
+ const scopeKey = (scope) => JSON.stringify([scope.kind, scope.id]);
666
+ const admitted = new Map(readable.map((scope) => [scopeKey(scope), scope]));
667
+ const effective = requested ?? [...admitted.values()];
668
+ const seen = new Set();
669
+ return effective.map((scope) => {
670
+ if (typeof scope !== 'object' ||
671
+ scope === null ||
672
+ typeof scope.kind !== 'string' ||
673
+ scope.kind.trim().length === 0 ||
674
+ typeof scope.id !== 'string' ||
675
+ scope.id.trim().length === 0) {
676
+ throw new judgments_js_1.JudgmentError('INVALID_SCOPE', 'scope kind is invalid');
677
+ }
678
+ const key = scopeKey(scope);
679
+ if (seen.has(key)) {
680
+ throw new judgments_js_1.JudgmentError('INVALID_SCOPE', 'Read scopes must be unique');
681
+ }
682
+ seen.add(key);
683
+ if (!admitted.has(key)) {
684
+ throw new judgments_js_1.JudgmentError('SCOPE_DENIED', 'Read scope is outside the admitted access');
685
+ }
686
+ return scope;
687
+ });
692
688
  }
693
- async function saveMemoryWithTrustedProvenance(input, options) {
694
- return saveMemoryInternal((0, provenance_js_1.sanitizePublicSaveMemoryInput)(input), options);
689
+ async function saveLegacyMemory(adapter, input, legacy, access) {
690
+ const clean = (0, provenance_js_1.sanitizePublicSaveMemoryInput)(input);
691
+ const provenance = (0, provenance_js_1.normalizeMemoryWriteProvenance)();
692
+ const effectiveAccess = access ?? (0, write_adapters_js_2.writeAccessForProvenance)(provenance, clean.scopes ?? []);
693
+ return saveMemoryInternal(adapter, clean, provenance, effectiveAccess, access !== undefined, legacy);
695
694
  }
696
- async function promoteMemoryStatus(input) {
697
- await (0, db_manager_js_1.initDB)();
698
- const adapter = (0, db_manager_js_1.getAdapter)();
695
+ async function promoteMemoryStatus(adapter, input) {
699
696
  const memoryId = input.memoryId;
700
697
  const now = input.nowMs ?? Date.now();
701
698
  const targetStatus = input.status;
@@ -718,7 +715,7 @@ async function promoteMemoryStatus(input) {
718
715
  const primaryScope = scopes[0] ?? null;
719
716
  let existingCandidates;
720
717
  if (primaryScope) {
721
- const scopeId = await (0, db_manager_js_1.ensureMemoryScope)(primaryScope.kind, primaryScope.id);
718
+ const scopeId = (0, db_manager_js_1.ensureMemoryScope)(adapter, primaryScope.kind, primaryScope.id);
722
719
  existingCandidates = adapter
723
720
  .prepare(`
724
721
  SELECT d.id, d.topic, d.summary, d.kind
@@ -748,10 +745,10 @@ async function promoteMemoryStatus(input) {
748
745
  if (existingCandidates.length === 0) {
749
746
  try {
750
747
  const queryText = `${topic} ${summary}`;
751
- const embedding = await (0, embeddings_js_1.generateEmbedding)(queryText, 'query');
748
+ const embedding = await (0, embedder_js_1.generateEmbedding)(queryText, 'query');
752
749
  // Same exclusion as saveMemoryInternal's fallback: superseded history must
753
750
  // not crowd out the prior ACTIVE decision from the 3 candidate slots.
754
- const semanticResults = await (0, db_manager_js_1.vectorSearch)(embedding, 3, 0.82, undefined, Array.from(EXCLUDED_STATUSES));
751
+ const semanticResults = await (0, search_js_1.vectorSearch)(adapter, embedding, 3, 0.82, undefined, Array.from(EXCLUDED_STATUSES));
755
752
  let scopeFiltered = semanticResults;
756
753
  if (primaryScope) {
757
754
  const semIds = semanticResults.map((result) => String(result.id));
@@ -792,46 +789,104 @@ async function promoteMemoryStatus(input) {
792
789
  const existingSupersedesTarget = typeof row.supersedes === 'string' && row.supersedes.length > 0 ? row.supersedes : null;
793
790
  const supersedesTarget = evolution.edges.find((edge) => edge.type === 'supersedes')?.to_id ??
794
791
  (targetStatus === 'active' ? existingSupersedesTarget : null);
795
- adapter.transaction(() => {
796
- const updated = adapter
797
- .prepare('UPDATE decisions SET status = ?, supersedes = ?, updated_at = ? WHERE id = ?')
798
- .run(targetStatus, supersedesTarget, now, memoryId);
799
- if (updated.changes !== 1) {
800
- throw new Error(`Cannot promote missing memory ${memoryId}`);
801
- }
802
- for (const edge of evolution.edges) {
803
- if (edge.type === 'supersedes') {
804
- adapter
805
- .prepare(`UPDATE decisions SET superseded_by = ?, status = 'superseded', updated_at = ? WHERE id = ?`)
806
- .run(memoryId, now, edge.to_id);
807
- }
808
- adapter
809
- .prepare(`
810
- INSERT OR REPLACE INTO decision_edges (from_id, to_id, relationship, reason, weight, created_at)
811
- VALUES (?, ?, ?, ?, ?, ?)
812
- `)
813
- .run(memoryId, edge.to_id, edge.type, edge.reason ?? null, 1.0, now);
814
- }
815
- });
816
- // Sync the adapter's status cache for every row whose status changed above.
817
- // Without this, a row promoted OUT of an excluded status (e.g. stale->active)
818
- // stays invisible to the vectorSearch pre-filter until the next full reload.
819
- if (adapter.refreshDecisionStatusCache) {
820
- const changedIds = [
821
- memoryId,
822
- ...evolution.edges.filter((e) => e.type === 'supersedes').map((e) => e.to_id),
823
- ];
824
- const ridStmt = adapter.prepare('SELECT rowid FROM decisions WHERE id = ?');
825
- for (const id of changedIds) {
826
- const ridRow = ridStmt.get(id);
827
- if (ridRow) {
828
- adapter.refreshDecisionStatusCache(ridRow.rowid);
829
- }
830
- }
792
+ // The status change is an authored amendment: one append-only judgment record
793
+ // carries it, and the target row's projection columns move in the same
794
+ // transaction. A replayed command (same id, same payload) returns the stored
795
+ // receipt instead of rewriting.
796
+ const commandId = `promote:${memoryId}:${node_crypto_1.default
797
+ .createHash('sha256')
798
+ .update((0, canonicalize_js_1.canonicalizeJSON)({
799
+ status: targetStatus,
800
+ supersedes: supersedesTarget,
801
+ edges: evolution.edges.map((edge) => `${edge.type}:${edge.to_id}`),
802
+ now,
803
+ }))
804
+ .digest('hex')
805
+ .slice(0, 16)}`;
806
+ const supersedesEdges = evolution.edges.filter((edge) => edge.type === 'supersedes');
807
+ const command = {
808
+ commandId,
809
+ // Audit records link to the amended memory instead of sharing its topic,
810
+ // keeping them out of the topic's evolution candidate pool.
811
+ topic: `judgment/${memoryId}`,
812
+ summary: `Status '${targetStatus}' applied to ${memoryId}`,
813
+ recordKind: 'judgment',
814
+ payload: {
815
+ amended: memoryId,
816
+ status: targetStatus,
817
+ supersedes: supersedesTarget,
818
+ edges: evolution.edges.map((edge) => ({ type: edge.type, to_id: edge.to_id })),
819
+ },
820
+ scopes,
821
+ agentId: null,
822
+ links: [{ relation: 'amends', target: { kind: 'memory', id: memoryId } }],
823
+ amends: [
824
+ // supersedes is included only when a target resolved: applyAmendment
825
+ // writes a column for every key present, so a null here would clear the
826
+ // target's predecessor pointer on non-active promotions.
827
+ {
828
+ target: { kind: 'memory', id: memoryId },
829
+ status: targetStatus,
830
+ ...(supersedesTarget !== null ? { supersedes: supersedesTarget } : {}),
831
+ },
832
+ ...supersedesEdges.map((edge) => ({
833
+ target: { kind: 'memory', id: edge.to_id },
834
+ supersededBy: memoryId,
835
+ status: 'superseded',
836
+ })),
837
+ ],
838
+ projections: {
839
+ decisionEdges: evolution.edges.map((edge) => ({
840
+ fromId: memoryId,
841
+ targetId: edge.to_id,
842
+ relationship: edge.type,
843
+ reason: edge.reason ?? null,
844
+ weight: 1,
845
+ })),
846
+ },
847
+ record: {
848
+ kind: 'fact',
849
+ status: 'active',
850
+ summary: `Status '${targetStatus}' applied to ${memoryId}`,
851
+ },
852
+ recordedAt: now,
853
+ event: { reason: `promote ${memoryId} to '${targetStatus}'` },
854
+ };
855
+ await (0, index_js_1.appendJudgment)(command, (0, write_adapters_js_2.unsignedWriteAccess)(scopes), { adapter });
856
+ }
857
+ /** Append an access-checked status amendment for one stored memory record. */
858
+ async function retireMemoryRecord(adapter, input, access, commandId) {
859
+ const id = input.memoryId.trim();
860
+ const reason = input.reason.trim();
861
+ if (!id)
862
+ throw new judgments_js_1.JudgmentError('INVALID_INPUT', 'memory.retire requires memory_id');
863
+ if (!reason)
864
+ throw new judgments_js_1.JudgmentError('INVALID_INPUT', 'memory.retire requires a reason');
865
+ if (input.status !== 'stale' && input.status !== 'superseded') {
866
+ throw new judgments_js_1.JudgmentError('INVALID_INPUT', 'memory.retire status must be stale or superseded');
831
867
  }
868
+ const record = await readMemoryRecordById(adapter, id, access.scopes);
869
+ if (!record) {
870
+ throw new judgments_js_1.JudgmentError('REFERENCE_NOT_FOUND', 'Memory record is unavailable in the admitted scopes');
871
+ }
872
+ const admittedScopes = record.scopes.filter((recordScope) => access.scopes.some((scope) => scope.kind === recordScope.kind && scope.id === recordScope.id));
873
+ const summary = `Status '${input.status}' applied to ${id}: ${reason}`;
874
+ const receipt = await (0, index_js_1.appendJudgment)({
875
+ commandId,
876
+ topic: `judgment/${id}`,
877
+ summary,
878
+ recordKind: 'judgment',
879
+ payload: { amended: id, status: input.status, reason },
880
+ scopes: admittedScopes,
881
+ links: [{ relation: 'amends', target: { kind: 'memory', id } }],
882
+ amends: [{ target: { kind: 'memory', id }, status: input.status }],
883
+ record: { kind: 'fact', status: 'active', summary },
884
+ event: { reason: `Retired ${id} as ${input.status}: ${reason}` },
885
+ }, access, { adapter });
886
+ return { success: true, id, status: input.status, reason, receiptId: receipt.recordId };
832
887
  }
833
- async function buildProfile(scopes) {
834
- const records = await loadScopedMemories(scopes);
888
+ async function buildProfile(adapter, scopes) {
889
+ const records = await loadScopedMemories(adapter, scopes);
835
890
  return (0, profile_builder_js_1.classifyProfileEntries)(records);
836
891
  }
837
892
  const EXCLUDED_STATUSES = new Set([
@@ -840,14 +895,17 @@ const EXCLUDED_STATUSES = new Set([
840
895
  'contradicted',
841
896
  'stale',
842
897
  ]);
843
- async function recallMemory(query, options = {}) {
898
+ async function recallMemory(adapter, query, options = {}) {
844
899
  const bundle = (0, types_js_1.createEmptyRecallBundle)(query);
845
900
  const searchOptions = (0, search_quality_js_1.normalizeSearchQualityOptions)(options);
901
+ const matchesKind = (kind) => options.kind === undefined ||
902
+ (Array.isArray(options.kind)
903
+ ? options.kind.some((value) => value === kind)
904
+ : kind === options.kind);
846
905
  const diagnostics = {
847
906
  candidate_counts: {
848
907
  vector: 0,
849
908
  lexical: 0,
850
- entity: 0,
851
909
  graph_expanded: 0,
852
910
  vector_only: 0,
853
911
  rejected_by_strictness: 0,
@@ -858,14 +916,12 @@ async function recallMemory(query, options = {}) {
858
916
  let matched = [];
859
917
  let fusedHits = [];
860
918
  let retrievalSource = 'none';
861
- const projectionMode = process.env.MAMA_ENTITY_PROJECTION_MODE ?? 'shadow';
862
919
  const vectorSimilarityById = new Map();
863
920
  const lexicalScoreById = new Map();
864
- const entitySupportIds = new Set();
865
921
  let _lexicalRecords = null;
866
922
  const loadLexical = async () => {
867
923
  if (_lexicalRecords === null) {
868
- _lexicalRecords = await loadScopedMemories(options.scopes ?? []);
924
+ _lexicalRecords = await loadScopedMemories(adapter, options.scopes ?? []);
869
925
  }
870
926
  return _lexicalRecords;
871
927
  };
@@ -888,25 +944,27 @@ async function recallMemory(query, options = {}) {
888
944
  }
889
945
  }
890
946
  // Hybrid search: vector + BM25/lexical in parallel, fused with RRF
891
- await (0, db_manager_js_1.initDB)();
947
+ // One adapter for the whole retrieval: generateEmbedding yields, and a reset
948
+ // between two adapter lookups would fuse candidates from two databases.
949
+ const searchAdapter = adapter;
892
950
  // Channel 1: Vector search (semantic similarity) — run all sub-queries
893
951
  const vectorMatched = [];
894
952
  let primaryQueryEmbedding = null;
895
953
  try {
896
954
  for (const sq of subQueries) {
897
- const queryEmbedding = await (0, embeddings_js_1.generateEmbedding)(sq, 'query');
955
+ const queryEmbedding = await (0, embedder_js_1.generateEmbedding)(sq, 'query');
898
956
  if (sq === query && primaryQueryEmbedding === null) {
899
957
  primaryQueryEmbedding = queryEmbedding;
900
958
  }
901
- const vectorResults = await (0, db_manager_js_1.vectorSearch)(queryEmbedding, vectorLimit, searchOptions.threshold, searchOptions.topicPrefix,
959
+ const vectorResults = await (0, search_js_1.vectorSearch)(searchAdapter, queryEmbedding, vectorLimit, searchOptions.threshold, searchOptions.topicPrefix,
902
960
  // Keep superseded history out of the candidate top-K at search time; the
903
961
  // post-filter below stays the authority (and includeHistory restores it).
904
- options.includeHistory ? undefined : Array.from(EXCLUDED_STATUSES));
962
+ options.includeHistory ? undefined : Array.from(EXCLUDED_STATUSES), options.kind);
905
963
  let filtered = vectorResults;
906
964
  let vectorScopeMap = new Map();
907
965
  if (options.scopes && options.scopes.length > 0) {
908
966
  const vectorIds = vectorResults.map((r) => String(r.id));
909
- vectorScopeMap = batchLoadScopes((0, db_manager_js_1.getAdapter)(), vectorIds);
967
+ vectorScopeMap = batchLoadScopes(adapter, vectorIds);
910
968
  const requestedScopes = new Set(options.scopes.map((s) => `${s.kind}:${s.id}`));
911
969
  filtered = vectorResults.filter((r) => {
912
970
  const scopes = vectorScopeMap.get(String(r.id)) ?? [];
@@ -924,7 +982,7 @@ async function recallMemory(query, options = {}) {
924
982
  vectorMatched.push({
925
983
  id: String(result.id),
926
984
  topic: String(result.topic || ''),
927
- kind: 'decision',
985
+ kind: (result.kind ?? 'decision'),
928
986
  summary: String(result.decision || ''),
929
987
  details: String(result.reasoning || ''),
930
988
  confidence: result.similarity ?? 0.5,
@@ -935,6 +993,7 @@ async function recallMemory(query, options = {}) {
935
993
  updated_at: result.created_at ?? Date.now(),
936
994
  event_date: result.event_date ?? null,
937
995
  event_datetime: result.event_datetime ?? null,
996
+ outcome: result.outcome ?? null,
938
997
  });
939
998
  diagnostics.candidate_counts.vector += 1;
940
999
  if (typeof result.similarity === 'number' && Number.isFinite(result.similarity)) {
@@ -944,7 +1003,7 @@ async function recallMemory(query, options = {}) {
944
1003
  } // end sub-query loop
945
1004
  }
946
1005
  catch (vectorErr) {
947
- (0, debug_logger_js_1.warn)(`[recallMemory] Vector search failed: ${vectorErr instanceof Error ? vectorErr.message : String(vectorErr)}`);
1006
+ (0, debug_logger_js_2.warn)(`[recallMemory] Vector search failed: ${vectorErr instanceof Error ? vectorErr.message : String(vectorErr)}`);
948
1007
  }
949
1008
  // Deduplicate vector results from multiple sub-queries
950
1009
  const seenIds = new Set();
@@ -1013,9 +1072,9 @@ async function recallMemory(query, options = {}) {
1013
1072
  .map((t) => stemToken(t))
1014
1073
  .filter((t) => !FTS5_NOISE_WORDS.has(t));
1015
1074
  const ftsQuery = ftsTokens.length > 0 ? ftsTokens.join(' OR ') : query;
1016
- const ftsResults = await (0, db_manager_js_1.fts5Search)(ftsQuery, lexicalLimit);
1075
+ const ftsResults = await (0, search_js_1.fts5Search)(searchAdapter, ftsQuery, lexicalLimit, options.kind);
1017
1076
  if (ftsResults.length > 0) {
1018
- const adapter = (0, db_manager_js_1.getAdapter)();
1077
+ const adapter = searchAdapter;
1019
1078
  const fallbackSource = {
1020
1079
  package: 'mama-core',
1021
1080
  source_type: 'fts5',
@@ -1025,7 +1084,7 @@ async function recallMemory(query, options = {}) {
1025
1084
  for (const ftsRow of ftsResults) {
1026
1085
  const row = adapter
1027
1086
  .prepare(`SELECT id, topic, decision, reasoning, confidence, created_at, updated_at,
1028
- trust_context, kind, status, summary, event_date, event_datetime
1087
+ trust_context, kind, status, summary, event_date, event_datetime, outcome
1029
1088
  FROM decisions WHERE id = ?`)
1030
1089
  .get(ftsRow.id);
1031
1090
  if (!row)
@@ -1042,6 +1101,8 @@ async function recallMemory(query, options = {}) {
1042
1101
  // Topic prefix filtering (matches vectorSearch behavior)
1043
1102
  if (searchOptions.topicPrefix && !record.topic.startsWith(searchOptions.topicPrefix))
1044
1103
  continue;
1104
+ if (!matchesKind(record.kind))
1105
+ continue;
1045
1106
  // Scope filtering
1046
1107
  if (options.scopes && options.scopes.length > 0) {
1047
1108
  const requestedScopes = new Set(options.scopes.map((s) => `${s.kind}:${s.id}`));
@@ -1070,14 +1131,14 @@ async function recallMemory(query, options = {}) {
1070
1131
  for (const candidate of lexicalCandidates) {
1071
1132
  candidate.score += topicAffinityBoost(candidate.memory.topic, boostTokens, boostQuery);
1072
1133
  }
1073
- lexicalCandidates.sort((left, right) => right.score - left.score);
1134
+ lexicalCandidates.sort(compareLexicalCandidates);
1074
1135
  }
1075
1136
  // Fallback: in-memory lexical if FTS5 returned nothing
1076
1137
  if (lexicalCandidates.length === 0) {
1077
1138
  let lexicalRecords = await loadLexical();
1078
1139
  if (options.scopes && options.scopes.length > 0) {
1079
1140
  const lexicalIds = lexicalRecords.map((r) => r.id);
1080
- const scopeMap = batchLoadScopes((0, db_manager_js_1.getAdapter)(), lexicalIds);
1141
+ const scopeMap = batchLoadScopes(adapter, lexicalIds);
1081
1142
  const requestedScopes = new Set(options.scopes.map((s) => `${s.kind}:${s.id}`));
1082
1143
  lexicalRecords = lexicalRecords.filter((r) => {
1083
1144
  const scopes = scopeMap.get(r.id) ?? [];
@@ -1090,6 +1151,9 @@ async function recallMemory(query, options = {}) {
1090
1151
  if (searchOptions.topicPrefix) {
1091
1152
  lexicalRecords = lexicalRecords.filter((r) => r.topic.startsWith(searchOptions.topicPrefix));
1092
1153
  }
1154
+ if (options.kind !== undefined) {
1155
+ lexicalRecords = lexicalRecords.filter((r) => matchesKind(r.kind));
1156
+ }
1093
1157
  lexicalCandidates = buildLexicalCandidates(lexicalRecords, query);
1094
1158
  if (subQueries.length > 1) {
1095
1159
  for (const sq of subQueries.slice(1)) {
@@ -1100,7 +1164,7 @@ async function recallMemory(query, options = {}) {
1100
1164
  }
1101
1165
  }
1102
1166
  }
1103
- lexicalCandidates.sort((a, b) => b.score - a.score);
1167
+ lexicalCandidates.sort(compareLexicalCandidates);
1104
1168
  }
1105
1169
  }
1106
1170
  } // end needsLexical
@@ -1149,56 +1213,6 @@ async function recallMemory(query, options = {}) {
1149
1213
  record: entry.record,
1150
1214
  fused_rank_score: entry.score,
1151
1215
  }));
1152
- let hasWikiHits = false;
1153
- const wikiScores = new Map();
1154
- const wikiVectorRankMap = new Map();
1155
- const wikiVectorScoreById = new Map();
1156
- try {
1157
- const adapter = (0, db_manager_js_1.getAdapter)();
1158
- const wikiFtsHits = (0, wiki_page_index_js_1.ftsSearchWikiPages)(adapter, query, requestedLimit * 2);
1159
- let wikiVectorHits = [];
1160
- if (primaryQueryEmbedding) {
1161
- try {
1162
- wikiVectorHits = (0, wiki_page_index_js_1.vectorSearchWikiPages)(adapter, primaryQueryEmbedding, requestedLimit * 2);
1163
- }
1164
- catch (wikiVectorErr) {
1165
- (0, debug_logger_js_1.warn)(`[recallMemory] Wiki vector search failed: ${wikiVectorErr instanceof Error ? wikiVectorErr.message : String(wikiVectorErr)}`);
1166
- }
1167
- }
1168
- for (let i = 0; i < wikiVectorHits.length; i++) {
1169
- wikiVectorRankMap.set(wikiVectorHits[i].record.id, i);
1170
- wikiVectorScoreById.set(wikiVectorHits[i].record.id, wikiVectorHits[i].raw_score);
1171
- }
1172
- diagnostics.candidate_counts.lexical += wikiFtsHits.length;
1173
- diagnostics.candidate_counts.vector += wikiVectorHits.length;
1174
- for (let i = 0; i < wikiFtsHits.length; i++) {
1175
- const hit = wikiFtsHits[i];
1176
- const lexScore = 1 / (RRF_K + i + 1);
1177
- const vecRank = wikiVectorRankMap.get(hit.record.id);
1178
- const vecBoost = vecRank !== undefined ? 0.2 * (1 / (RRF_K + vecRank + 1)) : 0;
1179
- wikiScores.set(hit.record.id, {
1180
- record: hit.record,
1181
- score: lexScore + vecBoost,
1182
- lexicalSupport: true,
1183
- vectorSimilarity: wikiVectorScoreById.get(hit.record.id) ?? null,
1184
- });
1185
- }
1186
- for (let i = 0; i < wikiVectorHits.length; i++) {
1187
- const hit = wikiVectorHits[i];
1188
- if (wikiScores.has(hit.record.id)) {
1189
- continue;
1190
- }
1191
- wikiScores.set(hit.record.id, {
1192
- record: hit.record,
1193
- score: 0.15 * (1 / (RRF_K + i + 1)),
1194
- lexicalSupport: false,
1195
- vectorSimilarity: hit.raw_score,
1196
- });
1197
- }
1198
- }
1199
- catch (wikiFtsErr) {
1200
- (0, debug_logger_js_1.warn)(`[recallMemory] Wiki FTS search failed: ${wikiFtsErr instanceof Error ? wikiFtsErr.message : String(wikiFtsErr)}`);
1201
- }
1202
1216
  fusedHits = decisionFusedHits;
1203
1217
  // Normalize RRF scores to 0-1 range so downstream consumers (threshold filters,
1204
1218
  // similarity displays) get meaningful values. The raw RRF score for K=60 tops out
@@ -1217,34 +1231,6 @@ async function recallMemory(query, options = {}) {
1217
1231
  else if (lexicalCandidates.length > 0) {
1218
1232
  retrievalSource = 'lexical_search';
1219
1233
  }
1220
- let canonicalMatched = [];
1221
- if (projectionMode !== 'off') {
1222
- try {
1223
- canonicalMatched = await (0, recall_bridge_js_1.queryCanonicalEntities)(query, options.scopes ?? [], { limit: 10 });
1224
- diagnostics.candidate_counts.entity = canonicalMatched.length;
1225
- for (const canonical of canonicalMatched) {
1226
- entitySupportIds.add(canonical.id);
1227
- }
1228
- if (projectionMode === 'dual-write' && canonicalMatched.length > 0) {
1229
- const seenIds = new Set(matched.map((item) => item.id));
1230
- for (const canonical of canonicalMatched) {
1231
- if (!seenIds.has(canonical.id)) {
1232
- matched.push(canonical);
1233
- seenIds.add(canonical.id);
1234
- }
1235
- }
1236
- retrievalSource =
1237
- retrievalSource === 'none' ? 'entity_canonical' : `${retrievalSource}+entity_canonical`;
1238
- }
1239
- else if (projectionMode === 'shadow' && canonicalMatched.length > 0) {
1240
- retrievalSource =
1241
- retrievalSource === 'none' ? 'shadow_entity_probe' : `${retrievalSource}+shadow_probe`;
1242
- }
1243
- }
1244
- catch (canonicalErr) {
1245
- (0, debug_logger_js_1.warn)(`[recallMemory] Canonical entity recall failed: ${canonicalErr instanceof Error ? canonicalErr.message : String(canonicalErr)}`);
1246
- }
1247
- }
1248
1234
  const requestedScopeKeys = new Set((options.scopes ?? []).map((scope) => `${scope.kind}:${scope.id}`));
1249
1235
  const hasRequestedScopeSupport = (record) => {
1250
1236
  if (requestedScopeKeys.size === 0) {
@@ -1276,160 +1262,19 @@ async function recallMemory(query, options = {}) {
1276
1262
  // (when itself a single token) appears as a complete word in the query.
1277
1263
  return topicTokens.includes(normalizedQuery) || queryTokens.includes(normalizedTopic);
1278
1264
  };
1279
- const hasExactWikiSupport = (record) => {
1280
- const normalizedQuery = query.trim().toLowerCase();
1281
- if (!normalizedQuery || normalizedQuery.length < EXACT_TOPIC_MIN_QUERY_LENGTH) {
1282
- return false;
1283
- }
1284
- const normalizedTitle = record.title.trim().toLowerCase();
1285
- if (!normalizedTitle) {
1286
- return false;
1287
- }
1288
- if (normalizedQuery === normalizedTitle) {
1289
- return true;
1290
- }
1291
- const queryTokens = tokenizeForExactTopic(normalizedQuery);
1292
- const titleTokens = tokenizeForExactTopic(normalizedTitle);
1293
- // Wiki exact_topic uses TITLE only — page body matches are too lenient
1294
- // for an "exact" signal and remain available through other diagnostics.
1295
- return titleTokens.includes(normalizedQuery) || queryTokens.includes(normalizedTitle);
1296
- };
1297
- const wikiDecisionIdCache = new Map();
1298
- const normalizeWikiDecisionSourceId = (sourceId) => {
1299
- const trimmed = sourceId.trim();
1300
- if (!trimmed) {
1301
- return null;
1302
- }
1303
- if (trimmed.startsWith('decision://')) {
1304
- const id = trimmed.slice('decision://'.length).trim();
1305
- return id || null;
1306
- }
1307
- if (trimmed.startsWith('decision:')) {
1308
- const id = trimmed.slice('decision:'.length).trim();
1309
- return id || null;
1310
- }
1311
- if (/^[A-Za-z][A-Za-z0-9_-]*:/.test(trimmed)) {
1312
- return null;
1313
- }
1314
- return trimmed;
1315
- };
1316
- const loadWikiDecisionIds = (record) => {
1317
- const cached = wikiDecisionIdCache.get(record.id);
1318
- if (cached) {
1319
- return cached;
1320
- }
1321
- const ids = new Set();
1322
- for (const sourceId of record.source_ids) {
1323
- const decisionId = normalizeWikiDecisionSourceId(sourceId);
1324
- if (decisionId) {
1325
- ids.add(decisionId);
1326
- }
1327
- }
1328
- if (record.case_id) {
1329
- const rows = (0, db_manager_js_1.getAdapter)()
1330
- .prepare(`
1331
- SELECT source_id
1332
- FROM case_memberships
1333
- WHERE case_id = ?
1334
- AND source_type = 'decision'
1335
- AND status = 'active'
1336
- `)
1337
- .all(record.case_id);
1338
- for (const row of rows) {
1339
- if (row.source_id) {
1340
- ids.add(row.source_id);
1341
- }
1342
- }
1343
- }
1344
- const decisionIds = Array.from(ids);
1345
- wikiDecisionIdCache.set(record.id, decisionIds);
1346
- return decisionIds;
1347
- };
1348
- const wikiScopeSupportCache = new Map();
1349
- const hasRequestedWikiScopeSupport = (record) => {
1350
- if (requestedScopeKeys.size === 0) {
1351
- return true;
1352
- }
1353
- const cached = wikiScopeSupportCache.get(record.id);
1354
- if (cached !== undefined) {
1355
- return cached;
1356
- }
1357
- const decisionIds = loadWikiDecisionIds(record);
1358
- if (decisionIds.length === 0) {
1359
- wikiScopeSupportCache.set(record.id, false);
1360
- return false;
1361
- }
1362
- const scopeMap = batchLoadScopes((0, db_manager_js_1.getAdapter)(), decisionIds);
1363
- const supported = decisionIds.some((id) => (scopeMap.get(id) ?? []).some((scope) => requestedScopeKeys.has(`${scope.kind}:${scope.id}`)));
1364
- wikiScopeSupportCache.set(record.id, supported);
1365
- return supported;
1366
- };
1367
1265
  // Combined check used for the wiki *filter* path: a wiki entry must have at
1368
1266
  // least one linked decision that matches BOTH the requested scope AND the
1369
1267
  // requested topicPrefix. Without this, the page passes when scope matches
1370
1268
  // decision A and topic matches decision B — i.e., neither single decision
1371
1269
  // satisfies the caller's filter. Diagnostics reporting (which only signals
1372
1270
  // scope confirmation) keeps using hasRequestedWikiScopeSupport.
1373
- const wikiCombinedSupportCache = new Map();
1374
- const hasWikiDecisionWithScopeAndTopic = (record) => {
1375
- const requiresScope = requestedScopeKeys.size > 0;
1376
- const requiresTopic = !!searchOptions.topicPrefix;
1377
- if (!requiresScope && !requiresTopic) {
1378
- return true;
1379
- }
1380
- const cached = wikiCombinedSupportCache.get(record.id);
1381
- if (cached !== undefined) {
1382
- return cached;
1383
- }
1384
- const decisionIds = loadWikiDecisionIds(record);
1385
- if (decisionIds.length === 0) {
1386
- wikiCombinedSupportCache.set(record.id, false);
1387
- return false;
1388
- }
1389
- const adapter = (0, db_manager_js_1.getAdapter)();
1390
- const scopeMap = requiresScope ? batchLoadScopes(adapter, decisionIds) : null;
1391
- const topicByDecisionId = new Map();
1392
- if (requiresTopic) {
1393
- const IN_CHUNK_SIZE = 900;
1394
- for (let i = 0; i < decisionIds.length; i += IN_CHUNK_SIZE) {
1395
- const chunk = decisionIds.slice(i, i + IN_CHUNK_SIZE);
1396
- const placeholders = chunk.map(() => '?').join(', ');
1397
- const rows = adapter
1398
- .prepare(`SELECT id, topic FROM decisions WHERE id IN (${placeholders})`)
1399
- .all(...chunk);
1400
- for (const row of rows) {
1401
- topicByDecisionId.set(row.id, String(row.topic ?? ''));
1402
- }
1403
- }
1404
- }
1405
- const supported = decisionIds.some((id) => {
1406
- if (requiresScope) {
1407
- const scopes = scopeMap?.get(id) ?? [];
1408
- const scopeOk = scopes.some((scope) => requestedScopeKeys.has(`${scope.kind}:${scope.id}`));
1409
- if (!scopeOk) {
1410
- return false;
1411
- }
1412
- }
1413
- if (requiresTopic) {
1414
- const topic = topicByDecisionId.get(id) ?? '';
1415
- if (!topic.startsWith(searchOptions.topicPrefix)) {
1416
- return false;
1417
- }
1418
- }
1419
- return true;
1420
- });
1421
- wikiCombinedSupportCache.set(record.id, supported);
1422
- return supported;
1423
- };
1424
1271
  const buildDiagnostics = (record, graphSource) => {
1425
1272
  const vectorSimilarity = vectorSimilarityById.get(record.id) ?? null;
1426
1273
  const lexicalSupport = lexicalScoreById.has(record.id);
1427
- const entitySupport = entitySupportIds.has(record.id);
1428
1274
  const exactTopicSupport = hasExactTopicSupport(record);
1429
1275
  const scopeSupport = hasRequestedScopeSupport(record);
1430
1276
  const confirmationSignals = [
1431
1277
  lexicalSupport ? 'lexical' : null,
1432
- entitySupport ? 'entity' : null,
1433
1278
  exactTopicSupport ? 'exact_topic' : null,
1434
1279
  ].filter((signal) => signal !== null);
1435
1280
  const metadataSignals = [
@@ -1443,14 +1288,11 @@ async function recallMemory(query, options = {}) {
1443
1288
  ? 'vector_search'
1444
1289
  : lexicalSupport
1445
1290
  ? 'lexical_search'
1446
- : entitySupport
1447
- ? 'entity_canonical'
1448
- : String(record.source.source_type || 'unknown');
1291
+ : String(record.source.source_type || 'unknown');
1449
1292
  return {
1450
1293
  retrieval_source: retrievalSourceForRecord,
1451
1294
  vector_similarity: vectorSimilarity,
1452
1295
  lexical_support: lexicalSupport,
1453
- entity_support: entitySupport,
1454
1296
  scope_support: scopeSupport,
1455
1297
  graph_source: graphSource,
1456
1298
  is_vector_only: vectorSimilarity !== null && confirmationSignals.length === 0,
@@ -1459,41 +1301,6 @@ async function recallMemory(query, options = {}) {
1459
1301
  candidate_threshold_used: searchOptions.threshold,
1460
1302
  };
1461
1303
  };
1462
- const buildWikiDiagnostics = (entry) => {
1463
- const lexicalSupport = entry.lexicalSupport;
1464
- const exactTopicSupport = hasExactWikiSupport(entry.record);
1465
- const scopeSupport = hasRequestedWikiScopeSupport(entry.record);
1466
- const confirmationSignals = [
1467
- lexicalSupport ? 'lexical' : null,
1468
- exactTopicSupport ? 'exact_topic' : null,
1469
- ].filter((signal) => signal !== null);
1470
- // Only attribute 'scope' when the caller actually requested a scope filter.
1471
- // hasRequestedWikiScopeSupport() returns true by default for unscoped searches,
1472
- // so without this guard the diagnostics would overstate why the wiki hit passed.
1473
- const metadataSignals = [
1474
- requestedScopeKeys.size > 0 && scopeSupport ? 'scope' : null,
1475
- 'graph_primary',
1476
- ].filter((signal) => signal !== null);
1477
- const retrievalSourceForRecord = entry.vectorSimilarity !== null && lexicalSupport
1478
- ? 'wiki_hybrid_rrf'
1479
- : entry.vectorSimilarity !== null
1480
- ? 'wiki_vector_search'
1481
- : lexicalSupport
1482
- ? 'wiki_lexical_search'
1483
- : 'wiki_page';
1484
- return {
1485
- retrieval_source: retrievalSourceForRecord,
1486
- vector_similarity: entry.vectorSimilarity,
1487
- lexical_support: lexicalSupport,
1488
- entity_support: false,
1489
- scope_support: scopeSupport,
1490
- graph_source: 'primary',
1491
- is_vector_only: entry.vectorSimilarity !== null && confirmationSignals.length === 0,
1492
- confirmation_signals: confirmationSignals,
1493
- metadata_signals: metadataSignals,
1494
- candidate_threshold_used: searchOptions.threshold,
1495
- };
1496
- };
1497
1304
  const passesStrictness = (hitDiagnostics) => {
1498
1305
  if (searchOptions.strictness === 'recall') {
1499
1306
  return true;
@@ -1504,9 +1311,7 @@ async function recallMemory(query, options = {}) {
1504
1311
  if (!searchOptions.minLexicalSupport) {
1505
1312
  return true;
1506
1313
  }
1507
- return (hitDiagnostics.lexical_support ||
1508
- hitDiagnostics.entity_support ||
1509
- hitDiagnostics.confirmation_signals.includes('exact_topic'));
1314
+ return (hitDiagnostics.lexical_support || hitDiagnostics.confirmation_signals.includes('exact_topic'));
1510
1315
  };
1511
1316
  const acceptedPrimaryIds = new Set();
1512
1317
  matched = matched.flatMap((record) => {
@@ -1535,54 +1340,11 @@ async function recallMemory(query, options = {}) {
1535
1340
  const cappedIds = new Set(matched.map((record) => record.id));
1536
1341
  fusedHits = fusedHits.filter((hit) => hit.source_type !== 'decision' || cappedIds.has(hit.source_id));
1537
1342
  }
1538
- const acceptedWikiFusedHits = Array.from(wikiScores.values())
1539
- .sort((a, b) => b.score - a.score)
1540
- .flatMap((entry) => {
1541
- if (!hasWikiDecisionWithScopeAndTopic(entry.record)) {
1542
- return [];
1543
- }
1544
- const wikiDiagnostics = buildWikiDiagnostics(entry);
1545
- if (wikiDiagnostics.is_vector_only) {
1546
- diagnostics.candidate_counts.vector_only += 1;
1547
- }
1548
- if (!passesStrictness(wikiDiagnostics)) {
1549
- diagnostics.candidate_counts.rejected_by_strictness += 1;
1550
- return [];
1551
- }
1552
- return [
1553
- {
1554
- source_type: 'wiki_page',
1555
- source_id: entry.record.source_locator,
1556
- record: searchOptions.diagnostics
1557
- ? { ...entry.record, retrieval_diagnostics: wikiDiagnostics }
1558
- : entry.record,
1559
- fused_rank_score: entry.score,
1560
- ...(searchOptions.diagnostics ? { retrieval_diagnostics: wikiDiagnostics } : {}),
1561
- ...(entry.record.page_type === 'case' ? { page_type: 'case' } : {}),
1562
- ...(entry.record.case_id ? { case_id: entry.record.case_id } : {}),
1563
- },
1564
- ];
1565
- });
1566
- hasWikiHits = acceptedWikiFusedHits.length > 0;
1567
- fusedHits = [...fusedHits, ...acceptedWikiFusedHits].sort((left, right) => right.fused_rank_score - left.fused_rank_score);
1568
- if (hasWikiHits) {
1569
- retrievalSource = retrievalSource === 'none' ? 'wiki_page' : `${retrievalSource}+wiki_page`;
1570
- }
1571
- if (matched.length > 0) {
1572
- const readIdentityIndex = await (0, read_identity_js_1.loadDecisionReadIdentityIndex)(matched.filter((record) => !record.read_identity).map((record) => record.id));
1573
- for (const record of matched) {
1574
- if (record.read_identity) {
1575
- continue;
1576
- }
1577
- record.read_identity = (0, read_identity_js_1.resolveReadIdentity)(record, readIdentityIndex.get(record.id) ?? []);
1578
- }
1579
- }
1580
1343
  // Enrich active records with summaries from their superseded predecessors.
1581
1344
  // When ingestConversation extracts multiple facts under the same topic, only
1582
1345
  // the last survives as "active" — the earlier ones become superseded and are
1583
1346
  // excluded from search. This recovers their key information so it is not lost.
1584
1347
  if (matched.length > 0) {
1585
- const adapter = (0, db_manager_js_1.getAdapter)();
1586
1348
  const stmtChain = adapter.prepare(`SELECT id, summary, decision FROM decisions WHERE superseded_by = ?`);
1587
1349
  for (const record of matched) {
1588
1350
  const predecessors = stmtChain.all(record.id);
@@ -1613,24 +1375,29 @@ async function recallMemory(query, options = {}) {
1613
1375
  created_at: m.created_at,
1614
1376
  similarity: m.confidence ?? 0.5,
1615
1377
  }));
1616
- const mamaApiModule = await import('../mama-api.js');
1617
- const mamaDefault = mamaApiModule.default;
1618
- const expanded = await mamaDefault.expandWithGraph(candidates);
1378
+ const expanded = await expandWithGraphInAdapter(adapter, candidates);
1619
1379
  const primaryIds = new Set(matched.map((m) => m.id));
1620
1380
  let expandedOnly = expanded.filter((e) => !primaryIds.has(e.id));
1621
1381
  let expandedScopeMap = new Map();
1622
1382
  // Re-filter expanded results: apply status and scope checks
1623
1383
  if (!options.includeHistory) {
1624
1384
  expandedOnly = expandedOnly.filter((e) => {
1625
- const adapter = (0, db_manager_js_1.getAdapter)();
1626
- const row = adapter.prepare(`SELECT status FROM decisions WHERE id = ?`).get(e.id);
1385
+ const row = adapter
1386
+ .prepare(`SELECT kind, status FROM decisions WHERE id = ?`)
1387
+ .get(e.id);
1627
1388
  const status = row?.status || '';
1628
- return !status || !EXCLUDED_STATUSES.has(status);
1389
+ return matchesKind(row?.kind) && (!status || !EXCLUDED_STATUSES.has(status));
1390
+ });
1391
+ }
1392
+ else if (options.kind !== undefined) {
1393
+ expandedOnly = expandedOnly.filter((e) => {
1394
+ const row = adapter.prepare(`SELECT kind FROM decisions WHERE id = ?`).get(e.id);
1395
+ return matchesKind(row?.kind);
1629
1396
  });
1630
1397
  }
1631
1398
  if (options.scopes && options.scopes.length > 0) {
1632
1399
  const expandedIds = expandedOnly.map((e) => e.id);
1633
- expandedScopeMap = batchLoadScopes((0, db_manager_js_1.getAdapter)(), expandedIds);
1400
+ expandedScopeMap = batchLoadScopes(adapter, expandedIds);
1634
1401
  const requestedScopes = new Set(options.scopes.map((s) => `${s.kind}:${s.id}`));
1635
1402
  expandedOnly = expandedOnly.filter((e) => {
1636
1403
  const scopes = expandedScopeMap.get(e.id) ?? [];
@@ -1640,10 +1407,11 @@ async function recallMemory(query, options = {}) {
1640
1407
  });
1641
1408
  }
1642
1409
  bundle.graph_context.expanded = expandedOnly.flatMap((e) => {
1410
+ const kindRow = adapter.prepare(`SELECT kind FROM decisions WHERE id = ?`).get(e.id);
1643
1411
  const expandedRecord = {
1644
1412
  id: String(e.id),
1645
1413
  topic: String(e.topic || ''),
1646
- kind: 'decision',
1414
+ kind: (kindRow?.kind ?? 'decision'),
1647
1415
  summary: String(e.decision || ''),
1648
1416
  details: '',
1649
1417
  confidence: e.graph_rank ?? 0.5,
@@ -1695,12 +1463,11 @@ async function recallMemory(query, options = {}) {
1695
1463
  // never point at nodes that never made it into the graph payload.
1696
1464
  const acceptedExpandedIds = bundle.graph_context.expanded.map((record) => record.id);
1697
1465
  const allIds = [...matched.map((m) => m.id), ...acceptedExpandedIds];
1698
- const allEdges = await loadEdgesForIds(allIds);
1466
+ const allEdges = await loadEdgesForIds(adapter, allIds);
1699
1467
  // Filter out edges pointing to decisions with excluded statuses
1700
1468
  const activeIds = new Set(allIds);
1701
1469
  const edgesToCheck = allEdges.filter((e) => !activeIds.has(e.to_id) || !activeIds.has(e.from_id));
1702
1470
  if (edgesToCheck.length > 0) {
1703
- const adapter = (0, db_manager_js_1.getAdapter)();
1704
1471
  const checkIds = [
1705
1472
  ...new Set(edgesToCheck.flatMap((e) => [e.from_id, e.to_id]).filter((id) => !activeIds.has(id))),
1706
1473
  ];
@@ -1726,176 +1493,1199 @@ async function recallMemory(query, options = {}) {
1726
1493
  bundle.search_meta.diagnostics = diagnostics;
1727
1494
  }
1728
1495
  if (options.includeProfile) {
1729
- bundle.profile = await buildProfile(options.scopes ?? []);
1496
+ bundle.profile = await buildProfile(adapter, options.scopes ?? []);
1730
1497
  }
1731
1498
  return bundle;
1732
1499
  }
1733
- async function ingestMemoryInternal(input, options) {
1734
- const normalized = input.content.trim();
1735
- return saveMemoryInternal({
1736
- topic: normalized
1737
- .slice(0, 40)
1738
- .toLowerCase()
1739
- .replace(/[^a-z0-9]+/g, '_')
1740
- .replace(/^_+|_+$/g, '') || 'ingested_memory',
1741
- kind: 'fact',
1742
- summary: normalized.slice(0, 200),
1743
- details: normalized,
1744
- scopes: input.scopes ?? [],
1500
+ async function ingestMemoryInternal(adapter, input) {
1501
+ // Raw evidence goes through source.ingest: exactly one immutable observation
1502
+ // per request, no judgment row, no extraction.
1503
+ const normalized = input.content;
1504
+ const provenance = (0, provenance_js_1.normalizeMemoryWriteProvenance)();
1505
+ const requestedScopes = input.scopes ?? [];
1506
+ const access = (0, write_adapters_js_2.writeAccessForProvenance)(provenance, requestedScopes);
1507
+ const commandId = `source:${node_crypto_1.default
1508
+ .createHash('sha256')
1509
+ .update((0, canonicalize_js_1.canonicalizeJSON)({
1510
+ content: normalized,
1511
+ scopes: requestedScopes,
1745
1512
  source: input.source,
1746
- eventDate: input.eventDate,
1747
- eventDateTime: input.eventDateTime,
1748
- }, options);
1749
- }
1750
- async function ingestMemory(input) {
1751
- return ingestMemoryInternal((0, provenance_js_1.sanitizePublicIngestMemoryInput)(input));
1513
+ eventDate: input.eventDate ?? null,
1514
+ eventDateTime: input.eventDateTime ?? null,
1515
+ provenance: provenance.provenance,
1516
+ }))
1517
+ .digest('hex')
1518
+ .slice(0, 24)}`;
1519
+ const receipt = await (0, index_js_1.ingestSource)({
1520
+ commandId,
1521
+ source: {
1522
+ connector: input.source.source_type ?? 'ingest',
1523
+ id: commandId,
1524
+ },
1525
+ body: normalized,
1526
+ sourceAt: typeof input.eventDateTime === 'number'
1527
+ ? input.eventDateTime
1528
+ : input.eventDate
1529
+ ? Date.parse(input.eventDate)
1530
+ : null,
1531
+ metadata: {
1532
+ source: input.source,
1533
+ eventDate: input.eventDate ?? null,
1534
+ eventDateTime: input.eventDateTime ?? null,
1535
+ },
1536
+ scopes: [...requestedScopes],
1537
+ event: {
1538
+ actor: provenance.actor,
1539
+ ...(provenance.source_turn_id ? { sourceTurnId: provenance.source_turn_id } : {}),
1540
+ ...(provenance.source_refs.length > 0 ? { evidenceRefs: provenance.source_refs } : {}),
1541
+ reason: 'ingest memory',
1542
+ },
1543
+ }, access, { adapter });
1544
+ return { success: true, id: receipt.observationId };
1752
1545
  }
1753
- async function ingestWithTrustedProvenance(input, options) {
1754
- return ingestMemoryInternal((0, provenance_js_1.sanitizePublicIngestMemoryInput)(input), options);
1546
+ async function ingestMemory(adapter, input) {
1547
+ return ingestMemoryInternal(adapter, (0, provenance_js_1.sanitizePublicIngestMemoryInput)(input));
1755
1548
  }
1756
1549
  async function evolveMemory(input) {
1757
1550
  return (0, evolution_engine_js_1.resolveMemoryEvolution)(input);
1758
1551
  }
1759
- async function buildMemoryBootstrap(params) {
1760
- return (0, bootstrap_builder_js_1.buildMemoryAgentBootstrap)(params);
1552
+ async function buildMemoryBootstrap(adapter, params) {
1553
+ return (0, bootstrap_builder_js_1.buildMemoryAgentBootstrap)(adapter, params);
1761
1554
  }
1762
1555
  function createAuditAck(input) {
1763
1556
  return (0, types_js_1.createMemoryAuditAck)(input);
1764
1557
  }
1765
- async function recordMemoryAudit(input) {
1766
- return (0, channel_summary_state_store_js_1.recordChannelAudit)(input);
1767
- }
1768
- async function callExtractionLLM(prompt, options) {
1769
- const model = options.model ?? 'claude-sonnet-4-6';
1770
- const baseUrl = options.baseUrl ?? 'https://api.anthropic.com';
1771
- // Security: only send ANTHROPIC_API_KEY to Anthropic's own domain
1772
- const isAnthropicDomain = /^https?:\/\/([^/]*\.)?anthropic\.com(\/|$)/i.test(baseUrl);
1773
- const apiKey = isAnthropicDomain
1774
- ? (options.apiKey ?? process.env.ANTHROPIC_API_KEY)
1775
- : options.apiKey; // custom baseUrl must supply its own key explicitly
1776
- if (!apiKey) {
1777
- throw new Error(isAnthropicDomain
1778
- ? 'ANTHROPIC_API_KEY is required for extraction'
1779
- : 'apiKey must be provided explicitly when using a custom baseUrl');
1780
- }
1781
- const res = await fetch(`${baseUrl}/v1/messages`, {
1782
- method: 'POST',
1783
- headers: {
1784
- 'Content-Type': 'application/json',
1785
- 'x-api-key': apiKey,
1786
- 'anthropic-version': '2023-06-01',
1787
- },
1788
- body: JSON.stringify({
1789
- model,
1790
- max_tokens: 4096,
1791
- messages: [{ role: 'user', content: prompt }],
1792
- }),
1793
- });
1794
- if (!res.ok) {
1795
- const errorBody = await res.text().catch(() => 'unknown');
1796
- throw new Error(`Anthropic API error ${res.status}: ${errorBody}`);
1797
- }
1798
- const data = (await res.json());
1799
- const text = data.content?.find((c) => c.type === 'text')?.text ?? '';
1800
- return (0, extraction_prompt_js_1.parseExtractionResponse)(text);
1801
- }
1802
- let extractionFn = callExtractionLLM;
1803
- function setExtractionFn(fn) {
1804
- extractionFn = fn ?? callExtractionLLM;
1558
+ async function recordMemoryAudit(adapter, input) {
1559
+ return (0, channel_summary_state_store_js_1.recordChannelAudit)(adapter, input);
1805
1560
  }
1806
- async function ingestConversationInternal(input, options) {
1561
+ async function ingestConversationInternal(adapter, input) {
1807
1562
  if (!input.messages || input.messages.length === 0) {
1808
1563
  throw new Error('messages array must not be empty');
1809
1564
  }
1565
+ // Extraction was removed from the write boundary: conversations are stored as
1566
+ // raw evidence only. The option is rejected before any write so a caller can
1567
+ // never mistake a raw observation for an extracted judgment.
1568
+ if (input.extract !== undefined) {
1569
+ throw new Error('ingestConversation() no longer supports the extract option; ' +
1570
+ 'conversations are stored as raw source observations without judgment writes');
1571
+ }
1810
1572
  const conversationText = input.messages.map((m) => `${m.role}: ${m.content}`).join('\n');
1811
1573
  const topicPrefix = input.topicPrefix || '';
1812
- const rawResult = await ingestMemoryInternal({
1813
- content: topicPrefix ? `${topicPrefix}${conversationText}` : conversationText,
1814
- scopes: input.scopes,
1574
+ const body = topicPrefix ? `${topicPrefix}${conversationText}` : conversationText;
1575
+ const provenance = (0, provenance_js_1.normalizeMemoryWriteProvenance)();
1576
+ const requestedScopes = input.scopes ?? [];
1577
+ const access = (0, write_adapters_js_2.writeAccessForProvenance)(provenance, requestedScopes);
1578
+ // No observedAt in the command: it must hash identically on a retry so the
1579
+ // same commandId replays its stored receipt instead of conflicting.
1580
+ const commandId = `source-conv:${node_crypto_1.default
1581
+ .createHash('sha256')
1582
+ .update((0, canonicalize_js_1.canonicalizeJSON)({
1583
+ body,
1584
+ scopes: requestedScopes,
1815
1585
  source: input.source,
1816
- eventDate: input.sessionDate,
1817
- }, options);
1818
- const result = {
1819
- rawId: rawResult.id,
1820
- extractedMemories: [],
1586
+ sessionDate: input.sessionDate ?? null,
1587
+ provenance: provenance.provenance,
1588
+ }))
1589
+ .digest('hex')
1590
+ .slice(0, 24)}`;
1591
+ const receipt = await (0, index_js_1.ingestSource)({
1592
+ commandId,
1593
+ source: {
1594
+ connector: `conversation:${input.source.source_type ?? 'unknown'}`,
1595
+ id: commandId,
1596
+ },
1597
+ body,
1598
+ sourceAt: input.sessionDate ? Date.parse(input.sessionDate) : null,
1599
+ metadata: {
1600
+ message_count: input.messages.length,
1601
+ roles: input.messages.map((m) => m.role),
1602
+ session_date: input.sessionDate ?? null,
1603
+ topic_prefix: topicPrefix || null,
1604
+ source: input.source,
1605
+ },
1606
+ scopes: [...requestedScopes],
1607
+ event: {
1608
+ actor: provenance.actor,
1609
+ ...(provenance.source_turn_id ? { sourceTurnId: provenance.source_turn_id } : {}),
1610
+ ...(provenance.source_refs.length > 0 ? { evidenceRefs: provenance.source_refs } : {}),
1611
+ reason: 'ingest conversation',
1612
+ },
1613
+ }, access, { adapter });
1614
+ return { rawId: receipt.observationId, extractedMemories: [] };
1615
+ }
1616
+ async function ingestConversation(adapter, input) {
1617
+ return ingestConversationInternal(adapter, (0, provenance_js_1.sanitizePublicIngestConversationInput)(input));
1618
+ }
1619
+ // ── Adapter-bound reads and writes the catalog actions call ─────────────────
1620
+ //
1621
+ // These were `mama-api.ts`'s, beside an ambient-handle facade for callers who
1622
+ // did not hold a database. The catalog holds one and calls these directly, so
1623
+ // they live with the rest of the memory API. What stays in `mama-api.ts` is the
1624
+ // facade and the formatting the CLI still uses (W22).
1625
+ function numberOrNull(value) {
1626
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
1627
+ return null;
1628
+ }
1629
+ return value;
1630
+ }
1631
+ function resultRecord(result) {
1632
+ if (typeof result.record === 'object' &&
1633
+ result.record !== null &&
1634
+ !Array.isArray(result.record)) {
1635
+ return result.record;
1636
+ }
1637
+ return {};
1638
+ }
1639
+ function stringOrNull(value) {
1640
+ if (value === null || value === undefined) {
1641
+ return null;
1642
+ }
1643
+ return String(value);
1644
+ }
1645
+ function confidenceValue(record, fallback) {
1646
+ const numeric = numberOrNull(record.confidence);
1647
+ if (numeric !== null) {
1648
+ return numeric;
1649
+ }
1650
+ switch (record.confidence) {
1651
+ case 'high':
1652
+ return 0.9;
1653
+ case 'medium':
1654
+ return 0.6;
1655
+ case 'low':
1656
+ return 0.3;
1657
+ default:
1658
+ return fallback;
1659
+ }
1660
+ }
1661
+ async function expandWithGraphInAdapter(adapter, candidates) {
1662
+ const graphEnhanced = new Map(); // Use Map for deduplication by ID
1663
+ const primaryIds = new Set(candidates.map((c) => c.id)); // Track primary candidates
1664
+ // Process each candidate
1665
+ for (const candidate of candidates) {
1666
+ // Add primary candidate with higher rank
1667
+ if (!graphEnhanced.has(candidate.id)) {
1668
+ graphEnhanced.set(candidate.id, {
1669
+ ...candidate,
1670
+ graph_source: 'primary', // Mark as primary result
1671
+ graph_rank: 1.0, // Highest rank
1672
+ });
1673
+ }
1674
+ // 1. Add supersedes chain (evolution history)
1675
+ try {
1676
+ const chain = await (0, graph_query_js_1.queryDecisionGraph)(adapter, candidate.topic, candidate.id);
1677
+ for (const decision of chain) {
1678
+ if (!graphEnhanced.has(decision.id)) {
1679
+ graphEnhanced.set(decision.id, {
1680
+ ...decision,
1681
+ graph_source: 'supersedes_chain',
1682
+ graph_rank: 0.8, // Lower rank than primary
1683
+ similarity: (candidate.similarity ?? 0) * 0.9, // Inherit similarity, slightly reduced
1684
+ related_to: candidate.id, // Track relationship
1685
+ });
1686
+ }
1687
+ }
1688
+ }
1689
+ catch (error) {
1690
+ (0, debug_logger_js_1.warn)(`Failed to get supersedes chain for ${candidate.topic}: ${error instanceof Error ? error.message : String(error)}`);
1691
+ }
1692
+ // 2. Add semantic edges (refines, contradicts, builds_on, debates, synthesizes)
1693
+ try {
1694
+ const rawEdges = (await (0, graph_query_js_1.querySemanticEdges)(adapter, [candidate.id])) || {};
1695
+ const edges = {
1696
+ refines: rawEdges.refines || [],
1697
+ refined_by: rawEdges.refined_by || [],
1698
+ contradicts: rawEdges.contradicts || [],
1699
+ contradicted_by: rawEdges.contradicted_by || [],
1700
+ builds_on: rawEdges.builds_on || [],
1701
+ built_on_by: rawEdges.built_on_by || [],
1702
+ debates: rawEdges.debates || [],
1703
+ debated_by: rawEdges.debated_by || [],
1704
+ synthesizes: rawEdges.synthesizes || [],
1705
+ synthesized_by: rawEdges.synthesized_by || [],
1706
+ };
1707
+ // Helper to add edge to graph
1708
+ const addEdge = (edge, idField, source, rank, simFactor) => {
1709
+ const id = edge[idField];
1710
+ if (!graphEnhanced.has(id)) {
1711
+ graphEnhanced.set(id, {
1712
+ id: id,
1713
+ topic: edge.topic,
1714
+ decision: edge.decision,
1715
+ confidence: edge.confidence,
1716
+ created_at: edge.created_at,
1717
+ graph_source: source,
1718
+ graph_rank: rank,
1719
+ similarity: (candidate.similarity ?? 0) * simFactor,
1720
+ related_to: candidate.id,
1721
+ edge_reason: edge.reason,
1722
+ });
1723
+ }
1724
+ };
1725
+ // Add refines edges
1726
+ for (const edge of edges.refines) {
1727
+ addEdge(edge, 'to_id', 'refines', 0.7, 0.85);
1728
+ }
1729
+ // Add refined_by edges
1730
+ for (const edge of edges.refined_by) {
1731
+ addEdge(edge, 'from_id', 'refined_by', 0.7, 0.85);
1732
+ }
1733
+ // Add contradicts edges (lower rank, but still relevant)
1734
+ for (const edge of edges.contradicts) {
1735
+ addEdge(edge, 'to_id', 'contradicts', 0.6, 0.8);
1736
+ }
1737
+ // Story 2.1: Add builds_on edges (high relevance - extending prior work)
1738
+ for (const edge of edges.builds_on) {
1739
+ addEdge(edge, 'to_id', 'builds_on', 0.75, 0.9);
1740
+ }
1741
+ // Add built_on_by edges (someone built on this decision)
1742
+ for (const edge of edges.built_on_by) {
1743
+ addEdge(edge, 'from_id', 'built_on_by', 0.75, 0.9);
1744
+ }
1745
+ // Add debates edges (alternative view)
1746
+ for (const edge of edges.debates) {
1747
+ addEdge(edge, 'to_id', 'debates', 0.65, 0.85);
1748
+ }
1749
+ // Add debated_by edges
1750
+ for (const edge of edges.debated_by) {
1751
+ addEdge(edge, 'from_id', 'debated_by', 0.65, 0.85);
1752
+ }
1753
+ // Add synthesizes edges (unified approach)
1754
+ for (const edge of edges.synthesizes) {
1755
+ addEdge(edge, 'to_id', 'synthesizes', 0.7, 0.88);
1756
+ }
1757
+ // Add synthesized_by edges
1758
+ for (const edge of edges.synthesized_by) {
1759
+ addEdge(edge, 'from_id', 'synthesized_by', 0.7, 0.88);
1760
+ }
1761
+ }
1762
+ catch (error) {
1763
+ (0, debug_logger_js_1.warn)(`Failed to get semantic edges for ${candidate.id}: ${error instanceof Error ? error.message : String(error)}`);
1764
+ }
1765
+ }
1766
+ // 3. Convert Map to Array
1767
+ const allResults = Array.from(graphEnhanced.values());
1768
+ // 4. Sort: Interleave expanded results after their related primary
1769
+ // This ensures edge-connected decisions appear near their source
1770
+ const primaryResults = allResults
1771
+ .filter((r) => primaryIds.has(r.id))
1772
+ .sort((a, b) => {
1773
+ const scoreA = a.final_score || a.similarity || 0;
1774
+ const scoreB = b.final_score || b.similarity || 0;
1775
+ return scoreB - scoreA;
1776
+ });
1777
+ const expandedResults = allResults.filter((r) => !primaryIds.has(r.id));
1778
+ // Build final results: each primary followed by its related expanded results
1779
+ const results = [];
1780
+ for (const primary of primaryResults) {
1781
+ results.push(primary);
1782
+ // Find expanded results related to this primary
1783
+ const relatedExpanded = expandedResults.filter((e) => e.related_to === primary.id);
1784
+ // Sort related by graph_rank (higher first)
1785
+ relatedExpanded.sort((a, b) => (b.graph_rank || 0) - (a.graph_rank || 0));
1786
+ // Add related expanded results right after their primary
1787
+ results.push(...relatedExpanded);
1788
+ }
1789
+ // Add any orphaned expanded results (shouldn't happen, but safety net)
1790
+ const includedIds = new Set(results.map((r) => r.id));
1791
+ const orphaned = expandedResults.filter((e) => !includedIds.has(e.id));
1792
+ results.push(...orphaned);
1793
+ return results;
1794
+ }
1795
+ function applyRecencyBoost(results, options = {}) {
1796
+ const { recencyWeight = 0.3, recencyScale = 7, recencyDecay = 0.5, disableRecency = false, } = options;
1797
+ if (disableRecency || recencyWeight === 0) {
1798
+ return results;
1799
+ }
1800
+ const now = Date.now(); // Current timestamp in milliseconds
1801
+ return results
1802
+ .map((r) => {
1803
+ // created_at is stored in milliseconds in the database
1804
+ const createdAt = typeof r.created_at === 'number' ? r.created_at : Date.parse(r.created_at || '0');
1805
+ const ageInDays = (now - createdAt) / (86400 * 1000);
1806
+ // Gaussian Decay: exp(-((age / scale)^2) / (2 * ln(1 / decay)))
1807
+ // At scale days: score = decay (e.g., 7 days = 50%)
1808
+ const gaussianDecay = Math.exp(-Math.pow(ageInDays / recencyScale, 2) / (2 * Math.log(1 / recencyDecay)));
1809
+ // Combine semantic similarity with recency
1810
+ const similarity = r.similarity ?? 0;
1811
+ const finalScore = similarity * (1 - recencyWeight) + gaussianDecay * recencyWeight;
1812
+ return {
1813
+ ...r,
1814
+ recency_score: gaussianDecay,
1815
+ recency_age_days: Math.round(ageInDays * 10) / 10,
1816
+ final_score: finalScore,
1817
+ };
1818
+ })
1819
+ .sort((a, b) => (b.final_score ?? 0) - (a.final_score ?? 0));
1820
+ }
1821
+ /** Phase 3 Task 33: learned-ranker meta attached to mama.suggest response. */
1822
+ function buildRankerMeta(applied, modelId, skippedReason) {
1823
+ const meta = {
1824
+ model_id: modelId,
1825
+ feature_set_version: ranker_features_js_1.SEARCH_RANKER_FEATURE_SET_VERSION,
1826
+ applied,
1827
+ mode: 'offline',
1828
+ };
1829
+ if (skippedReason) {
1830
+ meta.skipped_reason = skippedReason;
1831
+ }
1832
+ return meta;
1833
+ }
1834
+ function mapRolledUpResult(result) {
1835
+ const record = resultRecord(result);
1836
+ const retrievalDiagnostics = result.retrieval_diagnostics;
1837
+ const topic = stringOrNull(record.topic ?? record.title) ?? result.source_id;
1838
+ // For wiki_page leaves, prefer the markdown body (`content`) in `decision` so
1839
+ // downstream consumers see the meaningful body rather than the short title.
1840
+ // Decision/checkpoint records use their own fields (summary/decision).
1841
+ const isWikiPageLeaf = result.source_type === 'wiki_page';
1842
+ const decision = isWikiPageLeaf
1843
+ ? (stringOrNull(record.content ?? record.summary ?? record.decision ?? record.title) ??
1844
+ result.source_id)
1845
+ : (stringOrNull(record.summary ?? record.decision ?? record.title ?? record.content) ??
1846
+ result.source_id);
1847
+ const reasoning = stringOrNull(record.details ?? record.reasoning ?? record.status_reason ?? record.content) ??
1848
+ '';
1849
+ return {
1850
+ id: result.source_id,
1851
+ topic,
1852
+ decision,
1853
+ reasoning,
1854
+ confidence: confidenceValue(record, result.score),
1855
+ // NOT similarity. `result.score` is normalized Reciprocal Rank Fusion
1856
+ // (RRF_K = 60, divided by the top hit), so the second result scores 61/62
1857
+ // and the third 61/63 whatever they say -- rank, not likeness. Mirroring it
1858
+ // into `similarity` made every save warn "High similarity (98%)" against
1859
+ // something unrelated, which teaches the reader to ignore the check. The
1860
+ // rollup path already says this and returns null; this one now agrees.
1861
+ similarity: null,
1862
+ retrieval_score: result.score,
1863
+ created_at: record.created_at ?? null,
1864
+ event_date: record.event_date ?? null,
1865
+ event_datetime: record.event_datetime ?? null,
1866
+ graph_source: retrievalDiagnostics?.graph_source ?? 'primary',
1867
+ graph_rank: 1,
1868
+ related_to: null,
1869
+ edge_reason: null,
1870
+ case_id: result.case_id,
1871
+ source_type: result.source_type,
1872
+ kind: stringOrNull(record.kind) ?? 'decision',
1873
+ contributing_leaves: result.contributing_leaves ?? null,
1874
+ ...(result.contributing_leaf_diagnostics
1875
+ ? { contributing_leaf_diagnostics: result.contributing_leaf_diagnostics }
1876
+ : {}),
1877
+ ...(retrievalDiagnostics ? { retrieval_diagnostics: retrievalDiagnostics } : {}),
1821
1878
  };
1822
- if (!input.extract?.enabled) {
1823
- return result;
1879
+ }
1880
+ async function suggestInAdapter(adapter, userQuestion, options = {}
1881
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1882
+ ) {
1883
+ if (!userQuestion || typeof userQuestion !== 'string') {
1884
+ throw new Error('mama.suggest() requires userQuestion (string)');
1824
1885
  }
1825
- let units;
1886
+ const { format = 'json', limit = 5, threshold, useReranking = false, rerankWithLearned = false,
1887
+ // Recency boosting parameters (Gaussian Decay - Elasticsearch style)
1888
+ recencyWeight = 0.3, // 0-1: How much to weight recency (0.3 = 70% semantic, 30% recency)
1889
+ recencyScale = 7, // Days until recency score drops to 50%
1890
+ recencyDecay = 0.5, // Score at scale point (0.5 = 50%)
1891
+ disableRecency = false, // Set true to disable recency boosting entirely
1892
+ strict, strictness, includeRelated, minLexicalSupport, diagnostics: includeDiagnostics, kind, } = options;
1893
+ const normalizedSearchOptions = (0, search_quality_js_1.normalizeSearchQualityOptions)({
1894
+ threshold,
1895
+ strict,
1896
+ strictness,
1897
+ disableRecency,
1898
+ includeRelated,
1899
+ topicPrefix: options.topicPrefix,
1900
+ minLexicalSupport,
1901
+ diagnostics: includeDiagnostics,
1902
+ });
1903
+ const memoryV2QualityContractRequested = threshold !== undefined ||
1904
+ strict !== undefined ||
1905
+ strictness !== undefined ||
1906
+ includeRelated !== undefined ||
1907
+ minLexicalSupport !== undefined ||
1908
+ includeDiagnostics === true ||
1909
+ options.topicPrefix !== undefined ||
1910
+ options.scopes !== undefined ||
1911
+ options.kind !== undefined;
1912
+ const rerankPoolLimit = rerankWithLearned ? Math.max(limit * 4, limit + 5) : limit;
1826
1913
  try {
1827
- // Fetch existing topics so LLM can reuse them (enables supersedes edges)
1828
- await (0, db_manager_js_1.initDB)();
1829
- const adapter = (0, db_manager_js_1.getAdapter)();
1830
- let existingTopics;
1831
- if (input.scopes && input.scopes.length > 0) {
1832
- const scopeIds = await Promise.all(input.scopes.map((scope) => (0, db_manager_js_1.ensureMemoryScope)(scope.kind, scope.id)));
1833
- const placeholders = scopeIds.map(() => '?').join(', ');
1834
- existingTopics = adapter
1835
- .prepare(`SELECT DISTINCT d.topic FROM decisions d
1836
- JOIN memory_scope_bindings msb ON msb.memory_id = d.id
1837
- WHERE msb.scope_id IN (${placeholders})
1838
- AND (d.status = 'active' OR d.status IS NULL)
1839
- ORDER BY d.created_at DESC LIMIT 200`)
1840
- .all(...scopeIds);
1914
+ // `recallMemoryInAdapter` was mama-api's alias for this file's own
1915
+ // `recallMemory`. Inside it, the function has its name.
1916
+ const bundle = await recallMemory(adapter, userQuestion, {
1917
+ includeProfile: false,
1918
+ topicPrefix: options.topicPrefix,
1919
+ limit: rerankPoolLimit,
1920
+ threshold,
1921
+ strict,
1922
+ strictness,
1923
+ disableRecency,
1924
+ includeRelated,
1925
+ minLexicalSupport,
1926
+ diagnostics: includeDiagnostics,
1927
+ kind,
1928
+ ...(options.scopes && { scopes: options.scopes }),
1929
+ });
1930
+ const diagnosticsByMemoryId = new Map(bundle.memories
1931
+ .filter((memory) => memory.retrieval_diagnostics)
1932
+ .map((memory) => [memory.id, memory.retrieval_diagnostics]));
1933
+ const rawFusedHits = bundle.fused_hits ?? [];
1934
+ const fusedHits = rawFusedHits.map((hit) => {
1935
+ if (hit.source_type !== 'decision') {
1936
+ return hit;
1937
+ }
1938
+ const recordDiagnostics = typeof hit.record === 'object' && hit.record !== null && !Array.isArray(hit.record)
1939
+ ? hit.record.retrieval_diagnostics
1940
+ : undefined;
1941
+ const retrievalDiagnostics = diagnosticsByMemoryId.get(hit.source_id) ?? recordDiagnostics;
1942
+ if (!retrievalDiagnostics) {
1943
+ return hit;
1944
+ }
1945
+ const record = typeof hit.record === 'object' && hit.record !== null && !Array.isArray(hit.record)
1946
+ ? { ...hit.record, retrieval_diagnostics: retrievalDiagnostics }
1947
+ : hit.record;
1948
+ return {
1949
+ ...hit,
1950
+ record,
1951
+ retrieval_diagnostics: retrievalDiagnostics,
1952
+ };
1953
+ });
1954
+ const rolledUp = fusedHits.length > 0 ? (0, case_search_rollup_js_1.rollUpSearchHits)({ fusedHits, adapter }) : [];
1955
+ const diagnosticsResponse = includeDiagnostics === true ? { diagnostics: bundle.search_meta.diagnostics ?? null } : {};
1956
+ // Phase 3 Task 33: compute base ranker meta once so every return path
1957
+ // (rolledUp, memories fallback, vector-search fallback) can attach it.
1958
+ // rerankWithLearned-driven rescoring still only applies to result arrays
1959
+ // that match the ranker's expected shape (id + source_type + case_id).
1960
+ const baseRankerMeta = useReranking
1961
+ ? buildRankerMeta(false, null, 'llm_reranking_requested')
1962
+ : rerankWithLearned
1963
+ ? null // marker: rescoring requested, actual meta set per-path after rescore
1964
+ : buildRankerMeta(false, null, 'feature_disabled');
1965
+ const applyLearnedRanker = (results) => {
1966
+ if (baseRankerMeta !== null) {
1967
+ return { results, meta: baseRankerMeta };
1968
+ }
1969
+ // Phase 3 Task 33: the caller opted in via rerankWithLearned, but the
1970
+ // runtime `search_ranker_enabled` gate still has final say. This lets
1971
+ // operators disable the learned ranker globally during rollback without
1972
+ // touching any caller code.
1973
+ let runtimeEnabled = true;
1974
+ try {
1975
+ runtimeEnabled = (0, ranker_rescore_js_1.isSearchRankerEnabled)(adapter);
1976
+ }
1977
+ catch (err) {
1978
+ (0, debug_logger_js_1.warn)(`[mama.suggest] isSearchRankerEnabled check failed: ${String(err)}`);
1979
+ }
1980
+ if (!runtimeEnabled) {
1981
+ return { results, meta: buildRankerMeta(false, null, 'feature_disabled') };
1982
+ }
1983
+ try {
1984
+ const rescored = (0, ranker_rescore_js_1.rescoreSearchResults)(adapter, {
1985
+ query: userQuestion,
1986
+ results,
1987
+ });
1988
+ return {
1989
+ results: rescored.results,
1990
+ meta: buildRankerMeta(rescored.skipped_reason === undefined, rescored.model_id, rescored.skipped_reason),
1991
+ };
1992
+ }
1993
+ catch (err) {
1994
+ (0, debug_logger_js_1.warn)(`[mama.suggest] learned-ranker rescore failed: ${String(err)}`);
1995
+ return { results, meta: buildRankerMeta(false, null, 'rescore_error') };
1996
+ }
1997
+ };
1998
+ const summarizeGraphExpansion = (rows) => {
1999
+ const sources = {
2000
+ primary: 0,
2001
+ supersedes_chain: 0,
2002
+ refines: 0,
2003
+ refined_by: 0,
2004
+ contradicts: 0,
2005
+ };
2006
+ let expandedCount = 0;
2007
+ for (const row of rows) {
2008
+ const graphSource = row.graph_source ?? 'primary';
2009
+ if (graphSource === 'primary') {
2010
+ sources.primary += 1;
2011
+ continue;
2012
+ }
2013
+ expandedCount += 1;
2014
+ if (graphSource in sources) {
2015
+ const key = graphSource;
2016
+ sources[key] += 1;
2017
+ }
2018
+ }
2019
+ return {
2020
+ total_results: rows.length,
2021
+ primary_count: sources.primary,
2022
+ expanded_count: expandedCount,
2023
+ sources,
2024
+ };
2025
+ };
2026
+ if (rolledUp.length > 0) {
2027
+ const filteredResults = rolledUp.slice(0, rerankPoolLimit);
2028
+ const { results: mappedResults, meta: rankerMeta } = applyLearnedRanker(filteredResults.map(mapRolledUpResult));
2029
+ const limitedResults = mappedResults.slice(0, limit);
2030
+ if (format === 'markdown') {
2031
+ const context = limitedResults
2032
+ .map((result, index) => `${index + 1}. [${result.topic}] ${result.decision}\n ${result.reasoning}`)
2033
+ .join('\n');
2034
+ return `🔍 Search method: memory_v2\n${context}`;
2035
+ }
2036
+ return {
2037
+ query: userQuestion,
2038
+ results: limitedResults,
2039
+ ...diagnosticsResponse,
2040
+ meta: {
2041
+ count: limitedResults.length,
2042
+ search_method: 'memory_v2',
2043
+ threshold: normalizedSearchOptions.threshold,
2044
+ recency_boost: disableRecency
2045
+ ? null
2046
+ : {
2047
+ weight: recencyWeight,
2048
+ scale: recencyScale,
2049
+ decay: recencyDecay,
2050
+ },
2051
+ graph_expansion: summarizeGraphExpansion(limitedResults),
2052
+ ranker: rankerMeta,
2053
+ },
2054
+ };
1841
2055
  }
1842
- else {
1843
- existingTopics = adapter
1844
- .prepare(`SELECT DISTINCT topic FROM decisions
1845
- WHERE (status = 'active' OR status IS NULL)
1846
- ORDER BY created_at DESC LIMIT 200`)
1847
- .all();
2056
+ if (bundle.memories.length > 0) {
2057
+ // recallMemory uses RRF fusion — confidence is overwritten with the normalized
2058
+ // retrieval score (0-1 range, where 1.0 = best match in this result set).
2059
+ // The original stored confidence is lost after RRF normalization.
2060
+ // We capture the retrieval score separately so `similarity` reflects search
2061
+ // relevance while `confidence` is passed through as-is from the bundle.
2062
+ const filteredMemories = bundle.memories.slice(0, rerankPoolLimit);
2063
+ const baseRows = filteredMemories.map((memory) => ({
2064
+ id: memory.id,
2065
+ topic: memory.topic,
2066
+ decision: memory.summary,
2067
+ reasoning: memory.details,
2068
+ confidence: memory.confidence,
2069
+ // recallMemory currently normalizes fused retrieval rank into `confidence`.
2070
+ // Keep that value visible as retrieval_score, but do not pretend it is
2071
+ // semantic similarity; save-time warning logic keys off `similarity`.
2072
+ similarity: null,
2073
+ retrieval_score: memory.confidence ?? null,
2074
+ final_score: memory.confidence ?? null,
2075
+ created_at: memory.created_at,
2076
+ event_date: memory.event_date ?? null,
2077
+ event_datetime: memory.event_datetime ?? null,
2078
+ graph_source: memory.retrieval_diagnostics?.graph_source ?? 'primary',
2079
+ graph_rank: 1,
2080
+ related_to: null,
2081
+ edge_reason: null,
2082
+ case_id: null,
2083
+ source_type: memory.kind ??
2084
+ memory.source?.source_type ??
2085
+ memory.source_type ??
2086
+ memory.type ??
2087
+ 'decision',
2088
+ kind: memory.kind,
2089
+ ...(memory.retrieval_diagnostics
2090
+ ? { retrieval_diagnostics: memory.retrieval_diagnostics }
2091
+ : {}),
2092
+ }));
2093
+ const { results: rankedRows, meta: rankerMeta } = applyLearnedRanker(baseRows);
2094
+ const limitedRows = rankedRows.slice(0, limit);
2095
+ if (format === 'markdown') {
2096
+ const context = limitedRows
2097
+ .map((row, index) => `${index + 1}. [${row.topic}] ${row.decision}\n ${row.reasoning}`)
2098
+ .join('\n');
2099
+ return `🔍 Search method: memory_v2\n${context}`;
2100
+ }
2101
+ return {
2102
+ query: userQuestion,
2103
+ results: limitedRows,
2104
+ ...diagnosticsResponse,
2105
+ meta: {
2106
+ count: limitedRows.length,
2107
+ search_method: 'memory_v2',
2108
+ threshold: normalizedSearchOptions.threshold,
2109
+ recency_boost: disableRecency
2110
+ ? null
2111
+ : {
2112
+ weight: recencyWeight,
2113
+ scale: recencyScale,
2114
+ decay: recencyDecay,
2115
+ },
2116
+ graph_expansion: summarizeGraphExpansion(limitedRows),
2117
+ ranker: rankerMeta,
2118
+ },
2119
+ };
1848
2120
  }
1849
- const topicList = existingTopics.map((r) => r.topic);
1850
- const prompt = (0, extraction_prompt_js_1.buildExtractionPrompt)(input.messages, topicList);
1851
- units = await extractionFn(prompt, input.extract);
1852
- }
1853
- catch (err) {
1854
- (0, debug_logger_js_1.warn)(`[memory] extraction failed: ${err instanceof Error ? err.message : String(err)}`);
1855
- return result;
1856
- }
1857
- const adapter = (0, db_manager_js_1.getAdapter)();
1858
- const EXTRACTION_EDGE_RELATIONSHIP = 'builds_on';
1859
- const EXTRACTION_EDGE_REASON = 'Extracted from conversation';
1860
- // Track IDs saved in this batch so sibling facts don't supersede each other.
1861
- // Without this, when ingestConversation extracts multiple facts under the same topic,
1862
- // each subsequent save supersedes the previous one, losing independent information.
1863
- const batchSavedIds = [];
1864
- for (const unit of units) {
2121
+ if (memoryV2QualityContractRequested) {
2122
+ const emptyRows = [];
2123
+ const { meta: rankerMeta } = applyLearnedRanker(emptyRows);
2124
+ if (format === 'markdown') {
2125
+ return '🔍 Search method: memory_v2\n';
2126
+ }
2127
+ return {
2128
+ query: userQuestion,
2129
+ results: emptyRows,
2130
+ ...diagnosticsResponse,
2131
+ meta: {
2132
+ count: 0,
2133
+ search_method: 'memory_v2',
2134
+ threshold: normalizedSearchOptions.threshold,
2135
+ recency_boost: disableRecency
2136
+ ? null
2137
+ : {
2138
+ weight: recencyWeight,
2139
+ scale: recencyScale,
2140
+ decay: recencyDecay,
2141
+ },
2142
+ graph_expansion: summarizeGraphExpansion(emptyRows),
2143
+ ranker: rankerMeta,
2144
+ },
2145
+ };
2146
+ }
2147
+ // 1. Try vector search first (if sqlite-vss is available)
2148
+ // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-explicit-any
2149
+ let results = [];
2150
+ let searchMethod = 'vector';
1865
2151
  try {
1866
- const saved = await saveMemoryInternal({
1867
- topic: topicPrefix ? `${topicPrefix}${unit.topic}` : unit.topic,
1868
- kind: unit.kind,
1869
- summary: unit.summary,
1870
- details: unit.details,
1871
- confidence: unit.confidence,
1872
- scopes: input.scopes,
1873
- source: input.source,
1874
- excludeIds: batchSavedIds,
1875
- eventDate: input.sessionDate,
1876
- }, (0, provenance_js_1.appendProvenanceSourceRefs)(options, [`raw_memory:${rawResult.id}`]));
1877
- const now = Date.now();
1878
- adapter
1879
- .prepare(`INSERT OR REPLACE INTO decision_edges (from_id, to_id, relationship, reason, weight, created_at)
1880
- VALUES (?, ?, ?, ?, ?, ?)`)
1881
- .run(saved.id, rawResult.id, EXTRACTION_EDGE_RELATIONSHIP, EXTRACTION_EDGE_REASON, 1.0, now);
1882
- batchSavedIds.push(saved.id);
1883
- result.extractedMemories.push({
1884
- id: saved.id,
1885
- kind: unit.kind,
1886
- topic: unit.topic,
1887
- });
2152
+ // Generate query embedding
2153
+ const queryEmbedding = await (0, embedder_js_1.generateEmbedding)(userQuestion, 'query');
2154
+ // Adaptive threshold (shorter queries need higher confidence)
2155
+ const wordCount = userQuestion.split(/\s+/).length;
2156
+ const adaptiveThreshold = threshold !== undefined ? threshold : wordCount < 3 ? 0.7 : 0.6;
2157
+ // Vector search
2158
+ results = await (0, search_js_1.vectorSearch)(adapter, queryEmbedding, rerankPoolLimit * 2, 0.5, undefined, undefined, kind); // Get more candidates
2159
+ // Filter by adaptive threshold
2160
+ results = results.filter((r) => r.similarity >= adaptiveThreshold);
2161
+ // Stage 1.4: Temporal boost — detect time-related queries and boost matching results
2162
+ {
2163
+ const temporalPatterns = [
2164
+ // English
2165
+ /\b(yesterday|today|last\s+(?:week|month|year)|(\d+)\s+(?:days?|weeks?|months?)\s+ago)\b/i,
2166
+ /\b(before|after|since|until|during)\s+\w+/i,
2167
+ /\b(how\s+long|when\s+did|what\s+date|what\s+day)\b/i,
2168
+ // Korean
2169
+ /(?:어제|오늘|그제|지난\s*(?:주|달|해)|(\d+)\s*(?:일|주|달|개월)\s*(?:전|후|뒤))/, // Korean: temporal query detection
2170
+ /(?:언제|얼마나|며칠|몇\s*(?:일|주|달|개월))/, // Korean: temporal query detection
2171
+ ];
2172
+ const isTemporalQuery = temporalPatterns.some((p) => p.test(userQuestion));
2173
+ if (isTemporalQuery && results.length > 0) {
2174
+ // Boost results that contain date/time references in their content
2175
+ const datePatterns = [
2176
+ /\d{4}[-/]\d{1,2}[-/]\d{1,2}/,
2177
+ /(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)\w*\s+\d/i,
2178
+ /\d+\s*(?:일|월|년|주|시간|분)/, // Korean: date reference in content
2179
+ /(?:monday|tuesday|wednesday|thursday|friday|saturday|sunday)/i,
2180
+ /(?:월요일|화요일|수요일|목요일|금요일|토요일|일요일)/, // Korean: date reference in content
2181
+ ];
2182
+ for (const result of results) {
2183
+ const content = `${result.decision || ''} ${result.reasoning || ''}`;
2184
+ const hasDateRef = datePatterns.some((p) => p.test(content));
2185
+ if (hasDateRef) {
2186
+ result.similarity = Math.min(1.0, (result.similarity || 0) + 0.1);
2187
+ }
2188
+ }
2189
+ results.sort((a, b) => (b.similarity || 0) - (a.similarity || 0));
2190
+ }
2191
+ }
2192
+ // Stage 1.5: Apply recency boosting (Gaussian Decay)
2193
+ // Allows Claude to adjust search strategy (recent vs historical)
2194
+ if (results.length > 0 && !disableRecency) {
2195
+ results = applyRecencyBoost(results, {
2196
+ recencyWeight,
2197
+ recencyScale,
2198
+ recencyDecay,
2199
+ disableRecency,
2200
+ });
2201
+ searchMethod = 'vector+recency';
2202
+ }
2203
+ // Stage 1.7: FTS5 hybrid merge (Haiku Memory Layer)
2204
+ {
2205
+ try {
2206
+ const ftsResults = await (0, search_js_1.fts5Search)(adapter, userQuestion, rerankPoolLimit * 2, kind);
2207
+ if (ftsResults.length > 0) {
2208
+ // Normalize FTS5 ranks (BM25 returns negative values, closer to 0 = better)
2209
+ const maxRank = Math.max(...ftsResults.map((r) => Math.abs(r.rank)));
2210
+ const ftsMap = new Map(ftsResults.map((r) => [r.id, maxRank > 0 ? 1 - Math.abs(r.rank) / maxRank : 0.5]));
2211
+ // Tunable hybrid weights (env: MAMA_VECTOR_WEIGHT, MAMA_FTS5_WEIGHT)
2212
+ const vectorWeight = parseFloat(process.env.MAMA_VECTOR_WEIGHT || '0.6');
2213
+ const fts5Weight = parseFloat(process.env.MAMA_FTS5_WEIGHT || '0.4');
2214
+ // Merge: boost existing results that also matched FTS5
2215
+ for (const result of results) {
2216
+ const ftsScore = ftsMap.get(result.id);
2217
+ if (ftsScore !== undefined) {
2218
+ result.similarity = vectorWeight * result.similarity + fts5Weight * ftsScore;
2219
+ ftsMap.delete(result.id);
2220
+ }
2221
+ }
2222
+ // Add FTS5-only results (not in embedding results)
2223
+ for (const [id, ftsScore] of ftsMap) {
2224
+ const ftsResult = ftsResults.find((r) => r.id === id);
2225
+ if (ftsResult) {
2226
+ // Need to get full decision record
2227
+ const stmt = adapter.prepare('SELECT * FROM decisions WHERE id = ? AND superseded_by IS NULL');
2228
+ const decision = stmt.get(id);
2229
+ if (decision && (kind === undefined || decision.kind === kind)) {
2230
+ results.push({
2231
+ ...decision,
2232
+ similarity: fts5Weight * ftsScore, // Only FTS5 score component
2233
+ graph_source: 'fts5',
2234
+ });
2235
+ }
2236
+ }
2237
+ }
2238
+ // Re-sort by similarity
2239
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2240
+ results.sort((a, b) => (b.similarity || 0) - (a.similarity || 0));
2241
+ searchMethod = disableRecency ? 'vector+fts5' : 'vector+recency+fts5';
2242
+ }
2243
+ }
2244
+ catch {
2245
+ // FTS5 not available, continue with embedding-only results
2246
+ }
2247
+ }
2248
+ // Stage 2: Graph expansion (NEW - Phase 1)
2249
+ // Expand candidates with supersedes chain and semantic edges
2250
+ if (results.length > 0) {
2251
+ const graphEnhanced = await expandWithGraphInAdapter(adapter, results);
2252
+ results = graphEnhanced;
2253
+ searchMethod = disableRecency ? 'vector+graph' : 'vector+recency+graph';
2254
+ }
2255
+ // Stage 2.5: is_static boost (after graph expansion to preserve sort order)
2256
+ for (const result of results) {
2257
+ if (result.is_static === 1) {
2258
+ result.final_score = Math.min(1.0, (result.final_score ?? result.similarity ?? 0) + 0.2);
2259
+ }
2260
+ }
2261
+ // Re-sort by final_score after is_static boost
2262
+ results.sort((a, b) => (b.final_score ?? b.similarity ?? 0) - (a.final_score ?? a.similarity ?? 0));
2263
+ }
2264
+ catch (vectorError) {
2265
+ // Fallback to keyword search if vector search unavailable
2266
+ (0, debug_logger_js_1.warn)(`Vector search failed: ${vectorError instanceof Error ? vectorError.message : String(vectorError)}, falling back to keyword search`);
2267
+ searchMethod = 'keyword';
2268
+ // Keyword search fallback
2269
+ const keywords = userQuestion
2270
+ .toLowerCase()
2271
+ .split(/\s+/)
2272
+ .filter((w) => w.length > 2); // Filter short words
2273
+ if (keywords.length === 0) {
2274
+ if (format === 'markdown') {
2275
+ return `💡 Hint: Please be more specific.\nExample: "Railway Volume settings" or "mesh parameter optimization"`;
2276
+ }
2277
+ return null; // JSON mode returns null for empty/invalid queries
2278
+ }
2279
+ // Build LIKE query for each keyword
2280
+ const likeConditions = keywords.map(() => '(topic LIKE ? OR decision LIKE ?)').join(' OR ');
2281
+ const likeParams = keywords.flatMap((k) => [`%${k}%`, `%${k}%`]);
2282
+ const kindClause = kind === undefined ? '' : 'AND kind = ?';
2283
+ const stmt = adapter.prepare(`
2284
+ SELECT * FROM decisions
2285
+ WHERE ${likeConditions}
2286
+ AND superseded_by IS NULL
2287
+ ${kindClause}
2288
+ ORDER BY created_at DESC
2289
+ LIMIT ?
2290
+ `);
2291
+ const rows = (await stmt.all(...likeParams, ...(kind === undefined ? [] : [kind]), rerankPoolLimit));
2292
+ results = rows.map((row) => ({
2293
+ ...row,
2294
+ similarity: 0.75, // Assign moderate similarity for keyword matches
2295
+ }));
2296
+ // Stage 2: Graph expansion for keyword results (Phase 1)
2297
+ if (results.length > 0) {
2298
+ const graphEnhanced = await expandWithGraphInAdapter(adapter, results);
2299
+ results = graphEnhanced;
2300
+ searchMethod = 'keyword+graph';
2301
+ }
1888
2302
  }
1889
- catch (err) {
1890
- (0, debug_logger_js_1.warn)(`[memory] failed to save extracted unit topic=${unit.topic} kind=${unit.kind}: ${err instanceof Error ? err.message : String(err)}`);
2303
+ if (results.length === 0) {
2304
+ if (format === 'markdown') {
2305
+ const wordCount = userQuestion.split(/\s+/).length;
2306
+ if (wordCount < 3) {
2307
+ return `💡 Hint: Please be more specific.\nExample: "Why did we choose COMPLEX mesh structure?" or "What parameters are used for large layers?"`;
2308
+ }
2309
+ }
2310
+ return null;
1891
2311
  }
2312
+ // 5. Optional: LLM re-ranking (only if requested)
2313
+ if (useReranking) {
2314
+ results = await rerankWithLLM(options.runner, userQuestion, results);
2315
+ }
2316
+ const rerankCandidateResults = results.slice(0, rerankPoolLimit);
2317
+ const vectorRows = rerankCandidateResults.map((r) => ({
2318
+ id: r.id,
2319
+ topic: r.topic,
2320
+ decision: r.decision,
2321
+ reasoning: r.reasoning,
2322
+ confidence: r.confidence,
2323
+ similarity: r.similarity,
2324
+ created_at: r.created_at,
2325
+ event_date: r.event_date ?? null,
2326
+ event_datetime: r.event_datetime ?? null,
2327
+ kind: r.kind ?? 'decision',
2328
+ // Recency metadata (NEW - Gaussian Decay)
2329
+ recency_score: r.recency_score,
2330
+ recency_age_days: r.recency_age_days,
2331
+ final_score: r.final_score || r.similarity, // Falls back to similarity if no recency
2332
+ retrieval_score: r.similarity ?? null,
2333
+ // Graph metadata (NEW - Phase 1)
2334
+ graph_source: r.graph_source || 'primary',
2335
+ graph_rank: r.graph_rank || 1.0,
2336
+ related_to: r.related_to || null,
2337
+ edge_reason: r.edge_reason || null,
2338
+ case_id: null,
2339
+ source_type: 'decision',
2340
+ }));
2341
+ const { results: rankedVectorRows, meta: rankerMeta } = applyLearnedRanker(vectorRows);
2342
+ const finalResults = rankedVectorRows.slice(0, limit);
2343
+ // Markdown format (for human display)
2344
+ if (format === 'markdown') {
2345
+ const context = (0, decision_formatter_js_1.formatContext)(finalResults, { maxTokens: 500 });
2346
+ // Add graph expansion summary if applicable
2347
+ let graphSummary = '';
2348
+ if (searchMethod.includes('graph')) {
2349
+ const primaryCount = finalResults.filter((r) => r.graph_source === 'primary').length;
2350
+ const expandedCount = finalResults.filter((r) => r.graph_source !== 'primary').length;
2351
+ graphSummary = `\n📊 Graph expansion: ${primaryCount} primary + ${expandedCount} related (supersedes/refines/contradicts)\n`;
2352
+ }
2353
+ return `🔍 Search method: ${searchMethod}${graphSummary}\n${context}`;
2354
+ }
2355
+ // Calculate graph expansion stats
2356
+ const graphStats = {
2357
+ total_results: finalResults.length,
2358
+ primary_count: finalResults.filter((r) => r.graph_source === 'primary').length,
2359
+ expanded_count: finalResults.filter((r) => r.graph_source !== 'primary').length,
2360
+ sources: {
2361
+ primary: finalResults.filter((r) => r.graph_source === 'primary').length,
2362
+ supersedes_chain: finalResults.filter((r) => r.graph_source === 'supersedes_chain').length,
2363
+ refines: finalResults.filter((r) => r.graph_source === 'refines').length,
2364
+ refined_by: finalResults.filter((r) => r.graph_source === 'refined_by').length,
2365
+ contradicts: finalResults.filter((r) => r.graph_source === 'contradicts').length,
2366
+ },
2367
+ };
2368
+ return {
2369
+ query: userQuestion,
2370
+ results: finalResults,
2371
+ meta: {
2372
+ count: finalResults.length,
2373
+ search_method: searchMethod,
2374
+ threshold: threshold || 'adaptive',
2375
+ // Recency boosting config (NEW - Gaussian Decay)
2376
+ recency_boost: disableRecency
2377
+ ? null
2378
+ : {
2379
+ weight: recencyWeight,
2380
+ scale: recencyScale,
2381
+ decay: recencyDecay,
2382
+ },
2383
+ // Graph expansion stats (NEW - Phase 1)
2384
+ graph_expansion: searchMethod.includes('graph') ? graphStats : null,
2385
+ ranker: rankerMeta,
2386
+ },
2387
+ };
2388
+ }
2389
+ catch (error) {
2390
+ // Graceful degradation
2391
+ (0, debug_logger_js_1.warn)(`mama.suggest() failed: ${error instanceof Error ? error.message : String(error)}`);
2392
+ return null;
2393
+ }
2394
+ }
2395
+ /**
2396
+ * Re-rank search results using local LLM (optional enhancement)
2397
+ *
2398
+ * @param {string} userQuestion - User's question
2399
+ * @param {Array} results - Vector search results
2400
+ * @returns {Promise<Array>} Re-ranked results
2401
+ */
2402
+ async function rerankWithLLM(runner, userQuestion,
2403
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2404
+ results
2405
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2406
+ ) {
2407
+ if (!runner) {
2408
+ // No model was stated for this runtime, so there is nothing to ask. The
2409
+ // ranking already computed stands, which is what an unreachable model
2410
+ // produced before — said once, here, rather than discovered as a failure.
2411
+ (0, debug_logger_js_1.warn)('Re-ranking skipped: this runtime states no model runner; using vector ranking');
2412
+ return results;
2413
+ }
2414
+ try {
2415
+ const prompt = `User asked: "${userQuestion}"
2416
+
2417
+ Found decisions (ranked by vector similarity):
2418
+ ${results.map((r, i) => `${i + 1}. [${(r.similarity ?? 0).toFixed(3)}] ${r.topic}: ${r.decision.substring(0, 60)}...`).join('\n')}
2419
+
2420
+ Re-rank these by actual relevance to the user's intent (not just keyword similarity).
2421
+ Return JSON: { "ranking": [index1, index2, ...] } (0-based indices)
2422
+
2423
+ Example: { "ranking": [2, 0, 4, 1, 3] } means 3rd is most relevant, then 1st, then 5th...`;
2424
+ const response = await runner(prompt, {
2425
+ format: 'json',
2426
+ temperature: 0.3,
2427
+ maxTokens: 100,
2428
+ timeoutMs: 3000,
2429
+ });
2430
+ const parsed = typeof response === 'string' ? JSON.parse(response) : response;
2431
+ // Reorder results based on LLM ranking
2432
+ return parsed.ranking.map((idx) => results[idx]).filter(Boolean);
1892
2433
  }
1893
- return result;
2434
+ catch (error) {
2435
+ (0, debug_logger_js_1.warn)(`Re-ranking failed: ${error instanceof Error ? error.message : String(error)}, using vector ranking`);
2436
+ return results; // Fallback to vector ranking
2437
+ }
2438
+ }
2439
+ /** `LIKE ? ESCAPE '\\'` pattern that matches topics starting with `prefix`, metacharacters literal. */
2440
+ function topicPrefixLikePattern(prefix) {
2441
+ return `${prefix.replace(/[\\%_]/g, (ch) => `\\${ch}`)}%`;
1894
2442
  }
1895
- async function ingestConversation(input) {
1896
- return ingestConversationInternal((0, provenance_js_1.sanitizePublicIngestConversationInput)(input));
2443
+ async function listDecisionsInAdapter(adapter, options = {}) {
2444
+ const { limit = 10, format = 'json' } = options;
2445
+ try {
2446
+ let decisions;
2447
+ const topicPrefix = typeof options.topicPrefix === 'string' ? options.topicPrefix.trim() : '';
2448
+ // A prefix read keeps superseded rows: they are the item's earlier rounds.
2449
+ const currency = topicPrefix ? '' : 'AND d.superseded_by IS NULL';
2450
+ const kindClause = options.kind === undefined ? '' : 'AND d.kind = ?';
2451
+ const prefixClause = topicPrefix ? "AND d.topic LIKE ? ESCAPE '\\'" : '';
2452
+ const prefixParams = topicPrefix ? [topicPrefixLikePattern(topicPrefix)] : [];
2453
+ const kindParams = options.kind === undefined ? [] : [options.kind];
2454
+ if (options.scopes && options.scopes.length > 0) {
2455
+ // Scope-filtered query: JOIN memory_scope_bindings + memory_scopes
2456
+ const scopeIds = await Promise.all(options.scopes.map((s) => (0, db_manager_js_1.ensureMemoryScope)(adapter, s.kind, s.id)));
2457
+ const placeholders = scopeIds.map(() => '?').join(', ');
2458
+ const stmt = adapter.prepare(`
2459
+ SELECT DISTINCT d.* FROM decisions d
2460
+ JOIN memory_scope_bindings msb ON msb.memory_id = d.id
2461
+ WHERE msb.scope_id IN (${placeholders})
2462
+ ${currency}
2463
+ ${kindClause}
2464
+ ${prefixClause}
2465
+ ORDER BY COALESCE(d.event_datetime, d.created_at) DESC, d.created_at DESC
2466
+ LIMIT ?
2467
+ `);
2468
+ decisions = await stmt.all(...scopeIds, ...kindParams, ...prefixParams, limit);
2469
+ }
2470
+ else {
2471
+ const stmt = adapter.prepare(`
2472
+ SELECT d.* FROM decisions d
2473
+ WHERE 1 = 1
2474
+ ${currency}
2475
+ ${kindClause}
2476
+ ${prefixClause}
2477
+ ORDER BY COALESCE(d.event_datetime, d.created_at) DESC, d.created_at DESC
2478
+ LIMIT ?
2479
+ `);
2480
+ decisions = await stmt.all(...kindParams, ...prefixParams, limit);
2481
+ }
2482
+ if (format === 'markdown') {
2483
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2484
+ return (0, decision_formatter_js_1.formatList)(decisions);
2485
+ }
2486
+ return decisions;
2487
+ }
2488
+ catch (error) {
2489
+ throw new Error(`mama.listDecisions() failed: ${error instanceof Error ? error.message : String(error)}`);
2490
+ }
1897
2491
  }
1898
- async function ingestConversationWithTrustedProvenance(input, options) {
1899
- return ingestConversationInternal((0, provenance_js_1.sanitizePublicIngestConversationInput)(input), options);
2492
+ async function updateOutcomeInAdapter(adapter, decisionId, { outcome, failure_reason, limitation }) {
2493
+ if (!decisionId || typeof decisionId !== 'string') {
2494
+ throw new Error('mama.updateOutcome() requires decisionId (string)');
2495
+ }
2496
+ // AX Improvement: Be forgiving with case sensitivity
2497
+ const normalizedOutcome = outcome ? outcome.toUpperCase() : null;
2498
+ if (!normalizedOutcome || !['SUCCESS', 'FAILED', 'PARTIAL'].includes(normalizedOutcome)) {
2499
+ throw new Error('mama.updateOutcome() outcome must be "SUCCESS", "FAILED", or "PARTIAL"');
2500
+ }
2501
+ try {
2502
+ // Append-only: one judgment record carries the outcome change; the
2503
+ // maintained decisions projection columns move in the same transaction.
2504
+ await (0, write_adapters_js_1.appendOutcomeAmendment)(decisionId, {
2505
+ outcome: normalizedOutcome,
2506
+ failureReason: failure_reason || null,
2507
+ limitation: limitation || null,
2508
+ eventReason: `mama.updateOutcome(${decisionId})`,
2509
+ }, { adapter });
2510
+ return;
2511
+ }
2512
+ catch (error) {
2513
+ throw new Error(`mama.updateOutcome() failed: ${error instanceof Error ? error.message : String(error)}`);
2514
+ }
2515
+ }
2516
+ /**
2517
+ * Counts over the admitted corpus.
2518
+ *
2519
+ * Every count answers the same question the scope filter answers for a listing:
2520
+ * a decision this caller may not read is not a decision it may count. An
2521
+ * admitted-empty caller counts zero rather than counting everything — the same
2522
+ * fail-closed reading the listing gives.
2523
+ */
2524
+ async function readMemoryStatsInAdapter(adapter, scopes) {
2525
+ const empty = {
2526
+ total: 0,
2527
+ thisWeek: 0,
2528
+ thisMonth: 0,
2529
+ checkpoints: 0,
2530
+ outcomes: {},
2531
+ topTopics: [],
2532
+ };
2533
+ if (scopes.length === 0) {
2534
+ return empty;
2535
+ }
2536
+ const scopeIds = await Promise.all(scopes.map((scope) => (0, db_manager_js_1.ensureMemoryScope)(adapter, scope.kind, scope.id)));
2537
+ const placeholders = scopeIds.map(() => '?').join(', ');
2538
+ const admitted = `
2539
+ JOIN memory_scope_bindings msb ON msb.memory_id = d.id
2540
+ WHERE msb.scope_id IN (${placeholders})
2541
+ `;
2542
+ const now = Date.now();
2543
+ const countSince = async (since) => {
2544
+ const stmt = adapter.prepare(`
2545
+ SELECT COUNT(DISTINCT d.id) as count FROM decisions d
2546
+ ${admitted}
2547
+ ${since === undefined ? '' : 'AND d.created_at > ?'}
2548
+ `);
2549
+ const row = (await stmt.get(...scopeIds, ...(since === undefined ? [] : [since])));
2550
+ return row?.count ?? 0;
2551
+ };
2552
+ const day = 24 * 60 * 60 * 1000;
2553
+ const [total, thisWeek, thisMonth] = await Promise.all([
2554
+ countSince(),
2555
+ countSince(now - 7 * day),
2556
+ countSince(now - 30 * day),
2557
+ ]);
2558
+ const outcomeRows = (await adapter
2559
+ .prepare(`SELECT d.outcome as outcome, COUNT(DISTINCT d.id) as count FROM decisions d
2560
+ ${admitted} AND d.outcome IS NOT NULL
2561
+ GROUP BY d.outcome`)
2562
+ .all(...scopeIds));
2563
+ const outcomes = {};
2564
+ for (const row of outcomeRows) {
2565
+ outcomes[row.outcome?.toLowerCase() ?? 'unknown'] = row.count;
2566
+ }
2567
+ const topTopics = (await adapter
2568
+ .prepare(`SELECT d.topic as topic, COUNT(DISTINCT d.id) as count FROM decisions d
2569
+ ${admitted} AND d.topic IS NOT NULL
2570
+ GROUP BY d.topic
2571
+ ORDER BY count DESC
2572
+ LIMIT 5`)
2573
+ .all(...scopeIds));
2574
+ // Checkpoints carry no scope binding, and `memory.checkpoint.load` reads them
2575
+ // under the same authority: the count is of what that read can reach.
2576
+ const checkpointRow = (await adapter
2577
+ .prepare('SELECT COUNT(*) as count FROM checkpoints')
2578
+ .get());
2579
+ return {
2580
+ total,
2581
+ thisWeek,
2582
+ thisMonth,
2583
+ checkpoints: checkpointRow?.count ?? 0,
2584
+ outcomes,
2585
+ topTopics,
2586
+ };
2587
+ }
2588
+ /**
2589
+ * Save current session checkpoint (New Feature: Session Continuity)
2590
+ *
2591
+ * @param {string} summary - Summary of current session state
2592
+ * @param {Array<string>} openFiles - List of currently open files
2593
+ * @param {string} nextSteps - Next steps to be taken
2594
+ * @returns {Promise<number>} Checkpoint ID
2595
+ */
2596
+ async function saveCheckpointInAdapter(adapter, summary, openFiles = [], nextSteps = '',
2597
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2598
+ recentConversation = []) {
2599
+ if (!summary) {
2600
+ throw new Error('Summary is required for checkpoint');
2601
+ }
2602
+ const scan = (0, secret_filter_js_1.scanMemoryWriteInput)({
2603
+ summary,
2604
+ open_files: openFiles,
2605
+ next_steps: nextSteps,
2606
+ recent_conversation: recentConversation,
2607
+ });
2608
+ if (!scan.clean)
2609
+ throw new secret_filter_js_1.SecretMaterialRefusedError(scan.matches);
2610
+ if (scan.warnings.length > 0)
2611
+ (0, debug_logger_js_2.warn)(`Checkpoint content warnings: ${scan.warnings.join(', ')}`);
2612
+ try {
2613
+ const stmt = adapter.prepare(`
2614
+ INSERT INTO checkpoints (timestamp, summary, open_files, next_steps, recent_conversation, status)
2615
+ VALUES (?, ?, ?, ?, ?, 'active')
2616
+ `);
2617
+ const result = stmt.run(Date.now(), summary, JSON.stringify(openFiles), nextSteps, JSON.stringify(recentConversation || []));
2618
+ return result.lastInsertRowid;
2619
+ }
2620
+ catch (error) {
2621
+ throw new Error(`Failed to save checkpoint: ${error instanceof Error ? error.message : String(error)}`);
2622
+ }
2623
+ }
2624
+ async function loadCheckpointInAdapter(adapter) {
2625
+ try {
2626
+ const stmt = adapter.prepare(`
2627
+ SELECT * FROM checkpoints
2628
+ WHERE status = 'active'
2629
+ ORDER BY timestamp DESC
2630
+ LIMIT 1
2631
+ `);
2632
+ const checkpoint = stmt.get();
2633
+ if (checkpoint) {
2634
+ try {
2635
+ checkpoint.open_files =
2636
+ typeof checkpoint.open_files === 'string'
2637
+ ? JSON.parse(checkpoint.open_files)
2638
+ : checkpoint.open_files || [];
2639
+ }
2640
+ catch {
2641
+ checkpoint.open_files = [];
2642
+ }
2643
+ try {
2644
+ checkpoint.recent_conversation =
2645
+ typeof checkpoint.recent_conversation === 'string'
2646
+ ? JSON.parse(checkpoint.recent_conversation || '[]')
2647
+ : checkpoint.recent_conversation || [];
2648
+ }
2649
+ catch {
2650
+ checkpoint.recent_conversation = [];
2651
+ }
2652
+ }
2653
+ return checkpoint || null;
2654
+ }
2655
+ catch (error) {
2656
+ throw new Error(`Failed to load checkpoint: ${error instanceof Error ? error.message : String(error)}`);
2657
+ }
2658
+ }
2659
+ async function listCheckpointsInAdapter(adapter, limit = 10) {
2660
+ try {
2661
+ const stmt = adapter.prepare(`
2662
+ SELECT * FROM checkpoints
2663
+ ORDER BY timestamp DESC
2664
+ LIMIT ?
2665
+ `);
2666
+ const checkpoints = stmt.all(limit);
2667
+ return checkpoints.map((c) => {
2668
+ try {
2669
+ c.open_files =
2670
+ typeof c.open_files === 'string' ? JSON.parse(c.open_files) : c.open_files || [];
2671
+ }
2672
+ catch {
2673
+ c.open_files = [];
2674
+ }
2675
+ try {
2676
+ c.recent_conversation =
2677
+ typeof c.recent_conversation === 'string'
2678
+ ? JSON.parse(c.recent_conversation)
2679
+ : c.recent_conversation || [];
2680
+ }
2681
+ catch {
2682
+ c.recent_conversation = [];
2683
+ }
2684
+ return c;
2685
+ });
2686
+ }
2687
+ catch (error) {
2688
+ throw new Error(`Failed to list checkpoints: ${error instanceof Error ? error.message : String(error)}`);
2689
+ }
1900
2690
  }
1901
2691
  //# sourceMappingURL=api.js.map