@tekmidian/pai 0.66.6 → 0.66.7

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