@jungjaehoon/mama-core 2.4.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (371) 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/dist/action-contracts.d.ts +212 -0
  39. package/dist/action-contracts.js +10 -0
  40. package/dist/api/catalog.d.ts +118 -0
  41. package/dist/api/catalog.js +1756 -0
  42. package/dist/api/dispatch.d.ts +76 -0
  43. package/dist/api/dispatch.js +372 -0
  44. package/dist/canonicalize.d.ts +2 -2
  45. package/dist/canonicalize.js +3 -3
  46. package/dist/client/client.d.ts +33 -0
  47. package/dist/client/client.js +123 -0
  48. package/dist/client/ipc.d.ts +81 -0
  49. package/dist/client/ipc.js +314 -0
  50. package/dist/db-adapter/base-adapter.d.ts +6 -66
  51. package/dist/db-adapter/base-adapter.js +6 -6
  52. package/dist/db-adapter/index.d.ts +5 -5
  53. package/dist/db-adapter/index.js +2 -5
  54. package/dist/db-adapter/node-sqlite-adapter.d.ts +85 -15
  55. package/dist/db-adapter/node-sqlite-adapter.js +1968 -570
  56. package/dist/db-manager.d.ts +53 -97
  57. package/dist/db-manager.js +121 -603
  58. package/dist/embedding/embedder.d.ts +132 -0
  59. package/dist/embedding/embedder.js +299 -0
  60. package/dist/identity/principal-repository.d.ts +2 -2
  61. package/dist/index.d.ts +43 -58
  62. package/dist/index.js +97 -145
  63. package/dist/{context-compile/channel-grant.d.ts → knowledge/access.d.ts} +12 -17
  64. package/dist/knowledge/access.js +663 -0
  65. package/dist/knowledge/case-errors.d.ts +31 -0
  66. package/dist/knowledge/case-errors.js +40 -0
  67. package/dist/{cases/search-rollup.d.ts → knowledge/case-search-rollup.d.ts} +5 -4
  68. package/dist/{cases/search-rollup.js → knowledge/case-search-rollup.js} +6 -8
  69. package/dist/{cases/store.d.ts → knowledge/case-store.d.ts} +3 -6
  70. package/dist/{cases/store.js → knowledge/case-store.js} +7 -42
  71. package/dist/{cases/timeline-range.d.ts → knowledge/case-timeline-range.d.ts} +3 -3
  72. package/dist/{cases/timeline-range.js → knowledge/case-timeline-range.js} +47 -25
  73. package/dist/{cases/types.d.ts → knowledge/case-types.d.ts} +8 -2
  74. package/dist/{cases/types.js → knowledge/case-types.js} +1 -1
  75. package/dist/knowledge/commitments.d.ts +188 -0
  76. package/dist/knowledge/commitments.js +370 -0
  77. package/dist/knowledge/decision-edges.d.ts +70 -0
  78. package/dist/knowledge/decision-edges.js +123 -0
  79. package/dist/{agent-graph/types.d.ts → knowledge/graph-query.d.ts} +80 -70
  80. package/dist/knowledge/graph-query.js +1585 -0
  81. package/dist/knowledge/identity.d.ts +16 -0
  82. package/dist/knowledge/identity.js +22 -0
  83. package/dist/knowledge/index.d.ts +47 -0
  84. package/dist/knowledge/index.js +90 -0
  85. package/dist/knowledge/judgments.d.ts +118 -0
  86. package/dist/knowledge/judgments.js +808 -0
  87. package/dist/knowledge/observations.d.ts +160 -0
  88. package/dist/knowledge/observations.js +449 -0
  89. package/dist/knowledge/question-type.d.ts +34 -0
  90. package/dist/knowledge/question-type.js +70 -0
  91. package/dist/{search → knowledge}/ranker-features.js +1 -1
  92. package/dist/{search → knowledge}/search-quality.d.ts +0 -1
  93. package/dist/knowledge/search.d.ts +38 -0
  94. package/dist/knowledge/search.js +91 -0
  95. package/dist/knowledge/source-ingest.d.ts +65 -0
  96. package/dist/knowledge/source-ingest.js +174 -0
  97. package/dist/{edges/types.d.ts → knowledge/twin-edge-types.d.ts} +31 -4
  98. package/dist/{edges/types.js → knowledge/twin-edge-types.js} +13 -2
  99. package/dist/knowledge/work-dates.d.ts +49 -0
  100. package/dist/knowledge/work-dates.js +120 -0
  101. package/dist/mama-api.d.ts +114 -778
  102. package/dist/mama-api.js +313 -2461
  103. package/dist/memory/api.d.ts +264 -24
  104. package/dist/memory/api.js +1633 -843
  105. package/dist/memory/bootstrap-builder.d.ts +2 -1
  106. package/dist/memory/bootstrap-builder.js +5 -5
  107. package/dist/memory/channel-summary-state-store.d.ts +3 -2
  108. package/dist/memory/channel-summary-state-store.js +53 -51
  109. package/dist/memory/channel-summary-store.d.ts +5 -3
  110. package/dist/memory/channel-summary-store.js +3 -8
  111. package/dist/memory/dashboard-read.d.ts +46 -0
  112. package/dist/memory/dashboard-read.js +94 -0
  113. package/dist/memory/event-store.d.ts +3 -3
  114. package/dist/memory/event-store.js +3 -10
  115. package/dist/memory/finding-store.d.ts +3 -2
  116. package/dist/memory/finding-store.js +2 -7
  117. package/dist/memory/graph-read.d.ts +52 -0
  118. package/dist/memory/graph-read.js +130 -0
  119. package/dist/memory/judgment-types.d.ts +394 -0
  120. package/dist/memory/judgment-types.js +16 -0
  121. package/dist/memory/provenance-audit.d.ts +5 -2
  122. package/dist/memory/provenance-audit.js +8 -14
  123. package/dist/memory/provenance-live.d.ts +141 -0
  124. package/dist/memory/provenance-live.js +409 -0
  125. package/dist/memory/provenance-query.d.ts +5 -4
  126. package/dist/memory/provenance-query.js +21 -29
  127. package/dist/memory/provenance-resolver.d.ts +191 -0
  128. package/dist/memory/provenance-resolver.js +155 -0
  129. package/dist/memory/provenance.d.ts +1 -11
  130. package/dist/memory/provenance.js +2 -33
  131. package/dist/memory/recall-sanitize.d.ts +55 -0
  132. package/dist/memory/recall-sanitize.js +144 -0
  133. package/dist/memory/secret-filter.d.ts +32 -0
  134. package/dist/memory/secret-filter.js +126 -0
  135. package/dist/memory/truth-store.d.ts +2 -1
  136. package/dist/memory/truth-store.js +1 -4
  137. package/dist/memory/types.d.ts +74 -24
  138. package/dist/memory/types.js +75 -5
  139. package/dist/memory/write-adapters.d.ts +98 -0
  140. package/dist/memory/write-adapters.js +242 -0
  141. package/dist/operations/owner-action-effects.d.ts +53 -0
  142. package/dist/operations/owner-action-effects.js +96 -0
  143. package/dist/provenance/source-ref.d.ts +1 -1
  144. package/dist/provenance/source-ref.js +0 -2
  145. package/dist/registry/corrections.d.ts +31 -0
  146. package/dist/registry/corrections.js +332 -0
  147. package/dist/registry/record-identity.d.ts +49 -0
  148. package/dist/registry/record-identity.js +133 -0
  149. package/dist/registry/store.d.ts +101 -0
  150. package/dist/registry/store.js +395 -0
  151. package/dist/registry/types.d.ts +28 -0
  152. package/dist/registry/types.js +5 -0
  153. package/dist/relevance-scorer.js +2 -2
  154. package/dist/runtime/agent-event-bus.d.ts +50 -0
  155. package/dist/runtime/agent-event-bus.js +101 -0
  156. package/dist/runtime/drivers/claude-cli-wrapper.d.ts +155 -0
  157. package/dist/runtime/drivers/claude-cli-wrapper.js +392 -0
  158. package/dist/runtime/drivers/cli-arg-redaction.d.ts +2 -0
  159. package/dist/runtime/drivers/cli-arg-redaction.js +18 -0
  160. package/dist/runtime/drivers/cli-secret-redaction.d.ts +17 -0
  161. package/dist/runtime/drivers/cli-secret-redaction.js +126 -0
  162. package/dist/runtime/drivers/codex-app-server-process.d.ts +277 -0
  163. package/dist/runtime/drivers/codex-app-server-process.js +2342 -0
  164. package/dist/runtime/drivers/codex-auxiliary-tools.d.ts +17 -0
  165. package/dist/runtime/drivers/codex-auxiliary-tools.js +267 -0
  166. package/dist/runtime/drivers/codex-home.d.ts +20 -0
  167. package/dist/runtime/drivers/codex-home.js +674 -0
  168. package/dist/runtime/drivers/codex-thread-registry.d.ts +63 -0
  169. package/dist/runtime/drivers/codex-thread-registry.js +433 -0
  170. package/dist/runtime/drivers/persistent-cli-adapter.d.ts +160 -0
  171. package/dist/runtime/drivers/persistent-cli-adapter.js +396 -0
  172. package/dist/runtime/drivers/persistent-cli-process.d.ts +424 -0
  173. package/dist/runtime/drivers/persistent-cli-process.js +1543 -0
  174. package/dist/runtime/drivers/types.d.ts +456 -0
  175. package/dist/runtime/drivers/types.js +110 -0
  176. package/dist/runtime/mailbox.d.ts +223 -0
  177. package/dist/runtime/mailbox.js +582 -0
  178. package/dist/runtime/model-run-store.d.ts +28 -0
  179. package/dist/{model-runs/store.js → runtime/model-run-store.js} +81 -41
  180. package/dist/{model-runs/types.d.ts → runtime/model-run-types.d.ts} +33 -4
  181. package/dist/{model-runs/types.js → runtime/model-run-types.js} +1 -1
  182. package/dist/runtime/native-effect-observer.d.ts +22 -0
  183. package/dist/runtime/native-effect-observer.js +103 -0
  184. package/dist/runtime/native-input-journal.d.ts +46 -0
  185. package/dist/runtime/native-input-journal.js +308 -0
  186. package/dist/runtime/native-prompt.d.ts +118 -0
  187. package/dist/runtime/native-prompt.js +496 -0
  188. package/dist/runtime/native-session.d.ts +10 -0
  189. package/dist/{agent-graph/index.js → runtime/native-session.js} +9 -6
  190. package/dist/runtime/native-tool-trace-observer.d.ts +5 -0
  191. package/dist/runtime/native-tool-trace-observer.js +65 -0
  192. package/dist/runtime/native-turn.d.ts +272 -0
  193. package/dist/runtime/native-turn.js +1041 -0
  194. package/dist/runtime/operations.d.ts +236 -0
  195. package/dist/runtime/operations.js +565 -0
  196. package/dist/runtime/post-tool-handler.d.ts +74 -0
  197. package/dist/runtime/post-tool-handler.js +137 -0
  198. package/dist/runtime/prompt-layers.d.ts +126 -0
  199. package/dist/runtime/prompt-layers.js +239 -0
  200. package/dist/runtime/runtime-process.d.ts +102 -0
  201. package/dist/runtime/runtime-process.js +206 -0
  202. package/dist/runtime/runtime.d.ts +278 -0
  203. package/dist/runtime/runtime.js +563 -0
  204. package/dist/runtime/session-pool.d.ts +176 -0
  205. package/dist/runtime/session-pool.js +369 -0
  206. package/dist/runtime/stimulus-payload.d.ts +5 -0
  207. package/dist/runtime/stimulus-payload.js +50 -0
  208. package/dist/runtime/subagent-bridge.d.ts +24 -0
  209. package/dist/{agent-graph/types.js → runtime/subagent-bridge.js} +1 -1
  210. package/dist/runtime/text-completion.d.ts +22 -0
  211. package/dist/{agent-situation/types.js → runtime/text-completion.js} +1 -1
  212. package/dist/runtime/token-estimator.d.ts +25 -0
  213. package/dist/runtime/token-estimator.js +75 -0
  214. package/dist/runtime/tool-trace-store.d.ts +22 -0
  215. package/dist/{model-runs → runtime}/tool-trace-store.js +46 -27
  216. package/dist/runtime/trace-summary.d.ts +5 -0
  217. package/dist/runtime/trace-summary.js +74 -0
  218. package/dist/runtime/turn-text.d.ts +23 -0
  219. package/dist/runtime/turn-text.js +41 -0
  220. package/dist/storage/database.d.ts +67 -0
  221. package/dist/storage/database.js +158 -0
  222. package/package.json +20 -34
  223. package/dist/agent-graph/alias-write.d.ts +0 -3
  224. package/dist/agent-graph/alias-write.js +0 -234
  225. package/dist/agent-graph/entity-resolve.d.ts +0 -3
  226. package/dist/agent-graph/entity-resolve.js +0 -271
  227. package/dist/agent-graph/errors.d.ts +0 -4
  228. package/dist/agent-graph/errors.js +0 -11
  229. package/dist/agent-graph/graph-query.d.ts +0 -5
  230. package/dist/agent-graph/graph-query.js +0 -360
  231. package/dist/agent-graph/index.d.ts +0 -6
  232. package/dist/agent-situation/builder.d.ts +0 -8
  233. package/dist/agent-situation/builder.js +0 -239
  234. package/dist/agent-situation/cache-key.d.ts +0 -4
  235. package/dist/agent-situation/cache-key.js +0 -64
  236. package/dist/agent-situation/index.d.ts +0 -7
  237. package/dist/agent-situation/index.js +0 -28
  238. package/dist/agent-situation/packet-store.d.ts +0 -27
  239. package/dist/agent-situation/packet-store.js +0 -228
  240. package/dist/agent-situation/ranking-policy.d.ts +0 -18
  241. package/dist/agent-situation/ranking-policy.js +0 -75
  242. package/dist/agent-situation/source-readers.d.ts +0 -52
  243. package/dist/agent-situation/source-readers.js +0 -300
  244. package/dist/agent-situation/types.d.ts +0 -195
  245. package/dist/cases/wiki-page-index.d.ts +0 -37
  246. package/dist/cases/wiki-page-index.js +0 -201
  247. package/dist/config-loader.d.ts +0 -68
  248. package/dist/config-loader.js +0 -239
  249. package/dist/connectors/event-index.d.ts +0 -24
  250. package/dist/connectors/event-index.js +0 -245
  251. package/dist/connectors/raw-query.d.ts +0 -40
  252. package/dist/connectors/raw-query.js +0 -412
  253. package/dist/connectors/types.d.ts +0 -117
  254. package/dist/connectors/types.js +0 -3
  255. package/dist/context-compile/boundary-defaults.d.ts +0 -12
  256. package/dist/context-compile/boundary-defaults.js +0 -105
  257. package/dist/context-compile/channel-grant.js +0 -66
  258. package/dist/context-compile/compiler-policy.d.ts +0 -39
  259. package/dist/context-compile/compiler-policy.js +0 -147
  260. package/dist/context-compile/compiler.d.ts +0 -18
  261. package/dist/context-compile/compiler.js +0 -404
  262. package/dist/context-compile/index.d.ts +0 -10
  263. package/dist/context-compile/index.js +0 -26
  264. package/dist/context-compile/packet-store.d.ts +0 -8
  265. package/dist/context-compile/packet-store.js +0 -239
  266. package/dist/context-compile/ref.d.ts +0 -7
  267. package/dist/context-compile/ref.js +0 -102
  268. package/dist/context-compile/source-readers.d.ts +0 -70
  269. package/dist/context-compile/source-readers.js +0 -840
  270. package/dist/context-compile/types.d.ts +0 -155
  271. package/dist/context-compile/types.js +0 -11
  272. package/dist/context-compile/visibility.d.ts +0 -21
  273. package/dist/context-compile/visibility.js +0 -224
  274. package/dist/decision-tracker.d.ts +0 -206
  275. package/dist/decision-tracker.js +0 -481
  276. package/dist/edges/ref-validation.d.ts +0 -7
  277. package/dist/edges/ref-validation.js +0 -321
  278. package/dist/edges/store.d.ts +0 -8
  279. package/dist/edges/store.js +0 -129
  280. package/dist/embedding-client.d.ts +0 -28
  281. package/dist/embedding-client.js +0 -53
  282. package/dist/embedding-server/index.d.ts +0 -67
  283. package/dist/embedding-server/index.js +0 -405
  284. package/dist/embedding-server/mobile/auth.d.ts +0 -44
  285. package/dist/embedding-server/mobile/auth.js +0 -150
  286. package/dist/embedding-server/mobile/daemon.d.ts +0 -129
  287. package/dist/embedding-server/mobile/daemon.js +0 -313
  288. package/dist/embedding-server/mobile/output-parser.d.ts +0 -115
  289. package/dist/embedding-server/mobile/output-parser.js +0 -241
  290. package/dist/embedding-server/mobile/session-api.d.ts +0 -57
  291. package/dist/embedding-server/mobile/session-api.js +0 -261
  292. package/dist/embedding-server/mobile/session-manager.d.ts +0 -138
  293. package/dist/embedding-server/mobile/session-manager.js +0 -378
  294. package/dist/embedding-server/mobile/websocket-handler.d.ts +0 -137
  295. package/dist/embedding-server/mobile/websocket-handler.js +0 -486
  296. package/dist/embeddings.d.ts +0 -68
  297. package/dist/embeddings.js +0 -244
  298. package/dist/entities/audit-metrics.d.ts +0 -68
  299. package/dist/entities/audit-metrics.js +0 -124
  300. package/dist/entities/candidate-generator.d.ts +0 -8
  301. package/dist/entities/candidate-generator.js +0 -24
  302. package/dist/entities/entity-linked-decision-counts.d.ts +0 -2
  303. package/dist/entities/entity-linked-decision-counts.js +0 -29
  304. package/dist/entities/entity-list.d.ts +0 -22
  305. package/dist/entities/entity-list.js +0 -3
  306. package/dist/entities/entity-orphan-list.d.ts +0 -17
  307. package/dist/entities/entity-orphan-list.js +0 -3
  308. package/dist/entities/entity-search.d.ts +0 -20
  309. package/dist/entities/entity-search.js +0 -3
  310. package/dist/entities/errors.d.ts +0 -55
  311. package/dist/entities/errors.js +0 -100
  312. package/dist/entities/lineage-store.d.ts +0 -28
  313. package/dist/entities/lineage-store.js +0 -204
  314. package/dist/entities/normalization.d.ts +0 -14
  315. package/dist/entities/normalization.js +0 -63
  316. package/dist/entities/policy-store.d.ts +0 -8
  317. package/dist/entities/policy-store.js +0 -75
  318. package/dist/entities/policy-types.d.ts +0 -72
  319. package/dist/entities/policy-types.js +0 -59
  320. package/dist/entities/projection.d.ts +0 -8
  321. package/dist/entities/projection.js +0 -82
  322. package/dist/entities/provenance-query.d.ts +0 -32
  323. package/dist/entities/provenance-query.js +0 -3
  324. package/dist/entities/read-identity.d.ts +0 -31
  325. package/dist/entities/read-identity.js +0 -264
  326. package/dist/entities/recall-bridge.d.ts +0 -5
  327. package/dist/entities/recall-bridge.js +0 -171
  328. package/dist/entities/resolution-engine.d.ts +0 -8
  329. package/dist/entities/resolution-engine.js +0 -3
  330. package/dist/entities/rollback-preview.d.ts +0 -35
  331. package/dist/entities/rollback-preview.js +0 -3
  332. package/dist/entities/source-locator.d.ts +0 -3
  333. package/dist/entities/source-locator.js +0 -32
  334. package/dist/entities/store.d.ts +0 -69
  335. package/dist/entities/store.js +0 -544
  336. package/dist/entities/types.d.ts +0 -166
  337. package/dist/entities/types.js +0 -30
  338. package/dist/memory/extraction-prompt.d.ts +0 -4
  339. package/dist/memory/extraction-prompt.js +0 -89
  340. package/dist/memory/scope-backfill.d.ts +0 -14
  341. package/dist/memory/scope-backfill.js +0 -3
  342. package/dist/memory/scope-store.d.ts +0 -4
  343. package/dist/memory/scope-store.js +0 -9
  344. package/dist/memory-store.d.ts +0 -87
  345. package/dist/memory-store.js +0 -78
  346. package/dist/model-runs/store.d.ts +0 -13
  347. package/dist/model-runs/tool-trace-store.d.ts +0 -8
  348. package/dist/notification-manager.d.ts +0 -7
  349. package/dist/notification-manager.js +0 -12
  350. package/dist/outcome-tracker.d.ts +0 -151
  351. package/dist/outcome-tracker.js +0 -271
  352. package/dist/query-intent.d.ts +0 -23
  353. package/dist/query-intent.js +0 -114
  354. package/dist/runtime/trusted-provenance.d.ts +0 -3
  355. package/dist/runtime/trusted-provenance.js +0 -6
  356. package/dist/search/question-type.d.ts +0 -5
  357. package/dist/search/question-type.js +0 -41
  358. package/dist/test-utils.d.ts +0 -80
  359. package/dist/test-utils.js +0 -218
  360. package/dist/tier-validator.d.ts +0 -43
  361. package/dist/tier-validator.js +0 -159
  362. package/dist/time-formatter.d.ts +0 -25
  363. package/dist/time-formatter.js +0 -93
  364. /package/dist/{search → knowledge}/feedback-store.d.ts +0 -0
  365. /package/dist/{search → knowledge}/feedback-store.js +0 -0
  366. /package/dist/{search → knowledge}/ranker-features.d.ts +0 -0
  367. /package/dist/{search → knowledge}/ranker-rescore.d.ts +0 -0
  368. /package/dist/{search → knowledge}/ranker-rescore.js +0 -0
  369. /package/dist/{search → knowledge}/ranker-trainer.d.ts +0 -0
  370. /package/dist/{search → knowledge}/ranker-trainer.js +0 -0
  371. /package/dist/{search → knowledge}/search-quality.js +0 -0
@@ -0,0 +1,582 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Mailbox = exports.MailboxCorruptionError = void 0;
4
+ exports.isStimulusKind = isStimulusKind;
5
+ const stimulus_payload_js_1 = require("./stimulus-payload.js");
6
+ const native_input_journal_js_1 = require("./native-input-journal.js");
7
+ const STIMULUS_KINDS = new Set([
8
+ 'owner_message',
9
+ 'source_delta',
10
+ 'scheduled',
11
+ 'native_event',
12
+ ]);
13
+ function isStimulusKind(value) {
14
+ return typeof value === 'string' && STIMULUS_KINDS.has(value);
15
+ }
16
+ /** A row the mailbox refused to parse. Parked dead, never silently dropped. */
17
+ class MailboxCorruptionError extends Error {
18
+ rowId;
19
+ code = 'MAILBOX_CORRUPT';
20
+ constructor(rowId, cause) {
21
+ super(`Mailbox row ${rowId} is corrupt`, { cause });
22
+ this.rowId = rowId;
23
+ this.name = 'MailboxCorruptionError';
24
+ }
25
+ }
26
+ exports.MailboxCorruptionError = MailboxCorruptionError;
27
+ /**
28
+ * The backoff and retention schedule, carried over unchanged from the intake
29
+ * this generalises: five attempts, then the row parks dead and visible.
30
+ */
31
+ const MAX_ATTEMPTS = 5;
32
+ const ACKED_RETENTION_MS = 7 * 86_400_000;
33
+ /**
34
+ * Pending rows older than this park as dead so a long provider outage cannot
35
+ * replay months of stale input. Dead rows stay visible in depth().
36
+ */
37
+ const PENDING_RETENTION_MS = 7 * 86_400_000;
38
+ /**
39
+ * Dedupe horizon. Ref ids must outlive their row or a redelivery after pruning
40
+ * re-processes old input — but not forever either (one TEXT-PK row per ref ever
41
+ * accepted). 30 days is far wider than any redelivery gap a cursor produces.
42
+ */
43
+ const REF_RETENTION_MS = 30 * 86_400_000;
44
+ const SEEN_CHUNK = 500; // stay under SQLITE_MAX_VARIABLE_NUMBER
45
+ /**
46
+ * Replay deltas intentionally carry one channel's bounded source history. The
47
+ * feeder preflights this same contract before accepting any window so a
48
+ * large channel cannot be silently split or partially delivered.
49
+ */
50
+ const MAX_REFS = 500;
51
+ function backoffCase(column) {
52
+ return `CASE ${column}
53
+ WHEN 0 THEN 60000
54
+ WHEN 1 THEN 300000
55
+ WHEN 2 THEN 1800000
56
+ WHEN 3 THEN 7200000
57
+ ELSE 43200000
58
+ END`;
59
+ }
60
+ class Mailbox {
61
+ db;
62
+ clock;
63
+ nativeInputs;
64
+ stmtInsertSeen;
65
+ stmtInsert;
66
+ stmtInsertRef;
67
+ stmtRefsFor;
68
+ stmtClaimSelect;
69
+ stmtClaimSelectKind;
70
+ stmtClaimUpdate;
71
+ stmtAck;
72
+ stmtRetry;
73
+ stmtRetryStatus;
74
+ stmtPruneAcked;
75
+ stmtPrunePending;
76
+ stmtPruneSeen;
77
+ stmtReplay;
78
+ stmtDepth;
79
+ stmtPendingCoalesced;
80
+ stmtAppendPreview;
81
+ stmtIdentityPayload;
82
+ constructor(db, clock = () => Date.now()) {
83
+ this.db = db;
84
+ this.clock = clock;
85
+ // Declared by core migrations 086/087. Repeated here for the same reason every
86
+ // core store repeats its own shape: a consumer may open this against a
87
+ // database its own assembly created.
88
+ this.db.exec(`
89
+ CREATE TABLE IF NOT EXISTS mailbox_inputs (
90
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
91
+ stimulus_id TEXT NOT NULL,
92
+ kind TEXT,
93
+ principal_id TEXT NOT NULL,
94
+ channel_key TEXT NOT NULL,
95
+ preview_json TEXT NOT NULL DEFAULT '[]',
96
+ payload_json TEXT,
97
+ reply_to TEXT,
98
+ coalesce_key TEXT,
99
+ occurred_at INTEGER NOT NULL,
100
+ created_at INTEGER NOT NULL,
101
+ status TEXT NOT NULL DEFAULT 'pending'
102
+ CHECK (status IN ('pending','claimed','acked','dead')),
103
+ attempts INTEGER NOT NULL DEFAULT 0,
104
+ last_error TEXT,
105
+ claimed_at INTEGER,
106
+ acked_at INTEGER,
107
+ retry_after INTEGER
108
+ );
109
+ CREATE INDEX IF NOT EXISTS idx_mailbox_inputs_status
110
+ ON mailbox_inputs(status, id);
111
+ CREATE INDEX IF NOT EXISTS idx_mailbox_inputs_coalesce
112
+ ON mailbox_inputs(status, coalesce_key);
113
+ CREATE INDEX IF NOT EXISTS idx_mailbox_inputs_identity
114
+ ON mailbox_inputs(principal_id, stimulus_id);
115
+ CREATE TABLE IF NOT EXISTS mailbox_input_refs (
116
+ input_id INTEGER NOT NULL REFERENCES mailbox_inputs(id) ON DELETE CASCADE,
117
+ ref_id TEXT NOT NULL,
118
+ observation_ref TEXT,
119
+ PRIMARY KEY (input_id, ref_id)
120
+ );
121
+ CREATE TABLE IF NOT EXISTS mailbox_seen (
122
+ ref_id TEXT PRIMARY KEY,
123
+ seen_at INTEGER NOT NULL DEFAULT 0
124
+ );
125
+ CREATE INDEX IF NOT EXISTS idx_mailbox_seen_at ON mailbox_seen(seen_at);
126
+ CREATE TABLE IF NOT EXISTS mailbox_schedules (
127
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
128
+ channel_key TEXT NOT NULL,
129
+ due_at INTEGER NOT NULL,
130
+ created_at INTEGER NOT NULL,
131
+ fired_at INTEGER
132
+ );
133
+ CREATE INDEX IF NOT EXISTS idx_mailbox_schedules_due
134
+ ON mailbox_schedules(fired_at, due_at);
135
+ `);
136
+ this.nativeInputs = new native_input_journal_js_1.NativeInputJournal(db, clock);
137
+ // Prepared once: enqueue runs per producer tick and re-preparing per call
138
+ // was measurable on backfill drains.
139
+ this.stmtInsertSeen = this.db.prepare(`INSERT OR IGNORE INTO mailbox_seen (ref_id, seen_at) VALUES (?, ?)`);
140
+ this.stmtInsert = this.db.prepare(`INSERT INTO mailbox_inputs
141
+ (stimulus_id, kind, principal_id, channel_key, preview_json, payload_json, reply_to,
142
+ coalesce_key, occurred_at, created_at)
143
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
144
+ this.stmtInsertRef = this.db.prepare(`INSERT OR IGNORE INTO mailbox_input_refs (input_id, ref_id, observation_ref)
145
+ VALUES (?, ?, ?)`);
146
+ this.stmtRefsFor = this.db.prepare(`SELECT ref_id, observation_ref FROM mailbox_input_refs WHERE input_id = ? ORDER BY rowid ASC`);
147
+ this.stmtClaimSelect = this.db.prepare(`SELECT id, stimulus_id, kind, principal_id, channel_key, preview_json, payload_json, reply_to,
148
+ coalesce_key, occurred_at, created_at, attempts
149
+ FROM mailbox_inputs
150
+ WHERE status = 'pending' AND COALESCE(retry_after, 0) <= ?
151
+ AND NOT EXISTS (SELECT 1 FROM native_input_deliveries n WHERE n.input_id=mailbox_inputs.id AND n.state!='prepared')
152
+ ORDER BY id ASC LIMIT 1`);
153
+ this.stmtClaimSelectKind = this.db.prepare(`SELECT id, stimulus_id, kind, principal_id, channel_key, preview_json, payload_json, reply_to,
154
+ coalesce_key, occurred_at, created_at, attempts
155
+ FROM mailbox_inputs
156
+ WHERE status = 'pending' AND COALESCE(retry_after, 0) <= ? AND kind = ?
157
+ AND NOT EXISTS (SELECT 1 FROM native_input_deliveries n WHERE n.input_id=mailbox_inputs.id AND n.state!='prepared')
158
+ ORDER BY id ASC LIMIT 1`);
159
+ this.stmtClaimUpdate = this.db.prepare(`UPDATE mailbox_inputs SET status = 'claimed', claimed_at = ?
160
+ WHERE id = ? AND status = 'pending'`);
161
+ this.stmtAck = this.db.prepare(`UPDATE mailbox_inputs SET status = 'acked', acked_at = ? WHERE id = ?`);
162
+ this.stmtRetry = this.db.prepare(`UPDATE mailbox_inputs
163
+ SET status = CASE WHEN attempts + 1 >= ${MAX_ATTEMPTS} THEN 'dead' ELSE 'pending' END,
164
+ attempts = attempts + 1, last_error = ?, claimed_at = NULL,
165
+ retry_after = ? + ${backoffCase('attempts')}
166
+ WHERE id = ? AND status = 'claimed'`);
167
+ this.stmtRetryStatus = this.db.prepare(`SELECT status FROM mailbox_inputs WHERE id = ?`);
168
+ this.stmtPruneAcked = this.db.prepare(`DELETE FROM mailbox_inputs WHERE status = 'acked' AND acked_at <= ?
169
+ AND NOT EXISTS (SELECT 1 FROM native_input_deliveries n WHERE n.input_id=mailbox_inputs.id AND n.state IN ('dispatching','accepted','uncertain'))`);
170
+ this.stmtPrunePending = this.db.prepare(`UPDATE mailbox_inputs
171
+ SET status = 'dead', last_error = 'stale_pending_expired'
172
+ WHERE status = 'pending' AND created_at <= ?
173
+ AND NOT EXISTS (SELECT 1 FROM native_input_deliveries n WHERE n.input_id=mailbox_inputs.id AND n.state IN ('dispatching','accepted','uncertain'))`);
174
+ this.stmtPruneSeen = this.db.prepare(`DELETE FROM mailbox_seen WHERE seen_at <= ?
175
+ AND NOT EXISTS (SELECT 1 FROM mailbox_input_refs r JOIN native_input_deliveries n ON n.input_id=r.input_id
176
+ WHERE r.ref_id=mailbox_seen.ref_id AND n.state IN ('dispatching','accepted','uncertain'))`);
177
+ // Same poison cap as retry(): a claim that expires its lease repeatedly
178
+ // (hung run, process death mid-flight) parks dead too, rather than
179
+ // re-pending forever at the head of the queue.
180
+ this.stmtReplay = this.db.prepare(`UPDATE mailbox_inputs
181
+ SET status = CASE WHEN attempts + 1 >= ${MAX_ATTEMPTS} THEN 'dead' ELSE 'pending' END,
182
+ attempts = attempts + 1, claimed_at = NULL,
183
+ retry_after = ? + ${backoffCase('attempts')}
184
+ WHERE status = 'claimed' AND COALESCE(claimed_at, 0) <= ?
185
+ AND NOT EXISTS (SELECT 1 FROM native_input_deliveries n WHERE n.input_id=mailbox_inputs.id AND n.state!='prepared')`);
186
+ // Grouped on the (status, id) index; never scans the acked bulk.
187
+ this.stmtDepth = this.db.prepare(`SELECT status, COUNT(*) AS n FROM mailbox_inputs
188
+ WHERE status IN ('pending','claimed','dead') GROUP BY status`);
189
+ this.stmtPendingCoalesced = this.db.prepare(`SELECT id, preview_json FROM mailbox_inputs
190
+ WHERE status = 'pending' AND coalesce_key = ? AND principal_id = ?
191
+ AND payload_json IS ? AND kind IS ? AND channel_key = ? AND reply_to IS ?
192
+ AND NOT EXISTS (SELECT 1 FROM native_input_deliveries n WHERE n.input_id=mailbox_inputs.id AND n.state!='prepared')
193
+ ORDER BY id ASC LIMIT 1`);
194
+ this.stmtAppendPreview = this.db.prepare(`UPDATE mailbox_inputs SET preview_json = ? WHERE id = ? AND status = 'pending'`);
195
+ this.stmtIdentityPayload = this.db.prepare(`SELECT payload_json FROM mailbox_inputs
196
+ WHERE principal_id = ? AND (stimulus_id = ? OR id IN (
197
+ SELECT input_id FROM mailbox_input_refs WHERE ref_id = ?
198
+ )) ORDER BY id DESC LIMIT 1`);
199
+ }
200
+ now() {
201
+ return this.clock();
202
+ }
203
+ /**
204
+ * Make a stimulus durable. Returns the row id, or `null` when every ref it
205
+ * carries was already accepted.
206
+ *
207
+ * Dedupe is PER REF, not per stimulus shape. A boundary key fails on partial
208
+ * redelivery: [r1] enqueues, the producer's cursor commit fails, the next
209
+ * tick delivers [r1,r2] under a different boundary — and r1 runs twice.
210
+ * A stimulus with no unseen ref is dropped; one with any unseen ref is
211
+ * stored with exactly its unseen refs.
212
+ */
213
+ enqueue(stimulus) {
214
+ if (!isStimulusKind(stimulus.kind)) {
215
+ throw new Error(`Mailbox: unknown stimulus kind "${String(stimulus.kind)}"`);
216
+ }
217
+ if (stimulus.principalId.trim() === '') {
218
+ throw new Error('Mailbox: a stimulus states whose it is');
219
+ }
220
+ if (stimulus.channelKey.trim() === '') {
221
+ throw new Error('Mailbox: a stimulus states where it came from');
222
+ }
223
+ const payload = (0, stimulus_payload_js_1.encodeStimulusPayload)(stimulus.payload);
224
+ const accepted = this.stmtIdentityPayload.get(stimulus.principalId, stimulus.id, stimulus.id);
225
+ if (accepted && accepted.payload_json !== payload) {
226
+ throw new Error('Mailbox payload conflict for an accepted stimulus identity');
227
+ }
228
+ // With no refs stated the producer's own id is the identity, so a
229
+ // redelivery of the same stimulus dedupes on it rather than opening a
230
+ // second row.
231
+ const stated = stimulus.refs && stimulus.refs.length > 0
232
+ ? stimulus.refs.map((ref) => ({ refId: ref.refId, observationRef: ref.observationRef }))
233
+ : [{ refId: stimulus.id, observationRef: null }];
234
+ if (stated.length > MAX_REFS) {
235
+ throw new Error(`Mailbox: a stimulus carries at most ${MAX_REFS} refs`);
236
+ }
237
+ for (const ref of stated) {
238
+ if (typeof ref.refId !== 'string' || ref.refId.trim() === '') {
239
+ throw new Error('Mailbox: a ref states its id');
240
+ }
241
+ }
242
+ const seen = new Set();
243
+ const refIds = stated.map((ref) => ref.refId);
244
+ for (let i = 0; i < refIds.length; i += SEEN_CHUNK) {
245
+ const chunk = refIds.slice(i, i + SEEN_CHUNK);
246
+ const rows = this.db
247
+ .prepare(`SELECT ref_id FROM mailbox_seen WHERE ref_id IN (${chunk.map(() => '?').join(',')})`)
248
+ .all(...chunk);
249
+ for (const row of rows)
250
+ seen.add(row.ref_id);
251
+ }
252
+ const fresh = stated.filter((ref) => !seen.has(ref.refId));
253
+ if (fresh.length === 0) {
254
+ return null; // fully redelivered — already durable
255
+ }
256
+ const preview = [...(stimulus.preview ?? [])];
257
+ return this.db.transaction(() => {
258
+ const now = this.now();
259
+ for (const ref of fresh) {
260
+ this.stmtInsertSeen.run(ref.refId, now);
261
+ }
262
+ // Mechanical merge: an existing pending row with the same producer key
263
+ // and principal takes these refs instead of opening a second row. Both
264
+ // rows are still pending, so nothing that was already handed to the loop
265
+ // is touched.
266
+ const key = stimulus.coalesceKey ?? null;
267
+ if (key !== null && key !== '') {
268
+ const open = this.stmtPendingCoalesced.get(key, stimulus.principalId, payload, stimulus.kind, stimulus.channelKey, stimulus.replyTo ?? null);
269
+ if (open) {
270
+ for (const ref of fresh) {
271
+ this.stmtInsertRef.run(open.id, ref.refId, ref.observationRef);
272
+ }
273
+ if (preview.length > 0) {
274
+ const existing = JSON.parse(open.preview_json);
275
+ this.stmtAppendPreview.run(JSON.stringify([...existing, ...preview]), open.id);
276
+ }
277
+ return open.id;
278
+ }
279
+ }
280
+ const inserted = this.stmtInsert.run(stimulus.id, stimulus.kind, stimulus.principalId, stimulus.channelKey, JSON.stringify(preview), payload, stimulus.replyTo ?? null, key, stimulus.occurredAt, now);
281
+ const id = Number(inserted.lastInsertRowid);
282
+ for (const ref of fresh) {
283
+ this.stmtInsertRef.run(id, ref.refId, ref.observationRef);
284
+ }
285
+ return id;
286
+ });
287
+ }
288
+ /**
289
+ * Take the next pending row under a lease. A corrupt row parks dead and the
290
+ * scan continues.
291
+ *
292
+ * `prefer` orders kinds ahead of the rest, oldest-first within each. It is
293
+ * ordering, not meaning: §4.3 forbids a host FIFO in which the person's
294
+ * current request waits behind a mass replay, and the kind a producer stated
295
+ * is the only thing that can say which is which without reading content.
296
+ */
297
+ claimNext(options = {}) {
298
+ const prefer = options.prefer ?? [];
299
+ const select = () => {
300
+ const now = this.now();
301
+ for (const kind of prefer) {
302
+ const preferred = this.stmtClaimSelectKind.get(now, kind);
303
+ if (preferred)
304
+ return preferred;
305
+ }
306
+ return this.stmtClaimSelect.get(now);
307
+ };
308
+ let row = select();
309
+ while (row) {
310
+ const candidate = row;
311
+ let preview;
312
+ let hydrated;
313
+ try {
314
+ preview = JSON.parse(candidate.preview_json);
315
+ if (!Array.isArray(preview) || preview.some((line) => typeof line !== 'string')) {
316
+ throw new Error('preview_json is invalid');
317
+ }
318
+ hydrated = this.hydrate(candidate, preview);
319
+ }
320
+ catch (error) {
321
+ const diagnostic = new MailboxCorruptionError(candidate.id, error);
322
+ this.db
323
+ .prepare(`UPDATE mailbox_inputs SET status = 'dead', last_error = ?, claimed_at = NULL
324
+ WHERE id = ? AND status = 'pending'`)
325
+ .run(`${diagnostic.code}:row-${candidate.id}`, candidate.id);
326
+ row = select();
327
+ continue;
328
+ }
329
+ const claimed = this.stmtClaimUpdate.run(this.now(), candidate.id);
330
+ if (claimed.changes !== 1) {
331
+ return null;
332
+ }
333
+ return { ...hydrated, status: 'claimed' };
334
+ }
335
+ return null;
336
+ }
337
+ hydrate(row, preview) {
338
+ const nativeDelivery = this.nativeInputs.get(row.id);
339
+ const refs = this.stmtRefsFor.all(row.id).map((ref) => ({ refId: ref.ref_id, observationRef: ref.observation_ref }));
340
+ return {
341
+ id: row.id,
342
+ stimulusId: row.stimulus_id,
343
+ kind: isStimulusKind(row.kind) ? row.kind : null,
344
+ principalId: row.principal_id,
345
+ channelKey: row.channel_key,
346
+ refs,
347
+ preview,
348
+ ...(row.payload_json === null ? {} : { payload: (0, stimulus_payload_js_1.decodeStimulusPayload)(row.payload_json) }),
349
+ replyTo: row.reply_to,
350
+ coalesceKey: row.coalesce_key,
351
+ occurredAt: row.occurred_at,
352
+ createdAt: row.created_at,
353
+ status: 'pending',
354
+ attempts: row.attempts,
355
+ ...(nativeDelivery ? { nativeDelivery } : {}),
356
+ };
357
+ }
358
+ /** Renew only a live runtime-owned claim; native ACKs remain independent. */
359
+ renewClaim(id, now = this.now()) {
360
+ this.db
361
+ .prepare("UPDATE mailbox_inputs SET claimed_at=? WHERE id=? AND status='claimed'")
362
+ .run(now, id);
363
+ }
364
+ /** Inspect one accepted input without claiming it or changing its lease. */
365
+ readInput(stimulusId, principalId) {
366
+ const row = this.db
367
+ .prepare(`SELECT id, stimulus_id, kind, principal_id, channel_key,
368
+ preview_json, payload_json, reply_to, coalesce_key, occurred_at, created_at, attempts, status
369
+ FROM mailbox_inputs WHERE stimulus_id = ? AND principal_id = ? ORDER BY id DESC LIMIT 1`)
370
+ .get(stimulusId, principalId);
371
+ if (!row)
372
+ return null;
373
+ try {
374
+ const preview = JSON.parse(row.preview_json);
375
+ if (!Array.isArray(preview) || preview.some((line) => typeof line !== 'string')) {
376
+ throw new Error('Invalid input preview');
377
+ }
378
+ return { ...this.hydrate(row, preview), status: row.status };
379
+ }
380
+ catch (error) {
381
+ throw new MailboxCorruptionError(row.id, error);
382
+ }
383
+ }
384
+ /** Reopen claims that never crossed the native dispatch boundary. */
385
+ recoverOrphanedClaims(now = this.now()) {
386
+ return this.db.transaction(() => {
387
+ const replayed = this.db
388
+ .prepare(`UPDATE mailbox_inputs
389
+ SET status='pending', claimed_at=NULL, retry_after=NULL
390
+ WHERE status='claimed'
391
+ AND NOT EXISTS (
392
+ SELECT 1 FROM native_input_deliveries n
393
+ WHERE n.input_id=mailbox_inputs.id
394
+ AND n.state IN ('dispatching','accepted','uncertain','settled')
395
+ )`)
396
+ .run().changes;
397
+ this.db
398
+ .prepare(`UPDATE mailbox_inputs
399
+ SET status='acked', acked_at=COALESCE(acked_at, ?), claimed_at=NULL
400
+ WHERE status='claimed'
401
+ AND EXISTS (
402
+ SELECT 1 FROM native_input_deliveries n
403
+ WHERE n.input_id=mailbox_inputs.id AND n.state='settled'
404
+ )`)
405
+ .run(now);
406
+ return replayed;
407
+ });
408
+ }
409
+ /** Identity lookup for transport recovery; never returns another principal's payload. */
410
+ principalsForInput(stimulusId) {
411
+ return this.db
412
+ .prepare('SELECT DISTINCT principal_id FROM mailbox_inputs WHERE stimulus_id = ?')
413
+ .all(stimulusId).map((row) => row.principal_id);
414
+ }
415
+ /** The exact native turn's accepted input cohort, scoped to its principal. */
416
+ readNativeTurnInputs(stimulusId, principalId, options = {}) {
417
+ const primary = this.db
418
+ .prepare('SELECT id FROM mailbox_inputs WHERE stimulus_id=? AND principal_id=? ORDER BY id DESC LIMIT 1')
419
+ .get(stimulusId, principalId);
420
+ if (!primary)
421
+ throw new Error('Native turn input is not visible to this principal');
422
+ const receipt = this.nativeInputs.get(primary.id)?.receipt;
423
+ if (!receipt)
424
+ throw new Error('Native turn input has no accepted receipt');
425
+ const page = this.nativeInputs.listByReceipt(receipt, principalId, options);
426
+ if (page.inputIds.length === 0)
427
+ return { items: [], nextCursor: null };
428
+ const placeholders = page.inputIds.map(() => '?').join(',');
429
+ const rows = this.db
430
+ .prepare(`SELECT id, stimulus_id, kind, channel_key, occurred_at, status
431
+ FROM mailbox_inputs WHERE id IN (${placeholders}) AND principal_id=?`)
432
+ .all(...page.inputIds, principalId);
433
+ if (rows.length !== page.inputIds.length) {
434
+ throw new Error('Native turn input cohort is incomplete');
435
+ }
436
+ const byId = new Map(rows.map((row) => [row.id, row]));
437
+ return {
438
+ items: page.inputIds.map((id) => {
439
+ const row = byId.get(id);
440
+ return {
441
+ id,
442
+ stimulusId: row.stimulus_id,
443
+ kind: isStimulusKind(row.kind) ? row.kind : null,
444
+ channelKey: row.channel_key,
445
+ occurredAt: row.occurred_at,
446
+ status: row.status,
447
+ };
448
+ }),
449
+ nextCursor: page.nextCursor,
450
+ };
451
+ }
452
+ /** Inputs needing native/result reconciliation, never a second model execution. */
453
+ unsettledNative(limit, afterId = 0) {
454
+ return this.nativeInputs.pending(limit, afterId).map((id) => {
455
+ const row = this.db
456
+ .prepare('SELECT * FROM mailbox_inputs WHERE id=?')
457
+ .get(id);
458
+ if (!row)
459
+ throw new Error(`Native delivery input ${id} is missing`);
460
+ const preview = JSON.parse(row.preview_json);
461
+ if (!Array.isArray(preview) || preview.some((line) => typeof line !== 'string')) {
462
+ throw new MailboxCorruptionError(id, new Error('Invalid input preview'));
463
+ }
464
+ return { ...this.hydrate(row, preview), status: row.status };
465
+ });
466
+ }
467
+ /**
468
+ * The loop took this stimulus.
469
+ *
470
+ * That is all this says. It is not a claim that anything was read, decided
471
+ * or done — §4.4 is explicit that a delivery ACK is not a work receipt, and
472
+ * there is deliberately no field here to record one in.
473
+ */
474
+ ack(id) {
475
+ this.stmtAck.run(this.now(), id);
476
+ }
477
+ /**
478
+ * Return a claim to pending, or park it dead after MAX_ATTEMPTS. Returns the
479
+ * resulting status so the caller can be LOUD about a dead row — a permanent
480
+ * loss must never be silent.
481
+ */
482
+ retry(id, error) {
483
+ const result = this.stmtRetry.run(error.slice(0, 500), this.now(), id);
484
+ if (result.changes !== 1) {
485
+ return 'noop'; // replayStale already flipped it
486
+ }
487
+ const row = this.stmtRetryStatus.get(id);
488
+ return row?.status === 'dead' ? 'dead' : 'pending';
489
+ }
490
+ /** Park a row dead without spending an attempt. */
491
+ quarantine(id, reason) {
492
+ this.db
493
+ .prepare(`UPDATE mailbox_inputs SET status = 'dead', last_error = ?, claimed_at = NULL
494
+ WHERE id = ? AND status IN ('claimed', 'pending')`)
495
+ .run(reason.slice(0, 500), id);
496
+ }
497
+ replayStale(olderThanMs, now = this.now()) {
498
+ return this.replayStaleDetailed(olderThanMs, now).replayed;
499
+ }
500
+ replayStaleDetailed(olderThanMs, now = this.now()) {
501
+ // Housekeeping rides along: acked rows age out, stale pending rows park as
502
+ // dead (visible, bounded), and the dedupe horizon stays wide but finite —
503
+ // no table here grows without bound.
504
+ const stalePending = this.db
505
+ .prepare(`SELECT id, stimulus_id, kind, principal_id, channel_key, preview_json, payload_json, reply_to,
506
+ coalesce_key, occurred_at, created_at, attempts
507
+ FROM mailbox_inputs
508
+ WHERE status = 'pending' AND created_at <= ?
509
+ AND NOT EXISTS (SELECT 1 FROM native_input_deliveries n WHERE n.input_id=mailbox_inputs.id AND n.state IN ('dispatching','accepted','uncertain'))
510
+ ORDER BY id ASC`)
511
+ .all(now - PENDING_RETENTION_MS);
512
+ this.stmtPruneAcked.run(now - ACKED_RETENTION_MS);
513
+ this.stmtPrunePending.run(now - PENDING_RETENTION_MS);
514
+ this.stmtPruneSeen.run(now - REF_RETENTION_MS);
515
+ const cutoff = now - olderThanMs;
516
+ const dying = this.db
517
+ .prepare(`SELECT id, stimulus_id, kind, principal_id, channel_key, preview_json, payload_json, reply_to,
518
+ coalesce_key, occurred_at, created_at, attempts
519
+ FROM mailbox_inputs
520
+ WHERE status = 'claimed' AND attempts + 1 >= ${MAX_ATTEMPTS}
521
+ AND COALESCE(claimed_at, 0) <= ?
522
+ AND NOT EXISTS (SELECT 1 FROM native_input_deliveries n WHERE n.input_id=mailbox_inputs.id AND n.state IN ('dispatching','accepted','uncertain'))
523
+ ORDER BY id ASC`)
524
+ .all(cutoff);
525
+ const result = this.stmtReplay.run(now, cutoff);
526
+ const dead = (row, attempts) => {
527
+ let preview = [];
528
+ try {
529
+ preview = JSON.parse(row.preview_json);
530
+ }
531
+ catch {
532
+ preview = [];
533
+ }
534
+ return { ...this.hydrate(row, preview), status: 'dead', attempts };
535
+ };
536
+ return {
537
+ replayed: result.changes,
538
+ newlyDead: [
539
+ ...stalePending.map((row) => dead(row, row.attempts)),
540
+ ...dying.map((row) => dead(row, row.attempts + 1)),
541
+ ],
542
+ };
543
+ }
544
+ depth() {
545
+ const rows = this.stmtDepth.all();
546
+ const byStatus = new Map(rows.map((row) => [row.status, row.n]));
547
+ return {
548
+ pending: byStatus.get('pending') ?? 0,
549
+ claimed: byStatus.get('claimed') ?? 0,
550
+ dead: byStatus.get('dead') ?? 0,
551
+ };
552
+ }
553
+ /**
554
+ * Schedule a wake. The fourth job: when to come back, and for what channel.
555
+ *
556
+ * `mailbox_schedules` is this daemon's own alarm clock, not anyone's
557
+ * calendar — a consumer's calendar is source data and is read through the
558
+ * source actions (design §6.3).
559
+ */
560
+ scheduleWake(channelKey, dueAt) {
561
+ const inserted = this.db
562
+ .prepare(`INSERT INTO mailbox_schedules (channel_key, due_at, created_at) VALUES (?, ?, ?)`)
563
+ .run(channelKey, dueAt, this.now());
564
+ return Number(inserted.lastInsertRowid);
565
+ }
566
+ /** Wakes that are due and have not fired. The caller marks them fired when it acts. */
567
+ dueWakes(now = this.now(), limit = 50) {
568
+ return this.db
569
+ .prepare(`SELECT id, channel_key, due_at FROM mailbox_schedules
570
+ WHERE fired_at IS NULL AND due_at <= ?
571
+ ORDER BY due_at ASC, id ASC LIMIT ?`)
572
+ .all(now, Math.min(Math.max(limit, 1), 500)).map((row) => ({ id: row.id, channelKey: row.channel_key, dueAt: row.due_at }));
573
+ }
574
+ /** Mark a wake fired. Idempotent: a second call changes nothing. */
575
+ markWakeFired(id) {
576
+ return (this.db
577
+ .prepare(`UPDATE mailbox_schedules SET fired_at = ? WHERE id = ? AND fired_at IS NULL`)
578
+ .run(this.now(), id).changes === 1);
579
+ }
580
+ }
581
+ exports.Mailbox = Mailbox;
582
+ //# sourceMappingURL=mailbox.js.map
@@ -0,0 +1,28 @@
1
+ import type { DatabaseAdapter } from '../db-manager.js';
2
+ import type { BeginModelRunInput, ModelRunRecord, ModelRunStatus } from './model-run-types.js';
3
+ type ModelRunAdapter = Pick<DatabaseAdapter, 'prepare'>;
4
+ export declare function beginModelRun(adapter: ModelRunAdapter, input: BeginModelRunInput): ModelRunRecord;
5
+ export declare function commitModelRun(adapter: ModelRunAdapter, modelRunId: string, summary?: string, tokenCount?: number): ModelRunRecord;
6
+ export declare function failModelRun(adapter: ModelRunAdapter, modelRunId: string, errorSummary: string, tokenCount?: number): ModelRunRecord;
7
+ export declare function getModelRun(adapter: ModelRunAdapter, modelRunId: string): ModelRunRecord | null;
8
+ export interface ModelRunNativeInput {
9
+ inputId: number;
10
+ stimulusId: string;
11
+ kind: string;
12
+ principalId: string;
13
+ channelKey: string;
14
+ occurredAt: number;
15
+ status: string;
16
+ nativeState: string;
17
+ }
18
+ /** Read one run's native input cohort without copying payloads or inventing legacy principals. */
19
+ export declare function listModelRunNativeInputs(adapter: ModelRunAdapter, modelRunId: string, principalId: string, options?: {
20
+ afterId?: number;
21
+ limit?: number;
22
+ }): {
23
+ items: ModelRunNativeInput[];
24
+ nextCursor: number | null;
25
+ runStatus: ModelRunStatus;
26
+ };
27
+ export {};
28
+ //# sourceMappingURL=model-run-store.d.ts.map