@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 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/db/queries/batches.ts","../src/utils/error-message.ts","../src/db/queries/turns.ts","../src/db/queries/runs.ts","../src/db/queries/reports.ts","../src/db/queries/graph-edges.ts","../src/db/queries/lineage.ts","../src/db/queries/entities.ts","../src/db/queries/digest-extracts.ts"],"sourcesContent":["/**\n * Prompt batch CRUD query helpers.\n *\n * All functions obtain the SQLite instance internally via `getDatabase()`.\n * Queries use positional `?` placeholders throughout (better-sqlite3).\n */\n\nimport { getDatabase } from '@myco/db/client.js';\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Default number of unprocessed batches returned when no limit given. */\nconst DEFAULT_UNPROCESSED_LIMIT = 100;\n\n/** Default number of batches returned by listBatchesBySession when no limit given. */\nexport const BATCHES_DEFAULT_LIMIT = 200;\n\n/** Batch status value when a batch is closed normally. */\nconst STATUS_COMPLETED = 'completed';\n\n/** Default batch status for new batches. */\nconst DEFAULT_STATUS = 'active';\n\n/** Default activity count for new batches. */\nconst DEFAULT_ACTIVITY_COUNT = 0;\n\n/** Default processed flag for new batches. */\nconst DEFAULT_PROCESSED = 0;\n\n/** Processed flag value indicating a batch has been processed. */\nconst PROCESSED_FLAG = 1;\n\n/** Number of characters used for prompt prefix matching. */\nconst PROMPT_PREFIX_MATCH_CHARS = 60;\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Filter options for `listBatchesBySession`. */\nexport interface ListBatchesBySessionOptions {\n limit?: number;\n offset?: number;\n}\n\n/** Fields required (or optional) when inserting a prompt batch. */\nexport interface BatchInsert {\n session_id: string;\n created_at: number;\n prompt_number?: number | null;\n user_prompt?: string | null;\n response_summary?: string | null;\n classification?: string | null;\n started_at?: number | null;\n ended_at?: number | null;\n status?: string;\n activity_count?: number;\n processed?: number;\n content_hash?: string | null;\n}\n\n/** Row shape returned from batch queries. */\nexport interface BatchRow {\n id: number;\n session_id: string;\n prompt_number: number | null;\n user_prompt: string | null;\n response_summary: string | null;\n classification: string | null;\n started_at: number | null;\n ended_at: number | null;\n status: string;\n activity_count: number;\n processed: number;\n content_hash: string | null;\n created_at: number;\n}\n\n// ---------------------------------------------------------------------------\n// Column list\n// ---------------------------------------------------------------------------\n\nconst BATCH_COLUMNS = [\n 'id',\n 'session_id',\n 'prompt_number',\n 'user_prompt',\n 'response_summary',\n 'classification',\n 'started_at',\n 'ended_at',\n 'status',\n 'activity_count',\n 'processed',\n 'content_hash',\n 'created_at',\n] as const;\n\nconst SELECT_COLUMNS = BATCH_COLUMNS.join(', ');\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Normalize a SQLite result row into a typed BatchRow. */\nfunction toBatchRow(row: Record<string, unknown>): BatchRow {\n return {\n id: row.id as number,\n session_id: row.session_id as string,\n prompt_number: (row.prompt_number as number) ?? null,\n user_prompt: (row.user_prompt as string) ?? null,\n response_summary: (row.response_summary as string) ?? null,\n classification: (row.classification as string) ?? null,\n started_at: (row.started_at as number) ?? null,\n ended_at: (row.ended_at as number) ?? null,\n status: row.status as string,\n activity_count: row.activity_count as number,\n processed: row.processed as number,\n content_hash: (row.content_hash as string) ?? null,\n created_at: row.created_at as number,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Insert a new prompt batch.\n *\n * The `id` is auto-generated by the INTEGER PRIMARY KEY (AUTOINCREMENT).\n * FTS5 index is kept in sync via a follow-up INSERT into prompt_batches_fts.\n */\nexport function insertBatch(data: BatchInsert): BatchRow {\n const db = getDatabase();\n\n const info = db.prepare(\n `INSERT INTO prompt_batches (\n session_id, prompt_number, user_prompt, response_summary,\n classification, started_at, ended_at, status,\n activity_count, processed, content_hash, created_at\n ) VALUES (\n ?, ?, ?, ?,\n ?, ?, ?, ?,\n ?, ?, ?, ?\n )`,\n ).run(\n data.session_id,\n data.prompt_number ?? null,\n data.user_prompt ?? null,\n data.response_summary ?? null,\n data.classification ?? null,\n data.started_at ?? null,\n data.ended_at ?? null,\n data.status ?? DEFAULT_STATUS,\n data.activity_count ?? DEFAULT_ACTIVITY_COUNT,\n data.processed ?? DEFAULT_PROCESSED,\n data.content_hash ?? null,\n data.created_at,\n );\n\n const batchId = Number(info.lastInsertRowid);\n\n // FTS5 sync\n const userPrompt = data.user_prompt ?? null;\n if (userPrompt) {\n db.prepare('INSERT INTO prompt_batches_fts(rowid, user_prompt) VALUES (?, ?)').run(batchId, userPrompt);\n }\n\n return toBatchRow(\n db.prepare(`SELECT ${SELECT_COLUMNS} FROM prompt_batches WHERE id = ?`).get(batchId) as Record<string, unknown>,\n );\n}\n\n/**\n * Close a batch — set status to 'completed' and record the end time.\n *\n * @returns the updated row, or null if the batch does not exist.\n */\nexport function closeBatch(\n id: number,\n endedAt: number,\n): BatchRow | null {\n const db = getDatabase();\n\n const info = db.prepare(\n `UPDATE prompt_batches\n SET status = ?, ended_at = ?\n WHERE id = ?`,\n ).run(STATUS_COMPLETED, endedAt, id);\n\n if (info.changes === 0) return null;\n\n return toBatchRow(\n db.prepare(`SELECT ${SELECT_COLUMNS} FROM prompt_batches WHERE id = ?`).get(id) as Record<string, unknown>,\n );\n}\n\n/**\n * Populate response_summary on batches from transcript turns.\n *\n * Matches transcript turns (ordered by position) to batches (ordered by id ASC).\n * This is resilient to prompt_number duplicates caused by daemon restarts.\n * Only updates batches that don't already have a response_summary.\n *\n * @param sessionId — the session to update\n * @param responses — array of { response } ordered by turn position (1-indexed)\n */\nexport function populateBatchResponses(\n sessionId: string,\n responses: Array<{ turnIndex: number; response: string }>,\n): void {\n const db = getDatabase();\n\n // Get all batches for this session ordered by id (insertion order = true order)\n const batches = db.prepare(\n `SELECT id FROM prompt_batches WHERE session_id = ? ORDER BY id ASC`,\n ).all(sessionId) as Array<{ id: number }>;\n\n // Map each response to the batch at the same position\n for (const { turnIndex, response } of responses) {\n const batchIndex = turnIndex - 1; // turns are 1-indexed\n if (batchIndex >= 0 && batchIndex < batches.length) {\n const batchId = batches[batchIndex].id;\n db.prepare(\n `UPDATE prompt_batches SET response_summary = ? WHERE id = ? AND response_summary IS NULL`,\n ).run(response, batchId);\n }\n }\n}\n\n/**\n * Get unprocessed batches, ordered by id ASC (insertion order).\n *\n * Supports cursor-based pagination via `after_id` and a `limit` cap.\n */\nexport function getUnprocessedBatches(\n options: { after_id?: number; limit?: number } = {},\n): BatchRow[] {\n const db = getDatabase();\n\n const conditions: string[] = [`processed = ?`];\n const params: unknown[] = [DEFAULT_PROCESSED];\n\n if (options.after_id !== undefined) {\n conditions.push(`id > ?`);\n params.push(options.after_id);\n }\n\n const limit = options.limit ?? DEFAULT_UNPROCESSED_LIMIT;\n params.push(limit);\n\n const where = conditions.join(' AND ');\n\n const rows = db.prepare(\n `SELECT ${SELECT_COLUMNS}\n FROM prompt_batches\n WHERE ${where}\n ORDER BY id ASC\n LIMIT ?`,\n ).all(...params) as Record<string, unknown>[];\n\n return rows.map(toBatchRow);\n}\n\n/**\n * Increment the activity_count for a batch by 1.\n *\n * @returns the updated row, or null if the batch does not exist.\n */\nexport function incrementActivityCount(\n id: number,\n): BatchRow | null {\n const db = getDatabase();\n\n const info = db.prepare(\n `UPDATE prompt_batches\n SET activity_count = activity_count + 1\n WHERE id = ?`,\n ).run(id);\n\n if (info.changes === 0) return null;\n\n return toBatchRow(\n db.prepare(`SELECT ${SELECT_COLUMNS} FROM prompt_batches WHERE id = ?`).get(id) as Record<string, unknown>,\n );\n}\n\n/**\n * Mark a batch as processed (processed = 1).\n *\n * @returns the updated row, or null if the batch does not exist.\n */\nexport function markBatchProcessed(\n id: number,\n): BatchRow | null {\n const db = getDatabase();\n\n const info = db.prepare(\n `UPDATE prompt_batches\n SET processed = ?\n WHERE id = ?`,\n ).run(PROCESSED_FLAG, id);\n\n if (info.changes === 0) return null;\n\n return toBatchRow(\n db.prepare(`SELECT ${SELECT_COLUMNS} FROM prompt_batches WHERE id = ?`).get(id) as Record<string, unknown>,\n );\n}\n\n/**\n * Get a batch's ID by session and prompt number.\n * Used to link attachments to their prompt batch at stop time.\n */\nexport function getBatchIdByPromptNumber(\n sessionId: string,\n promptNumber: number,\n): number | null {\n const db = getDatabase();\n\n const row = db.prepare(\n `SELECT id FROM prompt_batches WHERE session_id = ? AND prompt_number = ? LIMIT 1`,\n ).get(sessionId, promptNumber) as { id: number } | undefined;\n\n return row ? row.id : null;\n}\n\n/**\n * Find a batch by matching the start of its user_prompt text.\n * Used for attachment matching after transcript compaction where turn indices no longer\n * align with prompt_numbers.\n */\nexport function findBatchByPromptPrefix(\n sessionId: string,\n promptPrefix: string,\n): { id: number; prompt_number: number } | null {\n const db = getDatabase();\n // Match first N chars — enough to be unique, tolerant of minor differences\n const prefix = promptPrefix.slice(0, PROMPT_PREFIX_MATCH_CHARS);\n const row = db.prepare(\n `SELECT id, prompt_number FROM prompt_batches\n WHERE session_id = ? AND user_prompt LIKE ? || '%'\n LIMIT 1`,\n ).get(sessionId, prefix) as { id: number; prompt_number: number } | undefined;\n return row ?? null;\n}\n\n/** Fields required when inserting a batch statelessly (prompt_number derived from DB). */\nexport interface StatelessBatchInsert {\n session_id: string;\n created_at: number;\n user_prompt?: string | null;\n started_at?: number | null;\n status?: string;\n}\n\n/**\n * Insert a new prompt batch with prompt_number derived from an inline subquery.\n *\n * The prompt_number is set to `COALESCE(MAX(prompt_number), 0) + 1` for the\n * session, so the caller never needs a separate SELECT. This makes the insert\n * stateless — no in-memory counter required.\n *\n * FTS5 index is kept in sync via a follow-up INSERT into prompt_batches_fts.\n */\nexport function insertBatchStateless(data: StatelessBatchInsert): BatchRow {\n const db = getDatabase();\n\n const info = db.prepare(\n `INSERT INTO prompt_batches (\n session_id, prompt_number, user_prompt, response_summary,\n classification, started_at, ended_at, status,\n activity_count, processed, content_hash, created_at\n ) VALUES (\n ?,\n (SELECT COALESCE(MAX(prompt_number), 0) + 1 FROM prompt_batches WHERE session_id = ?),\n ?, NULL,\n NULL, ?, NULL, ?,\n ?, ?, NULL, ?\n )`,\n ).run(\n data.session_id,\n data.session_id,\n data.user_prompt ?? null,\n data.started_at ?? null,\n data.status ?? DEFAULT_STATUS,\n DEFAULT_ACTIVITY_COUNT,\n DEFAULT_PROCESSED,\n data.created_at,\n );\n\n const batchId = Number(info.lastInsertRowid);\n\n // FTS5 sync\n const userPrompt = data.user_prompt ?? null;\n if (userPrompt) {\n db.prepare('INSERT INTO prompt_batches_fts(rowid, user_prompt) VALUES (?, ?)').run(batchId, userPrompt);\n }\n\n return toBatchRow(\n db.prepare(`SELECT ${SELECT_COLUMNS} FROM prompt_batches WHERE id = ?`).get(batchId) as Record<string, unknown>,\n );\n}\n\n/**\n * Close all open batches for a session — blind UPDATE, no prior SELECT needed.\n *\n * Sets `status = 'completed'` and `ended_at` on every batch that has no\n * `ended_at` value yet. Returns the number of batches closed.\n */\nexport function closeOpenBatches(\n sessionId: string,\n endedAt: number,\n): number {\n const db = getDatabase();\n\n const info = db.prepare(\n `UPDATE prompt_batches\n SET status = ?, ended_at = ?\n WHERE session_id = ? AND ended_at IS NULL`,\n ).run(STATUS_COMPLETED, endedAt, sessionId);\n\n return info.changes;\n}\n\n/**\n * Set response_summary on a batch if it doesn't already have one.\n *\n * Idempotent — only updates NULL response_summary.\n */\nexport function setResponseSummary(\n batchId: number,\n summary: string,\n): void {\n const db = getDatabase();\n db.prepare(\n `UPDATE prompt_batches SET response_summary = ? WHERE id = ? AND response_summary IS NULL`,\n ).run(summary, batchId);\n}\n\n/**\n * Get the most recent batch for a session (by id DESC), regardless of status.\n *\n * Used by processStopEvent to attach the AI response and images to the\n * correct batch without positional turn mapping.\n */\nexport function getLatestBatch(\n sessionId: string,\n): BatchRow | null {\n const db = getDatabase();\n\n const row = db.prepare(\n `SELECT ${SELECT_COLUMNS} FROM prompt_batches\n WHERE session_id = ?\n ORDER BY id DESC LIMIT 1`,\n ).get(sessionId) as Record<string, unknown> | undefined;\n\n if (!row) return null;\n return toBatchRow(row);\n}\n\nexport function listBatchesBySession(\n sessionId: string,\n options: ListBatchesBySessionOptions = {},\n): BatchRow[] {\n const db = getDatabase();\n\n const limit = options.limit ?? BATCHES_DEFAULT_LIMIT;\n const offset = options.offset ?? 0;\n\n const rows = db.prepare(\n `SELECT ${SELECT_COLUMNS}\n FROM prompt_batches\n WHERE session_id = ?\n ORDER BY prompt_number ASC\n LIMIT ?\n OFFSET ?`,\n ).all(sessionId, limit, offset) as Record<string, unknown>[];\n\n return rows.map(toBatchRow);\n}\n","/**\n * Extract a human-readable error message from an unknown thrown value.\n *\n * Handles Error instances, strings, and arbitrary objects. Never throws.\n */\nexport function errorMessage(err: unknown): string {\n if (err instanceof Error) return err.message || err.constructor.name || 'Error';\n if (typeof err === 'string') return err || 'Empty string error';\n try { return JSON.stringify(err); } catch { return 'Unserializable error'; }\n}\n","/**\n * Agent turn CRUD query helpers.\n *\n * All functions obtain the SQLite instance internally via `getDatabase()`.\n * Queries use positional `?` placeholders throughout (better-sqlite3).\n */\n\nimport { getDatabase } from '@myco/db/client.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Fields required (or optional) when inserting a turn. */\nexport interface TurnInsert {\n run_id: string;\n agent_id: string;\n turn_number: number;\n tool_name: string;\n tool_input?: string | null;\n tool_output_summary?: string | null;\n started_at?: number | null;\n completed_at?: number | null;\n}\n\n/** Row shape returned from agent_turns queries (all columns). */\nexport interface TurnRow {\n id: number;\n run_id: string;\n agent_id: string;\n turn_number: number;\n tool_name: string;\n tool_input: string | null;\n tool_output_summary: string | null;\n started_at: number | null;\n completed_at: number | null;\n}\n\n// ---------------------------------------------------------------------------\n// Column list\n// ---------------------------------------------------------------------------\n\nconst TURN_COLUMNS = [\n 'id',\n 'run_id',\n 'agent_id',\n 'turn_number',\n 'tool_name',\n 'tool_input',\n 'tool_output_summary',\n 'started_at',\n 'completed_at',\n] as const;\n\nconst SELECT_COLUMNS = TURN_COLUMNS.join(', ');\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Normalize a SQLite result row into a typed TurnRow. */\nfunction toTurnRow(row: Record<string, unknown>): TurnRow {\n return {\n id: row.id as number,\n run_id: row.run_id as string,\n agent_id: row.agent_id as string,\n turn_number: row.turn_number as number,\n tool_name: row.tool_name as string,\n tool_input: (row.tool_input as string) ?? null,\n tool_output_summary: (row.tool_output_summary as string) ?? null,\n started_at: (row.started_at as number) ?? null,\n completed_at: (row.completed_at as number) ?? null,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Insert a new agent turn.\n *\n * The `id` is auto-generated by the INTEGER PRIMARY KEY (AUTOINCREMENT).\n */\nexport function insertTurn(data: TurnInsert): TurnRow {\n const db = getDatabase();\n\n const info = db.prepare(\n `INSERT INTO agent_turns (\n run_id, agent_id, turn_number, tool_name,\n tool_input, tool_output_summary, started_at, completed_at\n ) VALUES (\n ?, ?, ?, ?,\n ?, ?, ?, ?\n )`,\n ).run(\n data.run_id,\n data.agent_id,\n data.turn_number,\n data.tool_name,\n data.tool_input ?? null,\n data.tool_output_summary ?? null,\n data.started_at ?? null,\n data.completed_at ?? null,\n );\n\n const turnId = Number(info.lastInsertRowid);\n\n return toTurnRow(\n db.prepare(`SELECT ${SELECT_COLUMNS} FROM agent_turns WHERE id = ?`).get(turnId) as Record<string, unknown>,\n );\n}\n\n/**\n * List all turns for a specific run, ordered by turn_number ASC.\n */\nexport function listTurns(runId: string): TurnRow[] {\n const db = getDatabase();\n\n const rows = db.prepare(\n `SELECT ${SELECT_COLUMNS}\n FROM agent_turns\n WHERE run_id = ?\n ORDER BY turn_number ASC`,\n ).all(runId) as Record<string, unknown>[];\n\n return rows.map(toTurnRow);\n}\n\n/**\n * List all agent turns for a run, ordered by turn_number ASC.\n *\n * Alias for `listTurns` with an explicit \"by run\" naming convention used\n * by the dashboard API layer.\n */\nexport function listTurnsByRun(runId: string): TurnRow[] {\n return listTurns(runId);\n}\n","/**\n * Agent run CRUD query helpers.\n *\n * All functions obtain the SQLite instance internally via `getDatabase()`.\n * Queries use positional `?` placeholders throughout (better-sqlite3).\n */\n\nimport { getDatabase } from '@myco/db/client.js';\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Default number of runs returned by listRuns when no limit given. */\nconst DEFAULT_LIST_LIMIT = 100;\n\n/** Default run status for new runs. */\nconst DEFAULT_STATUS = 'pending';\n\n/** Run status indicating the run is currently executing. */\nexport const STATUS_RUNNING = 'running';\n\n/** Run status for a successfully completed run. */\nexport const STATUS_COMPLETED = 'completed';\n\n/** Run status for a run that encountered an error. */\nexport const STATUS_FAILED = 'failed';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Fields required (or optional) when inserting a run. */\nexport interface RunInsert {\n id: string;\n agent_id: string;\n task?: string | null;\n instruction?: string | null;\n status?: string;\n started_at?: number | null;\n completed_at?: number | null;\n tokens_used?: number | null;\n cost_usd?: number | null;\n actions_taken?: string | null;\n error?: string | null;\n}\n\n/** Row shape returned from agent_runs queries (all columns). */\nexport interface RunRow {\n id: string;\n agent_id: string;\n task: string | null;\n instruction: string | null;\n status: string;\n started_at: number | null;\n completed_at: number | null;\n tokens_used: number | null;\n cost_usd: number | null;\n actions_taken: string | null;\n error: string | null;\n}\n\n/** Completion data passed to updateRunStatus. */\nexport interface RunCompletion {\n completed_at?: number;\n tokens_used?: number;\n cost_usd?: number;\n actions_taken?: string;\n error?: string;\n}\n\n/** Filter options for `listRuns`. */\nexport interface ListRunsOptions {\n limit?: number;\n offset?: number;\n agent_id?: string;\n status?: string;\n task?: string;\n search?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Column list\n// ---------------------------------------------------------------------------\n\nconst RUN_COLUMNS = [\n 'id',\n 'agent_id',\n 'task',\n 'instruction',\n 'status',\n 'started_at',\n 'completed_at',\n 'tokens_used',\n 'cost_usd',\n 'actions_taken',\n 'error',\n] as const;\n\nconst SELECT_COLUMNS = RUN_COLUMNS.join(', ');\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Normalize a SQLite result row into a typed RunRow. */\nfunction toRunRow(row: Record<string, unknown>): RunRow {\n return {\n id: row.id as string,\n agent_id: row.agent_id as string,\n task: (row.task as string) ?? null,\n instruction: (row.instruction as string) ?? null,\n status: row.status as string,\n started_at: (row.started_at as number) ?? null,\n completed_at: (row.completed_at as number) ?? null,\n tokens_used: (row.tokens_used as number) ?? null,\n cost_usd: (row.cost_usd as number) ?? null,\n actions_taken: (row.actions_taken as string) ?? null,\n error: (row.error as string) ?? null,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Insert a new agent run.\n */\nexport function insertRun(data: RunInsert): RunRow {\n const db = getDatabase();\n\n db.prepare(\n `INSERT INTO agent_runs (\n id, agent_id, task, instruction, status,\n started_at, completed_at, tokens_used, cost_usd,\n actions_taken, error\n ) VALUES (\n ?, ?, ?, ?, ?,\n ?, ?, ?, ?,\n ?, ?\n )`,\n ).run(\n data.id,\n data.agent_id,\n data.task ?? null,\n data.instruction ?? null,\n data.status ?? DEFAULT_STATUS,\n data.started_at ?? null,\n data.completed_at ?? null,\n data.tokens_used ?? null,\n data.cost_usd ?? null,\n data.actions_taken ?? null,\n data.error ?? null,\n );\n\n return toRunRow(\n db.prepare(`SELECT ${SELECT_COLUMNS} FROM agent_runs WHERE id = ?`).get(data.id) as Record<string, unknown>,\n );\n}\n\n/**\n * Retrieve a single run by id.\n *\n * @returns the run row, or null if not found.\n */\nexport function getRun(id: string): RunRow | null {\n const db = getDatabase();\n\n const row = db.prepare(\n `SELECT ${SELECT_COLUMNS} FROM agent_runs WHERE id = ?`,\n ).get(id) as Record<string, unknown> | undefined;\n\n if (!row) return null;\n return toRunRow(row);\n}\n\n/** Build a WHERE clause and params array from ListRunsOptions filter fields. */\nfunction buildRunsWhere(\n options: Omit<ListRunsOptions, 'limit' | 'offset'>,\n): { where: string; params: unknown[] } {\n const conditions: string[] = [];\n const params: unknown[] = [];\n\n if (options.agent_id !== undefined) {\n conditions.push(`agent_id = ?`);\n params.push(options.agent_id);\n }\n if (options.status !== undefined) {\n conditions.push(`status = ?`);\n params.push(options.status);\n }\n if (options.task !== undefined) {\n conditions.push(`task = ?`);\n params.push(options.task);\n }\n if (options.search !== undefined && options.search.length > 0) {\n conditions.push(`task LIKE ?`);\n params.push(`%${options.search}%`);\n }\n\n return {\n where: conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '',\n params,\n };\n}\n\n/**\n * List runs with optional filters, ordered by started_at DESC (nulls last).\n */\nexport function listRuns(\n options: ListRunsOptions = {},\n): RunRow[] {\n const db = getDatabase();\n const { where, params } = buildRunsWhere(options);\n const limit = options.limit ?? DEFAULT_LIST_LIMIT;\n const offset = options.offset ?? 0;\n\n const rows = db.prepare(\n `SELECT ${SELECT_COLUMNS}\n FROM agent_runs\n ${where}\n ORDER BY started_at DESC NULLS LAST\n LIMIT ?\n OFFSET ?`,\n ).all(...params, limit, offset) as Record<string, unknown>[];\n\n return rows.map(toRunRow);\n}\n\n/**\n * Count runs matching the given filters (no limit/offset).\n */\nexport function countRuns(\n options: Omit<ListRunsOptions, 'limit' | 'offset'> = {},\n): number {\n const db = getDatabase();\n const { where, params } = buildRunsWhere(options);\n\n const row = db.prepare(\n `SELECT COUNT(*) as count FROM agent_runs ${where}`,\n ).get(...params) as { count: number };\n\n return row.count;\n}\n\n/**\n * Update a run's status, with optional completion data.\n *\n * @returns the updated row, or null if the run does not exist.\n */\nexport function updateRunStatus(\n id: string,\n status: string,\n completion?: RunCompletion,\n): RunRow | null {\n const db = getDatabase();\n\n const setClauses: string[] = ['status = ?'];\n const params: unknown[] = [status];\n\n if (completion?.completed_at !== undefined) {\n setClauses.push(`completed_at = ?`);\n params.push(completion.completed_at);\n }\n\n if (completion?.tokens_used !== undefined) {\n setClauses.push(`tokens_used = ?`);\n params.push(completion.tokens_used);\n }\n\n if (completion?.cost_usd !== undefined) {\n setClauses.push(`cost_usd = ?`);\n params.push(completion.cost_usd);\n }\n\n if (completion?.actions_taken !== undefined) {\n setClauses.push(`actions_taken = ?`);\n params.push(completion.actions_taken);\n }\n\n if (completion?.error !== undefined) {\n setClauses.push(`error = ?`);\n params.push(completion.error);\n }\n\n params.push(id);\n\n const info = db.prepare(\n `UPDATE agent_runs\n SET ${setClauses.join(', ')}\n WHERE id = ?`,\n ).run(...params);\n\n if (info.changes === 0) return null;\n\n return toRunRow(\n db.prepare(`SELECT ${SELECT_COLUMNS} FROM agent_runs WHERE id = ?`).get(id) as Record<string, unknown>,\n );\n}\n\n/**\n * Get the currently running run for an agent, if any.\n *\n * @returns the running run row, or null if no run is active.\n */\nexport function getRunningRun(\n agentId: string,\n): RunRow | null {\n const db = getDatabase();\n\n const row = db.prepare(\n `SELECT ${SELECT_COLUMNS}\n FROM agent_runs\n WHERE agent_id = ? AND status = ?\n ORDER BY started_at DESC NULLS LAST\n LIMIT 1`,\n ).get(agentId, STATUS_RUNNING) as Record<string, unknown> | undefined;\n\n if (!row) return null;\n return toRunRow(row);\n}\n","/**\n * Agent report CRUD query helpers.\n *\n * All functions obtain the SQLite instance internally via `getDatabase()`.\n * Queries use positional `?` placeholders throughout (better-sqlite3).\n */\n\nimport { getDatabase } from '@myco/db/client.js';\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Default number of reports returned by list queries when no limit given. */\nconst DEFAULT_LIST_LIMIT = 100;\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Fields required (or optional) when inserting a report. */\nexport interface ReportInsert {\n run_id: string;\n agent_id: string;\n action: string;\n summary: string;\n details?: string | null;\n created_at: number;\n}\n\n/** Row shape returned from agent_reports queries (all columns). */\nexport interface ReportRow {\n id: number;\n run_id: string;\n agent_id: string;\n action: string;\n summary: string;\n details: string | null;\n created_at: number;\n}\n\n/** Filter options for `listReportsByAgent`. */\nexport interface ListReportsByAgentOptions {\n limit?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Column list\n// ---------------------------------------------------------------------------\n\nconst REPORT_COLUMNS = [\n 'id',\n 'run_id',\n 'agent_id',\n 'action',\n 'summary',\n 'details',\n 'created_at',\n] as const;\n\nconst SELECT_COLUMNS = REPORT_COLUMNS.join(', ');\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Normalize a SQLite result row into a typed ReportRow. */\nfunction toReportRow(row: Record<string, unknown>): ReportRow {\n return {\n id: row.id as number,\n run_id: row.run_id as string,\n agent_id: row.agent_id as string,\n action: row.action as string,\n summary: row.summary as string,\n details: (row.details as string) ?? null,\n created_at: row.created_at as number,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Insert a new agent report.\n *\n * The `id` is auto-generated by the INTEGER PRIMARY KEY (AUTOINCREMENT).\n */\nexport function insertReport(data: ReportInsert): ReportRow {\n const db = getDatabase();\n\n const info = db.prepare(\n `INSERT INTO agent_reports (\n run_id, agent_id, action, summary, details, created_at\n ) VALUES (\n ?, ?, ?, ?, ?, ?\n )`,\n ).run(\n data.run_id,\n data.agent_id,\n data.action,\n data.summary,\n data.details ?? null,\n data.created_at,\n );\n\n const reportId = Number(info.lastInsertRowid);\n\n return toReportRow(\n db.prepare(`SELECT ${SELECT_COLUMNS} FROM agent_reports WHERE id = ?`).get(reportId) as Record<string, unknown>,\n );\n}\n\n/**\n * List all reports for a specific run, ordered by created_at ASC.\n */\nexport function listReports(runId: string): ReportRow[] {\n const db = getDatabase();\n\n const rows = db.prepare(\n `SELECT ${SELECT_COLUMNS}\n FROM agent_reports\n WHERE run_id = ?\n ORDER BY created_at ASC`,\n ).all(runId) as Record<string, unknown>[];\n\n return rows.map(toReportRow);\n}\n\n/**\n * List reports by agent, ordered by created_at DESC.\n */\nexport function listReportsByAgent(\n agentId: string,\n options: ListReportsByAgentOptions = {},\n): ReportRow[] {\n const db = getDatabase();\n\n const limit = options.limit ?? DEFAULT_LIST_LIMIT;\n\n const rows = db.prepare(\n `SELECT ${SELECT_COLUMNS}\n FROM agent_reports\n WHERE agent_id = ?\n ORDER BY created_at DESC\n LIMIT ?`,\n ).all(agentId, limit) as Record<string, unknown>[];\n\n return rows.map(toReportRow);\n}\n","/**\n * Graph edge CRUD query helpers.\n *\n * Unlike the `edges` table (which has FK constraints to entities), `graph_edges`\n * supports edges between any node types (session, batch, spore, entity).\n *\n * All functions obtain the SQLite instance internally via `getDatabase()`.\n * Queries use positional `?` placeholders throughout (better-sqlite3).\n */\n\nimport crypto from 'node:crypto';\nimport { getDatabase } from '@myco/db/client.js';\nimport { QUERY_DEFAULT_LIST_LIMIT, GRAPH_EDGE_DEFAULT_CONFIDENCE } from '@myco/constants.js';\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Default BFS traversal depth. */\nconst DEFAULT_BFS_DEPTH = 2;\n\n/** Maximum BFS traversal depth (capped for performance). */\nconst MAX_BFS_DEPTH = 5;\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Valid node types in the graph. */\nexport type GraphNodeType = 'session' | 'batch' | 'spore' | 'entity';\n\n/** Lineage edge types (auto-created by daemon, no LLM). */\nexport type LineageEdgeType = 'FROM_SESSION' | 'EXTRACTED_FROM' | 'DERIVED_FROM' | 'HAS_BATCH';\n\n/** Semantic edge types (created by intelligence agent, LLM-driven). */\nexport type SemanticEdgeType = 'RELATES_TO' | 'SUPERSEDED_BY' | 'REFERENCES' | 'DEPENDS_ON' | 'AFFECTS';\n\n/** All valid graph edge types. */\nexport type GraphEdgeType = LineageEdgeType | SemanticEdgeType;\n\n/** Fields required (or optional) when inserting a graph edge. */\nexport interface GraphEdgeInsert {\n agent_id: string;\n source_id: string;\n source_type: GraphNodeType;\n target_id: string;\n target_type: GraphNodeType;\n type: GraphEdgeType;\n created_at: number;\n session_id?: string;\n confidence?: number;\n properties?: string;\n}\n\n/** Row shape returned from graph edge queries. */\nexport interface GraphEdgeRow {\n id: string;\n agent_id: string;\n source_id: string;\n source_type: string;\n target_id: string;\n target_type: string;\n type: string;\n session_id: string | null;\n confidence: number;\n properties: string | null;\n created_at: number;\n}\n\n/** Filter options for `listGraphEdges`. */\nexport interface ListGraphEdgesOptions {\n sourceId?: string;\n targetId?: string;\n type?: string;\n agentId?: string;\n limit?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Column list\n// ---------------------------------------------------------------------------\n\nconst GRAPH_EDGE_COLUMNS = [\n 'id',\n 'agent_id',\n 'source_id',\n 'source_type',\n 'target_id',\n 'target_type',\n 'type',\n 'session_id',\n 'confidence',\n 'properties',\n 'created_at',\n] as const;\n\nconst SELECT_COLUMNS = GRAPH_EDGE_COLUMNS.join(', ');\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Normalize a SQLite result row into a typed GraphEdgeRow. */\nfunction toGraphEdgeRow(row: Record<string, unknown>): GraphEdgeRow {\n return {\n id: row.id as string,\n agent_id: row.agent_id as string,\n source_id: row.source_id as string,\n source_type: row.source_type as string,\n target_id: row.target_id as string,\n target_type: row.target_type as string,\n type: row.type as string,\n session_id: (row.session_id as string) ?? null,\n confidence: row.confidence as number,\n properties: (row.properties as string) ?? null,\n created_at: row.created_at as number,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Insert a new graph edge.\n *\n * Generates a UUID id automatically.\n */\nexport function insertGraphEdge(data: GraphEdgeInsert): GraphEdgeRow {\n const db = getDatabase();\n const id = crypto.randomUUID();\n\n db.prepare(\n `INSERT INTO graph_edges (\n id, agent_id, source_id, source_type, target_id, target_type,\n type, session_id, confidence, properties, created_at\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n ).run(\n id,\n data.agent_id,\n data.source_id,\n data.source_type,\n data.target_id,\n data.target_type,\n data.type,\n data.session_id ?? null,\n data.confidence ?? GRAPH_EDGE_DEFAULT_CONFIDENCE,\n data.properties ?? null,\n data.created_at,\n );\n\n return toGraphEdgeRow(\n db.prepare(`SELECT ${SELECT_COLUMNS} FROM graph_edges WHERE id = ?`).get(id) as Record<string, unknown>,\n );\n}\n\n/**\n * List graph edges with optional filters, ordered by created_at DESC.\n */\nexport function listGraphEdges(\n options: ListGraphEdgesOptions = {},\n): GraphEdgeRow[] {\n const db = getDatabase();\n\n const conditions: string[] = [];\n const params: unknown[] = [];\n\n if (options.sourceId !== undefined) {\n conditions.push(`source_id = ?`);\n params.push(options.sourceId);\n }\n\n if (options.targetId !== undefined) {\n conditions.push(`target_id = ?`);\n params.push(options.targetId);\n }\n\n if (options.type !== undefined) {\n conditions.push(`type = ?`);\n params.push(options.type);\n }\n\n if (options.agentId !== undefined) {\n conditions.push(`agent_id = ?`);\n params.push(options.agentId);\n }\n\n const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';\n const limit = options.limit ?? QUERY_DEFAULT_LIST_LIMIT;\n\n params.push(limit);\n\n const rows = db.prepare(\n `SELECT ${SELECT_COLUMNS}\n FROM graph_edges\n ${where}\n ORDER BY created_at DESC\n LIMIT ?`,\n ).all(...params) as Record<string, unknown>[];\n\n return rows.map(toGraphEdgeRow);\n}\n\n/**\n * BFS traversal from a node across graph edges.\n *\n * Returns all edges reachable within `depth` hops from the starting node.\n *\n * @param nodeId - The starting node ID.\n * @param nodeType - The starting node type.\n * @param options - Optional depth limit (default 2, max 5).\n */\nexport function getGraphForNode(\n nodeId: string,\n nodeType: GraphNodeType,\n options?: { depth?: number },\n): { edges: GraphEdgeRow[] } {\n const db = getDatabase();\n const depth = Math.min(Math.max(options?.depth ?? DEFAULT_BFS_DEPTH, 1), MAX_BFS_DEPTH);\n\n const seenEdgeIds = new Set<string>();\n const collectedEdges: GraphEdgeRow[] = [];\n const visited = new Set<string>([`${nodeType}:${nodeId}`]);\n let frontier = new Set<string>([nodeId]);\n\n for (let hop = 0; hop < depth; hop++) {\n if (frontier.size === 0) break;\n\n const frontierArray = Array.from(frontier);\n const placeholders = frontierArray.map(() => `?`).join(', ');\n\n const rows = db.prepare(\n `SELECT ${SELECT_COLUMNS}\n FROM graph_edges\n WHERE source_id IN (${placeholders}) OR target_id IN (${placeholders})`,\n ).all(...frontierArray, ...frontierArray) as Record<string, unknown>[];\n\n const nextFrontier = new Set<string>();\n\n for (const row of rows) {\n const edge = toGraphEdgeRow(row);\n if (!seenEdgeIds.has(edge.id)) {\n seenEdgeIds.add(edge.id);\n collectedEdges.push(edge);\n }\n const sourceKey = `${edge.source_type}:${edge.source_id}`;\n const targetKey = `${edge.target_type}:${edge.target_id}`;\n if (!visited.has(sourceKey)) {\n visited.add(sourceKey);\n nextFrontier.add(edge.source_id);\n }\n if (!visited.has(targetKey)) {\n visited.add(targetKey);\n nextFrontier.add(edge.target_id);\n }\n }\n\n frontier = nextFrontier;\n }\n\n return { edges: collectedEdges };\n}\n","/**\n * Lineage edge creation helpers.\n *\n * Creates automatic graph edges when spores and batches are inserted.\n * These are structural (no LLM needed) — the daemon layer calls them.\n *\n * Edge types created:\n * - FROM_SESSION: spore -> session (the session it was extracted from)\n * - EXTRACTED_FROM: spore -> batch (the prompt batch it was extracted from)\n * - DERIVED_FROM: wisdom spore -> source spore (consolidation provenance)\n * - HAS_BATCH: session -> batch (prompt batch belongs to session)\n */\n\nimport { insertGraphEdge } from './graph-edges.js';\nimport {\n EDGE_TYPE_FROM_SESSION,\n EDGE_TYPE_EXTRACTED_FROM,\n EDGE_TYPE_DERIVED_FROM,\n EDGE_TYPE_HAS_BATCH,\n} from '@myco/constants.js';\n\n// ---------------------------------------------------------------------------\n// Spore lineage\n// ---------------------------------------------------------------------------\n\n/** Create lineage edges for a newly inserted spore. */\nexport function createSporeLineage(spore: {\n id: string;\n agent_id: string;\n session_id?: string | null;\n prompt_batch_id?: number | null;\n observation_type?: string;\n properties?: string | null;\n created_at: number;\n}): void {\n if (spore.session_id) {\n insertGraphEdge({\n agent_id: spore.agent_id,\n source_id: spore.id,\n source_type: 'spore',\n target_id: spore.session_id,\n target_type: 'session',\n type: EDGE_TYPE_FROM_SESSION,\n created_at: spore.created_at,\n });\n }\n\n if (spore.prompt_batch_id != null) {\n insertGraphEdge({\n agent_id: spore.agent_id,\n source_id: spore.id,\n source_type: 'spore',\n target_id: String(spore.prompt_batch_id),\n target_type: 'batch',\n type: EDGE_TYPE_EXTRACTED_FROM,\n created_at: spore.created_at,\n });\n }\n\n // DERIVED_FROM edges for wisdom spores\n if (spore.observation_type === 'wisdom' && spore.properties) {\n try {\n const props = JSON.parse(spore.properties);\n if (Array.isArray(props.consolidated_from)) {\n for (const sourceId of props.consolidated_from) {\n insertGraphEdge({\n agent_id: spore.agent_id,\n source_id: spore.id,\n source_type: 'spore',\n target_id: sourceId,\n target_type: 'spore',\n type: EDGE_TYPE_DERIVED_FROM,\n created_at: spore.created_at,\n });\n }\n }\n } catch { /* ignore malformed properties */ }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Batch lineage\n// ---------------------------------------------------------------------------\n\n/** Create a HAS_BATCH lineage edge from session to batch. */\nexport function createBatchLineage(\n agentId: string,\n sessionId: string,\n batchId: number,\n createdAt: number,\n): void {\n insertGraphEdge({\n agent_id: agentId,\n source_id: sessionId,\n source_type: 'session',\n target_id: String(batchId),\n target_type: 'batch',\n type: EDGE_TYPE_HAS_BATCH,\n created_at: createdAt,\n });\n}\n","/**\n * Entity CRUD query helpers for the knowledge graph.\n *\n * All functions obtain the SQLite instance internally via `getDatabase()`.\n * Queries use positional `?` placeholders throughout (better-sqlite3).\n */\n\nimport { getDatabase } from '@myco/db/client.js';\nimport { getGraphForNode, type GraphEdgeRow } from '@myco/db/queries/graph-edges.js';\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Default number of entities returned by listEntities when no limit given. */\nconst DEFAULT_LIST_LIMIT = 100;\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Fields required (or optional) when inserting an entity. */\nexport interface EntityInsert {\n id: string;\n agent_id: string;\n type: string;\n name: string;\n first_seen: number;\n last_seen: number;\n properties?: string | null;\n}\n\n/** Row shape returned from entity queries (all columns). */\nexport interface EntityRow {\n id: string;\n agent_id: string;\n type: string;\n name: string;\n properties: string | null;\n first_seen: number;\n last_seen: number;\n status: string;\n}\n\n/** Filter options for `listEntities`. */\nexport interface ListEntitiesOptions {\n agent_id?: string;\n type?: string;\n /** Filter by exact entity name. */\n name?: string;\n /** Filter by status (default 'active'). */\n status?: string;\n /** Filter by entity_mentions subquery — must be paired with note_type. */\n mentioned_in?: string;\n /** Required when mentioned_in is provided. */\n note_type?: string;\n limit?: number;\n offset?: number;\n}\n\n/** Return type for `getEntityWithEdges`. */\nexport interface EntityGraph {\n center: EntityRow;\n nodes: EntityRow[];\n edges: GraphEdgeRow[];\n}\n\n// ---------------------------------------------------------------------------\n// Column list\n// ---------------------------------------------------------------------------\n\nconst ENTITY_COLUMNS = [\n 'id',\n 'agent_id',\n 'type',\n 'name',\n 'properties',\n 'first_seen',\n 'last_seen',\n 'status',\n] as const;\n\nconst SELECT_COLUMNS = ENTITY_COLUMNS.join(', ');\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Normalize a SQLite result row into a typed EntityRow. */\nfunction toEntityRow(row: Record<string, unknown>): EntityRow {\n return {\n id: row.id as string,\n agent_id: row.agent_id as string,\n type: row.type as string,\n name: row.name as string,\n properties: (row.properties as string) ?? null,\n first_seen: row.first_seen as number,\n last_seen: row.last_seen as number,\n status: (row.status as string) ?? 'active',\n };\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Insert or update an entity. Uses UPSERT on (agent_id, type, name).\n *\n * On conflict, updates properties (if provided) and last_seen.\n */\nexport function insertEntity(data: EntityInsert): EntityRow {\n const db = getDatabase();\n\n db.prepare(\n `INSERT INTO entities (id, agent_id, type, name, properties, first_seen, last_seen)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (agent_id, type, name) DO UPDATE SET\n properties = COALESCE(EXCLUDED.properties, entities.properties),\n last_seen = EXCLUDED.last_seen`,\n ).run(\n data.id,\n data.agent_id,\n data.type,\n data.name,\n data.properties ?? null,\n data.first_seen,\n data.last_seen,\n );\n\n // On conflict, the passed-in id may not be the actual row id. Look up by unique key.\n return toEntityRow(\n db.prepare(`SELECT ${SELECT_COLUMNS} FROM entities WHERE agent_id = ? AND type = ? AND name = ?`).get(\n data.agent_id,\n data.type,\n data.name,\n ) as Record<string, unknown>,\n );\n}\n\n/**\n * Retrieve a single entity by id.\n *\n * @returns the entity row, or null if not found.\n */\nexport function getEntity(id: string): EntityRow | null {\n const db = getDatabase();\n\n const row = db.prepare(\n `SELECT ${SELECT_COLUMNS} FROM entities WHERE id = ?`,\n ).get(id) as Record<string, unknown> | undefined;\n\n if (!row) return null;\n return toEntityRow(row);\n}\n\n/**\n * List entities with optional filters, ordered by last_seen DESC.\n *\n * Defaults to `status = 'active'` — archived entities are excluded unless\n * `status` is explicitly provided. Pass `status: undefined` in options to\n * get only active entities (the default), or set a specific status string.\n *\n * When both `mentioned_in` and `note_type` are provided, filters to entities\n * referenced in a specific note via the entity_mentions subquery.\n */\nexport function listEntities(\n options: ListEntitiesOptions = {},\n): EntityRow[] {\n const db = getDatabase();\n\n const conditions: string[] = [];\n const params: unknown[] = [];\n\n if (options.agent_id !== undefined) {\n conditions.push(`agent_id = ?`);\n params.push(options.agent_id);\n }\n\n if (options.type !== undefined) {\n conditions.push(`type = ?`);\n params.push(options.type);\n }\n\n if (options.name !== undefined) {\n conditions.push(`name = ?`);\n params.push(options.name);\n }\n\n // Default: only show active entities (status column added in v5)\n if (options.status !== undefined) {\n conditions.push(`status = ?`);\n params.push(options.status);\n } else {\n conditions.push(`status = ?`);\n params.push('active');\n }\n\n if (options.mentioned_in !== undefined && options.note_type !== undefined) {\n conditions.push(\n `id IN (SELECT entity_id FROM entity_mentions WHERE note_id = ? AND note_type = ?)`,\n );\n params.push(options.mentioned_in);\n params.push(options.note_type);\n }\n\n const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';\n const limit = options.limit ?? DEFAULT_LIST_LIMIT;\n const offset = options.offset ?? 0;\n\n params.push(limit);\n params.push(offset);\n\n const rows = db.prepare(\n `SELECT ${SELECT_COLUMNS}\n FROM entities\n ${where}\n ORDER BY last_seen DESC\n LIMIT ?\n OFFSET ?`,\n ).all(...params) as Record<string, unknown>[];\n\n return rows.map(toEntityRow);\n}\n\n/**\n * Fetch an entity and its surrounding graph via BFS traversal.\n *\n * Delegates to `getGraphForNode` (graph_edges table) for the BFS,\n * then fetches entity rows for all connected entity nodes.\n *\n * @param entityId - The center entity to expand from.\n * @param depth - Number of hops to traverse (1-3, default 1).\n * @returns `{ center, nodes, edges }` where nodes are all connected entities\n * (excluding center) and edges are deduplicated across BFS iterations.\n */\nexport function getEntityWithEdges(\n entityId: string,\n depth = 1,\n): EntityGraph | null {\n const db = getDatabase();\n\n const center = getEntity(entityId);\n if (center === null) return null;\n\n const clampedDepth = Math.min(Math.max(depth, 1), 3);\n const graph = getGraphForNode(entityId, 'entity', { depth: clampedDepth });\n\n // Collect all entity node IDs from edges (excluding center)\n const nodeIdSet = new Set<string>();\n for (const edge of graph.edges) {\n if (edge.source_type === 'entity' && edge.source_id !== entityId) nodeIdSet.add(edge.source_id);\n if (edge.target_type === 'entity' && edge.target_id !== entityId) nodeIdSet.add(edge.target_id);\n }\n\n // Fetch all connected entity nodes\n const nodeIds = Array.from(nodeIdSet);\n let nodes: EntityRow[] = [];\n if (nodeIds.length > 0) {\n const placeholders = nodeIds.map(() => `?`).join(', ');\n const nodeRows = db.prepare(\n `SELECT ${SELECT_COLUMNS} FROM entities WHERE id IN (${placeholders})`,\n ).all(...nodeIds) as Record<string, unknown>[];\n nodes = nodeRows.map(toEntityRow);\n }\n\n return { center, nodes, edges: graph.edges };\n}\n","/**\n * Digest extract CRUD query helpers.\n *\n * All functions obtain the SQLite instance internally via `getDatabase()`.\n * Queries use positional `?` placeholders throughout (better-sqlite3).\n */\n\nimport { getDatabase } from '@myco/db/client.js';\nimport { DIGEST_TIERS } from '@myco/constants.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Fields required when upserting a digest extract. */\nexport interface DigestExtractUpsert {\n agent_id: string;\n tier: number;\n content: string;\n generated_at: number;\n}\n\n/** Row shape returned from digest_extracts queries (all columns). */\nexport interface DigestExtractRow {\n id: number;\n agent_id: string;\n tier: number;\n content: string;\n substrate_hash: string | null;\n generated_at: number;\n}\n\n// ---------------------------------------------------------------------------\n// Column list\n// ---------------------------------------------------------------------------\n\nconst EXTRACT_COLUMNS = [\n 'id',\n 'agent_id',\n 'tier',\n 'content',\n 'substrate_hash',\n 'generated_at',\n] as const;\n\nconst SELECT_COLUMNS = EXTRACT_COLUMNS.join(', ');\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Normalize a SQLite result row into a typed DigestExtractRow. */\nfunction toDigestExtractRow(row: Record<string, unknown>): DigestExtractRow {\n return {\n id: row.id as number,\n agent_id: row.agent_id as string,\n tier: row.tier as number,\n content: row.content as string,\n substrate_hash: (row.substrate_hash as string) ?? null,\n generated_at: row.generated_at as number,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Upsert a digest extract. Uses ON CONFLICT on (agent_id, tier).\n *\n * Creates or updates the extract for the given agent and token tier.\n * Uses lastInsertRowid for SERIAL PK on insert, or falls back to\n * SELECT for the conflict (update) case.\n */\nexport function upsertDigestExtract(\n data: DigestExtractUpsert,\n): DigestExtractRow {\n const db = getDatabase();\n\n db.prepare(\n `INSERT INTO digest_extracts (agent_id, tier, content, generated_at)\n VALUES (?, ?, ?, ?)\n ON CONFLICT (agent_id, tier) DO UPDATE SET\n content = EXCLUDED.content,\n generated_at = EXCLUDED.generated_at`,\n ).run(data.agent_id, data.tier, data.content, data.generated_at);\n\n // Always look up by composite unique key — works for both insert and update cases.\n const row = db.prepare(\n `SELECT ${SELECT_COLUMNS} FROM digest_extracts WHERE agent_id = ? AND tier = ?`,\n ).get(data.agent_id, data.tier);\n\n return toDigestExtractRow(row as Record<string, unknown>);\n}\n\n/**\n * Get a digest extract for a specific agent and tier.\n *\n * @returns the extract row, or null if not found.\n */\nexport function getDigestExtract(\n agentId: string,\n tier: number,\n): DigestExtractRow | null {\n const db = getDatabase();\n\n const row = db.prepare(\n `SELECT ${SELECT_COLUMNS} FROM digest_extracts\n WHERE agent_id = ? AND tier = ?`,\n ).get(agentId, tier) as Record<string, unknown> | undefined;\n\n if (!row) return null;\n return toDigestExtractRow(row);\n}\n\n/**\n * List digest extracts for an agent, filtered to configured tiers, ordered by tier ASC.\n */\nexport function listDigestExtracts(\n agentId: string,\n): DigestExtractRow[] {\n const db = getDatabase();\n const tierPlaceholders = DIGEST_TIERS.map(() => '?').join(', ');\n\n const rows = db.prepare(\n `SELECT ${SELECT_COLUMNS}\n FROM digest_extracts\n WHERE agent_id = ? AND tier IN (${tierPlaceholders})\n ORDER BY tier ASC`,\n ).all(agentId, ...DIGEST_TIERS) as Record<string, unknown>[];\n\n return rows.map(toDigestExtractRow);\n}\n"],"mappings":"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","names":["SELECT_COLUMNS","DEFAULT_STATUS","STATUS_COMPLETED","SELECT_COLUMNS","SELECT_COLUMNS","SELECT_COLUMNS","DEFAULT_LIST_LIMIT","SELECT_COLUMNS","SELECT_COLUMNS"]}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
findPackageRoot
|
|
4
|
+
} from "./chunk-LPUQPDC2.js";
|
|
5
|
+
|
|
6
|
+
// src/version.ts
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
var cached;
|
|
11
|
+
function getPluginVersion() {
|
|
12
|
+
if (cached) return cached;
|
|
13
|
+
if (true) {
|
|
14
|
+
cached = "0.9.0";
|
|
15
|
+
return cached;
|
|
16
|
+
}
|
|
17
|
+
const root = findPackageRoot(path.dirname(fileURLToPath(import.meta.url)));
|
|
18
|
+
if (root) {
|
|
19
|
+
try {
|
|
20
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf-8"));
|
|
21
|
+
if (pkg.version) {
|
|
22
|
+
cached = pkg.version;
|
|
23
|
+
return cached;
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
cached = "0.0.0";
|
|
29
|
+
return cached;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export {
|
|
33
|
+
getPluginVersion
|
|
34
|
+
};
|
|
35
|
+
//# sourceMappingURL=chunk-PB6TOLRQ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/version.ts"],"sourcesContent":["/**\n * Plugin version — injected at build time by tsup define.\n * Falls back to reading package.json for unbundled execution (tests, tsx).\n */\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { findPackageRoot } from './utils/find-package-root.js';\n\ndeclare const __MYCO_VERSION__: string;\n\nlet cached: string | undefined;\n\nexport function getPluginVersion(): string {\n if (cached) return cached;\n\n // Primary: build-time injected constant\n if (typeof __MYCO_VERSION__ !== 'undefined') {\n cached = __MYCO_VERSION__;\n return cached;\n }\n\n // Fallback: read package.json from package root (unbundled/test execution)\n const root = findPackageRoot(path.dirname(fileURLToPath(import.meta.url)));\n if (root) {\n try {\n const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf-8')) as { version?: string };\n if (pkg.version) { cached = pkg.version; return cached; }\n } catch { /* continue */ }\n }\n\n cached = '0.0.0';\n return cached;\n}\n"],"mappings":";;;;;;AAIA,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,SAAS,qBAAqB;AAK9B,IAAI;AAEG,SAAS,mBAA2B;AACzC,MAAI,OAAQ,QAAO;AAGnB,MAAI,MAAyC;AAC3C,aAAS;AACT,WAAO;AAAA,EACT;AAGA,QAAM,OAAO,gBAAgB,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC,CAAC;AACzE,MAAI,MAAM;AACR,QAAI;AACF,YAAM,MAAM,KAAK,MAAM,GAAG,aAAa,KAAK,KAAK,MAAM,cAAc,GAAG,OAAO,CAAC;AAChF,UAAI,IAAI,SAAS;AAAE,iBAAS,IAAI;AAAS,eAAO;AAAA,MAAQ;AAAA,IAC1D,QAAQ;AAAA,IAAiB;AAAA,EAC3B;AAEA,WAAS;AACT,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
getDatabase
|
|
4
|
+
} from "./chunk-MYX5NCRH.js";
|
|
5
|
+
import {
|
|
6
|
+
SEARCH_PREVIEW_CHARS,
|
|
7
|
+
SEARCH_RESULTS_DEFAULT_LIMIT
|
|
8
|
+
} from "./chunk-5VZ52A4T.js";
|
|
9
|
+
|
|
10
|
+
// src/db/queries/search.ts
|
|
11
|
+
function fullTextSearch(query, options = {}) {
|
|
12
|
+
const db = getDatabase();
|
|
13
|
+
const limit = options.limit ?? SEARCH_RESULTS_DEFAULT_LIMIT;
|
|
14
|
+
const typeFilter = options.type;
|
|
15
|
+
const results = [];
|
|
16
|
+
if (typeFilter === void 0 || typeFilter === "prompt_batch") {
|
|
17
|
+
const batchRows = db.prepare(
|
|
18
|
+
`SELECT pb.id, pb.prompt_number, pb.session_id,
|
|
19
|
+
substr(COALESCE(pb.user_prompt, ''), 1, ?) AS preview,
|
|
20
|
+
fts.rank
|
|
21
|
+
FROM prompt_batches_fts fts
|
|
22
|
+
JOIN prompt_batches pb ON pb.id = fts.rowid
|
|
23
|
+
WHERE prompt_batches_fts MATCH ?
|
|
24
|
+
ORDER BY fts.rank
|
|
25
|
+
LIMIT ?`
|
|
26
|
+
).all(SEARCH_PREVIEW_CHARS, query, limit);
|
|
27
|
+
for (const row of batchRows) {
|
|
28
|
+
results.push({
|
|
29
|
+
id: String(row.id),
|
|
30
|
+
type: "prompt_batch",
|
|
31
|
+
title: row.prompt_number != null ? `Batch #${row.prompt_number}` : `Batch ${row.id}`,
|
|
32
|
+
preview: row.preview,
|
|
33
|
+
score: Math.abs(row.rank),
|
|
34
|
+
...row.session_id != null ? { session_id: row.session_id } : {}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (typeFilter === void 0 || typeFilter === "activity") {
|
|
39
|
+
const activityRows = db.prepare(
|
|
40
|
+
`SELECT a.id, a.tool_name, a.tool_input, a.file_path, a.session_id,
|
|
41
|
+
fts.rank
|
|
42
|
+
FROM activities_fts fts
|
|
43
|
+
JOIN activities a ON a.id = fts.rowid
|
|
44
|
+
WHERE activities_fts MATCH ?
|
|
45
|
+
ORDER BY fts.rank
|
|
46
|
+
LIMIT ?`
|
|
47
|
+
).all(query, limit);
|
|
48
|
+
for (const row of activityRows) {
|
|
49
|
+
const preview = (row.tool_input ?? row.file_path ?? "").slice(0, SEARCH_PREVIEW_CHARS);
|
|
50
|
+
results.push({
|
|
51
|
+
id: String(row.id),
|
|
52
|
+
type: "activity",
|
|
53
|
+
title: row.tool_name,
|
|
54
|
+
preview,
|
|
55
|
+
score: Math.abs(row.rank),
|
|
56
|
+
...row.session_id != null ? { session_id: row.session_id } : {}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
results.sort((a, b) => b.score - a.score);
|
|
61
|
+
return results.slice(0, limit);
|
|
62
|
+
}
|
|
63
|
+
function hydrateSearchResults(vectorResults) {
|
|
64
|
+
if (vectorResults.length === 0) return [];
|
|
65
|
+
const db = getDatabase();
|
|
66
|
+
const results = [];
|
|
67
|
+
const byNamespace = /* @__PURE__ */ new Map();
|
|
68
|
+
for (const vr of vectorResults) {
|
|
69
|
+
const group = byNamespace.get(vr.namespace) ?? [];
|
|
70
|
+
group.push(vr);
|
|
71
|
+
byNamespace.set(vr.namespace, group);
|
|
72
|
+
}
|
|
73
|
+
const sessionStmt = db.prepare(
|
|
74
|
+
`SELECT id, title, summary FROM sessions WHERE id IN (SELECT value FROM json_each(?))`
|
|
75
|
+
);
|
|
76
|
+
const sporeStmt = db.prepare(
|
|
77
|
+
`SELECT id, observation_type, content, session_id FROM spores WHERE id IN (SELECT value FROM json_each(?))`
|
|
78
|
+
);
|
|
79
|
+
const planStmt = db.prepare(
|
|
80
|
+
`SELECT id, title, content FROM plans WHERE id IN (SELECT value FROM json_each(?))`
|
|
81
|
+
);
|
|
82
|
+
const artifactStmt = db.prepare(
|
|
83
|
+
`SELECT id, title, content FROM artifacts WHERE id IN (SELECT value FROM json_each(?))`
|
|
84
|
+
);
|
|
85
|
+
const sessionResults = byNamespace.get("sessions");
|
|
86
|
+
if (sessionResults && sessionResults.length > 0) {
|
|
87
|
+
const ids = sessionResults.map((r) => r.id);
|
|
88
|
+
const rows = sessionStmt.all(JSON.stringify(ids));
|
|
89
|
+
const rowMap = new Map(rows.map((r) => [r.id, r]));
|
|
90
|
+
for (const vr of sessionResults) {
|
|
91
|
+
const row = rowMap.get(vr.id);
|
|
92
|
+
if (!row) continue;
|
|
93
|
+
results.push({
|
|
94
|
+
id: row.id,
|
|
95
|
+
type: "session",
|
|
96
|
+
title: row.title ?? `Session ${row.id.slice(-6)}`,
|
|
97
|
+
preview: (row.summary ?? "").slice(0, SEARCH_PREVIEW_CHARS),
|
|
98
|
+
score: vr.similarity
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const sporeResults = byNamespace.get("spores");
|
|
103
|
+
if (sporeResults && sporeResults.length > 0) {
|
|
104
|
+
const ids = sporeResults.map((r) => r.id);
|
|
105
|
+
const rows = sporeStmt.all(JSON.stringify(ids));
|
|
106
|
+
const rowMap = new Map(rows.map((r) => [r.id, r]));
|
|
107
|
+
for (const vr of sporeResults) {
|
|
108
|
+
const row = rowMap.get(vr.id);
|
|
109
|
+
if (!row) continue;
|
|
110
|
+
results.push({
|
|
111
|
+
id: row.id,
|
|
112
|
+
type: "spore",
|
|
113
|
+
title: row.observation_type,
|
|
114
|
+
preview: row.content.slice(0, SEARCH_PREVIEW_CHARS),
|
|
115
|
+
score: vr.similarity,
|
|
116
|
+
...row.session_id != null ? { session_id: row.session_id } : {}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const planResults = byNamespace.get("plans");
|
|
121
|
+
if (planResults && planResults.length > 0) {
|
|
122
|
+
const ids = planResults.map((r) => r.id);
|
|
123
|
+
const rows = planStmt.all(JSON.stringify(ids));
|
|
124
|
+
const rowMap = new Map(rows.map((r) => [r.id, r]));
|
|
125
|
+
for (const vr of planResults) {
|
|
126
|
+
const row = rowMap.get(vr.id);
|
|
127
|
+
if (!row) continue;
|
|
128
|
+
results.push({
|
|
129
|
+
id: row.id,
|
|
130
|
+
type: "plan",
|
|
131
|
+
title: row.title ?? `Plan ${row.id.slice(-6)}`,
|
|
132
|
+
preview: (row.content ?? "").slice(0, SEARCH_PREVIEW_CHARS),
|
|
133
|
+
score: vr.similarity
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const artifactResults = byNamespace.get("artifacts");
|
|
138
|
+
if (artifactResults && artifactResults.length > 0) {
|
|
139
|
+
const ids = artifactResults.map((r) => r.id);
|
|
140
|
+
const rows = artifactStmt.all(JSON.stringify(ids));
|
|
141
|
+
const rowMap = new Map(rows.map((r) => [r.id, r]));
|
|
142
|
+
for (const vr of artifactResults) {
|
|
143
|
+
const row = rowMap.get(vr.id);
|
|
144
|
+
if (!row) continue;
|
|
145
|
+
results.push({
|
|
146
|
+
id: row.id,
|
|
147
|
+
type: "artifact",
|
|
148
|
+
title: row.title,
|
|
149
|
+
preview: (row.content ?? "").slice(0, SEARCH_PREVIEW_CHARS),
|
|
150
|
+
score: vr.similarity
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
results.sort((a, b) => b.score - a.score);
|
|
155
|
+
return results;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export {
|
|
159
|
+
fullTextSearch,
|
|
160
|
+
hydrateSearchResults
|
|
161
|
+
};
|
|
162
|
+
//# sourceMappingURL=chunk-PT5IC642.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/db/queries/search.ts"],"sourcesContent":["/**\n * Full-text search using SQLite FTS5.\n *\n * Searches prompt_batches and activities via their FTS5 virtual tables.\n * Semantic search (vector similarity) is handled by the external VectorStore —\n * this module covers text-based retrieval only.\n *\n * All queries use parameterized placeholders throughout.\n */\n\nimport { getDatabase } from '@myco/db/client.js';\nimport {\n SEARCH_RESULTS_DEFAULT_LIMIT,\n SEARCH_PREVIEW_CHARS,\n} from '@myco/constants.js';\nimport type { VectorSearchResult } from '@myco/daemon/embedding/types.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** All result types that can appear in search results. */\nexport type SearchResultType =\n | 'session'\n | 'spore'\n | 'plan'\n | 'artifact'\n | 'prompt_batch'\n | 'activity';\n\n/** A single result returned from full-text or semantic search. */\nexport interface SearchResult {\n id: string;\n type: SearchResultType;\n title: string;\n preview: string;\n score: number;\n session_id?: string;\n}\n\n/** Options for fullTextSearch. */\nexport interface SearchOptions {\n /** Restrict results to a single type. */\n type?: string;\n /** Maximum number of results to return (default: SEARCH_RESULTS_DEFAULT_LIMIT). */\n limit?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Full-text search across capture tables using SQLite FTS5.\n *\n * Searches prompt_batches (indexed on user_prompt) and activities (indexed\n * on tool_name, tool_input, file_path). The raw query string is passed\n * directly to FTS5 MATCH — callers should sanitize if needed.\n *\n * FTS5 `rank` values are negative (lower = better match). This function\n * converts them to positive scores via `Math.abs()` so higher = better\n * in the returned results.\n *\n * When `options.type` is specified, only the matching table branch is queried.\n *\n * @param query — search string (FTS5 MATCH syntax)\n * @param options — optional type filter and result limit\n * @returns SearchResult[] ordered by score DESC\n */\nexport function fullTextSearch(\n query: string,\n options: SearchOptions = {},\n): SearchResult[] {\n const db = getDatabase();\n const limit = options.limit ?? SEARCH_RESULTS_DEFAULT_LIMIT;\n const typeFilter = options.type;\n\n const results: SearchResult[] = [];\n\n // -- prompt_batches branch ------------------------------------------------\n if (typeFilter === undefined || typeFilter === 'prompt_batch') {\n const batchRows = db.prepare(\n `SELECT pb.id, pb.prompt_number, pb.session_id,\n substr(COALESCE(pb.user_prompt, ''), 1, ?) AS preview,\n fts.rank\n FROM prompt_batches_fts fts\n JOIN prompt_batches pb ON pb.id = fts.rowid\n WHERE prompt_batches_fts MATCH ?\n ORDER BY fts.rank\n LIMIT ?`\n ).all(SEARCH_PREVIEW_CHARS, query, limit) as Array<{\n id: number;\n prompt_number: number | null;\n session_id: string | null;\n preview: string;\n rank: number;\n }>;\n\n for (const row of batchRows) {\n results.push({\n id: String(row.id),\n type: 'prompt_batch',\n title: row.prompt_number != null\n ? `Batch #${row.prompt_number}`\n : `Batch ${row.id}`,\n preview: row.preview,\n score: Math.abs(row.rank),\n ...(row.session_id != null ? { session_id: row.session_id } : {}),\n });\n }\n }\n\n // -- activities branch ----------------------------------------------------\n if (typeFilter === undefined || typeFilter === 'activity') {\n const activityRows = db.prepare(\n `SELECT a.id, a.tool_name, a.tool_input, a.file_path, a.session_id,\n fts.rank\n FROM activities_fts fts\n JOIN activities a ON a.id = fts.rowid\n WHERE activities_fts MATCH ?\n ORDER BY fts.rank\n LIMIT ?`\n ).all(query, limit) as Array<{\n id: number;\n tool_name: string;\n tool_input: string | null;\n file_path: string | null;\n session_id: string | null;\n rank: number;\n }>;\n\n for (const row of activityRows) {\n const preview = (row.tool_input ?? row.file_path ?? '').slice(0, SEARCH_PREVIEW_CHARS);\n results.push({\n id: String(row.id),\n type: 'activity',\n title: row.tool_name,\n preview,\n score: Math.abs(row.rank),\n ...(row.session_id != null ? { session_id: row.session_id } : {}),\n });\n }\n }\n\n // Sort combined results by score DESC and apply limit.\n results.sort((a, b) => b.score - a.score);\n return results.slice(0, limit);\n}\n\n// ---------------------------------------------------------------------------\n// Hydration — convert VectorSearchResults into SearchResults\n// ---------------------------------------------------------------------------\n\n/** Row shape returned from sessions table for hydration. */\ninterface SessionRow {\n id: string;\n title: string | null;\n summary: string | null;\n session_id?: undefined;\n}\n\n/** Row shape returned from spores table for hydration. */\ninterface SporeRow {\n id: string;\n observation_type: string;\n content: string;\n session_id: string | null;\n}\n\n/** Row shape returned from plans table for hydration. */\ninterface PlanRow {\n id: string;\n title: string | null;\n content: string | null;\n}\n\n/** Row shape returned from artifacts table for hydration. */\ninterface ArtifactRow {\n id: string;\n title: string;\n content: string | null;\n}\n\n/**\n * Hydrate vector search results into SearchResults by fetching full records\n * from the record store.\n *\n * Groups results by namespace, queries each table for the relevant IDs, then\n * maps them into SearchResult format with titles and previews.\n */\nexport function hydrateSearchResults(\n vectorResults: VectorSearchResult[],\n): SearchResult[] {\n if (vectorResults.length === 0) return [];\n\n const db = getDatabase();\n const results: SearchResult[] = [];\n\n // Group result IDs by namespace\n const byNamespace = new Map<string, VectorSearchResult[]>();\n for (const vr of vectorResults) {\n const group = byNamespace.get(vr.namespace) ?? [];\n group.push(vr);\n byNamespace.set(vr.namespace, group);\n }\n\n // Use json_each so the statement text is stable and SQLite can cache the plan.\n const sessionStmt = db.prepare(\n `SELECT id, title, summary FROM sessions WHERE id IN (SELECT value FROM json_each(?))`,\n );\n const sporeStmt = db.prepare(\n `SELECT id, observation_type, content, session_id FROM spores WHERE id IN (SELECT value FROM json_each(?))`,\n );\n const planStmt = db.prepare(\n `SELECT id, title, content FROM plans WHERE id IN (SELECT value FROM json_each(?))`,\n );\n const artifactStmt = db.prepare(\n `SELECT id, title, content FROM artifacts WHERE id IN (SELECT value FROM json_each(?))`,\n );\n\n // --- sessions ---\n const sessionResults = byNamespace.get('sessions');\n if (sessionResults && sessionResults.length > 0) {\n const ids = sessionResults.map((r) => r.id);\n const rows = sessionStmt.all(JSON.stringify(ids)) as SessionRow[];\n\n const rowMap = new Map(rows.map((r) => [r.id, r]));\n for (const vr of sessionResults) {\n const row = rowMap.get(vr.id);\n if (!row) continue;\n results.push({\n id: row.id,\n type: 'session',\n title: row.title ?? `Session ${row.id.slice(-6)}`,\n preview: (row.summary ?? '').slice(0, SEARCH_PREVIEW_CHARS),\n score: vr.similarity,\n });\n }\n }\n\n // --- spores ---\n const sporeResults = byNamespace.get('spores');\n if (sporeResults && sporeResults.length > 0) {\n const ids = sporeResults.map((r) => r.id);\n const rows = sporeStmt.all(JSON.stringify(ids)) as SporeRow[];\n\n const rowMap = new Map(rows.map((r) => [r.id, r]));\n for (const vr of sporeResults) {\n const row = rowMap.get(vr.id);\n if (!row) continue;\n results.push({\n id: row.id,\n type: 'spore',\n title: row.observation_type,\n preview: row.content.slice(0, SEARCH_PREVIEW_CHARS),\n score: vr.similarity,\n ...(row.session_id != null ? { session_id: row.session_id } : {}),\n });\n }\n }\n\n // --- plans ---\n const planResults = byNamespace.get('plans');\n if (planResults && planResults.length > 0) {\n const ids = planResults.map((r) => r.id);\n const rows = planStmt.all(JSON.stringify(ids)) as PlanRow[];\n\n const rowMap = new Map(rows.map((r) => [r.id, r]));\n for (const vr of planResults) {\n const row = rowMap.get(vr.id);\n if (!row) continue;\n results.push({\n id: row.id,\n type: 'plan',\n title: row.title ?? `Plan ${row.id.slice(-6)}`,\n preview: (row.content ?? '').slice(0, SEARCH_PREVIEW_CHARS),\n score: vr.similarity,\n });\n }\n }\n\n // --- artifacts ---\n const artifactResults = byNamespace.get('artifacts');\n if (artifactResults && artifactResults.length > 0) {\n const ids = artifactResults.map((r) => r.id);\n const rows = artifactStmt.all(JSON.stringify(ids)) as ArtifactRow[];\n\n const rowMap = new Map(rows.map((r) => [r.id, r]));\n for (const vr of artifactResults) {\n const row = rowMap.get(vr.id);\n if (!row) continue;\n results.push({\n id: row.id,\n type: 'artifact',\n title: row.title,\n preview: (row.content ?? '').slice(0, SEARCH_PREVIEW_CHARS),\n score: vr.similarity,\n });\n }\n }\n\n // Preserve the original similarity-based ordering from vector search\n results.sort((a, b) => b.score - a.score);\n return results;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
isProcessAlive
|
|
4
|
+
} from "./chunk-KB4DGYIY.js";
|
|
5
|
+
import {
|
|
6
|
+
loadConfig
|
|
7
|
+
} from "./chunk-MHSCMET3.js";
|
|
8
|
+
import {
|
|
9
|
+
getDatabase
|
|
10
|
+
} from "./chunk-MYX5NCRH.js";
|
|
11
|
+
import {
|
|
12
|
+
DIGEST_TIERS
|
|
13
|
+
} from "./chunk-5VZ52A4T.js";
|
|
14
|
+
|
|
15
|
+
// src/db/queries/embeddings.ts
|
|
16
|
+
var EMBEDDABLE_TABLES = ["sessions", "spores", "plans", "artifacts"];
|
|
17
|
+
var EMBEDDABLE_TEXT_COLUMNS = {
|
|
18
|
+
sessions: "summary",
|
|
19
|
+
spores: "content",
|
|
20
|
+
plans: "content",
|
|
21
|
+
artifacts: "content"
|
|
22
|
+
};
|
|
23
|
+
var INVALID_TABLE_MSG = "Invalid table name \u2014 must be one of: sessions, spores, plans, artifacts";
|
|
24
|
+
var DEFAULT_UNEMBEDDED_LIMIT = 100;
|
|
25
|
+
function assertValidTable(table) {
|
|
26
|
+
if (!EMBEDDABLE_TABLES.includes(table)) {
|
|
27
|
+
throw new Error(INVALID_TABLE_MSG);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function markEmbedded(table, id) {
|
|
31
|
+
assertValidTable(table);
|
|
32
|
+
const db = getDatabase();
|
|
33
|
+
db.prepare(`UPDATE ${table} SET embedded = 1 WHERE id = ?`).run(id);
|
|
34
|
+
}
|
|
35
|
+
function clearEmbedded(table, id) {
|
|
36
|
+
assertValidTable(table);
|
|
37
|
+
const db = getDatabase();
|
|
38
|
+
db.prepare(`UPDATE ${table} SET embedded = 0 WHERE id = ?`).run(id);
|
|
39
|
+
}
|
|
40
|
+
function getUnembedded(table, limit = DEFAULT_UNEMBEDDED_LIMIT) {
|
|
41
|
+
assertValidTable(table);
|
|
42
|
+
const db = getDatabase();
|
|
43
|
+
const textCol = EMBEDDABLE_TEXT_COLUMNS[table];
|
|
44
|
+
const contentFilter = table === "sessions" ? " AND summary IS NOT NULL" : "";
|
|
45
|
+
const statusFilter = table === "spores" ? " AND status = 'active'" : "";
|
|
46
|
+
return db.prepare(
|
|
47
|
+
`SELECT id, created_at, ${textCol} AS text
|
|
48
|
+
FROM ${table}
|
|
49
|
+
WHERE embedded = 0${contentFilter}${statusFilter}
|
|
50
|
+
ORDER BY created_at ASC
|
|
51
|
+
LIMIT ?`
|
|
52
|
+
).all(limit);
|
|
53
|
+
}
|
|
54
|
+
function getEmbeddingQueueDepth() {
|
|
55
|
+
const db = getDatabase();
|
|
56
|
+
const queueRow = db.prepare(`
|
|
57
|
+
SELECT
|
|
58
|
+
(SELECT COUNT(*) FROM sessions WHERE embedded = 0 AND summary IS NOT NULL) +
|
|
59
|
+
(SELECT COUNT(*) FROM spores WHERE embedded = 0 AND status = 'active') +
|
|
60
|
+
(SELECT COUNT(*) FROM plans WHERE embedded = 0 AND content IS NOT NULL) +
|
|
61
|
+
(SELECT COUNT(*) FROM artifacts WHERE embedded = 0 AND content IS NOT NULL)
|
|
62
|
+
AS cnt
|
|
63
|
+
`).get();
|
|
64
|
+
const embeddedRow = db.prepare(`
|
|
65
|
+
SELECT
|
|
66
|
+
(SELECT COUNT(*) FROM sessions WHERE embedded = 1) +
|
|
67
|
+
(SELECT COUNT(*) FROM spores WHERE embedded = 1) +
|
|
68
|
+
(SELECT COUNT(*) FROM plans WHERE embedded = 1) +
|
|
69
|
+
(SELECT COUNT(*) FROM artifacts WHERE embedded = 1)
|
|
70
|
+
AS cnt
|
|
71
|
+
`).get();
|
|
72
|
+
const queue_depth = Number(queueRow.cnt ?? 0);
|
|
73
|
+
const embedded_count = Number(embeddedRow.cnt ?? 0);
|
|
74
|
+
return { queue_depth, embedded_count, total: queue_depth + embedded_count };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/services/stats.ts
|
|
78
|
+
import fs from "fs";
|
|
79
|
+
import path from "path";
|
|
80
|
+
var DAEMON_JSON_FILENAME = "daemon.json";
|
|
81
|
+
function countTable(db, table) {
|
|
82
|
+
const row = db.prepare(`SELECT COUNT(*) AS cnt FROM ${table}`).get();
|
|
83
|
+
return Number(row.cnt);
|
|
84
|
+
}
|
|
85
|
+
function gatherStats(vaultDir, options) {
|
|
86
|
+
const db = getDatabase();
|
|
87
|
+
const config = loadConfig(vaultDir);
|
|
88
|
+
const session_count = countTable(db, "sessions");
|
|
89
|
+
const batch_count = countTable(db, "prompt_batches");
|
|
90
|
+
const spore_count = countTable(db, "spores");
|
|
91
|
+
const plan_count = countTable(db, "plans");
|
|
92
|
+
const artifact_count = countTable(db, "artifacts");
|
|
93
|
+
const entity_count = countTable(db, "entities");
|
|
94
|
+
const edge_count = countTable(db, "graph_edges");
|
|
95
|
+
const embeddingStats = getEmbeddingQueueDepth();
|
|
96
|
+
const { queue_depth, embedded_count, total: total_embeddable } = embeddingStats;
|
|
97
|
+
const unprocessedRow = db.prepare(
|
|
98
|
+
"SELECT COUNT(*) AS cnt FROM prompt_batches WHERE processed = 0"
|
|
99
|
+
).get();
|
|
100
|
+
const unprocessed_batches = Number(unprocessedRow.cnt ?? 0);
|
|
101
|
+
const lastRun = db.prepare(
|
|
102
|
+
"SELECT started_at, status FROM agent_runs ORDER BY started_at DESC LIMIT 1"
|
|
103
|
+
).get();
|
|
104
|
+
const last_run_at = lastRun ? lastRun.started_at : null;
|
|
105
|
+
const last_run_status = lastRun ? lastRun.status : null;
|
|
106
|
+
const agentTotalRow = db.prepare(
|
|
107
|
+
"SELECT COUNT(*) AS cnt FROM agent_runs"
|
|
108
|
+
).get();
|
|
109
|
+
const total_runs = Number(agentTotalRow.cnt ?? 0);
|
|
110
|
+
const digestRows = db.prepare(
|
|
111
|
+
"SELECT tier, generated_at FROM digest_extracts ORDER BY tier ASC"
|
|
112
|
+
).all();
|
|
113
|
+
const configuredTiers = new Set(DIGEST_TIERS);
|
|
114
|
+
const activeDigestRows = digestRows.filter((r) => configuredTiers.has(r.tier));
|
|
115
|
+
const tiers_available = activeDigestRows.map((r) => r.tier);
|
|
116
|
+
const freshest_tier = tiers_available.length > 0 ? Math.max(...tiers_available) : null;
|
|
117
|
+
const freshestRow = activeDigestRows.find((r) => r.tier === freshest_tier);
|
|
118
|
+
const generated_at = freshestRow ? freshestRow.generated_at : null;
|
|
119
|
+
let daemonPid = 0;
|
|
120
|
+
let daemonPort = 0;
|
|
121
|
+
let daemonVersion = "";
|
|
122
|
+
let daemonUptimeSeconds = 0;
|
|
123
|
+
const daemonPath = path.join(vaultDir, DAEMON_JSON_FILENAME);
|
|
124
|
+
if (fs.existsSync(daemonPath)) {
|
|
125
|
+
try {
|
|
126
|
+
const info = JSON.parse(fs.readFileSync(daemonPath, "utf-8"));
|
|
127
|
+
daemonPid = info.pid ?? 0;
|
|
128
|
+
daemonPort = info.port ?? 0;
|
|
129
|
+
daemonVersion = info.version ?? "";
|
|
130
|
+
if (typeof info.started === "string" && isProcessAlive(daemonPid)) {
|
|
131
|
+
const startedMs = new Date(info.started).getTime();
|
|
132
|
+
daemonUptimeSeconds = Math.floor((Date.now() - startedMs) / 1e3);
|
|
133
|
+
}
|
|
134
|
+
} catch {
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
daemon: {
|
|
139
|
+
pid: daemonPid,
|
|
140
|
+
port: daemonPort,
|
|
141
|
+
version: daemonVersion,
|
|
142
|
+
uptime_seconds: daemonUptimeSeconds,
|
|
143
|
+
active_sessions: options?.active_sessions ?? []
|
|
144
|
+
},
|
|
145
|
+
vault: {
|
|
146
|
+
path: vaultDir,
|
|
147
|
+
name: path.basename(vaultDir),
|
|
148
|
+
session_count,
|
|
149
|
+
batch_count,
|
|
150
|
+
spore_count,
|
|
151
|
+
plan_count,
|
|
152
|
+
artifact_count,
|
|
153
|
+
entity_count,
|
|
154
|
+
edge_count
|
|
155
|
+
},
|
|
156
|
+
embedding: {
|
|
157
|
+
provider: config.embedding.provider,
|
|
158
|
+
model: config.embedding.model,
|
|
159
|
+
queue_depth,
|
|
160
|
+
embedded_count,
|
|
161
|
+
total_embeddable
|
|
162
|
+
},
|
|
163
|
+
agent: {
|
|
164
|
+
last_run_at,
|
|
165
|
+
last_run_status,
|
|
166
|
+
total_runs
|
|
167
|
+
},
|
|
168
|
+
digest: {
|
|
169
|
+
freshest_tier,
|
|
170
|
+
generated_at,
|
|
171
|
+
tiers_available
|
|
172
|
+
},
|
|
173
|
+
unprocessed_batches
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export {
|
|
178
|
+
EMBEDDABLE_TABLES,
|
|
179
|
+
EMBEDDABLE_TEXT_COLUMNS,
|
|
180
|
+
assertValidTable,
|
|
181
|
+
markEmbedded,
|
|
182
|
+
clearEmbedded,
|
|
183
|
+
getUnembedded,
|
|
184
|
+
getEmbeddingQueueDepth,
|
|
185
|
+
gatherStats
|
|
186
|
+
};
|
|
187
|
+
//# sourceMappingURL=chunk-QIK2XSDQ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/db/queries/embeddings.ts","../src/services/stats.ts"],"sourcesContent":["/**\n * Embedded flag management — tracks which rows have been indexed in the external vector store.\n *\n * All vector storage and similarity search is handled by the external VectorStore.\n * This module only manages the `embedded` flag on relational tables.\n */\n\nimport { getDatabase } from '@myco/db/client.js';\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Tables that participate in vector embedding. */\nexport const EMBEDDABLE_TABLES = ['sessions', 'spores', 'plans', 'artifacts'] as const;\n\n/** TypeScript type for valid embeddable table names. */\nexport type EmbeddableTable = (typeof EMBEDDABLE_TABLES)[number];\n\n/** Per-table column that holds the text content used for embedding. */\nexport const EMBEDDABLE_TEXT_COLUMNS: Record<EmbeddableTable, string> = {\n sessions: 'summary',\n spores: 'content',\n plans: 'content',\n artifacts: 'content',\n};\n\n/** Error message for invalid table names. */\nconst INVALID_TABLE_MSG = 'Invalid table name — must be one of: sessions, spores, plans, artifacts';\n\n/** Default number of rows returned by getUnembedded. */\nconst DEFAULT_UNEMBEDDED_LIMIT = 100;\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Validate that a table name is in the allowlist.\n *\n * @throws if the table name is not one of the embeddable tables.\n */\nexport function assertValidTable(table: string): asserts table is EmbeddableTable {\n if (!(EMBEDDABLE_TABLES as readonly string[]).includes(table)) {\n throw new Error(INVALID_TABLE_MSG);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/** Mark a row as embedded in the external vector store. */\nexport function markEmbedded(table: string, id: string | number): void {\n assertValidTable(table);\n const db = getDatabase();\n db.prepare(`UPDATE ${table} SET embedded = 1 WHERE id = ?`).run(id);\n}\n\n/** Clear the embedded flag (e.g., when vector is removed or needs re-embedding). */\nexport function clearEmbedded(table: string, id: string | number): void {\n assertValidTable(table);\n const db = getDatabase();\n db.prepare(`UPDATE ${table} SET embedded = 0 WHERE id = ?`).run(id);\n}\n\n/** Find rows that have not yet been embedded, oldest first. */\nexport function getUnembedded(\n table: string,\n limit: number = DEFAULT_UNEMBEDDED_LIMIT,\n): Array<{ id: string | number; created_at: number; text: string }> {\n assertValidTable(table);\n const db = getDatabase();\n const textCol = EMBEDDABLE_TEXT_COLUMNS[table as EmbeddableTable];\n const contentFilter = table === 'sessions' ? ' AND summary IS NOT NULL' : '';\n const statusFilter = table === 'spores' ? \" AND status = 'active'\" : '';\n\n return db.prepare(\n `SELECT id, created_at, ${textCol} AS text\n FROM ${table}\n WHERE embedded = 0${contentFilter}${statusFilter}\n ORDER BY created_at ASC\n LIMIT ?`\n ).all(limit) as Array<{ id: string | number; created_at: number; text: string }>;\n}\n\n/** Get aggregated embedding queue depth across all embeddable tables. */\nexport function getEmbeddingQueueDepth(): {\n queue_depth: number;\n embedded_count: number;\n total: number;\n} {\n const db = getDatabase();\n\n const queueRow = db.prepare(`\n SELECT\n (SELECT COUNT(*) FROM sessions WHERE embedded = 0 AND summary IS NOT NULL) +\n (SELECT COUNT(*) FROM spores WHERE embedded = 0 AND status = 'active') +\n (SELECT COUNT(*) FROM plans WHERE embedded = 0 AND content IS NOT NULL) +\n (SELECT COUNT(*) FROM artifacts WHERE embedded = 0 AND content IS NOT NULL)\n AS cnt\n `).get() as { cnt: number };\n\n const embeddedRow = db.prepare(`\n SELECT\n (SELECT COUNT(*) FROM sessions WHERE embedded = 1) +\n (SELECT COUNT(*) FROM spores WHERE embedded = 1) +\n (SELECT COUNT(*) FROM plans WHERE embedded = 1) +\n (SELECT COUNT(*) FROM artifacts WHERE embedded = 1)\n AS cnt\n `).get() as { cnt: number };\n\n const queue_depth = Number(queueRow.cnt ?? 0);\n const embedded_count = Number(embeddedRow.cnt ?? 0);\n return { queue_depth, embedded_count, total: queue_depth + embedded_count };\n}\n","/**\n * Vault statistics — gathered from SQLite.\n */\n\nimport { getDatabase } from '@myco/db/client.js';\nimport { getEmbeddingQueueDepth } from '@myco/db/queries/embeddings.js';\nimport { loadConfig } from '@myco/config/loader.js';\nimport { isProcessAlive } from '@myco/cli/shared.js';\nimport { DIGEST_TIERS } from '@myco/constants.js';\nimport fs from 'node:fs';\nimport path from 'node:path';\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Process uptime is available directly from the daemon process via process.uptime(). */\nconst DAEMON_JSON_FILENAME = 'daemon.json';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface V2Stats {\n daemon: {\n pid: number;\n port: number;\n version: string;\n uptime_seconds: number;\n active_sessions: string[];\n };\n vault: {\n path: string;\n name: string;\n session_count: number;\n batch_count: number;\n spore_count: number;\n plan_count: number;\n artifact_count: number;\n entity_count: number;\n edge_count: number;\n };\n embedding: {\n provider: string;\n model: string;\n queue_depth: number;\n embedded_count: number;\n total_embeddable: number;\n };\n agent: {\n last_run_at: number | null;\n last_run_status: string | null;\n total_runs: number;\n };\n digest: {\n freshest_tier: number | null;\n generated_at: number | null;\n tiers_available: number[];\n };\n unprocessed_batches: number;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Count rows in a table (sync). */\nfunction countTable(db: ReturnType<typeof getDatabase>, table: string): number {\n const row = db.prepare(`SELECT COUNT(*) AS cnt FROM ${table}`).get() as { cnt: number };\n return Number(row.cnt);\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\nexport function gatherStats(vaultDir: string, options?: { active_sessions?: string[] }): V2Stats {\n const db = getDatabase();\n\n // Load config for embedding provider info (sync — already on disk)\n const config = loadConfig(vaultDir);\n\n // All queries are synchronous — no Promise.all needed\n const session_count = countTable(db, 'sessions');\n const batch_count = countTable(db, 'prompt_batches');\n const spore_count = countTable(db, 'spores');\n const plan_count = countTable(db, 'plans');\n const artifact_count = countTable(db, 'artifacts');\n const entity_count = countTable(db, 'entities');\n const edge_count = countTable(db, 'graph_edges');\n\n // Shared embedding queue depth helper (consistent filter logic)\n const embeddingStats = getEmbeddingQueueDepth();\n const { queue_depth, embedded_count, total: total_embeddable } = embeddingStats;\n\n // Unprocessed batches\n const unprocessedRow = db.prepare(\n 'SELECT COUNT(*) AS cnt FROM prompt_batches WHERE processed = 0',\n ).get() as { cnt: number };\n const unprocessed_batches = Number(unprocessedRow.cnt ?? 0);\n\n // Agent: most recent run\n const lastRun = db.prepare(\n 'SELECT started_at, status FROM agent_runs ORDER BY started_at DESC LIMIT 1',\n ).get() as { started_at: number; status: string } | undefined;\n const last_run_at = lastRun ? lastRun.started_at : null;\n const last_run_status = lastRun ? lastRun.status : null;\n\n // Total agent runs\n const agentTotalRow = db.prepare(\n 'SELECT COUNT(*) AS cnt FROM agent_runs',\n ).get() as { cnt: number };\n const total_runs = Number(agentTotalRow.cnt ?? 0);\n\n // Digest extracts: only report tiers that are currently configured\n const digestRows = db.prepare(\n 'SELECT tier, generated_at FROM digest_extracts ORDER BY tier ASC',\n ).all() as Array<{ tier: number; generated_at: number }>;\n const configuredTiers = new Set<number>(DIGEST_TIERS);\n const activeDigestRows = digestRows.filter((r) => configuredTiers.has(r.tier));\n const tiers_available = activeDigestRows.map((r) => r.tier);\n const freshest_tier = tiers_available.length > 0 ? Math.max(...tiers_available) : null;\n const freshestRow = activeDigestRows.find((r) => r.tier === freshest_tier);\n const generated_at = freshestRow ? freshestRow.generated_at : null;\n\n // Daemon info from daemon.json\n let daemonPid = 0;\n let daemonPort = 0;\n let daemonVersion = '';\n let daemonUptimeSeconds = 0;\n const daemonPath = path.join(vaultDir, DAEMON_JSON_FILENAME);\n if (fs.existsSync(daemonPath)) {\n try {\n const info = JSON.parse(fs.readFileSync(daemonPath, 'utf-8')) as Record<string, unknown>;\n daemonPid = (info.pid as number) ?? 0;\n daemonPort = (info.port as number) ?? 0;\n daemonVersion = (info.version as string) ?? '';\n // uptime: if daemon is alive, compute from started timestamp\n if (typeof info.started === 'string' && isProcessAlive(daemonPid)) {\n const startedMs = new Date(info.started as string).getTime();\n daemonUptimeSeconds = Math.floor((Date.now() - startedMs) / 1000);\n }\n } catch { /* ignore corrupt daemon.json */ }\n }\n\n return {\n daemon: {\n pid: daemonPid,\n port: daemonPort,\n version: daemonVersion,\n uptime_seconds: daemonUptimeSeconds,\n active_sessions: options?.active_sessions ?? [],\n },\n vault: {\n path: vaultDir,\n name: path.basename(vaultDir),\n session_count,\n batch_count,\n spore_count,\n plan_count,\n artifact_count,\n entity_count,\n edge_count,\n },\n embedding: {\n provider: config.embedding.provider,\n model: config.embedding.model,\n queue_depth,\n embedded_count,\n total_embeddable,\n },\n agent: {\n last_run_at,\n last_run_status,\n total_runs,\n },\n digest: {\n freshest_tier,\n generated_at,\n tiers_available,\n },\n unprocessed_batches,\n };\n}\n"],"mappings":"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","names":[]}
|