@tekmidian/pai 0.66.6 → 0.66.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/dist/{aibroker-client-WCZi_tn-.mjs → aibroker-client-BfnVwGZl.mjs} +3 -3
  2. package/dist/{aibroker-client-WCZi_tn-.mjs.map → aibroker-client-BfnVwGZl.mjs.map} +1 -1
  3. package/dist/{aibroker-client-Ck3p3k_-.mjs → aibroker-client-C-O3Fflh.mjs} +1 -1
  4. package/dist/{auto-route-BnizyALK.mjs → auto-route-D8TBDNqb.mjs} +3 -3
  5. package/dist/{auto-route-BnizyALK.mjs.map → auto-route-D8TBDNqb.mjs.map} +1 -1
  6. package/dist/auto-route-DCxTKz19.mjs +7 -0
  7. package/dist/{chain-2_uoiEdD.mjs → chain-BL4khGgy.mjs} +4 -3
  8. package/dist/{chain-2_uoiEdD.mjs.map → chain-BL4khGgy.mjs.map} +1 -1
  9. package/dist/cli/index.mjs +20 -16
  10. package/dist/cli/index.mjs.map +1 -1
  11. package/dist/cli/program.mjs +19 -15
  12. package/dist/{config-Ddc4DIPk.mjs → config-DjZELvoA.mjs} +12 -3
  13. package/dist/config-DjZELvoA.mjs.map +1 -0
  14. package/dist/{run-env-CQRnxNht.mjs → config-Dpe2JlbY.mjs} +5 -114
  15. package/dist/config-Dpe2JlbY.mjs.map +1 -0
  16. package/dist/{config-CnsDMETf.mjs → config-yomayAwe.mjs} +1 -1
  17. package/dist/{context-handover-cache-SFkWucuT.mjs → context-handover-cache-DFqxNh-H.mjs} +27 -13
  18. package/dist/context-handover-cache-DFqxNh-H.mjs.map +1 -0
  19. package/dist/daemon/index.mjs +20 -15
  20. package/dist/daemon/index.mjs.map +1 -1
  21. package/dist/{daemon-vLtT3yaI.mjs → daemon-CsR1S_Qr.mjs} +762 -24
  22. package/dist/daemon-CsR1S_Qr.mjs.map +1 -0
  23. package/dist/daemon-WrncnGua.mjs +23 -0
  24. package/dist/daemon-mcp/index.mjs +15 -12
  25. package/dist/daemon-mcp/index.mjs.map +1 -1
  26. package/dist/embedding-gate-DqbKvn56.mjs +378 -0
  27. package/dist/embedding-gate-DqbKvn56.mjs.map +1 -0
  28. package/dist/embedding-gate-SJMCPwxG.mjs +5 -0
  29. package/dist/{embeddings-DMVzpvRZ.mjs → embeddings-BUjZMD9F.mjs} +16 -2
  30. package/dist/{embeddings-DMVzpvRZ.mjs.map → embeddings-BUjZMD9F.mjs.map} +1 -1
  31. package/dist/embeddings-DrJ-ewfi.mjs +3 -0
  32. package/dist/{env-DiolswKQ.mjs → env-CPCNHz3i.mjs} +1 -1
  33. package/dist/{env-DiolswKQ.mjs.map → env-CPCNHz3i.mjs.map} +1 -1
  34. package/dist/factory-DjtXElOI.mjs +9 -0
  35. package/dist/{factory-CXFHwvcv.mjs → factory-yElzJc6f.mjs} +31 -9
  36. package/dist/factory-yElzJc6f.mjs.map +1 -0
  37. package/dist/{fallback-DNzd3jJc.mjs → fallback-2yWDDYiT.mjs} +5 -4
  38. package/dist/{fallback-DNzd3jJc.mjs.map → fallback-2yWDDYiT.mjs.map} +1 -1
  39. package/dist/{helpers-CZsi_49C.mjs → helpers-B9LHRJ8R.mjs} +1 -1
  40. package/dist/{helpers-CZsi_49C.mjs.map → helpers-B9LHRJ8R.mjs.map} +1 -1
  41. package/dist/hooks/block-sleep-poll.mjs +7 -2
  42. package/dist/hooks/block-sleep-poll.mjs.map +3 -3
  43. package/dist/hooks/context-compression-hook.mjs +2 -0
  44. package/dist/hooks/context-compression-hook.mjs.map +2 -2
  45. package/dist/hooks/load-core-context.mjs +5 -5
  46. package/dist/hooks/load-core-context.mjs.map +2 -2
  47. package/dist/hooks/load-project-context.mjs +2 -0
  48. package/dist/hooks/load-project-context.mjs.map +2 -2
  49. package/dist/hooks/post-compact-inject.mjs +2 -0
  50. package/dist/hooks/post-compact-inject.mjs.map +2 -2
  51. package/dist/hooks/route-agents-to-worker.mjs +2 -0
  52. package/dist/hooks/route-agents-to-worker.mjs.map +2 -2
  53. package/dist/hooks/security-validator.mjs +2 -0
  54. package/dist/hooks/security-validator.mjs.map +2 -2
  55. package/dist/hooks/whisper-rules.mjs +2 -0
  56. package/dist/hooks/whisper-rules.mjs.map +2 -2
  57. package/dist/hooks/worker-guard.mjs +82 -5
  58. package/dist/hooks/worker-guard.mjs.map +4 -4
  59. package/dist/hooks/worker-proxy.mjs +2 -0
  60. package/dist/hooks/worker-proxy.mjs.map +2 -2
  61. package/dist/hooks/worker-status-line.mjs +7 -2
  62. package/dist/hooks/worker-status-line.mjs.map +3 -3
  63. package/dist/hooks/worker-supervision.mjs +2 -0
  64. package/dist/hooks/worker-supervision.mjs.map +2 -2
  65. package/dist/index.d.mts.map +1 -1
  66. package/dist/index.mjs +7 -4
  67. package/dist/{ipc-client-DkxRay-o.mjs → ipc-client-DVZ_EGnu.mjs} +2 -2
  68. package/dist/{ipc-client-DkxRay-o.mjs.map → ipc-client-DVZ_EGnu.mjs.map} +1 -1
  69. package/dist/main-resolver-DjQsexge.mjs +8 -0
  70. package/dist/{main-resolver-SfGGg1DB.mjs → main-resolver-Dxvn4Mv8.mjs} +7 -6
  71. package/dist/{main-resolver-SfGGg1DB.mjs.map → main-resolver-Dxvn4Mv8.mjs.map} +1 -1
  72. package/dist/{pai-marker-DXVpFsYz.mjs → pai-marker-1B7iC7oq.mjs} +121 -3
  73. package/dist/pai-marker-1B7iC7oq.mjs.map +1 -0
  74. package/dist/{planner-C-gzg7ye.mjs → planner-CspP59ya.mjs} +13 -10
  75. package/dist/{planner-C-gzg7ye.mjs.map → planner-CspP59ya.mjs.map} +1 -1
  76. package/dist/{postgres-BxmyQHoA.mjs → postgres-CCAGq1qp.mjs} +86 -5
  77. package/dist/postgres-CCAGq1qp.mjs.map +1 -0
  78. package/dist/postgres-CzkEPfAm.mjs +7 -0
  79. package/dist/{program-CfG1T_ko.mjs → program-cWqZI0_L.mjs} +231 -125
  80. package/dist/program-cWqZI0_L.mjs.map +1 -0
  81. package/dist/{query-feedback-BPdapXKd.mjs → query-feedback-GPeVGvIa.mjs} +2 -2
  82. package/dist/{query-feedback-BPdapXKd.mjs.map → query-feedback-GPeVGvIa.mjs.map} +1 -1
  83. package/dist/{reranker-3lnggwgq.mjs → reranker-BTiOia8Y.mjs} +1 -1
  84. package/dist/{reranker-3lnggwgq.mjs.map → reranker-BTiOia8Y.mjs.map} +1 -1
  85. package/dist/{reranker-CZ2mP4cf.mjs → reranker-YwDT2n5e.mjs} +1 -1
  86. package/dist/{router-BaTbc9VX.mjs → router-Bk_ySIZT.mjs} +2 -2
  87. package/dist/{router-BaTbc9VX.mjs.map → router-Bk_ySIZT.mjs.map} +1 -1
  88. package/dist/router-DyiZNI6o.mjs +3 -0
  89. package/dist/{run-ChlIbDJ0.mjs → run-BZNdpUGQ.mjs} +226 -1797
  90. package/dist/run-BZNdpUGQ.mjs.map +1 -0
  91. package/dist/run-env-Cd0Son-Y.mjs +115 -0
  92. package/dist/run-env-Cd0Son-Y.mjs.map +1 -0
  93. package/dist/{pai-home-UncxWxlX.mjs → runtime-paths-DkZoxofH.mjs} +50 -2
  94. package/dist/runtime-paths-DkZoxofH.mjs.map +1 -0
  95. package/dist/{search-CNAGTiJP.mjs → search-cPDj-9fO.mjs} +2 -2
  96. package/dist/{search-CNAGTiJP.mjs.map → search-cPDj-9fO.mjs.map} +1 -1
  97. package/dist/{sources-CM2g-CLT.mjs → sources-CvvzNZad.mjs} +1 -1
  98. package/dist/{sources-CM2g-CLT.mjs.map → sources-CvvzNZad.mjs.map} +1 -1
  99. package/dist/{stop-words-DtxaTWU_.mjs → stop-words-LtkS-Pej.mjs} +1 -1
  100. package/dist/{stop-words-DtxaTWU_.mjs.map → stop-words-LtkS-Pej.mjs.map} +1 -1
  101. package/dist/worktree-CwMRTa0H.mjs +1578 -0
  102. package/dist/worktree-CwMRTa0H.mjs.map +1 -0
  103. package/dist/{zettelkasten-BRIaHzFm.mjs → zettelkasten-DSIe6wpF.mjs} +6 -5
  104. package/dist/zettelkasten-DSIe6wpF.mjs.map +1 -0
  105. package/dist/{zettelkasten-G0-ZTybr.mjs → zettelkasten-DtkAvA18.mjs} +5 -3
  106. package/docker/docker-compose.yml +4 -0
  107. package/docs/commands/README.md +6 -0
  108. package/docs/commands/memory.md +52 -0
  109. package/docs/commands/worker.md +22 -0
  110. package/docs/install-linux.md +1 -0
  111. package/docs/memory.md +69 -0
  112. package/package.json +1 -1
  113. package/src/hooks/ts/lib/sleep-poll-gate.test.ts +2 -2
  114. package/src/hooks/ts/lib/sleep-poll-gate.ts +4 -4
  115. package/src/hooks/ts/lib/worker-guard-longwait.test.ts +48 -0
  116. package/src/hooks/ts/lib/worker-guard.ts +50 -6
  117. package/src/hooks/ts/pre-tool-use/block-sleep-poll.test.ts +1 -1
  118. package/src/hooks/ts/session-start/load-core-context.ts +11 -8
  119. package/src/hooks/ts/session-start/session-start-worker-guard.test.ts +34 -2
  120. package/dist/auto-route-D6fW1Q8z.mjs +0 -3
  121. package/dist/config-Ddc4DIPk.mjs.map +0 -1
  122. package/dist/context-handover-cache-SFkWucuT.mjs.map +0 -1
  123. package/dist/daemon-BHQoEjt0.mjs +0 -18
  124. package/dist/daemon-vLtT3yaI.mjs.map +0 -1
  125. package/dist/embeddings-D0WOQpYR.mjs +0 -3
  126. package/dist/factory-CXFHwvcv.mjs.map +0 -1
  127. package/dist/factory-CnKsU6zX.mjs +0 -8
  128. package/dist/main-resolver-DrHtx8iW.mjs +0 -7
  129. package/dist/pai-home-UncxWxlX.mjs.map +0 -1
  130. package/dist/pai-marker-DXVpFsYz.mjs.map +0 -1
  131. package/dist/postgres-B2BEBT-J.mjs +0 -5
  132. package/dist/postgres-BxmyQHoA.mjs.map +0 -1
  133. package/dist/program-CfG1T_ko.mjs.map +0 -1
  134. package/dist/router-BK-hFeQ6.mjs +0 -3
  135. package/dist/run-ChlIbDJ0.mjs.map +0 -1
  136. package/dist/run-env-CQRnxNht.mjs.map +0 -1
  137. package/dist/runtime-paths-Bw3Jk8qJ.mjs +0 -52
  138. package/dist/runtime-paths-Bw3Jk8qJ.mjs.map +0 -1
  139. package/dist/zettelkasten-BRIaHzFm.mjs.map +0 -1
@@ -1,24 +1,30 @@
1
- import { c as yieldToEventLoop, f as fileContentHash, l as buildOutline, n as chunkId, u as chunkMarkdown } from "./helpers-CZsi_49C.mjs";
2
- import { n as TITLE_STOP_WORDS, t as STOP_WORDS } from "./stop-words-DtxaTWU_.mjs";
3
- import { i as populateSlugs, t as applyRecencyBoost } from "./search-CNAGTiJP.mjs";
4
- import { n as rerankResults } from "./reranker-3lnggwgq.mjs";
5
- import { n as embedChunksWithBackend } from "./postgres-BxmyQHoA.mjs";
6
- import { d as getBackendOutage, n as createRegistryBackend, r as createStorageBackend } from "./factory-CXFHwvcv.mjs";
7
- import { i as generateEmbedding, n as cosineSimilarity$1, r as deserializeEmbedding, t as configureEmbeddingModel } from "./embeddings-DMVzpvRZ.mjs";
8
- import { t as routeNotification } from "./router-BaTbc9VX.mjs";
9
- import { S as DEFAULT_NOTIFICATION_CONFIG, d as readMainConfigRaw, f as writeMainConfigRaw } from "./config-Ddc4DIPk.mjs";
10
- import { $ as shutdownRequested, A as indexInProgress, B as setEmbedInProgress, C as handleSessionSummary, D as daemonConfig, E as cacheKeepaliveTimer, F as notificationConfig, G as setLastIndexTime, H as setIndexInProgress, I as passFailures, It as formatDetectionJson, J as setRegistryBackend, K as setLastVaultIndexTime, L as registryBackend, M as lastEmbedTime, N as lastIndexTime, O as embedInProgress, P as lastVaultIndexTime, Pt as detectProject, Q as setVaultIndexInProgress, R as setCacheKeepaliveTimer, S as getGitContext, T as spawnSummarizer, Tt as performScan, U as setIndexSchedulerTimer, V as setEmbedSchedulerTimer, W as setLastEmbedTime, X as setStartTime, Y as setShutdownRequested, Z as setStorageBackend, _ as markCompleted, _t as buildWakeupContext, at as updateTodoContinue, b as contentToText$2, c as removeDaemonPidfile, d as cleanup, dt as addWorkToSessionNote, et as startTime, f as dequeue, ft as appendCheckpoint, g as loadQueue, gt as findNotesDir, h as hasPendingOrProcessingOfType, ht as archiveSessionFilesToSessionsDir, i as runSessionKeepaliveTick, it as sessionIdFromTranscript, j as indexSchedulerTimer, k as embedSchedulerTimer, m as getStats, mt as getCurrentNotePath, n as writeContextHandoverCache, nt as vaultIndexInProgress, p as enqueue, pt as finalizeSessionNote, q as setNotificationConfig, t as readContextHandoverCache, tt as storageBackend, u as writeDaemonPidfile, v as markFailed, x as findLatestJsonl, xt as runPassChild, yt as PassSpawnError, z as setDaemonConfig } from "./context-handover-cache-SFkWucuT.mjs";
11
- import { D as readWorkersSection } from "./run-env-CQRnxNht.mjs";
12
- import { G as isChatPane, J as loadStatuses, L as appendLedger, S as itermForClaudeSession, V as alive, X as nowStamp, ft as eventsPath, mt as workersLogDir, n as runWorker, pt as ledgerPath, q as loadStatus } from "./run-ChlIbDJ0.mjs";
13
- import { r as saveQueryResult } from "./query-feedback-BPdapXKd.mjs";
14
- import { i as sendToSession } from "./aibroker-client-WCZi_tn-.mjs";
15
- import { n as formatAutoRouteJson, r as detectTopicShift, t as autoRoute } from "./auto-route-BnizyALK.mjs";
16
- import { a as zettelThemes, c as zettelExplore, i as zettelHealth, n as zettelGodNotes, o as zettelConverse, r as zettelSuggest, s as zettelSurprise, t as zettelCommunities } from "./zettelkasten-BRIaHzFm.mjs";
1
+ import { S as DEFAULT_NOTIFICATION_CONFIG, d as readMainConfigRaw, f as writeMainConfigRaw } from "./config-DjZELvoA.mjs";
2
+ import { u as paiHomePath } from "./runtime-paths-DkZoxofH.mjs";
3
+ import { T as readWorkersSection } from "./config-Dpe2JlbY.mjs";
4
+ import { E as isChatPane, O as loadStatus, U as eventsPath, W as ledgerPath, _ as appendLedger, j as nowStamp, k as loadStatuses, q as workersLogDir, x as alive } from "./worktree-CwMRTa0H.mjs";
5
+ import { c as yieldToEventLoop, f as fileContentHash, l as buildOutline, n as chunkId, u as chunkMarkdown } from "./helpers-B9LHRJ8R.mjs";
6
+ import { n as TITLE_STOP_WORDS, t as STOP_WORDS } from "./stop-words-LtkS-Pej.mjs";
7
+ import { i as populateSlugs, t as applyRecencyBoost } from "./search-cPDj-9fO.mjs";
8
+ import { n as rerankResults } from "./reranker-BTiOia8Y.mjs";
9
+ import { c as resolveFromModule, n as embedChunksWithBackend } from "./postgres-CCAGq1qp.mjs";
10
+ import { d as getBackendOutage, n as createRegistryBackend, r as createStorageBackend } from "./factory-yElzJc6f.mjs";
11
+ import { a as requireQueryEmbedding, g as mismatchReason, h as bindingMatches, l as getConfiguredBackend, n as embedQuery } from "./embedding-gate-DqbKvn56.mjs";
12
+ import { i as deserializeEmbedding, n as configureEmbeddingModel, r as cosineSimilarity$1 } from "./embeddings-BUjZMD9F.mjs";
13
+ import { t as routeNotification } from "./router-Bk_ySIZT.mjs";
14
+ import { $ as shutdownRequested, A as indexInProgress, B as setEmbedInProgress, C as handleSessionSummary, D as daemonConfig, E as cacheKeepaliveTimer, F as notificationConfig, G as setLastIndexTime, H as setIndexInProgress, I as passFailures, It as formatDetectionJson, J as setRegistryBackend, K as setLastVaultIndexTime, L as registryBackend, M as lastEmbedTime, N as lastIndexTime, O as embedInProgress, P as lastVaultIndexTime, Pt as detectProject, Q as setVaultIndexInProgress, R as setCacheKeepaliveTimer, S as getGitContext, T as spawnSummarizer, Tt as performScan, U as setIndexSchedulerTimer, V as setEmbedSchedulerTimer, W as setLastEmbedTime, X as setStartTime, Y as setShutdownRequested, Z as setStorageBackend, _ as markCompleted, _t as buildWakeupContext, at as updateTodoContinue, b as contentToText$2, c as removeDaemonPidfile, d as cleanup, dt as addWorkToSessionNote, et as startTime, f as dequeue, ft as appendCheckpoint, g as loadQueue, gt as findNotesDir, h as hasPendingOrProcessingOfType, ht as archiveSessionFilesToSessionsDir, i as runSessionKeepaliveTick, it as sessionIdFromTranscript, j as indexSchedulerTimer, k as embedSchedulerTimer, m as getStats, mt as getCurrentNotePath, n as writeContextHandoverCache, nt as vaultIndexInProgress, p as enqueue, pt as finalizeSessionNote, q as setNotificationConfig, t as readContextHandoverCache, tt as storageBackend, u as writeDaemonPidfile, v as markFailed, x as findLatestJsonl, xt as runPassChild, yt as PassSpawnError, z as setDaemonConfig } from "./context-handover-cache-DFqxNh-H.mjs";
15
+ import { C as itermForClaudeSession, n as runWorker } from "./run-BZNdpUGQ.mjs";
16
+ import { r as saveQueryResult } from "./query-feedback-GPeVGvIa.mjs";
17
+ import { i as sendToSession } from "./aibroker-client-BfnVwGZl.mjs";
18
+ import { n as formatAutoRouteJson, r as detectTopicShift, t as autoRoute } from "./auto-route-D8TBDNqb.mjs";
19
+ import { a as zettelThemes, c as zettelExplore, i as zettelHealth, n as zettelGodNotes, o as zettelConverse, r as zettelSuggest, s as zettelSurprise, t as zettelCommunities } from "./zettelkasten-DSIe6wpF.mjs";
17
20
  import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
18
21
  import { setPriority } from "node:os";
19
22
  import { basename, dirname, isAbsolute, join, normalize, relative, resolve } from "node:path";
23
+ import { execFile, execFileSync } from "node:child_process";
20
24
  import { connect, createServer } from "node:net";
21
- import { execFileSync } from "node:child_process";
25
+ import { createServer as createServer$1 } from "node:http";
26
+ import { promisify } from "node:util";
27
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
22
28
 
23
29
  //#region src/notifications/config.ts
24
30
  /**
@@ -2152,10 +2158,15 @@ async function toolMemorySearch(registry, federation, params, searchDefaults) {
2152
2158
  maxResults: params.limit ?? searchDefaults?.defaultLimit ?? 5
2153
2159
  };
2154
2160
  let results;
2161
+ let embedNote = "";
2155
2162
  if (mode === "keyword") results = await federation.searchKeyword(params.query, searchOpts);
2156
2163
  else if (mode === "semantic" || mode === "hybrid") {
2157
- const queryEmbedding = await generateEmbedding(params.query, true);
2158
- if (mode === "semantic") results = await federation.searchSemantic(queryEmbedding, searchOpts);
2164
+ const q = await embedQuery(federation, params.query);
2165
+ const queryEmbedding = q.vec;
2166
+ if (!queryEmbedding) {
2167
+ embedNote = `\nNote: ${q.note}`;
2168
+ results = await federation.searchKeyword(params.query, searchOpts);
2169
+ } else if (mode === "semantic") results = await federation.searchSemantic(queryEmbedding, searchOpts);
2159
2170
  else {
2160
2171
  const [kwResults, semResults] = await Promise.all([federation.searchKeyword(params.query, {
2161
2172
  ...searchOpts,
@@ -2192,7 +2203,7 @@ async function toolMemorySearch(registry, federation, params, searchDefaults) {
2192
2203
  const withSlugs = await populateSlugs(results, registry);
2193
2204
  if (withSlugs.length === 0) return { content: [{
2194
2205
  type: "text",
2195
- text: `No results found for query: "${params.query}" (mode: ${mode})`
2206
+ text: `No results found for query: "${params.query}" (mode: ${mode})${embedNote}`
2196
2207
  }] };
2197
2208
  const rerankLabel = shouldRerank ? " +rerank" : "";
2198
2209
  const useCompact = params.format === "compact";
@@ -2216,7 +2227,7 @@ async function toolMemorySearch(registry, federation, params, searchDefaults) {
2216
2227
  } catch {}
2217
2228
  return { content: [{
2218
2229
  type: "text",
2219
- text: `Found ${withSlugs.length} result(s) for "${params.query}" (mode: ${mode}${rerankLabel}):\n\n${formatted}`
2230
+ text: `Found ${withSlugs.length} result(s) for "${params.query}" (mode: ${mode}${rerankLabel}):${embedNote}\n\n${formatted}`
2220
2231
  }] };
2221
2232
  } catch (e) {
2222
2233
  return {
@@ -3419,10 +3430,12 @@ async function bfsExpand(federation, entityNames, hops, projectId) {
3419
3430
  */
3420
3431
  async function rerankTriples(triples, queryVec, maxTriples) {
3421
3432
  if (triples.length === 0) return [];
3433
+ const embedder = getConfiguredBackend();
3422
3434
  return (await Promise.all(triples.map(async (triple) => {
3423
3435
  const text = `${triple.subject} ${triple.predicate} ${triple.object}`;
3424
3436
  try {
3425
- const score = cosineSimilarity$1(queryVec, await generateEmbedding(text, false));
3437
+ const [vec] = await embedder.embed([text]);
3438
+ const score = cosineSimilarity$1(queryVec, vec);
3426
3439
  return {
3427
3440
  ...triple,
3428
3441
  relevanceScore: score
@@ -3511,7 +3524,7 @@ async function toolMemoryKgSearch(federation, params) {
3511
3524
  }],
3512
3525
  isError: true
3513
3526
  };
3514
- const result = await graphCompletionSearch(federation, await generateEmbedding(params.query, true), {
3527
+ const result = await graphCompletionSearch(federation, await requireQueryEmbedding(federation, params.query), {
3515
3528
  seedCount: params.wide_k ?? 50,
3516
3529
  hops: params.neighborhood_depth ?? 1,
3517
3530
  maxTriples: params.top_k ?? 20,
@@ -4361,6 +4374,7 @@ async function handleRequest(request, socket) {
4361
4374
  lastVaultIndexTime: lastVaultIndexTime ? new Date(lastVaultIndexTime).toISOString() : null,
4362
4375
  vaultPath: daemonConfig.vaultPath ?? null,
4363
4376
  workQueue: getStats(),
4377
+ dashboard: daemonConfig.dashboard.enabled ? `http://${daemonConfig.dashboard.bind}:${daemonConfig.dashboard.port}/` : null,
4364
4378
  passFailures: Object.fromEntries([...passFailures].map(([k, v]) => [k, {
4365
4379
  ...v,
4366
4380
  at: new Date(v.at).toISOString()
@@ -4819,6 +4833,725 @@ async function handleRequest(request, socket) {
4819
4833
  socket.end();
4820
4834
  }
4821
4835
 
4836
+ //#endregion
4837
+ //#region src/daemon/dashboard-page.ts
4838
+ /** The dashboard page: one self-contained file (inline SVG + CSS), updated in place with textContent/attributes only. */
4839
+ const DASHBOARD_HTML = `<!doctype html>
4840
+ <html lang="en"><head><meta charset="utf-8">
4841
+ <meta name="viewport" content="width=device-width, initial-scale=1">
4842
+ <title>PAI memory</title>
4843
+ <style>
4844
+ :root { color-scheme: light dark; --green:#2e9e5b; --amber:#d99a1c; --red:#d64545; --grey:#8a8f98;
4845
+ --bg:#f2f3f5; --card:#fff; --fg:#1b1d21; --mute:#6b7280; --track:rgba(128,128,128,.25); --new:#2e9e5b; --old:#d99a1c; --miss:#8a8f98; }
4846
+ @media (prefers-color-scheme: dark) { :root { --bg:#111317; --card:#1b1e24; --fg:#e8eaed; --mute:#9aa0a8; } }
4847
+ * { box-sizing: border-box; }
4848
+ body { font: 15px/1.4 -apple-system, system-ui, sans-serif; margin: 0; padding: 12px; background: var(--bg); color: var(--fg); }
4849
+ main { max-width: 1000px; margin: 0 auto; }
4850
+ header { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 12px; }
4851
+ h1 { font-size: 20px; margin: 0; }
4852
+ .pill { color: #fff; font-weight: 700; font-size: 13px; text-transform: uppercase; letter-spacing: .05em; padding: 4px 12px; border-radius: 999px; background: var(--grey); }
4853
+ .pill.green { background: var(--green); } .pill.amber { background: var(--amber); } .pill.red { background: var(--red); }
4854
+ #reason { color: var(--mute); flex: 1 1 200px; }
4855
+ .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
4856
+ .wide { grid-column: 1 / -1; }
4857
+ @media (max-width: 700px) { .grid { grid-template-columns: 1fr; } }
4858
+ .card { background: var(--card); border-radius: 12px; padding: 12px 14px; border-top: 4px solid var(--grey); box-shadow: 0 1px 3px rgba(0,0,0,.12); min-width: 0; }
4859
+ .card.green { border-color: var(--green); } .card.amber { border-color: var(--amber); } .card.red { border-color: var(--red); }
4860
+ h2 { font-size: 12px; margin: 0 0 8px; text-transform: uppercase; letter-spacing: .06em; color: var(--mute); }
4861
+ .k { color: var(--mute); } .bad { color: var(--red); font-weight: 600; }
4862
+ .big { font-size: 34px; font-weight: 700; line-height: 1.1; font-variant-numeric: tabular-nums; }
4863
+ .unit { font-size: 14px; font-weight: 400; color: var(--mute); }
4864
+ .job { display: flex; gap: 16px; align-items: center; flex-wrap: wrap; }
4865
+ .ring { width: 150px; height: 150px; flex: none; }
4866
+ .ring .bg { stroke: var(--track); } .ring .fg { stroke: var(--green); transition: stroke-dashoffset .6s; }
4867
+ .ring text { fill: var(--fg); text-anchor: middle; }
4868
+ .stats { flex: 1 1 200px; min-width: 0; display: grid; gap: 6px; }
4869
+ svg { display: block; }
4870
+ .spark { width: 100%; height: 56px; }
4871
+ .spark polyline { fill: none; stroke: var(--green); stroke-width: 2; vector-effect: non-scaling-stroke; }
4872
+ .spark polygon { fill: var(--green); opacity: .15; }
4873
+ .stack { width: 100%; height: 22px; border-radius: 6px; }
4874
+ .legend { display: grid; gap: 4px; margin: 8px 0; }
4875
+ .legend div { display: flex; align-items: center; gap: 8px; }
4876
+ .legend i { width: 12px; height: 12px; border-radius: 3px; flex: none; }
4877
+ .legend b { margin-left: auto; font-variant-numeric: tabular-nums; }
4878
+ .dot { width: 12px; height: 12px; border-radius: 50%; background: var(--grey); display: inline-block; flex: none; }
4879
+ .dot.green { background: var(--green); } .dot.amber { background: var(--amber); } .dot.red { background: var(--red); }
4880
+ .line { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
4881
+ .gauge { width: 100%; height: 14px; border-radius: 7px; }
4882
+ .pass { padding: 6px 0; border-top: 1px solid var(--track); }
4883
+ .pass:first-of-type { border-top: 0; }
4884
+ .pass .tl { width: 100%; height: 16px; }
4885
+ .pass .tl line { stroke: var(--track); stroke-width: 3; stroke-linecap: round; }
4886
+ .times { display: flex; justify-content: space-between; gap: 8px; color: var(--mute); font-size: 13px; flex-wrap: wrap; }
4887
+ .err { color: var(--red); font-size: 13px; word-break: break-word; }
4888
+ .kv { display: flex; flex-wrap: wrap; gap: 4px 18px; }
4889
+ .kv span span { color: var(--mute); margin-right: 4px; }
4890
+ #foot { color: var(--mute); font-size: 12px; margin-top: 12px; }
4891
+ [hidden] { display: none !important; }
4892
+ </style></head><body><main>
4893
+ <header><h1>PAI memory</h1><span class="pill" id="pill">…</span><span id="reason">loading…</span></header>
4894
+ <div class="grid">
4895
+ <section class="card wide" id="jobs-empty"><h2>Re-embed job</h2><span class="k">no job running</span></section>
4896
+ <div class="wide" id="jobs" style="display:contents"></div>
4897
+
4898
+ <section class="card" id="index"><h2>Index</h2>
4899
+ <div class="line"><span class="big" id="ix-cov">?</span><span class="unit" id="ix-chunks"></span></div>
4900
+ <svg class="stack" id="ix-stack" viewBox="0 0 100 10" preserveAspectRatio="none" role="img" aria-label="vector distribution">
4901
+ <rect id="sg-new" x="0" y="0" width="0" height="10" fill="var(--new)"/>
4902
+ <rect id="sg-old" x="0" y="0" width="0" height="10" fill="var(--old)"/>
4903
+ <rect id="sg-miss" x="0" y="0" width="0" height="10" fill="var(--miss)"/>
4904
+ </svg>
4905
+ <div class="legend">
4906
+ <div id="lg-new"><i style="background:var(--new)"></i><span>new backend</span><b id="n-new">0</b></div>
4907
+ <div id="lg-old"><i style="background:var(--old)"></i><span id="t-old">embedded</span><b id="n-old">0</b></div>
4908
+ <div><i style="background:var(--miss)"></i><span>missing</span><b id="n-miss">0</b></div>
4909
+ </div>
4910
+ <div><span class="k">files</span> <span id="ix-files">?</span></div>
4911
+ <div><span class="k">bound to</span> <span id="ix-bound">?</span>, <span class="k">configured</span> <span id="ix-conf">?</span></div>
4912
+ <div class="bad" id="ix-hint" hidden></div>
4913
+ </section>
4914
+
4915
+ <section class="card" id="backend"><h2>Backend</h2>
4916
+ <div class="line"><span class="dot" id="be-dot"></span><b id="be-id">?</b><span id="be-state" class="k"></span></div>
4917
+ <div class="k" id="be-reason"></div>
4918
+ <div style="margin-top:10px"><span class="k">loaded model</span> <b id="be-model">none</b></div>
4919
+ <div class="line" style="margin-top:8px"><span class="big" id="gpu-pct">?</span><span class="unit">GPU</span></div>
4920
+ <svg class="gauge" viewBox="0 0 100 10" preserveAspectRatio="none" role="img" aria-label="GPU share">
4921
+ <rect x="0" y="0" width="100" height="10" fill="var(--track)"/>
4922
+ <rect id="gpu-bar" x="0" y="0" width="0" height="10" fill="var(--green)"/>
4923
+ </svg>
4924
+ </section>
4925
+
4926
+ <section class="card" id="passes"><h2>Passes</h2><div id="pass-list"></div></section>
4927
+
4928
+ <section class="card" id="daemon"><h2>Daemon</h2>
4929
+ <div class="kv">
4930
+ <span><span>pid</span><b id="d-pid">?</b></span><span><span>uptime</span><b id="d-up">?</b></span>
4931
+ <span><span>version</span><b id="d-ver">?</b></span><span><span>storage</span><b id="d-sto">?</b></span>
4932
+ </div>
4933
+ </section>
4934
+ </div>
4935
+ <div id="foot"></div>
4936
+
4937
+ <template id="job-tpl"><section class="card">
4938
+ <h2></h2>
4939
+ <div class="job">
4940
+ <svg class="ring" viewBox="0 0 120 120" role="img" aria-label="progress">
4941
+ <circle class="bg" cx="60" cy="60" r="52" fill="none" stroke-width="12"/>
4942
+ <circle class="fg" cx="60" cy="60" r="52" fill="none" stroke-width="12" stroke-linecap="round" transform="rotate(-90 60 60)" stroke-dasharray="326.73" stroke-dashoffset="326.73"/>
4943
+ <text class="pct" x="60" y="68" font-size="24" font-weight="700">0%</text>
4944
+ </svg>
4945
+ <div class="stats">
4946
+ <div><span class="big rate">?</span> <span class="unit">chunks/s</span></div>
4947
+ <div><span class="k">done</span> <b class="done">?</b></div>
4948
+ <div><span class="k">ETA</span> <b class="eta">?</b> <span class="k fin"></span></div>
4949
+ <div class="k phase"></div>
4950
+ <div class="bad stall" hidden>STALLED</div>
4951
+ </div>
4952
+ </div>
4953
+ <div class="k" style="margin-top:8px;font-size:12px">rate, last hour</div>
4954
+ <svg class="spark" viewBox="0 0 240 56" preserveAspectRatio="none" role="img" aria-label="rate sparkline">
4955
+ <polygon points=""/><polyline points=""/>
4956
+ </svg>
4957
+ </section></template>
4958
+
4959
+ <script>
4960
+ const $ = (id) => document.getElementById(id);
4961
+ const n = (x) => (x === null || x === undefined ? "?" : Number(x).toLocaleString());
4962
+ const when = (t) => (t ? new Date(t).toLocaleString([], { dateStyle: "short", timeStyle: "short" }) : "never");
4963
+ const clock = (t) => new Date(t).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
4964
+ function dur(secs) {
4965
+ if (secs === null || secs === undefined) return "?";
4966
+ const h = Math.floor(secs / 3600), m = Math.floor((secs % 3600) / 60);
4967
+ return h >= 24 ? Math.floor(h / 24) + "d " + (h % 24) + "h" : h > 0 ? h + "h " + m + "m" : m + "m";
4968
+ }
4969
+ const set = (id, text) => { const e = $(id); if (e.textContent !== text) e.textContent = text; };
4970
+ const lvl = (e, base, level) => { e.className = base + " " + (level || ""); };
4971
+ const CIRC = 326.73;
4972
+
4973
+ function sparkPoints(history) {
4974
+ const r = [];
4975
+ for (let i = 1; i < history.length; i++) {
4976
+ const dt = (history[i].t - history[i - 1].t) / 1000;
4977
+ r.push(dt > 0 ? Math.max(0, (history[i].done - history[i - 1].done) / dt) : 0);
4978
+ }
4979
+ if (r.length < 2) return ["", ""];
4980
+ const max = Math.max(...r, 0.001);
4981
+ const pts = r.map((v, i) => (i / (r.length - 1) * 240).toFixed(1) + "," + (52 - (v / max) * 48).toFixed(1));
4982
+ return [pts.join(" "), "0,56 " + pts.join(" ") + " 240,56"];
4983
+ }
4984
+
4985
+ const jobEls = new Map();
4986
+ function renderJobs(s) {
4987
+ const names = new Set(s.jobs.map((j) => j.name));
4988
+ for (const [name, e] of jobEls) if (!names.has(name)) { e.remove(); jobEls.delete(name); }
4989
+ $("jobs-empty").hidden = s.jobs.length > 0;
4990
+ for (const j of s.jobs) {
4991
+ let e = jobEls.get(j.name);
4992
+ if (!e) { e = $("job-tpl").content.firstElementChild.cloneNode(true); $("jobs").append(e); jobEls.set(j.name, e); }
4993
+ const q = (c) => e.querySelector(c);
4994
+ const pct = j.total > 0 ? Math.min(100, (j.done / j.total) * 100) : 0;
4995
+ e.className = "card wide " + j.level;
4996
+ q("h2").textContent = "Re-embed job " + j.name;
4997
+ q(".fg").setAttribute("stroke-dashoffset", String(CIRC * (1 - pct / 100)));
4998
+ q(".pct").textContent = pct.toFixed(1) + "%";
4999
+ q(".rate").textContent = j.rate === null ? "?" : j.rate.toFixed(1);
5000
+ q(".done").textContent = n(j.done) + " / " + n(j.total);
5001
+ q(".eta").textContent = j.etaSecs === null ? "?" : dur(j.etaSecs);
5002
+ q(".fin").textContent = j.etaSecs === null ? "" : "done about " + clock(s.generatedAt + j.etaSecs * 1000);
5003
+ q(".phase").textContent = "phase " + j.phase + ", last progress " + when(j.lastProgressAt);
5004
+ q(".stall").hidden = !j.stalled;
5005
+ const [line, area] = sparkPoints(j.history || []);
5006
+ q("polyline").setAttribute("points", line);
5007
+ q("polygon").setAttribute("points", area);
5008
+ }
5009
+ }
5010
+
5011
+ function renderIndex(s) {
5012
+ const i = s.index, g = i.segments;
5013
+ $("index").className = "card " + i.level;
5014
+ set("ix-cov", i.coverage === null ? "?" : (i.coverage * 100).toFixed(1) + "%");
5015
+ set("ix-chunks", "embedded of " + n(i.chunks) + (i.countsExact ? "" : " (est.)") + " chunks");
5016
+ set("ix-files", n(i.files));
5017
+ const total = g ? g.fresh + g.old + g.missing : 0;
5018
+ const w = (v) => (total > 0 ? (v / total) * 100 : 0);
5019
+ const a = w(g ? g.fresh : 0), b = w(g ? g.old : 0), c = w(g ? g.missing : 0);
5020
+ const setRect = (id, x, width) => { $(id).setAttribute("x", String(x)); $(id).setAttribute("width", String(width)); };
5021
+ setRect("sg-new", 0, a); setRect("sg-old", a, b); setRect("sg-miss", a + b, c);
5022
+ const reembed = !!g && s.jobs.some((j) => j.phase !== "done") && g.fresh > 0;
5023
+ $("lg-new").hidden = !reembed;
5024
+ set("t-old", reembed ? "old backend, still to replace" : "embedded");
5025
+ set("n-new", n(g && g.fresh)); set("n-old", n(g && g.old)); set("n-miss", n(g && g.missing));
5026
+ const bound = i.binding ? i.binding.backend + (i.binding.model ? " / " + i.binding.model : "") : "none";
5027
+ set("ix-bound", bound);
5028
+ set("ix-conf", i.configured.backend + " / " + i.configured.model);
5029
+ const msg = i.error ? "binding error: " + i.error : i.hint || "";
5030
+ $("ix-hint").hidden = !msg; set("ix-hint", msg);
5031
+ $("ix-hint").style.color = i.level === "amber" ? "var(--amber)" : "";
5032
+ }
5033
+
5034
+ function renderBackend(s) {
5035
+ const b = s.backend, m = b.loaded && b.loaded[0];
5036
+ $("backend").className = "card " + b.level;
5037
+ lvl($("be-dot"), "dot", b.available === null ? "" : b.available ? "green" : "red");
5038
+ set("be-id", b.id);
5039
+ set("be-state", b.available === null ? "unknown" : b.available ? "available" : "unavailable");
5040
+ set("be-reason", b.reason || "");
5041
+ set("be-model", m ? m.name : b.model + " (not loaded)");
5042
+ const share = m && m.gpuShare !== null ? m.gpuShare : null;
5043
+ set("gpu-pct", share === null ? "?" : (share * 100).toFixed(0) + "%");
5044
+ $("gpu-bar").setAttribute("width", String(share === null ? 0 : Math.min(100, share * 100)));
5045
+ $("gpu-bar").setAttribute("fill", share !== null && share < 1 ? "var(--amber)" : "var(--green)");
5046
+ }
5047
+
5048
+ const passEls = new Map();
5049
+ const NS = "http://www.w3.org/2000/svg";
5050
+ function passRow(name) {
5051
+ const e = document.createElement("div"); e.className = "pass";
5052
+ e.innerHTML = '<div class="line"><span class="dot"></span><b></b><span class="k st"></span></div>' +
5053
+ '<svg class="tl" viewBox="0 0 100 16" preserveAspectRatio="none"><line x1="4" y1="8" x2="96" y2="8"/></svg>' +
5054
+ '<div class="times"><span class="last"></span><span class="next"></span></div><div class="err" hidden></div>';
5055
+ e.querySelector("b").textContent = name;
5056
+ const svg = e.querySelector("svg");
5057
+ const mk = (cls, cx) => { const c = document.createElementNS(NS, "ellipse"); c.setAttribute("class", cls); c.setAttribute("cx", cx); c.setAttribute("cy", "8"); c.setAttribute("rx", "2.2"); c.setAttribute("ry", "5"); svg.append(c); return c; };
5058
+ mk("m-last", "4"); mk("m-next", "96");
5059
+ return e;
5060
+ }
5061
+ function renderPasses(s) {
5062
+ $("passes").className = "card " + (s.passes.some((p) => p.level === "red") ? "red" : "green");
5063
+ for (const p of s.passes) {
5064
+ let e = passEls.get(p.name);
5065
+ if (!e) { e = passRow(p.name); $("pass-list").append(e); passEls.set(p.name, e); }
5066
+ const q = (c) => e.querySelector(c);
5067
+ const state = p.failure ? "red" : p.running ? "amber" : "green";
5068
+ lvl(q(".dot"), "dot", state);
5069
+ q(".st").textContent = p.failure ? "failed" : p.running ? "running" : "ok";
5070
+ q(".last").textContent = "last " + (p.lastEnd ? when(p.lastEnd) : "none since start");
5071
+ q(".next").textContent = p.nextAt ? "next " + when(p.nextAt) : "";
5072
+ const col = "var(--" + state + ")";
5073
+ q(".m-last").setAttribute("fill", col);
5074
+ q(".m-next").setAttribute("fill", "none"); q(".m-next").setAttribute("stroke", "var(--grey)"); q(".m-next").setAttribute("stroke-width", "1.5");
5075
+ q(".m-next").style.display = p.nextAt ? "" : "none";
5076
+ const err = q(".err");
5077
+ err.hidden = !p.failure;
5078
+ if (p.failure) err.textContent = p.failure.error + " (" + (p.failure.gaveUp ? "gave up until next scheduled run" : "retry " + when(p.failure.retryAt)) + ")";
5079
+ }
5080
+ }
5081
+
5082
+ function render(s) {
5083
+ lvl($("pill"), "pill", s.level);
5084
+ set("pill", s.level === "green" ? "healthy" : s.level === "amber" ? "attention" : "problem");
5085
+ set("reason", s.reason);
5086
+ renderJobs(s); renderIndex(s); renderBackend(s); renderPasses(s);
5087
+ set("d-pid", String(s.daemon.pid)); set("d-up", dur(s.daemon.uptimeSecs));
5088
+ set("d-ver", s.daemon.version); set("d-sto", s.daemon.storage);
5089
+ set("foot", "updated " + new Date(s.generatedAt).toLocaleTimeString() + " · refreshes every 10 s");
5090
+ }
5091
+ async function load() {
5092
+ try {
5093
+ const r = await fetch("/api/status", { cache: "no-store" });
5094
+ if (!r.ok) throw new Error("HTTP " + r.status);
5095
+ render(await r.json());
5096
+ } catch (e) {
5097
+ lvl($("pill"), "pill", "red"); set("pill", "unreachable");
5098
+ set("reason", "daemon unreachable: " + e.message);
5099
+ }
5100
+ }
5101
+ load();
5102
+ setInterval(load, 10000);
5103
+ <\/script></main></body></html>
5104
+ `;
5105
+
5106
+ //#endregion
5107
+ //#region src/daemon/dashboard.ts
5108
+ /**
5109
+ * dashboard.ts — read-only memory status dashboard served by the daemon.
5110
+ *
5111
+ * Embedding and index passes run unattended for hours; a crashed embed pass
5112
+ * went unnoticed for 7 h. This puts what is running and whether it is healthy
5113
+ * on one page (and /api/status) reachable from a phone on the tailnet.
5114
+ *
5115
+ * Nothing here computes anything per request: a background tick (30 s) samples
5116
+ * the job table, binding and backend health, and real row counts are refreshed
5117
+ * at most every 5 min. Requests only assemble the cached values.
5118
+ */
5119
+ const TICK_MS = 3e4;
5120
+ const COUNTS_TTL_MS = 5 * 6e4;
5121
+ const STALL_MS = 5 * 6e4;
5122
+ const RATE_WINDOW_MS = 5 * 6e4;
5123
+ const HISTORY_MAX = 120;
5124
+ const HISTORY_PERSIST_MS = 3e4;
5125
+ const HISTORY_PRUNE_AGE_MS = 120 * 6e4;
5126
+ const RANK = {
5127
+ green: 0,
5128
+ amber: 1,
5129
+ red: 2
5130
+ };
5131
+ const worst = (ls) => ls.reduce((a, b) => RANK[b] > RANK[a] ? b : a, "green");
5132
+ /** Host header without port; IPv6 literals keep their brackets stripped. */
5133
+ function hostName(header) {
5134
+ if (!header) return "";
5135
+ const h = header.trim().toLowerCase();
5136
+ if (h.startsWith("[")) return h.slice(1, h.indexOf("]"));
5137
+ return h.replace(/:\d+$/, "");
5138
+ }
5139
+ function hostAllowed(header, allowed) {
5140
+ const h = hostName(header);
5141
+ return h !== "" && new Set([...allowed].map((a) => a.toLowerCase())).has(h);
5142
+ }
5143
+ /** Tailnet DNS name and IPs of this machine from `tailscale status --json`; empty when unavailable. */
5144
+ async function tailnetHosts() {
5145
+ try {
5146
+ const { stdout } = await promisify(execFile)("tailscale", ["status", "--json"], { timeout: 3e3 });
5147
+ const self = JSON.parse(stdout).Self;
5148
+ return [self?.DNSName?.replace(/\.$/, ""), ...self?.TailscaleIPs ?? []].filter((x) => !!x);
5149
+ } catch {
5150
+ return [];
5151
+ }
5152
+ }
5153
+ /** Stacked-bar numbers: done/embedded/total in, three non-negative segments out. `done` null = no re-embed. */
5154
+ function indexSegments(total, embedded, done) {
5155
+ const missing = Math.max(0, total - embedded);
5156
+ if (done === null) return {
5157
+ fresh: 0,
5158
+ old: Math.max(0, embedded),
5159
+ missing
5160
+ };
5161
+ return {
5162
+ fresh: done,
5163
+ old: Math.max(0, embedded - done),
5164
+ missing
5165
+ };
5166
+ }
5167
+ function phaseOf(s) {
5168
+ if (s.finished) return "done";
5169
+ if (s.embedDone) return "finish";
5170
+ if (s.carryDone) return "embed";
5171
+ if (s.prepared) return "carry";
5172
+ return "prepare";
5173
+ }
5174
+ var DashboardState = class {
5175
+ cache = {
5176
+ counts: null,
5177
+ binding: null,
5178
+ bindingError: null,
5179
+ jobs: [],
5180
+ backend: null,
5181
+ loaded: null
5182
+ };
5183
+ samples = /* @__PURE__ */ new Map();
5184
+ lastExactAt;
5185
+ exactRunning = false;
5186
+ lastPersistAt;
5187
+ historyPath;
5188
+ constructor(src, now = Date.now, historyPath) {
5189
+ this.src = src;
5190
+ this.now = now;
5191
+ this.lastExactAt = now();
5192
+ this.lastPersistAt = now();
5193
+ this.historyPath = historyPath ?? paiHomePath("state", "dashboard-history.json");
5194
+ }
5195
+ async loadHistory() {
5196
+ try {
5197
+ if (!existsSync(this.historyPath)) return;
5198
+ const content = await readFile(this.historyPath, "utf8");
5199
+ const data = JSON.parse(content);
5200
+ const now = this.now();
5201
+ for (const [name, hist] of Object.entries(data.jobs ?? {})) {
5202
+ const pruned = hist.filter((s) => now - s.t <= HISTORY_PRUNE_AGE_MS);
5203
+ if (pruned.length > 0) {
5204
+ const s = this.samples.get(name) ?? {
5205
+ list: [],
5206
+ history: [],
5207
+ progressAt: now,
5208
+ lastDone: 0
5209
+ };
5210
+ s.history = pruned.slice(-HISTORY_MAX);
5211
+ this.samples.set(name, s);
5212
+ }
5213
+ }
5214
+ } catch {}
5215
+ }
5216
+ async persistHistory() {
5217
+ const t = this.now();
5218
+ if (t - this.lastPersistAt < HISTORY_PERSIST_MS) return;
5219
+ this.lastPersistAt = t;
5220
+ try {
5221
+ const jobs = {};
5222
+ for (const [name, s] of this.samples) if (s.history.length > 0) jobs[name] = s.history;
5223
+ const data = { jobs };
5224
+ await mkdir(join(this.historyPath, ".."), { recursive: true });
5225
+ await writeFile(this.historyPath + ".tmp", JSON.stringify(data), "utf8");
5226
+ await writeFile(this.historyPath, JSON.stringify(data), "utf8");
5227
+ } catch {}
5228
+ }
5229
+ /** Sample everything once. Never throws: a failing source shows up in the status instead. */
5230
+ async tick() {
5231
+ const t = this.now();
5232
+ const c = this.cache;
5233
+ const run = async (f, onErr) => {
5234
+ try {
5235
+ return await f();
5236
+ } catch (e) {
5237
+ onErr(e);
5238
+ return;
5239
+ }
5240
+ };
5241
+ if (!c.counts) {
5242
+ const est = await run(() => this.src.estimate(), () => {});
5243
+ if (est) c.counts = {
5244
+ ...est,
5245
+ exact: false,
5246
+ at: t
5247
+ };
5248
+ }
5249
+ this.refreshExact(t);
5250
+ c.bindingError = null;
5251
+ const b = await run(() => this.src.binding(), (e) => {
5252
+ c.bindingError = e instanceof Error ? e.message : String(e);
5253
+ });
5254
+ if (b !== void 0) c.binding = b;
5255
+ const jobs = await run(() => this.src.jobs(), () => {});
5256
+ if (jobs) {
5257
+ c.jobs = jobs;
5258
+ for (const j of jobs) this.sample(j, t);
5259
+ }
5260
+ const be = this.src.backend();
5261
+ c.backend = await run(() => be.available(), () => {}) ?? {
5262
+ ok: false,
5263
+ reason: "availability probe threw"
5264
+ };
5265
+ c.loaded = be.id.startsWith("ollama") ? await run(() => this.src.loadedModels(), () => {}) ?? null : null;
5266
+ await this.persistHistory();
5267
+ }
5268
+ /** Real counts at most every COUNTS_TTL_MS, in the background; the tick never waits for them. */
5269
+ refreshExact(t) {
5270
+ if (this.exactRunning || t - this.lastExactAt < COUNTS_TTL_MS) return;
5271
+ this.exactRunning = true;
5272
+ this.lastExactAt = t;
5273
+ this.src.exact().then((n) => {
5274
+ this.cache.counts = {
5275
+ ...n,
5276
+ exact: true,
5277
+ at: this.now()
5278
+ };
5279
+ }, () => {}).finally(() => {
5280
+ this.exactRunning = false;
5281
+ });
5282
+ }
5283
+ sample(j, t) {
5284
+ const done = Number(j.state.done ?? 0);
5285
+ const s = this.samples.get(j.name) ?? {
5286
+ list: [],
5287
+ history: [],
5288
+ progressAt: t,
5289
+ lastDone: done
5290
+ };
5291
+ if (done !== s.lastDone) {
5292
+ s.progressAt = t;
5293
+ s.lastDone = done;
5294
+ }
5295
+ s.list.push({
5296
+ t,
5297
+ done
5298
+ });
5299
+ s.history.push({
5300
+ t,
5301
+ done
5302
+ });
5303
+ if (s.history.length > HISTORY_MAX) s.history.shift();
5304
+ while (s.list.length > 2 && t - s.list[0].t > RATE_WINDOW_MS) s.list.shift();
5305
+ this.samples.set(j.name, s);
5306
+ }
5307
+ jobView(j, t) {
5308
+ const total = Number(j.state.total ?? 0);
5309
+ const done = Number(j.state.done ?? 0);
5310
+ const phase = phaseOf(j.state);
5311
+ const s = this.samples.get(j.name);
5312
+ const first = s?.list[0];
5313
+ const last = s?.list[s.list.length - 1];
5314
+ const rate = first && last && last.t > first.t ? Math.max(0, (last.done - first.done) / ((last.t - first.t) / 1e3)) : null;
5315
+ const etaSecs = rate && rate > 0 && total > done ? Math.round((total - done) / rate) : null;
5316
+ const lastProgressAt = s?.progressAt ?? t;
5317
+ const stalled = phase !== "done" && t - lastProgressAt >= STALL_MS;
5318
+ return {
5319
+ name: j.name,
5320
+ phase,
5321
+ done,
5322
+ total,
5323
+ rate,
5324
+ etaSecs,
5325
+ lastProgressAt,
5326
+ stalled,
5327
+ level: stalled ? "red" : "green",
5328
+ history: s?.history ?? []
5329
+ };
5330
+ }
5331
+ status() {
5332
+ const t = this.now();
5333
+ const c = this.cache;
5334
+ const d = this.src.daemon();
5335
+ const be = this.src.backend();
5336
+ const jobs = c.jobs.filter((j) => Object.keys(j.state).length > 0).map((j) => this.jobView(j, t));
5337
+ const mismatch = !!c.binding && !bindingMatches(c.binding, be);
5338
+ const counts = c.counts;
5339
+ const reembedJob = this.findReembedJob(c.jobs, be.id);
5340
+ const reembedActive = reembedJob && !["done"].includes(phaseOf(reembedJob.state));
5341
+ const indexLevel = mismatch && !reembedActive || c.bindingError ? "red" : reembedActive ? "amber" : "green";
5342
+ const backendLevel = c.backend && !c.backend.ok ? "red" : c.backend ? "green" : "amber";
5343
+ const passes = this.src.passes().map((p) => ({
5344
+ ...p,
5345
+ level: p.failure ? "red" : "green"
5346
+ }));
5347
+ let hint = null;
5348
+ if (mismatch && c.binding) if (reembedActive) hint = `re-embed to ${this.deriveBackendFromJobName(reembedJob.name)} in progress, semantic search keyword-only until it finishes`;
5349
+ else hint = mismatchReason(c.binding, be);
5350
+ const level = worst([
5351
+ indexLevel,
5352
+ backendLevel,
5353
+ ...jobs.map((j) => j.level),
5354
+ ...passes.map((p) => p.level)
5355
+ ]);
5356
+ const reason = level === "green" ? "all systems healthy" : c.bindingError ? `binding check failed: ${c.bindingError}` : jobs.find((j) => j.stalled) ? `job ${jobs.find((j) => j.stalled).name} stalled` : c.backend && !c.backend.ok ? `backend unavailable: ${c.backend.reason}` : passes.find((p) => p.failure) ? `pass ${passes.find((p) => p.failure).name} failed` : hint ?? (reembedActive ? `re-embed ${reembedJob.name} running` : c.backend ? "degraded" : "backend not probed yet");
5357
+ const activeJob = reembedActive ? jobs.find((j) => j.name === reembedJob.name) : void 0;
5358
+ const segments = counts && counts.embedded !== null ? indexSegments(counts.chunks, counts.embedded, activeJob ? activeJob.done : null) : null;
5359
+ return {
5360
+ generatedAt: t,
5361
+ level,
5362
+ reason,
5363
+ daemon: {
5364
+ pid: d.pid,
5365
+ uptimeSecs: Math.floor((t - d.startTime) / 1e3),
5366
+ version: d.version,
5367
+ storage: this.src.storage
5368
+ },
5369
+ index: {
5370
+ level: indexLevel,
5371
+ files: counts?.files ?? null,
5372
+ chunks: counts?.chunks ?? null,
5373
+ embedded: counts?.embedded ?? null,
5374
+ coverage: counts && counts.embedded !== null && counts.chunks > 0 ? Math.min(1, counts.embedded / counts.chunks) : null,
5375
+ countsExact: counts?.exact ?? false,
5376
+ countsAt: counts?.at ?? null,
5377
+ segments,
5378
+ binding: c.binding,
5379
+ configured: {
5380
+ backend: be.id,
5381
+ model: be.model,
5382
+ dims: be.dims
5383
+ },
5384
+ mismatch,
5385
+ hint,
5386
+ error: c.bindingError
5387
+ },
5388
+ jobs,
5389
+ passes,
5390
+ backend: {
5391
+ level: backendLevel,
5392
+ id: be.id,
5393
+ model: be.model,
5394
+ available: c.backend?.ok ?? null,
5395
+ reason: c.backend?.reason ?? null,
5396
+ loaded: c.loaded?.map((m) => ({
5397
+ name: m.name,
5398
+ size: m.size,
5399
+ sizeVram: m.sizeVram,
5400
+ gpuShare: m.size > 0 ? m.sizeVram / m.size : null
5401
+ })) ?? null
5402
+ }
5403
+ };
5404
+ }
5405
+ findReembedJob(jobs, configuredBackendId) {
5406
+ return jobs.find((j) => j.name.endsWith("-reembed") && this.deriveBackendFromJobName(j.name) === configuredBackendId);
5407
+ }
5408
+ deriveBackendFromJobName(jobName) {
5409
+ const match = jobName.match(/^(.+)-reembed$/);
5410
+ return match ? match[1] : jobName;
5411
+ }
5412
+ };
5413
+ async function startDashboard(cfg, src, opts = {}) {
5414
+ const state = new DashboardState(src, Date.now, opts.historyPath);
5415
+ await state.loadHistory();
5416
+ const allowed = [
5417
+ cfg.bind,
5418
+ "localhost",
5419
+ "127.0.0.1",
5420
+ "::1",
5421
+ ...opts.extraHosts ?? await tailnetHosts()
5422
+ ];
5423
+ const server = createServer$1((req, res) => {
5424
+ const deny = (code, msg) => {
5425
+ res.writeHead(code, { "content-type": "text/plain" });
5426
+ res.end(msg);
5427
+ };
5428
+ if (!hostAllowed(req.headers.host, allowed)) return deny(403, "forbidden host");
5429
+ if (req.method !== "GET" && req.method !== "HEAD") return deny(405, "read-only");
5430
+ const path = (req.url ?? "/").split("?")[0];
5431
+ if (path === "/api/status") {
5432
+ res.writeHead(200, {
5433
+ "content-type": "application/json",
5434
+ "cache-control": "no-store"
5435
+ });
5436
+ return res.end(JSON.stringify(state.status()));
5437
+ }
5438
+ if (path === "/") {
5439
+ res.writeHead(200, {
5440
+ "content-type": "text/html; charset=utf-8",
5441
+ "cache-control": "no-store"
5442
+ });
5443
+ return res.end(DASHBOARD_HTML);
5444
+ }
5445
+ deny(404, "not found");
5446
+ });
5447
+ await state.tick();
5448
+ const timer = setInterval(() => void state.tick(), TICK_MS);
5449
+ timer.unref();
5450
+ await new Promise((resolve, reject) => {
5451
+ server.once("error", reject);
5452
+ server.listen(cfg.port, cfg.bind, resolve);
5453
+ });
5454
+ return {
5455
+ server,
5456
+ url: `http://${cfg.bind}:${cfg.port}/`,
5457
+ stop: () => {
5458
+ clearInterval(timer);
5459
+ server.close();
5460
+ }
5461
+ };
5462
+ }
5463
+ async function ollamaPs(baseUrl) {
5464
+ try {
5465
+ const r = await fetch(`${baseUrl.replace(/\/+$/, "")}/api/ps`, { signal: AbortSignal.timeout(2e3) });
5466
+ if (!r.ok) return null;
5467
+ return ((await r.json()).models ?? []).map((m) => ({
5468
+ name: m.name,
5469
+ size: m.size,
5470
+ sizeVram: m.size_vram
5471
+ }));
5472
+ } catch {
5473
+ return null;
5474
+ }
5475
+ }
5476
+ function liveSource(deps) {
5477
+ const st = deps.storage;
5478
+ const sqliteCounts = async () => ({
5479
+ ...await deps.storage.getStats(),
5480
+ embedded: null
5481
+ });
5482
+ return {
5483
+ storage: deps.storage.backendType,
5484
+ daemon: deps.daemon,
5485
+ estimate: st.estimateCounts?.bind(st) ?? sqliteCounts,
5486
+ exact: st.exactCounts?.bind(st) ?? sqliteCounts,
5487
+ binding: () => deps.storage.getEmbeddingBinding(),
5488
+ jobs: st.embeddingJobs?.bind(st) ?? (async () => []),
5489
+ backend: deps.backend,
5490
+ loadedModels: () => ollamaPs(deps.ollamaBaseUrl),
5491
+ passes: deps.passes
5492
+ };
5493
+ }
5494
+
5495
+ //#endregion
5496
+ //#region src/daemon/daemon/dashboard-live.ts
5497
+ /**
5498
+ * Wires the status dashboard to the daemon's live state (state.ts, scheduler
5499
+ * bookkeeping) and the configured embedding backend.
5500
+ */
5501
+ function packageVersion() {
5502
+ try {
5503
+ return JSON.parse(readFileSync(resolveFromModule(import.meta.url, "package.json"), "utf8")).version ?? "0.0.0";
5504
+ } catch {
5505
+ return "0.0.0";
5506
+ }
5507
+ }
5508
+ /** The index/embed/vault passes as the dashboard shows them, from the existing daemon state. */
5509
+ function livePasses(now = Date.now()) {
5510
+ const cfg = daemonConfig;
5511
+ const next = (last, intervalSecs) => cfg.maintenanceHour !== void 0 ? now + msUntilNextAnchor(cfg.maintenanceHour, intervalSecs * 1e3) : (last || now) + intervalSecs * 1e3;
5512
+ const snap = (name, running, lastEnd, nextAt) => {
5513
+ const f = passFailures.get(name);
5514
+ const delay = f ? PASS_RETRY_DELAYS_MS[f.attempts - 1] : void 0;
5515
+ return {
5516
+ name,
5517
+ running,
5518
+ lastEnd,
5519
+ failure: f ? {
5520
+ ...f,
5521
+ retryAt: f.gaveUp || delay === void 0 ? null : f.at + delay
5522
+ } : null,
5523
+ nextAt
5524
+ };
5525
+ };
5526
+ return [
5527
+ snap("index", indexInProgress, lastIndexTime, next(lastIndexTime, cfg.indexIntervalSecs)),
5528
+ snap("embed", embedInProgress, lastEmbedTime, next(lastEmbedTime, cfg.embedIntervalSecs)),
5529
+ snap("vault", vaultIndexInProgress, lastVaultIndexTime, null)
5530
+ ];
5531
+ }
5532
+ /** Start the dashboard when enabled; a failure to bind is logged, never fatal for the daemon. */
5533
+ async function startLiveDashboard(config) {
5534
+ if (!config.dashboard.enabled) return null;
5535
+ try {
5536
+ const handle = await startDashboard(config.dashboard, liveSource({
5537
+ storage: storageBackend,
5538
+ backend: () => getConfiguredBackend(config),
5539
+ ollamaBaseUrl: config.embedding?.ollama?.baseUrl ?? "http://127.0.0.1:11434",
5540
+ daemon: () => ({
5541
+ pid: process.pid,
5542
+ startTime,
5543
+ version: packageVersion()
5544
+ }),
5545
+ passes: livePasses
5546
+ }));
5547
+ process.stderr.write(`[pai-daemon] Dashboard: ${handle.url}\n`);
5548
+ return handle;
5549
+ } catch (e) {
5550
+ process.stderr.write(`[pai-daemon] Dashboard not started: ${e instanceof Error ? e.message : String(e)}\n`);
5551
+ return null;
5552
+ }
5553
+ }
5554
+
4822
5555
  //#endregion
4823
5556
  //#region src/storage/postgres/fts-migration.ts
4824
5557
  const DONE_MARKER = "fts:simple";
@@ -4993,6 +5726,9 @@ async function serve(config) {
4993
5726
  setStorageBackend(backend);
4994
5727
  process.stderr.write(`[pai-daemon] Federation backend: ${backend.backendType}\n`);
4995
5728
  startIndexScheduler();
5729
+ startLiveDashboard(config).then((h) => {
5730
+ dashboard = h;
5731
+ });
4996
5732
  if (backend.supportsPostgresFeatures) {
4997
5733
  startEmbedScheduler();
4998
5734
  migrateFtsConfig(backend).catch((e) => {
@@ -5007,6 +5743,7 @@ async function serve(config) {
5007
5743
  process.stderr.write(`[pai-daemon] Fatal: Could not open federation storage: ${msg}\n`);
5008
5744
  process.exit(1);
5009
5745
  });
5746
+ let dashboard = null;
5010
5747
  const shutdown = async (signal) => {
5011
5748
  process.stderr.write(`\n[pai-daemon] ${signal} received. Stopping.\n`);
5012
5749
  setShutdownRequested(true);
@@ -5014,6 +5751,7 @@ async function serve(config) {
5014
5751
  if (embedSchedulerTimer) clearInterval(embedSchedulerTimer);
5015
5752
  if (cacheKeepaliveTimer) clearInterval(cacheKeepaliveTimer);
5016
5753
  stopWorker();
5754
+ dashboard?.stop();
5017
5755
  server.close();
5018
5756
  const SHUTDOWN_TIMEOUT_MS = 1e4;
5019
5757
  const POLL_INTERVAL_MS = 100;
@@ -5045,4 +5783,4 @@ async function serve(config) {
5045
5783
 
5046
5784
  //#endregion
5047
5785
  export { serve as t };
5048
- //# sourceMappingURL=daemon-vLtT3yaI.mjs.map
5786
+ //# sourceMappingURL=daemon-CsR1S_Qr.mjs.map