sensorium-mcp 3.1.2 → 3.1.3-board.10

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 (625) hide show
  1. package/Install-Sensorium.ps1 +257 -43
  2. package/README.md +2 -0
  3. package/dist/board/api/api.js +84 -0
  4. package/dist/board/api/api.test.ts +23 -0
  5. package/dist/board/api/client.js +113 -0
  6. package/dist/board/components/BottomNav.js +32 -0
  7. package/dist/board/components/Card.js +78 -0
  8. package/dist/board/components/Combobox.js +79 -0
  9. package/dist/board/components/CommentComposer.js +29 -0
  10. package/dist/board/components/CommentList.js +42 -0
  11. package/dist/board/components/DirTreePicker.js +128 -0
  12. package/dist/board/components/Dropdown.js +67 -0
  13. package/dist/board/components/Lane.js +47 -0
  14. package/dist/board/components/PriorityChip.js +20 -0
  15. package/dist/board/components/ProfileSummary.js +89 -0
  16. package/dist/board/components/StateBanner.js +24 -0
  17. package/dist/board/components/StatusChip.js +26 -0
  18. package/dist/board/components/rateLimit.js +32 -0
  19. package/dist/board/components/rateLimit.test.ts +16 -0
  20. package/dist/board/components/time.js +14 -0
  21. package/dist/board/config.js +56 -0
  22. package/dist/board/index.html +18 -0
  23. package/dist/board/main.js +37 -0
  24. package/dist/board/mock/api.mock.js +491 -0
  25. package/dist/board/mock/api.mock.test.ts +460 -0
  26. package/dist/board/mock/fixtures.js +512 -0
  27. package/dist/board/schema/policy.js +261 -0
  28. package/dist/board/state/recur.js +24 -0
  29. package/dist/board/state/recur.test.ts +24 -0
  30. package/dist/board/state/useBoard.js +161 -0
  31. package/dist/board/state/useBuilder.js +357 -0
  32. package/dist/board/state/useBuilder.test.ts +468 -0
  33. package/dist/board/state/useCard.js +193 -0
  34. package/dist/board/state/useCard.test.ts +435 -0
  35. package/dist/board/state/useDirBrowser.js +23 -0
  36. package/dist/board/styles/board.css +2885 -0
  37. package/dist/board/styles/tokens.css +72 -0
  38. package/dist/board/vendor/vue.esm-browser.js +9 -0
  39. package/dist/board/views/App.js +61 -0
  40. package/dist/board/views/BoardView.js +153 -0
  41. package/dist/board/views/BuilderView.js +582 -0
  42. package/dist/board/views/CardView.js +382 -0
  43. package/dist/board/views/SettingsView.js +152 -0
  44. package/dist/config/agent-settings.d.ts +11 -0
  45. package/dist/config/agent-settings.d.ts.map +1 -1
  46. package/dist/config/agent-settings.js +79 -0
  47. package/dist/config/agent-settings.js.map +1 -1
  48. package/dist/config/detail-grant.d.ts +7 -16
  49. package/dist/config/detail-grant.d.ts.map +1 -1
  50. package/dist/config/detail-grant.js +12 -35
  51. package/dist/config/detail-grant.js.map +1 -1
  52. package/dist/config/index.d.ts +2 -0
  53. package/dist/config/index.d.ts.map +1 -1
  54. package/dist/config/index.js +17 -0
  55. package/dist/config/index.js.map +1 -1
  56. package/dist/config/mcp-settings.d.ts +7 -1
  57. package/dist/config/mcp-settings.d.ts.map +1 -1
  58. package/dist/config/mcp-settings.js +9 -0
  59. package/dist/config/mcp-settings.js.map +1 -1
  60. package/dist/config/misc-settings.d.ts +22 -1
  61. package/dist/config/misc-settings.d.ts.map +1 -1
  62. package/dist/config/misc-settings.js +93 -3
  63. package/dist/config/misc-settings.js.map +1 -1
  64. package/dist/config/operator-grant.d.ts +9 -0
  65. package/dist/config/operator-grant.d.ts.map +1 -0
  66. package/dist/config/operator-grant.js +24 -0
  67. package/dist/config/operator-grant.js.map +1 -0
  68. package/dist/config/voice-grant.d.ts +10 -0
  69. package/dist/config/voice-grant.d.ts.map +1 -0
  70. package/dist/config/voice-grant.js +18 -0
  71. package/dist/config/voice-grant.js.map +1 -0
  72. package/dist/dashboard/board-static.d.ts +22 -0
  73. package/dist/dashboard/board-static.d.ts.map +1 -0
  74. package/dist/dashboard/board-static.js +130 -0
  75. package/dist/dashboard/board-static.js.map +1 -0
  76. package/dist/dashboard/presets.d.ts +1 -1
  77. package/dist/dashboard/presets.d.ts.map +1 -1
  78. package/dist/dashboard/presets.js +1 -1
  79. package/dist/dashboard/presets.js.map +1 -1
  80. package/dist/dashboard/routes/agent-templates.d.ts.map +1 -1
  81. package/dist/dashboard/routes/agent-templates.js +6 -9
  82. package/dist/dashboard/routes/agent-templates.js.map +1 -1
  83. package/dist/dashboard/routes/board-body.d.ts +8 -0
  84. package/dist/dashboard/routes/board-body.d.ts.map +1 -0
  85. package/dist/dashboard/routes/board-body.js +20 -0
  86. package/dist/dashboard/routes/board-body.js.map +1 -0
  87. package/dist/dashboard/routes/board-dir-listing.d.ts +70 -0
  88. package/dist/dashboard/routes/board-dir-listing.d.ts.map +1 -0
  89. package/dist/dashboard/routes/board-dir-listing.js +192 -0
  90. package/dist/dashboard/routes/board-dir-listing.js.map +1 -0
  91. package/dist/dashboard/routes/board-lane-groups.d.ts +6 -0
  92. package/dist/dashboard/routes/board-lane-groups.d.ts.map +1 -0
  93. package/dist/dashboard/routes/board-lane-groups.js +52 -0
  94. package/dist/dashboard/routes/board-lane-groups.js.map +1 -0
  95. package/dist/dashboard/routes/board-policy.d.ts +224 -0
  96. package/dist/dashboard/routes/board-policy.d.ts.map +1 -0
  97. package/dist/dashboard/routes/board-policy.js +214 -0
  98. package/dist/dashboard/routes/board-policy.js.map +1 -0
  99. package/dist/dashboard/routes/board-profile-parse.d.ts +32 -0
  100. package/dist/dashboard/routes/board-profile-parse.d.ts.map +1 -0
  101. package/dist/dashboard/routes/board-profile-parse.js +338 -0
  102. package/dist/dashboard/routes/board-profile-parse.js.map +1 -0
  103. package/dist/dashboard/routes/board.d.ts +90 -0
  104. package/dist/dashboard/routes/board.d.ts.map +1 -0
  105. package/dist/dashboard/routes/board.js +721 -0
  106. package/dist/dashboard/routes/board.js.map +1 -0
  107. package/dist/dashboard/routes/codex.d.ts +14 -0
  108. package/dist/dashboard/routes/codex.d.ts.map +1 -0
  109. package/dist/dashboard/routes/codex.js +92 -0
  110. package/dist/dashboard/routes/codex.js.map +1 -0
  111. package/dist/dashboard/routes/prompts.d.ts +2 -2
  112. package/dist/dashboard/routes/prompts.d.ts.map +1 -1
  113. package/dist/dashboard/routes/prompts.js +2 -2
  114. package/dist/dashboard/routes/prompts.js.map +1 -1
  115. package/dist/dashboard/routes/settings.d.ts +2 -0
  116. package/dist/dashboard/routes/settings.d.ts.map +1 -1
  117. package/dist/dashboard/routes/settings.js +24 -1
  118. package/dist/dashboard/routes/settings.js.map +1 -1
  119. package/dist/dashboard/routes/skills.d.ts +7 -1
  120. package/dist/dashboard/routes/skills.d.ts.map +1 -1
  121. package/dist/dashboard/routes/skills.js +6 -6
  122. package/dist/dashboard/routes/skills.js.map +1 -1
  123. package/dist/dashboard/routes/snapshots.d.ts +2 -1
  124. package/dist/dashboard/routes/snapshots.d.ts.map +1 -1
  125. package/dist/dashboard/routes/snapshots.js +24 -18
  126. package/dist/dashboard/routes/snapshots.js.map +1 -1
  127. package/dist/dashboard/routes/templates.js +5 -5
  128. package/dist/dashboard/routes/templates.js.map +1 -1
  129. package/dist/dashboard/routes/threads-create.d.ts +17 -0
  130. package/dist/dashboard/routes/threads-create.d.ts.map +1 -1
  131. package/dist/dashboard/routes/threads-create.js +32 -10
  132. package/dist/dashboard/routes/threads-create.js.map +1 -1
  133. package/dist/dashboard/routes/threads-helpers.d.ts +11 -2
  134. package/dist/dashboard/routes/threads-helpers.d.ts.map +1 -1
  135. package/dist/dashboard/routes/threads-helpers.js +20 -3
  136. package/dist/dashboard/routes/threads-helpers.js.map +1 -1
  137. package/dist/dashboard/routes/threads-read.d.ts +16 -4
  138. package/dist/dashboard/routes/threads-read.d.ts.map +1 -1
  139. package/dist/dashboard/routes/threads-read.js +61 -55
  140. package/dist/dashboard/routes/threads-read.js.map +1 -1
  141. package/dist/dashboard/routes/threads.d.ts +2 -2
  142. package/dist/dashboard/routes/threads.d.ts.map +1 -1
  143. package/dist/dashboard/routes/threads.js +143 -29
  144. package/dist/dashboard/routes/threads.js.map +1 -1
  145. package/dist/dashboard/routes/types.d.ts +6 -0
  146. package/dist/dashboard/routes/types.d.ts.map +1 -1
  147. package/dist/dashboard/routes/types.js.map +1 -1
  148. package/dist/dashboard/routes.d.ts +2 -2
  149. package/dist/dashboard/routes.d.ts.map +1 -1
  150. package/dist/dashboard/routes.js +85 -7
  151. package/dist/dashboard/routes.js.map +1 -1
  152. package/dist/dashboard/spa.html +15 -12
  153. package/dist/data/board/board-schema.d.ts +29 -0
  154. package/dist/data/board/board-schema.d.ts.map +1 -0
  155. package/dist/data/board/board-schema.js +202 -0
  156. package/dist/data/board/board-schema.js.map +1 -0
  157. package/dist/data/board/board-types.d.ts +175 -0
  158. package/dist/data/board/board-types.d.ts.map +1 -0
  159. package/dist/data/board/board-types.js +9 -0
  160. package/dist/data/board/board-types.js.map +1 -0
  161. package/dist/data/board/card.repository.d.ts +137 -0
  162. package/dist/data/board/card.repository.d.ts.map +1 -0
  163. package/dist/data/board/card.repository.js +470 -0
  164. package/dist/data/board/card.repository.js.map +1 -0
  165. package/dist/data/board/comment.repository.d.ts +14 -0
  166. package/dist/data/board/comment.repository.d.ts.map +1 -0
  167. package/dist/data/board/comment.repository.js +42 -0
  168. package/dist/data/board/comment.repository.js.map +1 -0
  169. package/dist/data/board/lane-group.repository.d.ts +19 -0
  170. package/dist/data/board/lane-group.repository.d.ts.map +1 -0
  171. package/dist/data/board/lane-group.repository.js +66 -0
  172. package/dist/data/board/lane-group.repository.js.map +1 -0
  173. package/dist/data/board/profile-defaults.d.ts +17 -0
  174. package/dist/data/board/profile-defaults.d.ts.map +1 -0
  175. package/dist/data/board/profile-defaults.js +107 -0
  176. package/dist/data/board/profile-defaults.js.map +1 -0
  177. package/dist/data/interfaces.d.ts +1 -0
  178. package/dist/data/interfaces.d.ts.map +1 -1
  179. package/dist/data/memory/bootstrap.d.ts +1 -8
  180. package/dist/data/memory/bootstrap.d.ts.map +1 -1
  181. package/dist/data/memory/bootstrap.js +27 -37
  182. package/dist/data/memory/bootstrap.js.map +1 -1
  183. package/dist/data/memory/consolidation.d.ts +9 -31
  184. package/dist/data/memory/consolidation.d.ts.map +1 -1
  185. package/dist/data/memory/consolidation.js +22 -32
  186. package/dist/data/memory/consolidation.js.map +1 -1
  187. package/dist/data/memory/db-maintenance.d.ts +54 -0
  188. package/dist/data/memory/db-maintenance.d.ts.map +1 -0
  189. package/dist/data/memory/db-maintenance.js +81 -0
  190. package/dist/data/memory/db-maintenance.js.map +1 -0
  191. package/dist/data/memory/episodes.d.ts +2 -1
  192. package/dist/data/memory/episodes.d.ts.map +1 -1
  193. package/dist/data/memory/episodes.js +5 -1
  194. package/dist/data/memory/episodes.js.map +1 -1
  195. package/dist/data/memory/index.d.ts +0 -4
  196. package/dist/data/memory/index.d.ts.map +1 -1
  197. package/dist/data/memory/index.js +0 -4
  198. package/dist/data/memory/index.js.map +1 -1
  199. package/dist/data/memory/key-knowledge.d.ts +51 -0
  200. package/dist/data/memory/key-knowledge.d.ts.map +1 -0
  201. package/dist/data/memory/key-knowledge.js +128 -0
  202. package/dist/data/memory/key-knowledge.js.map +1 -0
  203. package/dist/data/memory/migration-runner.d.ts +1 -1
  204. package/dist/data/memory/migration-runner.d.ts.map +1 -1
  205. package/dist/data/memory/migration-runner.js +407 -1
  206. package/dist/data/memory/migration-runner.js.map +1 -1
  207. package/dist/data/memory/narrative-lint.d.ts +2 -2
  208. package/dist/data/memory/narrative-lint.d.ts.map +1 -1
  209. package/dist/data/memory/narrative-lint.js +23 -13
  210. package/dist/data/memory/narrative-lint.js.map +1 -1
  211. package/dist/data/memory/narrative-resolutions.d.ts +9 -2
  212. package/dist/data/memory/narrative-resolutions.d.ts.map +1 -1
  213. package/dist/data/memory/narrative-resolutions.js +9 -8
  214. package/dist/data/memory/narrative-resolutions.js.map +1 -1
  215. package/dist/data/memory/narrative-sources.d.ts +1 -1
  216. package/dist/data/memory/narrative-sources.d.ts.map +1 -1
  217. package/dist/data/memory/narrative-sources.js +8 -3
  218. package/dist/data/memory/narrative-sources.js.map +1 -1
  219. package/dist/data/memory/narrative-store.d.ts +24 -0
  220. package/dist/data/memory/narrative-store.d.ts.map +1 -1
  221. package/dist/data/memory/narrative-store.js +73 -35
  222. package/dist/data/memory/narrative-store.js.map +1 -1
  223. package/dist/data/memory/narrative.d.ts.map +1 -1
  224. package/dist/data/memory/narrative.js +47 -29
  225. package/dist/data/memory/narrative.js.map +1 -1
  226. package/dist/data/memory/note-archival.d.ts +1 -1
  227. package/dist/data/memory/note-archival.d.ts.map +1 -1
  228. package/dist/data/memory/note-archival.js +7 -3
  229. package/dist/data/memory/note-archival.js.map +1 -1
  230. package/dist/data/memory/note-embeddings.d.ts +7 -0
  231. package/dist/data/memory/note-embeddings.d.ts.map +1 -1
  232. package/dist/data/memory/note-embeddings.js +6 -2
  233. package/dist/data/memory/note-embeddings.js.map +1 -1
  234. package/dist/data/memory/passive-notice.repository.d.ts +70 -0
  235. package/dist/data/memory/passive-notice.repository.d.ts.map +1 -0
  236. package/dist/data/memory/passive-notice.repository.js +85 -0
  237. package/dist/data/memory/passive-notice.repository.js.map +1 -0
  238. package/dist/data/memory/schema-ddl.d.ts +120 -1
  239. package/dist/data/memory/schema-ddl.d.ts.map +1 -1
  240. package/dist/data/memory/schema-ddl.js +171 -4
  241. package/dist/data/memory/schema-ddl.js.map +1 -1
  242. package/dist/data/memory/schema-guard.d.ts.map +1 -1
  243. package/dist/data/memory/schema-guard.js +5 -41
  244. package/dist/data/memory/schema-guard.js.map +1 -1
  245. package/dist/data/memory/schema.d.ts.map +1 -1
  246. package/dist/data/memory/schema.js +10 -34
  247. package/dist/data/memory/schema.js.map +1 -1
  248. package/dist/data/memory/semantic-notes.d.ts +13 -8
  249. package/dist/data/memory/semantic-notes.d.ts.map +1 -1
  250. package/dist/data/memory/semantic-notes.js +12 -17
  251. package/dist/data/memory/semantic-notes.js.map +1 -1
  252. package/dist/data/memory/thread-registry.d.ts +330 -15
  253. package/dist/data/memory/thread-registry.d.ts.map +1 -1
  254. package/dist/data/memory/thread-registry.js +523 -67
  255. package/dist/data/memory/thread-registry.js.map +1 -1
  256. package/dist/data/memory/utils.d.ts +6 -0
  257. package/dist/data/memory/utils.d.ts.map +1 -1
  258. package/dist/data/memory/utils.js +13 -0
  259. package/dist/data/memory/utils.js.map +1 -1
  260. package/dist/data/memory/worker-lifecycle.repository.d.ts +308 -0
  261. package/dist/data/memory/worker-lifecycle.repository.d.ts.map +1 -0
  262. package/dist/data/memory/worker-lifecycle.repository.js +428 -0
  263. package/dist/data/memory/worker-lifecycle.repository.js.map +1 -0
  264. package/dist/data/optional.d.ts +16 -0
  265. package/dist/data/optional.d.ts.map +1 -0
  266. package/dist/data/optional.js +47 -0
  267. package/dist/data/optional.js.map +1 -0
  268. package/dist/index.js +156 -24
  269. package/dist/index.js.map +1 -1
  270. package/dist/integrations/openai/speech.d.ts.map +1 -1
  271. package/dist/integrations/openai/speech.js +52 -19
  272. package/dist/integrations/openai/speech.js.map +1 -1
  273. package/dist/integrations/openai/video.d.ts +1 -1
  274. package/dist/integrations/openai/video.d.ts.map +1 -1
  275. package/dist/integrations/openai/video.js +13 -8
  276. package/dist/integrations/openai/video.js.map +1 -1
  277. package/dist/integrations/telegram/types.d.ts +11 -0
  278. package/dist/integrations/telegram/types.d.ts.map +1 -1
  279. package/dist/lib/http-retry.d.ts +91 -0
  280. package/dist/lib/http-retry.d.ts.map +1 -0
  281. package/dist/lib/http-retry.js +177 -0
  282. package/dist/lib/http-retry.js.map +1 -0
  283. package/dist/lib/mcp-http-port.d.ts +12 -0
  284. package/dist/lib/mcp-http-port.d.ts.map +1 -0
  285. package/dist/lib/mcp-http-port.js +19 -0
  286. package/dist/lib/mcp-http-port.js.map +1 -0
  287. package/dist/lib/paths.d.ts +29 -4
  288. package/dist/lib/paths.d.ts.map +1 -1
  289. package/dist/lib/paths.js +31 -4
  290. package/dist/lib/paths.js.map +1 -1
  291. package/dist/lib/types.d.ts +2 -0
  292. package/dist/lib/types.d.ts.map +1 -1
  293. package/dist/lib/usage-metering/pricing.js +1 -1
  294. package/dist/lib/usage-metering/pricing.js.map +1 -1
  295. package/dist/lib/usage-metering/sink.d.ts +4 -0
  296. package/dist/lib/usage-metering/sink.d.ts.map +1 -1
  297. package/dist/lib/usage-metering/sink.js +6 -4
  298. package/dist/lib/usage-metering/sink.js.map +1 -1
  299. package/dist/lib/utils.d.ts +31 -0
  300. package/dist/lib/utils.d.ts.map +1 -1
  301. package/dist/lib/utils.js +49 -0
  302. package/dist/lib/utils.js.map +1 -1
  303. package/dist/llm/prompts/narrative.d.ts.map +1 -1
  304. package/dist/llm/prompts/narrative.js +27 -86
  305. package/dist/llm/prompts/narrative.js.map +1 -1
  306. package/dist/log-parsing/log-events.d.ts +38 -0
  307. package/dist/log-parsing/log-events.d.ts.map +1 -0
  308. package/dist/log-parsing/log-events.js +2 -0
  309. package/dist/log-parsing/log-events.js.map +1 -0
  310. package/dist/log-parsing/parser-registry.d.ts +4 -0
  311. package/dist/log-parsing/parser-registry.d.ts.map +1 -0
  312. package/dist/log-parsing/parser-registry.js +18 -0
  313. package/dist/log-parsing/parser-registry.js.map +1 -0
  314. package/dist/log-parsing/parsers/claude.d.ts +6 -0
  315. package/dist/log-parsing/parsers/claude.d.ts.map +1 -0
  316. package/dist/log-parsing/parsers/claude.js +100 -0
  317. package/dist/log-parsing/parsers/claude.js.map +1 -0
  318. package/dist/log-parsing/parsers/codex.d.ts +6 -0
  319. package/dist/log-parsing/parsers/codex.d.ts.map +1 -0
  320. package/dist/log-parsing/parsers/codex.js +116 -0
  321. package/dist/log-parsing/parsers/codex.js.map +1 -0
  322. package/dist/log-parsing/parsers/copilot.d.ts +6 -0
  323. package/dist/log-parsing/parsers/copilot.d.ts.map +1 -0
  324. package/dist/log-parsing/parsers/copilot.js +65 -0
  325. package/dist/log-parsing/parsers/copilot.js.map +1 -0
  326. package/dist/log-parsing/parsers/json-lines.d.ts +9 -0
  327. package/dist/log-parsing/parsers/json-lines.d.ts.map +1 -0
  328. package/dist/log-parsing/parsers/json-lines.js +25 -0
  329. package/dist/log-parsing/parsers/json-lines.js.map +1 -0
  330. package/dist/log-parsing/render.d.ts +33 -0
  331. package/dist/log-parsing/render.d.ts.map +1 -0
  332. package/dist/log-parsing/render.js +113 -0
  333. package/dist/log-parsing/render.js.map +1 -0
  334. package/dist/log-parsing/text.d.ts +8 -0
  335. package/dist/log-parsing/text.d.ts.map +1 -0
  336. package/dist/log-parsing/text.js +20 -0
  337. package/dist/log-parsing/text.js.map +1 -0
  338. package/dist/server/http-server.d.ts +8 -1
  339. package/dist/server/http-server.d.ts.map +1 -1
  340. package/dist/server/http-server.js +41 -19
  341. package/dist/server/http-server.js.map +1 -1
  342. package/dist/server/port-holder.d.ts +94 -0
  343. package/dist/server/port-holder.d.ts.map +1 -0
  344. package/dist/server/port-holder.js +122 -0
  345. package/dist/server/port-holder.js.map +1 -0
  346. package/dist/services/agent-spawn.service.d.ts +50 -5
  347. package/dist/services/agent-spawn.service.d.ts.map +1 -1
  348. package/dist/services/agent-spawn.service.js +59 -66
  349. package/dist/services/agent-spawn.service.js.map +1 -1
  350. package/dist/services/background-runner.d.ts +0 -1
  351. package/dist/services/background-runner.d.ts.map +1 -1
  352. package/dist/services/background-runner.js +0 -58
  353. package/dist/services/background-runner.js.map +1 -1
  354. package/dist/services/board/board-signals.d.ts +45 -0
  355. package/dist/services/board/board-signals.d.ts.map +1 -0
  356. package/dist/services/board/board-signals.js +170 -0
  357. package/dist/services/board/board-signals.js.map +1 -0
  358. package/dist/services/board/card-briefing.d.ts +24 -0
  359. package/dist/services/board/card-briefing.d.ts.map +1 -0
  360. package/dist/services/board/card-briefing.js +132 -0
  361. package/dist/services/board/card-briefing.js.map +1 -0
  362. package/dist/services/board/card-diagnostics.d.ts +25 -0
  363. package/dist/services/board/card-diagnostics.d.ts.map +1 -0
  364. package/dist/services/board/card-diagnostics.js +50 -0
  365. package/dist/services/board/card-diagnostics.js.map +1 -0
  366. package/dist/services/board/live-root-thread.d.ts +8 -0
  367. package/dist/services/board/live-root-thread.d.ts.map +1 -0
  368. package/dist/services/board/live-root-thread.js +2 -0
  369. package/dist/services/board/live-root-thread.js.map +1 -0
  370. package/dist/services/board/orchestrator.service.d.ts +119 -0
  371. package/dist/services/board/orchestrator.service.d.ts.map +1 -0
  372. package/dist/services/board/orchestrator.service.js +387 -0
  373. package/dist/services/board/orchestrator.service.js.map +1 -0
  374. package/dist/services/board/release-worker.d.ts +43 -0
  375. package/dist/services/board/release-worker.d.ts.map +1 -0
  376. package/dist/services/board/release-worker.js +96 -0
  377. package/dist/services/board/release-worker.js.map +1 -0
  378. package/dist/services/board/report-capture.d.ts +94 -0
  379. package/dist/services/board/report-capture.d.ts.map +1 -0
  380. package/dist/services/board/report-capture.js +398 -0
  381. package/dist/services/board/report-capture.js.map +1 -0
  382. package/dist/services/board/worker-spawn.d.ts +89 -0
  383. package/dist/services/board/worker-spawn.d.ts.map +1 -0
  384. package/dist/services/board/worker-spawn.js +177 -0
  385. package/dist/services/board/worker-spawn.js.map +1 -0
  386. package/dist/services/board/worktree.d.ts +45 -0
  387. package/dist/services/board/worktree.d.ts.map +1 -0
  388. package/dist/services/board/worktree.js +183 -0
  389. package/dist/services/board/worktree.js.map +1 -0
  390. package/dist/services/broker-client.d.ts +84 -2
  391. package/dist/services/broker-client.d.ts.map +1 -1
  392. package/dist/services/broker-client.js +72 -6
  393. package/dist/services/broker-client.js.map +1 -1
  394. package/dist/services/claude-spawn.service.d.ts +5 -0
  395. package/dist/services/claude-spawn.service.d.ts.map +1 -1
  396. package/dist/services/claude-spawn.service.js +37 -21
  397. package/dist/services/claude-spawn.service.js.map +1 -1
  398. package/dist/services/codex-spawn.service.d.ts +8 -3
  399. package/dist/services/codex-spawn.service.d.ts.map +1 -1
  400. package/dist/services/codex-spawn.service.js +84 -67
  401. package/dist/services/codex-spawn.service.js.map +1 -1
  402. package/dist/services/consolidation.service.d.ts.map +1 -1
  403. package/dist/services/consolidation.service.js +5 -49
  404. package/dist/services/consolidation.service.js.map +1 -1
  405. package/dist/services/copilot-spawn.service.d.ts +1 -1
  406. package/dist/services/copilot-spawn.service.d.ts.map +1 -1
  407. package/dist/services/copilot-spawn.service.js +10 -5
  408. package/dist/services/copilot-spawn.service.js.map +1 -1
  409. package/dist/services/daily-session.d.ts +24 -11
  410. package/dist/services/daily-session.d.ts.map +1 -1
  411. package/dist/services/daily-session.js +113 -33
  412. package/dist/services/daily-session.js.map +1 -1
  413. package/dist/services/death-alert.service.d.ts +55 -0
  414. package/dist/services/death-alert.service.d.ts.map +1 -0
  415. package/dist/services/death-alert.service.js +64 -0
  416. package/dist/services/death-alert.service.js.map +1 -0
  417. package/dist/services/dispatcher/broker.d.ts +66 -0
  418. package/dist/services/dispatcher/broker.d.ts.map +1 -1
  419. package/dist/services/dispatcher/broker.js +60 -52
  420. package/dist/services/dispatcher/broker.js.map +1 -1
  421. package/dist/services/dispatcher/poller.d.ts.map +1 -1
  422. package/dist/services/dispatcher/poller.js +31 -24
  423. package/dist/services/dispatcher/poller.js.map +1 -1
  424. package/dist/services/http-retry.d.ts +1 -90
  425. package/dist/services/http-retry.d.ts.map +1 -1
  426. package/dist/services/http-retry.js +1 -176
  427. package/dist/services/http-retry.js.map +1 -1
  428. package/dist/services/intent.d.ts +15 -1
  429. package/dist/services/intent.d.ts.map +1 -1
  430. package/dist/services/intent.js +39 -27
  431. package/dist/services/intent.js.map +1 -1
  432. package/dist/services/keeper.service.d.ts +76 -3
  433. package/dist/services/keeper.service.d.ts.map +1 -1
  434. package/dist/services/keeper.service.js +112 -35
  435. package/dist/services/keeper.service.js.map +1 -1
  436. package/dist/services/mcp-config.service.d.ts.map +1 -1
  437. package/dist/services/mcp-config.service.js +14 -1
  438. package/dist/services/mcp-config.service.js.map +1 -1
  439. package/dist/services/memory-briefing.service.d.ts +1 -1
  440. package/dist/services/memory-briefing.service.d.ts.map +1 -1
  441. package/dist/services/memory-briefing.service.js +28 -73
  442. package/dist/services/memory-briefing.service.js.map +1 -1
  443. package/dist/services/passive-notice.service.d.ts +56 -0
  444. package/dist/services/passive-notice.service.d.ts.map +1 -0
  445. package/dist/services/passive-notice.service.js +92 -0
  446. package/dist/services/passive-notice.service.js.map +1 -0
  447. package/dist/services/predefined-prompts.d.ts +11 -0
  448. package/dist/services/predefined-prompts.d.ts.map +1 -0
  449. package/dist/services/predefined-prompts.js +25 -0
  450. package/dist/services/predefined-prompts.js.map +1 -0
  451. package/dist/services/process.service.d.ts +111 -27
  452. package/dist/services/process.service.d.ts.map +1 -1
  453. package/dist/services/process.service.js +245 -173
  454. package/dist/services/process.service.js.map +1 -1
  455. package/dist/services/spawn-common.d.ts +281 -5
  456. package/dist/services/spawn-common.d.ts.map +1 -1
  457. package/dist/services/spawn-common.js +884 -188
  458. package/dist/services/spawn-common.js.map +1 -1
  459. package/dist/services/spawn-exit-watcher.d.ts +45 -35
  460. package/dist/services/spawn-exit-watcher.d.ts.map +1 -1
  461. package/dist/services/spawn-exit-watcher.js +120 -100
  462. package/dist/services/spawn-exit-watcher.js.map +1 -1
  463. package/dist/services/telegram.d.ts +31 -8
  464. package/dist/services/telegram.d.ts.map +1 -1
  465. package/dist/services/telegram.js +58 -26
  466. package/dist/services/telegram.js.map +1 -1
  467. package/dist/services/thread-health.service.d.ts +8 -0
  468. package/dist/services/thread-health.service.d.ts.map +1 -1
  469. package/dist/services/thread-health.service.js +14 -10
  470. package/dist/services/thread-health.service.js.map +1 -1
  471. package/dist/services/thread-lifecycle.service.d.ts +17 -1
  472. package/dist/services/thread-lifecycle.service.d.ts.map +1 -1
  473. package/dist/services/thread-lifecycle.service.js +47 -6
  474. package/dist/services/thread-lifecycle.service.js.map +1 -1
  475. package/dist/services/thread-log-reader.d.ts +81 -0
  476. package/dist/services/thread-log-reader.d.ts.map +1 -0
  477. package/dist/services/thread-log-reader.js +141 -0
  478. package/dist/services/thread-log-reader.js.map +1 -0
  479. package/dist/services/thread-purge.service.d.ts +200 -0
  480. package/dist/services/thread-purge.service.d.ts.map +1 -0
  481. package/dist/services/thread-purge.service.js +644 -0
  482. package/dist/services/thread-purge.service.js.map +1 -0
  483. package/dist/services/topic.service.d.ts +44 -2
  484. package/dist/services/topic.service.d.ts.map +1 -1
  485. package/dist/services/topic.service.js +74 -13
  486. package/dist/services/topic.service.js.map +1 -1
  487. package/dist/services/usage/usage-inbox-notifier.d.ts +50 -0
  488. package/dist/services/usage/usage-inbox-notifier.d.ts.map +1 -0
  489. package/dist/services/usage/usage-inbox-notifier.js +118 -0
  490. package/dist/services/usage/usage-inbox-notifier.js.map +1 -0
  491. package/dist/services/usage/usage-poller.service.d.ts +41 -0
  492. package/dist/services/usage/usage-poller.service.d.ts.map +1 -0
  493. package/dist/services/usage/usage-poller.service.js +67 -0
  494. package/dist/services/usage/usage-poller.service.js.map +1 -0
  495. package/dist/services/usage/usage-snapshot-provider.d.ts +40 -0
  496. package/dist/services/usage/usage-snapshot-provider.d.ts.map +1 -0
  497. package/dist/services/usage/usage-snapshot-provider.js +89 -0
  498. package/dist/services/usage/usage-snapshot-provider.js.map +1 -0
  499. package/dist/services/usage/usage-types.d.ts +28 -0
  500. package/dist/services/usage/usage-types.d.ts.map +1 -0
  501. package/dist/services/usage/usage-types.js +14 -0
  502. package/dist/services/usage/usage-types.js.map +1 -0
  503. package/dist/services/worker-birth.d.ts +154 -0
  504. package/dist/services/worker-birth.d.ts.map +1 -0
  505. package/dist/services/worker-birth.js +274 -0
  506. package/dist/services/worker-birth.js.map +1 -0
  507. package/dist/services/worker-cleanup.service.d.ts +242 -19
  508. package/dist/services/worker-cleanup.service.d.ts.map +1 -1
  509. package/dist/services/worker-cleanup.service.js +893 -355
  510. package/dist/services/worker-cleanup.service.js.map +1 -1
  511. package/dist/services/worker-lifecycle-executor.d.ts +42 -0
  512. package/dist/services/worker-lifecycle-executor.d.ts.map +1 -0
  513. package/dist/services/worker-lifecycle-executor.js +63 -0
  514. package/dist/services/worker-lifecycle-executor.js.map +1 -0
  515. package/dist/services/worker-single-use.d.ts +92 -0
  516. package/dist/services/worker-single-use.d.ts.map +1 -0
  517. package/dist/services/worker-single-use.js +155 -0
  518. package/dist/services/worker-single-use.js.map +1 -0
  519. package/dist/sessions/mcp-registry.d.ts +33 -2
  520. package/dist/sessions/mcp-registry.d.ts.map +1 -1
  521. package/dist/sessions/mcp-registry.js +153 -30
  522. package/dist/sessions/mcp-registry.js.map +1 -1
  523. package/dist/test-bootstrap.d.ts +19 -12
  524. package/dist/test-bootstrap.d.ts.map +1 -1
  525. package/dist/test-bootstrap.js +69 -21
  526. package/dist/test-bootstrap.js.map +1 -1
  527. package/dist/test-tripwire.test-support.d.ts +75 -0
  528. package/dist/test-tripwire.test-support.d.ts.map +1 -0
  529. package/dist/test-tripwire.test-support.js +353 -0
  530. package/dist/test-tripwire.test-support.js.map +1 -0
  531. package/dist/tools/defs/memory-defs.d.ts.map +1 -1
  532. package/dist/tools/defs/memory-defs.js +12 -4
  533. package/dist/tools/defs/memory-defs.js.map +1 -1
  534. package/dist/tools/defs/session-defs.d.ts.map +1 -1
  535. package/dist/tools/defs/session-defs.js +26 -10
  536. package/dist/tools/defs/session-defs.js.map +1 -1
  537. package/dist/tools/defs/utility-defs.js +1 -1
  538. package/dist/tools/defs/utility-defs.js.map +1 -1
  539. package/dist/tools/delegate-tool.d.ts +81 -1
  540. package/dist/tools/delegate-tool.d.ts.map +1 -1
  541. package/dist/tools/delegate-tool.js +840 -219
  542. package/dist/tools/delegate-tool.js.map +1 -1
  543. package/dist/tools/memory-tools.d.ts.map +1 -1
  544. package/dist/tools/memory-tools.js +66 -9
  545. package/dist/tools/memory-tools.js.map +1 -1
  546. package/dist/tools/response-builders.d.ts.map +1 -1
  547. package/dist/tools/response-builders.js +12 -8
  548. package/dist/tools/response-builders.js.map +1 -1
  549. package/dist/tools/session-tools.d.ts.map +1 -1
  550. package/dist/tools/session-tools.js +3 -32
  551. package/dist/tools/session-tools.js.map +1 -1
  552. package/dist/tools/skill-tools.js +1 -1
  553. package/dist/tools/skill-tools.js.map +1 -1
  554. package/dist/tools/start-session-tool.d.ts.map +1 -1
  555. package/dist/tools/start-session-tool.js +66 -20
  556. package/dist/tools/start-session-tool.js.map +1 -1
  557. package/dist/tools/utility-tools.d.ts.map +1 -1
  558. package/dist/tools/utility-tools.js +38 -3
  559. package/dist/tools/utility-tools.js.map +1 -1
  560. package/dist/tools/wait/auto-forward.d.ts.map +1 -1
  561. package/dist/tools/wait/auto-forward.js +8 -3
  562. package/dist/tools/wait/auto-forward.js.map +1 -1
  563. package/dist/tools/wait/consolidation-strategy.d.ts +42 -0
  564. package/dist/tools/wait/consolidation-strategy.d.ts.map +1 -0
  565. package/dist/tools/wait/consolidation-strategy.js +50 -0
  566. package/dist/tools/wait/consolidation-strategy.js.map +1 -0
  567. package/dist/tools/wait/drive-handler.d.ts +5 -7
  568. package/dist/tools/wait/drive-handler.d.ts.map +1 -1
  569. package/dist/tools/wait/drive-handler.js +45 -59
  570. package/dist/tools/wait/drive-handler.js.map +1 -1
  571. package/dist/tools/wait/media-processor.d.ts.map +1 -1
  572. package/dist/tools/wait/media-processor.js +8 -4
  573. package/dist/tools/wait/media-processor.js.map +1 -1
  574. package/dist/tools/wait/message-delivery.d.ts +9 -6
  575. package/dist/tools/wait/message-delivery.d.ts.map +1 -1
  576. package/dist/tools/wait/message-delivery.js +77 -58
  577. package/dist/tools/wait/message-delivery.js.map +1 -1
  578. package/dist/tools/wait/message-processing.d.ts.map +1 -1
  579. package/dist/tools/wait/message-processing.js +2 -1
  580. package/dist/tools/wait/message-processing.js.map +1 -1
  581. package/dist/tools/wait/poll-loop.d.ts.map +1 -1
  582. package/dist/tools/wait/poll-loop.js +339 -239
  583. package/dist/tools/wait/poll-loop.js.map +1 -1
  584. package/package.json +4 -3
  585. package/templates/azure-devops-securevault.default.md +20 -6
  586. package/templates/board-quality-checklist.default.md +121 -0
  587. package/templates/buildless-vue-preview.default.md +47 -0
  588. package/templates/expert-code-review.default.md +12 -0
  589. package/templates/handoff.default.md +41 -18
  590. package/templates/increment-pr-pipeline.default.md +79 -0
  591. package/templates/swarm.default.md +1 -0
  592. package/templates/worker-autonomous.default.md +1 -0
  593. package/templates/worker-delegate.default.md +1 -0
  594. package/dist/dashboard/log-parser.d.ts +0 -65
  595. package/dist/dashboard/log-parser.d.ts.map +0 -1
  596. package/dist/dashboard/log-parser.js +0 -219
  597. package/dist/dashboard/log-parser.js.map +0 -1
  598. package/dist/data/memory/reflection-dedup.d.ts +0 -17
  599. package/dist/data/memory/reflection-dedup.d.ts.map +0 -1
  600. package/dist/data/memory/reflection-dedup.js +0 -55
  601. package/dist/data/memory/reflection-dedup.js.map +0 -1
  602. package/dist/data/memory/reflection-gates.d.ts +0 -40
  603. package/dist/data/memory/reflection-gates.d.ts.map +0 -1
  604. package/dist/data/memory/reflection-gates.js +0 -220
  605. package/dist/data/memory/reflection-gates.js.map +0 -1
  606. package/dist/data/memory/reflection-sources.d.ts +0 -13
  607. package/dist/data/memory/reflection-sources.d.ts.map +0 -1
  608. package/dist/data/memory/reflection-sources.js +0 -115
  609. package/dist/data/memory/reflection-sources.js.map +0 -1
  610. package/dist/data/memory/reflection.d.ts +0 -43
  611. package/dist/data/memory/reflection.d.ts.map +0 -1
  612. package/dist/data/memory/reflection.js +0 -329
  613. package/dist/data/memory/reflection.js.map +0 -1
  614. package/dist/llm/prompts/reflection.d.ts +0 -6
  615. package/dist/llm/prompts/reflection.d.ts.map +0 -1
  616. package/dist/llm/prompts/reflection.js +0 -77
  617. package/dist/llm/prompts/reflection.js.map +0 -1
  618. package/dist/services/memory-pruning.service.d.ts +0 -12
  619. package/dist/services/memory-pruning.service.d.ts.map +0 -1
  620. package/dist/services/memory-pruning.service.js +0 -179
  621. package/dist/services/memory-pruning.service.js.map +0 -1
  622. package/templates/daily-review.default.md +0 -26
  623. package/templates/fire-and-forget.default.md +0 -76
  624. package/templates/offload.default.md +0 -33
  625. package/templates/sensorium-repair.default.md +0 -114
@@ -5,7 +5,9 @@
5
5
  * (root, daily, branch, worker) with their configuration and status.
6
6
  */
7
7
  import { nowISO } from "./utils.js";
8
+ import { Optional } from "../optional.js";
8
9
  import { restoreNotesForThread } from "./note-archival.js";
10
+ import { MINTED_THREAD_ID_FLOOR } from "./schema-ddl.js";
9
11
  export class ThreadRepository {
10
12
  registerThread(db, entry) {
11
13
  return registerThread(db, entry);
@@ -35,6 +37,7 @@ function rowToEntry(row) {
35
37
  name: row.name,
36
38
  type: row.type,
37
39
  rootThreadId: row.root_thread_id ?? null,
40
+ notifyThreadId: row.notify_thread_id ?? null, // eslint-disable-line no-restricted-syntax -- nullable DB column, mirrors sibling rootThreadId at the thread_registry row boundary
38
41
  badge: row.badge,
39
42
  client: row.client,
40
43
  maxRetries: row.max_retries,
@@ -55,21 +58,54 @@ function rowToEntry(row) {
55
58
  };
56
59
  }
57
60
  // ─── CRUD ────────────────────────────────────────────────────────────────────
61
+ /**
62
+ * The row seam, and the one place the minted-id rule is stated for `thread_registry`.
63
+ *
64
+ * A worker's identity is MINTED ({@link mintThreadId}, called from `birthWorker`), so a
65
+ * `type = 'worker'` row below {@link MINTED_THREAD_ID_FLOOR} is not a worker anything
66
+ * mints — it is a Telegram topic number wearing a worker's type, and every id-reuse
67
+ * failure this line has chased (a worker inheriting an earlier occupant's lifecycle, a
68
+ * sweep deleting a live thread's forum topic because the id IS the topic) starts there.
69
+ *
70
+ * It is refused HERE because this statement is where `type` enters the table: the INSERT
71
+ * writes `entry.type`, and the ON CONFLICT arm below either PRESERVES the stored type
72
+ * (while `keep_alive = 1`) or writes `excluded.type`, which is this same `entry.type`.
73
+ * No other writer introduces one — `UpdateThreadPatch` has no `type` field, and
74
+ * `handleConvertToRoot`'s direct UPDATE writes the literal 'root'. A preserved type was
75
+ * therefore introduced by an earlier call through this guard, or predates migration 39,
76
+ * which purged what did.
77
+ *
78
+ * It THROWS where the entrances above it answer in prose. `handleCreateThread` and
79
+ * `handleStartThread` refuse their own callers with a sentence naming the one way to get
80
+ * a worker, because those callers are an agent or an HTTP client that can act on it;
81
+ * an entry arriving here already carries the contradiction, and there is no row this
82
+ * function could write instead that the caller asked for.
83
+ */
58
84
  export function registerThread(db, entry) {
85
+ if (entry.type === "worker" && entry.threadId < MINTED_THREAD_ID_FLOOR) {
86
+ throw new Error(`registerThread: refusing to write a worker row on thread_id ${entry.threadId}, which is below ` +
87
+ `MINTED_THREAD_ID_FLOOR (${MINTED_THREAD_ID_FLOOR}) — a worker's id is minted, and a number in ` +
88
+ `the Telegram topic range names a topic rather than a worker. Mint the id first (birthWorker), ` +
89
+ `or register this row as the type it actually is.`);
90
+ }
59
91
  const now = nowISO();
60
92
  // Root and branch threads are the persistent tier: keep-alive (watcher
61
- // respawn) and daily rotation (consolidation) are on by default. Daily and
62
- // worker threads stay off. Explicit values still win over the type default.
93
+ // respawn) is on by default. The nightly MEMORY pass runs for every
94
+ // non-worker thread regardless of this flag (see getConsolidationThreads),
95
+ // so daily_rotation now only gates the OPT-IN session RESTART and therefore
96
+ // defaults OFF — a fresh root is never force-restarted unless explicitly
97
+ // enabled. Explicit values still win over the type default.
63
98
  const persistentByDefault = entry.type === "root" || entry.type === "branch";
64
- const dailyRotation = entry.dailyRotation ?? persistentByDefault;
99
+ const dailyRotation = entry.dailyRotation ?? false;
65
100
  const keepAlive = entry.keepAlive ?? persistentByDefault;
66
101
  db.prepare(`INSERT INTO thread_registry
67
- (thread_id, name, type, root_thread_id, badge, client, max_retries, cooldown_ms, keep_alive, daily_rotation, working_directory, created_at, last_active_at, status)
68
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active')
102
+ (thread_id, name, type, root_thread_id, notify_thread_id, badge, client, max_retries, cooldown_ms, keep_alive, daily_rotation, working_directory, created_at, last_active_at, status)
103
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active')
69
104
  ON CONFLICT(thread_id) DO UPDATE SET
70
105
  name = excluded.name,
71
106
  type = CASE WHEN thread_registry.keep_alive = 1 THEN thread_registry.type ELSE excluded.type END,
72
107
  root_thread_id = excluded.root_thread_id,
108
+ notify_thread_id = excluded.notify_thread_id,
73
109
  badge = excluded.badge,
74
110
  client = CASE WHEN thread_registry.keep_alive = 1 THEN thread_registry.client ELSE excluded.client END,
75
111
  max_retries = excluded.max_retries,
@@ -78,17 +114,82 @@ export function registerThread(db, entry) {
78
114
  daily_rotation = CASE WHEN thread_registry.keep_alive = 1 THEN thread_registry.daily_rotation ELSE excluded.daily_rotation END,
79
115
  working_directory = COALESCE(excluded.working_directory, thread_registry.working_directory),
80
116
  last_active_at = excluded.last_active_at,
81
- status = 'active'`).run(entry.threadId, entry.name, entry.type, entry.rootThreadId ?? null, entry.badge ?? "root", entry.client ?? "claude", entry.maxRetries ?? 5, entry.cooldownMs ?? 300000, keepAlive ? 1 : 0, dailyRotation ? 1 : 0, entry.workingDirectory ?? null, now, now);
117
+ status = 'active',
118
+ -- Re-registering an archived thread revives it, so its archive stamp is
119
+ -- no longer true. Cleared with the status in the same statement, keeping
120
+ -- "archived_at is set exactly while status = 'archived'" an invariant the
121
+ -- purge anchor can rely on.
122
+ archived_at = NULL
123
+ -- A row a purge has marked is not revived: its files are being removed and
124
+ -- its rows are next (see "The row is the lock" below).
125
+ WHERE thread_registry.purging_at IS NULL`).run(entry.threadId, entry.name, entry.type, entry.rootThreadId ?? null, entry.notifyThreadId ?? null, // eslint-disable-line no-restricted-syntax -- nullable DB column, mirrors sibling rootThreadId at the thread_registry row boundary
126
+ entry.badge ?? "root", entry.client ?? "claude", entry.maxRetries ?? 5, entry.cooldownMs ?? 300000, keepAlive ? 1 : 0, dailyRotation ? 1 : 0, entry.workingDirectory ?? null, now, now);
127
+ // The upsert's WHERE leaves a row a purge has marked untouched, and a marked row
128
+ // is still archived, so reading the mark back is the refusal.
129
+ if (isThreadMarkedForPurge(db, entry.threadId)) {
130
+ throw new Error(`registerThread: thread ${entry.threadId} is being permanently deleted — register it again once the delete has finished`);
131
+ }
82
132
  const result = getThread(db, entry.threadId);
83
133
  if (!result) {
84
134
  throw new Error(`registerThread: failed to retrieve thread after insert (threadId=${entry.threadId})`);
85
135
  }
86
136
  return result;
87
137
  }
138
+ /**
139
+ * Allocate a thread id that no thread has ever held and none ever will again —
140
+ * GIVEN the allocating INSERT autocommits, which is what the first guard enforces.
141
+ * Inside an enclosing `db.transaction`, SQLite's AUTOINCREMENT mark rolls back with
142
+ * everything else and the next mint is handed the same number (measured), so the
143
+ * promise is not available mid-transaction and is refused rather than weakened.
144
+ *
145
+ * That refusal breaks no correct caller, because the sound order is mint, commit,
146
+ * then register: the id has to be durable before any row, file, process or wire
147
+ * message names it. A caller wanting mint-and-register atomically still mints first
148
+ * and tolerates the gap on a crash — a minted-never-used id is this design's native
149
+ * waste product, and never reusing one is exactly what makes those gaps free.
150
+ *
151
+ * Why the sequence is shaped the way it is — and why the mark survives a hard
152
+ * `deleteThread` and the 180-day purge — is argued on THREAD_ID_SEQUENCE_DDL. The
153
+ * trim that keeps the table at one row carries a WHERE and so cannot lower it.
154
+ *
155
+ * The floor is asserted rather than assumed. An id below it is a Telegram topic
156
+ * number as far as every reader of `thread_registry.thread_id` is concerned, and the
157
+ * two states that produce one — an unseeded sequence, or a lowered floor — are
158
+ * both silent. Refusing to mint is the only answer a caller cannot paper over.
159
+ */
160
+ export function mintThreadId(db) {
161
+ if (db.inTransaction) {
162
+ throw new Error(`mintThreadId: refusing to mint inside a transaction — a rollback would discard the ` +
163
+ `allocating row and reissue its id, so the id would not be mint-once. Mint and let it ` +
164
+ `commit first, then open the transaction that records it.`);
165
+ }
166
+ const allocated = db
167
+ .prepare(`INSERT INTO thread_id_sequence (minted_at) VALUES (?)`)
168
+ .run(nowISO()).lastInsertRowid;
169
+ const minted = Number(allocated);
170
+ if (!Number.isSafeInteger(minted) || minted < MINTED_THREAD_ID_FLOOR) {
171
+ throw new Error(`mintThreadId: thread_id_sequence produced ${allocated}, which is not a safe integer at or above MINTED_THREAD_ID_FLOOR (${MINTED_THREAD_ID_FLOOR})`);
172
+ }
173
+ db.prepare(`DELETE FROM thread_id_sequence WHERE id < ?`).run(minted);
174
+ return minted;
175
+ }
88
176
  export function getThread(db, threadId) {
89
177
  const row = db.prepare(`SELECT * FROM thread_registry WHERE thread_id = ?`).get(threadId);
90
178
  return row ? rowToEntry(row) : null;
91
179
  }
180
+ /** Registry badge marking an ephemeral task-board card worker. */
181
+ export const BOARD_WORKER_BADGE = "board-worker";
182
+ /**
183
+ * Whether a thread's own episodic memory writes are suppressed. Currently true
184
+ * for ephemeral board-card workers, whose state lives in card comments rather
185
+ * than Sensorium episodes. Named for the memory concern so the episode layer
186
+ * asks a memory-domain question and never learns the board badge value.
187
+ */
188
+ export function suppressesEpisodicMemory(db, threadId) {
189
+ return Optional.fromNullable(getThread(db, threadId))
190
+ .map((thread) => thread.badge === BOARD_WORKER_BADGE)
191
+ .orElse(false);
192
+ }
92
193
  export function getThreadByName(db, name) {
93
194
  const row = db
94
195
  .prepare(`SELECT * FROM thread_registry WHERE name = ? AND status = 'active' ORDER BY last_active_at DESC LIMIT 1`)
@@ -107,9 +208,15 @@ export function getRootThreads(db) {
107
208
  .all();
108
209
  return rows.map(rowToEntry);
109
210
  }
110
- export function getDailyRotationThreads(db) {
211
+ /**
212
+ * All non-worker, non-archived threads. The nightly memory pass (consolidation,
213
+ * reflection, narrative) runs for every one of these — memory integrity is
214
+ * required by all threads, not just those opted into session rotation. Workers
215
+ * are excluded because their memory is no-persist (discarded on decommission).
216
+ */
217
+ export function getConsolidationThreads(db) {
111
218
  const rows = db
112
- .prepare(`SELECT * FROM thread_registry WHERE daily_rotation = 1 AND type != 'worker' AND status != 'archived' ORDER BY created_at DESC`)
219
+ .prepare(`SELECT * FROM thread_registry WHERE type != 'worker' AND status != 'archived' ORDER BY created_at DESC`)
113
220
  .all();
114
221
  return rows.map(rowToEntry);
115
222
  }
@@ -146,8 +253,13 @@ export function updateThread(db, threadId, updates) {
146
253
  params.push(updates.name);
147
254
  }
148
255
  if (updates.status !== undefined) {
149
- setClauses.push("status = ?");
150
- params.push(updates.status);
256
+ // `archived_at` is DERIVED from the status in this one general status write, so
257
+ // "archived_at is set exactly while status = 'archived'" holds for every path that
258
+ // archives or revives a thread through here, with no caller stamping it by hand.
259
+ // It is the purge window's anchor. A row that is already archived keeps its
260
+ // stamp (COALESCE): re-archiving does not restart the clock it was archived on.
261
+ setClauses.push("status = ?", "archived_at = CASE WHEN ? = 'archived' THEN COALESCE(archived_at, ?) END");
262
+ params.push(updates.status, updates.status, nowISO());
151
263
  }
152
264
  if (updates.lastActiveAt !== undefined) {
153
265
  setClauses.push("last_active_at = ?");
@@ -200,14 +312,25 @@ export function updateThread(db, threadId, updates) {
200
312
  if (setClauses.length === 0)
201
313
  return false;
202
314
  params.push(threadId);
315
+ // A status write is a door like every start: a row a purge has marked keeps its
316
+ // status, or the sweep's mid-purge finisher would delete a thread revived here.
317
+ // And a 'spawning' row is held by a start (claimThreadStart): archiving it would
318
+ // hand it to the purge while that start is still writing the id's files (inc66 M2).
319
+ const statusGuard = updates.status === undefined
320
+ ? ""
321
+ : " AND purging_at IS NULL" +
322
+ (updates.status === "archived" ? " AND status <> 'spawning'" : "");
203
323
  const result = db
204
- .prepare(`UPDATE thread_registry SET ${setClauses.join(", ")} WHERE thread_id = ?`)
324
+ .prepare(`UPDATE thread_registry SET ${setClauses.join(", ")} WHERE thread_id = ?${statusGuard}`)
205
325
  .run(...params);
206
326
  return result.changes > 0;
207
327
  }
328
+ /** Archive a row. False when there is no row, a start holds it ('spawning'), or a
329
+ * purge has marked it — re-stamping archived_at under a purge would be a false 200. */
208
330
  export function archiveThread(db, threadId) {
209
331
  const result = db
210
- .prepare(`UPDATE thread_registry SET status = 'archived', keep_alive = 0, clean_shutdown = 1, archived_at = ? WHERE thread_id = ?`)
332
+ .prepare(`UPDATE thread_registry SET status = 'archived', keep_alive = 0, clean_shutdown = 1, archived_at = ?
333
+ WHERE thread_id = ? AND status <> 'spawning' AND purging_at IS NULL`)
211
334
  .run(nowISO(), threadId);
212
335
  return result.changes > 0;
213
336
  }
@@ -219,7 +342,8 @@ export function unarchiveThread(db, threadId) {
219
342
  // transaction rolls the status change back on any restore failure.
220
343
  return db.transaction(() => {
221
344
  const result = db
222
- .prepare(`UPDATE thread_registry SET status = 'active', keep_alive = 1, archived_at = NULL WHERE thread_id = ? AND status = 'archived'`)
345
+ .prepare(`UPDATE thread_registry SET status = 'active', keep_alive = 1, archived_at = NULL
346
+ WHERE thread_id = ? AND status = 'archived' AND purging_at IS NULL`)
223
347
  .run(threadId);
224
348
  if (result.changes === 0)
225
349
  return false;
@@ -232,50 +356,219 @@ export function unarchiveThread(db, threadId) {
232
356
  return true;
233
357
  })();
234
358
  }
235
- export function getArchivedThreads(db) {
359
+ /**
360
+ * Archived threads, workers EXCLUDED by default.
361
+ *
362
+ * The exclusion is right for the overview this feed was written for — a swarm
363
+ * run leaves dozens of decommissioned workers that would bury the handful of
364
+ * roots and branches an operator actually recognises. It was wrong as the ONLY
365
+ * answer: this is the single feed behind the archived-threads tab, which is the
366
+ * single place the permanent delete can be invoked from, and 220 of the 225
367
+ * per-thread config dirs on the live host (230 of its 320 MB) belong to exactly
368
+ * the archived workers it hid. The delete API itself never had a type filter —
369
+ * only the list did — so the bytes were unreachable rather than protected.
370
+ *
371
+ * `includeWorkers` is opt-in so every existing caller keeps the behaviour it
372
+ * was written against.
373
+ */
374
+ export function getArchivedThreads(db, opts = {}) {
375
+ const workerFilter = opts.includeWorkers ? "" : ` AND type != 'worker'`;
236
376
  const rows = db
237
- .prepare(`SELECT * FROM thread_registry WHERE status = 'archived' AND type != 'worker' ORDER BY archived_at DESC, last_active_at DESC`)
377
+ .prepare(`SELECT * FROM thread_registry WHERE status = 'archived'${workerFilter}
378
+ ORDER BY archived_at DESC, last_active_at DESC`)
238
379
  .all();
239
380
  return rows.map(rowToEntry);
240
381
  }
382
+ /**
383
+ * Drop ONLY the registry row. Not a purge, and not reachable from the operator's
384
+ * delete any more — that goes through `purgeThread`, which takes every dependent
385
+ * table with it.
386
+ *
387
+ * It stays because worker-birth's PRE-SPAWN teardown wants exactly this: a row it
388
+ * registered moments ago, for a worker no process has run as, so nothing has been
389
+ * written against the id yet and there is nothing for a purge to find.
390
+ * `purgeThread` would refuse that row (no purge marked it: it is active, not
391
+ * archived). Do not reach for this to retire a thread that has ever run.
392
+ */
241
393
  export function deleteThread(db, threadId) {
242
394
  const result = db.prepare(`DELETE FROM thread_registry WHERE thread_id = ?`).run(threadId);
243
395
  return result.changes > 0;
244
396
  }
397
+ // ─── Purge ───────────────────────────────────────────────────────────────────
245
398
  /**
246
- * Purge archived threads older than `maxAgeMs` (default 180 days).
247
- * Deletes the thread's associated data and registry entry.
248
- * Returns the number of threads purged.
399
+ * How long an archived thread survives before the daily sweep purges it.
400
+ *
401
+ * Was 180 days. The sweep is a floor, not the only way out — an archived thread
402
+ * now also has an explicit, operator-driven permanent delete (purgeThread +
403
+ * purgeThreadFiles, surfaced as `DELETE /api/threads/:id?hard=true`). With a
404
+ * deliberate path available, the automatic one no longer has to be the thing
405
+ * that reclaims disk, so the window is long enough that nothing is ever lost to
406
+ * a timer the operator forgot about. Exported so operator-facing text states
407
+ * the real number instead of a stale literal.
249
408
  */
250
- export function purgeOldArchivedThreads(db, maxAgeMs = 180 * 24 * 60 * 60 * 1000) {
251
- const cutoff = new Date(Date.now() - maxAgeMs).toISOString();
252
- const rows = db
253
- .prepare(`SELECT thread_id FROM thread_registry
254
- WHERE status = 'archived' AND last_active_at < ?`)
255
- .all(cutoff);
256
- if (rows.length === 0)
257
- return 0;
258
- const deleteEmbeddings = db.prepare(`DELETE FROM note_embeddings WHERE note_id IN (SELECT note_id FROM semantic_notes WHERE thread_id = ?)`);
259
- const deleteNarratives = db.prepare(`DELETE FROM temporal_narratives WHERE thread_id = ?`);
260
- const deleteVoiceSignatures = db.prepare(`DELETE FROM voice_signatures WHERE episode_id IN (SELECT episode_id FROM episodes WHERE thread_id = ?)`);
261
- const deleteNotes = db.prepare(`DELETE FROM semantic_notes WHERE thread_id = ?`);
262
- const deleteEpisodes = db.prepare(`DELETE FROM episodes WHERE thread_id = ?`);
263
- const deleteSentMessages = db.prepare(`DELETE FROM sent_messages WHERE thread_id = ?`);
264
- const deleteTopicRegistry = db.prepare(`DELETE FROM topic_registry WHERE thread_id = ?`);
265
- const deleteRegistry = db.prepare(`DELETE FROM thread_registry WHERE thread_id = ?`);
266
- db.transaction(() => {
267
- for (const { thread_id } of rows) {
268
- deleteEmbeddings.run(thread_id);
269
- deleteNarratives.run(thread_id);
270
- deleteVoiceSignatures.run(thread_id);
271
- deleteNotes.run(thread_id);
272
- deleteEpisodes.run(thread_id);
273
- deleteSentMessages.run(thread_id);
274
- deleteTopicRegistry.run(thread_id);
275
- deleteRegistry.run(thread_id);
409
+ export const ARCHIVED_THREAD_PURGE_DAYS = 730;
410
+ /**
411
+ * Every table that stores rows keyed to one thread, in an order that clears
412
+ * child rows before their parents (note_embeddings via semantic_notes,
413
+ * voice_signatures via episodes). The thread_registry row itself is not on
414
+ * this list: purgeThread deletes it FIRST, so that a purge whose row was never
415
+ * marked (or was taken by a start) has touched none of these.
416
+ *
417
+ * THIS IS THE ONE HOME FOR THE LIST. A table added here is purged by both the
418
+ * single-thread delete and the daily sweep; a table missed here orphans its
419
+ * rows in both.
420
+ */
421
+ const THREAD_DEPENDENT_DELETES = [
422
+ `DELETE FROM note_embeddings WHERE note_id IN (SELECT note_id FROM semantic_notes WHERE thread_id = ?)`,
423
+ `DELETE FROM temporal_narratives WHERE thread_id = ?`,
424
+ `DELETE FROM voice_signatures WHERE episode_id IN (SELECT episode_id FROM episodes WHERE thread_id = ?)`,
425
+ `DELETE FROM semantic_notes WHERE thread_id = ?`,
426
+ `DELETE FROM episodes WHERE thread_id = ?`,
427
+ `DELETE FROM sent_messages WHERE thread_id = ?`,
428
+ `DELETE FROM topic_registry WHERE thread_id = ?`,
429
+ // A worker's launch/death/release record. Purge runs only once no process can
430
+ // be alive for the thread (liveProcessRefusal), so nothing is left for the row
431
+ // to arbitrate. Dropping it also removes the worker from its card's
432
+ // prior-worker candidate set (listCardWorkers), which is correct for a worker
433
+ // proven gone.
434
+ `DELETE FROM worker_lifecycle WHERE thread_id = ?`,
435
+ // FYI lines queued for the thread's next turn, which it will never take.
436
+ `DELETE FROM passive_notices WHERE thread_id = ?`,
437
+ ];
438
+ // ─── The row is the lock ─────────────────────────────────────────────────────
439
+ //
440
+ // A purge and a start of the SAME id race on one question: who owns the id right
441
+ // now? The registry row answers it, and SQLite changes a row atomically. So both
442
+ // sides take the row with one conditional UPDATE BEFORE any side effect, and the
443
+ // loser sees `changes = 0` and touches nothing:
444
+ // - a purge marks the row (`purging_at`), then removes files, then deletes rows;
445
+ // - a start flips an archived row out of 'archived' before its first file write,
446
+ // and only while it is not marked.
447
+ // This holds across an `await` on either side, and across two server processes
448
+ // (a self-update overlap), which no in-memory lock can see. A crash mid-purge
449
+ // leaves the mark, and the next sweep finishes the purge (selectThreadsMidPurge).
450
+ // `purging_at` is a column, not a status, because a new status would mean
451
+ // rebuilding the table's CHECK.
452
+ /**
453
+ * Mark an archived row as being purged, answering the stamp written: the fence
454
+ * a finisher must still see to act. Empty means the purge lost: the row is not
455
+ * archived any more (a start claimed it), already marked, gone, or — when
456
+ * `archivedAt` is given — re-archived with a newer stamp than the one judged.
457
+ */
458
+ export function markThreadPurging(db, threadId, archivedAt) {
459
+ const stamp = archivedAt === undefined ? "" : " AND archived_at = ?";
460
+ const params = archivedAt === undefined ? [] : [archivedAt];
461
+ const purgingAt = nowISO();
462
+ const changes = db
463
+ .prepare(`UPDATE thread_registry SET purging_at = ?
464
+ WHERE thread_id = ? AND status = 'archived' AND purging_at IS NULL${stamp}`)
465
+ .run(purgingAt, threadId, ...params).changes;
466
+ return changes > 0 ? Optional.of(purgingAt) : Optional.empty();
467
+ }
468
+ /** True while a purge owns the row (markThreadPurging): every door refuses it. */
469
+ export function isThreadMarkedForPurge(db, threadId) {
470
+ return threadPurgeMark(db, threadId).has();
471
+ }
472
+ /** The mark a purge holds on the row now, or empty when none does. */
473
+ export function threadPurgeMark(db, threadId) {
474
+ return Optional.fromNullable(db
475
+ .prepare(`SELECT purging_at FROM thread_registry WHERE thread_id = ? AND purging_at IS NOT NULL`)
476
+ // eslint-disable-next-line no-restricted-syntax -- SQLite returns undefined for an absent row
477
+ .get(threadId)).map((marked) => marked.purging_at);
478
+ }
479
+ /** The resting states a start takes a row out of: everything a process is not on. */
480
+ const START_CLAIMABLE_STATUSES = ["archived", "exited", "created"];
481
+ /**
482
+ * Every start of an existing id calls this BEFORE its first file write. It flips
483
+ * a resting row to `status` ('spawning' for a spawn, 'active' for a session that
484
+ * revives its thread), which is a state a purge refuses to mark. Synchronous and
485
+ * in one transaction, so the read, the flip and the note restore see the same row.
486
+ * A revived ARCHIVED row gets its archived notes back, exactly as unarchiveThread.
487
+ */
488
+ export function claimThreadStart(db, threadId, status) {
489
+ return db.transaction(() => {
490
+ if (isThreadMarkedForPurge(db, threadId))
491
+ return { kind: "purging" };
492
+ const previous = Optional.fromNullable(getThread(db, threadId)).map((row) => row.status);
493
+ if (previous.orElse("") === "spawning")
494
+ return { kind: "starting" };
495
+ if (!previous.has() || !START_CLAIMABLE_STATUSES.includes(previous.get())) {
496
+ return { kind: "free" };
497
+ }
498
+ db.prepare(`UPDATE thread_registry SET status = ?, archived_at = NULL WHERE thread_id = ?`).run(status, threadId);
499
+ if (previous.get() === "archived") {
500
+ restoreNotesForThread(db, threadId, { includeUnknownLegacy: true });
276
501
  }
502
+ return { kind: "claimed", previous: previous.get(), claimedAs: status };
277
503
  })();
278
- return rows.length;
504
+ }
505
+ /** Hand a claimed row back to the status it was taken from when its start failed. */
506
+ export function releaseThreadStart(db, threadId, claim) {
507
+ if (claim.kind !== "claimed")
508
+ return;
509
+ db.prepare(`UPDATE thread_registry SET status = ?, archived_at = CASE WHEN ? = 'archived' THEN ? END
510
+ WHERE thread_id = ? AND status = ?`).run(claim.previous, claim.previous, nowISO(), threadId, claim.claimedAs);
511
+ }
512
+ /**
513
+ * Delete ONE thread's entire DB footprint — the registry row, then every table in
514
+ * THREAD_DEPENDENT_DELETES — in a single transaction. This is the LAST step of a
515
+ * purge: only a row still carrying the exact mark `purgingAt` this purge holds is
516
+ * deleted, so a row nobody marked, one a start took first, or a successor's row
517
+ * marked again matches nothing and the purge touches no dependent row. A row
518
+ * already gone means another purge finished it, and anything still keyed to the
519
+ * id is an orphan, which this cleans up. Returns true when this call deleted the
520
+ * registry row.
521
+ *
522
+ * DB only. On-disk per-thread artifacts are purgeThreadFiles' job
523
+ * (src/services/thread-purge.service.ts), and they are removed BEFORE this runs.
524
+ */
525
+ export function purgeThread(db, threadId, purgingAt) {
526
+ const dependents = THREAD_DEPENDENT_DELETES.map((sql) => db.prepare(sql));
527
+ return db.transaction(() => {
528
+ const removed = db
529
+ .prepare(`DELETE FROM thread_registry WHERE thread_id = ? AND purging_at = ?`)
530
+ .run(threadId, purgingAt).changes > 0;
531
+ const rowLeft = db.prepare(`SELECT 1 FROM thread_registry WHERE thread_id = ?`).get(threadId);
532
+ if (rowLeft !== undefined)
533
+ return false;
534
+ for (const statement of dependents)
535
+ statement.run(threadId);
536
+ return removed;
537
+ })();
538
+ }
539
+ /** Rows a purge marked and has not finished, with each mark: a crash between
540
+ * mark and row delete, a leftover that kept the rows, or a purge still awaiting
541
+ * its file work. */
542
+ export function selectThreadsMidPurge(db) {
543
+ return db
544
+ .prepare(`SELECT thread_id, purging_at FROM thread_registry WHERE purging_at IS NOT NULL`)
545
+ .all().map((row) => ({ threadId: row.thread_id, purgingAt: row.purging_at }));
546
+ }
547
+ /**
548
+ * Every archived thread that has an archive anchor — NOT a cutoff query, and
549
+ * nothing is deleted here. The age test lives in the sweep, in JavaScript, on
550
+ * parsed instants: `archived_at < cutoff` in SQL is a TEXT comparison, so a
551
+ * stamp that is not a canonical UTC ISO instant (`...-02:00`, a different
552
+ * precision) compares by characters rather than by time, and this is the
553
+ * predicate of an unattended delete.
554
+ *
555
+ * The sweep also has to gate each candidate on liveness — an async question no
556
+ * SQLite transaction can hold open — which is why selection and deletion are
557
+ * separate calls at all: `sweepOldArchivedThreads` in
558
+ * services/thread-purge.service.ts selects, validates, gates, marks, then purges.
559
+ *
560
+ * ANCHORED ON `archived_at`, NOT `last_active_at`. "730 days of grace after
561
+ * archiving" is what the window means, and last activity does not express it: a
562
+ * thread archived today whose last message was three years ago was eligible on
563
+ * the very next sweep. Every archived row has an anchor: updateThread derives it
564
+ * from the status, and migration 40 stamped the rows archived before it did.
565
+ */
566
+ export function selectAnchoredArchivedThreads(db) {
567
+ const rows = db
568
+ .prepare(`SELECT thread_id, archived_at FROM thread_registry
569
+ WHERE status = 'archived' AND archived_at IS NOT NULL AND purging_at IS NULL`)
570
+ .all();
571
+ return rows.map((row) => ({ threadId: row.thread_id, archivedAt: row.archived_at }));
279
572
  }
280
573
  /**
281
574
  * Reset the daily session timestamp for a thread.
@@ -288,9 +581,16 @@ export function resetDailySession(db, threadId) {
288
581
  return result.changes > 0;
289
582
  }
290
583
  /**
291
- * Mark a thread's session as running (clean_shutdown = 0). Called at session
292
- * start; if the process dies before a graceful exit flips it back to 1, the
293
- * next resume sees 0 and knows the previous session was interrupted.
584
+ * Mark a thread's session as running (clean_shutdown = 0). Called from
585
+ * start_session, and the only write that clears the flag.
586
+ *
587
+ * The column now carries ONE meaning that code acts on: 0 = "the process
588
+ * currently attached to this thread reached start_session". spawn-common's
589
+ * registerSpawnedProcess sets it back to 1 for every new process, and the
590
+ * birth-death respawn net reads it to tell a thread that died before it ever
591
+ * registered from one that ran and then exited. (Its original job — letting the
592
+ * next resume notice an interrupted session — is handled by the memory
593
+ * briefing's self-judge prompt instead.)
294
594
  */
295
595
  export function markSessionRunning(db, threadId) {
296
596
  const result = db
@@ -303,8 +603,13 @@ export function markSessionRunning(db, threadId) {
303
603
  * alive at graceful exit (SIGTERM/SIGINT, maintenance restart), so a planned
304
604
  * server stop is not mistaken for a crash by the threads it takes down.
305
605
  * A thread that had already crashed lingers as status='active' but is absent
306
- * from aliveThreadIds, so its clean_shutdown stays 0 and the next resume
307
- * correctly detects the interruption. An empty list marks nothing.
606
+ * from aliveThreadIds, so its flag stays 0. An empty list marks nothing.
607
+ *
608
+ * In the {@link markSessionRunning} reading this says "no live process of ours
609
+ * is registered on that thread any more", which is true — the server is exiting.
610
+ * Agent children are detached and outlive it, so nothing re-reads the flag until
611
+ * they are re-adopted or respawned, and the respawn net's uptime window keeps a
612
+ * long-lived survivor out of scope.
308
613
  */
309
614
  export function markAliveThreadsCleanShutdown(db, aliveThreadIds) {
310
615
  if (aliveThreadIds.length === 0)
@@ -338,23 +643,174 @@ export function getExplicitTelegramTopicId(db, threadId) {
338
643
  return row?.telegram_topic_id ?? null;
339
644
  }
340
645
  /**
341
- * Backfill thread names from topic_registry for threads with empty/missing names.
342
- * Returns the number of threads updated.
646
+ * Record that `topicId` is gone by clearing the thread's stored pointer — only while
647
+ * the pointer still names `topicId`. A compare-and-clear, because every caller clears
648
+ * after an AWAITED delete: a pointer written in the meantime (the dashboard PATCH)
649
+ * names a topic that caller never deleted, and blanking it would hide that topic from
650
+ * the orphan sweep, which selects only stored pointers. Returns whether it cleared.
343
651
  */
344
- export function backfillMissingNames(db) {
345
- const result = db
346
- .prepare(`UPDATE thread_registry
347
- SET name = (
348
- SELECT tr.name FROM topic_registry tr
349
- WHERE tr.thread_id = thread_registry.thread_id
350
- LIMIT 1
351
- )
352
- WHERE (thread_registry.name IS NULL OR thread_registry.name = '')
353
- AND EXISTS (
354
- SELECT 1 FROM topic_registry tr
355
- WHERE tr.thread_id = thread_registry.thread_id AND tr.name IS NOT NULL AND tr.name != ''
356
- )`)
357
- .run();
358
- return result.changes;
652
+ export function clearTelegramTopicIdIfStill(db, threadId, topicId) {
653
+ return (db
654
+ .prepare(`UPDATE thread_registry SET telegram_topic_id = NULL
655
+ WHERE thread_id = ? AND telegram_topic_id = ?`)
656
+ .run(threadId, topicId).changes > 0);
657
+ }
658
+ /**
659
+ * The topic number a DESTRUCTIVE teardown may aim at, stated once so no call site
660
+ * has to guess. Three arms, and the third is the point:
661
+ *
662
+ * 1. the explicit pointer when the row carries one — the only positive evidence of
663
+ * which topic this thread owns;
664
+ * 2. else the thread id itself, but ONLY below {@link MINTED_THREAD_ID_FLOOR}: a
665
+ * NULL column on a topic-numbered thread means the id IS the topic, which is
666
+ * still true of every thread not born on the board's minting path;
667
+ * 3. else NOTHING. A minted id is not a topic number — the floor sits above int32
668
+ * and Telegram topic ids are int32 — so falling back to it aims the delete at a
669
+ * number no topic can have. Telegram answers TOPIC_ID_INVALID, which teardown
670
+ * reads as "already gone", and the real topic is archived out of reach of every
671
+ * sweep that looks for a stored pointer. Returning nothing is safe precisely
672
+ * because no topic is recorded: there is no delete to retry later, so a caller
673
+ * may treat the empty answer as a reached end state rather than a failure.
674
+ *
675
+ * Arm 2 becomes dead code once no below-floor worker remains.
676
+ */
677
+ export function resolveDeletableTelegramTopicId(db, threadId) {
678
+ const explicit = Optional.fromNullable(getExplicitTelegramTopicId(db, threadId));
679
+ if (explicit.has())
680
+ return explicit;
681
+ return threadId < MINTED_THREAD_ID_FLOOR ? Optional.of(threadId) : Optional.empty();
682
+ }
683
+ /** The statuses in which a thread still OWNS its Telegram topic. Deliberately
684
+ * expressed as the complement of the three terminal ones, so a status added to
685
+ * the CHECK constraint later counts as live by default — the fail-safe
686
+ * direction, since this predicate can only ever PREVENT a delete.
687
+ *
688
+ * `exiting` is live, and that is the non-obvious member: `decommissionWorker`
689
+ * ABORTS teardown and LEAVES the row `exiting` when the kill failed and the
690
+ * agent is still running, so another actor must not delete that topic out from
691
+ * under it. A row wedged in `exiting` cannot leak forever — the stale-row pass
692
+ * of `cleanupExpiredWorkers` re-reaps any non-archived worker past its TTL.
693
+ *
694
+ * `archived` is NOT live: the archived-worker orphan sweep exists precisely to
695
+ * delete the topics of archived rows, so counting archived as live would turn
696
+ * that sweep into a permanent no-op.
697
+ *
698
+ * Two callers share this set, and they do NOT share the same answer.
699
+ * `liveThreadClaimingTopic` below (may I destroy this topic) stops here: all
700
+ * three terminal statuses are non-claimants. `threadOwningTopic` (who should
701
+ * receive a message arriving in a topic) uses the set as its FIRST stage only,
702
+ * then admits `exited` in a second, lower-priority one — because a thread that
703
+ * may no longer DESTROY a topic can still be owed the messages arriving in it.
704
+ * The fail-safe argument above is strict for the delete site; in routing's
705
+ * stage 1 the same set decides only who WINS a contested topic, and the
706
+ * membership rationale for stage 2 lives on `threadOwningTopic`. */
707
+ const TERMINAL_TOPIC_STATUSES = ["archived", "expired", "exited"];
708
+ /**
709
+ * The liveness predicate for a Telegram topic: which OTHER live thread, if any,
710
+ * still claims `topicId`.
711
+ *
712
+ * A Telegram topic is a shared, externally owned, DESTRUCTIVE resource, and this
713
+ * is the one question every delete site must ask first. Three separate paths
714
+ * (a stale `decommissionWorker` that continued past a failed transition, the
715
+ * periodic archived-worker orphan sweep, and the dashboard archive/hard-delete
716
+ * route) each resolved a topic and deleted it while a live thread still owned it,
717
+ * because each checked only its OWN row.
718
+ *
719
+ * Two arms, both load-bearing:
720
+ * - `telegram_topic_id = ?` — the explicit claim.
721
+ * - `telegram_topic_id IS NULL AND thread_id = ?` — the IMPLICIT claim.
722
+ * `resolveTelegramTopicId` reads a NULL column as "my topic is my thread_id",
723
+ * so a live row with a NULL column genuinely owns that topic number. Without
724
+ * this arm the orphan sweep can delete a live thread's topic.
725
+ *
726
+ * `excludingThreadId` is the thread doing the deleting: it must never count
727
+ * itself as a claimant, or the ordinary case (a row tearing down the topic it
728
+ * alone holds) would stop deleting anything at all.
729
+ *
730
+ * Synchronous, like every other read here — which is what lets the dashboard's
731
+ * sync `handleDeleteThread` gate its fire-and-forget delete without becoming
732
+ * async and without moving its response.
733
+ */
734
+ export function liveThreadClaimingTopic(db, topicId, excludingThreadId) {
735
+ const row = db
736
+ .prepare(`SELECT thread_id FROM thread_registry
737
+ WHERE thread_id != ?
738
+ AND status NOT IN (${TERMINAL_TOPIC_STATUSES.map(() => "?").join(", ")})
739
+ AND (telegram_topic_id = ? OR (telegram_topic_id IS NULL AND thread_id = ?))
740
+ LIMIT 1`)
741
+ .get(excludingThreadId, ...TERMINAL_TOPIC_STATUSES, topicId, topicId);
742
+ // `get` hands back `unknown`: absence is answered by `fromNullable`, which
743
+ // checks it at runtime, and the row SHAPE is asserted only inside `map`, where
744
+ // a row is known to be present.
745
+ return Optional.fromNullable(row).map((claimant) => claimant.thread_id);
746
+ }
747
+ /** One stage of `threadOwningTopic`: the first row its `WHERE` admits, or empty.
748
+ * `get` hands back `unknown`: absence is answered by `fromNullable`, which checks
749
+ * it at runtime, and the row SHAPE is asserted only inside `map`, where a row is
750
+ * known to be present. */
751
+ function topicClaimantWhere(db, where, binds) {
752
+ const row = db
753
+ .prepare(`SELECT thread_id FROM thread_registry WHERE ${where} LIMIT 1`)
754
+ .get(...binds);
755
+ return Optional.fromNullable(row).map((owner) => owner.thread_id);
756
+ }
757
+ /**
758
+ * The routing half of the ownership question: which thread, if any, should
759
+ * receive a message that arrived in `topicId`.
760
+ *
761
+ * TWO STAGES, and the priority between them is the point:
762
+ * 1. the LIVE owner — `TERMINAL_TOPIC_STATUSES` excluded, as the delete gate
763
+ * does;
764
+ * 2. only when stage 1 is empty, the `exited` row that explicitly claims it.
765
+ *
766
+ * The stages are two SEQUENTIAL QUERIES with an early return, NOT an ordering
767
+ * inside one SELECT, and the distinction is worth stating because the paragraph
768
+ * below argues against an `ORDER BY`. The thing rejected there is an ARBITRARY
769
+ * tiebreak between rows no requirement ranks; this is a named priority between
770
+ * two different QUESTIONS, asked in order. A reader must not take one for the
771
+ * other.
772
+ *
773
+ * LIVE ALWAYS BEATS EXITED. That ordering is what stops a dead predecessor
774
+ * stealing a live successor's messages when both claim one topic — a state the
775
+ * dashboard PATCH route makes reachable today, and one that mint-once thread ids
776
+ * will GUARANTEE, since every respawn then has an exited row and a live row on
777
+ * the same topic by design.
778
+ *
779
+ * Stage 2 admits `exited` and only `exited`. `exited` → Active is a legal
780
+ * transition (`VALID_TRANSITIONS`), the dashboard permits starting an exited
781
+ * thread, and the wait poll loop evicts a parked agent only on `archived` /
782
+ * `expired` — so an exited row is one that may still be listening at its logical
783
+ * id, or be resumed to consume its backlog, while a message left at
784
+ * `<topicId>.jsonl` is read by nothing. `expired` stays OUT because `expired` →
785
+ * Active is structurally forbidden (Expired goes only to Archived) and the
786
+ * dashboard refuses to start it; `archived` stays OUT because an archived thread
787
+ * resumes by being REMAPPED onto a new topic while its old one is torn down by
788
+ * design.
789
+ *
790
+ * Only the EXPLICIT claim, in both stages. `liveThreadClaimingTopic`'s second arm
791
+ * (`telegram_topic_id IS NULL AND thread_id = ?`) is dead for routing: it can
792
+ * only ever answer `thread_id`, which equals `topicId`, which is already what the
793
+ * caller falls back to when this returns empty.
794
+ *
795
+ * No ORDER BY, matching the sibling. No LIFECYCLE writer can give two rows the
796
+ * same explicit `telegram_topic_id`: the column's one write seam is
797
+ * `updateThread`, every lifecycle caller writes either the row's own `thread_id`
798
+ * or an id Telegram just minted for that write, and the orphan sweep and
799
+ * `decommissionWorker` only clear.
800
+ * Minted ids are assumed unique within the configured chat — Telegram's
801
+ * numbering, not ours; unenforced here, and void if this registry is ever pointed
802
+ * at a different chat. The dashboard PATCH route (`buildThreadUpdates`) is the
803
+ * known exception: it writes any number the operator sends, so a two-claimant
804
+ * pair is operator-reachable, and WITHIN one stage this query then returns
805
+ * whichever row SQLite scans first — exactly the base behaviour. An ORDER BY
806
+ * would encode a winner no requirement names. The typed-open
807
+ * `ActivateThreadInput` / `TouchThreadInput` seam accepts a `telegramTopicId`
808
+ * too, with no production caller passing one today.
809
+ */
810
+ export function threadOwningTopic(db, topicId) {
811
+ const liveOwner = topicClaimantWhere(db, `status NOT IN (${TERMINAL_TOPIC_STATUSES.map(() => "?").join(", ")}) AND telegram_topic_id = ?`, [...TERMINAL_TOPIC_STATUSES, topicId]);
812
+ if (liveOwner.has())
813
+ return liveOwner;
814
+ return topicClaimantWhere(db, `status = ? AND telegram_topic_id = ?`, ["exited", topicId]);
359
815
  }
360
816
  //# sourceMappingURL=thread-registry.js.map