@goondocks/myco 0.6.4 → 0.9.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 (288) hide show
  1. package/.claude-plugin/marketplace.json +2 -3
  2. package/.claude-plugin/plugin.json +3 -3
  3. package/CONTRIBUTING.md +37 -30
  4. package/README.md +64 -28
  5. package/bin/myco-run +2 -0
  6. package/dist/agent-run-EFICNTAU.js +34 -0
  7. package/dist/agent-run-EFICNTAU.js.map +1 -0
  8. package/dist/agent-tasks-RXJ7Z5NG.js +180 -0
  9. package/dist/agent-tasks-RXJ7Z5NG.js.map +1 -0
  10. package/dist/chunk-2T7RPVPP.js +116 -0
  11. package/dist/chunk-2T7RPVPP.js.map +1 -0
  12. package/dist/chunk-3K5WGSJ4.js +165 -0
  13. package/dist/chunk-3K5WGSJ4.js.map +1 -0
  14. package/dist/chunk-46PWOKSI.js +26 -0
  15. package/dist/chunk-46PWOKSI.js.map +1 -0
  16. package/dist/chunk-4LPQ26CK.js +277 -0
  17. package/dist/chunk-4LPQ26CK.js.map +1 -0
  18. package/dist/chunk-5PEUFJ6U.js +92 -0
  19. package/dist/chunk-5PEUFJ6U.js.map +1 -0
  20. package/dist/chunk-5VZ52A4T.js +136 -0
  21. package/dist/chunk-5VZ52A4T.js.map +1 -0
  22. package/dist/chunk-BUSP3OJB.js +103 -0
  23. package/dist/chunk-BUSP3OJB.js.map +1 -0
  24. package/dist/chunk-D7TYRPRM.js +7312 -0
  25. package/dist/chunk-D7TYRPRM.js.map +1 -0
  26. package/dist/chunk-DCXRSSBP.js +22 -0
  27. package/dist/chunk-DCXRSSBP.js.map +1 -0
  28. package/dist/chunk-E4VLWIJC.js +2 -0
  29. package/dist/chunk-FFAYUQ5N.js +39 -0
  30. package/dist/chunk-FFAYUQ5N.js.map +1 -0
  31. package/dist/chunk-IB76KGBY.js +2 -0
  32. package/dist/chunk-JMJJEQ3P.js +486 -0
  33. package/dist/chunk-JMJJEQ3P.js.map +1 -0
  34. package/dist/{chunk-N33KUCFP.js → chunk-JTYZRPX5.js} +1 -9
  35. package/dist/chunk-JTYZRPX5.js.map +1 -0
  36. package/dist/{chunk-NLUE6CYG.js → chunk-JYOOJCPQ.js} +33 -17
  37. package/dist/chunk-JYOOJCPQ.js.map +1 -0
  38. package/dist/{chunk-Z74SDEKE.js → chunk-KB4DGYIY.js} +91 -9
  39. package/dist/chunk-KB4DGYIY.js.map +1 -0
  40. package/dist/{chunk-ERG2IEWX.js → chunk-KH64DHOY.js} +3 -7413
  41. package/dist/chunk-KH64DHOY.js.map +1 -0
  42. package/dist/chunk-KV4OC4H3.js +498 -0
  43. package/dist/chunk-KV4OC4H3.js.map +1 -0
  44. package/dist/chunk-KYLDNM7H.js +66 -0
  45. package/dist/chunk-KYLDNM7H.js.map +1 -0
  46. package/dist/chunk-LPUQPDC2.js +19 -0
  47. package/dist/chunk-LPUQPDC2.js.map +1 -0
  48. package/dist/chunk-M5XWW7UI.js +97 -0
  49. package/dist/chunk-M5XWW7UI.js.map +1 -0
  50. package/dist/chunk-MHSCMET3.js +275 -0
  51. package/dist/chunk-MHSCMET3.js.map +1 -0
  52. package/dist/chunk-MYX5NCRH.js +45 -0
  53. package/dist/chunk-MYX5NCRH.js.map +1 -0
  54. package/dist/chunk-OXZSXYAT.js +877 -0
  55. package/dist/chunk-OXZSXYAT.js.map +1 -0
  56. package/dist/chunk-PB6TOLRQ.js +35 -0
  57. package/dist/chunk-PB6TOLRQ.js.map +1 -0
  58. package/dist/chunk-PT5IC642.js +162 -0
  59. package/dist/chunk-PT5IC642.js.map +1 -0
  60. package/dist/chunk-QIK2XSDQ.js +187 -0
  61. package/dist/chunk-QIK2XSDQ.js.map +1 -0
  62. package/dist/chunk-RJ6ZQKG5.js +26 -0
  63. package/dist/chunk-RJ6ZQKG5.js.map +1 -0
  64. package/dist/{chunk-YIQLYIHW.js → chunk-TRUJLI6K.js} +29 -43
  65. package/dist/chunk-TRUJLI6K.js.map +1 -0
  66. package/dist/chunk-U3IBO3O3.js +41 -0
  67. package/dist/chunk-U3IBO3O3.js.map +1 -0
  68. package/dist/{chunk-7WHF2OIZ.js → chunk-UBZPD4HN.js} +25 -7
  69. package/dist/chunk-UBZPD4HN.js.map +1 -0
  70. package/dist/{chunk-HIN3UVOG.js → chunk-V7XG6V6C.js} +20 -11
  71. package/dist/chunk-V7XG6V6C.js.map +1 -0
  72. package/dist/chunk-WGTCA2NU.js +84 -0
  73. package/dist/chunk-WGTCA2NU.js.map +1 -0
  74. package/dist/{chunk-O6PERU7U.js → chunk-XNOCTDHF.js} +2 -2
  75. package/dist/chunk-YDN4OM33.js +80 -0
  76. package/dist/chunk-YDN4OM33.js.map +1 -0
  77. package/dist/cli-ODLFRIYS.js +128 -0
  78. package/dist/cli-ODLFRIYS.js.map +1 -0
  79. package/dist/client-EYOTW3JU.js +19 -0
  80. package/dist/client-MXRNQ5FI.js +13 -0
  81. package/dist/{config-IBS6KOLQ.js → config-UR5BSGVX.js} +21 -34
  82. package/dist/config-UR5BSGVX.js.map +1 -0
  83. package/dist/detect-H5OPI7GD.js +17 -0
  84. package/dist/detect-H5OPI7GD.js.map +1 -0
  85. package/dist/detect-providers-Q42OD4OS.js +26 -0
  86. package/dist/detect-providers-Q42OD4OS.js.map +1 -0
  87. package/dist/doctor-JLKTXDEH.js +258 -0
  88. package/dist/doctor-JLKTXDEH.js.map +1 -0
  89. package/dist/executor-ONSDHPGX.js +1441 -0
  90. package/dist/executor-ONSDHPGX.js.map +1 -0
  91. package/dist/init-6GWY345B.js +198 -0
  92. package/dist/init-6GWY345B.js.map +1 -0
  93. package/dist/init-wizard-UONLDYLI.js +294 -0
  94. package/dist/init-wizard-UONLDYLI.js.map +1 -0
  95. package/dist/llm-BV3QNVRD.js +17 -0
  96. package/dist/llm-BV3QNVRD.js.map +1 -0
  97. package/dist/loader-SH67XD54.js +28 -0
  98. package/dist/loader-SH67XD54.js.map +1 -0
  99. package/dist/loader-XVXKZZDH.js +18 -0
  100. package/dist/loader-XVXKZZDH.js.map +1 -0
  101. package/dist/{chunk-H7PRCVGQ.js → logs-QZVYF6FP.js} +74 -5
  102. package/dist/logs-QZVYF6FP.js.map +1 -0
  103. package/dist/main-BMCL7CPO.js +4393 -0
  104. package/dist/main-BMCL7CPO.js.map +1 -0
  105. package/dist/openai-embeddings-C265WRNK.js +14 -0
  106. package/dist/openai-embeddings-C265WRNK.js.map +1 -0
  107. package/dist/openrouter-U6VFCRX2.js +14 -0
  108. package/dist/openrouter-U6VFCRX2.js.map +1 -0
  109. package/dist/post-compact-OWFSOITU.js +26 -0
  110. package/dist/post-compact-OWFSOITU.js.map +1 -0
  111. package/dist/post-tool-use-DOUM7CGQ.js +56 -0
  112. package/dist/post-tool-use-DOUM7CGQ.js.map +1 -0
  113. package/dist/post-tool-use-failure-SG3C7PE6.js +28 -0
  114. package/dist/post-tool-use-failure-SG3C7PE6.js.map +1 -0
  115. package/dist/pre-compact-3J33CHXQ.js +25 -0
  116. package/dist/pre-compact-3J33CHXQ.js.map +1 -0
  117. package/dist/provider-check-3WBPZADE.js +12 -0
  118. package/dist/provider-check-3WBPZADE.js.map +1 -0
  119. package/dist/registry-J4XTWARS.js +25 -0
  120. package/dist/registry-J4XTWARS.js.map +1 -0
  121. package/dist/resolution-events-TFEQPVKS.js +12 -0
  122. package/dist/resolution-events-TFEQPVKS.js.map +1 -0
  123. package/dist/resolve-3FEUV462.js +9 -0
  124. package/dist/resolve-3FEUV462.js.map +1 -0
  125. package/dist/{restart-XCMILOL5.js → restart-2VM33WOB.js} +10 -6
  126. package/dist/{restart-XCMILOL5.js.map → restart-2VM33WOB.js.map} +1 -1
  127. package/dist/search-ZGQR5MDE.js +91 -0
  128. package/dist/search-ZGQR5MDE.js.map +1 -0
  129. package/dist/{server-6UDN35QN.js → server-6KMBJCHZ.js} +308 -517
  130. package/dist/server-6KMBJCHZ.js.map +1 -0
  131. package/dist/session-Z2FXDDG6.js +68 -0
  132. package/dist/session-Z2FXDDG6.js.map +1 -0
  133. package/dist/session-end-FLVX32LE.js +38 -0
  134. package/dist/session-end-FLVX32LE.js.map +1 -0
  135. package/dist/session-start-UCLK7PXE.js +169 -0
  136. package/dist/session-start-UCLK7PXE.js.map +1 -0
  137. package/dist/setup-digest-4KDSXAIV.js +15 -0
  138. package/dist/setup-digest-4KDSXAIV.js.map +1 -0
  139. package/dist/setup-llm-GKMCHURK.js +81 -0
  140. package/dist/setup-llm-GKMCHURK.js.map +1 -0
  141. package/dist/src/agent/definitions/agent.yaml +35 -0
  142. package/dist/src/agent/definitions/tasks/digest-only.yaml +84 -0
  143. package/dist/src/agent/definitions/tasks/extract-only.yaml +87 -0
  144. package/dist/src/agent/definitions/tasks/full-intelligence.yaml +472 -0
  145. package/dist/src/agent/definitions/tasks/graph-maintenance.yaml +92 -0
  146. package/dist/src/agent/definitions/tasks/review-session.yaml +132 -0
  147. package/dist/src/agent/definitions/tasks/supersession-sweep.yaml +86 -0
  148. package/dist/src/agent/definitions/tasks/title-summary.yaml +88 -0
  149. package/dist/src/agent/prompts/agent.md +121 -0
  150. package/dist/src/agent/prompts/orchestrator.md +91 -0
  151. package/dist/src/cli.js +1 -8
  152. package/dist/src/cli.js.map +1 -1
  153. package/dist/src/daemon/main.js +1 -8
  154. package/dist/src/daemon/main.js.map +1 -1
  155. package/dist/src/hooks/post-tool-use.js +3 -50
  156. package/dist/src/hooks/post-tool-use.js.map +1 -1
  157. package/dist/src/hooks/session-end.js +3 -32
  158. package/dist/src/hooks/session-end.js.map +1 -1
  159. package/dist/src/hooks/session-start.js +2 -8
  160. package/dist/src/hooks/session-start.js.map +1 -1
  161. package/dist/src/hooks/stop.js +3 -42
  162. package/dist/src/hooks/stop.js.map +1 -1
  163. package/dist/src/hooks/user-prompt-submit.js +3 -53
  164. package/dist/src/hooks/user-prompt-submit.js.map +1 -1
  165. package/dist/src/mcp/server.js +1 -8
  166. package/dist/src/mcp/server.js.map +1 -1
  167. package/dist/src/prompts/digest-system.md +1 -1
  168. package/dist/src/symbionts/manifests/claude-code.yaml +16 -0
  169. package/dist/src/symbionts/manifests/cursor.yaml +14 -0
  170. package/dist/stats-IUJPZSVZ.js +94 -0
  171. package/dist/stats-IUJPZSVZ.js.map +1 -0
  172. package/dist/stop-XRQLLXST.js +42 -0
  173. package/dist/stop-XRQLLXST.js.map +1 -0
  174. package/dist/stop-failure-2CAJJKRG.js +26 -0
  175. package/dist/stop-failure-2CAJJKRG.js.map +1 -0
  176. package/dist/subagent-start-MWWQTZMQ.js +26 -0
  177. package/dist/subagent-start-MWWQTZMQ.js.map +1 -0
  178. package/dist/subagent-stop-PJXYGRXB.js +28 -0
  179. package/dist/subagent-stop-PJXYGRXB.js.map +1 -0
  180. package/dist/task-completed-4LFRJVGI.js +27 -0
  181. package/dist/task-completed-4LFRJVGI.js.map +1 -0
  182. package/dist/ui/assets/index-DZrElonz.js +744 -0
  183. package/dist/ui/assets/index-TkeiYbZB.css +1 -0
  184. package/dist/ui/favicon.svg +7 -7
  185. package/dist/ui/fonts/Inter-Variable.woff2 +0 -0
  186. package/dist/ui/fonts/JetBrainsMono-Variable.woff2 +0 -0
  187. package/dist/ui/fonts/Newsreader-Italic-Variable.woff2 +0 -0
  188. package/dist/ui/fonts/Newsreader-Variable.woff2 +0 -0
  189. package/dist/ui/index.html +2 -2
  190. package/dist/user-prompt-submit-KSM3AR6P.js +59 -0
  191. package/dist/user-prompt-submit-KSM3AR6P.js.map +1 -0
  192. package/dist/{verify-TOWQHPBX.js → verify-UDAYVX37.js} +17 -22
  193. package/dist/verify-UDAYVX37.js.map +1 -0
  194. package/dist/{version-36RVCQA6.js → version-KLBN4HZT.js} +3 -4
  195. package/dist/version-KLBN4HZT.js.map +1 -0
  196. package/hooks/hooks.json +82 -5
  197. package/package.json +6 -3
  198. package/skills/myco/SKILL.md +10 -10
  199. package/skills/myco/references/cli-usage.md +15 -13
  200. package/skills/myco/references/vault-status.md +3 -3
  201. package/skills/myco/references/wisdom.md +4 -4
  202. package/skills/myco-curate/SKILL.md +86 -0
  203. package/dist/chunk-2ZIBCEYO.js +0 -113
  204. package/dist/chunk-2ZIBCEYO.js.map +0 -1
  205. package/dist/chunk-4RMSHZE4.js +0 -107
  206. package/dist/chunk-4RMSHZE4.js.map +0 -1
  207. package/dist/chunk-4XVKZ3WA.js +0 -1078
  208. package/dist/chunk-4XVKZ3WA.js.map +0 -1
  209. package/dist/chunk-6FQISQNA.js +0 -61
  210. package/dist/chunk-6FQISQNA.js.map +0 -1
  211. package/dist/chunk-7WHF2OIZ.js.map +0 -1
  212. package/dist/chunk-ERG2IEWX.js.map +0 -1
  213. package/dist/chunk-FPRXMJLT.js +0 -56
  214. package/dist/chunk-FPRXMJLT.js.map +0 -1
  215. package/dist/chunk-GENQ5QGP.js +0 -37
  216. package/dist/chunk-GENQ5QGP.js.map +0 -1
  217. package/dist/chunk-H7PRCVGQ.js.map +0 -1
  218. package/dist/chunk-HIN3UVOG.js.map +0 -1
  219. package/dist/chunk-HYVT345Y.js +0 -159
  220. package/dist/chunk-HYVT345Y.js.map +0 -1
  221. package/dist/chunk-J4D4CROB.js +0 -143
  222. package/dist/chunk-J4D4CROB.js.map +0 -1
  223. package/dist/chunk-MDLSAFPP.js +0 -99
  224. package/dist/chunk-MDLSAFPP.js.map +0 -1
  225. package/dist/chunk-N33KUCFP.js.map +0 -1
  226. package/dist/chunk-NL6WQO56.js +0 -65
  227. package/dist/chunk-NL6WQO56.js.map +0 -1
  228. package/dist/chunk-NLUE6CYG.js.map +0 -1
  229. package/dist/chunk-P723N2LP.js +0 -147
  230. package/dist/chunk-P723N2LP.js.map +0 -1
  231. package/dist/chunk-QLUE3BUL.js +0 -161
  232. package/dist/chunk-QLUE3BUL.js.map +0 -1
  233. package/dist/chunk-QN4W3JUA.js +0 -43
  234. package/dist/chunk-QN4W3JUA.js.map +0 -1
  235. package/dist/chunk-RGVBGTD6.js +0 -21
  236. package/dist/chunk-RGVBGTD6.js.map +0 -1
  237. package/dist/chunk-TWSTAVLO.js +0 -132
  238. package/dist/chunk-TWSTAVLO.js.map +0 -1
  239. package/dist/chunk-UP4P4OAA.js +0 -4423
  240. package/dist/chunk-UP4P4OAA.js.map +0 -1
  241. package/dist/chunk-YIQLYIHW.js.map +0 -1
  242. package/dist/chunk-YTFXA4RX.js +0 -86
  243. package/dist/chunk-YTFXA4RX.js.map +0 -1
  244. package/dist/chunk-Z74SDEKE.js.map +0 -1
  245. package/dist/cli-IHILSS6N.js +0 -97
  246. package/dist/cli-IHILSS6N.js.map +0 -1
  247. package/dist/client-AGFNR2S4.js +0 -12
  248. package/dist/config-IBS6KOLQ.js.map +0 -1
  249. package/dist/curate-3D4GHKJH.js +0 -78
  250. package/dist/curate-3D4GHKJH.js.map +0 -1
  251. package/dist/detect-providers-XEP4QA3R.js +0 -35
  252. package/dist/detect-providers-XEP4QA3R.js.map +0 -1
  253. package/dist/digest-7HLJXL77.js +0 -85
  254. package/dist/digest-7HLJXL77.js.map +0 -1
  255. package/dist/init-ARQ53JOR.js +0 -109
  256. package/dist/init-ARQ53JOR.js.map +0 -1
  257. package/dist/logs-IENORIYR.js +0 -84
  258. package/dist/logs-IENORIYR.js.map +0 -1
  259. package/dist/main-6AGPIMH2.js +0 -5715
  260. package/dist/main-6AGPIMH2.js.map +0 -1
  261. package/dist/rebuild-Q2ACEB6F.js +0 -64
  262. package/dist/rebuild-Q2ACEB6F.js.map +0 -1
  263. package/dist/reprocess-CDEFGQOV.js +0 -79
  264. package/dist/reprocess-CDEFGQOV.js.map +0 -1
  265. package/dist/search-7W25SKCB.js +0 -120
  266. package/dist/search-7W25SKCB.js.map +0 -1
  267. package/dist/server-6UDN35QN.js.map +0 -1
  268. package/dist/session-F326AWCH.js +0 -44
  269. package/dist/session-F326AWCH.js.map +0 -1
  270. package/dist/session-start-K6IGAC7H.js +0 -192
  271. package/dist/session-start-K6IGAC7H.js.map +0 -1
  272. package/dist/setup-digest-X5PN27F4.js +0 -15
  273. package/dist/setup-llm-S5OHQJXK.js +0 -15
  274. package/dist/src/prompts/classification.md +0 -43
  275. package/dist/stats-TTSDXGJV.js +0 -58
  276. package/dist/stats-TTSDXGJV.js.map +0 -1
  277. package/dist/templates-XPRBOWCE.js +0 -38
  278. package/dist/templates-XPRBOWCE.js.map +0 -1
  279. package/dist/ui/assets/index-08wKT7wS.css +0 -1
  280. package/dist/ui/assets/index-CMSMi4Jb.js +0 -369
  281. package/dist/verify-TOWQHPBX.js.map +0 -1
  282. package/skills/setup/SKILL.md +0 -174
  283. package/skills/setup/references/model-recommendations.md +0 -83
  284. /package/dist/{client-AGFNR2S4.js.map → chunk-E4VLWIJC.js.map} +0 -0
  285. /package/dist/{setup-digest-X5PN27F4.js.map → chunk-IB76KGBY.js.map} +0 -0
  286. /package/dist/{chunk-O6PERU7U.js.map → chunk-XNOCTDHF.js.map} +0 -0
  287. /package/dist/{setup-llm-S5OHQJXK.js.map → client-EYOTW3JU.js.map} +0 -0
  288. /package/dist/{version-36RVCQA6.js.map → client-MXRNQ5FI.js.map} +0 -0
@@ -0,0 +1,4393 @@
1
+ import { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);
2
+ import {
3
+ DaemonLogger,
4
+ LEVEL_ORDER
5
+ } from "./chunk-2T7RPVPP.js";
6
+ import {
7
+ EMBEDDABLE_TABLES,
8
+ EMBEDDABLE_TEXT_COLUMNS,
9
+ assertValidTable,
10
+ clearEmbedded,
11
+ gatherStats,
12
+ getEmbeddingQueueDepth,
13
+ getUnembedded,
14
+ markEmbedded
15
+ } from "./chunk-QIK2XSDQ.js";
16
+ import {
17
+ withTaskConfig
18
+ } from "./chunk-M5XWW7UI.js";
19
+ import {
20
+ createEmbeddingProvider
21
+ } from "./chunk-JYOOJCPQ.js";
22
+ import {
23
+ closeOpenBatches,
24
+ countRuns,
25
+ createBatchLineage,
26
+ errorMessage,
27
+ findBatchByPromptPrefix,
28
+ getDigestExtract,
29
+ getEntity,
30
+ getGraphForNode,
31
+ getLatestBatch,
32
+ getRun,
33
+ getRunningRun,
34
+ incrementActivityCount,
35
+ insertBatchStateless,
36
+ listBatchesBySession,
37
+ listDigestExtracts,
38
+ listEntities,
39
+ listReports,
40
+ listRuns,
41
+ listTurnsByRun,
42
+ populateBatchResponses,
43
+ setResponseSummary
44
+ } from "./chunk-OXZSXYAT.js";
45
+ import {
46
+ fullTextSearch,
47
+ hydrateSearchResults
48
+ } from "./chunk-PT5IC642.js";
49
+ import {
50
+ copyTaskToUser,
51
+ deleteUserTask,
52
+ loadAllTasks,
53
+ validateTaskName,
54
+ writeUserTask
55
+ } from "./chunk-BUSP3OJB.js";
56
+ import {
57
+ AgentTaskSchema,
58
+ registerAgent,
59
+ resolveDefinitionsDir,
60
+ taskFromParsed
61
+ } from "./chunk-JMJJEQ3P.js";
62
+ import {
63
+ checkLocalProvider
64
+ } from "./chunk-DCXRSSBP.js";
65
+ import {
66
+ EventBuffer,
67
+ cleanStaleBuffers,
68
+ listBufferSessionIds
69
+ } from "./chunk-V7XG6V6C.js";
70
+ import "./chunk-IB76KGBY.js";
71
+ import "./chunk-46PWOKSI.js";
72
+ import "./chunk-RJ6ZQKG5.js";
73
+ import "./chunk-KYLDNM7H.js";
74
+ import {
75
+ loadSecrets
76
+ } from "./chunk-FFAYUQ5N.js";
77
+ import {
78
+ SymbiontRegistry,
79
+ claudeCodeAdapter,
80
+ createPerProjectAdapter,
81
+ extensionForMimeType
82
+ } from "./chunk-KB4DGYIY.js";
83
+ import "./chunk-SAKJMNSR.js";
84
+ import {
85
+ loadManifests
86
+ } from "./chunk-5PEUFJ6U.js";
87
+ import {
88
+ LmStudioBackend,
89
+ OllamaBackend
90
+ } from "./chunk-UBZPD4HN.js";
91
+ import {
92
+ countSpores,
93
+ getSpore,
94
+ insertSpore,
95
+ listSpores,
96
+ updateSporeStatus
97
+ } from "./chunk-3K5WGSJ4.js";
98
+ import {
99
+ closeSession,
100
+ countSessions,
101
+ deleteSessionCascade,
102
+ getSession,
103
+ getSessionImpact,
104
+ listSessions,
105
+ updateSession,
106
+ upsertSession
107
+ } from "./chunk-4LPQ26CK.js";
108
+ import {
109
+ EMBEDDING_DIMENSIONS,
110
+ createSchema
111
+ } from "./chunk-KV4OC4H3.js";
112
+ import {
113
+ CONFIG_FILENAME,
114
+ MycoConfigSchema,
115
+ loadConfig,
116
+ updateConfig
117
+ } from "./chunk-MHSCMET3.js";
118
+ import {
119
+ require_dist
120
+ } from "./chunk-D7TYRPRM.js";
121
+ import "./chunk-E4VLWIJC.js";
122
+ import {
123
+ external_exports
124
+ } from "./chunk-KH64DHOY.js";
125
+ import {
126
+ closeDatabase,
127
+ getDatabase,
128
+ initDatabase,
129
+ vaultDbPath
130
+ } from "./chunk-MYX5NCRH.js";
131
+ import "./chunk-TRUJLI6K.js";
132
+ import {
133
+ CONTENT_HASH_ALGORITHM,
134
+ DAEMON_EVICT_POLL_MS,
135
+ DAEMON_EVICT_TIMEOUT_MS,
136
+ DEAD_SESSION_MAX_PROMPTS,
137
+ DEFAULT_AGENT_ID,
138
+ EMBEDDING_BATCH_SIZE,
139
+ EXCLUDED_SPORE_STATUSES,
140
+ FEED_DEFAULT_LIMIT,
141
+ LOG_CONTEXT_PREVIEW_CHARS,
142
+ LOG_MESSAGE_PREVIEW_CHARS,
143
+ LOG_PROMPT_PREVIEW_CHARS,
144
+ MS_PER_DAY,
145
+ MS_PER_SECOND,
146
+ POWER_ACTIVE_INTERVAL_MS,
147
+ POWER_DEEP_SLEEP_THRESHOLD_MS,
148
+ POWER_IDLE_THRESHOLD_MS,
149
+ POWER_SLEEP_INTERVAL_MS,
150
+ POWER_SLEEP_THRESHOLD_MS,
151
+ PROMPT_CONTEXT_MAX_TOKENS,
152
+ PROMPT_CONTEXT_MIN_LENGTH,
153
+ PROMPT_CONTEXT_MIN_SIMILARITY,
154
+ PROMPT_PREVIEW_CHARS,
155
+ PROMPT_VECTOR_OVER_FETCH,
156
+ SEARCH_RESULTS_DEFAULT_LIMIT,
157
+ SEARCH_SIMILARITY_THRESHOLD,
158
+ STALE_BUFFER_MAX_AGE_MS,
159
+ STALE_SESSION_THRESHOLD_MS,
160
+ USER_AGENT_ID,
161
+ USER_AGENT_NAME,
162
+ USER_TASK_SOURCE,
163
+ epochSeconds,
164
+ estimateTokens
165
+ } from "./chunk-5VZ52A4T.js";
166
+ import {
167
+ LOG_KINDS,
168
+ kindToComponent
169
+ } from "./chunk-WGTCA2NU.js";
170
+ import {
171
+ getPluginVersion
172
+ } from "./chunk-PB6TOLRQ.js";
173
+ import {
174
+ findPackageRoot
175
+ } from "./chunk-LPUQPDC2.js";
176
+ import {
177
+ __toESM
178
+ } from "./chunk-PZUWP5VK.js";
179
+
180
+ // src/daemon/server.ts
181
+ import http from "http";
182
+ import fs2 from "fs";
183
+ import path2 from "path";
184
+
185
+ // src/daemon/router.ts
186
+ var Router = class {
187
+ routes = [];
188
+ add(method, pattern, handler) {
189
+ const type = pattern.includes(":") ? "param" : pattern.endsWith("/*") ? "prefix" : "exact";
190
+ const segments = type === "param" ? pattern.split("/") : void 0;
191
+ this.routes.push({ method, pattern, handler, type, segments });
192
+ }
193
+ /**
194
+ * Match a request against registered routes.
195
+ * Priority: exact > parameterized > prefix. Within parameterized routes,
196
+ * first-registered wins if multiple patterns match at the same depth.
197
+ */
198
+ match(method, rawUrl) {
199
+ const url = new URL(rawUrl, "http://localhost");
200
+ const pathname = url.pathname;
201
+ const query = {};
202
+ url.searchParams.forEach((v, k) => {
203
+ query[k] = v;
204
+ });
205
+ let paramMatch;
206
+ let prefixMatch;
207
+ for (const route of this.routes) {
208
+ if (route.method !== method) continue;
209
+ if (route.type === "exact" && route.pattern === pathname) {
210
+ return { handler: route.handler, params: {}, query, pathname };
211
+ }
212
+ if (route.type === "param" && !paramMatch && route.segments) {
213
+ const parts = pathname.split("/");
214
+ if (parts.length === route.segments.length) {
215
+ const params = {};
216
+ let matched = true;
217
+ for (let i = 0; i < route.segments.length; i++) {
218
+ if (route.segments[i].startsWith(":")) {
219
+ params[route.segments[i].slice(1)] = parts[i];
220
+ } else if (route.segments[i] !== parts[i]) {
221
+ matched = false;
222
+ break;
223
+ }
224
+ }
225
+ if (matched) {
226
+ paramMatch = { handler: route.handler, params, query, pathname };
227
+ }
228
+ }
229
+ }
230
+ if (route.type === "prefix" && !prefixMatch) {
231
+ const prefix = route.pattern.slice(0, -1);
232
+ if (pathname.startsWith(prefix)) {
233
+ prefixMatch = { handler: route.handler, params: {}, query, pathname };
234
+ }
235
+ }
236
+ }
237
+ return paramMatch ?? prefixMatch;
238
+ }
239
+ };
240
+
241
+ // src/daemon/static.ts
242
+ import fs from "fs";
243
+ import path from "path";
244
+ var HASHED_ASSET_PREFIX = "/assets/";
245
+ var IMMUTABLE_CACHE = "public, max-age=31536000, immutable";
246
+ var NO_CACHE = "no-cache";
247
+ var MIME_TYPES = {
248
+ ".html": "text/html",
249
+ ".js": "application/javascript",
250
+ ".css": "text/css",
251
+ ".json": "application/json",
252
+ ".svg": "image/svg+xml",
253
+ ".png": "image/png",
254
+ ".ico": "image/x-icon",
255
+ ".woff": "font/woff",
256
+ ".woff2": "font/woff2",
257
+ ".ttf": "font/ttf"
258
+ };
259
+ function resolveStaticFile(uiDir, pathname) {
260
+ const relative = pathname.startsWith("/") ? pathname.slice(1) : pathname;
261
+ const resolved = path.resolve(uiDir, relative || "index.html");
262
+ if (!resolved.startsWith(path.resolve(uiDir))) {
263
+ return void 0;
264
+ }
265
+ if (fs.existsSync(resolved) && fs.statSync(resolved).isFile()) {
266
+ const ext = path.extname(resolved);
267
+ const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
268
+ const cacheControl = pathname.startsWith(HASHED_ASSET_PREFIX) ? IMMUTABLE_CACHE : NO_CACHE;
269
+ return { filePath: resolved, contentType, cacheControl };
270
+ }
271
+ const indexPath = path.join(uiDir, "index.html");
272
+ if (fs.existsSync(indexPath)) {
273
+ return { filePath: indexPath, contentType: "text/html", cacheControl: NO_CACHE };
274
+ }
275
+ return void 0;
276
+ }
277
+
278
+ // src/daemon/server.ts
279
+ var DEFAULT_STATUS = 200;
280
+ var DaemonServer = class {
281
+ port = 0;
282
+ version;
283
+ uiDir;
284
+ server = null;
285
+ vaultDir;
286
+ logger;
287
+ router = new Router();
288
+ onRequest;
289
+ constructor(config) {
290
+ this.vaultDir = config.vaultDir;
291
+ this.logger = config.logger;
292
+ this.uiDir = config.uiDir ?? null;
293
+ this.onRequest = config.onRequest ?? null;
294
+ this.version = getPluginVersion();
295
+ this.registerDefaultRoutes();
296
+ }
297
+ registerRoute(method, routePath, handler) {
298
+ this.router.add(method, routePath, handler);
299
+ }
300
+ async start(port = 0) {
301
+ return new Promise((resolve, reject) => {
302
+ this.server = http.createServer((req, res) => this.handleRequest(req, res));
303
+ this.server.on("error", reject);
304
+ this.server.listen(port, "127.0.0.1", () => {
305
+ const addr = this.server.address();
306
+ this.port = addr.port;
307
+ this.writeDaemonJson();
308
+ this.logger.info(LOG_KINDS.DAEMON_PORT, "Server started", { port: this.port, dashboard: `http://localhost:${this.port}/` });
309
+ resolve();
310
+ });
311
+ });
312
+ }
313
+ async stop() {
314
+ return new Promise((resolve) => {
315
+ this.removeDaemonJson();
316
+ if (this.server) {
317
+ this.server.close(() => {
318
+ this.logger.info(LOG_KINDS.DAEMON_START, "Server stopped");
319
+ resolve();
320
+ });
321
+ } else {
322
+ resolve();
323
+ }
324
+ });
325
+ }
326
+ registerDefaultRoutes() {
327
+ this.registerRoute("GET", "/health", async () => ({
328
+ body: {
329
+ myco: true,
330
+ version: this.version,
331
+ pid: process.pid,
332
+ uptime: process.uptime()
333
+ }
334
+ }));
335
+ }
336
+ async handleRequest(req, res) {
337
+ const match = this.router.match(req.method, req.url);
338
+ if (match) {
339
+ this.onRequest?.();
340
+ try {
341
+ const body = req.method === "POST" || req.method === "PUT" ? await readBody(req) : void 0;
342
+ const result = await match.handler({
343
+ body,
344
+ query: match.query,
345
+ params: match.params,
346
+ pathname: match.pathname
347
+ });
348
+ const status = result.status ?? DEFAULT_STATUS;
349
+ if (Buffer.isBuffer(result.body)) {
350
+ res.writeHead(status, result.headers ?? {});
351
+ res.end(result.body);
352
+ return;
353
+ }
354
+ const headers = { "Content-Type": "application/json", ...result.headers };
355
+ res.writeHead(status, headers);
356
+ res.end(JSON.stringify(result.body));
357
+ } catch (error) {
358
+ this.logger.error(LOG_KINDS.SERVER_ERROR, "Request handler error", {
359
+ path: req.url,
360
+ error: error.message
361
+ });
362
+ res.writeHead(500, { "Content-Type": "application/json" });
363
+ res.end(JSON.stringify({ error: error.message }));
364
+ }
365
+ return;
366
+ }
367
+ if (this.uiDir && req.method === "GET") {
368
+ const pathname = new URL(req.url, "http://localhost").pathname;
369
+ const result = resolveStaticFile(this.uiDir, pathname);
370
+ if (result) {
371
+ try {
372
+ const content = await fs2.promises.readFile(result.filePath);
373
+ res.writeHead(200, {
374
+ "Content-Type": result.contentType,
375
+ "Cache-Control": result.cacheControl
376
+ });
377
+ res.end(content);
378
+ } catch {
379
+ res.writeHead(404, { "Content-Type": "application/json" });
380
+ res.end(JSON.stringify({ error: "not found" }));
381
+ }
382
+ return;
383
+ }
384
+ }
385
+ res.writeHead(404, { "Content-Type": "application/json" });
386
+ res.end(JSON.stringify({ error: "not found" }));
387
+ }
388
+ updateDaemonJsonSessions(sessions) {
389
+ const jsonPath = path2.join(this.vaultDir, "daemon.json");
390
+ try {
391
+ const info = JSON.parse(fs2.readFileSync(jsonPath, "utf-8"));
392
+ info.sessions = sessions;
393
+ fs2.writeFileSync(jsonPath, JSON.stringify(info, null, 2));
394
+ } catch {
395
+ }
396
+ }
397
+ /**
398
+ * Kill any existing daemon for this vault before taking over.
399
+ * Prevents orphaned daemons when spawned from worktrees or plugin upgrades.
400
+ * Must be called BEFORE resolvePort() so the old daemon releases the port.
401
+ */
402
+ async evictExistingDaemon() {
403
+ const jsonPath = path2.join(this.vaultDir, "daemon.json");
404
+ let existingPid;
405
+ try {
406
+ const content = fs2.readFileSync(jsonPath, "utf-8");
407
+ const info = JSON.parse(content);
408
+ if (typeof info.pid === "number" && info.pid !== process.pid) {
409
+ existingPid = info.pid;
410
+ }
411
+ } catch {
412
+ }
413
+ if (!existingPid) return;
414
+ try {
415
+ process.kill(existingPid, 0);
416
+ } catch {
417
+ return;
418
+ }
419
+ this.logger.info(LOG_KINDS.DAEMON_START, "Evicting existing daemon", { pid: existingPid });
420
+ try {
421
+ process.kill(existingPid, "SIGTERM");
422
+ } catch {
423
+ return;
424
+ }
425
+ const deadline = Date.now() + DAEMON_EVICT_TIMEOUT_MS;
426
+ while (Date.now() < deadline) {
427
+ await new Promise((r) => setTimeout(r, DAEMON_EVICT_POLL_MS));
428
+ try {
429
+ process.kill(existingPid, 0);
430
+ } catch {
431
+ return;
432
+ }
433
+ }
434
+ this.logger.warn(LOG_KINDS.DAEMON_START, "Evicted daemon did not exit in time, sending SIGKILL", { pid: existingPid });
435
+ try {
436
+ process.kill(existingPid, "SIGKILL");
437
+ } catch {
438
+ return;
439
+ }
440
+ await new Promise((r) => setTimeout(r, DAEMON_EVICT_POLL_MS));
441
+ try {
442
+ process.kill(existingPid, 0);
443
+ } catch {
444
+ return;
445
+ }
446
+ this.logger.warn(LOG_KINDS.DAEMON_START, "Evicted daemon still alive after SIGKILL", { pid: existingPid });
447
+ }
448
+ writeDaemonJson() {
449
+ const info = {
450
+ pid: process.pid,
451
+ port: this.port,
452
+ started: (/* @__PURE__ */ new Date()).toISOString(),
453
+ sessions: []
454
+ };
455
+ const jsonPath = path2.join(this.vaultDir, "daemon.json");
456
+ fs2.writeFileSync(jsonPath, JSON.stringify(info, null, 2));
457
+ }
458
+ removeDaemonJson() {
459
+ const jsonPath = path2.join(this.vaultDir, "daemon.json");
460
+ try {
461
+ const content = fs2.readFileSync(jsonPath, "utf-8");
462
+ const info = JSON.parse(content);
463
+ if (info.pid !== process.pid) return;
464
+ fs2.unlinkSync(jsonPath);
465
+ } catch {
466
+ }
467
+ }
468
+ };
469
+ function readBody(req) {
470
+ return new Promise((resolve, reject) => {
471
+ let data = "";
472
+ req.on("data", (chunk) => {
473
+ data += chunk;
474
+ });
475
+ req.on("end", () => {
476
+ try {
477
+ resolve(data ? JSON.parse(data) : {});
478
+ } catch (e) {
479
+ reject(e);
480
+ }
481
+ });
482
+ req.on("error", reject);
483
+ });
484
+ }
485
+
486
+ // src/daemon/lifecycle.ts
487
+ var SessionRegistry = class {
488
+ _sessions = /* @__PURE__ */ new Map();
489
+ graceTimer = null;
490
+ gracePeriod;
491
+ onEmpty;
492
+ constructor(options) {
493
+ this.gracePeriod = options.gracePeriod;
494
+ this.onEmpty = options.onEmpty;
495
+ }
496
+ get sessions() {
497
+ return [...this._sessions.keys()];
498
+ }
499
+ register(sessionId, metadata) {
500
+ if (!this._sessions.has(sessionId)) {
501
+ this._sessions.set(sessionId, metadata ?? { started_at: (/* @__PURE__ */ new Date()).toISOString() });
502
+ }
503
+ this.cancelGrace();
504
+ }
505
+ getSession(sessionId) {
506
+ const meta = this._sessions.get(sessionId);
507
+ if (!meta) return void 0;
508
+ return { id: sessionId, ...meta };
509
+ }
510
+ unregister(sessionId) {
511
+ this._sessions.delete(sessionId);
512
+ if (this._sessions.size === 0) {
513
+ this.startGrace();
514
+ }
515
+ }
516
+ destroy() {
517
+ this.cancelGrace();
518
+ this._sessions.clear();
519
+ }
520
+ startGrace() {
521
+ this.cancelGrace();
522
+ this.graceTimer = setTimeout(() => {
523
+ if (this._sessions.size === 0) {
524
+ this.onEmpty();
525
+ }
526
+ }, this.gracePeriod * 1e3);
527
+ }
528
+ cancelGrace() {
529
+ if (this.graceTimer) {
530
+ clearTimeout(this.graceTimer);
531
+ this.graceTimer = null;
532
+ }
533
+ }
534
+ };
535
+
536
+ // src/daemon/port.ts
537
+ import { createHash } from "crypto";
538
+ import net from "net";
539
+ var PORT_RANGE_START = 19200;
540
+ var PORT_RANGE_SIZE = 1e4;
541
+ var PORT_RETRY_COUNT = 10;
542
+ function derivePort(vaultPath) {
543
+ const hash = createHash("md5").update(vaultPath).digest();
544
+ const num = hash.readUInt16LE(0);
545
+ return PORT_RANGE_START + num % PORT_RANGE_SIZE;
546
+ }
547
+ async function resolvePort(configPort, vaultPath) {
548
+ const basePort = configPort ?? derivePort(vaultPath);
549
+ for (let offset = 0; offset < PORT_RETRY_COUNT; offset++) {
550
+ const candidate = basePort + offset;
551
+ if (candidate > 65535) break;
552
+ if (await isPortAvailable(candidate)) return candidate;
553
+ }
554
+ return 0;
555
+ }
556
+ function isPortAvailable(port) {
557
+ return new Promise((resolve) => {
558
+ const server = net.createServer();
559
+ server.once("error", () => resolve(false));
560
+ server.once("listening", () => {
561
+ server.close(() => resolve(true));
562
+ });
563
+ server.listen(port, "127.0.0.1");
564
+ });
565
+ }
566
+
567
+ // src/capture/transcript-miner.ts
568
+ var TranscriptMiner = class {
569
+ registry;
570
+ constructor(config) {
571
+ this.registry = new SymbiontRegistry(config?.additionalAdapters);
572
+ }
573
+ /**
574
+ * Extract all conversation turns for a session.
575
+ * Convenience wrapper — delegates to getAllTurnsWithSource.
576
+ */
577
+ getAllTurns(sessionId) {
578
+ return this.getAllTurnsWithSource(sessionId).turns;
579
+ }
580
+ /**
581
+ * Extract turns using the hook-provided transcript path first (fast, no scanning),
582
+ * then fall back to adapter registry scanning if the path isn't provided.
583
+ */
584
+ getAllTurnsWithSource(sessionId, transcriptPath) {
585
+ if (transcriptPath) {
586
+ const result2 = this.registry.parseTurnsFromPath(transcriptPath);
587
+ if (result2) return result2;
588
+ }
589
+ const result = this.registry.getTranscriptTurns(sessionId);
590
+ if (result) return result;
591
+ return { turns: [], source: "none" };
592
+ }
593
+ };
594
+ function extractTurnsFromBuffer(events) {
595
+ const turns = [];
596
+ let current = null;
597
+ for (const event of events) {
598
+ const type = event.type;
599
+ if (type === "user_prompt") {
600
+ if (current) turns.push(current);
601
+ current = {
602
+ prompt: String(event.prompt ?? "").slice(0, PROMPT_PREVIEW_CHARS),
603
+ toolCount: 0,
604
+ timestamp: String(event.timestamp ?? (/* @__PURE__ */ new Date()).toISOString())
605
+ };
606
+ } else if (type === "tool_use") {
607
+ if (current) current.toolCount++;
608
+ }
609
+ }
610
+ if (current) turns.push(current);
611
+ return turns;
612
+ }
613
+
614
+ // src/daemon/plan-capture.ts
615
+ import { createHash as createHash2 } from "crypto";
616
+ import path3 from "path";
617
+
618
+ // src/db/queries/plans.ts
619
+ var DEFAULT_LIST_LIMIT = 100;
620
+ var DEFAULT_STATUS2 = "active";
621
+ var DEFAULT_PROCESSED = 0;
622
+ var PLAN_COLUMNS = [
623
+ "id",
624
+ "status",
625
+ "author",
626
+ "title",
627
+ "content",
628
+ "source_path",
629
+ "tags",
630
+ "session_id",
631
+ "prompt_batch_id",
632
+ "content_hash",
633
+ "processed",
634
+ "embedded",
635
+ "created_at",
636
+ "updated_at"
637
+ ];
638
+ var SELECT_COLUMNS = PLAN_COLUMNS.join(", ");
639
+ function toPlanRow(row) {
640
+ return {
641
+ id: row.id,
642
+ status: row.status,
643
+ author: row.author ?? null,
644
+ title: row.title ?? null,
645
+ content: row.content ?? null,
646
+ source_path: row.source_path ?? null,
647
+ tags: row.tags ?? null,
648
+ session_id: row.session_id ?? null,
649
+ prompt_batch_id: row.prompt_batch_id ?? null,
650
+ content_hash: row.content_hash ?? null,
651
+ processed: row.processed,
652
+ embedded: row.embedded ?? 0,
653
+ created_at: row.created_at,
654
+ updated_at: row.updated_at ?? null
655
+ };
656
+ }
657
+ function upsertPlan(data) {
658
+ const db = getDatabase();
659
+ db.prepare(
660
+ `INSERT INTO plans (
661
+ id, status, author, title, content,
662
+ source_path, tags, session_id, prompt_batch_id, content_hash,
663
+ processed, created_at, updated_at
664
+ ) VALUES (
665
+ ?, ?, ?, ?, ?,
666
+ ?, ?, ?, ?, ?,
667
+ ?, ?, ?
668
+ )
669
+ ON CONFLICT (id) DO UPDATE SET
670
+ status = EXCLUDED.status,
671
+ author = EXCLUDED.author,
672
+ title = EXCLUDED.title,
673
+ content = EXCLUDED.content,
674
+ source_path = EXCLUDED.source_path,
675
+ tags = EXCLUDED.tags,
676
+ session_id = EXCLUDED.session_id,
677
+ prompt_batch_id = EXCLUDED.prompt_batch_id,
678
+ content_hash = EXCLUDED.content_hash,
679
+ processed = EXCLUDED.processed,
680
+ updated_at = EXCLUDED.updated_at,
681
+ embedded = CASE
682
+ WHEN EXCLUDED.content_hash != plans.content_hash THEN 0
683
+ ELSE plans.embedded
684
+ END`
685
+ ).run(
686
+ data.id,
687
+ data.status ?? DEFAULT_STATUS2,
688
+ data.author ?? null,
689
+ data.title ?? null,
690
+ data.content ?? null,
691
+ data.source_path ?? null,
692
+ data.tags ?? null,
693
+ data.session_id ?? null,
694
+ data.prompt_batch_id ?? null,
695
+ data.content_hash ?? null,
696
+ data.processed ?? DEFAULT_PROCESSED,
697
+ data.created_at,
698
+ data.updated_at ?? null
699
+ );
700
+ return toPlanRow(
701
+ db.prepare(`SELECT ${SELECT_COLUMNS} FROM plans WHERE id = ?`).get(data.id)
702
+ );
703
+ }
704
+ function listPlans(options = {}) {
705
+ const db = getDatabase();
706
+ const conditions = [];
707
+ const params = [];
708
+ if (options.status !== void 0) {
709
+ conditions.push(`status = ?`);
710
+ params.push(options.status);
711
+ }
712
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
713
+ const limit = options.limit ?? DEFAULT_LIST_LIMIT;
714
+ params.push(limit);
715
+ const rows = db.prepare(
716
+ `SELECT ${SELECT_COLUMNS}
717
+ FROM plans
718
+ ${where}
719
+ ORDER BY created_at DESC
720
+ LIMIT ?`
721
+ ).all(...params);
722
+ return rows.map(toPlanRow);
723
+ }
724
+ function listPlansBySession(sessionId) {
725
+ const db = getDatabase();
726
+ const rows = db.prepare(
727
+ `SELECT ${SELECT_COLUMNS}
728
+ FROM plans
729
+ WHERE session_id = ?
730
+ ORDER BY created_at DESC`
731
+ ).all(sessionId);
732
+ return rows.map(toPlanRow);
733
+ }
734
+
735
+ // src/daemon/plan-capture.ts
736
+ var FILE_WRITE_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "Create"]);
737
+ var HEADING_REGEX = /^#\s+(.+)$/m;
738
+ var PLAN_ID_HASH_LENGTH = 16;
739
+ function isInPlanDirectory(filePath, watchDirs, projectRoot) {
740
+ const abs = path3.isAbsolute(filePath) ? filePath : path3.resolve(projectRoot, filePath);
741
+ return watchDirs.some((dir) => {
742
+ const absDir = path3.resolve(projectRoot, dir);
743
+ const prefix = absDir.endsWith(path3.sep) ? absDir : absDir + path3.sep;
744
+ return abs === absDir || abs.startsWith(prefix);
745
+ });
746
+ }
747
+ function isPlanWriteEvent(toolName, toolInput, config) {
748
+ if (!FILE_WRITE_TOOLS.has(toolName)) return null;
749
+ const filePath = toolInput?.file_path ?? toolInput?.path;
750
+ if (typeof filePath !== "string") return null;
751
+ if (!isInPlanDirectory(filePath, config.watchDirs, config.projectRoot)) return null;
752
+ if (config.extensions?.length) {
753
+ const ext = path3.extname(filePath).toLowerCase();
754
+ if (!config.extensions.includes(ext)) return null;
755
+ }
756
+ return filePath;
757
+ }
758
+ function parsePlanTitle(content, filename) {
759
+ const match = HEADING_REGEX.exec(content);
760
+ if (match) return match[1].trim();
761
+ return filename ?? null;
762
+ }
763
+ function capturePlan(input) {
764
+ const now = Math.floor(Date.now() / 1e3);
765
+ const contentHash = createHash2(CONTENT_HASH_ALGORITHM).update(input.content).digest("hex");
766
+ const id = createHash2("md5").update(input.sourcePath).digest("hex").slice(0, PLAN_ID_HASH_LENGTH);
767
+ const title = parsePlanTitle(input.content, path3.basename(input.sourcePath));
768
+ return upsertPlan({
769
+ id,
770
+ title,
771
+ content: input.content,
772
+ source_path: input.sourcePath,
773
+ session_id: input.sessionId,
774
+ prompt_batch_id: input.promptBatchId ?? null,
775
+ content_hash: contentHash,
776
+ status: "active",
777
+ created_at: now,
778
+ updated_at: now
779
+ });
780
+ }
781
+
782
+ // src/daemon/api/config.ts
783
+ function mergeConfigSections(current, incoming) {
784
+ return {
785
+ ...current,
786
+ daemon: { ...current.daemon, ...incoming.daemon },
787
+ embedding: { ...current.embedding, ...incoming.embedding },
788
+ capture: { ...current.capture, ...incoming.capture },
789
+ agent: { ...current.agent, ...incoming.agent },
790
+ context: { ...current.context, ...incoming.context }
791
+ };
792
+ }
793
+ async function handleGetConfig(vaultDir) {
794
+ const config = loadConfig(vaultDir);
795
+ return { body: config };
796
+ }
797
+ async function handlePutConfig(vaultDir, body) {
798
+ const result = MycoConfigSchema.safeParse(body);
799
+ if (!result.success) {
800
+ return {
801
+ status: 400,
802
+ body: { error: "validation_failed", issues: result.error.issues }
803
+ };
804
+ }
805
+ const updated = updateConfig(vaultDir, (current) => mergeConfigSections(current, result.data));
806
+ return { body: updated };
807
+ }
808
+
809
+ // src/db/queries/logs.ts
810
+ var DEFAULT_PAGE_SIZE = 100;
811
+ var DEFAULT_STREAM_LIMIT = 200;
812
+ function toLogEntryRow(row) {
813
+ return {
814
+ id: row.id,
815
+ timestamp: row.timestamp,
816
+ level: row.level,
817
+ kind: row.kind,
818
+ component: row.component,
819
+ message: row.message,
820
+ data: row.data ?? null,
821
+ session_id: row.session_id ?? null
822
+ };
823
+ }
824
+ function levelsAtOrAbove(minLevel) {
825
+ const minOrder = LEVEL_ORDER[minLevel] ?? 0;
826
+ return Object.keys(LEVEL_ORDER).filter(
827
+ (l) => LEVEL_ORDER[l] >= minOrder
828
+ );
829
+ }
830
+ function insertLogEntry(entry) {
831
+ const db = getDatabase();
832
+ const info = db.prepare(
833
+ `INSERT INTO log_entries (timestamp, level, kind, component, message, data, session_id)
834
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
835
+ ).run(
836
+ entry.timestamp,
837
+ entry.level,
838
+ entry.kind,
839
+ entry.component,
840
+ entry.message,
841
+ entry.data,
842
+ entry.session_id
843
+ );
844
+ return info.lastInsertRowid;
845
+ }
846
+ function searchLogs(params) {
847
+ const db = getDatabase();
848
+ const page = params.page ?? 1;
849
+ const pageSize = params.page_size ?? DEFAULT_PAGE_SIZE;
850
+ const offset = (page - 1) * pageSize;
851
+ const conditions = [];
852
+ const queryParams = [];
853
+ if (params.q !== void 0 && params.q.length > 0) {
854
+ conditions.push(`le.id IN (SELECT rowid FROM log_entries_fts WHERE log_entries_fts MATCH ?)`);
855
+ queryParams.push(params.q);
856
+ }
857
+ if (params.level !== void 0 && params.level.length > 0) {
858
+ const levels = levelsAtOrAbove(params.level);
859
+ if (levels.length > 0) {
860
+ conditions.push(`le.level IN (SELECT value FROM json_each(?))`);
861
+ queryParams.push(JSON.stringify(levels));
862
+ }
863
+ }
864
+ if (params.component !== void 0 && params.component.length > 0) {
865
+ const components = params.component.split(",").map((c) => c.trim()).filter(Boolean);
866
+ if (components.length > 0) {
867
+ conditions.push(`le.component IN (SELECT value FROM json_each(?))`);
868
+ queryParams.push(JSON.stringify(components));
869
+ }
870
+ }
871
+ if (params.kind !== void 0 && params.kind.length > 0) {
872
+ conditions.push(`le.kind = ?`);
873
+ queryParams.push(params.kind);
874
+ }
875
+ if (params.session_id !== void 0 && params.session_id.length > 0) {
876
+ conditions.push(`le.session_id = ?`);
877
+ queryParams.push(params.session_id);
878
+ }
879
+ if (params.from !== void 0 && params.from.length > 0) {
880
+ conditions.push(`le.timestamp >= ?`);
881
+ queryParams.push(params.from);
882
+ }
883
+ if (params.to !== void 0 && params.to.length > 0) {
884
+ conditions.push(`le.timestamp <= ?`);
885
+ queryParams.push(params.to);
886
+ }
887
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
888
+ const countRow = db.prepare(
889
+ `SELECT COUNT(*) as count FROM log_entries le ${where}`
890
+ ).get(...queryParams);
891
+ const rows = db.prepare(
892
+ `SELECT le.id, le.timestamp, le.level, le.kind, le.component, le.message, le.data, le.session_id
893
+ FROM log_entries le
894
+ ${where}
895
+ ORDER BY le.timestamp DESC, le.id DESC
896
+ LIMIT ?
897
+ OFFSET ?`
898
+ ).all(...queryParams, pageSize, offset);
899
+ return {
900
+ entries: rows.map(toLogEntryRow),
901
+ total: countRow.count,
902
+ page,
903
+ page_size: pageSize
904
+ };
905
+ }
906
+ function getLogsSince(sinceId, limit) {
907
+ const db = getDatabase();
908
+ const effectiveLimit = limit ?? DEFAULT_STREAM_LIMIT;
909
+ const rows = db.prepare(
910
+ `SELECT id, timestamp, level, kind, component, message, data, session_id
911
+ FROM log_entries
912
+ WHERE id > ?
913
+ ORDER BY id ASC
914
+ LIMIT ?`
915
+ ).all(sinceId, effectiveLimit);
916
+ const entries = rows.map(toLogEntryRow);
917
+ const cursor = entries.length > 0 ? entries[entries.length - 1].id : sinceId;
918
+ return { entries, cursor };
919
+ }
920
+ function getLogEntry(id) {
921
+ const db = getDatabase();
922
+ const row = db.prepare(
923
+ `SELECT id, timestamp, level, kind, component, message, data, session_id
924
+ FROM log_entries
925
+ WHERE id = ?`
926
+ ).get(id);
927
+ if (!row) return null;
928
+ return toLogEntryRow(row);
929
+ }
930
+ function deleteOldLogs(beforeTimestamp) {
931
+ const db = getDatabase();
932
+ const info = db.prepare(
933
+ `DELETE FROM log_entries WHERE timestamp < ?`
934
+ ).run(beforeTimestamp);
935
+ return info.changes;
936
+ }
937
+ function getMaxTimestamp() {
938
+ const db = getDatabase();
939
+ const row = db.prepare(
940
+ `SELECT MAX(timestamp) as max_ts FROM log_entries`
941
+ ).get();
942
+ return row.max_ts;
943
+ }
944
+
945
+ // src/daemon/api/log-explorer.ts
946
+ async function handleLogSearch(req) {
947
+ const { q, level, component, kind, session_id, from, to, page, page_size } = req.query;
948
+ const result = searchLogs({
949
+ q: q || void 0,
950
+ level: level || void 0,
951
+ component: component || void 0,
952
+ kind: kind || void 0,
953
+ session_id: session_id || void 0,
954
+ from: from || void 0,
955
+ to: to || void 0,
956
+ page: page ? parseInt(page, 10) : void 0,
957
+ page_size: page_size ? parseInt(page_size, 10) : void 0
958
+ });
959
+ return {
960
+ body: {
961
+ entries: result.entries.map(formatEntry),
962
+ total: result.total,
963
+ page: result.page,
964
+ page_size: result.page_size
965
+ }
966
+ };
967
+ }
968
+ async function handleLogStream(req) {
969
+ const sinceStr = req.query.since;
970
+ const limitStr = req.query.limit;
971
+ const sinceId = sinceStr ? parseInt(sinceStr, 10) : 0;
972
+ const limit = limitStr ? parseInt(limitStr, 10) : void 0;
973
+ const result = getLogsSince(sinceId, limit);
974
+ return {
975
+ body: {
976
+ entries: result.entries.map(formatEntry),
977
+ cursor: result.cursor
978
+ }
979
+ };
980
+ }
981
+ async function handleLogDetail(req) {
982
+ const id = parseInt(req.params.id, 10);
983
+ if (isNaN(id)) return { status: 400, body: { error: "Invalid log entry ID" } };
984
+ const entry = getLogEntry(id);
985
+ if (!entry) return { status: 404, body: { error: "Log entry not found" } };
986
+ const parsed = entry.data ? JSON.parse(entry.data) : {};
987
+ const resolved = {};
988
+ if (entry.session_id) {
989
+ try {
990
+ const session = getSession(entry.session_id);
991
+ if (session) {
992
+ resolved.session_title = session.title ?? null;
993
+ }
994
+ } catch {
995
+ }
996
+ }
997
+ return {
998
+ body: {
999
+ ...entry,
1000
+ data: parsed,
1001
+ resolved
1002
+ }
1003
+ };
1004
+ }
1005
+ function formatEntry(entry) {
1006
+ return {
1007
+ ...entry,
1008
+ data: entry.data ? JSON.parse(entry.data) : null
1009
+ };
1010
+ }
1011
+
1012
+ // src/daemon/api/restart.ts
1013
+ import { spawn } from "child_process";
1014
+ var RestartBodySchema = external_exports.object({
1015
+ force: external_exports.boolean().optional()
1016
+ }).optional();
1017
+ var RESTART_RESPONSE_FLUSH_MS = 500;
1018
+ var RESTART_CHILD_DELAY_SECONDS = 3;
1019
+ async function handleRestart(deps, body) {
1020
+ const parsed = RestartBodySchema.safeParse(body);
1021
+ const force = parsed.success ? parsed.data?.force : false;
1022
+ if (!force && deps.progressTracker.hasActiveOperations()) {
1023
+ return {
1024
+ status: 409,
1025
+ body: { status: "busy", message: "Active operations in progress. Use force=true to override." }
1026
+ };
1027
+ }
1028
+ const mycoCmd = process.env.MYCO_CMD || "myco";
1029
+ const shellCmd = `sleep ${RESTART_CHILD_DELAY_SECONDS} && ${mycoCmd} daemon --vault ${deps.vaultDir}`;
1030
+ const child = spawn("/bin/sh", ["-c", shellCmd], {
1031
+ detached: true,
1032
+ stdio: "ignore"
1033
+ });
1034
+ child.unref();
1035
+ setTimeout(() => {
1036
+ process.kill(process.pid, "SIGTERM");
1037
+ }, RESTART_RESPONSE_FLUSH_MS);
1038
+ return { body: { status: "restarting" } };
1039
+ }
1040
+
1041
+ // src/daemon/api/progress.ts
1042
+ import { randomUUID } from "crypto";
1043
+ var MAX_CONCURRENT_OPERATIONS = 10;
1044
+ var PROGRESS_TTL_MS = 5 * 60 * 1e3;
1045
+ var ProgressTracker = class {
1046
+ entries = /* @__PURE__ */ new Map();
1047
+ /**
1048
+ * Create a new tracked operation. Returns the existing token if an
1049
+ * operation of the same type is already running (duplicate prevention).
1050
+ * Throws if the maximum concurrent operations limit is reached.
1051
+ */
1052
+ /**
1053
+ * Create a new tracked operation or return existing one.
1054
+ * Returns `{ token, isNew }` — if `isNew` is false, the operation
1055
+ * was already running and the caller should NOT launch it again.
1056
+ * Throws if the maximum concurrent operations limit is reached.
1057
+ */
1058
+ create(type) {
1059
+ this.cleanup();
1060
+ for (const entry of this.entries.values()) {
1061
+ if (entry.type === type && entry.status === "running") {
1062
+ return { token: entry.token, isNew: false };
1063
+ }
1064
+ }
1065
+ const runningCount = [...this.entries.values()].filter((e) => e.status === "running").length;
1066
+ if (runningCount >= MAX_CONCURRENT_OPERATIONS) {
1067
+ throw new Error(`Maximum concurrent operations reached (${MAX_CONCURRENT_OPERATIONS})`);
1068
+ }
1069
+ const token = randomUUID();
1070
+ const now = Date.now();
1071
+ this.entries.set(token, {
1072
+ token,
1073
+ type,
1074
+ status: "running",
1075
+ created: now,
1076
+ updated: now
1077
+ });
1078
+ return { token, isNew: true };
1079
+ }
1080
+ /**
1081
+ * Update progress for a tracked operation.
1082
+ */
1083
+ update(token, data) {
1084
+ const entry = this.entries.get(token);
1085
+ if (!entry) return;
1086
+ if (data.percent !== void 0) entry.percent = data.percent;
1087
+ if (data.message !== void 0) entry.message = data.message;
1088
+ if (data.status !== void 0) entry.status = data.status;
1089
+ entry.updated = Date.now();
1090
+ }
1091
+ /**
1092
+ * Get the current state of a tracked operation.
1093
+ */
1094
+ get(token) {
1095
+ return this.entries.get(token);
1096
+ }
1097
+ /**
1098
+ * Check whether any operations are currently running.
1099
+ */
1100
+ hasActiveOperations() {
1101
+ for (const entry of this.entries.values()) {
1102
+ if (entry.status === "running") return true;
1103
+ }
1104
+ return false;
1105
+ }
1106
+ /**
1107
+ * Remove completed/failed entries older than PROGRESS_TTL_MS.
1108
+ */
1109
+ cleanup() {
1110
+ const cutoff = Date.now() - PROGRESS_TTL_MS;
1111
+ for (const [token, entry] of this.entries) {
1112
+ if (entry.status !== "running" && entry.updated < cutoff) {
1113
+ this.entries.delete(token);
1114
+ }
1115
+ }
1116
+ }
1117
+ };
1118
+ async function handleGetProgress(tracker, token) {
1119
+ const entry = tracker.get(token);
1120
+ if (!entry) {
1121
+ return { status: 404, body: { error: "not_found", message: "Progress token not found" } };
1122
+ }
1123
+ return { body: entry };
1124
+ }
1125
+
1126
+ // src/daemon/api/models.ts
1127
+ var MODEL_LIST_TIMEOUT_MS = 5e3;
1128
+ var ANTHROPIC_MODELS = [
1129
+ "claude-opus-4-6",
1130
+ "claude-sonnet-4-6",
1131
+ "claude-haiku-4-5-20251001"
1132
+ ];
1133
+ var EMBEDDING_PATTERNS = [
1134
+ "embed",
1135
+ "bge-",
1136
+ "nomic-embed",
1137
+ "e5-",
1138
+ "gte-",
1139
+ "granite-embedding"
1140
+ ];
1141
+ function filterEmbeddingModels(models) {
1142
+ return models.filter((m) => {
1143
+ const name = m.toLowerCase();
1144
+ return EMBEDDING_PATTERNS.some((p) => name.includes(p));
1145
+ });
1146
+ }
1147
+ function filterLlmModels(models) {
1148
+ return models.filter((m) => {
1149
+ const name = m.toLowerCase();
1150
+ return !EMBEDDING_PATTERNS.some((p) => name.includes(p));
1151
+ });
1152
+ }
1153
+ async function handleGetModels(req) {
1154
+ const provider = req.query.provider;
1155
+ const type = req.query.type;
1156
+ if (!provider) {
1157
+ return { status: 400, body: { error: "provider query parameter required" } };
1158
+ }
1159
+ let models = [];
1160
+ try {
1161
+ if (provider === "ollama") {
1162
+ const backend = new OllamaBackend({ base_url: req.query.base_url });
1163
+ models = await backend.listModels(MODEL_LIST_TIMEOUT_MS);
1164
+ } else if (provider === "lm-studio" || provider === "openai-compatible") {
1165
+ const backend = new LmStudioBackend({ base_url: req.query.base_url });
1166
+ models = await backend.listModels(MODEL_LIST_TIMEOUT_MS);
1167
+ } else if (provider === "anthropic") {
1168
+ models = ANTHROPIC_MODELS;
1169
+ }
1170
+ } catch {
1171
+ }
1172
+ if (type === "embedding") {
1173
+ models = filterEmbeddingModels(models);
1174
+ } else if (type === "llm") {
1175
+ models = filterLlmModels(models);
1176
+ }
1177
+ return { body: { provider, models } };
1178
+ }
1179
+
1180
+ // src/daemon/api/stats.ts
1181
+ import { createHash as createHash3 } from "crypto";
1182
+ import fs3 from "fs";
1183
+ import path4 from "path";
1184
+ function computeConfigHash(vaultDir) {
1185
+ try {
1186
+ const configPath = path4.join(vaultDir, CONFIG_FILENAME);
1187
+ const raw = fs3.readFileSync(configPath, "utf-8");
1188
+ return createHash3("md5").update(raw).digest("hex");
1189
+ } catch {
1190
+ return "";
1191
+ }
1192
+ }
1193
+
1194
+ // src/db/queries/activities.ts
1195
+ var DEFAULT_SUCCESS = 1;
1196
+ var DEFAULT_PROCESSED2 = 0;
1197
+ var ACTIVITY_COLUMNS = [
1198
+ "id",
1199
+ "session_id",
1200
+ "prompt_batch_id",
1201
+ "tool_name",
1202
+ "tool_input",
1203
+ "tool_output_summary",
1204
+ "file_path",
1205
+ "files_affected",
1206
+ "duration_ms",
1207
+ "success",
1208
+ "error_message",
1209
+ "timestamp",
1210
+ "processed",
1211
+ "content_hash",
1212
+ "created_at"
1213
+ ];
1214
+ var SELECT_COLUMNS2 = ACTIVITY_COLUMNS.join(", ");
1215
+ function toActivityRow(row) {
1216
+ return {
1217
+ id: row.id,
1218
+ session_id: row.session_id,
1219
+ prompt_batch_id: row.prompt_batch_id ?? null,
1220
+ tool_name: row.tool_name,
1221
+ tool_input: row.tool_input ?? null,
1222
+ tool_output_summary: row.tool_output_summary ?? null,
1223
+ file_path: row.file_path ?? null,
1224
+ files_affected: row.files_affected ?? null,
1225
+ duration_ms: row.duration_ms ?? null,
1226
+ success: row.success,
1227
+ error_message: row.error_message ?? null,
1228
+ timestamp: row.timestamp,
1229
+ processed: row.processed,
1230
+ content_hash: row.content_hash ?? null,
1231
+ created_at: row.created_at
1232
+ };
1233
+ }
1234
+ function insertActivityWithBatch(data) {
1235
+ const db = getDatabase();
1236
+ const info = db.prepare(
1237
+ `INSERT INTO activities (
1238
+ session_id, prompt_batch_id, tool_name, tool_input,
1239
+ tool_output_summary, file_path, files_affected, duration_ms,
1240
+ success, error_message, timestamp, processed,
1241
+ content_hash, created_at
1242
+ ) VALUES (
1243
+ ?,
1244
+ (SELECT id FROM prompt_batches WHERE session_id = ? AND ended_at IS NULL ORDER BY id DESC LIMIT 1),
1245
+ ?, ?,
1246
+ ?, ?, ?, ?,
1247
+ ?, ?, ?, ?,
1248
+ ?, ?
1249
+ )`
1250
+ ).run(
1251
+ data.session_id,
1252
+ data.session_id,
1253
+ data.tool_name,
1254
+ data.tool_input ?? null,
1255
+ data.tool_output_summary ?? null,
1256
+ data.file_path ?? null,
1257
+ data.files_affected ?? null,
1258
+ data.duration_ms ?? null,
1259
+ data.success ?? DEFAULT_SUCCESS,
1260
+ data.error_message ?? null,
1261
+ data.timestamp,
1262
+ DEFAULT_PROCESSED2,
1263
+ data.content_hash ?? null,
1264
+ data.created_at
1265
+ );
1266
+ const activityId = Number(info.lastInsertRowid);
1267
+ const toolName = data.tool_name;
1268
+ const toolInput = data.tool_input ?? null;
1269
+ const filePath = data.file_path ?? null;
1270
+ if (toolName || toolInput || filePath) {
1271
+ db.prepare(
1272
+ "INSERT INTO activities_fts(rowid, tool_name, tool_input, file_path) VALUES (?, ?, ?, ?)"
1273
+ ).run(activityId, toolName ?? "", toolInput ?? "", filePath ?? "");
1274
+ }
1275
+ return toActivityRow(
1276
+ db.prepare(`SELECT ${SELECT_COLUMNS2} FROM activities WHERE id = ?`).get(activityId)
1277
+ );
1278
+ }
1279
+ function listActivitiesByBatch(batchId) {
1280
+ const db = getDatabase();
1281
+ const rows = db.prepare(
1282
+ `SELECT ${SELECT_COLUMNS2}
1283
+ FROM activities
1284
+ WHERE prompt_batch_id = ?
1285
+ ORDER BY timestamp ASC`
1286
+ ).all(batchId);
1287
+ return rows.map(toActivityRow);
1288
+ }
1289
+
1290
+ // src/db/queries/attachments.ts
1291
+ var ATTACHMENT_COLUMNS = [
1292
+ "id",
1293
+ "session_id",
1294
+ "prompt_batch_id",
1295
+ "file_path",
1296
+ "media_type",
1297
+ "description",
1298
+ "data",
1299
+ "content_hash",
1300
+ "created_at"
1301
+ ];
1302
+ var ATTACHMENT_LIST_COLUMNS = [
1303
+ "id",
1304
+ "session_id",
1305
+ "prompt_batch_id",
1306
+ "file_path",
1307
+ "media_type",
1308
+ "description",
1309
+ "content_hash",
1310
+ "created_at"
1311
+ ];
1312
+ var SELECT_COLUMNS3 = ATTACHMENT_COLUMNS.join(", ");
1313
+ var SELECT_LIST_COLUMNS = ATTACHMENT_LIST_COLUMNS.join(", ");
1314
+ function toAttachmentBase(row) {
1315
+ return {
1316
+ id: row.id,
1317
+ session_id: row.session_id,
1318
+ prompt_batch_id: row.prompt_batch_id ?? null,
1319
+ file_path: row.file_path,
1320
+ media_type: row.media_type ?? null,
1321
+ description: row.description ?? null,
1322
+ content_hash: row.content_hash ?? null,
1323
+ created_at: row.created_at
1324
+ };
1325
+ }
1326
+ function toAttachmentRow(row) {
1327
+ return { ...toAttachmentBase(row), data: row.data ?? null };
1328
+ }
1329
+ function toAttachmentListRow(row) {
1330
+ return toAttachmentBase(row);
1331
+ }
1332
+ function insertAttachment(data) {
1333
+ const db = getDatabase();
1334
+ const info = db.prepare(
1335
+ `INSERT INTO attachments (${SELECT_COLUMNS3})
1336
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1337
+ ON CONFLICT (id) DO NOTHING`
1338
+ ).run(
1339
+ data.id,
1340
+ data.session_id,
1341
+ data.prompt_batch_id ?? null,
1342
+ data.file_path,
1343
+ data.media_type ?? null,
1344
+ data.description ?? null,
1345
+ data.data ?? null,
1346
+ data.content_hash ?? null,
1347
+ data.created_at
1348
+ );
1349
+ if (info.changes === 0) return void 0;
1350
+ return toAttachmentRow(
1351
+ db.prepare(`SELECT ${SELECT_COLUMNS3} FROM attachments WHERE id = ?`).get(data.id)
1352
+ );
1353
+ }
1354
+ function listAttachmentsBySession(sessionId) {
1355
+ const db = getDatabase();
1356
+ const rows = db.prepare(
1357
+ `SELECT ${SELECT_LIST_COLUMNS} FROM attachments WHERE session_id = ? ORDER BY created_at ASC`
1358
+ ).all(sessionId);
1359
+ return rows.map(toAttachmentListRow);
1360
+ }
1361
+ function getAttachmentByFilePath(filePath) {
1362
+ const db = getDatabase();
1363
+ const row = db.prepare(
1364
+ `SELECT ${SELECT_COLUMNS3} FROM attachments WHERE file_path = ? LIMIT 1`
1365
+ ).get(filePath);
1366
+ return row ? toAttachmentRow(row) : null;
1367
+ }
1368
+
1369
+ // src/daemon/api/sessions.ts
1370
+ var DEFAULT_LIST_LIMIT2 = 50;
1371
+ var DEFAULT_LIST_OFFSET = 0;
1372
+ async function handleListSessions(req) {
1373
+ const limit = req.query.limit ? Number(req.query.limit) : DEFAULT_LIST_LIMIT2;
1374
+ const offset = req.query.offset ? Number(req.query.offset) : DEFAULT_LIST_OFFSET;
1375
+ const status = req.query.status || void 0;
1376
+ const agent = req.query.agent || void 0;
1377
+ const search = req.query.search || void 0;
1378
+ const filterOpts = { status, agent, search };
1379
+ const sessions = listSessions({ ...filterOpts, limit, offset }).map((s) => ({
1380
+ id: s.id,
1381
+ date: new Date(s.started_at * 1e3).toISOString().slice(0, 10),
1382
+ title: s.title || s.id.slice(0, 8),
1383
+ status: s.status,
1384
+ agent: s.agent,
1385
+ prompt_count: s.prompt_count,
1386
+ tool_count: s.tool_count,
1387
+ started_at: s.started_at,
1388
+ ended_at: s.ended_at
1389
+ }));
1390
+ const total = countSessions(filterOpts);
1391
+ return { body: { sessions, total, offset, limit } };
1392
+ }
1393
+ async function handleGetSession(req) {
1394
+ const session = getSession(req.params.id);
1395
+ if (!session) return { status: 404, body: { error: "not_found" } };
1396
+ return { body: session };
1397
+ }
1398
+ async function handleGetSessionBatches(req) {
1399
+ const batches = listBatchesBySession(req.params.id);
1400
+ return { body: batches };
1401
+ }
1402
+ async function handleGetBatchActivities(req) {
1403
+ const batchId = Number(req.params.id);
1404
+ if (isNaN(batchId)) return { status: 400, body: { error: "invalid_batch_id" } };
1405
+ const activities = listActivitiesByBatch(batchId);
1406
+ return { body: activities };
1407
+ }
1408
+ async function handleGetSessionAttachments(req) {
1409
+ const attachments = listAttachmentsBySession(req.params.id);
1410
+ return { body: attachments };
1411
+ }
1412
+ async function handleGetSessionPlans(req) {
1413
+ const plans = listPlansBySession(req.params.id);
1414
+ return { body: plans };
1415
+ }
1416
+
1417
+ // src/daemon/api/mycelium.ts
1418
+ var DEFAULT_LIST_LIMIT3 = 50;
1419
+ var DEFAULT_LIST_OFFSET2 = 0;
1420
+ var DEFAULT_GRAPH_DEPTH = 1;
1421
+ var MAX_GRAPH_DEPTH = 3;
1422
+ var SPORE_NAME_PREVIEW_CHARS = 60;
1423
+ var EXCLUDED_GRAPH_EDGE_TYPES = /* @__PURE__ */ new Set(["HAS_BATCH", "EXTRACTED_FROM"]);
1424
+ async function handleListSpores(req) {
1425
+ const agentId = req.query.agent_id;
1426
+ const type = req.query.type;
1427
+ const status = req.query.status;
1428
+ const limit = req.query.limit ? Number(req.query.limit) : DEFAULT_LIST_LIMIT3;
1429
+ const offset = req.query.offset ? Number(req.query.offset) : DEFAULT_LIST_OFFSET2;
1430
+ const search = req.query.search || void 0;
1431
+ const filterOpts = {
1432
+ ...agentId ? { agent_id: agentId } : {},
1433
+ observation_type: type,
1434
+ status,
1435
+ search
1436
+ };
1437
+ const spores = listSpores({ ...filterOpts, limit, offset });
1438
+ const total = countSpores(filterOpts);
1439
+ return { body: { spores, total, offset, limit } };
1440
+ }
1441
+ async function handleGetSpore(req) {
1442
+ const spore = getSpore(req.params.id);
1443
+ if (!spore) return { status: 404, body: { error: "not_found" } };
1444
+ return { body: spore };
1445
+ }
1446
+ async function handleListEntities(req) {
1447
+ const agentId = req.query.agent_id ?? DEFAULT_AGENT_ID;
1448
+ const type = req.query.type;
1449
+ const mentioned_in = req.query.mentioned_in;
1450
+ const note_type = req.query.note_type;
1451
+ const limit = req.query.limit ? Number(req.query.limit) : DEFAULT_LIST_LIMIT3;
1452
+ const offset = req.query.offset ? Number(req.query.offset) : DEFAULT_LIST_OFFSET2;
1453
+ const entities = listEntities({
1454
+ agent_id: agentId,
1455
+ type,
1456
+ mentioned_in,
1457
+ note_type,
1458
+ limit,
1459
+ offset
1460
+ });
1461
+ return { body: { entities } };
1462
+ }
1463
+ async function handleGetGraph(req) {
1464
+ const depth = Math.min(Number(req.query.depth) || DEFAULT_GRAPH_DEPTH, MAX_GRAPH_DEPTH);
1465
+ const center = getEntity(req.params.id);
1466
+ if (!center) return { status: 404, body: { error: "not_found" } };
1467
+ const graph = getGraphForNode(req.params.id, "entity", { depth });
1468
+ const filteredEdges = graph.edges.filter(
1469
+ (e) => !EXCLUDED_GRAPH_EDGE_TYPES.has(e.type)
1470
+ );
1471
+ const graphDb = getDatabase();
1472
+ const entityIds = /* @__PURE__ */ new Set();
1473
+ const sporeIds = /* @__PURE__ */ new Set();
1474
+ const sessionIds = /* @__PURE__ */ new Set();
1475
+ for (const edge of filteredEdges) {
1476
+ for (const [id, type] of [
1477
+ [edge.source_id, edge.source_type],
1478
+ [edge.target_id, edge.target_type]
1479
+ ]) {
1480
+ switch (type) {
1481
+ case "entity":
1482
+ entityIds.add(id);
1483
+ break;
1484
+ case "spore":
1485
+ sporeIds.add(id);
1486
+ break;
1487
+ case "session":
1488
+ sessionIds.add(id);
1489
+ break;
1490
+ }
1491
+ }
1492
+ }
1493
+ entityIds.add(center.id);
1494
+ const entityIdArray = Array.from(entityIds);
1495
+ let entityNodes = [];
1496
+ if (entityIdArray.length > 0) {
1497
+ const placeholders = entityIdArray.map(() => "?").join(", ");
1498
+ entityNodes = graphDb.prepare(
1499
+ `SELECT id, type, name, properties, status, first_seen as created_at
1500
+ FROM entities WHERE id IN (${placeholders})`
1501
+ ).all(...entityIdArray);
1502
+ }
1503
+ const sporeIdArray = Array.from(sporeIds);
1504
+ let sporeNodes = [];
1505
+ if (sporeIdArray.length > 0) {
1506
+ const placeholders = sporeIdArray.map(() => "?").join(", ");
1507
+ sporeNodes = graphDb.prepare(
1508
+ `SELECT id, observation_type, status, content, properties, created_at
1509
+ FROM spores WHERE id IN (${placeholders})`
1510
+ ).all(...sporeIdArray);
1511
+ }
1512
+ const sessionIdArray = Array.from(sessionIds);
1513
+ let sessionNodes = [];
1514
+ if (sessionIdArray.length > 0) {
1515
+ const placeholders = sessionIdArray.map(() => "?").join(", ");
1516
+ sessionNodes = graphDb.prepare(
1517
+ `SELECT id, title, summary, status, started_at as created_at
1518
+ FROM sessions WHERE id IN (${placeholders})`
1519
+ ).all(...sessionIdArray);
1520
+ }
1521
+ const mentionCounts = /* @__PURE__ */ new Map();
1522
+ if (entityIdArray.length > 0) {
1523
+ const placeholders = entityIdArray.map(() => "?").join(", ");
1524
+ const mentionRows = graphDb.prepare(
1525
+ `SELECT entity_id, COUNT(*) as count FROM entity_mentions
1526
+ WHERE entity_id IN (${placeholders}) GROUP BY entity_id`
1527
+ ).all(...entityIdArray);
1528
+ for (const row of mentionRows) {
1529
+ mentionCounts.set(row.entity_id, Number(row.count));
1530
+ }
1531
+ }
1532
+ const allNodes = [
1533
+ ...entityNodes.map((n) => ({
1534
+ id: n.id,
1535
+ name: n.name,
1536
+ type: n.type,
1537
+ status: n.status ?? void 0,
1538
+ created_at: n.created_at,
1539
+ properties: n.properties ?? void 0,
1540
+ mention_count: mentionCounts.get(n.id) ?? 0
1541
+ })),
1542
+ ...sporeNodes.map((n) => ({
1543
+ id: n.id,
1544
+ name: (n.content ?? "").slice(0, SPORE_NAME_PREVIEW_CHARS),
1545
+ type: "spore",
1546
+ status: n.status ?? void 0,
1547
+ created_at: n.created_at,
1548
+ content: n.content,
1549
+ properties: n.properties ?? void 0,
1550
+ observation_type: n.observation_type
1551
+ })),
1552
+ ...sessionNodes.map((n) => ({
1553
+ id: n.id,
1554
+ name: n.title ?? `Session ${n.id.slice(-6)}`,
1555
+ type: "session",
1556
+ status: n.status ?? void 0,
1557
+ created_at: n.created_at,
1558
+ content: n.summary ?? void 0
1559
+ }))
1560
+ ];
1561
+ const centerNode = allNodes.find((n) => n.id === center.id);
1562
+ const uiEdges = filteredEdges.map((e) => ({
1563
+ source_id: e.source_id,
1564
+ target_id: e.target_id,
1565
+ label: e.type,
1566
+ weight: e.confidence
1567
+ }));
1568
+ return {
1569
+ body: {
1570
+ center: centerNode ?? { ...center, mention_count: mentionCounts.get(center.id) ?? 0 },
1571
+ nodes: allNodes.filter((n) => n.id !== center.id),
1572
+ edges: uiEdges,
1573
+ depth
1574
+ }
1575
+ };
1576
+ }
1577
+ async function handleGetDigest(req) {
1578
+ const agentId = req.query.agent_id ?? DEFAULT_AGENT_ID;
1579
+ const extracts = listDigestExtracts(agentId);
1580
+ return { body: { tiers: extracts } };
1581
+ }
1582
+
1583
+ // src/daemon/api/search.ts
1584
+ function createSearchHandler(deps) {
1585
+ return async function handleSearch(req) {
1586
+ const query = req.query.q;
1587
+ if (!query) return { status: 400, body: { error: "missing_query" } };
1588
+ const mode = req.query.mode ?? "auto";
1589
+ const type = req.query.type;
1590
+ const limit = Number(req.query.limit) || SEARCH_RESULTS_DEFAULT_LIMIT;
1591
+ const namespace = req.query.namespace;
1592
+ if (mode === "fts") {
1593
+ const results2 = fullTextSearch(query, { type, limit });
1594
+ return { body: { mode: "fts", results: results2 } };
1595
+ }
1596
+ const queryVector = await deps.embeddingManager.embedQuery(query);
1597
+ if (queryVector === null) {
1598
+ if (mode === "auto") {
1599
+ const results2 = fullTextSearch(query, { type, limit });
1600
+ return { body: { mode: "fts", results: results2, fallback: true } };
1601
+ }
1602
+ return { body: { mode: "semantic", results: [], provider_unavailable: true } };
1603
+ }
1604
+ const searchNamespace = namespace ?? type;
1605
+ const vectorResults = deps.embeddingManager.searchVectors(queryVector, {
1606
+ namespace: searchNamespace,
1607
+ limit,
1608
+ threshold: SEARCH_SIMILARITY_THRESHOLD
1609
+ });
1610
+ const results = hydrateSearchResults(vectorResults);
1611
+ return { body: { mode: "semantic", results } };
1612
+ };
1613
+ }
1614
+
1615
+ // src/daemon/api/context.ts
1616
+ var SessionContextBody = external_exports.object({
1617
+ session_id: external_exports.string().optional(),
1618
+ branch: external_exports.string().optional()
1619
+ });
1620
+ var PromptContextBody = external_exports.object({
1621
+ prompt: external_exports.string(),
1622
+ session_id: external_exports.string().optional()
1623
+ });
1624
+ function createSessionContextHandler(deps) {
1625
+ return async function handleSessionContext(req) {
1626
+ const { session_id, branch } = SessionContextBody.parse(req.body);
1627
+ const { logger, config } = deps;
1628
+ logger.debug(LOG_KINDS.CONTEXT_QUERY, "Session context query", { session_id });
1629
+ try {
1630
+ const parts = [];
1631
+ const tier = config.context.digest_tier;
1632
+ const extract = getDigestExtract(DEFAULT_AGENT_ID, tier);
1633
+ if (extract) {
1634
+ parts.push(extract.content);
1635
+ logger.info(LOG_KINDS.CONTEXT_DIGEST, "Digest extract found", {
1636
+ session_id,
1637
+ tier,
1638
+ content_length: extract.content.length,
1639
+ generated_at: extract.generated_at
1640
+ });
1641
+ } else {
1642
+ logger.debug(LOG_KINDS.CONTEXT_DIGEST, "No digest extract available", { session_id, tier });
1643
+ }
1644
+ if (branch) {
1645
+ parts.push(`Branch:: \`${branch}\``);
1646
+ }
1647
+ parts.push(`Session:: \`${session_id}\``);
1648
+ const source = extract ? "digest" : "basic";
1649
+ const contextText = parts.join("\n\n");
1650
+ const estimatedTokens = estimateTokens(contextText);
1651
+ const preview = contextText.slice(0, LOG_CONTEXT_PREVIEW_CHARS);
1652
+ logger.info(LOG_KINDS.CONTEXT_SESSION, "Session context injected", {
1653
+ session_id,
1654
+ source,
1655
+ tier: extract ? tier : void 0,
1656
+ text_length: contextText.length,
1657
+ estimated_tokens: estimatedTokens,
1658
+ generated_at: extract?.generated_at,
1659
+ preview
1660
+ });
1661
+ logger.debug(LOG_KINDS.CONTEXT_SESSION, `Session context: "${preview}\u2026" (${estimatedTokens} est. tokens, source=${source}${extract ? `, tier=${tier}, generated=${extract.generated_at}` : ""})`, {
1662
+ session_id
1663
+ });
1664
+ return {
1665
+ body: {
1666
+ text: contextText,
1667
+ source,
1668
+ ...extract ? { tier } : {}
1669
+ }
1670
+ };
1671
+ } catch (error) {
1672
+ logger.error(LOG_KINDS.CONTEXT_SESSION, "Session context failed", { error: error.message });
1673
+ return { body: { text: "" } };
1674
+ }
1675
+ };
1676
+ }
1677
+ function createPromptContextHandler(deps) {
1678
+ return async function handlePromptContext(req) {
1679
+ const { prompt, session_id } = PromptContextBody.parse(req.body);
1680
+ const { logger, config, embeddingManager } = deps;
1681
+ if (!config.context.prompt_search) {
1682
+ logger.debug(LOG_KINDS.CONTEXT_PROMPT, "Prompt search disabled by config", { session_id });
1683
+ return { body: { text: "" } };
1684
+ }
1685
+ if (prompt.length < PROMPT_CONTEXT_MIN_LENGTH) {
1686
+ logger.debug(LOG_KINDS.CONTEXT_PROMPT, "Prompt too short for search", {
1687
+ session_id,
1688
+ length: prompt.length,
1689
+ min: PROMPT_CONTEXT_MIN_LENGTH
1690
+ });
1691
+ return { body: { text: "" } };
1692
+ }
1693
+ const maxSpores = config.context.prompt_max_spores;
1694
+ if (maxSpores === 0) {
1695
+ logger.debug(LOG_KINDS.CONTEXT_PROMPT, "Prompt spore injection disabled (max_spores=0)", { session_id });
1696
+ return { body: { text: "" } };
1697
+ }
1698
+ const queryVector = await embeddingManager.embedQuery(prompt);
1699
+ if (!queryVector) {
1700
+ logger.debug(LOG_KINDS.CONTEXT_EMBED, "Embedding provider unavailable for prompt search", { session_id });
1701
+ return { body: { text: "" } };
1702
+ }
1703
+ const vectorResults = embeddingManager.searchVectors(queryVector, {
1704
+ namespace: "spores",
1705
+ limit: maxSpores * PROMPT_VECTOR_OVER_FETCH,
1706
+ threshold: PROMPT_CONTEXT_MIN_SIMILARITY
1707
+ });
1708
+ logger.debug(LOG_KINDS.CONTEXT_SEARCH, "Prompt vector search completed", {
1709
+ session_id,
1710
+ raw_results: vectorResults.length,
1711
+ top_similarity: vectorResults[0]?.similarity
1712
+ });
1713
+ if (vectorResults.length === 0) {
1714
+ return { body: { text: "" } };
1715
+ }
1716
+ const eligible = vectorResults.filter(
1717
+ (r) => !EXCLUDED_SPORE_STATUSES.has(r.metadata.status)
1718
+ );
1719
+ if (eligible.length === 0) {
1720
+ logger.debug(LOG_KINDS.CONTEXT_FILTER, "All spore results excluded by status filter", { session_id });
1721
+ return { body: { text: "" } };
1722
+ }
1723
+ const topResults = eligible.slice(0, maxSpores);
1724
+ const hydrated = hydrateSearchResults(topResults);
1725
+ const spores = hydrated.filter((r) => r.type === "spore");
1726
+ if (spores.length === 0) {
1727
+ return { body: { text: "" } };
1728
+ }
1729
+ const text = formatSporeContext(spores);
1730
+ const promptTokens = estimateTokens(text);
1731
+ const titles = spores.map((s) => s.title);
1732
+ logger.info(LOG_KINDS.CONTEXT_PROMPT, "Prompt context injected", {
1733
+ session_id,
1734
+ spore_count: spores.length,
1735
+ scores: spores.map((s) => s.score.toFixed(3)),
1736
+ spore_titles: titles,
1737
+ estimated_tokens: promptTokens,
1738
+ preview: text.slice(0, LOG_CONTEXT_PREVIEW_CHARS)
1739
+ });
1740
+ logger.debug(LOG_KINDS.CONTEXT_PROMPT, `Prompt context: ${spores.length} spores [${titles.join(", ")}] (~${promptTokens} tokens)`, {
1741
+ session_id,
1742
+ scores: spores.map((s) => s.score.toFixed(3))
1743
+ });
1744
+ return { body: { text } };
1745
+ };
1746
+ }
1747
+ function formatSporeContext(spores) {
1748
+ const header = "Relevant vault observations:";
1749
+ let text = header;
1750
+ let tokens = estimateTokens(text);
1751
+ for (const spore of spores) {
1752
+ const line = `
1753
+ - (${spore.title}) ${spore.preview}`;
1754
+ const lineTokens = estimateTokens(line);
1755
+ if (tokens + lineTokens > PROMPT_CONTEXT_MAX_TOKENS) break;
1756
+ text += line;
1757
+ tokens += lineTokens;
1758
+ }
1759
+ return text === header ? "" : text;
1760
+ }
1761
+
1762
+ // src/db/queries/feed.ts
1763
+ function getActivityFeed(limit = FEED_DEFAULT_LIMIT) {
1764
+ const db = getDatabase();
1765
+ const rows = db.prepare(`
1766
+ SELECT * FROM (
1767
+ SELECT 'session' as type, id, COALESCE(title, 'Session ' || substr(id, 1, 8)) as summary,
1768
+ COALESCE(ended_at, started_at) as timestamp
1769
+ FROM sessions ORDER BY started_at DESC LIMIT ?
1770
+ )
1771
+
1772
+ UNION ALL
1773
+
1774
+ SELECT * FROM (
1775
+ SELECT 'agent_run' as type, id, task || ' \u2014 ' || status as summary,
1776
+ COALESCE(completed_at, started_at) as timestamp
1777
+ FROM agent_runs ORDER BY started_at DESC LIMIT ?
1778
+ )
1779
+
1780
+ UNION ALL
1781
+
1782
+ SELECT * FROM (
1783
+ SELECT 'spore' as type, id, observation_type || ': ' || substr(content, 1, 80) as summary,
1784
+ created_at as timestamp
1785
+ FROM spores WHERE status = 'active' ORDER BY created_at DESC LIMIT ?
1786
+ )
1787
+
1788
+ ORDER BY timestamp DESC LIMIT ?
1789
+ `).all(limit, limit, limit, limit);
1790
+ return rows;
1791
+ }
1792
+
1793
+ // src/daemon/api/feed.ts
1794
+ async function handleGetFeed(req) {
1795
+ const limit = Number(req.query.limit) || FEED_DEFAULT_LIMIT;
1796
+ const feed = getActivityFeed(limit);
1797
+ return { body: feed };
1798
+ }
1799
+
1800
+ // src/daemon/api/symbionts.ts
1801
+ async function handleListSymbionts() {
1802
+ const manifests = loadManifests();
1803
+ const symbionts = manifests.map((m) => ({
1804
+ name: m.name,
1805
+ displayName: m.displayName,
1806
+ binary: m.binary
1807
+ }));
1808
+ return { body: { symbionts } };
1809
+ }
1810
+
1811
+ // src/daemon/api/embedding.ts
1812
+ var EMBEDDING_STATUS_IDLE = "idle";
1813
+ var EMBEDDING_STATUS_PENDING = "pending";
1814
+ async function handleGetEmbeddingStatus(vaultDir) {
1815
+ const config = loadConfig(vaultDir);
1816
+ const { queue_depth, embedded_count } = getEmbeddingQueueDepth();
1817
+ return {
1818
+ body: {
1819
+ provider: config.embedding.provider,
1820
+ model: config.embedding.model,
1821
+ base_url: config.embedding.base_url ?? null,
1822
+ queue_depth,
1823
+ embedded_count,
1824
+ status: queue_depth === 0 ? EMBEDDING_STATUS_IDLE : EMBEDDING_STATUS_PENDING
1825
+ }
1826
+ };
1827
+ }
1828
+ function handleEmbeddingDetails(manager) {
1829
+ const details = manager.getDetails();
1830
+ return { body: details };
1831
+ }
1832
+ function handleEmbeddingRebuild(manager) {
1833
+ const result = manager.rebuildAll();
1834
+ return { body: result };
1835
+ }
1836
+ async function handleEmbeddingReconcile(manager) {
1837
+ const result = await manager.reconcile(EMBEDDING_BATCH_SIZE);
1838
+ return { body: result };
1839
+ }
1840
+ function handleEmbeddingCleanOrphans(manager) {
1841
+ const result = manager.cleanOrphans();
1842
+ return { body: result };
1843
+ }
1844
+ async function handleEmbeddingReembedStale(manager) {
1845
+ const result = await manager.reembedStale(EMBEDDING_BATCH_SIZE);
1846
+ return { body: result };
1847
+ }
1848
+
1849
+ // src/daemon/embedding/manager.ts
1850
+ import { createHash as createHash4 } from "crypto";
1851
+ var ACTIVE_STATUS = "active";
1852
+ var EmbeddingManager = class {
1853
+ constructor(vectorStore, embeddingProvider, recordSource, logger) {
1854
+ this.vectorStore = vectorStore;
1855
+ this.embeddingProvider = embeddingProvider;
1856
+ this.recordSource = recordSource;
1857
+ this.logger = logger;
1858
+ }
1859
+ // -------------------------------------------------------------------------
1860
+ // Private helpers
1861
+ // -------------------------------------------------------------------------
1862
+ contentHash(text) {
1863
+ return createHash4(CONTENT_HASH_ALGORITHM).update(text).digest("hex");
1864
+ }
1865
+ // -------------------------------------------------------------------------
1866
+ // Write-path event handlers
1867
+ // -------------------------------------------------------------------------
1868
+ /**
1869
+ * Called when content is written (session note, spore, plan, artifact).
1870
+ * Embeds the text and stores the vector. Fire-and-forget safe.
1871
+ */
1872
+ async onContentWritten(namespace, id, text, metadata) {
1873
+ try {
1874
+ const embedding = await this.embeddingProvider.embed(text);
1875
+ if (embedding === null) {
1876
+ this.logger.warn(LOG_KINDS.EMBEDDING_PROVIDER, "Provider unavailable, skipping embed", {
1877
+ namespace,
1878
+ id
1879
+ });
1880
+ return;
1881
+ }
1882
+ const hash = this.contentHash(text);
1883
+ this.vectorStore.upsert(namespace, id, embedding, {
1884
+ model: this.embeddingProvider.model,
1885
+ provider: this.embeddingProvider.providerName,
1886
+ dimensions: this.embeddingProvider.dimensions,
1887
+ content_hash: hash,
1888
+ embedded_at: epochSeconds(),
1889
+ domain_metadata: metadata
1890
+ });
1891
+ this.recordSource.markEmbedded(namespace, id);
1892
+ this.logger.debug(LOG_KINDS.EMBEDDING_EMBED, "Vector stored", { namespace, id });
1893
+ } catch (err) {
1894
+ this.logger.warn(LOG_KINDS.EMBEDDING_EMBED, "Failed to embed content", {
1895
+ namespace,
1896
+ id,
1897
+ error: String(err)
1898
+ });
1899
+ }
1900
+ }
1901
+ /**
1902
+ * Called when a spore's status changes (e.g., superseded, archived).
1903
+ * Removes the vector for non-active statuses.
1904
+ */
1905
+ onStatusChanged(namespace, id, status) {
1906
+ try {
1907
+ if (status === ACTIVE_STATUS) return;
1908
+ this.vectorStore.remove(namespace, id);
1909
+ this.recordSource.clearEmbedded(namespace, id);
1910
+ this.logger.debug(LOG_KINDS.EMBEDDING_CLEANUP, "Vector removed", {
1911
+ namespace,
1912
+ id,
1913
+ reason: `status=${status}`
1914
+ });
1915
+ } catch (err) {
1916
+ this.logger.warn(LOG_KINDS.EMBEDDING_CLEANUP, "Failed to remove vector on status change", {
1917
+ namespace,
1918
+ id,
1919
+ status,
1920
+ error: String(err)
1921
+ });
1922
+ }
1923
+ }
1924
+ /**
1925
+ * Called when a record is deleted. Removes the vector.
1926
+ * No clearEmbedded needed — the record itself is being deleted.
1927
+ */
1928
+ onRemoved(namespace, id) {
1929
+ try {
1930
+ this.vectorStore.remove(namespace, id);
1931
+ this.logger.debug(LOG_KINDS.EMBEDDING_CLEANUP, "Vector removed", {
1932
+ namespace,
1933
+ id,
1934
+ reason: "record deleted"
1935
+ });
1936
+ } catch (err) {
1937
+ this.logger.warn(LOG_KINDS.EMBEDDING_CLEANUP, "Failed to remove vector on delete", {
1938
+ namespace,
1939
+ id,
1940
+ error: String(err)
1941
+ });
1942
+ }
1943
+ }
1944
+ // -------------------------------------------------------------------------
1945
+ // Reconciliation
1946
+ // -------------------------------------------------------------------------
1947
+ /**
1948
+ * Embed missing rows, re-embed stale vectors, and clean orphans across all namespaces.
1949
+ * Called by the reconcile worker on a timer.
1950
+ */
1951
+ async reconcile(batchSize) {
1952
+ const start = Date.now();
1953
+ let embedded = 0;
1954
+ let stale_reembedded = 0;
1955
+ let orphans_cleaned = 0;
1956
+ const currentModel = this.embeddingProvider.model;
1957
+ for (const namespace of EMBEDDABLE_TABLES) {
1958
+ const rows = this.recordSource.getEmbeddableRows(namespace, batchSize);
1959
+ for (const row of rows) {
1960
+ const embedding = await this.embeddingProvider.embed(row.text);
1961
+ if (embedding === null) {
1962
+ this.logger.warn(LOG_KINDS.EMBEDDING_PROVIDER, "Provider unavailable during reconcile, returning partial progress", {
1963
+ namespace,
1964
+ embedded
1965
+ });
1966
+ return {
1967
+ embedded,
1968
+ stale_reembedded,
1969
+ orphans_cleaned,
1970
+ duration_ms: Date.now() - start
1971
+ };
1972
+ }
1973
+ const hash = this.contentHash(row.text);
1974
+ this.vectorStore.upsert(namespace, row.id, embedding, {
1975
+ model: currentModel,
1976
+ provider: this.embeddingProvider.providerName,
1977
+ dimensions: this.embeddingProvider.dimensions,
1978
+ content_hash: hash,
1979
+ embedded_at: epochSeconds(),
1980
+ domain_metadata: row.metadata
1981
+ });
1982
+ this.recordSource.markEmbedded(namespace, row.id);
1983
+ embedded++;
1984
+ }
1985
+ const staleIds = this.vectorStore.getStaleIds(namespace, currentModel, batchSize);
1986
+ if (staleIds.length > 0) {
1987
+ const records = this.recordSource.getRecordContent(namespace, staleIds);
1988
+ const foundIds = new Set(records.map((r) => r.id));
1989
+ for (const record of records) {
1990
+ const embedding = await this.embeddingProvider.embed(record.text);
1991
+ if (embedding === null) {
1992
+ this.logger.warn(LOG_KINDS.EMBEDDING_PROVIDER, "Provider unavailable during stale re-embed, returning partial progress", {
1993
+ namespace,
1994
+ stale_reembedded
1995
+ });
1996
+ return {
1997
+ embedded,
1998
+ stale_reembedded,
1999
+ orphans_cleaned,
2000
+ duration_ms: Date.now() - start
2001
+ };
2002
+ }
2003
+ this.vectorStore.upsert(namespace, record.id, embedding, {
2004
+ model: currentModel,
2005
+ provider: this.embeddingProvider.providerName,
2006
+ dimensions: this.embeddingProvider.dimensions,
2007
+ content_hash: this.contentHash(record.text),
2008
+ embedded_at: epochSeconds(),
2009
+ domain_metadata: record.metadata
2010
+ });
2011
+ stale_reembedded++;
2012
+ }
2013
+ for (const staleId of staleIds) {
2014
+ if (!foundIds.has(staleId)) {
2015
+ this.vectorStore.remove(namespace, staleId);
2016
+ this.logger.warn(LOG_KINDS.EMBEDDING_CLEANUP, "Stale orphan vector cleaned", {
2017
+ namespace,
2018
+ id: staleId
2019
+ });
2020
+ orphans_cleaned++;
2021
+ }
2022
+ }
2023
+ }
2024
+ orphans_cleaned += this.sweepOrphans(namespace);
2025
+ }
2026
+ const duration_ms = Date.now() - start;
2027
+ if (embedded > 0 || stale_reembedded > 0 || orphans_cleaned > 0) {
2028
+ this.logger.info(LOG_KINDS.EMBEDDING_RECONCILE, "Reconcile cycle completed", {
2029
+ embedded,
2030
+ stale_reembedded,
2031
+ orphans_cleaned,
2032
+ duration_ms
2033
+ });
2034
+ }
2035
+ return { embedded, stale_reembedded, orphans_cleaned, duration_ms };
2036
+ }
2037
+ /**
2038
+ * Remove orphan vectors (vectors without corresponding active records).
2039
+ */
2040
+ cleanOrphans() {
2041
+ let orphans_cleaned = 0;
2042
+ for (const namespace of EMBEDDABLE_TABLES) {
2043
+ orphans_cleaned += this.sweepOrphans(namespace);
2044
+ }
2045
+ return { orphans_cleaned };
2046
+ }
2047
+ // -------------------------------------------------------------------------
2048
+ // Operations
2049
+ // -------------------------------------------------------------------------
2050
+ /**
2051
+ * Clear all vectors and reset embedded flags.
2052
+ * The reconcile worker picks up all rows on subsequent cycles.
2053
+ */
2054
+ rebuildAll() {
2055
+ const { cleared } = this.vectorStore.clear();
2056
+ this.recordSource.clearAllEmbedded();
2057
+ this.logger.info(LOG_KINDS.EMBEDDING_REBUILD, "Rebuild started", { cleared });
2058
+ return { queued: cleared };
2059
+ }
2060
+ /**
2061
+ * Re-embed vectors that were created with a different model.
2062
+ */
2063
+ async reembedStale(batchSize) {
2064
+ let reembedded = 0;
2065
+ const currentModel = this.embeddingProvider.model;
2066
+ for (const namespace of EMBEDDABLE_TABLES) {
2067
+ const staleIds = this.vectorStore.getStaleIds(namespace, currentModel, batchSize);
2068
+ if (staleIds.length === 0) continue;
2069
+ const records = this.recordSource.getRecordContent(namespace, staleIds);
2070
+ for (const record of records) {
2071
+ const embedding = await this.embeddingProvider.embed(record.text);
2072
+ if (embedding === null) {
2073
+ this.logger.warn(LOG_KINDS.EMBEDDING_PROVIDER, "Provider unavailable during re-embed", {
2074
+ namespace,
2075
+ reembedded
2076
+ });
2077
+ return { reembedded };
2078
+ }
2079
+ const hash = this.contentHash(record.text);
2080
+ this.vectorStore.upsert(namespace, record.id, embedding, {
2081
+ model: currentModel,
2082
+ provider: this.embeddingProvider.providerName,
2083
+ dimensions: this.embeddingProvider.dimensions,
2084
+ content_hash: hash,
2085
+ embedded_at: epochSeconds(),
2086
+ domain_metadata: record.metadata
2087
+ });
2088
+ reembedded++;
2089
+ }
2090
+ }
2091
+ return { reembedded };
2092
+ }
2093
+ /**
2094
+ * Get details for the operations UI: vector stats, pending counts, provider info.
2095
+ */
2096
+ getDetails() {
2097
+ const stats = this.vectorStore.stats();
2098
+ const pending = {};
2099
+ for (const namespace of EMBEDDABLE_TABLES) {
2100
+ pending[namespace] = this.recordSource.getPendingCount(namespace);
2101
+ }
2102
+ return {
2103
+ ...stats,
2104
+ pending,
2105
+ provider: {
2106
+ name: this.embeddingProvider.providerName,
2107
+ model: this.embeddingProvider.model,
2108
+ available: true
2109
+ // If we got here, the manager was constructed with a provider
2110
+ }
2111
+ };
2112
+ }
2113
+ /**
2114
+ * Pass-through for search handler — embed a query string.
2115
+ */
2116
+ async embedQuery(text) {
2117
+ return this.embeddingProvider.embed(text);
2118
+ }
2119
+ /**
2120
+ * Pass-through for search handler — similarity search via the vector store.
2121
+ * Keeps the VectorStore private to the manager.
2122
+ */
2123
+ searchVectors(query, options) {
2124
+ return this.vectorStore.search(query, options);
2125
+ }
2126
+ // -------------------------------------------------------------------------
2127
+ // Private helpers
2128
+ // -------------------------------------------------------------------------
2129
+ /**
2130
+ * Sweep orphan vectors for a single namespace. Returns count removed.
2131
+ *
2132
+ * Compares vector IDs against active record IDs — vectors without a matching
2133
+ * active record are removed. Does NOT short-circuit on count equality because
2134
+ * equal counts can mask orphans (e.g., 3 orphan vectors + 3 active records
2135
+ * missing vectors = same count, zero cleanup).
2136
+ */
2137
+ sweepOrphans(namespace) {
2138
+ const embeddedIds = this.vectorStore.getEmbeddedIds(namespace);
2139
+ if (embeddedIds.length === 0) return 0;
2140
+ const activeIds = this.recordSource.getActiveRecordIds(namespace);
2141
+ const activeSet = new Set(activeIds);
2142
+ let cleaned = 0;
2143
+ for (const vecId of embeddedIds) {
2144
+ if (!activeSet.has(vecId)) {
2145
+ this.vectorStore.remove(namespace, vecId);
2146
+ this.logger.warn(LOG_KINDS.EMBEDDING_CLEANUP, "Orphan vector cleaned", {
2147
+ namespace,
2148
+ id: vecId
2149
+ });
2150
+ cleaned++;
2151
+ }
2152
+ }
2153
+ return cleaned;
2154
+ }
2155
+ };
2156
+
2157
+ // src/daemon/embedding/sqlite-vec-store.ts
2158
+ import Database from "better-sqlite3";
2159
+ import * as sqliteVec from "sqlite-vec";
2160
+ var DEFAULT_SEARCH_LIMIT = 10;
2161
+ var DEFAULT_SIMILARITY_THRESHOLD = 0;
2162
+ var DEFAULT_META_MODEL = "unknown";
2163
+ var DEFAULT_META_PROVIDER = "unknown";
2164
+ var DEFAULT_META_CONTENT_HASH = "";
2165
+ var FILTERABLE_COLUMNS = /* @__PURE__ */ new Set(["model", "provider", "namespace"]);
2166
+ function cosineDistanceToSimilarity(distance) {
2167
+ return 1 - distance;
2168
+ }
2169
+ var METADATA_TABLE = `
2170
+ CREATE TABLE IF NOT EXISTS embedding_metadata (
2171
+ namespace TEXT NOT NULL,
2172
+ record_id TEXT NOT NULL,
2173
+ model TEXT NOT NULL,
2174
+ provider TEXT NOT NULL,
2175
+ dimensions INTEGER NOT NULL,
2176
+ content_hash TEXT NOT NULL,
2177
+ embedded_at INTEGER NOT NULL,
2178
+ domain_metadata TEXT,
2179
+ PRIMARY KEY (namespace, record_id)
2180
+ )`;
2181
+ var METADATA_MODEL_INDEX = `
2182
+ CREATE INDEX IF NOT EXISTS idx_emb_meta_model
2183
+ ON embedding_metadata (namespace, model)`;
2184
+ function vecTableDDL(namespace) {
2185
+ return `CREATE VIRTUAL TABLE IF NOT EXISTS vec_${namespace} USING vec0(
2186
+ record_id TEXT PRIMARY KEY,
2187
+ embedding float[${EMBEDDING_DIMENSIONS}] distance_metric=cosine
2188
+ )`;
2189
+ }
2190
+ var SqliteVecVectorStore = class {
2191
+ db;
2192
+ // Cached prepared statements (lazy-initialized per namespace)
2193
+ deleteVecStmts = /* @__PURE__ */ new Map();
2194
+ insertVecStmts = /* @__PURE__ */ new Map();
2195
+ upsertMetaStmt;
2196
+ deleteMetaStmt;
2197
+ searchStmts = /* @__PURE__ */ new Map();
2198
+ statsCountStmt;
2199
+ statsModelsStmt;
2200
+ staleIdsStmt;
2201
+ embeddedIdsStmt;
2202
+ constructor(dbPath) {
2203
+ this.db = new Database(dbPath ?? ":memory:");
2204
+ sqliteVec.load(this.db);
2205
+ this.db.pragma("journal_mode = WAL");
2206
+ this.createSchema();
2207
+ this.prepareStatements();
2208
+ }
2209
+ // -------------------------------------------------------------------------
2210
+ // Schema
2211
+ // -------------------------------------------------------------------------
2212
+ createSchema() {
2213
+ this.db.exec(METADATA_TABLE);
2214
+ this.db.exec(METADATA_MODEL_INDEX);
2215
+ for (const ns of EMBEDDABLE_TABLES) {
2216
+ this.db.exec(vecTableDDL(ns));
2217
+ }
2218
+ }
2219
+ prepareStatements() {
2220
+ this.upsertMetaStmt = this.db.prepare(`
2221
+ INSERT INTO embedding_metadata (namespace, record_id, model, provider, dimensions, content_hash, embedded_at, domain_metadata)
2222
+ VALUES (@namespace, @record_id, @model, @provider, @dimensions, @content_hash, @embedded_at, @domain_metadata)
2223
+ ON CONFLICT (namespace, record_id) DO UPDATE SET
2224
+ model = excluded.model,
2225
+ provider = excluded.provider,
2226
+ dimensions = excluded.dimensions,
2227
+ content_hash = excluded.content_hash,
2228
+ embedded_at = excluded.embedded_at,
2229
+ domain_metadata = excluded.domain_metadata
2230
+ `);
2231
+ this.deleteMetaStmt = this.db.prepare(
2232
+ `DELETE FROM embedding_metadata WHERE namespace = ? AND record_id = ?`
2233
+ );
2234
+ this.statsCountStmt = this.db.prepare(
2235
+ `SELECT COUNT(*) AS cnt FROM embedding_metadata WHERE namespace = ?`
2236
+ );
2237
+ this.statsModelsStmt = this.db.prepare(
2238
+ `SELECT model, COUNT(*) AS cnt FROM embedding_metadata WHERE namespace = ? GROUP BY model`
2239
+ );
2240
+ this.staleIdsStmt = this.db.prepare(
2241
+ `SELECT record_id FROM embedding_metadata WHERE namespace = ? AND model != ? LIMIT ?`
2242
+ );
2243
+ this.embeddedIdsStmt = this.db.prepare(
2244
+ `SELECT record_id FROM embedding_metadata WHERE namespace = ?`
2245
+ );
2246
+ for (const ns of EMBEDDABLE_TABLES) {
2247
+ this.deleteVecStmts.set(
2248
+ ns,
2249
+ this.db.prepare(`DELETE FROM vec_${ns} WHERE record_id = ?`)
2250
+ );
2251
+ this.insertVecStmts.set(
2252
+ ns,
2253
+ this.db.prepare(`INSERT INTO vec_${ns}(record_id, embedding) VALUES (?, ?)`)
2254
+ );
2255
+ this.searchStmts.set(
2256
+ ns,
2257
+ this.db.prepare(`
2258
+ SELECT v.record_id, v.distance,
2259
+ em.model, em.provider, em.content_hash, em.embedded_at, em.domain_metadata
2260
+ FROM vec_${ns} v
2261
+ LEFT JOIN embedding_metadata em
2262
+ ON em.namespace = '${ns}' AND em.record_id = v.record_id
2263
+ WHERE v.embedding MATCH ?
2264
+ AND k = ?
2265
+ ORDER BY v.distance
2266
+ `)
2267
+ );
2268
+ }
2269
+ }
2270
+ // -------------------------------------------------------------------------
2271
+ // VectorStore interface
2272
+ // -------------------------------------------------------------------------
2273
+ upsert(namespace, id, embedding, metadata) {
2274
+ this.validateNamespace(namespace);
2275
+ const ns = namespace;
2276
+ const vec = new Float32Array(embedding);
2277
+ const txn = this.db.transaction(() => {
2278
+ this.deleteVecStmts.get(ns).run(id);
2279
+ this.insertVecStmts.get(ns).run(id, vec);
2280
+ this.upsertMetaStmt.run({
2281
+ namespace: ns,
2282
+ record_id: id,
2283
+ model: metadata?.["model"] ?? DEFAULT_META_MODEL,
2284
+ provider: metadata?.["provider"] ?? DEFAULT_META_PROVIDER,
2285
+ dimensions: embedding.length,
2286
+ content_hash: metadata?.["content_hash"] ?? DEFAULT_META_CONTENT_HASH,
2287
+ embedded_at: metadata?.["embedded_at"] ?? Date.now(),
2288
+ domain_metadata: metadata?.["domain_metadata"] ? JSON.stringify(metadata["domain_metadata"]) : null
2289
+ });
2290
+ });
2291
+ txn();
2292
+ }
2293
+ remove(namespace, id) {
2294
+ this.validateNamespace(namespace);
2295
+ const ns = namespace;
2296
+ const txn = this.db.transaction(() => {
2297
+ this.deleteVecStmts.get(ns).run(id);
2298
+ this.deleteMetaStmt.run(ns, id);
2299
+ });
2300
+ txn();
2301
+ }
2302
+ clear(namespace) {
2303
+ let cleared = 0;
2304
+ const targets = namespace ? [this.validatedNamespace(namespace)] : [...EMBEDDABLE_TABLES];
2305
+ const txn = this.db.transaction(() => {
2306
+ for (const ns of targets) {
2307
+ const countRow = this.db.prepare(`SELECT COUNT(*) as cnt FROM embedding_metadata WHERE namespace = ?`).get(ns);
2308
+ cleared += countRow.cnt;
2309
+ this.db.exec(`DELETE FROM vec_${ns}`);
2310
+ this.db.prepare(`DELETE FROM embedding_metadata WHERE namespace = ?`).run(ns);
2311
+ }
2312
+ });
2313
+ txn();
2314
+ return { cleared };
2315
+ }
2316
+ /**
2317
+ * KNN similarity search across one or all namespaces.
2318
+ *
2319
+ * Threshold filtering is applied **post-KNN**: sqlite-vec returns the top-k
2320
+ * nearest neighbors first, then results below `threshold` are discarded.
2321
+ * This means fewer than `limit` results may be returned when a threshold is set.
2322
+ * This is standard KNN behavior, not a bug.
2323
+ */
2324
+ search(query, options) {
2325
+ const limit = options?.limit ?? DEFAULT_SEARCH_LIMIT;
2326
+ const threshold = options?.threshold ?? DEFAULT_SIMILARITY_THRESHOLD;
2327
+ const queryVec = new Float32Array(query);
2328
+ const targets = options?.namespace ? [this.validatedNamespace(options.namespace)] : [...EMBEDDABLE_TABLES];
2329
+ const hasFilters = options?.filters && Object.keys(options.filters).length > 0;
2330
+ const results = [];
2331
+ for (const ns of targets) {
2332
+ let rows;
2333
+ if (hasFilters) {
2334
+ const { sql, params } = this.buildFilteredSearchQuery(
2335
+ ns,
2336
+ options.filters,
2337
+ limit
2338
+ );
2339
+ const stmt = this.db.prepare(sql);
2340
+ rows = stmt.all(queryVec, limit, ...params);
2341
+ } else {
2342
+ rows = this.searchStmts.get(ns).all(queryVec, limit);
2343
+ }
2344
+ for (const row of rows) {
2345
+ const similarity = cosineDistanceToSimilarity(row.distance);
2346
+ if (similarity >= threshold) {
2347
+ results.push({
2348
+ id: row.record_id,
2349
+ namespace: ns,
2350
+ similarity,
2351
+ metadata: {
2352
+ model: row.model,
2353
+ provider: row.provider,
2354
+ content_hash: row.content_hash,
2355
+ embedded_at: row.embedded_at,
2356
+ ...row.domain_metadata ? JSON.parse(row.domain_metadata) : {}
2357
+ }
2358
+ });
2359
+ }
2360
+ }
2361
+ }
2362
+ results.sort((a, b) => b.similarity - a.similarity);
2363
+ return results.slice(0, limit);
2364
+ }
2365
+ stats(namespace) {
2366
+ const targets = namespace ? [this.validatedNamespace(namespace)] : [...EMBEDDABLE_TABLES];
2367
+ let total = 0;
2368
+ const by_namespace = {};
2369
+ const models = {};
2370
+ for (const ns of targets) {
2371
+ const countRow = this.statsCountStmt.get(ns);
2372
+ const modelRows = this.statsModelsStmt.all(ns);
2373
+ let stale = 0;
2374
+ let maxModelCount = 0;
2375
+ for (const mr of modelRows) {
2376
+ models[mr.model] = (models[mr.model] ?? 0) + mr.cnt;
2377
+ if (mr.cnt > maxModelCount) maxModelCount = mr.cnt;
2378
+ }
2379
+ stale = countRow.cnt - maxModelCount;
2380
+ if (stale < 0) stale = 0;
2381
+ by_namespace[ns] = { embedded: countRow.cnt, stale };
2382
+ total += countRow.cnt;
2383
+ }
2384
+ return { total, by_namespace, models };
2385
+ }
2386
+ getStaleIds(namespace, currentModel, limit) {
2387
+ this.validateNamespace(namespace);
2388
+ const rows = this.staleIdsStmt.all(namespace, currentModel, limit);
2389
+ return rows.map((r) => r.record_id);
2390
+ }
2391
+ getEmbeddedIds(namespace) {
2392
+ this.validateNamespace(namespace);
2393
+ const rows = this.embeddedIdsStmt.all(namespace);
2394
+ return rows.map((r) => r.record_id);
2395
+ }
2396
+ // -------------------------------------------------------------------------
2397
+ // Lifecycle
2398
+ // -------------------------------------------------------------------------
2399
+ close() {
2400
+ this.db.close();
2401
+ }
2402
+ // -------------------------------------------------------------------------
2403
+ // Private helpers
2404
+ // -------------------------------------------------------------------------
2405
+ validateNamespace(namespace) {
2406
+ if (!EMBEDDABLE_TABLES.includes(namespace)) {
2407
+ throw new Error(
2408
+ `Invalid namespace "${namespace}". Must be one of: ${EMBEDDABLE_TABLES.join(", ")}`
2409
+ );
2410
+ }
2411
+ }
2412
+ validatedNamespace(namespace) {
2413
+ this.validateNamespace(namespace);
2414
+ return namespace;
2415
+ }
2416
+ /**
2417
+ * Build a filtered KNN query that JOINs vec results with embedding_metadata.
2418
+ * Filters are applied as WHERE conditions on the metadata table.
2419
+ */
2420
+ buildFilteredSearchQuery(namespace, filters, _limit) {
2421
+ const conditions = [];
2422
+ const params = [];
2423
+ for (const [key, value] of Object.entries(filters)) {
2424
+ if (FILTERABLE_COLUMNS.has(key)) {
2425
+ conditions.push(`em.${key} = ?`);
2426
+ params.push(value);
2427
+ }
2428
+ }
2429
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2430
+ const sql = `
2431
+ WITH knn AS (
2432
+ SELECT record_id, distance
2433
+ FROM vec_${namespace}
2434
+ WHERE embedding MATCH ?
2435
+ AND k = ?
2436
+ ORDER BY distance
2437
+ )
2438
+ SELECT knn.record_id, knn.distance,
2439
+ em.model, em.provider, em.content_hash, em.embedded_at, em.domain_metadata
2440
+ FROM knn
2441
+ INNER JOIN embedding_metadata em
2442
+ ON em.namespace = '${namespace}' AND em.record_id = knn.record_id
2443
+ ${whereClause}
2444
+ `;
2445
+ return { sql, params };
2446
+ }
2447
+ };
2448
+
2449
+ // src/intelligence/embeddings.ts
2450
+ async function generateEmbedding(backend, text) {
2451
+ const raw = await backend.embed(text);
2452
+ return {
2453
+ embedding: normalize(raw.embedding),
2454
+ model: raw.model,
2455
+ dimensions: raw.dimensions
2456
+ };
2457
+ }
2458
+ function normalize(vec) {
2459
+ const magnitude = Math.sqrt(vec.reduce((sum, v) => sum + v * v, 0));
2460
+ if (magnitude === 0) return vec;
2461
+ return vec.map((v) => v / magnitude);
2462
+ }
2463
+
2464
+ // src/daemon/embedding/provider-adapter.ts
2465
+ var AVAILABILITY_CACHE_TTL_MS = 5e3;
2466
+ var OLLAMA_DEFAULT_TAG = ":latest";
2467
+ var TAGGED_PROVIDERS = /* @__PURE__ */ new Set(["ollama"]);
2468
+ function normalizeModelName(model, provider) {
2469
+ if (TAGGED_PROVIDERS.has(provider) && !model.includes(":")) {
2470
+ return model + OLLAMA_DEFAULT_TAG;
2471
+ }
2472
+ return model;
2473
+ }
2474
+ var EmbeddingProviderAdapter = class {
2475
+ constructor(provider, config) {
2476
+ this.provider = provider;
2477
+ this.model = normalizeModelName(config.model, config.provider);
2478
+ this.providerName = config.provider;
2479
+ this.dimensions = EMBEDDING_DIMENSIONS;
2480
+ }
2481
+ model;
2482
+ providerName;
2483
+ dimensions;
2484
+ /** Cached availability state to avoid per-embed HTTP probes. */
2485
+ cachedAvailable = null;
2486
+ cachedAvailableAt = 0;
2487
+ async embed(text) {
2488
+ try {
2489
+ const isUp = await this.checkAvailability();
2490
+ if (!isUp) return null;
2491
+ const result = await generateEmbedding(this.provider, text);
2492
+ return result.embedding;
2493
+ } catch {
2494
+ this.cachedAvailable = null;
2495
+ return null;
2496
+ }
2497
+ }
2498
+ /** Check availability with a short TTL cache to avoid HTTP probes on every call. */
2499
+ async checkAvailability() {
2500
+ const now = Date.now();
2501
+ if (this.cachedAvailable !== null && now - this.cachedAvailableAt < AVAILABILITY_CACHE_TTL_MS) {
2502
+ return this.cachedAvailable;
2503
+ }
2504
+ this.cachedAvailable = await this.provider.isAvailable();
2505
+ this.cachedAvailableAt = now;
2506
+ return this.cachedAvailable;
2507
+ }
2508
+ };
2509
+
2510
+ // src/daemon/embedding/record-source.ts
2511
+ var ACTIVE_STATUS2 = "active";
2512
+ function sessionMetadata(row) {
2513
+ return {
2514
+ ...row.project_root != null ? { project_root: row.project_root } : {}
2515
+ };
2516
+ }
2517
+ function sporeMetadata(row) {
2518
+ return {
2519
+ ...row.status != null ? { status: row.status } : {},
2520
+ ...row.session_id != null ? { session_id: row.session_id } : {},
2521
+ ...row.observation_type != null ? { observation_type: row.observation_type } : {}
2522
+ };
2523
+ }
2524
+ function emptyMetadata() {
2525
+ return {};
2526
+ }
2527
+ function planMetadata(row) {
2528
+ return {
2529
+ ...row.session_id != null ? { session_id: row.session_id } : {},
2530
+ ...row.source_path != null ? { source_path: row.source_path } : {}
2531
+ };
2532
+ }
2533
+ function metadataFor(namespace, row) {
2534
+ switch (namespace) {
2535
+ case "sessions":
2536
+ return sessionMetadata(row);
2537
+ case "spores":
2538
+ return sporeMetadata(row);
2539
+ case "plans":
2540
+ return planMetadata(row);
2541
+ case "artifacts":
2542
+ return emptyMetadata();
2543
+ }
2544
+ }
2545
+ var SqliteRecordSource = class {
2546
+ /**
2547
+ * Get rows that need embedding (embedded=0, content non-null).
2548
+ *
2549
+ * For spores: additionally filters WHERE status = 'active'.
2550
+ * For sessions: delegates to getUnembedded (which filters summary IS NOT NULL).
2551
+ */
2552
+ getEmbeddableRows(namespace, limit) {
2553
+ assertValidTable(namespace);
2554
+ if (namespace === "spores") {
2555
+ return this.getUnembeddedActiveSpores(limit);
2556
+ }
2557
+ const rows = getUnembedded(namespace, limit);
2558
+ const db = getDatabase();
2559
+ return rows.map((row) => {
2560
+ const fullRow = db.prepare(`SELECT * FROM ${namespace} WHERE id = ?`).get(row.id);
2561
+ return {
2562
+ id: String(row.id),
2563
+ text: row.text,
2564
+ metadata: metadataFor(namespace, fullRow)
2565
+ };
2566
+ });
2567
+ }
2568
+ /**
2569
+ * Get IDs of all records that should have embeddings.
2570
+ *
2571
+ * - sessions: those with a non-null summary
2572
+ * - spores: those with status = 'active'
2573
+ * - plans/artifacts: those with non-null content
2574
+ */
2575
+ getActiveRecordIds(namespace) {
2576
+ assertValidTable(namespace);
2577
+ const db = getDatabase();
2578
+ switch (namespace) {
2579
+ case "sessions": {
2580
+ const rows = db.prepare(
2581
+ `SELECT id FROM sessions WHERE summary IS NOT NULL`
2582
+ ).all();
2583
+ return rows.map((r) => r.id);
2584
+ }
2585
+ case "spores": {
2586
+ const rows = db.prepare(
2587
+ `SELECT id FROM spores WHERE status = ?`
2588
+ ).all(ACTIVE_STATUS2);
2589
+ return rows.map((r) => r.id);
2590
+ }
2591
+ case "plans": {
2592
+ const rows = db.prepare(
2593
+ `SELECT id FROM plans WHERE content IS NOT NULL`
2594
+ ).all();
2595
+ return rows.map((r) => r.id);
2596
+ }
2597
+ case "artifacts": {
2598
+ const rows = db.prepare(
2599
+ `SELECT id FROM artifacts WHERE content IS NOT NULL`
2600
+ ).all();
2601
+ return rows.map((r) => r.id);
2602
+ }
2603
+ }
2604
+ }
2605
+ /**
2606
+ * Fetch content + metadata for specific record IDs.
2607
+ *
2608
+ * Returns same shape as getEmbeddableRows but for specific records.
2609
+ * Empty ids array returns empty result.
2610
+ */
2611
+ getRecordContent(namespace, ids) {
2612
+ assertValidTable(namespace);
2613
+ if (ids.length === 0) return [];
2614
+ const db = getDatabase();
2615
+ const textCol = EMBEDDABLE_TEXT_COLUMNS[namespace];
2616
+ const placeholders = ids.map(() => "?").join(", ");
2617
+ const rows = db.prepare(
2618
+ `SELECT *, ${textCol} AS text FROM ${namespace} WHERE id IN (${placeholders})`
2619
+ ).all(...ids);
2620
+ return rows.map((row) => ({
2621
+ id: String(row.id),
2622
+ text: row.text,
2623
+ metadata: metadataFor(namespace, row)
2624
+ }));
2625
+ }
2626
+ /** Mark a record as embedded. Delegates to existing helper. */
2627
+ markEmbedded(namespace, id) {
2628
+ markEmbedded(namespace, id);
2629
+ }
2630
+ /** Clear the embedded flag on a record. Delegates to existing helper. */
2631
+ clearEmbedded(namespace, id) {
2632
+ clearEmbedded(namespace, id);
2633
+ }
2634
+ /**
2635
+ * Clear the embedded flag on all records, optionally scoped to a namespace.
2636
+ *
2637
+ * If namespace is omitted, clears all embeddable tables.
2638
+ */
2639
+ clearAllEmbedded(namespace) {
2640
+ const db = getDatabase();
2641
+ if (namespace !== void 0) {
2642
+ assertValidTable(namespace);
2643
+ db.prepare(`UPDATE ${namespace} SET embedded = 0`).run();
2644
+ return;
2645
+ }
2646
+ for (const table of EMBEDDABLE_TABLES) {
2647
+ db.prepare(`UPDATE ${table} SET embedded = 0`).run();
2648
+ }
2649
+ }
2650
+ /**
2651
+ * Count rows that need embedding — lightweight SELECT COUNT(*), no row materialization.
2652
+ */
2653
+ getPendingCount(namespace) {
2654
+ assertValidTable(namespace);
2655
+ const db = getDatabase();
2656
+ const contentFilter = namespace === "sessions" ? " AND summary IS NOT NULL" : "";
2657
+ const statusFilter = namespace === "spores" ? ` AND status = '${ACTIVE_STATUS2}'` : "";
2658
+ const row = db.prepare(
2659
+ `SELECT COUNT(*) AS cnt FROM ${namespace} WHERE embedded = 0${contentFilter}${statusFilter}`
2660
+ ).get();
2661
+ return Number(row.cnt);
2662
+ }
2663
+ // ---------------------------------------------------------------------------
2664
+ // Private helpers
2665
+ // ---------------------------------------------------------------------------
2666
+ /** Custom query for spores: embedded=0 AND status='active'. */
2667
+ getUnembeddedActiveSpores(limit) {
2668
+ const db = getDatabase();
2669
+ const rows = db.prepare(
2670
+ `SELECT id, content AS text, status, session_id, observation_type
2671
+ FROM spores
2672
+ WHERE embedded = 0 AND status = ?
2673
+ ORDER BY created_at ASC
2674
+ LIMIT ?`
2675
+ ).all(ACTIVE_STATUS2, limit);
2676
+ return rows.map((row) => ({
2677
+ id: String(row.id),
2678
+ text: row.text,
2679
+ metadata: sporeMetadata(row)
2680
+ }));
2681
+ }
2682
+ };
2683
+
2684
+ // src/daemon/api/agent-tasks.ts
2685
+ var import_yaml = __toESM(require_dist(), 1);
2686
+ var HTTP_OK = 200;
2687
+ var HTTP_CREATED = 201;
2688
+ var HTTP_BAD_REQUEST = 400;
2689
+ var HTTP_FORBIDDEN = 403;
2690
+ var HTTP_NOT_FOUND = 404;
2691
+ var HTTP_CONFLICT = 409;
2692
+ async function handleListTasks(req, vaultDir) {
2693
+ const definitionsDir = resolveDefinitionsDir();
2694
+ const allTasks = loadAllTasks(definitionsDir, vaultDir);
2695
+ let tasks = Array.from(allTasks.values());
2696
+ const sourceFilter = req.query?.source;
2697
+ if (sourceFilter) {
2698
+ tasks = tasks.filter((t) => t.source === sourceFilter);
2699
+ }
2700
+ return { status: HTTP_OK, body: { tasks } };
2701
+ }
2702
+ async function handleGetTask(req, vaultDir) {
2703
+ const definitionsDir = resolveDefinitionsDir();
2704
+ const allTasks = loadAllTasks(definitionsDir, vaultDir);
2705
+ const task = allTasks.get(req.params.id);
2706
+ if (!task) {
2707
+ return { status: HTTP_NOT_FOUND, body: { error: "task_not_found" } };
2708
+ }
2709
+ return { status: HTTP_OK, body: { task } };
2710
+ }
2711
+ async function handleCreateTask(req, vaultDir) {
2712
+ const result = AgentTaskSchema.safeParse(req.body);
2713
+ if (!result.success) {
2714
+ return {
2715
+ status: HTTP_BAD_REQUEST,
2716
+ body: { error: "validation_failed", issues: result.error.issues }
2717
+ };
2718
+ }
2719
+ const parsed = result.data;
2720
+ if (!validateTaskName(parsed.name)) {
2721
+ return {
2722
+ status: HTTP_BAD_REQUEST,
2723
+ body: { error: "invalid_task_name", name: parsed.name }
2724
+ };
2725
+ }
2726
+ const definitionsDir = resolveDefinitionsDir();
2727
+ const allTasks = loadAllTasks(definitionsDir, vaultDir);
2728
+ const existing = allTasks.get(parsed.name);
2729
+ if (existing && existing.source === USER_TASK_SOURCE) {
2730
+ return {
2731
+ status: HTTP_CONFLICT,
2732
+ body: { error: "task_already_exists", name: parsed.name }
2733
+ };
2734
+ }
2735
+ const task = {
2736
+ ...parsed,
2737
+ isBuiltin: false,
2738
+ source: USER_TASK_SOURCE
2739
+ };
2740
+ writeUserTask(vaultDir, task);
2741
+ return { status: HTTP_CREATED, body: { task } };
2742
+ }
2743
+ async function handleCopyTask(req, vaultDir) {
2744
+ const sourceName = req.params.id;
2745
+ const newName = req.body?.name;
2746
+ const definitionsDir = resolveDefinitionsDir();
2747
+ if (newName !== void 0 && !validateTaskName(newName)) {
2748
+ return {
2749
+ status: HTTP_BAD_REQUEST,
2750
+ body: { error: "invalid_task_name", name: newName }
2751
+ };
2752
+ }
2753
+ try {
2754
+ const copy = copyTaskToUser(definitionsDir, vaultDir, sourceName, newName);
2755
+ return { status: HTTP_CREATED, body: { task: copy } };
2756
+ } catch (err) {
2757
+ const message = errorMessage(err);
2758
+ if (message.includes("not found")) {
2759
+ return { status: HTTP_NOT_FOUND, body: { error: "task_not_found", name: sourceName } };
2760
+ }
2761
+ return { status: HTTP_BAD_REQUEST, body: { error: "copy_failed", message } };
2762
+ }
2763
+ }
2764
+ async function handleGetTaskYaml(req, vaultDir) {
2765
+ const taskName = req.params.id;
2766
+ const definitionsDir = resolveDefinitionsDir();
2767
+ const allTasks = loadAllTasks(definitionsDir, vaultDir);
2768
+ const task = allTasks.get(taskName);
2769
+ if (!task) {
2770
+ return { status: HTTP_NOT_FOUND, body: { error: "task_not_found", name: taskName } };
2771
+ }
2772
+ const { isBuiltin: _ib, source: _src, ...serializable } = task;
2773
+ const yaml = (0, import_yaml.stringify)(serializable);
2774
+ return { status: HTTP_OK, body: { yaml, source: task.source } };
2775
+ }
2776
+ async function handleUpdateTask(req, vaultDir) {
2777
+ const taskName = req.params.id;
2778
+ const definitionsDir = resolveDefinitionsDir();
2779
+ const allTasks = loadAllTasks(definitionsDir, vaultDir);
2780
+ const existing = allTasks.get(taskName);
2781
+ if (!existing) {
2782
+ return { status: HTTP_NOT_FOUND, body: { error: "task_not_found", name: taskName } };
2783
+ }
2784
+ if (existing.isBuiltin || existing.source !== USER_TASK_SOURCE) {
2785
+ return { status: HTTP_FORBIDDEN, body: { error: "cannot_update_builtin", name: taskName } };
2786
+ }
2787
+ const body = req.body;
2788
+ const yamlContent = body?.yaml;
2789
+ if (typeof yamlContent !== "string") {
2790
+ return { status: HTTP_BAD_REQUEST, body: { error: "missing_yaml_field" } };
2791
+ }
2792
+ try {
2793
+ const parsed = AgentTaskSchema.parse((0, import_yaml.parse)(yamlContent));
2794
+ const task = { ...taskFromParsed(parsed), isBuiltin: false, source: USER_TASK_SOURCE };
2795
+ if (task.name !== taskName) {
2796
+ return { status: HTTP_BAD_REQUEST, body: { error: "name_mismatch", expected: taskName, got: task.name } };
2797
+ }
2798
+ writeUserTask(vaultDir, task);
2799
+ return { status: HTTP_OK, body: { task } };
2800
+ } catch (err) {
2801
+ const message = errorMessage(err);
2802
+ return { status: HTTP_BAD_REQUEST, body: { error: "validation_failed", message } };
2803
+ }
2804
+ }
2805
+ async function handleDeleteTask(req, vaultDir) {
2806
+ const taskName = req.params.id;
2807
+ const definitionsDir = resolveDefinitionsDir();
2808
+ const allTasks = loadAllTasks(definitionsDir, vaultDir);
2809
+ const task = allTasks.get(taskName);
2810
+ if (!task) {
2811
+ return { status: HTTP_NOT_FOUND, body: { error: "task_not_found", name: taskName } };
2812
+ }
2813
+ if (task.isBuiltin || task.source !== USER_TASK_SOURCE) {
2814
+ return {
2815
+ status: HTTP_FORBIDDEN,
2816
+ body: { error: "cannot_delete_builtin", name: taskName }
2817
+ };
2818
+ }
2819
+ deleteUserTask(vaultDir, taskName);
2820
+ return { status: HTTP_OK, body: { deleted: taskName } };
2821
+ }
2822
+ async function handleGetTaskConfig(req, vaultDir) {
2823
+ const taskId = req.params.id;
2824
+ const config = loadConfig(vaultDir);
2825
+ const taskConfig = config.agent.tasks?.[taskId] ?? null;
2826
+ return { status: HTTP_OK, body: { taskId, config: taskConfig } };
2827
+ }
2828
+ async function handleUpdateTaskConfig(req, vaultDir) {
2829
+ const taskId = req.params.id;
2830
+ const body = req.body;
2831
+ if (!body) {
2832
+ return { status: HTTP_BAD_REQUEST, body: { error: "missing_body" } };
2833
+ }
2834
+ const updated = updateConfig(
2835
+ vaultDir,
2836
+ (config) => withTaskConfig(config, taskId, body)
2837
+ );
2838
+ return {
2839
+ status: HTTP_OK,
2840
+ body: { taskId, config: updated.agent.tasks?.[taskId] ?? null }
2841
+ };
2842
+ }
2843
+
2844
+ // src/daemon/api/providers.ts
2845
+ var HTTP_OK2 = 200;
2846
+ var HTTP_BAD_REQUEST2 = 400;
2847
+ async function handleGetProviders() {
2848
+ const results = await Promise.allSettled([
2849
+ detectLocalProviderInfo("ollama", OllamaBackend.DEFAULT_BASE_URL),
2850
+ detectLocalProviderInfo("lmstudio", LmStudioBackend.DEFAULT_BASE_URL),
2851
+ detectCloud()
2852
+ ]);
2853
+ const providers = results.map(
2854
+ (r) => r.status === "fulfilled" ? r.value : { type: "unknown", available: false, models: [] }
2855
+ );
2856
+ return { status: HTTP_OK2, body: { providers } };
2857
+ }
2858
+ async function handleTestProvider(req) {
2859
+ const body = req.body;
2860
+ const type = body?.type;
2861
+ if (!type || !["cloud", "ollama", "lmstudio"].includes(type)) {
2862
+ return {
2863
+ status: HTTP_BAD_REQUEST2,
2864
+ body: { error: "type is required and must be one of: cloud, ollama, lmstudio" }
2865
+ };
2866
+ }
2867
+ const baseUrl = body?.baseUrl;
2868
+ const start = performance.now();
2869
+ let result;
2870
+ try {
2871
+ if (type === "ollama") {
2872
+ result = await testLocalProvider(new OllamaBackend({ base_url: baseUrl }), "Ollama", OllamaBackend.DEFAULT_BASE_URL, baseUrl);
2873
+ } else if (type === "lmstudio") {
2874
+ result = await testLocalProvider(new LmStudioBackend({ base_url: baseUrl }), "LM Studio", LmStudioBackend.DEFAULT_BASE_URL, baseUrl);
2875
+ } else {
2876
+ result = testCloud();
2877
+ }
2878
+ } catch (err) {
2879
+ result = { ok: false, error: String(err) };
2880
+ }
2881
+ if (result.ok) {
2882
+ result.latency_ms = Math.round(performance.now() - start);
2883
+ }
2884
+ return { status: HTTP_OK2, body: result };
2885
+ }
2886
+ async function detectLocalProviderInfo(type, defaultBaseUrl) {
2887
+ const status = await checkLocalProvider(type);
2888
+ const models = status.models.filter((m) => !/-ctx\d+/.test(m));
2889
+ return { type, available: status.available, baseUrl: defaultBaseUrl, models };
2890
+ }
2891
+ async function detectCloud() {
2892
+ return { type: "cloud", available: true, models: ANTHROPIC_MODELS };
2893
+ }
2894
+ async function testLocalProvider(backend, label, defaultBaseUrl, baseUrl) {
2895
+ const available = await backend.isAvailable();
2896
+ if (!available) {
2897
+ return { ok: false, error: `${label} not reachable at ${baseUrl ?? defaultBaseUrl}` };
2898
+ }
2899
+ return { ok: true };
2900
+ }
2901
+ function testCloud() {
2902
+ return { ok: true };
2903
+ }
2904
+
2905
+ // src/daemon/log-reconcile.ts
2906
+ import fs4 from "fs";
2907
+ import path5 from "path";
2908
+ function reconcileLogBuffer(logDir, sinceTimestamp) {
2909
+ let replayed = 0;
2910
+ const files = [];
2911
+ for (let i = 3; i >= 1; i--) {
2912
+ const rotated = path5.join(logDir, `daemon.${i}.log`);
2913
+ if (fs4.existsSync(rotated)) files.push(rotated);
2914
+ }
2915
+ const current = path5.join(logDir, "daemon.log");
2916
+ if (fs4.existsSync(current)) files.push(current);
2917
+ for (const file of files) {
2918
+ const content = fs4.readFileSync(file, "utf-8");
2919
+ for (const line of content.split("\n")) {
2920
+ if (!line.trim()) continue;
2921
+ try {
2922
+ const entry = JSON.parse(line);
2923
+ if (entry.timestamp > sinceTimestamp) {
2924
+ const { timestamp, level, kind, component, message, ...rest } = entry;
2925
+ insertLogEntry({
2926
+ timestamp,
2927
+ level,
2928
+ kind: kind ?? `${component ?? "unknown"}.unknown`,
2929
+ component: component ?? kindToComponent(kind ?? "unknown"),
2930
+ message,
2931
+ data: Object.keys(rest).length > 0 ? JSON.stringify(rest) : null,
2932
+ session_id: rest.session_id ?? null
2933
+ });
2934
+ replayed++;
2935
+ }
2936
+ } catch {
2937
+ }
2938
+ }
2939
+ }
2940
+ return replayed;
2941
+ }
2942
+
2943
+ // src/daemon/power.ts
2944
+ var PowerManager = class {
2945
+ state = "active";
2946
+ lastActivity = Date.now();
2947
+ jobs = [];
2948
+ timer = null;
2949
+ running = false;
2950
+ config;
2951
+ logger;
2952
+ constructor(config) {
2953
+ this.config = config;
2954
+ this.logger = config.logger;
2955
+ }
2956
+ register(job) {
2957
+ this.jobs.push(job);
2958
+ }
2959
+ recordActivity() {
2960
+ this.lastActivity = Date.now();
2961
+ if (this.state === "deep_sleep") {
2962
+ this.logger.info(LOG_KINDS.POWER_STATE, "Waking from deep sleep");
2963
+ this.state = "active";
2964
+ this.scheduleNextTick();
2965
+ }
2966
+ }
2967
+ start() {
2968
+ this.lastActivity = Date.now();
2969
+ this.state = "active";
2970
+ this.running = true;
2971
+ this.scheduleNextTick();
2972
+ this.logger.info(LOG_KINDS.POWER_STATE, "PowerManager started", {
2973
+ jobs: this.jobs.map((j) => j.name)
2974
+ });
2975
+ }
2976
+ stop() {
2977
+ this.running = false;
2978
+ if (this.timer) {
2979
+ clearTimeout(this.timer);
2980
+ this.timer = null;
2981
+ }
2982
+ this.logger.info(LOG_KINDS.POWER_STATE, "PowerManager stopped");
2983
+ }
2984
+ getState() {
2985
+ this.evaluateState();
2986
+ return this.state;
2987
+ }
2988
+ evaluateState() {
2989
+ const idleMs = Date.now() - this.lastActivity;
2990
+ let target;
2991
+ if (idleMs >= this.config.deepSleepThresholdMs) {
2992
+ target = "deep_sleep";
2993
+ } else if (idleMs >= this.config.sleepThresholdMs) {
2994
+ target = "sleep";
2995
+ } else if (idleMs >= this.config.idleThresholdMs) {
2996
+ target = "idle";
2997
+ } else {
2998
+ target = "active";
2999
+ }
3000
+ if (target !== this.state) {
3001
+ this.logger.info(LOG_KINDS.POWER_STATE, "Power state transition", {
3002
+ from: this.state,
3003
+ to: target,
3004
+ idle_ms: idleMs
3005
+ });
3006
+ this.state = target;
3007
+ }
3008
+ }
3009
+ scheduleNextTick() {
3010
+ if (!this.running) return;
3011
+ if (this.timer) clearTimeout(this.timer);
3012
+ const interval = this.state === "sleep" ? this.config.sleepIntervalMs : this.config.activeIntervalMs;
3013
+ this.timer = setTimeout(() => this.tick(), interval);
3014
+ }
3015
+ async tick() {
3016
+ if (!this.running) return;
3017
+ this.evaluateState();
3018
+ if (this.state === "deep_sleep") {
3019
+ this.logger.info(LOG_KINDS.POWER_STATE, "Entering deep sleep \u2014 timer stopped");
3020
+ this.timer = null;
3021
+ return;
3022
+ }
3023
+ const eligible = this.jobs.filter((j) => j.runIn.includes(this.state));
3024
+ this.logger.debug(LOG_KINDS.POWER_TICK, "Tick", {
3025
+ state: this.state,
3026
+ jobs: eligible.map((j) => j.name)
3027
+ });
3028
+ for (const job of eligible) {
3029
+ try {
3030
+ await job.fn();
3031
+ } catch (err) {
3032
+ this.logger.error(LOG_KINDS.POWER_JOB_ERROR, `Job "${job.name}" failed`, {
3033
+ error: err.message
3034
+ });
3035
+ }
3036
+ }
3037
+ this.scheduleNextTick();
3038
+ }
3039
+ };
3040
+
3041
+ // src/daemon/jobs/session-cleanup.ts
3042
+ import { unlink, glob } from "fs/promises";
3043
+ async function cleanupAfterSessionCascade(sessionId, result, embeddingManager, vaultDir) {
3044
+ try {
3045
+ embeddingManager.onRemoved("sessions", sessionId);
3046
+ } catch {
3047
+ }
3048
+ for (const sporeId of result.deletedSporeIds) {
3049
+ try {
3050
+ embeddingManager.onRemoved("spores", sporeId);
3051
+ } catch {
3052
+ }
3053
+ }
3054
+ try {
3055
+ for await (const f of glob(`sessions/**/session-${sessionId}.md`, { cwd: vaultDir })) {
3056
+ await unlink(`${vaultDir}/${f}`).catch(() => {
3057
+ });
3058
+ }
3059
+ } catch {
3060
+ }
3061
+ for (const sporeId of result.deletedSporeIds) {
3062
+ try {
3063
+ for await (const f of glob(`spores/**/${sporeId}*.md`, { cwd: vaultDir })) {
3064
+ await unlink(`${vaultDir}/${f}`).catch(() => {
3065
+ });
3066
+ }
3067
+ } catch {
3068
+ }
3069
+ }
3070
+ for (const filePath of result.deletedAttachmentPaths) {
3071
+ try {
3072
+ await unlink(filePath);
3073
+ } catch {
3074
+ }
3075
+ }
3076
+ }
3077
+
3078
+ // src/daemon/jobs/session-maintenance.ts
3079
+ var STALE_SESSION_THRESHOLD_S = STALE_SESSION_THRESHOLD_MS / MS_PER_SECOND;
3080
+ function completeStaleActiveSessions(registeredSessionIds) {
3081
+ const db = getDatabase();
3082
+ const cutoff = epochSeconds() - STALE_SESSION_THRESHOLD_S;
3083
+ const excludePlaceholders = registeredSessionIds.length > 0 ? `AND id NOT IN (${registeredSessionIds.map(() => "?").join(", ")})` : "";
3084
+ const params = [cutoff, ...registeredSessionIds];
3085
+ const info = db.prepare(
3086
+ `UPDATE sessions
3087
+ SET status = 'completed'
3088
+ WHERE status = 'active'
3089
+ AND COALESCE(
3090
+ (SELECT MAX(pb.started_at) FROM prompt_batches pb WHERE pb.session_id = sessions.id),
3091
+ sessions.started_at
3092
+ ) < ?
3093
+ ${excludePlaceholders}`
3094
+ ).run(...params);
3095
+ return info.changes;
3096
+ }
3097
+ function findDeadSessionIds(registeredSessionIds) {
3098
+ const db = getDatabase();
3099
+ const excludePlaceholders = registeredSessionIds.length > 0 ? `AND id NOT IN (${registeredSessionIds.map(() => "?").join(", ")})` : "";
3100
+ const params = [DEAD_SESSION_MAX_PROMPTS, ...registeredSessionIds];
3101
+ const rows = db.prepare(
3102
+ `SELECT id FROM sessions
3103
+ WHERE prompt_count <= ?
3104
+ ${excludePlaceholders}`
3105
+ ).all(...params);
3106
+ return rows.map((r) => r.id);
3107
+ }
3108
+ async function runSessionMaintenance(deps) {
3109
+ const { logger, registeredSessionIds, embeddingManager, vaultDir } = deps;
3110
+ const registered = registeredSessionIds();
3111
+ const completed = completeStaleActiveSessions(registered);
3112
+ if (completed > 0) {
3113
+ logger.info(LOG_KINDS.MAINTENANCE_SESSION, "Completed stale sessions", { count: completed });
3114
+ }
3115
+ const deadIds = findDeadSessionIds(registered);
3116
+ if (deadIds.length === 0) return;
3117
+ let deletedCount = 0;
3118
+ for (const sessionId of deadIds) {
3119
+ const result = deleteSessionCascade(sessionId);
3120
+ if (!result.deleted) continue;
3121
+ await cleanupAfterSessionCascade(sessionId, result, embeddingManager, vaultDir);
3122
+ deletedCount++;
3123
+ logger.info(LOG_KINDS.MAINTENANCE_SESSION, "Deleted dead session", {
3124
+ session_id: sessionId,
3125
+ counts: result.counts
3126
+ });
3127
+ }
3128
+ if (deletedCount > 0) {
3129
+ logger.info(LOG_KINDS.MAINTENANCE_SESSION, "Dead session cleanup complete", { deleted: deletedCount });
3130
+ }
3131
+ }
3132
+
3133
+ // src/daemon/main.ts
3134
+ import fs5 from "fs";
3135
+ import path6 from "path";
3136
+ var AGENT_RUNS_DEFAULT_LIMIT = 50;
3137
+ var TOOL_INPUT_STORE_LIMIT = 4e3;
3138
+ var TOOL_OUTPUT_STORE_LIMIT = 2e3;
3139
+ var TITLE_PREVIEW_CHARS = 80;
3140
+ var SYSTEM_MESSAGE_PREFIXES = [
3141
+ "<task-notification>",
3142
+ "<system-reminder>"
3143
+ ];
3144
+ function isSystemMessage(prompt) {
3145
+ const trimmed = prompt.trimStart();
3146
+ return SYSTEM_MESSAGE_PREFIXES.some((prefix) => trimmed.startsWith(prefix));
3147
+ }
3148
+ var REPLAYABLE_EVENT_TYPES = /* @__PURE__ */ new Set(["user_prompt", "tool_use", "tool_failure"]);
3149
+ function handleUserPrompt(sessionId, prompt) {
3150
+ const now = epochSeconds();
3151
+ closeOpenBatches(sessionId, now);
3152
+ const batch = insertBatchStateless({
3153
+ session_id: sessionId,
3154
+ user_prompt: prompt ?? null,
3155
+ started_at: now,
3156
+ created_at: now
3157
+ });
3158
+ const promptNumber = batch.prompt_number;
3159
+ try {
3160
+ createBatchLineage(DEFAULT_AGENT_ID, sessionId, batch.id, now);
3161
+ } catch {
3162
+ }
3163
+ updateSession(sessionId, { prompt_count: promptNumber });
3164
+ return { batchId: batch.id, promptNumber };
3165
+ }
3166
+ function handleToolUse(sessionId, toolName, toolInput, toolOutput) {
3167
+ const now = epochSeconds();
3168
+ const inputObj = toolInput;
3169
+ const filePath = typeof inputObj?.file_path === "string" ? inputObj.file_path : null;
3170
+ const activity = insertActivityWithBatch({
3171
+ session_id: sessionId,
3172
+ tool_name: toolName,
3173
+ tool_input: toolInput ? JSON.stringify(toolInput).slice(0, TOOL_INPUT_STORE_LIMIT) : null,
3174
+ tool_output_summary: toolOutput?.slice(0, TOOL_OUTPUT_STORE_LIMIT) ?? null,
3175
+ file_path: filePath,
3176
+ timestamp: now,
3177
+ created_at: now
3178
+ });
3179
+ if (activity.prompt_batch_id !== null) {
3180
+ incrementActivityCount(activity.prompt_batch_id);
3181
+ }
3182
+ }
3183
+ function handleStopBatches(sessionId) {
3184
+ closeOpenBatches(sessionId, epochSeconds());
3185
+ }
3186
+ function handleToolFailure(sessionId, toolName, toolInput, error, isInterrupt) {
3187
+ const now = epochSeconds();
3188
+ const inputObj = toolInput;
3189
+ const filePath = typeof inputObj?.file_path === "string" ? inputObj.file_path : null;
3190
+ const activity = insertActivityWithBatch({
3191
+ session_id: sessionId,
3192
+ tool_name: toolName,
3193
+ tool_input: toolInput ? JSON.stringify(toolInput).slice(0, TOOL_INPUT_STORE_LIMIT) : null,
3194
+ tool_output_summary: error?.slice(0, TOOL_OUTPUT_STORE_LIMIT) ?? null,
3195
+ file_path: filePath,
3196
+ success: 0,
3197
+ error_message: error?.slice(0, TOOL_OUTPUT_STORE_LIMIT) ?? (isInterrupt ? "interrupted" : null),
3198
+ timestamp: now,
3199
+ created_at: now
3200
+ });
3201
+ if (activity.prompt_batch_id !== null) {
3202
+ incrementActivityCount(activity.prompt_batch_id);
3203
+ }
3204
+ }
3205
+ function handleSubagentStart(sessionId, agentId, agentType) {
3206
+ const now = epochSeconds();
3207
+ insertActivityWithBatch({
3208
+ session_id: sessionId,
3209
+ tool_name: "subagent_start",
3210
+ tool_input: JSON.stringify({ agent_id: agentId, agent_type: agentType }).slice(0, TOOL_INPUT_STORE_LIMIT),
3211
+ timestamp: now,
3212
+ created_at: now
3213
+ });
3214
+ }
3215
+ function handleSubagentStop(sessionId, agentId, agentType, lastAssistantMessage) {
3216
+ const now = epochSeconds();
3217
+ insertActivityWithBatch({
3218
+ session_id: sessionId,
3219
+ tool_name: "subagent_stop",
3220
+ tool_input: JSON.stringify({ agent_id: agentId, agent_type: agentType }).slice(0, TOOL_INPUT_STORE_LIMIT),
3221
+ tool_output_summary: lastAssistantMessage?.slice(0, TOOL_OUTPUT_STORE_LIMIT) ?? null,
3222
+ timestamp: now,
3223
+ created_at: now
3224
+ });
3225
+ }
3226
+ function handleStopFailure(sessionId, error, errorDetails) {
3227
+ const now = epochSeconds();
3228
+ insertActivityWithBatch({
3229
+ session_id: sessionId,
3230
+ tool_name: "stop_failure",
3231
+ tool_output_summary: errorDetails?.slice(0, TOOL_OUTPUT_STORE_LIMIT) ?? null,
3232
+ success: 0,
3233
+ error_message: error?.slice(0, TOOL_OUTPUT_STORE_LIMIT) ?? null,
3234
+ timestamp: now,
3235
+ created_at: now
3236
+ });
3237
+ }
3238
+ function handleTaskCompleted(sessionId, taskId, taskSubject, taskDescription) {
3239
+ const now = epochSeconds();
3240
+ insertActivityWithBatch({
3241
+ session_id: sessionId,
3242
+ tool_name: "task_completed",
3243
+ tool_input: JSON.stringify({ task_id: taskId, task_subject: taskSubject, task_description: taskDescription }).slice(0, TOOL_INPUT_STORE_LIMIT),
3244
+ tool_output_summary: taskSubject?.slice(0, TOOL_OUTPUT_STORE_LIMIT) ?? null,
3245
+ timestamp: now,
3246
+ created_at: now
3247
+ });
3248
+ }
3249
+ function handleCompact(sessionId, phase, trigger, compactSummary) {
3250
+ const now = epochSeconds();
3251
+ insertActivityWithBatch({
3252
+ session_id: sessionId,
3253
+ tool_name: `${phase}_compact`,
3254
+ tool_input: trigger ? JSON.stringify({ trigger }).slice(0, TOOL_INPUT_STORE_LIMIT) : null,
3255
+ tool_output_summary: compactSummary?.slice(0, TOOL_OUTPUT_STORE_LIMIT) ?? null,
3256
+ timestamp: now,
3257
+ created_at: now
3258
+ });
3259
+ }
3260
+ function killStaleDaemon(vaultDir, logger) {
3261
+ const daemonJsonPath = path6.join(vaultDir, "daemon.json");
3262
+ try {
3263
+ if (!fs5.existsSync(daemonJsonPath)) return;
3264
+ const info = JSON.parse(fs5.readFileSync(daemonJsonPath, "utf-8"));
3265
+ if (!info.pid) return;
3266
+ if (info.pid === process.pid) return;
3267
+ try {
3268
+ process.kill(info.pid, 0);
3269
+ process.kill(info.pid, "SIGTERM");
3270
+ logger.info(LOG_KINDS.DAEMON_START, "Killed stale daemon", { pid: info.pid });
3271
+ } catch {
3272
+ }
3273
+ fs5.unlinkSync(daemonJsonPath);
3274
+ } catch {
3275
+ }
3276
+ }
3277
+ async function main() {
3278
+ const vaultArg = process.argv.find((_, i) => process.argv[i - 1] === "--vault");
3279
+ if (!vaultArg) {
3280
+ process.stderr.write("Usage: mycod --vault <path>\n");
3281
+ process.exit(1);
3282
+ }
3283
+ const vaultDir = path6.resolve(vaultArg);
3284
+ loadSecrets(vaultDir);
3285
+ const config = loadConfig(vaultDir);
3286
+ const manifests = loadManifests();
3287
+ const symbiontPlanDirs = manifests.flatMap((m) => m.capture?.planDirs ?? []);
3288
+ const projectRoot = process.cwd();
3289
+ let planWatchConfig = {
3290
+ watchDirs: [.../* @__PURE__ */ new Set([...symbiontPlanDirs, ...config.capture.plan_dirs ?? []])],
3291
+ projectRoot,
3292
+ extensions: config.capture.artifact_extensions
3293
+ };
3294
+ const logger = new DaemonLogger(path6.join(vaultDir, "logs"), {
3295
+ level: config.daemon.log_level
3296
+ });
3297
+ killStaleDaemon(vaultDir, logger);
3298
+ logger.info(LOG_KINDS.DAEMON_CONFIG, "Config loaded", {
3299
+ vault: vaultDir,
3300
+ embedding_provider: config.embedding.provider
3301
+ });
3302
+ logger.info(LOG_KINDS.CAPTURE_PLAN, "Plan watch directories", { dirs: planWatchConfig.watchDirs });
3303
+ const db = initDatabase(vaultDbPath(vaultDir));
3304
+ createSchema(db);
3305
+ logger.info(LOG_KINDS.DAEMON_START, "SQLite initialized", { vault: vaultDir });
3306
+ logger.setPersistFn((entry) => {
3307
+ const { timestamp, level, kind, component, message, ...rest } = entry;
3308
+ insertLogEntry({
3309
+ timestamp,
3310
+ level,
3311
+ kind,
3312
+ component,
3313
+ message,
3314
+ data: Object.keys(rest).length > 0 ? JSON.stringify(rest) : null,
3315
+ session_id: rest.session_id ?? null
3316
+ });
3317
+ });
3318
+ const lastLogTimestamp = getMaxTimestamp();
3319
+ if (lastLogTimestamp) {
3320
+ const logDir = path6.join(vaultDir, "logs");
3321
+ const replayedCount = reconcileLogBuffer(logDir, lastLogTimestamp);
3322
+ if (replayedCount > 0) {
3323
+ logger.info(LOG_KINDS.DAEMON_RECONCILE, `Replayed ${replayedCount} log entries from buffer`, { replayed: replayedCount });
3324
+ }
3325
+ }
3326
+ const vectorsDbPath = path6.join(vaultDir, "vectors.db");
3327
+ const vectorStore = new SqliteVecVectorStore(vectorsDbPath);
3328
+ const llmProvider = createEmbeddingProvider(config.embedding);
3329
+ const embeddingProvider = new EmbeddingProviderAdapter(llmProvider, config.embedding);
3330
+ const recordSource = new SqliteRecordSource();
3331
+ const embeddingManager = new EmbeddingManager(vectorStore, embeddingProvider, recordSource, logger);
3332
+ logger.info(LOG_KINDS.EMBEDDING_EMBED, "EmbeddingManager initialized", { vectors_db: vectorsDbPath });
3333
+ try {
3334
+ const { registerBuiltInAgentsAndTasks, resolveDefinitionsDir: resolveDefinitionsDir2 } = await import("./loader-SH67XD54.js");
3335
+ const definitionsDir = resolveDefinitionsDir2();
3336
+ await registerBuiltInAgentsAndTasks(definitionsDir, vaultDir);
3337
+ logger.info(LOG_KINDS.AGENT_TASK, "Built-in agents and tasks registered");
3338
+ } catch (err) {
3339
+ logger.warn(LOG_KINDS.AGENT_ERROR, "Failed to register built-in agents/tasks", { error: err.message });
3340
+ }
3341
+ try {
3342
+ const staleDb = getDatabase();
3343
+ const staleRows = staleDb.prepare(
3344
+ `SELECT id FROM agent_runs WHERE status = 'running'`
3345
+ ).all();
3346
+ if (staleRows.length > 0) {
3347
+ staleDb.prepare(
3348
+ `UPDATE agent_runs SET status = 'failed', completed_at = ?, error = 'Daemon restarted while run was in progress' WHERE status = 'running'`
3349
+ ).run(epochSeconds());
3350
+ logger.info(LOG_KINDS.AGENT_RUN, "Cleaned stale running agent runs", {
3351
+ count: staleRows.length,
3352
+ ids: staleRows.map((r) => r.id)
3353
+ });
3354
+ }
3355
+ } catch (err) {
3356
+ logger.warn(LOG_KINDS.AGENT_ERROR, "Failed to clean stale runs", { error: err.message });
3357
+ }
3358
+ let uiDir = null;
3359
+ {
3360
+ const root = findPackageRoot(path6.dirname(new URL(import.meta.url).pathname));
3361
+ if (root) {
3362
+ const candidate = path6.join(root, "dist", "ui");
3363
+ if (fs5.existsSync(candidate)) uiDir = candidate;
3364
+ }
3365
+ }
3366
+ if (uiDir) {
3367
+ logger.debug(LOG_KINDS.DAEMON_START, "Static UI directory found", { path: uiDir });
3368
+ }
3369
+ const powerManager = new PowerManager({
3370
+ idleThresholdMs: POWER_IDLE_THRESHOLD_MS,
3371
+ sleepThresholdMs: POWER_SLEEP_THRESHOLD_MS,
3372
+ deepSleepThresholdMs: POWER_DEEP_SLEEP_THRESHOLD_MS,
3373
+ activeIntervalMs: POWER_ACTIVE_INTERVAL_MS,
3374
+ sleepIntervalMs: POWER_SLEEP_INTERVAL_MS,
3375
+ logger
3376
+ });
3377
+ const server = new DaemonServer({
3378
+ vaultDir,
3379
+ logger,
3380
+ uiDir: uiDir ?? void 0,
3381
+ onRequest: () => powerManager.recordActivity()
3382
+ });
3383
+ const registry = new SessionRegistry({
3384
+ gracePeriod: 0,
3385
+ onEmpty: () => {
3386
+ }
3387
+ });
3388
+ const transcriptMiner = new TranscriptMiner({
3389
+ additionalAdapters: config.capture.transcript_paths.map(
3390
+ (p) => createPerProjectAdapter(p, claudeCodeAdapter.parseTurns)
3391
+ )
3392
+ });
3393
+ let activeStopProcessing = null;
3394
+ const sessionTitleCache = /* @__PURE__ */ new Map();
3395
+ async function triggerTitleSummary(sessionId) {
3396
+ if (config.agent.summary_batch_interval <= 0) return;
3397
+ const running = getRunningRun(DEFAULT_AGENT_ID);
3398
+ if (running) return;
3399
+ try {
3400
+ const { runAgent } = await import("./executor-ONSDHPGX.js");
3401
+ runAgent(vaultDir, {
3402
+ task: "title-summary",
3403
+ instruction: `Process session ${sessionId} only`,
3404
+ embeddingManager
3405
+ }).catch((err) => logger.warn(LOG_KINDS.AGENT_ERROR, "Title-summary task failed", { error: String(err) }));
3406
+ } catch {
3407
+ }
3408
+ }
3409
+ const bufferDir = path6.join(vaultDir, "buffer");
3410
+ const sessionBuffers = /* @__PURE__ */ new Map();
3411
+ const startupCleanedCount = cleanStaleBuffers(bufferDir, STALE_BUFFER_MAX_AGE_MS);
3412
+ if (startupCleanedCount > 0) {
3413
+ logger.info(LOG_KINDS.CAPTURE_BUFFER, "Buffer cleanup complete", { stale_removed: startupCleanedCount });
3414
+ }
3415
+ const reconciledSessions = /* @__PURE__ */ new Set();
3416
+ for (const sessionId of listBufferSessionIds(bufferDir)) {
3417
+ try {
3418
+ reconcileSession(sessionId);
3419
+ } catch (err) {
3420
+ logger.warn(LOG_KINDS.LIFECYCLE_RECONCILE, "Startup reconciliation failed", { session_id: sessionId, error: String(err) });
3421
+ }
3422
+ }
3423
+ function replayEvent(sessionId, event) {
3424
+ if (event.type === "user_prompt") {
3425
+ if (isSystemMessage(String(event.prompt ?? ""))) return null;
3426
+ handleUserPrompt(sessionId, String(event.prompt ?? ""));
3427
+ return "prompt";
3428
+ }
3429
+ if (event.type === "tool_use") {
3430
+ handleToolUse(
3431
+ sessionId,
3432
+ String(event.tool_name ?? ""),
3433
+ event.tool_input,
3434
+ typeof event.output_preview === "string" ? event.output_preview : void 0
3435
+ );
3436
+ return "activity";
3437
+ }
3438
+ if (event.type === "tool_failure") {
3439
+ handleToolFailure(
3440
+ sessionId,
3441
+ String(event.tool_name ?? ""),
3442
+ event.tool_input,
3443
+ typeof event.error === "string" ? event.error : void 0,
3444
+ !!event.is_interrupt
3445
+ );
3446
+ return "activity";
3447
+ }
3448
+ return null;
3449
+ }
3450
+ function reconcileSession(sessionId) {
3451
+ if (reconciledSessions.has(sessionId)) return;
3452
+ reconciledSessions.add(sessionId);
3453
+ const bufferPath = path6.join(bufferDir, `${sessionId}.jsonl`);
3454
+ if (!fs5.existsSync(bufferPath)) return;
3455
+ const content = fs5.readFileSync(bufferPath, "utf-8").trim();
3456
+ if (!content) return;
3457
+ if (!getSession(sessionId)) {
3458
+ logger.debug(LOG_KINDS.LIFECYCLE_RECONCILE, "Skipping reconciliation for deleted session", { session_id: sessionId });
3459
+ return;
3460
+ }
3461
+ const allEvents = content.split("\n").map((line) => JSON.parse(line));
3462
+ const existingBatchCount = listBatchesBySession(sessionId).length;
3463
+ let promptsSeen = 0;
3464
+ let replayStartIndex = -1;
3465
+ for (let i = 0; i < allEvents.length; i++) {
3466
+ const e = allEvents[i];
3467
+ if (e.type === "user_prompt" && !isSystemMessage(String(e.prompt ?? ""))) {
3468
+ promptsSeen++;
3469
+ if (promptsSeen === existingBatchCount + 1) {
3470
+ replayStartIndex = i;
3471
+ break;
3472
+ }
3473
+ }
3474
+ }
3475
+ if (replayStartIndex === -1) return;
3476
+ const eventsToReplay = allEvents.slice(replayStartIndex).filter(
3477
+ (e) => REPLAYABLE_EVENT_TYPES.has(String(e.type))
3478
+ );
3479
+ let promptsRecovered = 0;
3480
+ let activitiesRecovered = 0;
3481
+ for (const event of eventsToReplay) {
3482
+ try {
3483
+ const result = replayEvent(sessionId, event);
3484
+ if (result === "prompt") promptsRecovered++;
3485
+ else if (result === "activity") activitiesRecovered++;
3486
+ } catch (err) {
3487
+ logger.warn(LOG_KINDS.LIFECYCLE_RECONCILE, "Reconciliation: failed to replay event", {
3488
+ type: String(event.type),
3489
+ error: String(err)
3490
+ });
3491
+ }
3492
+ }
3493
+ if (promptsRecovered > 0 || activitiesRecovered > 0) {
3494
+ logger.info(LOG_KINDS.LIFECYCLE_RECONCILE, "Buffer reconciliation complete", {
3495
+ session_id: sessionId,
3496
+ prompts_recovered: promptsRecovered,
3497
+ activities_recovered: activitiesRecovered
3498
+ });
3499
+ }
3500
+ }
3501
+ const RegisterBody = external_exports.object({
3502
+ session_id: external_exports.string(),
3503
+ branch: external_exports.string().optional(),
3504
+ started_at: external_exports.string().optional()
3505
+ });
3506
+ const UnregisterBody = external_exports.object({ session_id: external_exports.string() });
3507
+ const EventBody = external_exports.object({ type: external_exports.string(), session_id: external_exports.string() }).passthrough();
3508
+ const StopBody = external_exports.object({
3509
+ session_id: external_exports.string(),
3510
+ user: external_exports.string().optional(),
3511
+ transcript_path: external_exports.string().optional(),
3512
+ last_assistant_message: external_exports.string().optional()
3513
+ });
3514
+ server.registerRoute("POST", "/sessions/register", async (req) => {
3515
+ const { session_id, branch, started_at } = RegisterBody.parse(req.body);
3516
+ const resolvedStartedAt = started_at ?? (/* @__PURE__ */ new Date()).toISOString();
3517
+ registry.register(session_id, { started_at: resolvedStartedAt, branch });
3518
+ server.updateDaemonJsonSessions(registry.sessions);
3519
+ const now = epochSeconds();
3520
+ const startedEpoch = Math.floor(new Date(resolvedStartedAt).getTime() / 1e3);
3521
+ upsertSession({
3522
+ id: session_id,
3523
+ agent: "claude-code",
3524
+ user: null,
3525
+ project_root: process.cwd(),
3526
+ branch: branch ?? null,
3527
+ started_at: startedEpoch,
3528
+ created_at: now,
3529
+ status: "active"
3530
+ });
3531
+ updateSession(session_id, { ended_at: null, status: "active" });
3532
+ reconcileSession(session_id);
3533
+ logger.info(LOG_KINDS.LIFECYCLE_REGISTER, "Session registered", { session_id, branch, started_at: started_at ?? null });
3534
+ return { body: { ok: true, sessions: registry.sessions } };
3535
+ });
3536
+ server.registerRoute("POST", "/sessions/unregister", async (req) => {
3537
+ const { session_id } = UnregisterBody.parse(req.body);
3538
+ registry.unregister(session_id);
3539
+ cleanStaleBuffers(bufferDir, STALE_BUFFER_MAX_AGE_MS, session_id);
3540
+ closeSession(session_id, epochSeconds());
3541
+ sessionBuffers.delete(session_id);
3542
+ sessionTitleCache.delete(session_id);
3543
+ reconciledSessions.delete(session_id);
3544
+ server.updateDaemonJsonSessions(registry.sessions);
3545
+ logger.info(LOG_KINDS.LIFECYCLE_UNREGISTER, "Session unregistered", { session_id });
3546
+ return { body: { ok: true, sessions: registry.sessions } };
3547
+ });
3548
+ server.registerRoute("POST", "/events", async (req) => {
3549
+ const validated = EventBody.parse(req.body);
3550
+ const event = { ...validated, timestamp: validated.timestamp ?? (/* @__PURE__ */ new Date()).toISOString() };
3551
+ logger.debug(LOG_KINDS.HOOKS_EVENT, "Event received", { type: event.type, session_id: event.session_id });
3552
+ if (!registry.getSession(event.session_id)) {
3553
+ registry.register(event.session_id, { started_at: event.timestamp });
3554
+ logger.debug(LOG_KINDS.LIFECYCLE_AUTO_REGISTER, "Auto-registered session from event", { session_id: event.session_id });
3555
+ const now = epochSeconds();
3556
+ const startedEpoch = Math.floor(new Date(event.timestamp).getTime() / 1e3);
3557
+ upsertSession({
3558
+ id: event.session_id,
3559
+ agent: "claude-code",
3560
+ status: "active",
3561
+ started_at: startedEpoch,
3562
+ created_at: now
3563
+ });
3564
+ reconcileSession(event.session_id);
3565
+ }
3566
+ if (!sessionBuffers.has(event.session_id)) {
3567
+ sessionBuffers.set(event.session_id, new EventBuffer(bufferDir, event.session_id));
3568
+ }
3569
+ sessionBuffers.get(event.session_id).append(event);
3570
+ if (event.type === "user_prompt") {
3571
+ const promptText = String(event.prompt ?? "");
3572
+ if (isSystemMessage(promptText)) {
3573
+ logger.debug(LOG_KINDS.HOOKS_PROMPT, "Skipped system-injected message", {
3574
+ session_id: event.session_id,
3575
+ prefix: promptText.trimStart().slice(0, LOG_PROMPT_PREVIEW_CHARS)
3576
+ });
3577
+ } else {
3578
+ logger.info(LOG_KINDS.HOOKS_PROMPT, "User prompt received", {
3579
+ session_id: event.session_id,
3580
+ prompt_preview: promptText.slice(0, LOG_PROMPT_PREVIEW_CHARS),
3581
+ prompt_length: promptText.length
3582
+ });
3583
+ try {
3584
+ const { batchId, promptNumber } = handleUserPrompt(event.session_id, promptText || void 0);
3585
+ logger.debug(LOG_KINDS.CAPTURE_BATCH, "Batch opened", { session_id: event.session_id, batch_id: batchId, prompt_number: promptNumber });
3586
+ const batchCount = promptNumber;
3587
+ const summaryInterval = config.agent.summary_batch_interval;
3588
+ if (summaryInterval > 0 && batchCount > 0 && batchCount % summaryInterval === 0) {
3589
+ triggerTitleSummary(event.session_id);
3590
+ }
3591
+ } catch (err) {
3592
+ logger.warn(LOG_KINDS.CAPTURE_BATCH, "Failed to open batch", { session_id: event.session_id, error: err.message });
3593
+ }
3594
+ }
3595
+ }
3596
+ if (event.type === "tool_use") {
3597
+ const toolName = String(event.tool_name ?? "");
3598
+ logger.debug(LOG_KINDS.HOOKS_TOOL, "Tool use event", {
3599
+ session_id: event.session_id,
3600
+ tool_name: toolName
3601
+ });
3602
+ const planFilePath = isPlanWriteEvent(
3603
+ toolName,
3604
+ event.tool_input,
3605
+ planWatchConfig
3606
+ );
3607
+ if (planFilePath) {
3608
+ const captureSessionId = event.session_id;
3609
+ fs5.promises.readFile(planFilePath, "utf-8").then((planContent) => {
3610
+ const latestBatch = getLatestBatch(captureSessionId);
3611
+ capturePlan({
3612
+ sourcePath: path6.relative(projectRoot, planFilePath),
3613
+ content: planContent,
3614
+ sessionId: captureSessionId,
3615
+ promptBatchId: latestBatch?.id ?? null
3616
+ });
3617
+ logger.info(LOG_KINDS.CAPTURE_PLAN, "Plan captured", {
3618
+ session_id: captureSessionId,
3619
+ source_path: planFilePath
3620
+ });
3621
+ }).catch((err) => {
3622
+ logger.warn(LOG_KINDS.CAPTURE_PLAN, "Failed to capture plan", {
3623
+ error: err.message,
3624
+ path: planFilePath
3625
+ });
3626
+ });
3627
+ }
3628
+ try {
3629
+ handleToolUse(
3630
+ event.session_id,
3631
+ toolName,
3632
+ event.tool_input,
3633
+ typeof event.output_preview === "string" ? event.output_preview : void 0
3634
+ );
3635
+ } catch (err) {
3636
+ logger.warn(LOG_KINDS.CAPTURE_ACTIVITY, "Failed to record activity", { session_id: event.session_id, error: err.message });
3637
+ }
3638
+ }
3639
+ if (event.type === "tool_failure") {
3640
+ const toolName = String(event.tool_name ?? "");
3641
+ logger.info(LOG_KINDS.HOOKS_TOOL, "Tool failure event", {
3642
+ session_id: event.session_id,
3643
+ tool_name: toolName,
3644
+ is_interrupt: !!event.is_interrupt
3645
+ });
3646
+ try {
3647
+ handleToolFailure(
3648
+ event.session_id,
3649
+ toolName,
3650
+ event.tool_input,
3651
+ typeof event.error === "string" ? event.error : void 0,
3652
+ !!event.is_interrupt
3653
+ );
3654
+ } catch (err) {
3655
+ logger.warn(LOG_KINDS.CAPTURE_ACTIVITY, "Failed to record tool failure", { session_id: event.session_id, error: err.message });
3656
+ }
3657
+ }
3658
+ if (event.type === "subagent_start") {
3659
+ logger.info(LOG_KINDS.HOOKS_SUBAGENT, "Subagent start event", {
3660
+ session_id: event.session_id,
3661
+ agent_id: event.agent_id,
3662
+ agent_type: event.agent_type
3663
+ });
3664
+ try {
3665
+ handleSubagentStart(
3666
+ event.session_id,
3667
+ typeof event.agent_id === "string" ? event.agent_id : void 0,
3668
+ typeof event.agent_type === "string" ? event.agent_type : void 0
3669
+ );
3670
+ } catch (err) {
3671
+ logger.warn(LOG_KINDS.CAPTURE_ACTIVITY, "Failed to record subagent start", { session_id: event.session_id, error: err.message });
3672
+ }
3673
+ }
3674
+ if (event.type === "subagent_stop") {
3675
+ logger.info(LOG_KINDS.HOOKS_SUBAGENT, "Subagent stop event", {
3676
+ session_id: event.session_id,
3677
+ agent_id: event.agent_id,
3678
+ agent_type: event.agent_type
3679
+ });
3680
+ try {
3681
+ handleSubagentStop(
3682
+ event.session_id,
3683
+ typeof event.agent_id === "string" ? event.agent_id : void 0,
3684
+ typeof event.agent_type === "string" ? event.agent_type : void 0,
3685
+ typeof event.last_assistant_message === "string" ? event.last_assistant_message : void 0
3686
+ );
3687
+ } catch (err) {
3688
+ logger.warn(LOG_KINDS.CAPTURE_ACTIVITY, "Failed to record subagent stop", { session_id: event.session_id, error: err.message });
3689
+ }
3690
+ }
3691
+ if (event.type === "stop_failure") {
3692
+ logger.warn(LOG_KINDS.HOOKS_STOP, "Stop failure event", {
3693
+ session_id: event.session_id,
3694
+ error: event.error
3695
+ });
3696
+ try {
3697
+ handleStopFailure(
3698
+ event.session_id,
3699
+ typeof event.error === "string" ? event.error : void 0,
3700
+ typeof event.error_details === "string" ? event.error_details : void 0
3701
+ );
3702
+ } catch (err) {
3703
+ logger.warn(LOG_KINDS.CAPTURE_ACTIVITY, "Failed to record stop failure", { session_id: event.session_id, error: err.message });
3704
+ }
3705
+ }
3706
+ if (event.type === "task_completed") {
3707
+ logger.info(LOG_KINDS.HOOKS_EVENT, "Task completed event", {
3708
+ session_id: event.session_id,
3709
+ task_id: event.task_id,
3710
+ task_subject: event.task_subject
3711
+ });
3712
+ try {
3713
+ handleTaskCompleted(
3714
+ event.session_id,
3715
+ typeof event.task_id === "string" ? event.task_id : void 0,
3716
+ typeof event.task_subject === "string" ? event.task_subject : void 0,
3717
+ typeof event.task_description === "string" ? event.task_description : void 0
3718
+ );
3719
+ } catch (err) {
3720
+ logger.warn(LOG_KINDS.CAPTURE_ACTIVITY, "Failed to record task completion", { session_id: event.session_id, error: err.message });
3721
+ }
3722
+ }
3723
+ if (event.type === "pre_compact") {
3724
+ logger.info(LOG_KINDS.HOOKS_EVENT, "Pre-compact event", { session_id: event.session_id });
3725
+ try {
3726
+ handleCompact(
3727
+ event.session_id,
3728
+ "pre",
3729
+ typeof event.trigger === "string" ? event.trigger : void 0,
3730
+ void 0
3731
+ );
3732
+ } catch (err) {
3733
+ logger.warn(LOG_KINDS.CAPTURE_ACTIVITY, "Failed to record pre-compact", { session_id: event.session_id, error: err.message });
3734
+ }
3735
+ }
3736
+ if (event.type === "post_compact") {
3737
+ logger.info(LOG_KINDS.HOOKS_EVENT, "Post-compact event", { session_id: event.session_id });
3738
+ try {
3739
+ handleCompact(
3740
+ event.session_id,
3741
+ "post",
3742
+ typeof event.trigger === "string" ? event.trigger : void 0,
3743
+ typeof event.compact_summary === "string" ? event.compact_summary : void 0
3744
+ );
3745
+ } catch (err) {
3746
+ logger.warn(LOG_KINDS.CAPTURE_ACTIVITY, "Failed to record post-compact", { session_id: event.session_id, error: err.message });
3747
+ }
3748
+ }
3749
+ return { body: { ok: true } };
3750
+ });
3751
+ server.registerRoute("POST", "/events/stop", async (req) => {
3752
+ const { session_id: sessionId, user, transcript_path: hookTranscriptPath, last_assistant_message: lastAssistantMessage } = StopBody.parse(req.body);
3753
+ if (!registry.getSession(sessionId)) {
3754
+ registry.register(sessionId, { started_at: (/* @__PURE__ */ new Date()).toISOString() });
3755
+ logger.debug(LOG_KINDS.LIFECYCLE_AUTO_REGISTER, "Auto-registered session from stop event", { session_id: sessionId });
3756
+ }
3757
+ const sessionMeta = registry.getSession(sessionId);
3758
+ logger.info(LOG_KINDS.HOOKS_STOP, "Stop received", {
3759
+ session_id: sessionId,
3760
+ has_transcript_path: !!hookTranscriptPath,
3761
+ has_response: !!lastAssistantMessage
3762
+ });
3763
+ logger.debug(LOG_KINDS.HOOKS_STOP, "Stop event detail", {
3764
+ session_id: sessionId,
3765
+ transcript_path: hookTranscriptPath ?? null,
3766
+ last_message_preview: lastAssistantMessage?.slice(0, LOG_MESSAGE_PREVIEW_CHARS) ?? null
3767
+ });
3768
+ const run = () => processStopEvent(sessionId, user, sessionMeta, hookTranscriptPath, lastAssistantMessage).catch((err) => {
3769
+ logger.error(LOG_KINDS.PROCESSOR_SESSION, "Stop processing failed", { session_id: sessionId, error: err.message });
3770
+ });
3771
+ const prev = activeStopProcessing ?? Promise.resolve();
3772
+ activeStopProcessing = prev.then(run).finally(() => {
3773
+ activeStopProcessing = null;
3774
+ });
3775
+ return { body: { ok: true } };
3776
+ });
3777
+ function enrichTurnsWithToolMetadata(turns, events) {
3778
+ if (events.length === 0 || turns.length === 0) return;
3779
+ const toolEvents = events.filter((e) => e.type === "tool_use");
3780
+ if (toolEvents.length === 0) return;
3781
+ let cursor = 0;
3782
+ for (let i = 0; i < turns.length; i++) {
3783
+ const turnEnd = i + 1 < turns.length ? turns[i + 1].timestamp : null;
3784
+ const breakdown = {};
3785
+ const files = /* @__PURE__ */ new Set();
3786
+ while (cursor < toolEvents.length) {
3787
+ const ts = String(toolEvents[cursor].timestamp ?? "");
3788
+ if (turnEnd !== null && ts >= turnEnd) break;
3789
+ const evt = toolEvents[cursor];
3790
+ const toolName = String(evt.tool_name ?? evt.tool ?? "unknown");
3791
+ breakdown[toolName] = (breakdown[toolName] ?? 0) + 1;
3792
+ const input = evt.tool_input;
3793
+ const filePath = input?.file_path ?? input?.path;
3794
+ if (typeof filePath === "string") files.add(filePath);
3795
+ cursor++;
3796
+ }
3797
+ if (Object.keys(breakdown).length > 0) {
3798
+ turns[i].toolBreakdown = breakdown;
3799
+ if (files.size > 0) turns[i].files = [...files];
3800
+ }
3801
+ }
3802
+ }
3803
+ async function processStopEvent(sessionId, user, sessionMeta, hookTranscriptPath, lastAssistantMessage) {
3804
+ const transcriptResult = transcriptMiner.getAllTurnsWithSource(sessionId, hookTranscriptPath);
3805
+ let allTurns = transcriptResult.turns;
3806
+ let turnSource = transcriptResult.source;
3807
+ const bufferEvents = sessionBuffers.get(sessionId)?.readAll() ?? [];
3808
+ if (allTurns.length === 0) {
3809
+ allTurns = extractTurnsFromBuffer(bufferEvents);
3810
+ turnSource = "buffer";
3811
+ } else if (bufferEvents.length > 0) {
3812
+ const lastTranscriptTs = allTurns[allTurns.length - 1].timestamp;
3813
+ if (lastTranscriptTs) {
3814
+ const newerEvents = bufferEvents.filter(
3815
+ (e) => String(e.timestamp ?? "") > lastTranscriptTs
3816
+ );
3817
+ if (newerEvents.length > 0) {
3818
+ const bufferTurns = extractTurnsFromBuffer(newerEvents);
3819
+ allTurns = [...allTurns, ...bufferTurns];
3820
+ turnSource = `${transcriptResult.source}+buffer`;
3821
+ logger.info(LOG_KINDS.PROCESSOR_TRANSCRIPT, "Appended buffer turns missing from transcript", {
3822
+ session_id: sessionId,
3823
+ transcriptTurns: transcriptResult.turns.length,
3824
+ bufferTurns: bufferTurns.length
3825
+ });
3826
+ }
3827
+ }
3828
+ }
3829
+ if (lastAssistantMessage && allTurns.length > 0) {
3830
+ const lastTurn = allTurns[allTurns.length - 1];
3831
+ if (!lastTurn.aiResponse) {
3832
+ lastTurn.aiResponse = lastAssistantMessage;
3833
+ }
3834
+ }
3835
+ enrichTurnsWithToolMetadata(allTurns, bufferEvents);
3836
+ const imageCount = allTurns.reduce((sum, t) => sum + (t.images?.length ?? 0), 0);
3837
+ logger.debug(LOG_KINDS.PROCESSOR_TRANSCRIPT, "Transcript parsed", {
3838
+ session_id: sessionId,
3839
+ turn_count: allTurns.length,
3840
+ image_count: imageCount
3841
+ });
3842
+ const latestBatch = getLatestBatch(sessionId);
3843
+ if (lastAssistantMessage && latestBatch && !latestBatch.response_summary) {
3844
+ try {
3845
+ setResponseSummary(latestBatch.id, lastAssistantMessage);
3846
+ } catch (err) {
3847
+ logger.warn(LOG_KINDS.PROCESSOR_BATCH, "Failed to set response_summary on latest batch", { error: String(err) });
3848
+ }
3849
+ }
3850
+ closeOpenBatches(sessionId, epochSeconds());
3851
+ const existingSession = getSession(sessionId);
3852
+ const hasTitle = existingSession?.title !== null && existingSession?.title !== void 0;
3853
+ if (!hasTitle) {
3854
+ let title = sessionTitleCache.get(sessionId) ?? null;
3855
+ if (!title) {
3856
+ const firstBatch = listBatchesBySession(sessionId, { limit: 1 })[0];
3857
+ if (firstBatch?.user_prompt) {
3858
+ title = firstBatch.user_prompt.slice(0, TITLE_PREVIEW_CHARS);
3859
+ if (firstBatch.user_prompt.length > TITLE_PREVIEW_CHARS) {
3860
+ title += "...";
3861
+ }
3862
+ sessionTitleCache.set(sessionId, title);
3863
+ }
3864
+ }
3865
+ }
3866
+ const updateFields = {
3867
+ transcript_path: hookTranscriptPath ?? null,
3868
+ prompt_count: allTurns.length,
3869
+ tool_count: allTurns.reduce((sum, t) => sum + t.toolCount, 0)
3870
+ };
3871
+ if (user) updateFields.user = user;
3872
+ if (!hasTitle && sessionTitleCache.has(sessionId)) {
3873
+ updateFields.title = sessionTitleCache.get(sessionId);
3874
+ }
3875
+ updateSession(sessionId, updateFields);
3876
+ const responses = [];
3877
+ for (let i = 0; i < allTurns.length; i++) {
3878
+ if (allTurns[i].aiResponse) {
3879
+ responses.push({ turnIndex: i + 1, response: allTurns[i].aiResponse });
3880
+ }
3881
+ }
3882
+ if (responses.length > 0) {
3883
+ try {
3884
+ populateBatchResponses(sessionId, responses);
3885
+ } catch (err) {
3886
+ logger.warn(LOG_KINDS.PROCESSOR_BATCH, "Failed to populate batch responses", { error: String(err) });
3887
+ }
3888
+ }
3889
+ if (!hasTitle) {
3890
+ triggerTitleSummary(sessionId);
3891
+ }
3892
+ const sessionShort = sessionId.slice(-6);
3893
+ for (let i = 0; i < allTurns.length; i++) {
3894
+ const turn = allTurns[i];
3895
+ if (!turn.images?.length) continue;
3896
+ const isLastTurn = i === allTurns.length - 1;
3897
+ let resolvedBatchId = null;
3898
+ let resolvedPromptNumber = i + 1;
3899
+ if (isLastTurn && latestBatch) {
3900
+ resolvedBatchId = latestBatch.id;
3901
+ resolvedPromptNumber = latestBatch.prompt_number ?? resolvedPromptNumber;
3902
+ } else if (turn.prompt) {
3903
+ try {
3904
+ const match = findBatchByPromptPrefix(sessionId, turn.prompt);
3905
+ if (match) {
3906
+ resolvedBatchId = match.id;
3907
+ resolvedPromptNumber = match.prompt_number;
3908
+ }
3909
+ } catch {
3910
+ }
3911
+ }
3912
+ for (let j = 0; j < turn.images.length; j++) {
3913
+ const img = turn.images[j];
3914
+ const ext = extensionForMimeType(img.mediaType);
3915
+ const filename = `${sessionShort}-t${resolvedPromptNumber}-${j + 1}.${ext}`;
3916
+ const imageBuffer = Buffer.from(img.data, "base64");
3917
+ try {
3918
+ insertAttachment({
3919
+ id: `${sessionShort}-b${resolvedPromptNumber}-${j + 1}`,
3920
+ session_id: sessionId,
3921
+ prompt_batch_id: resolvedBatchId ?? void 0,
3922
+ file_path: filename,
3923
+ media_type: img.mediaType,
3924
+ data: imageBuffer,
3925
+ created_at: epochSeconds()
3926
+ });
3927
+ logger.debug(LOG_KINDS.CAPTURE_ATTACHMENT, "Image stored in DB", { filename, batch: resolvedPromptNumber });
3928
+ } catch (err) {
3929
+ logger.warn(LOG_KINDS.CAPTURE_ATTACHMENT, "Failed to record attachment", { error: String(err) });
3930
+ }
3931
+ }
3932
+ }
3933
+ logger.info(LOG_KINDS.PROCESSOR_SESSION, "Session captured", {
3934
+ session_id: sessionId,
3935
+ turns: allTurns.length,
3936
+ source: turnSource,
3937
+ title: existingSession?.title ?? sessionTitleCache.get(sessionId) ?? "(untitled)"
3938
+ });
3939
+ }
3940
+ const contextDeps = { embeddingManager, config, logger };
3941
+ server.registerRoute("POST", "/context", createSessionContextHandler(contextDeps));
3942
+ server.registerRoute("POST", "/context/prompt", createPromptContextHandler(contextDeps));
3943
+ const progressTracker = new ProgressTracker();
3944
+ let configHash = computeConfigHash(vaultDir);
3945
+ server.registerRoute("GET", "/api/config", async () => handleGetConfig(vaultDir));
3946
+ server.registerRoute("GET", "/api/symbionts", handleListSymbionts);
3947
+ server.registerRoute("PUT", "/api/config", async (req) => {
3948
+ const result = await handlePutConfig(vaultDir, req.body);
3949
+ if (!result.status || result.status < 400) {
3950
+ configHash = computeConfigHash(vaultDir);
3951
+ }
3952
+ return result;
3953
+ });
3954
+ const symbiontPlanDirsByAgent = {};
3955
+ for (const m of manifests) {
3956
+ const dirs = m.capture?.planDirs ?? [];
3957
+ if (dirs.length > 0) symbiontPlanDirsByAgent[m.displayName] = dirs;
3958
+ }
3959
+ server.registerRoute("GET", "/api/config/plan-dirs", async () => {
3960
+ return { body: { symbiont: symbiontPlanDirsByAgent, custom: planWatchConfig.watchDirs.filter((d) => !symbiontPlanDirs.includes(d)) } };
3961
+ });
3962
+ server.registerRoute("POST", "/api/config/plan-dirs", async (req) => {
3963
+ const body = req.body;
3964
+ if (!Array.isArray(body.plan_dirs)) {
3965
+ return { status: 400, body: { error: "plan_dirs must be an array" } };
3966
+ }
3967
+ const updated = updateConfig(vaultDir, (cfg) => ({
3968
+ ...cfg,
3969
+ capture: { ...cfg.capture, plan_dirs: body.plan_dirs }
3970
+ }));
3971
+ planWatchConfig = { ...planWatchConfig, watchDirs: [.../* @__PURE__ */ new Set([...symbiontPlanDirs, ...body.plan_dirs])] };
3972
+ return { body: { custom: updated.capture.plan_dirs } };
3973
+ });
3974
+ server.registerRoute("GET", "/api/stats", async () => {
3975
+ const stats = gatherStats(vaultDir, { active_sessions: registry.sessions });
3976
+ stats.daemon.pid = process.pid;
3977
+ stats.daemon.port = server.port;
3978
+ stats.daemon.version = server.version;
3979
+ stats.daemon.uptime_seconds = Math.floor(process.uptime());
3980
+ return { body: { ...stats, config_hash: configHash } };
3981
+ });
3982
+ server.registerRoute("GET", "/api/logs/search", handleLogSearch);
3983
+ server.registerRoute("GET", "/api/logs/stream", handleLogStream);
3984
+ server.registerRoute("GET", "/api/logs/:id", handleLogDetail);
3985
+ const ExternalLogBody = external_exports.object({
3986
+ level: external_exports.enum(["debug", "info", "warn", "error"]),
3987
+ component: external_exports.string(),
3988
+ message: external_exports.string(),
3989
+ data: external_exports.record(external_exports.string(), external_exports.unknown()).optional()
3990
+ });
3991
+ server.registerRoute("POST", "/api/log", async (req) => {
3992
+ const { level, component, message, data } = ExternalLogBody.parse(req.body);
3993
+ logger.log(level, LOG_KINDS.MCP_EVENT, message, { ...data, mcp_component: component });
3994
+ return { body: { ok: true } };
3995
+ });
3996
+ server.registerRoute("GET", "/api/models", async (req) => handleGetModels(req));
3997
+ server.registerRoute("POST", "/api/restart", async (req) => handleRestart({ vaultDir, progressTracker }, req.body));
3998
+ server.registerRoute("GET", "/api/progress/:token", async (req) => handleGetProgress(progressTracker, req.params.token));
3999
+ server.registerRoute("GET", "/api/sessions", handleListSessions);
4000
+ server.registerRoute("GET", "/api/sessions/:id", handleGetSession);
4001
+ server.registerRoute("GET", "/api/sessions/:id/impact", async (req) => {
4002
+ const sessionId = req.params.id;
4003
+ const session = getSession(sessionId);
4004
+ if (!session) return { status: 404, body: { error: "Session not found" } };
4005
+ const impact = getSessionImpact(sessionId);
4006
+ return { body: impact };
4007
+ });
4008
+ server.registerRoute("DELETE", "/api/sessions/:id", async (req) => {
4009
+ const sessionId = req.params.id;
4010
+ const result = deleteSessionCascade(sessionId);
4011
+ if (!result.deleted) return { status: 404, body: { error: "Session not found" } };
4012
+ cleanupAfterSessionCascade(sessionId, result, embeddingManager, vaultDir).catch(() => {
4013
+ });
4014
+ logger.info(LOG_KINDS.API_SESSION_DELETE, "Session cascade deleted", {
4015
+ session_id: sessionId,
4016
+ counts: result.counts
4017
+ });
4018
+ return { body: { ok: true, counts: result.counts } };
4019
+ });
4020
+ server.registerRoute("GET", "/api/sessions/:id/batches", handleGetSessionBatches);
4021
+ server.registerRoute("GET", "/api/batches/:id/activities", handleGetBatchActivities);
4022
+ server.registerRoute("GET", "/api/sessions/:id/attachments", handleGetSessionAttachments);
4023
+ server.registerRoute("GET", "/api/sessions/:id/plans", handleGetSessionPlans);
4024
+ server.registerRoute("GET", "/api/spores", handleListSpores);
4025
+ server.registerRoute("GET", "/api/spores/:id", handleGetSpore);
4026
+ server.registerRoute("GET", "/api/entities", handleListEntities);
4027
+ server.registerRoute("GET", "/api/graph/:id", handleGetGraph);
4028
+ server.registerRoute("GET", "/api/digest", handleGetDigest);
4029
+ const ATTACHMENT_MEDIA_TYPES = {
4030
+ png: "image/png",
4031
+ jpg: "image/jpeg",
4032
+ jpeg: "image/jpeg",
4033
+ gif: "image/gif",
4034
+ webp: "image/webp"
4035
+ };
4036
+ server.registerRoute("GET", "/api/attachments/:filename", async (req) => {
4037
+ const filename = req.params.filename;
4038
+ if (filename.includes("..") || filename.includes("/")) {
4039
+ return { status: 400, body: { error: "invalid_filename" } };
4040
+ }
4041
+ const att = getAttachmentByFilePath(filename);
4042
+ if (att?.data) {
4043
+ const contentType2 = att.media_type ?? "application/octet-stream";
4044
+ return { status: 200, headers: { "Content-Type": contentType2 }, body: att.data };
4045
+ }
4046
+ const filePath = path6.join(vaultDir, "attachments", filename);
4047
+ let diskData;
4048
+ try {
4049
+ diskData = fs5.readFileSync(filePath);
4050
+ } catch {
4051
+ return { status: 404, body: { error: "not_found" } };
4052
+ }
4053
+ const ext = path6.extname(filename).slice(1).toLowerCase();
4054
+ const contentType = ATTACHMENT_MEDIA_TYPES[ext] ?? "application/octet-stream";
4055
+ return { status: 200, headers: { "Content-Type": contentType }, body: diskData };
4056
+ });
4057
+ const AgentRunBody = external_exports.object({
4058
+ task: external_exports.string().optional(),
4059
+ instruction: external_exports.string().optional(),
4060
+ agentId: external_exports.string().optional()
4061
+ });
4062
+ server.registerRoute("POST", "/api/agent/run", async (req) => {
4063
+ const { task, instruction, agentId } = AgentRunBody.parse(req.body);
4064
+ const { runAgent } = await import("./executor-ONSDHPGX.js");
4065
+ const resultPromise = runAgent(vaultDir, { task, instruction, agentId, embeddingManager });
4066
+ const effectiveAgentId = agentId ?? "myco-agent";
4067
+ const latestRun = getRunningRun(effectiveAgentId);
4068
+ const runId = latestRun?.id;
4069
+ resultPromise.then((result) => {
4070
+ if (result.status === "failed") {
4071
+ logger.error(LOG_KINDS.AGENT_ERROR, "Agent run failed", {
4072
+ runId: result.runId,
4073
+ error: result.error ?? "No error message",
4074
+ phases: result.phases?.map((p) => `${p.name}:${p.status}`) ?? []
4075
+ });
4076
+ } else {
4077
+ logger.info(LOG_KINDS.AGENT_RUN, "Agent run completed", {
4078
+ runId: result.runId,
4079
+ status: result.status,
4080
+ phases: result.phases?.map((p) => `${p.name}:${p.status}`) ?? []
4081
+ });
4082
+ }
4083
+ }).catch((err) => {
4084
+ logger.error(LOG_KINDS.AGENT_ERROR, "Agent run threw unhandled error", {
4085
+ error: err.message ?? String(err),
4086
+ stack: err.stack?.split("\n").slice(0, 3).join(" | ")
4087
+ });
4088
+ });
4089
+ return { body: { ok: true, message: "Agent started", runId } };
4090
+ });
4091
+ server.registerRoute("GET", "/api/agent/runs", async (req) => {
4092
+ const limit = req.query.limit ? Number(req.query.limit) : AGENT_RUNS_DEFAULT_LIMIT;
4093
+ const offset = req.query.offset ? Number(req.query.offset) : 0;
4094
+ const agentId = req.query.agentId || void 0;
4095
+ const status = req.query.status || void 0;
4096
+ const task = req.query.task || void 0;
4097
+ const search = req.query.search || void 0;
4098
+ const filterOpts = { agent_id: agentId, status, task, search };
4099
+ const runs = listRuns({ ...filterOpts, limit, offset });
4100
+ const total = countRuns(filterOpts);
4101
+ return { body: { runs, total, offset, limit } };
4102
+ });
4103
+ server.registerRoute("GET", "/api/agent/runs/:id", async (req) => {
4104
+ const run = getRun(req.params.id);
4105
+ if (!run) {
4106
+ return { status: 404, body: { error: "Run not found" } };
4107
+ }
4108
+ return { body: { run } };
4109
+ });
4110
+ server.registerRoute("GET", "/api/agent/runs/:id/reports", async (req) => {
4111
+ const reports = listReports(req.params.id);
4112
+ return { body: { reports } };
4113
+ });
4114
+ server.registerRoute("GET", "/api/agent/runs/:id/turns", async (req) => {
4115
+ const turns = listTurnsByRun(req.params.id);
4116
+ return { body: turns };
4117
+ });
4118
+ server.registerRoute("GET", "/api/agent/tasks", async (req) => handleListTasks(req, vaultDir));
4119
+ server.registerRoute("GET", "/api/agent/tasks/:id", async (req) => handleGetTask(req, vaultDir));
4120
+ server.registerRoute("GET", "/api/agent/tasks/:id/yaml", async (req) => handleGetTaskYaml(req, vaultDir));
4121
+ server.registerRoute("PUT", "/api/agent/tasks/:id", async (req) => handleUpdateTask(req, vaultDir));
4122
+ server.registerRoute("POST", "/api/agent/tasks", async (req) => handleCreateTask(req, vaultDir));
4123
+ server.registerRoute("POST", "/api/agent/tasks/:id/copy", async (req) => handleCopyTask(req, vaultDir));
4124
+ server.registerRoute("DELETE", "/api/agent/tasks/:id", async (req) => handleDeleteTask(req, vaultDir));
4125
+ server.registerRoute("GET", "/api/agent/tasks/:id/config", async (req) => handleGetTaskConfig(req, vaultDir));
4126
+ server.registerRoute("PUT", "/api/agent/tasks/:id/config", async (req) => handleUpdateTaskConfig(req, vaultDir));
4127
+ server.registerRoute("GET", "/api/providers", async () => handleGetProviders());
4128
+ server.registerRoute("POST", "/api/providers/test", async (req) => handleTestProvider(req));
4129
+ const SPORE_ID_RANDOM_BYTES = 4;
4130
+ const RESOLUTION_ID_RANDOM_BYTES = 8;
4131
+ const RememberBody = external_exports.object({
4132
+ content: external_exports.string(),
4133
+ type: external_exports.string().optional(),
4134
+ tags: external_exports.array(external_exports.string()).optional()
4135
+ });
4136
+ server.registerRoute("POST", "/api/mcp/remember", async (req) => {
4137
+ const { content, type, tags } = RememberBody.parse(req.body);
4138
+ const { randomBytes } = await import("crypto");
4139
+ const observationType = type ?? "discovery";
4140
+ const id = `${observationType}-${randomBytes(SPORE_ID_RANDOM_BYTES).toString("hex")}`;
4141
+ const now = epochSeconds();
4142
+ registerAgent({
4143
+ id: USER_AGENT_ID,
4144
+ name: USER_AGENT_NAME,
4145
+ created_at: now
4146
+ });
4147
+ const spore = insertSpore({
4148
+ id,
4149
+ agent_id: USER_AGENT_ID,
4150
+ observation_type: observationType,
4151
+ content,
4152
+ tags: tags ? tags.join(", ") : null,
4153
+ created_at: now
4154
+ });
4155
+ embeddingManager.onContentWritten("spores", spore.id, content, {
4156
+ status: "active",
4157
+ observation_type: observationType
4158
+ }).catch(() => {
4159
+ });
4160
+ return {
4161
+ body: {
4162
+ id: spore.id,
4163
+ observation_type: spore.observation_type,
4164
+ status: spore.status,
4165
+ created_at: spore.created_at
4166
+ }
4167
+ };
4168
+ });
4169
+ server.registerRoute("GET", "/api/mcp/plans", async (req) => {
4170
+ const statusFilter = req.query.status === "all" ? void 0 : req.query.status;
4171
+ const limit = req.query.limit ? Number(req.query.limit) : void 0;
4172
+ const rows = listPlans({ status: statusFilter, limit });
4173
+ const plans = rows.map((row) => {
4174
+ const content = row.content ?? "";
4175
+ const checked = (content.match(/- \[x\]/gi) ?? []).length;
4176
+ const unchecked = (content.match(/- \[ \]/g) ?? []).length;
4177
+ const total = checked + unchecked;
4178
+ const progress = total === 0 ? "N/A" : `${checked}/${total}`;
4179
+ return {
4180
+ id: row.id,
4181
+ title: row.title,
4182
+ status: row.status,
4183
+ progress,
4184
+ tags: row.tags ? row.tags.split(",").map((t) => t.trim()) : [],
4185
+ created_at: row.created_at
4186
+ };
4187
+ });
4188
+ return { body: { plans } };
4189
+ });
4190
+ server.registerRoute("GET", "/api/mcp/sessions", async (req) => {
4191
+ const limit = req.query.limit ? Number(req.query.limit) : 20;
4192
+ const status = req.query.status;
4193
+ const rows = listSessions({ limit, status });
4194
+ const sessions = rows.map((row) => ({
4195
+ id: row.id,
4196
+ agent: row.agent,
4197
+ user: row.user,
4198
+ branch: row.branch,
4199
+ started_at: row.started_at,
4200
+ ended_at: row.ended_at,
4201
+ status: row.status,
4202
+ title: row.title,
4203
+ summary: (row.summary ?? "").slice(0, 300),
4204
+ prompt_count: row.prompt_count,
4205
+ tool_count: row.tool_count,
4206
+ parent_session_id: row.parent_session_id
4207
+ }));
4208
+ return { body: { sessions } };
4209
+ });
4210
+ server.registerRoute("GET", "/api/mcp/team", async () => {
4211
+ const teamDb = getDatabase();
4212
+ const rows = teamDb.prepare(
4213
+ `SELECT id, "user", role, joined, tags
4214
+ FROM team_members
4215
+ ORDER BY id ASC`
4216
+ ).all();
4217
+ const members = rows.map((row) => ({
4218
+ id: row.id,
4219
+ user: row.user,
4220
+ role: row.role ?? null,
4221
+ joined: row.joined ?? null,
4222
+ tags: row.tags ? row.tags.split(",").map((t) => t.trim()) : []
4223
+ }));
4224
+ return { body: { members } };
4225
+ });
4226
+ const SupersedeBody = external_exports.object({
4227
+ old_spore_id: external_exports.string(),
4228
+ new_spore_id: external_exports.string(),
4229
+ reason: external_exports.string().optional()
4230
+ });
4231
+ server.registerRoute("POST", "/api/mcp/supersede", async (req) => {
4232
+ const { old_spore_id, new_spore_id, reason } = SupersedeBody.parse(req.body);
4233
+ const { randomBytes } = await import("crypto");
4234
+ const now = epochSeconds();
4235
+ updateSporeStatus(old_spore_id, "superseded", now);
4236
+ try {
4237
+ embeddingManager.onStatusChanged("spores", old_spore_id, "superseded");
4238
+ } catch {
4239
+ }
4240
+ registerAgent({
4241
+ id: USER_AGENT_ID,
4242
+ name: USER_AGENT_NAME,
4243
+ created_at: now
4244
+ });
4245
+ const { insertResolutionEvent } = await import("./resolution-events-TFEQPVKS.js");
4246
+ const resolutionId = `res-${randomBytes(RESOLUTION_ID_RANDOM_BYTES).toString("hex")}`;
4247
+ insertResolutionEvent({
4248
+ id: resolutionId,
4249
+ agent_id: USER_AGENT_ID,
4250
+ spore_id: old_spore_id,
4251
+ action: "supersede",
4252
+ new_spore_id,
4253
+ reason: reason ?? null,
4254
+ created_at: now
4255
+ });
4256
+ return {
4257
+ body: {
4258
+ old_spore: old_spore_id,
4259
+ new_spore: new_spore_id,
4260
+ status: "superseded"
4261
+ }
4262
+ };
4263
+ });
4264
+ server.registerRoute("GET", "/api/search", createSearchHandler({ embeddingManager }));
4265
+ server.registerRoute("GET", "/api/activity", handleGetFeed);
4266
+ server.registerRoute("GET", "/api/embedding/status", async () => handleGetEmbeddingStatus(vaultDir));
4267
+ server.registerRoute("GET", "/api/embedding/details", async () => handleEmbeddingDetails(embeddingManager));
4268
+ server.registerRoute("POST", "/api/embedding/rebuild", async () => handleEmbeddingRebuild(embeddingManager));
4269
+ server.registerRoute("POST", "/api/embedding/reconcile", async () => handleEmbeddingReconcile(embeddingManager));
4270
+ server.registerRoute("POST", "/api/embedding/clean-orphans", async () => handleEmbeddingCleanOrphans(embeddingManager));
4271
+ server.registerRoute("POST", "/api/embedding/reembed-stale", async () => handleEmbeddingReembedStale(embeddingManager));
4272
+ await server.evictExistingDaemon();
4273
+ const resolvedPort = await resolvePort(config.daemon.port, vaultDir);
4274
+ if (resolvedPort === 0) {
4275
+ logger.warn(LOG_KINDS.DAEMON_PORT, "All preferred ports occupied, using ephemeral port");
4276
+ }
4277
+ await server.start(resolvedPort);
4278
+ logger.info(LOG_KINDS.DAEMON_READY, "Daemon ready", { vault: vaultDir, port: server.port });
4279
+ if (config.daemon.port === null && resolvedPort !== 0) {
4280
+ try {
4281
+ updateConfig(vaultDir, (c) => ({
4282
+ ...c,
4283
+ daemon: { ...c.daemon, port: resolvedPort }
4284
+ }));
4285
+ logger.info(LOG_KINDS.DAEMON_CONFIG, "Persisted auto-derived port to myco.yaml", { port: resolvedPort });
4286
+ } catch (err) {
4287
+ logger.warn(LOG_KINDS.DAEMON_CONFIG, "Failed to persist auto-derived port", { error: err.message });
4288
+ }
4289
+ }
4290
+ let reconcileRunning = false;
4291
+ powerManager.register({
4292
+ name: "embedding-reconcile",
4293
+ runIn: ["active", "idle"],
4294
+ fn: async () => {
4295
+ if (reconcileRunning) return;
4296
+ reconcileRunning = true;
4297
+ try {
4298
+ await embeddingManager.reconcile(EMBEDDING_BATCH_SIZE);
4299
+ } finally {
4300
+ reconcileRunning = false;
4301
+ }
4302
+ }
4303
+ });
4304
+ powerManager.register({
4305
+ name: "session-maintenance",
4306
+ runIn: ["active", "idle", "sleep"],
4307
+ fn: () => runSessionMaintenance({
4308
+ logger,
4309
+ registeredSessionIds: () => registry.sessions,
4310
+ embeddingManager,
4311
+ vaultDir
4312
+ })
4313
+ });
4314
+ if (config.agent.auto_run) {
4315
+ let agentRunning = false;
4316
+ const agentIntervalMs = config.agent.interval_seconds * MS_PER_SECOND;
4317
+ const lastRunRow = getDatabase().prepare(
4318
+ `SELECT started_at FROM agent_runs WHERE agent_id = ? AND status IN ('completed', 'failed') ORDER BY started_at DESC LIMIT 1`
4319
+ ).get(DEFAULT_AGENT_ID);
4320
+ let lastAgentRun = lastRunRow ? lastRunRow.started_at * MS_PER_SECOND : 0;
4321
+ powerManager.register({
4322
+ name: "agent-auto-run",
4323
+ runIn: ["active", "idle"],
4324
+ fn: async () => {
4325
+ if (agentRunning) return;
4326
+ if (Date.now() - lastAgentRun < agentIntervalMs) return;
4327
+ const agentDb = getDatabase();
4328
+ const checkRow = agentDb.prepare("SELECT COUNT(*) as count FROM prompt_batches WHERE processed = 0").get();
4329
+ const count = Number(checkRow.count);
4330
+ if (count === 0) {
4331
+ logger.debug(LOG_KINDS.AGENT_AUTO_RUN, "No unprocessed batches, skipping cycle");
4332
+ return;
4333
+ }
4334
+ agentRunning = true;
4335
+ lastAgentRun = Date.now();
4336
+ try {
4337
+ logger.info(LOG_KINDS.AGENT_AUTO_RUN, "Unprocessed batches found, starting agent", { count });
4338
+ const { runAgent } = await import("./executor-ONSDHPGX.js");
4339
+ const runResult = await runAgent(vaultDir, { embeddingManager });
4340
+ logger.info(LOG_KINDS.AGENT_RUN, "Agent run completed", { status: runResult.status, runId: runResult.runId });
4341
+ } catch (err) {
4342
+ logger.error(LOG_KINDS.AGENT_ERROR, "Agent auto-run failed", { error: err.message });
4343
+ } finally {
4344
+ agentRunning = false;
4345
+ }
4346
+ }
4347
+ });
4348
+ } else {
4349
+ logger.info(LOG_KINDS.AGENT_AUTO_RUN, "Auto-agent disabled (agent.auto_run = false)");
4350
+ }
4351
+ powerManager.register({
4352
+ name: "log-retention",
4353
+ runIn: ["idle", "sleep"],
4354
+ fn: async () => {
4355
+ const retentionDays = config.daemon.log_retention_days;
4356
+ const cutoff = new Date(Date.now() - retentionDays * MS_PER_DAY).toISOString();
4357
+ const deleted = deleteOldLogs(cutoff);
4358
+ if (deleted > 0) {
4359
+ logger.info(LOG_KINDS.LOG_RETENTION, `Deleted ${deleted} log entries older than ${retentionDays} days`, { deleted, retention_days: retentionDays });
4360
+ }
4361
+ }
4362
+ });
4363
+ powerManager.start();
4364
+ const shutdown = async (signal) => {
4365
+ logger.info(LOG_KINDS.DAEMON_START, `${signal} received`);
4366
+ powerManager.stop();
4367
+ if (activeStopProcessing) {
4368
+ logger.info(LOG_KINDS.DAEMON_START, "Waiting for active stop processing to complete...");
4369
+ await activeStopProcessing;
4370
+ }
4371
+ registry.destroy();
4372
+ await server.stop();
4373
+ vectorStore.close();
4374
+ closeDatabase();
4375
+ logger.close();
4376
+ process.exit(0);
4377
+ };
4378
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
4379
+ process.on("SIGINT", () => shutdown("SIGINT"));
4380
+ }
4381
+ export {
4382
+ handleCompact,
4383
+ handleStopBatches,
4384
+ handleStopFailure,
4385
+ handleSubagentStart,
4386
+ handleSubagentStop,
4387
+ handleTaskCompleted,
4388
+ handleToolFailure,
4389
+ handleToolUse,
4390
+ handleUserPrompt,
4391
+ main
4392
+ };
4393
+ //# sourceMappingURL=main-BMCL7CPO.js.map