@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.
- package/.claude-plugin/marketplace.json +2 -3
- package/.claude-plugin/plugin.json +3 -3
- package/CONTRIBUTING.md +37 -30
- package/README.md +64 -28
- package/bin/myco-run +2 -0
- package/dist/agent-run-EFICNTAU.js +34 -0
- package/dist/agent-run-EFICNTAU.js.map +1 -0
- package/dist/agent-tasks-RXJ7Z5NG.js +180 -0
- package/dist/agent-tasks-RXJ7Z5NG.js.map +1 -0
- package/dist/chunk-2T7RPVPP.js +116 -0
- package/dist/chunk-2T7RPVPP.js.map +1 -0
- package/dist/chunk-3K5WGSJ4.js +165 -0
- package/dist/chunk-3K5WGSJ4.js.map +1 -0
- package/dist/chunk-46PWOKSI.js +26 -0
- package/dist/chunk-46PWOKSI.js.map +1 -0
- package/dist/chunk-4LPQ26CK.js +277 -0
- package/dist/chunk-4LPQ26CK.js.map +1 -0
- package/dist/chunk-5PEUFJ6U.js +92 -0
- package/dist/chunk-5PEUFJ6U.js.map +1 -0
- package/dist/chunk-5VZ52A4T.js +136 -0
- package/dist/chunk-5VZ52A4T.js.map +1 -0
- package/dist/chunk-BUSP3OJB.js +103 -0
- package/dist/chunk-BUSP3OJB.js.map +1 -0
- package/dist/chunk-D7TYRPRM.js +7312 -0
- package/dist/chunk-D7TYRPRM.js.map +1 -0
- package/dist/chunk-DCXRSSBP.js +22 -0
- package/dist/chunk-DCXRSSBP.js.map +1 -0
- package/dist/chunk-E4VLWIJC.js +2 -0
- package/dist/chunk-FFAYUQ5N.js +39 -0
- package/dist/chunk-FFAYUQ5N.js.map +1 -0
- package/dist/chunk-IB76KGBY.js +2 -0
- package/dist/chunk-JMJJEQ3P.js +486 -0
- package/dist/chunk-JMJJEQ3P.js.map +1 -0
- package/dist/{chunk-N33KUCFP.js → chunk-JTYZRPX5.js} +1 -9
- package/dist/chunk-JTYZRPX5.js.map +1 -0
- package/dist/{chunk-NLUE6CYG.js → chunk-JYOOJCPQ.js} +33 -17
- package/dist/chunk-JYOOJCPQ.js.map +1 -0
- package/dist/{chunk-Z74SDEKE.js → chunk-KB4DGYIY.js} +91 -9
- package/dist/chunk-KB4DGYIY.js.map +1 -0
- package/dist/{chunk-ERG2IEWX.js → chunk-KH64DHOY.js} +3 -7413
- package/dist/chunk-KH64DHOY.js.map +1 -0
- package/dist/chunk-KV4OC4H3.js +498 -0
- package/dist/chunk-KV4OC4H3.js.map +1 -0
- package/dist/chunk-KYLDNM7H.js +66 -0
- package/dist/chunk-KYLDNM7H.js.map +1 -0
- package/dist/chunk-LPUQPDC2.js +19 -0
- package/dist/chunk-LPUQPDC2.js.map +1 -0
- package/dist/chunk-M5XWW7UI.js +97 -0
- package/dist/chunk-M5XWW7UI.js.map +1 -0
- package/dist/chunk-MHSCMET3.js +275 -0
- package/dist/chunk-MHSCMET3.js.map +1 -0
- package/dist/chunk-MYX5NCRH.js +45 -0
- package/dist/chunk-MYX5NCRH.js.map +1 -0
- package/dist/chunk-OXZSXYAT.js +877 -0
- package/dist/chunk-OXZSXYAT.js.map +1 -0
- package/dist/chunk-PB6TOLRQ.js +35 -0
- package/dist/chunk-PB6TOLRQ.js.map +1 -0
- package/dist/chunk-PT5IC642.js +162 -0
- package/dist/chunk-PT5IC642.js.map +1 -0
- package/dist/chunk-QIK2XSDQ.js +187 -0
- package/dist/chunk-QIK2XSDQ.js.map +1 -0
- package/dist/chunk-RJ6ZQKG5.js +26 -0
- package/dist/chunk-RJ6ZQKG5.js.map +1 -0
- package/dist/{chunk-YIQLYIHW.js → chunk-TRUJLI6K.js} +29 -43
- package/dist/chunk-TRUJLI6K.js.map +1 -0
- package/dist/chunk-U3IBO3O3.js +41 -0
- package/dist/chunk-U3IBO3O3.js.map +1 -0
- package/dist/{chunk-7WHF2OIZ.js → chunk-UBZPD4HN.js} +25 -7
- package/dist/chunk-UBZPD4HN.js.map +1 -0
- package/dist/{chunk-HIN3UVOG.js → chunk-V7XG6V6C.js} +20 -11
- package/dist/chunk-V7XG6V6C.js.map +1 -0
- package/dist/chunk-WGTCA2NU.js +84 -0
- package/dist/chunk-WGTCA2NU.js.map +1 -0
- package/dist/{chunk-O6PERU7U.js → chunk-XNOCTDHF.js} +2 -2
- package/dist/chunk-YDN4OM33.js +80 -0
- package/dist/chunk-YDN4OM33.js.map +1 -0
- package/dist/cli-ODLFRIYS.js +128 -0
- package/dist/cli-ODLFRIYS.js.map +1 -0
- package/dist/client-EYOTW3JU.js +19 -0
- package/dist/client-MXRNQ5FI.js +13 -0
- package/dist/{config-IBS6KOLQ.js → config-UR5BSGVX.js} +21 -34
- package/dist/config-UR5BSGVX.js.map +1 -0
- package/dist/detect-H5OPI7GD.js +17 -0
- package/dist/detect-H5OPI7GD.js.map +1 -0
- package/dist/detect-providers-Q42OD4OS.js +26 -0
- package/dist/detect-providers-Q42OD4OS.js.map +1 -0
- package/dist/doctor-JLKTXDEH.js +258 -0
- package/dist/doctor-JLKTXDEH.js.map +1 -0
- package/dist/executor-ONSDHPGX.js +1441 -0
- package/dist/executor-ONSDHPGX.js.map +1 -0
- package/dist/init-6GWY345B.js +198 -0
- package/dist/init-6GWY345B.js.map +1 -0
- package/dist/init-wizard-UONLDYLI.js +294 -0
- package/dist/init-wizard-UONLDYLI.js.map +1 -0
- package/dist/llm-BV3QNVRD.js +17 -0
- package/dist/llm-BV3QNVRD.js.map +1 -0
- package/dist/loader-SH67XD54.js +28 -0
- package/dist/loader-SH67XD54.js.map +1 -0
- package/dist/loader-XVXKZZDH.js +18 -0
- package/dist/loader-XVXKZZDH.js.map +1 -0
- package/dist/{chunk-H7PRCVGQ.js → logs-QZVYF6FP.js} +74 -5
- package/dist/logs-QZVYF6FP.js.map +1 -0
- package/dist/main-BMCL7CPO.js +4393 -0
- package/dist/main-BMCL7CPO.js.map +1 -0
- package/dist/openai-embeddings-C265WRNK.js +14 -0
- package/dist/openai-embeddings-C265WRNK.js.map +1 -0
- package/dist/openrouter-U6VFCRX2.js +14 -0
- package/dist/openrouter-U6VFCRX2.js.map +1 -0
- package/dist/post-compact-OWFSOITU.js +26 -0
- package/dist/post-compact-OWFSOITU.js.map +1 -0
- package/dist/post-tool-use-DOUM7CGQ.js +56 -0
- package/dist/post-tool-use-DOUM7CGQ.js.map +1 -0
- package/dist/post-tool-use-failure-SG3C7PE6.js +28 -0
- package/dist/post-tool-use-failure-SG3C7PE6.js.map +1 -0
- package/dist/pre-compact-3J33CHXQ.js +25 -0
- package/dist/pre-compact-3J33CHXQ.js.map +1 -0
- package/dist/provider-check-3WBPZADE.js +12 -0
- package/dist/provider-check-3WBPZADE.js.map +1 -0
- package/dist/registry-J4XTWARS.js +25 -0
- package/dist/registry-J4XTWARS.js.map +1 -0
- package/dist/resolution-events-TFEQPVKS.js +12 -0
- package/dist/resolution-events-TFEQPVKS.js.map +1 -0
- package/dist/resolve-3FEUV462.js +9 -0
- package/dist/resolve-3FEUV462.js.map +1 -0
- package/dist/{restart-XCMILOL5.js → restart-2VM33WOB.js} +10 -6
- package/dist/{restart-XCMILOL5.js.map → restart-2VM33WOB.js.map} +1 -1
- package/dist/search-ZGQR5MDE.js +91 -0
- package/dist/search-ZGQR5MDE.js.map +1 -0
- package/dist/{server-6UDN35QN.js → server-6KMBJCHZ.js} +308 -517
- package/dist/server-6KMBJCHZ.js.map +1 -0
- package/dist/session-Z2FXDDG6.js +68 -0
- package/dist/session-Z2FXDDG6.js.map +1 -0
- package/dist/session-end-FLVX32LE.js +38 -0
- package/dist/session-end-FLVX32LE.js.map +1 -0
- package/dist/session-start-UCLK7PXE.js +169 -0
- package/dist/session-start-UCLK7PXE.js.map +1 -0
- package/dist/setup-digest-4KDSXAIV.js +15 -0
- package/dist/setup-digest-4KDSXAIV.js.map +1 -0
- package/dist/setup-llm-GKMCHURK.js +81 -0
- package/dist/setup-llm-GKMCHURK.js.map +1 -0
- package/dist/src/agent/definitions/agent.yaml +35 -0
- package/dist/src/agent/definitions/tasks/digest-only.yaml +84 -0
- package/dist/src/agent/definitions/tasks/extract-only.yaml +87 -0
- package/dist/src/agent/definitions/tasks/full-intelligence.yaml +472 -0
- package/dist/src/agent/definitions/tasks/graph-maintenance.yaml +92 -0
- package/dist/src/agent/definitions/tasks/review-session.yaml +132 -0
- package/dist/src/agent/definitions/tasks/supersession-sweep.yaml +86 -0
- package/dist/src/agent/definitions/tasks/title-summary.yaml +88 -0
- package/dist/src/agent/prompts/agent.md +121 -0
- package/dist/src/agent/prompts/orchestrator.md +91 -0
- package/dist/src/cli.js +1 -8
- package/dist/src/cli.js.map +1 -1
- package/dist/src/daemon/main.js +1 -8
- package/dist/src/daemon/main.js.map +1 -1
- package/dist/src/hooks/post-tool-use.js +3 -50
- package/dist/src/hooks/post-tool-use.js.map +1 -1
- package/dist/src/hooks/session-end.js +3 -32
- package/dist/src/hooks/session-end.js.map +1 -1
- package/dist/src/hooks/session-start.js +2 -8
- package/dist/src/hooks/session-start.js.map +1 -1
- package/dist/src/hooks/stop.js +3 -42
- package/dist/src/hooks/stop.js.map +1 -1
- package/dist/src/hooks/user-prompt-submit.js +3 -53
- package/dist/src/hooks/user-prompt-submit.js.map +1 -1
- package/dist/src/mcp/server.js +1 -8
- package/dist/src/mcp/server.js.map +1 -1
- package/dist/src/prompts/digest-system.md +1 -1
- package/dist/src/symbionts/manifests/claude-code.yaml +16 -0
- package/dist/src/symbionts/manifests/cursor.yaml +14 -0
- package/dist/stats-IUJPZSVZ.js +94 -0
- package/dist/stats-IUJPZSVZ.js.map +1 -0
- package/dist/stop-XRQLLXST.js +42 -0
- package/dist/stop-XRQLLXST.js.map +1 -0
- package/dist/stop-failure-2CAJJKRG.js +26 -0
- package/dist/stop-failure-2CAJJKRG.js.map +1 -0
- package/dist/subagent-start-MWWQTZMQ.js +26 -0
- package/dist/subagent-start-MWWQTZMQ.js.map +1 -0
- package/dist/subagent-stop-PJXYGRXB.js +28 -0
- package/dist/subagent-stop-PJXYGRXB.js.map +1 -0
- package/dist/task-completed-4LFRJVGI.js +27 -0
- package/dist/task-completed-4LFRJVGI.js.map +1 -0
- package/dist/ui/assets/index-DZrElonz.js +744 -0
- package/dist/ui/assets/index-TkeiYbZB.css +1 -0
- package/dist/ui/favicon.svg +7 -7
- package/dist/ui/fonts/Inter-Variable.woff2 +0 -0
- package/dist/ui/fonts/JetBrainsMono-Variable.woff2 +0 -0
- package/dist/ui/fonts/Newsreader-Italic-Variable.woff2 +0 -0
- package/dist/ui/fonts/Newsreader-Variable.woff2 +0 -0
- package/dist/ui/index.html +2 -2
- package/dist/user-prompt-submit-KSM3AR6P.js +59 -0
- package/dist/user-prompt-submit-KSM3AR6P.js.map +1 -0
- package/dist/{verify-TOWQHPBX.js → verify-UDAYVX37.js} +17 -22
- package/dist/verify-UDAYVX37.js.map +1 -0
- package/dist/{version-36RVCQA6.js → version-KLBN4HZT.js} +3 -4
- package/dist/version-KLBN4HZT.js.map +1 -0
- package/hooks/hooks.json +82 -5
- package/package.json +6 -3
- package/skills/myco/SKILL.md +10 -10
- package/skills/myco/references/cli-usage.md +15 -13
- package/skills/myco/references/vault-status.md +3 -3
- package/skills/myco/references/wisdom.md +4 -4
- package/skills/myco-curate/SKILL.md +86 -0
- package/dist/chunk-2ZIBCEYO.js +0 -113
- package/dist/chunk-2ZIBCEYO.js.map +0 -1
- package/dist/chunk-4RMSHZE4.js +0 -107
- package/dist/chunk-4RMSHZE4.js.map +0 -1
- package/dist/chunk-4XVKZ3WA.js +0 -1078
- package/dist/chunk-4XVKZ3WA.js.map +0 -1
- package/dist/chunk-6FQISQNA.js +0 -61
- package/dist/chunk-6FQISQNA.js.map +0 -1
- package/dist/chunk-7WHF2OIZ.js.map +0 -1
- package/dist/chunk-ERG2IEWX.js.map +0 -1
- package/dist/chunk-FPRXMJLT.js +0 -56
- package/dist/chunk-FPRXMJLT.js.map +0 -1
- package/dist/chunk-GENQ5QGP.js +0 -37
- package/dist/chunk-GENQ5QGP.js.map +0 -1
- package/dist/chunk-H7PRCVGQ.js.map +0 -1
- package/dist/chunk-HIN3UVOG.js.map +0 -1
- package/dist/chunk-HYVT345Y.js +0 -159
- package/dist/chunk-HYVT345Y.js.map +0 -1
- package/dist/chunk-J4D4CROB.js +0 -143
- package/dist/chunk-J4D4CROB.js.map +0 -1
- package/dist/chunk-MDLSAFPP.js +0 -99
- package/dist/chunk-MDLSAFPP.js.map +0 -1
- package/dist/chunk-N33KUCFP.js.map +0 -1
- package/dist/chunk-NL6WQO56.js +0 -65
- package/dist/chunk-NL6WQO56.js.map +0 -1
- package/dist/chunk-NLUE6CYG.js.map +0 -1
- package/dist/chunk-P723N2LP.js +0 -147
- package/dist/chunk-P723N2LP.js.map +0 -1
- package/dist/chunk-QLUE3BUL.js +0 -161
- package/dist/chunk-QLUE3BUL.js.map +0 -1
- package/dist/chunk-QN4W3JUA.js +0 -43
- package/dist/chunk-QN4W3JUA.js.map +0 -1
- package/dist/chunk-RGVBGTD6.js +0 -21
- package/dist/chunk-RGVBGTD6.js.map +0 -1
- package/dist/chunk-TWSTAVLO.js +0 -132
- package/dist/chunk-TWSTAVLO.js.map +0 -1
- package/dist/chunk-UP4P4OAA.js +0 -4423
- package/dist/chunk-UP4P4OAA.js.map +0 -1
- package/dist/chunk-YIQLYIHW.js.map +0 -1
- package/dist/chunk-YTFXA4RX.js +0 -86
- package/dist/chunk-YTFXA4RX.js.map +0 -1
- package/dist/chunk-Z74SDEKE.js.map +0 -1
- package/dist/cli-IHILSS6N.js +0 -97
- package/dist/cli-IHILSS6N.js.map +0 -1
- package/dist/client-AGFNR2S4.js +0 -12
- package/dist/config-IBS6KOLQ.js.map +0 -1
- package/dist/curate-3D4GHKJH.js +0 -78
- package/dist/curate-3D4GHKJH.js.map +0 -1
- package/dist/detect-providers-XEP4QA3R.js +0 -35
- package/dist/detect-providers-XEP4QA3R.js.map +0 -1
- package/dist/digest-7HLJXL77.js +0 -85
- package/dist/digest-7HLJXL77.js.map +0 -1
- package/dist/init-ARQ53JOR.js +0 -109
- package/dist/init-ARQ53JOR.js.map +0 -1
- package/dist/logs-IENORIYR.js +0 -84
- package/dist/logs-IENORIYR.js.map +0 -1
- package/dist/main-6AGPIMH2.js +0 -5715
- package/dist/main-6AGPIMH2.js.map +0 -1
- package/dist/rebuild-Q2ACEB6F.js +0 -64
- package/dist/rebuild-Q2ACEB6F.js.map +0 -1
- package/dist/reprocess-CDEFGQOV.js +0 -79
- package/dist/reprocess-CDEFGQOV.js.map +0 -1
- package/dist/search-7W25SKCB.js +0 -120
- package/dist/search-7W25SKCB.js.map +0 -1
- package/dist/server-6UDN35QN.js.map +0 -1
- package/dist/session-F326AWCH.js +0 -44
- package/dist/session-F326AWCH.js.map +0 -1
- package/dist/session-start-K6IGAC7H.js +0 -192
- package/dist/session-start-K6IGAC7H.js.map +0 -1
- package/dist/setup-digest-X5PN27F4.js +0 -15
- package/dist/setup-llm-S5OHQJXK.js +0 -15
- package/dist/src/prompts/classification.md +0 -43
- package/dist/stats-TTSDXGJV.js +0 -58
- package/dist/stats-TTSDXGJV.js.map +0 -1
- package/dist/templates-XPRBOWCE.js +0 -38
- package/dist/templates-XPRBOWCE.js.map +0 -1
- package/dist/ui/assets/index-08wKT7wS.css +0 -1
- package/dist/ui/assets/index-CMSMi4Jb.js +0 -369
- package/dist/verify-TOWQHPBX.js.map +0 -1
- package/skills/setup/SKILL.md +0 -174
- package/skills/setup/references/model-recommendations.md +0 -83
- /package/dist/{client-AGFNR2S4.js.map → chunk-E4VLWIJC.js.map} +0 -0
- /package/dist/{setup-digest-X5PN27F4.js.map → chunk-IB76KGBY.js.map} +0 -0
- /package/dist/{chunk-O6PERU7U.js.map → chunk-XNOCTDHF.js.map} +0 -0
- /package/dist/{setup-llm-S5OHQJXK.js.map → client-EYOTW3JU.js.map} +0 -0
- /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
|