@tekmidian/pai 0.66.1 → 0.66.3

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 (185) hide show
  1. package/dist/{aibroker-client-B8c42Lh8.mjs → aibroker-client-Ck3p3k_-.mjs} +2 -2
  2. package/dist/{aibroker-client-C5Fw7DNz.mjs → aibroker-client-WCZi_tn-.mjs} +3 -3
  3. package/dist/{aibroker-client-C5Fw7DNz.mjs.map → aibroker-client-WCZi_tn-.mjs.map} +1 -1
  4. package/dist/{auto-route-o0BOsXn7.mjs → auto-route-BnizyALK.mjs} +65 -4
  5. package/dist/auto-route-BnizyALK.mjs.map +1 -0
  6. package/dist/auto-route-D6fW1Q8z.mjs +3 -0
  7. package/dist/{chain-CZfKIEq9.mjs → chain-Fz4NLm4D.mjs} +3 -4
  8. package/dist/{chain-CZfKIEq9.mjs.map → chain-Fz4NLm4D.mjs.map} +1 -1
  9. package/dist/cli/index.mjs +18 -23
  10. package/dist/cli/index.mjs.map +1 -1
  11. package/dist/cli/program.mjs +17 -22
  12. package/dist/{config-YinjgXEJ.mjs → config-CnsDMETf.mjs} +1 -1
  13. package/dist/{config-BbLFD7Uf.mjs → config-Ddc4DIPk.mjs} +5 -3
  14. package/dist/config-Ddc4DIPk.mjs.map +1 -0
  15. package/dist/{context-handover-cache-pkHzmL3c.mjs → context-handover-cache-DcPEW6vf.mjs} +1221 -13
  16. package/dist/context-handover-cache-DcPEW6vf.mjs.map +1 -0
  17. package/dist/daemon/index.mjs +17 -19
  18. package/dist/daemon/index.mjs.map +1 -1
  19. package/dist/daemon-Bl8TpHdM.mjs +5048 -0
  20. package/dist/daemon-Bl8TpHdM.mjs.map +1 -0
  21. package/dist/daemon-ZhHU6D0i.mjs +18 -0
  22. package/dist/daemon-mcp/index.mjs +12 -9
  23. package/dist/daemon-mcp/index.mjs.map +1 -1
  24. package/dist/{embeddings-CcscYWwk.mjs → embeddings-D0WOQpYR.mjs} +1 -1
  25. package/dist/{embeddings-Bx3q0QOY.mjs → embeddings-DMVzpvRZ.mjs} +18 -9
  26. package/dist/embeddings-DMVzpvRZ.mjs.map +1 -0
  27. package/dist/{env-JNEIrQWg.mjs → env-DiolswKQ.mjs} +1 -1
  28. package/dist/{env-JNEIrQWg.mjs.map → env-DiolswKQ.mjs.map} +1 -1
  29. package/dist/factory-BUnZgnKW.mjs +2892 -0
  30. package/dist/factory-BUnZgnKW.mjs.map +1 -0
  31. package/dist/factory-CrVlo2hY.mjs +8 -0
  32. package/dist/{fallback-CIiq2Raw.mjs → fallback-awOmq_9A.mjs} +23 -11
  33. package/dist/fallback-awOmq_9A.mjs.map +1 -0
  34. package/dist/{chunker-BH4i-F2b.mjs → helpers-CZsi_49C.mjs} +221 -2
  35. package/dist/helpers-CZsi_49C.mjs.map +1 -0
  36. package/dist/hooks/block-sleep-poll.mjs +6 -5
  37. package/dist/hooks/block-sleep-poll.mjs.map +3 -3
  38. package/dist/hooks/context-compression-hook.mjs +1 -0
  39. package/dist/hooks/context-compression-hook.mjs.map +2 -2
  40. package/dist/hooks/load-project-context.mjs +6 -5
  41. package/dist/hooks/load-project-context.mjs.map +3 -3
  42. package/dist/hooks/post-compact-inject.mjs +6 -5
  43. package/dist/hooks/post-compact-inject.mjs.map +3 -3
  44. package/dist/hooks/route-agents-to-worker.mjs +6 -5
  45. package/dist/hooks/route-agents-to-worker.mjs.map +3 -3
  46. package/dist/hooks/security-validator.mjs +1 -0
  47. package/dist/hooks/security-validator.mjs.map +2 -2
  48. package/dist/hooks/whisper-rules.mjs +1 -0
  49. package/dist/hooks/whisper-rules.mjs.map +2 -2
  50. package/dist/hooks/worker-guard.mjs +6 -5
  51. package/dist/hooks/worker-guard.mjs.map +3 -3
  52. package/dist/hooks/worker-proxy.mjs +6 -5
  53. package/dist/hooks/worker-proxy.mjs.map +3 -3
  54. package/dist/hooks/worker-status-line.mjs +6 -5
  55. package/dist/hooks/worker-status-line.mjs.map +3 -3
  56. package/dist/hooks/worker-supervision.mjs +6 -5
  57. package/dist/hooks/worker-supervision.mjs.map +3 -3
  58. package/dist/index.mjs +6 -7
  59. package/dist/{ipc-client-D16Xw6Uo.mjs → ipc-client-DkxRay-o.mjs} +2 -2
  60. package/dist/{ipc-client-D16Xw6Uo.mjs.map → ipc-client-DkxRay-o.mjs.map} +1 -1
  61. package/dist/{main-resolver-DPPwHNtn.mjs → main-resolver-CVGPICy-.mjs} +7 -7
  62. package/dist/{main-resolver-DPPwHNtn.mjs.map → main-resolver-CVGPICy-.mjs.map} +1 -1
  63. package/dist/main-resolver-DDShdDBm.mjs +7 -0
  64. package/dist/{migrate-BD7D8EEh.mjs → migrate-Bq0esMOa.mjs} +2 -2
  65. package/dist/{migrate-BD7D8EEh.mjs.map → migrate-Bq0esMOa.mjs.map} +1 -1
  66. package/dist/{pai-home-Cm9rcJgX.mjs → pai-home-UncxWxlX.mjs} +1 -1
  67. package/dist/{pai-home-Cm9rcJgX.mjs.map → pai-home-UncxWxlX.mjs.map} +1 -1
  68. package/dist/{pai-marker-D1MMswkz.mjs → pai-marker-DXVpFsYz.mjs} +1 -1
  69. package/dist/{pai-marker-D1MMswkz.mjs.map → pai-marker-DXVpFsYz.mjs.map} +1 -1
  70. package/dist/{planner-Dm5Acyo1.mjs → planner-GdiYe2FO.mjs} +10 -7
  71. package/dist/{planner-Dm5Acyo1.mjs.map → planner-GdiYe2FO.mjs.map} +1 -1
  72. package/dist/postgres-DKyiCA5V.mjs +5 -0
  73. package/dist/{postgres-Ceqsa64C.mjs → postgres-H85VSYHI.mjs} +364 -9
  74. package/dist/postgres-H85VSYHI.mjs.map +1 -0
  75. package/dist/{program-JJ0TwSHI.mjs → program-D3MSfx86.mjs} +142 -100
  76. package/dist/program-D3MSfx86.mjs.map +1 -0
  77. package/dist/{query-feedback-BIaZTTFO.mjs → query-feedback-BPdapXKd.mjs} +2 -2
  78. package/dist/{query-feedback-BIaZTTFO.mjs.map → query-feedback-BPdapXKd.mjs.map} +1 -1
  79. package/dist/{reranker-DKv80KO5.mjs → reranker-3lnggwgq.mjs} +1 -1
  80. package/dist/{reranker-DKv80KO5.mjs.map → reranker-3lnggwgq.mjs.map} +1 -1
  81. package/dist/{reranker-CFiEzHQu.mjs → reranker-CZ2mP4cf.mjs} +1 -1
  82. package/dist/router-BK-hFeQ6.mjs +3 -0
  83. package/dist/{router-a5G7q8_3.mjs → router-BaTbc9VX.mjs} +2 -2
  84. package/dist/{router-a5G7q8_3.mjs.map → router-BaTbc9VX.mjs.map} +1 -1
  85. package/dist/{run-BCpbxqA5.mjs → run-D9YbWsnL.mjs} +599 -22
  86. package/dist/run-D9YbWsnL.mjs.map +1 -0
  87. package/dist/{run-env-DRcp7A8K.mjs → run-env-CQRnxNht.mjs} +3 -3
  88. package/dist/{run-env-DRcp7A8K.mjs.map → run-env-CQRnxNht.mjs.map} +1 -1
  89. package/dist/{runtime-paths-CHTg3ywb.mjs → runtime-paths-Bw3Jk8qJ.mjs} +4 -2
  90. package/dist/runtime-paths-Bw3Jk8qJ.mjs.map +1 -0
  91. package/dist/{search-BOSphCJ1.mjs → search-CNAGTiJP.mjs} +2 -2
  92. package/dist/{search-BOSphCJ1.mjs.map → search-CNAGTiJP.mjs.map} +1 -1
  93. package/dist/{sources-CijVso3n.mjs → sources-CM2g-CLT.mjs} +2 -2
  94. package/dist/{sources-CijVso3n.mjs.map → sources-CM2g-CLT.mjs.map} +1 -1
  95. package/dist/{stop-words-BdQuaE9K.mjs → stop-words-DtxaTWU_.mjs} +1 -1
  96. package/dist/{stop-words-BdQuaE9K.mjs.map → stop-words-DtxaTWU_.mjs.map} +1 -1
  97. package/dist/{utils-C9HsYDpO.mjs → utils-DddRwMsG.mjs} +2 -2
  98. package/dist/{utils-C9HsYDpO.mjs.map → utils-DddRwMsG.mjs.map} +1 -1
  99. package/dist/{zettelkasten-CtGHQWPU.mjs → zettelkasten-BRIaHzFm.mjs} +149 -7
  100. package/dist/zettelkasten-BRIaHzFm.mjs.map +1 -0
  101. package/dist/zettelkasten-G0-ZTybr.mjs +5 -0
  102. package/docs/commands/memory.md +2 -0
  103. package/docs/commands/worker.md +2 -0
  104. package/package.json +1 -1
  105. package/dist/async-C2Bm_Lal.mjs +0 -300
  106. package/dist/async-C2Bm_Lal.mjs.map +0 -1
  107. package/dist/auto-route-o0BOsXn7.mjs.map +0 -1
  108. package/dist/chunker-BH4i-F2b.mjs.map +0 -1
  109. package/dist/clusters-BCtD3fbe.mjs +0 -169
  110. package/dist/clusters-BCtD3fbe.mjs.map +0 -1
  111. package/dist/config-BbLFD7Uf.mjs.map +0 -1
  112. package/dist/context-handover-cache-pkHzmL3c.mjs.map +0 -1
  113. package/dist/daemon-DXlaVCkw.mjs +0 -1741
  114. package/dist/daemon-DXlaVCkw.mjs.map +0 -1
  115. package/dist/daemon-DyORmxZu.mjs +0 -19
  116. package/dist/detector-BEPFsINR.mjs +0 -3
  117. package/dist/detector-BRTtAWrM.mjs +0 -65
  118. package/dist/detector-BRTtAWrM.mjs.map +0 -1
  119. package/dist/embeddings-Bx3q0QOY.mjs.map +0 -1
  120. package/dist/factory-CNpPQQ-5.mjs +0 -249
  121. package/dist/factory-CNpPQQ-5.mjs.map +0 -1
  122. package/dist/factory-DY7x73mE.mjs +0 -3
  123. package/dist/fallback-CIiq2Raw.mjs.map +0 -1
  124. package/dist/federation-db-BTyoufBh.mjs +0 -139
  125. package/dist/federation-db-BTyoufBh.mjs.map +0 -1
  126. package/dist/federation-db-HfIFc7FG.mjs +0 -3
  127. package/dist/helpers-BRCJg0G3.mjs +0 -223
  128. package/dist/helpers-BRCJg0G3.mjs.map +0 -1
  129. package/dist/indexer-backend-DFF2FrYx.mjs +0 -5
  130. package/dist/indexer-backend-isSLg6yE.mjs +0 -1
  131. package/dist/kg-entity-DCOcsVFD.mjs +0 -29
  132. package/dist/kg-entity-DCOcsVFD.mjs.map +0 -1
  133. package/dist/latent-ideas-wwAXquGe.mjs +0 -191
  134. package/dist/latent-ideas-wwAXquGe.mjs.map +0 -1
  135. package/dist/link-boost-CnI7UVMJ.mjs +0 -34
  136. package/dist/link-boost-CnI7UVMJ.mjs.map +0 -1
  137. package/dist/main-resolver-BKz_OuEh.mjs +0 -7
  138. package/dist/merge-2gqRPFu2.mjs +0 -3
  139. package/dist/merge-DgU9OgZy.mjs +0 -6
  140. package/dist/merge-DgU9OgZy.mjs.map +0 -1
  141. package/dist/module-paths-DdRzbkUI.mjs +0 -44
  142. package/dist/module-paths-DdRzbkUI.mjs.map +0 -1
  143. package/dist/neighborhood-2FsmzoxG.mjs +0 -114
  144. package/dist/neighborhood-2FsmzoxG.mjs.map +0 -1
  145. package/dist/note-context-De63k8na.mjs +0 -106
  146. package/dist/note-context-De63k8na.mjs.map +0 -1
  147. package/dist/postgres-Ceqsa64C.mjs.map +0 -1
  148. package/dist/program-JJ0TwSHI.mjs.map +0 -1
  149. package/dist/query-feedback-B_iigYj-.mjs +0 -3
  150. package/dist/registry-db-C7voqML9.mjs +0 -213
  151. package/dist/registry-db-C7voqML9.mjs.map +0 -1
  152. package/dist/registry-db-JHPhA8vF.mjs +0 -3
  153. package/dist/registry-postgres-nOfyBSIW.mjs +0 -795
  154. package/dist/registry-postgres-nOfyBSIW.mjs.map +0 -1
  155. package/dist/registry-sqlite-DrCW1aRK.mjs +0 -593
  156. package/dist/registry-sqlite-DrCW1aRK.mjs.map +0 -1
  157. package/dist/router-CXUGsv85.mjs +0 -3
  158. package/dist/run-BCpbxqA5.mjs.map +0 -1
  159. package/dist/runtime-paths-CHTg3ywb.mjs.map +0 -1
  160. package/dist/search-Bf3Kub3F.mjs +0 -3
  161. package/dist/server-DgmAHyFK.mjs +0 -403
  162. package/dist/server-DgmAHyFK.mjs.map +0 -1
  163. package/dist/session-keepalive-DxjTHGcK.mjs +0 -582
  164. package/dist/session-keepalive-DxjTHGcK.mjs.map +0 -1
  165. package/dist/sqlite-CqsTy6xo.mjs +0 -936
  166. package/dist/sqlite-CqsTy6xo.mjs.map +0 -1
  167. package/dist/state-DW8zdweW.mjs +0 -3
  168. package/dist/state-HyjqTihC.mjs +0 -76
  169. package/dist/state-HyjqTihC.mjs.map +0 -1
  170. package/dist/themes-CTaOj3e1.mjs +0 -148
  171. package/dist/themes-CTaOj3e1.mjs.map +0 -1
  172. package/dist/tools-BcOKPBtg.mjs +0 -1489
  173. package/dist/tools-BcOKPBtg.mjs.map +0 -1
  174. package/dist/tools-C-_l0o8Z.mjs +0 -6
  175. package/dist/trace-CtV0RO6n.mjs +0 -137
  176. package/dist/trace-CtV0RO6n.mjs.map +0 -1
  177. package/dist/utils-BrX9FLeK.mjs +0 -3
  178. package/dist/vault-indexer-BG1sEVyM.mjs +0 -537
  179. package/dist/vault-indexer-BG1sEVyM.mjs.map +0 -1
  180. package/dist/wakeup-D8n33hYV.mjs +0 -335
  181. package/dist/wakeup-D8n33hYV.mjs.map +0 -1
  182. package/dist/work-queue-worker-4tLa5gpQ.mjs +0 -567
  183. package/dist/work-queue-worker-4tLa5gpQ.mjs.map +0 -1
  184. package/dist/work-queue-worker-rFZvTO3p.mjs +0 -11
  185. package/dist/zettelkasten-CtGHQWPU.mjs.map +0 -1
@@ -1,19 +1,113 @@
1
- import { _ as warn, c as ok, i as err, n as dim, r as encodeDir, s as now, t as bold } from "./utils-C9HsYDpO.mjs";
2
- import { a as slugify, i as parseSessionFilename, n as decodeEncodedDir, t as buildEncodedDirMap } from "./migrate-BD7D8EEh.mjs";
3
- import { n as ensurePaiMarker, t as discoverPaiMarkers } from "./pai-marker-D1MMswkz.mjs";
4
- import { i as getRegistryBackend } from "./factory-CNpPQQ-5.mjs";
5
- import { a as paiHomePath, o as resolvePaiFile, r as migratePaiFile } from "./pai-home-Cm9rcJgX.mjs";
6
- import { D as readWorkersSection, k as resolveModelCapability, t as buildRunEnv, v as defaultWorkersConfig } from "./run-env-DRcp7A8K.mjs";
7
- import { d as resolveTarget, n as ensureProxyRunning, p as ensureNoMcpConfig, t as DEFAULT_PROXY_PORT, v as workersLogDir } from "./server-DgmAHyFK.mjs";
8
- import { k as storageBackend, n as daemonConfig } from "./state-HyjqTihC.mjs";
9
- import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
10
- import { homedir, tmpdir } from "node:os";
11
- import { basename, dirname, join, resolve } from "node:path";
1
+ import { c as ok, g as warn, i as err, n as dim, r as encodeDir, s as now, t as bold } from "./utils-DddRwMsG.mjs";
2
+ import { a as slugify, i as parseSessionFilename, n as decodeEncodedDir, t as buildEncodedDirMap } from "./migrate-Bq0esMOa.mjs";
3
+ import { n as ensurePaiMarker, t as discoverPaiMarkers } from "./pai-marker-DXVpFsYz.mjs";
4
+ import { i as getRegistryBackend } from "./factory-BUnZgnKW.mjs";
5
+ import { y as writeJsonAtomic } from "./config-Ddc4DIPk.mjs";
6
+ import { a as paiHomePath, o as resolvePaiFile, r as migratePaiFile } from "./pai-home-UncxWxlX.mjs";
7
+ import { r as daemonPidPath } from "./runtime-paths-Bw3Jk8qJ.mjs";
8
+ import { D as readWorkersSection, k as resolveModelCapability, t as buildRunEnv, v as defaultWorkersConfig } from "./run-env-CQRnxNht.mjs";
9
+ import { C as itermUuid, F as appendLedger, P as worktreesDir, T as sessionMapPath, ct as resolveTarget, lt as ensureNoMcpConfig, nt as DEFAULT_PROXY_PORT, pt as workersLogDir, rt as ensureProxyRunning } from "./run-D9YbWsnL.mjs";
10
+ import { i as sendToSession, n as fetchLiveSessions } from "./aibroker-client-WCZi_tn-.mjs";
11
+ import { createReadStream, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
12
+ import { homedir, setPriority, tmpdir, userInfo } from "node:os";
13
+ import { basename, delimiter, dirname, join, resolve } from "node:path";
12
14
  import { randomUUID } from "node:crypto";
15
+ import { spawn, spawnSync } from "node:child_process";
13
16
  import { copyFileSync as copyFileSync$1, existsSync as existsSync$1, linkSync as linkSync$1, mkdirSync as mkdirSync$1, readFileSync as readFileSync$1, readdirSync as readdirSync$1, renameSync as renameSync$1, writeFileSync as writeFileSync$1 } from "fs";
14
17
  import { basename as basename$1, join as join$1, resolve as resolve$1 } from "path";
15
18
  import { homedir as homedir$1 } from "os";
19
+ import { createInterface } from "node:readline";
16
20
 
21
+ //#region src/cli/commands/detect.ts
22
+ /**
23
+ * Project detection logic for PAI.
24
+ *
25
+ * detectProject(cwd) — given a filesystem path, returns the best matching
26
+ * project from the registry:
27
+ * 1. Exact path match
28
+ * 2. Longest parent match (project whose root_path is an ancestor of cwd)
29
+ *
30
+ * Exported for use by the CLI `pai project detect` command and the MCP
31
+ * `project_detect` tool.
32
+ */
33
+ /**
34
+ * Detect which registered project a filesystem path belongs to.
35
+ *
36
+ * @param cwd Absolute path to detect (defaults to process.cwd())
37
+ * @returns The best matching project, or null if no match
38
+ */
39
+ async function detectProject(cwd) {
40
+ const target = resolve(cwd ?? process.cwd());
41
+ const backend = await getRegistryBackend();
42
+ const projects = await backend.listProjectsByPathLengthDesc({ excludeArchived: true });
43
+ let matched = null;
44
+ let matchType = "exact";
45
+ for (const p of projects) {
46
+ const root = resolve(p.root_path);
47
+ if (target === root) {
48
+ matched = p;
49
+ matchType = "exact";
50
+ break;
51
+ }
52
+ if (!matched && target.startsWith(root + "/")) {
53
+ matched = p;
54
+ matchType = "parent";
55
+ break;
56
+ }
57
+ }
58
+ if (!matched) return null;
59
+ const sessionCount = await backend.countSessionsForProject(matched.id);
60
+ const lastDate = await backend.getMostRecentSessionDate(matched.id);
61
+ const relative = matchType === "parent" ? target.slice(resolve(matched.root_path).length + 1) : null;
62
+ return {
63
+ id: matched.id,
64
+ slug: matched.slug,
65
+ display_name: matched.display_name,
66
+ root_path: matched.root_path,
67
+ encoded_dir: matched.encoded_dir,
68
+ type: matched.type,
69
+ status: matched.status,
70
+ session_count: sessionCount,
71
+ last_session_date: lastDate,
72
+ match_type: matchType,
73
+ relative_path: relative
74
+ };
75
+ }
76
+ /**
77
+ * Format a DetectedProject for human-readable CLI output.
78
+ */
79
+ function formatDetection(d) {
80
+ const lines = [
81
+ `slug: ${d.slug}`,
82
+ `display_name: ${d.display_name}`,
83
+ `root_path: ${d.root_path}`,
84
+ `type: ${d.type}`,
85
+ `status: ${d.status}`,
86
+ `match: ${d.match_type}${d.relative_path ? ` (+${d.relative_path})` : ""}`,
87
+ `sessions: ${d.session_count}`
88
+ ];
89
+ if (d.last_session_date) lines.push(`last_session: ${d.last_session_date}`);
90
+ return lines.join("\n");
91
+ }
92
+ /**
93
+ * Format a DetectedProject as JSON for machine consumption.
94
+ */
95
+ function formatDetectionJson(d) {
96
+ return JSON.stringify({
97
+ slug: d.slug,
98
+ display_name: d.display_name,
99
+ root_path: d.root_path,
100
+ encoded_dir: d.encoded_dir,
101
+ type: d.type,
102
+ status: d.status,
103
+ match_type: d.match_type,
104
+ relative_path: d.relative_path,
105
+ session_count: d.session_count,
106
+ last_session_date: d.last_session_date
107
+ }, null, 2);
108
+ }
109
+
110
+ //#endregion
17
111
  //#region src/registry/moved.ts
18
112
  /**
19
113
  * moved.ts — reconnect a project to transcripts that moved out from under it.
@@ -553,6 +647,386 @@ async function cmdScan(opts = {}) {
553
647
  } else process.stderr.write(`[registry-scan] ${result.projectsScanned} projects, ${result.sessionsScanned} sessions (${result.projectsNew}+${result.sessionsNew} new).\n`);
554
648
  }
555
649
 
650
+ //#endregion
651
+ //#region src/daemon/pass-priority.ts
652
+ /**
653
+ * pass-priority.ts — run heavy index/embed pass work at background priority.
654
+ *
655
+ * The daemon also serves MCP, so its own priority must stay normal. The heavy
656
+ * work runs in a child (`pai memory pass <name>`) spawned under
657
+ * `taskpolicy -b nice -n 19` (darwin) or `nice -n 19 ionice -c3` (linux);
658
+ * onnxruntime threads inherit the child's priority. The child reports over
659
+ * stdout as one JSON line: {ok:true,result} or {ok:false,error}.
660
+ */
661
+ function onPath(bin) {
662
+ return (process.env.PATH ?? "").split(delimiter).some((d) => d && existsSync(join(d, bin)));
663
+ }
664
+ /**
665
+ * Wrap `node <entry> <passArgs>` in the platform's low-priority incantation.
666
+ * Darwin uses nice 19, not 20: measured on macOS, node started with nice 20
667
+ * reverts to nice 0 within a second; 19 sticks.
668
+ */
669
+ function buildPassCommand(platform, node, entry, passArgs, have = onPath) {
670
+ const run = [
671
+ node,
672
+ entry,
673
+ ...passArgs
674
+ ];
675
+ let wrap;
676
+ if (platform === "darwin") wrap = have("taskpolicy") ? [
677
+ "taskpolicy",
678
+ "-b",
679
+ "nice",
680
+ "-n",
681
+ "19"
682
+ ] : [
683
+ "nice",
684
+ "-n",
685
+ "19"
686
+ ];
687
+ else if (platform === "win32") wrap = [];
688
+ else wrap = have("ionice") ? [
689
+ "nice",
690
+ "-n",
691
+ "19",
692
+ "ionice",
693
+ "-c3"
694
+ ] : [
695
+ "nice",
696
+ "-n",
697
+ "19"
698
+ ];
699
+ const [cmd, ...rest] = [...wrap, ...run];
700
+ return {
701
+ cmd,
702
+ args: rest
703
+ };
704
+ }
705
+ /** The CLI entry the child runs: dist/cli/index.mjs next to the daemon's dist. */
706
+ function cliEntry() {
707
+ return join(dirname(dirname(process.argv[1] ?? "")), "cli", "index.mjs");
708
+ }
709
+ /** Spawning itself failed (binary missing): the caller may fall back in-process. */
710
+ var PassSpawnError = class extends Error {};
711
+ const children = /* @__PURE__ */ new Set();
712
+ process.on("exit", () => children.forEach((c) => c.kill()));
713
+ /**
714
+ * Run one pass in a low-priority child. Resolves with the child's result;
715
+ * rejects with the child's error (pass failure) or PassSpawnError (no child).
716
+ */
717
+ function runPassChild(name, extraArgs = []) {
718
+ const { cmd, args } = buildPassCommand(process.platform, process.execPath, cliEntry(), [
719
+ "memory",
720
+ "pass",
721
+ name,
722
+ ...extraArgs
723
+ ]);
724
+ return new Promise((resolve, reject) => {
725
+ const child = spawn(cmd, args, {
726
+ stdio: [
727
+ "ignore",
728
+ "pipe",
729
+ "pipe"
730
+ ],
731
+ env: {
732
+ ...process.env,
733
+ PAI_PASS_BACKGROUND: "1"
734
+ }
735
+ });
736
+ children.add(child);
737
+ let out = "";
738
+ let err = "";
739
+ child.stdout.on("data", (d) => out += d);
740
+ child.stderr.on("data", (d) => err = (err + d).slice(-4e3));
741
+ child.on("error", (e) => {
742
+ children.delete(child);
743
+ reject(new PassSpawnError(`cannot spawn ${cmd}: ${e.message}`));
744
+ });
745
+ child.on("close", (code, signal) => {
746
+ children.delete(child);
747
+ const line = out.trim().split("\n").pop() ?? "";
748
+ let msg;
749
+ try {
750
+ msg = JSON.parse(line);
751
+ } catch {}
752
+ if (code === 0 && msg?.ok) return resolve(msg.result);
753
+ reject(new Error(msg?.error ?? `pass child ${name} exited ${code ?? signal}: ${err.trim().split("\n").pop() ?? ""}`));
754
+ });
755
+ });
756
+ }
757
+ /**
758
+ * Inside the wrapper, keep nice at 19: measured on macOS, node's nice resets
759
+ * to 0 about a second after start (5s re-apply interval holds it; the
760
+ * taskpolicy -b QoS is unaffected).
761
+ */
762
+ function holdNice() {
763
+ const apply = () => {
764
+ try {
765
+ setPriority(process.pid, 19);
766
+ } catch {}
767
+ };
768
+ apply();
769
+ setInterval(apply, 5e3).unref();
770
+ }
771
+ /**
772
+ * `--background` for CLI commands: re-exec this very command under the
773
+ * low-priority wrapper and exit with its status. Inside the re-exec (or a
774
+ * daemon-spawned pass child, which sets the same env) it only holds nice.
775
+ */
776
+ function reexecBackground() {
777
+ if (process.env.PAI_PASS_BACKGROUND === "1") return holdNice();
778
+ const { cmd, args } = buildPassCommand(process.platform, process.execPath, process.argv[1], process.argv.slice(2));
779
+ const r = spawnSync(cmd, args, {
780
+ stdio: "inherit",
781
+ env: {
782
+ ...process.env,
783
+ PAI_PASS_BACKGROUND: "1"
784
+ }
785
+ });
786
+ if (r.error) {
787
+ process.stderr.write(`--background: cannot spawn ${cmd} (${r.error.message}); running at normal priority\n`);
788
+ return;
789
+ }
790
+ process.exit(r.status ?? 1);
791
+ }
792
+
793
+ //#endregion
794
+ //#region src/memory/wakeup.ts
795
+ /**
796
+ * Wake-up context system — progressive context loading inspired by mempalace.
797
+ *
798
+ * Layers:
799
+ * L0 Identity (~100 tokens) — user identity from ~/.pai/identity.txt. Always loaded.
800
+ * L1 Essential Story (~500-800t) — top session notes for the project, key lines extracted.
801
+ * L2 On-Demand — triggered by topic queries (handled by memory_search).
802
+ * L3 Deep Search — unlimited federated memory search (memory_search tool).
803
+ */
804
+ /** Maximum tokens for the L1 essential story block. Approx 4 chars/token.
805
+ * Measured 2026-09-20: 800 tokens was mostly stale bullets. Reduced to 300.
806
+ */
807
+ const L1_TOKEN_BUDGET = 300;
808
+ L1_TOKEN_BUDGET * 4;
809
+ /** Maximum session notes to scan when building L1. Reduced from 10 to 3 (2026-09-20). */
810
+ const L1_MAX_NOTES = 3;
811
+ /** Sections to extract from session notes (in priority order). */
812
+ const EXTRACT_SECTIONS = [
813
+ "Work Done",
814
+ "Key Decisions",
815
+ "Next Steps",
816
+ "Checkpoint"
817
+ ];
818
+ /** Identity file location. */
819
+ const IDENTITY_FILE = join(homedir(), ".pai", "identity.txt");
820
+ /**
821
+ * Load L0 identity. Prefers the PAI_HOME location (identity.txt migrated by
822
+ * `pai config migrate`), falling back to the legacy ~/.pai/identity.txt so a
823
+ * not-yet-migrated file keeps working. Returns "" if neither exists. Never
824
+ * throws.
825
+ */
826
+ function loadL0Identity(opts = {}) {
827
+ const newPath = opts.newPath ?? paiHomePath("identity.txt");
828
+ const legacyPath = opts.legacyPath ?? IDENTITY_FILE;
829
+ const path = existsSync(newPath) ? newPath : legacyPath;
830
+ if (!existsSync(path)) return "";
831
+ try {
832
+ return readFileSync(path, "utf-8").trim();
833
+ } catch {
834
+ return "";
835
+ }
836
+ }
837
+ /**
838
+ * Move ~/.pai/identity.txt into PAI_HOME, following the same copy /
839
+ * verify-byte-identical / rename-aside semantics as every other per-user
840
+ * file (see migratePaiFile). `from`/`to` are overridable for tests.
841
+ */
842
+ function migrateIdentityFile(opts = {}) {
843
+ return migratePaiFile(opts.to ?? paiHomePath("identity.txt"), [opts.from ?? IDENTITY_FILE], { dryRun: opts.dryRun });
844
+ }
845
+ /**
846
+ * Find the Notes directory for a project given its root_path from the registry.
847
+ * Checks local Notes/ first, then central ~/.claude/projects/... path.
848
+ */
849
+ function findNotesDirForProject(rootPath) {
850
+ const localCandidates = [
851
+ join(rootPath, "Notes"),
852
+ join(rootPath, "notes"),
853
+ join(rootPath, ".claude", "Notes")
854
+ ];
855
+ for (const p of localCandidates) if (existsSync(p)) return p;
856
+ const encoded = rootPath.replace(/\//g, "-").replace(/\./g, "-").replace(/ /g, "-");
857
+ const centralNotes = join(homedir(), ".claude", "projects", encoded, "Notes");
858
+ if (existsSync(centralNotes)) return centralNotes;
859
+ return null;
860
+ }
861
+ /**
862
+ * Recursively find all .md session note files in a Notes directory.
863
+ * Handles both flat layout (Notes/*.md) and month-subdirectory layout
864
+ * (Notes/YYYY/MM/*.md). Returns files sorted newest-first by filename
865
+ * (note numbers are monotonically increasing, so lexicographic = newest-last,
866
+ * so we reverse).
867
+ */
868
+ function findSessionNotes(notesDir) {
869
+ const result = [];
870
+ const scanDir = (dir) => {
871
+ if (!existsSync(dir)) return;
872
+ let entries;
873
+ try {
874
+ entries = readdirSync(dir, { withFileTypes: true }).map((e) => ({
875
+ name: e.name,
876
+ isDir: e.isDirectory()
877
+ }));
878
+ } catch {
879
+ return;
880
+ }
881
+ for (const entry of entries) {
882
+ const fullPath = join(dir, entry.name);
883
+ if (entry.isDir) scanDir(fullPath);
884
+ else if (entry.name.match(/^\d{3,4}[\s_-].*\.md$/)) result.push(fullPath);
885
+ }
886
+ };
887
+ scanDir(notesDir);
888
+ const dateOf = (p) => basename(p).match(/(\d{4}-\d{2}-\d{2})/)?.[1] ?? "";
889
+ const numberOf = (p) => parseInt(basename(p).match(/^(\d+)/)?.[1] ?? "0", 10);
890
+ const mtimeOf = (p) => {
891
+ try {
892
+ return statSync(p).mtimeMs;
893
+ } catch {
894
+ return 0;
895
+ }
896
+ };
897
+ result.sort((a, b) => {
898
+ const dateA = dateOf(a);
899
+ const dateB = dateOf(b);
900
+ if (dateA !== dateB) {
901
+ if (!dateA) return 1;
902
+ if (!dateB) return -1;
903
+ return dateB.localeCompare(dateA);
904
+ }
905
+ const byNumber = numberOf(b) - numberOf(a);
906
+ if (byNumber !== 0) return byNumber;
907
+ return mtimeOf(b) - mtimeOf(a);
908
+ });
909
+ return result;
910
+ }
911
+ /**
912
+ * Extract the most important lines from a session note.
913
+ * Prioritises: Work Done items, Key Decisions, Next Steps, Checkpoint headings.
914
+ * Returns a condensed string under maxChars.
915
+ */
916
+ function normalizeLine(line) {
917
+ return line.replace(/^(- \[[ x]\] |- |\* |\d+\.\s)/, "").trim().replace(/\s+/g, " ");
918
+ }
919
+ function extractKeyLines(content, maxChars, seen = /* @__PURE__ */ new Set(), seenLabels = /* @__PURE__ */ new Set()) {
920
+ const lines = content.split("\n");
921
+ const selected = [];
922
+ let inTargetSection = false;
923
+ let currentSection = "";
924
+ let charCount = 0;
925
+ let pendingLabel = null;
926
+ for (const line of lines) {
927
+ const h2Match = line.match(/^## (.+)$/);
928
+ const h3Match = line.match(/^### (.+)$/);
929
+ if (h2Match) {
930
+ currentSection = h2Match[1];
931
+ inTargetSection = EXTRACT_SECTIONS.some((s) => currentSection.toLowerCase().includes(s.toLowerCase()));
932
+ pendingLabel = null;
933
+ continue;
934
+ }
935
+ if (h3Match) {
936
+ if (inTargetSection) pendingLabel = h3Match[1];
937
+ continue;
938
+ }
939
+ if (!inTargetSection) continue;
940
+ const trimmed = line.trim();
941
+ if (!trimmed || trimmed.startsWith("<!--") || trimmed === "---") continue;
942
+ if (trimmed.startsWith("- ") || trimmed.startsWith("* ") || trimmed.match(/^\d+\./) || trimmed.startsWith("**")) {
943
+ const normalized = normalizeLine(trimmed);
944
+ if (seen.has(normalized)) continue;
945
+ let labelText = "";
946
+ let labelKey = "";
947
+ if (pendingLabel !== null) {
948
+ labelKey = pendingLabel.toLowerCase().trim();
949
+ if (!seenLabels.has(labelKey)) labelText = `[${pendingLabel}]`;
950
+ }
951
+ if (charCount + labelText.length + trimmed.length + 2 > maxChars) break;
952
+ if (labelText) {
953
+ selected.push(labelText);
954
+ seenLabels.add(labelKey);
955
+ charCount += labelText.length + 1;
956
+ }
957
+ pendingLabel = null;
958
+ seen.add(normalized);
959
+ selected.push(trimmed);
960
+ charCount += trimmed.length + 1;
961
+ }
962
+ }
963
+ return selected.join("\n");
964
+ }
965
+ /**
966
+ * Build the L1 essential story block.
967
+ *
968
+ * Reads the most recent session notes for the project and extracts the key
969
+ * lines (Work Done, Key Decisions, Next Steps) within the token budget.
970
+ *
971
+ * @param rootPath The project root path (from the registry).
972
+ * @param tokenBudget Max tokens to consume. Default 800 (~3200 chars).
973
+ * @returns Formatted L1 block, or empty string if no notes found.
974
+ */
975
+ function buildL1EssentialStory(rootPath, tokenBudget = L1_TOKEN_BUDGET) {
976
+ const charBudget = tokenBudget * 4;
977
+ const notesDir = findNotesDirForProject(rootPath);
978
+ if (!notesDir) return "";
979
+ const noteFiles = findSessionNotes(notesDir).slice(0, L1_MAX_NOTES);
980
+ if (noteFiles.length === 0) return "";
981
+ const sections = [];
982
+ const seen = /* @__PURE__ */ new Set();
983
+ const seenLabels = /* @__PURE__ */ new Set();
984
+ let remaining = charBudget;
985
+ for (const noteFile of noteFiles) {
986
+ if (remaining <= 50) break;
987
+ let content;
988
+ try {
989
+ content = readFileSync(noteFile, "utf-8");
990
+ } catch {
991
+ continue;
992
+ }
993
+ const name = basename(noteFile);
994
+ const titleMatch = name.match(/^\d+ - (\d{4}-\d{2}-\d{2}) - (.+)\.md$/);
995
+ const dateLabel = titleMatch ? titleMatch[1] : "";
996
+ const titleLabel = titleMatch ? titleMatch[2] : name.replace(/^\d+ - /, "").replace(/\.md$/, "");
997
+ const perNoteChars = Math.min(remaining, Math.floor(charBudget / noteFiles.length) + 200);
998
+ const extracted = extractKeyLines(content, perNoteChars, seen, seenLabels);
999
+ const hasContentLine = extracted.split("\n").some((l) => !/^\[.*\]$/.test(l));
1000
+ if (!extracted || !hasContentLine) continue;
1001
+ const noteBlock = `[${dateLabel} - ${titleLabel}]\n${extracted}`;
1002
+ sections.push(noteBlock);
1003
+ remaining -= noteBlock.length + 1;
1004
+ }
1005
+ if (sections.length === 0) return "";
1006
+ return sections.join("\n\n");
1007
+ }
1008
+ /**
1009
+ * Build the combined wake-up context block (L0 + L1).
1010
+ *
1011
+ * Returns a formatted string suitable for injection as a system-reminder,
1012
+ * or an empty string if both layers are empty.
1013
+ *
1014
+ * @param rootPath Project root path for L1 note lookup. Optional.
1015
+ * @param tokenBudget L1 token budget. Default 800.
1016
+ * @param opts.skipStory Skip the L1 essential story. Set when a handover
1017
+ * checkpoint was already injected — the story re-summarises the same note
1018
+ * the handover came from, at lower quality.
1019
+ */
1020
+ function buildWakeupContext(rootPath, tokenBudget = L1_TOKEN_BUDGET, opts = {}) {
1021
+ const identity = loadL0Identity();
1022
+ const essentialStory = rootPath && !opts.skipStory ? buildL1EssentialStory(rootPath, tokenBudget) : "";
1023
+ if (!identity && !essentialStory) return "";
1024
+ const parts = [];
1025
+ if (identity) parts.push(`## L0 Identity\n\n${identity}`);
1026
+ if (essentialStory) parts.push(`## L1 Essential Story\n\n${essentialStory}`);
1027
+ return parts.join("\n\n");
1028
+ }
1029
+
556
1030
  //#endregion
557
1031
  //#region src/hooks/ts/lib/pai-paths.ts
558
1032
  /**
@@ -2228,6 +2702,82 @@ async function extractAndStoreTriples$1(backend, params) {
2228
2702
  return stats;
2229
2703
  }
2230
2704
 
2705
+ //#endregion
2706
+ //#region src/daemon/daemon/state.ts
2707
+ /** RegistryBackend-based access (projects, sessions, tags, aliases, links, compaction_log). */
2708
+ let registryBackend;
2709
+ let storageBackend;
2710
+ let daemonConfig;
2711
+ let startTime = Date.now();
2712
+ /** True while a project index pass is running. */
2713
+ let indexInProgress = false;
2714
+ let lastIndexTime = 0;
2715
+ let indexSchedulerTimer = null;
2716
+ /** True while an embedding pass is running. */
2717
+ let embedInProgress = false;
2718
+ let lastEmbedTime = 0;
2719
+ let embedSchedulerTimer = null;
2720
+ /** True while a vault index pass is running. */
2721
+ let vaultIndexInProgress = false;
2722
+ let lastVaultIndexTime = 0;
2723
+ /** Interval timer of the worker prompt-cache keepalive (null = not armed). */
2724
+ let cacheKeepaliveTimer = null;
2725
+ /** Mutable notification config — loaded from disk at startup, patchable at runtime. */
2726
+ let notificationConfig;
2727
+ /**
2728
+ * Set to true when a SIGTERM/SIGINT is received so that long-running loops
2729
+ * (embed, index) can detect the signal and exit their inner loops before the
2730
+ * pool/backend is closed.
2731
+ */
2732
+ let shutdownRequested = false;
2733
+ function setRegistryBackend(b) {
2734
+ registryBackend = b;
2735
+ }
2736
+ function setStorageBackend(b) {
2737
+ storageBackend = b;
2738
+ }
2739
+ function setDaemonConfig(c) {
2740
+ daemonConfig = c;
2741
+ }
2742
+ function setStartTime(t) {
2743
+ startTime = t;
2744
+ }
2745
+ function setNotificationConfig(c) {
2746
+ notificationConfig = c;
2747
+ }
2748
+ function setShutdownRequested(v) {
2749
+ shutdownRequested = v;
2750
+ }
2751
+ function setIndexInProgress(v) {
2752
+ indexInProgress = v;
2753
+ }
2754
+ function setLastIndexTime(v) {
2755
+ lastIndexTime = v;
2756
+ }
2757
+ function setIndexSchedulerTimer(v) {
2758
+ indexSchedulerTimer = v;
2759
+ }
2760
+ function setEmbedInProgress(v) {
2761
+ embedInProgress = v;
2762
+ }
2763
+ function setLastEmbedTime(v) {
2764
+ lastEmbedTime = v;
2765
+ }
2766
+ function setEmbedSchedulerTimer(v) {
2767
+ embedSchedulerTimer = v;
2768
+ }
2769
+ function setVaultIndexInProgress(v) {
2770
+ vaultIndexInProgress = v;
2771
+ }
2772
+ function setLastVaultIndexTime(v) {
2773
+ lastVaultIndexTime = v;
2774
+ }
2775
+ function setCacheKeepaliveTimer(v) {
2776
+ cacheKeepaliveTimer = v;
2777
+ }
2778
+ /** Failing passes by name ("index" | "embed" | "vault"); absent = healthy. */
2779
+ const passFailures = /* @__PURE__ */ new Map();
2780
+
2231
2781
  //#endregion
2232
2782
  //#region src/daemon/session-summary-worker.ts
2233
2783
  /**
@@ -3171,6 +3721,664 @@ function cleanup() {
3171
3721
  saveIfDirty();
3172
3722
  }
3173
3723
 
3724
+ //#endregion
3725
+ //#region src/daemon/daemon-pid.ts
3726
+ /**
3727
+ * daemon-pid.ts — the one way PAI finds the running daemon's PID.
3728
+ *
3729
+ * Never by command-line pattern: `pgrep -f` matches any process whose argv
3730
+ * mentions the text (a worker whose spec quotes it, an editor), and the caller
3731
+ * then signals the wrong process. The service manager knows the PID; when none
3732
+ * is in use (`pai daemon serve` in a container) the daemon's own pidfile does.
3733
+ */
3734
+ const LAUNCHD_LABEL = "com.pai.pai-daemon";
3735
+ const SYSTEMD_UNIT = "pai-daemon.service";
3736
+ /** `pid = N` from `launchctl print gui/<uid>/<label>`; undefined when not running. */
3737
+ function parseLaunchctlPid(out) {
3738
+ const m = /^\s*pid = (\d+)\s*$/m.exec(out);
3739
+ return m ? Number(m[1]) : void 0;
3740
+ }
3741
+ /** `MainPID=N` from `systemctl --user show -p MainPID`; 0 means not running. */
3742
+ function parseSystemdMainPid(out) {
3743
+ const n = Number(/^MainPID=(\d+)\s*$/m.exec(out)?.[1]);
3744
+ return n > 0 ? n : void 0;
3745
+ }
3746
+ const run = (cmd, args) => {
3747
+ const r = spawnSync(cmd, args, { encoding: "utf8" });
3748
+ return {
3749
+ status: r.status,
3750
+ stdout: r.stdout ?? ""
3751
+ };
3752
+ };
3753
+ /** Start time of a process (epoch ms) or undefined when it does not exist. */
3754
+ function processStart(pid, sh) {
3755
+ const r = sh("ps", [
3756
+ "-o",
3757
+ "lstart=",
3758
+ "-p",
3759
+ String(pid)
3760
+ ]);
3761
+ if (r.status !== 0 || !r.stdout.trim()) return void 0;
3762
+ const t = Date.parse(r.stdout.trim());
3763
+ return Number.isNaN(t) ? void 0 : t;
3764
+ }
3765
+ /**
3766
+ * PID from the pidfile, only if that process is alive and started no later
3767
+ * than the pidfile was written (ps start times have 1 s resolution). A process
3768
+ * that started after the file is a reused PID and is not the daemon.
3769
+ */
3770
+ function pidfilePid(path = daemonPidPath(), sh = run) {
3771
+ try {
3772
+ const pid = parseInt(readFileSync(path, "utf8").trim(), 10);
3773
+ if (!(pid > 0)) return void 0;
3774
+ const started = processStart(pid, sh);
3775
+ if (started === void 0 || started > statSync(path).mtimeMs + 2e3) return void 0;
3776
+ return pid;
3777
+ } catch {
3778
+ return;
3779
+ }
3780
+ }
3781
+ /** The running daemon's PID: service manager first, pidfile second, else undefined. */
3782
+ function resolveDaemonPid(plat = process.platform, sh = run, path = daemonPidPath()) {
3783
+ if (plat === "darwin") {
3784
+ const r = sh("launchctl", ["print", `gui/${process.getuid?.() ?? userInfo().uid}/${LAUNCHD_LABEL}`]);
3785
+ const pid = r.status === 0 ? parseLaunchctlPid(r.stdout) : void 0;
3786
+ if (pid) return pid;
3787
+ } else if (plat === "linux") {
3788
+ const r = sh("systemctl", [
3789
+ "--user",
3790
+ "show",
3791
+ "-p",
3792
+ "MainPID",
3793
+ SYSTEMD_UNIT
3794
+ ]);
3795
+ const pid = r.status === 0 ? parseSystemdMainPid(r.stdout) : void 0;
3796
+ if (pid) return pid;
3797
+ }
3798
+ return pidfilePid(path, sh);
3799
+ }
3800
+ function writeDaemonPidfile(path = daemonPidPath()) {
3801
+ try {
3802
+ mkdirSync(dirname(path), { recursive: true });
3803
+ writeFileSync(path, `${process.pid}\n`, "utf8");
3804
+ } catch {}
3805
+ }
3806
+ /** Remove the pidfile only if it is ours (a newer daemon may own it). */
3807
+ function removeDaemonPidfile(path = daemonPidPath()) {
3808
+ try {
3809
+ if (parseInt(readFileSync(path, "utf8").trim(), 10) === process.pid) unlinkSync(path);
3810
+ } catch {}
3811
+ }
3812
+
3813
+ //#endregion
3814
+ //#region src/audit/session-usage.ts
3815
+ /**
3816
+ * session-usage.ts — parse the usage numbers out of a Claude Code session
3817
+ * (or subagent, or pai-worker event-mirror) JSONL transcript.
3818
+ *
3819
+ * All three log shapes share the same assistant-message envelope
3820
+ * ({ type: "assistant", message: { id, model, usage } }), so one parser
3821
+ * covers `pai audit tokens session` and the per-log readings feeding
3822
+ * `pai audit tokens spawn`.
3823
+ *
3824
+ * Streaming writes one JSONL line per content block of the same logical
3825
+ * turn, repeating message.id and usage each time — summing every line would
3826
+ * multiply usage by the block count. Keeping only the first line seen per
3827
+ * message.id is what the reference script (and this parser) count instead.
3828
+ */
3829
+ const USAGE_KEYS = [
3830
+ "cache_read_input_tokens",
3831
+ "cache_creation_input_tokens",
3832
+ "input_tokens",
3833
+ "output_tokens"
3834
+ ];
3835
+ function emptyTotals() {
3836
+ return {
3837
+ cache_read_input_tokens: 0,
3838
+ cache_creation_input_tokens: 0,
3839
+ input_tokens: 0,
3840
+ output_tokens: 0
3841
+ };
3842
+ }
3843
+ /**
3844
+ * `type:"system"` `subtype:"compact_boundary"` lines mark a compaction.
3845
+ * turnIndex is the count of assistant turns already folded when it fired,
3846
+ * so it lines up with the turn numbering the text/JSON report prints.
3847
+ */
3848
+ function isCompactBoundary(line) {
3849
+ return line.type === "system" && line.subtype === "compact_boundary";
3850
+ }
3851
+ function parseCompactionEvent(line, turnIndex) {
3852
+ const meta = line.compactMetadata;
3853
+ if (!meta || typeof meta.preTokens !== "number") return null;
3854
+ return {
3855
+ trigger: meta.trigger ?? "unknown",
3856
+ preTokens: meta.preTokens,
3857
+ turnIndex
3858
+ };
3859
+ }
3860
+ /**
3861
+ * `type:"system"` `subtype:"model_refusal_fallback"` lines mark an
3862
+ * automatic model switch fired by an API safety refusal (e.g. "cyber"
3863
+ * category), not a user or config choice — worth flagging separately since
3864
+ * it can rebuild the whole prompt cache mid-session.
3865
+ */
3866
+ function isModelFallback(line) {
3867
+ return line.type === "system" && line.subtype === "model_refusal_fallback";
3868
+ }
3869
+ function parseFallbackEvent(line, turnIndex) {
3870
+ return {
3871
+ turnIndex,
3872
+ from: line.originalModel ?? "unknown",
3873
+ to: line.fallbackModel ?? "unknown",
3874
+ category: line.apiRefusalCategory ?? "unknown",
3875
+ scope: line.scope ?? "unknown"
3876
+ };
3877
+ }
3878
+ /**
3879
+ * Fold one already-parsed JSONL line into a report being accumulated.
3880
+ * Exported separately so both the streaming file reader below and tests
3881
+ * (which build fixtures as arrays of objects, not files) share one path.
3882
+ * Returns the turn's context value when a new turn was counted, else null
3883
+ * (non-assistant line, no usage, or a duplicate message.id already seen).
3884
+ */
3885
+ function foldAssistantLine(report, seenIds, line, threshold) {
3886
+ if (line.type !== "assistant") return null;
3887
+ const message = line.message;
3888
+ const usage = message?.usage;
3889
+ if (!usage) return null;
3890
+ const id = message?.id ?? line.uuid;
3891
+ if (id) {
3892
+ if (seenIds.has(id)) return null;
3893
+ seenIds.add(id);
3894
+ }
3895
+ report.turns++;
3896
+ for (const key of USAGE_KEYS) {
3897
+ const v = usage[key];
3898
+ if (typeof v === "number") report.totals[key] += v;
3899
+ }
3900
+ const context = (Number(usage.cache_read_input_tokens) || 0) + (Number(usage.cache_creation_input_tokens) || 0) + (Number(usage.input_tokens) || 0);
3901
+ if (report.firstTurnContext === null) {
3902
+ report.firstTurnContext = context;
3903
+ report.firstTurnAt = line.timestamp ?? null;
3904
+ }
3905
+ const atMs = line.timestamp ? Date.parse(line.timestamp) : NaN;
3906
+ if (!Number.isNaN(atMs)) {
3907
+ if (report.lastTurnAtMs !== null && atMs - report.lastTurnAtMs > 3600 * 1e3) report.idleGapsOver60min++;
3908
+ report.lastTurnAtMs = atMs;
3909
+ }
3910
+ report.lastTurnContext = context;
3911
+ report.maxContext = report.maxContext === null ? context : Math.max(report.maxContext, context);
3912
+ if (context > threshold) report.turnsAboveThreshold++;
3913
+ if ((Number(usage.cache_creation_input_tokens) || 0) > 2e4) report.cacheRebuildTurns++;
3914
+ const model = message?.model ?? "unknown";
3915
+ report.models[model] = (report.models[model] ?? 0) + 1;
3916
+ if (report.lastModel !== null && report.lastModel !== model) report.modelSwitches.push({
3917
+ turnIndex: report.turns,
3918
+ from: report.lastModel,
3919
+ to: model,
3920
+ cacheRead: Number(usage.cache_read_input_tokens) || 0,
3921
+ cacheCreation: Number(usage.cache_creation_input_tokens) || 0
3922
+ });
3923
+ report.lastModel = model;
3924
+ const split = usage.cache_creation;
3925
+ if (split) {
3926
+ report.cacheCreationSplit.ephemeral5m += Number(split.ephemeral_5m_input_tokens) || 0;
3927
+ report.cacheCreationSplit.ephemeral1h += Number(split.ephemeral_1h_input_tokens) || 0;
3928
+ }
3929
+ return context;
3930
+ }
3931
+ /**
3932
+ * True for a real human-authored `type:"user"` prompt line: content is a
3933
+ * plain string, or a content array with no `tool_result` block. Claude Code
3934
+ * encodes tool results as `type:"user"` messages whose content array is
3935
+ * entirely (or partly) tool_result blocks — those must not count as prompts.
3936
+ */
3937
+ function isRealUserPrompt(line) {
3938
+ if (line.type !== "user") return false;
3939
+ if (line.isMeta) return false;
3940
+ if (line.origin?.kind && line.origin.kind !== "human") return false;
3941
+ const content = line.message?.content;
3942
+ if (typeof content === "string") return true;
3943
+ if (Array.isArray(content)) return !content.some((block) => block?.type === "tool_result");
3944
+ return false;
3945
+ }
3946
+ /** Plain text of a real user prompt: the string content, or its first text block. */
3947
+ function extractUserPromptText(line) {
3948
+ const content = line.message?.content;
3949
+ if (typeof content === "string") return content;
3950
+ if (Array.isArray(content)) return content.find((b) => b?.type === "text")?.text ?? "";
3951
+ return "";
3952
+ }
3953
+ /**
3954
+ * Parse a session/subagent/worker-event JSONL file into a usage report.
3955
+ * `keepaliveWord`, when given, counts real user prompts whose text exactly
3956
+ * matches it (trimmed) — the sessions.cacheKeepalive beat prompt — into
3957
+ * `keepaliveBeats`; omitted, `keepaliveBeats` stays null.
3958
+ */
3959
+ async function parseSessionUsage(path, threshold = 2e5, keepaliveWord) {
3960
+ const report = {
3961
+ path,
3962
+ sizeBytes: existsSync(path) ? readFileSync(path).byteLength : 0,
3963
+ turns: 0,
3964
+ totals: emptyTotals(),
3965
+ models: {},
3966
+ firstTurnContext: null,
3967
+ lastTurnContext: null,
3968
+ cacheCreationSplit: {
3969
+ ephemeral5m: 0,
3970
+ ephemeral1h: 0
3971
+ },
3972
+ avgContext: null,
3973
+ maxContext: null,
3974
+ turnsAboveThreshold: 0,
3975
+ cacheRebuildTurns: 0,
3976
+ userPrompts: 0,
3977
+ promptExposure: 0,
3978
+ compactions: [],
3979
+ firstTurnAt: null,
3980
+ lastModel: null,
3981
+ modelSwitches: [],
3982
+ fallbacks: [],
3983
+ lastTurnAtMs: null,
3984
+ idleGapsOver60min: 0,
3985
+ keepaliveBeats: keepaliveWord ? 0 : null
3986
+ };
3987
+ const seenIds = /* @__PURE__ */ new Set();
3988
+ let contextSum = 0;
3989
+ const rl = createInterface({
3990
+ input: createReadStream(path, "utf8"),
3991
+ crlfDelay: Infinity
3992
+ });
3993
+ for await (const raw of rl) {
3994
+ if (!raw.trim()) continue;
3995
+ let obj;
3996
+ try {
3997
+ obj = JSON.parse(raw);
3998
+ } catch {
3999
+ continue;
4000
+ }
4001
+ if (obj.type === "user") {
4002
+ if (isRealUserPrompt(obj)) {
4003
+ report.userPrompts++;
4004
+ if (keepaliveWord && extractUserPromptText(obj).trim() === keepaliveWord) report.keepaliveBeats = (report.keepaliveBeats ?? 0) + 1;
4005
+ }
4006
+ continue;
4007
+ }
4008
+ if (isCompactBoundary(obj)) {
4009
+ const event = parseCompactionEvent(obj, report.turns);
4010
+ if (event) report.compactions.push(event);
4011
+ continue;
4012
+ }
4013
+ if (isModelFallback(obj)) {
4014
+ report.fallbacks.push(parseFallbackEvent(obj, report.turns));
4015
+ continue;
4016
+ }
4017
+ const context = foldAssistantLine(report, seenIds, obj, threshold);
4018
+ if (context !== null) {
4019
+ contextSum += context;
4020
+ report.promptExposure += report.userPrompts;
4021
+ }
4022
+ }
4023
+ report.avgContext = report.turns > 0 ? Math.round(contextSum / report.turns) : null;
4024
+ return report;
4025
+ }
4026
+ function totalUsageTokens(totals) {
4027
+ return totals.cache_read_input_tokens + totals.cache_creation_input_tokens + totals.input_tokens + totals.output_tokens;
4028
+ }
4029
+
4030
+ //#endregion
4031
+ //#region src/daemon/session-keepalive.ts
4032
+ /**
4033
+ * session-keepalive.ts — idle-triggered prompt-cache keepalive beat for live
4034
+ * interactive Claude Code sessions (`sessions.cacheKeepalive`, see config.ts
4035
+ * and docs/cache-keepalive.md, "Interactive sessions").
4036
+ *
4037
+ * One beat = one trivial prompt typed into a session through AIBroker's
4038
+ * send_to_session (src/cli/lib/aibroker-client.ts), sent with `noReply: true`
4039
+ * so the target sees only the bare word typed into its input line and
4040
+ * nothing is queued into its mailbox as a peer message demanding a reply.
4041
+ * A cache READ refreshes the provider's ephemeral prompt-cache TTL at
4042
+ * roughly 0.1x the cost of the 2x rewrite a cold cache forces on the next
4043
+ * real prompt.
4044
+ *
4045
+ * Distinct from workers/keepalive.ts, which beats a *worker provider's*
4046
+ * cache on a fixed timer regardless of activity: this only beats a session
4047
+ * that has actually been idle long enough to be at risk, and only inside a
4048
+ * configured working-hours window, so it never fires while the user is
4049
+ * present at the keyboard or overnight when nobody will read the reply
4050
+ * before the cache would have expired anyway.
4051
+ */
4052
+ function sessionKeepaliveStatePath() {
4053
+ return paiHomePath("session-keepalive.json");
4054
+ }
4055
+ function emptyEntry() {
4056
+ return {
4057
+ beats: 0,
4058
+ lastRealPromptKey: null,
4059
+ lastBeatAt: null
4060
+ };
4061
+ }
4062
+ function loadSessionKeepaliveState(path = sessionKeepaliveStatePath()) {
4063
+ if (!existsSync(path)) return {};
4064
+ try {
4065
+ return JSON.parse(readFileSync(path, "utf8"));
4066
+ } catch {
4067
+ return {};
4068
+ }
4069
+ }
4070
+ function saveSessionKeepaliveState(state, path = sessionKeepaliveStatePath()) {
4071
+ writeJsonAtomic(path, state, {
4072
+ backup: false,
4073
+ label: path
4074
+ });
4075
+ }
4076
+ /** Parse "HH:MM-HH:MM" into minutes-since-midnight. Throws on a malformed
4077
+ * spec — an explicit config error beats a window that is silently always
4078
+ * on or always off. */
4079
+ function parseActiveHours(spec) {
4080
+ const m = spec.match(/^(\d{1,2}):(\d{2})-(\d{1,2}):(\d{2})$/);
4081
+ if (!m) throw new Error(`sessions.cacheKeepalive.activeHours: invalid "${spec}" (want "HH:MM-HH:MM")`);
4082
+ return {
4083
+ startMin: Number(m[1]) * 60 + Number(m[2]),
4084
+ endMin: Number(m[3]) * 60 + Number(m[4])
4085
+ };
4086
+ }
4087
+ /**
4088
+ * Whether `now` (local time) sits inside the window. A window that wraps
4089
+ * midnight (e.g. "22:00-06:00") is honoured by inverting the test instead of
4090
+ * requiring startMin < endMin.
4091
+ */
4092
+ function isWithinActiveHours(now, spec) {
4093
+ const { startMin, endMin } = parseActiveHours(spec);
4094
+ const nowMin = now.getHours() * 60 + now.getMinutes();
4095
+ if (startMin <= endMin) return nowMin >= startMin && nowMin < endMin;
4096
+ return nowMin >= startMin || nowMin < endMin;
4097
+ }
4098
+ /**
4099
+ * Full path to a live session's transcript under ~/.claude/projects, or null
4100
+ * when none is found — the case for a session too new to have written a file
4101
+ * yet. A worker running in a worktree DOES write a transcript here (Claude
4102
+ * Code encodes its worktree cwd as the project dir name), so workers are not
4103
+ * excluded "for free" — see isWorkerSession.
4104
+ */
4105
+ function findSessionTranscript(sessionId, projectsDir = join(homedir(), ".claude", "projects")) {
4106
+ let projectDirs;
4107
+ try {
4108
+ projectDirs = readdirSync(projectsDir);
4109
+ } catch {
4110
+ return null;
4111
+ }
4112
+ for (const projectDir of projectDirs) {
4113
+ const candidate = join(projectsDir, projectDir, `${sessionId}.jsonl`);
4114
+ if (existsSync(candidate)) return candidate;
4115
+ }
4116
+ return null;
4117
+ }
4118
+ /**
4119
+ * AIBroker's `fetchLiveSessions()` identifies a session by its iTerm2 pane id
4120
+ * (e.g. an AIBroker/iTerm UUID), not the Claude session id `<uuid>.jsonl`
4121
+ * transcripts are named after — the two are unrelated identifiers. The
4122
+ * status line bridges them on every refresh via `claude-session-map.json`
4123
+ * (see `recordSessionMapEntry` in ../workers/scope.ts): this picks, among the
4124
+ * map entries whose `term` pane UUID matches, the most recently written one.
4125
+ * Returns null when the pane has no (fresh enough) mapped Claude session.
4126
+ */
4127
+ function resolveClaudeSessionIdFromMap(paneId, logDir) {
4128
+ const path = sessionMapPath(logDir);
4129
+ if (!paneId || !existsSync(path)) return null;
4130
+ let map;
4131
+ try {
4132
+ map = JSON.parse(readFileSync(path, "utf8"));
4133
+ } catch {
4134
+ return null;
4135
+ }
4136
+ let best = null;
4137
+ for (const entry of Object.values(map)) {
4138
+ if (!entry.term || itermUuid(entry.term) !== paneId) continue;
4139
+ if (!best || entry.ts > best.ts) best = entry;
4140
+ }
4141
+ return best?.session ?? null;
4142
+ }
4143
+ /** Claude Code's project-dir encoding of a cwd: every "/" becomes "-". */
4144
+ function encodeProjectDirName(cwd) {
4145
+ return cwd.replace(/\//g, "-");
4146
+ }
4147
+ /**
4148
+ * Is this live "claude"-kind session actually a worker pane rather than an
4149
+ * interactive one? `claude -p` workers running in a worktree write their
4150
+ * transcript under ~/.claude/projects too (encoded cwd = the worktree dir),
4151
+ * so a missing transcript is NOT how workers get excluded — this predicate
4152
+ * is. Either signal is enough:
4153
+ * (a) the transcript's project-dir name is the encoded form of a path
4154
+ * under <logDir>/worktrees (every worker worktree lives there), or
4155
+ * (b) the broker's session name/paiName names a path under the workers
4156
+ * log dir or one of its worktrees (best-effort: AIBroker does not
4157
+ * guarantee this, but honors it when present).
4158
+ */
4159
+ function isWorkerSession(meta, transcriptPath, logDir) {
4160
+ const worktreesPrefix = encodeProjectDirName(worktreesDir(logDir));
4161
+ if (transcriptPath) {
4162
+ if ((transcriptPath.split("/").slice(0, -1).pop() ?? "").startsWith(worktreesPrefix)) return true;
4163
+ }
4164
+ const rawPrefix = worktreesDir(logDir);
4165
+ for (const field of [meta.name, meta.paiName]) if (field && (field.includes(rawPrefix) || field.includes(logDir))) return true;
4166
+ return false;
4167
+ }
4168
+ function isMidTurn(line) {
4169
+ if (!line) return false;
4170
+ if (line.type === "user" && isRealUserPrompt(line)) return true;
4171
+ if (line.type === "assistant") {
4172
+ const stopReason = line.message?.stop_reason;
4173
+ if (stopReason === void 0 || stopReason === null) return true;
4174
+ return stopReason !== "end_turn" && stopReason !== "stop_sequence";
4175
+ }
4176
+ return false;
4177
+ }
4178
+ function readTranscriptSnapshot(path) {
4179
+ let lines;
4180
+ try {
4181
+ lines = readFileSync(path, "utf8").split("\n");
4182
+ } catch {
4183
+ return {
4184
+ context: null,
4185
+ midTurn: false,
4186
+ lastRealPrompt: null
4187
+ };
4188
+ }
4189
+ let lastLine = null;
4190
+ let context = null;
4191
+ let lastRealPrompt = null;
4192
+ for (let i = lines.length - 1; i >= 0; i--) {
4193
+ const raw = lines[i].trim();
4194
+ if (!raw) continue;
4195
+ let obj;
4196
+ try {
4197
+ obj = JSON.parse(raw);
4198
+ } catch {
4199
+ continue;
4200
+ }
4201
+ if (lastLine === null) lastLine = obj;
4202
+ if (context === null && obj.type === "assistant" && obj.message?.usage) {
4203
+ const usage = obj.message.usage;
4204
+ context = (Number(usage.cache_read_input_tokens) || 0) + (Number(usage.cache_creation_input_tokens) || 0) + (Number(usage.input_tokens) || 0);
4205
+ }
4206
+ if (lastRealPrompt === null && isRealUserPrompt(obj)) lastRealPrompt = {
4207
+ key: obj.uuid ?? obj.timestamp ?? String(i),
4208
+ text: extractUserPromptText(obj).trim()
4209
+ };
4210
+ if (context !== null && lastRealPrompt !== null) break;
4211
+ }
4212
+ return {
4213
+ context,
4214
+ midTurn: isMidTurn(lastLine),
4215
+ lastRealPrompt
4216
+ };
4217
+ }
4218
+ /** Ledger event tag; `pai daemon keepalive` and any log-tailer key off this. */
4219
+ const SESSION_KEEPALIVE_EVENT = "SESSION-KEEPALIVE";
4220
+ function sessionKeepaliveLedgerPath() {
4221
+ const { workers } = readWorkersSection();
4222
+ return join(workersLogDir(workers), "ledger.log");
4223
+ }
4224
+ /** The counts/last lines `pai daemon keepalive` prints, scoped to this event. */
4225
+ function sessionKeepaliveLedgerSummary(path = sessionKeepaliveLedgerPath(), lastN = 10) {
4226
+ if (!existsSync(path)) return {
4227
+ sent: 0,
4228
+ skipped: 0,
4229
+ lastLines: []
4230
+ };
4231
+ const lines = readFileSync(path, "utf8").split("\n").filter((l) => l.includes(SESSION_KEEPALIVE_EVENT));
4232
+ const sent = lines.filter((l) => / result=sent(\s|$)/.test(l)).length;
4233
+ return {
4234
+ sent,
4235
+ skipped: lines.length - sent,
4236
+ lastLines: lines.slice(-lastN)
4237
+ };
4238
+ }
4239
+ function defaultDeps() {
4240
+ const { workers } = readWorkersSection();
4241
+ const logDir = workersLogDir(workers);
4242
+ return {
4243
+ now: () => /* @__PURE__ */ new Date(),
4244
+ fetchLiveSessions,
4245
+ resolveClaudeSessionId: (paneId) => resolveClaudeSessionIdFromMap(paneId, logDir),
4246
+ findTranscript: (id) => findSessionTranscript(id),
4247
+ mtimeMs: (p) => {
4248
+ try {
4249
+ return statSync(p).mtimeMs;
4250
+ } catch {
4251
+ return null;
4252
+ }
4253
+ },
4254
+ readSnapshot: readTranscriptSnapshot,
4255
+ sendBeat: (id, text) => sendToSession(id, text, void 0, { noReply: true }),
4256
+ loadState: () => loadSessionKeepaliveState(),
4257
+ saveState: (s) => saveSessionKeepaliveState(s),
4258
+ ledger: (kv) => appendLedger(sessionKeepaliveLedgerPath(), SESSION_KEEPALIVE_EVENT, kv)
4259
+ };
4260
+ }
4261
+ /**
4262
+ * One tick over every live interactive session: beat the ones that qualify,
4263
+ * skip (with a reason) the ones that don't. Returns one result per live
4264
+ * "claude"-kind session for tests to assert against; writes the ledger line
4265
+ * and (when any counter changed) the state file as side effects.
4266
+ */
4267
+ async function runSessionKeepaliveTick(config, overrides = {}) {
4268
+ if (!config.enabled) return [];
4269
+ const d = {
4270
+ ...defaultDeps(),
4271
+ ...overrides
4272
+ };
4273
+ const sessions = await d.fetchLiveSessions();
4274
+ const state = d.loadState();
4275
+ const now = d.now();
4276
+ const { workers } = readWorkersSection();
4277
+ const logDir = workersLogDir(workers);
4278
+ const results = [];
4279
+ let anyChanged = false;
4280
+ for (const s of sessions) {
4281
+ if (s.kind !== "claude") continue;
4282
+ const paneId = s.sessionId;
4283
+ const claudeId = d.resolveClaudeSessionId(paneId);
4284
+ if (!claudeId) {
4285
+ d.ledger({
4286
+ session: null,
4287
+ pane: paneId,
4288
+ result: "skipped:unmapped"
4289
+ });
4290
+ results.push({
4291
+ sessionId: paneId,
4292
+ result: "skipped:unmapped"
4293
+ });
4294
+ continue;
4295
+ }
4296
+ const sessionId = claudeId;
4297
+ const entry = { ...state[sessionId] ?? emptyEntry() };
4298
+ let changed = false;
4299
+ const transcript = d.findTranscript(sessionId);
4300
+ if (isWorkerSession(s, transcript, logDir)) {
4301
+ d.ledger({
4302
+ session: sessionId,
4303
+ pane: paneId,
4304
+ result: "skipped:worker"
4305
+ });
4306
+ results.push({
4307
+ sessionId,
4308
+ result: "skipped:worker"
4309
+ });
4310
+ continue;
4311
+ }
4312
+ if (!transcript) {
4313
+ d.ledger({
4314
+ session: sessionId,
4315
+ pane: paneId,
4316
+ result: "skipped:no-transcript"
4317
+ });
4318
+ results.push({
4319
+ sessionId,
4320
+ result: "skipped:no-transcript"
4321
+ });
4322
+ continue;
4323
+ }
4324
+ const snapshot = d.readSnapshot(transcript);
4325
+ if (snapshot.lastRealPrompt && snapshot.lastRealPrompt.key !== entry.lastRealPromptKey) {
4326
+ entry.lastRealPromptKey = snapshot.lastRealPrompt.key;
4327
+ if (snapshot.lastRealPrompt.text !== config.prompt) entry.beats = 0;
4328
+ changed = true;
4329
+ }
4330
+ const mtime = d.mtimeMs(transcript);
4331
+ const idleMin = mtime === null ? null : (now.getTime() - mtime) / 6e4;
4332
+ const skip = mtime === null || idleMin === null ? "no-mtime" : idleMin < config.idleMinutes ? `idle:${idleMin.toFixed(1)}min` : !isWithinActiveHours(now, config.activeHours) ? "hours" : snapshot.context === null || snapshot.context < config.minContextTokens ? "context" : snapshot.midTurn ? "mid-turn" : entry.beats >= config.maxBeats ? "max-beats" : null;
4333
+ const ledgerBase = {
4334
+ session: sessionId,
4335
+ pane: paneId,
4336
+ idle_min: idleMin === null ? null : idleMin.toFixed(1),
4337
+ context: snapshot.context
4338
+ };
4339
+ if (skip) {
4340
+ d.ledger({
4341
+ ...ledgerBase,
4342
+ beat: `${entry.beats}/${config.maxBeats}`,
4343
+ result: `skipped:${skip}`
4344
+ });
4345
+ results.push({
4346
+ sessionId,
4347
+ result: `skipped:${skip}`
4348
+ });
4349
+ } else if ((await d.sendBeat(paneId, config.prompt)).ok) {
4350
+ entry.beats += 1;
4351
+ entry.lastBeatAt = now.toISOString();
4352
+ changed = true;
4353
+ d.ledger({
4354
+ ...ledgerBase,
4355
+ beat: `${entry.beats}/${config.maxBeats}`,
4356
+ result: "sent"
4357
+ });
4358
+ results.push({
4359
+ sessionId,
4360
+ result: "sent"
4361
+ });
4362
+ } else {
4363
+ d.ledger({
4364
+ ...ledgerBase,
4365
+ beat: `${entry.beats}/${config.maxBeats}`,
4366
+ result: "skipped:send-failed"
4367
+ });
4368
+ results.push({
4369
+ sessionId,
4370
+ result: "skipped:send-failed"
4371
+ });
4372
+ }
4373
+ if (changed) {
4374
+ state[sessionId] = entry;
4375
+ anyChanged = true;
4376
+ }
4377
+ }
4378
+ if (anyChanged) d.saveState(state);
4379
+ return results;
4380
+ }
4381
+
3174
4382
  //#endregion
3175
4383
  //#region src/hooks/ts/lib/context-handover-cache.ts
3176
4384
  /**
@@ -3203,5 +4411,5 @@ function writeContextHandoverCache(cache) {
3203
4411
  }
3204
4412
 
3205
4413
  //#endregion
3206
- export { archiveSessionFilesToSessionsDir as A, claudeProjectsDir as B, findLatestNote as C, appendCheckpoint as D, addWorkToSessionNote as E, performScan as F, scanTranscriptFolders as H, resolveHome as I, saveScanConfig as L, cmdScan as M, loadScanConfig as N, finalizeSessionNote as O, migrateScanConfig as P, upsertProject as R, applyContinue as S, readBodyFile as T, transcriptFiles as U, findMovedProjects as V, spawnSummarizer as _, enqueue as a, updateTodoContinue as b, loadQueue as c, migrateWorkQueue as d, contentToText as f, migrateSummaryCooldowns as g, handleSessionSummary as h, dequeue as i, findNotesDir$1 as j, getCurrentNotePath as k, markCompleted as l, getGitContext as m, writeContextHandoverCache as n, getStats as o, findLatestJsonl as p, cleanup as r, hasPendingOrProcessingOfType as s, readContextHandoverCache as t, markFailed as u, extractAndStoreTriples$1 as v, findNotesDir as w, appendCheckpointToNote as x, sessionIdFromTranscript as y, upsertSession as z };
3207
- //# sourceMappingURL=context-handover-cache-pkHzmL3c.mjs.map
4414
+ export { shutdownRequested as $, indexInProgress as A, claudeProjectsDir as At, setEmbedInProgress as B, handleSessionSummary as C, loadScanConfig as Ct, daemonConfig as D, saveScanConfig as Dt, cacheKeepaliveTimer as E, resolveHome as Et, notificationConfig as F, formatDetection as Ft, setLastIndexTime as G, setIndexInProgress as H, passFailures as I, formatDetectionJson as It, setRegistryBackend as J, setLastVaultIndexTime as K, registryBackend as L, lastEmbedTime as M, scanTranscriptFolders as Mt, lastIndexTime as N, transcriptFiles as Nt, embedInProgress as O, upsertProject as Ot, lastVaultIndexTime as P, detectProject as Pt, setVaultIndexInProgress as Q, setCacheKeepaliveTimer as R, getGitContext as S, cmdScan as St, spawnSummarizer as T, performScan as Tt, setIndexSchedulerTimer as U, setEmbedSchedulerTimer as V, setLastEmbedTime as W, setStartTime as X, setShutdownRequested as Y, setStorageBackend as Z, markCompleted as _, buildWakeupContext as _t, sessionKeepaliveLedgerSummary as a, updateTodoContinue as at, contentToText as b, reexecBackground as bt, removeDaemonPidfile as c, findLatestNote as ct, cleanup as d, addWorkToSessionNote as dt, startTime as et, dequeue as f, appendCheckpoint as ft, loadQueue as g, findNotesDir$1 as gt, hasPendingOrProcessingOfType as h, archiveSessionFilesToSessionsDir as ht, runSessionKeepaliveTick as i, sessionIdFromTranscript as it, indexSchedulerTimer as j, findMovedProjects as jt, embedSchedulerTimer as k, upsertSession as kt, resolveDaemonPid as l, findNotesDir as lt, getStats as m, getCurrentNotePath as mt, writeContextHandoverCache as n, vaultIndexInProgress as nt, parseSessionUsage as o, appendCheckpointToNote as ot, enqueue as p, finalizeSessionNote as pt, setNotificationConfig as q, loadSessionKeepaliveState as r, extractAndStoreTriples$1 as rt, totalUsageTokens as s, applyContinue as st, readContextHandoverCache as t, storageBackend as tt, writeDaemonPidfile as u, readBodyFile as ut, markFailed as v, migrateIdentityFile as vt, migrateSummaryCooldowns as w, migrateScanConfig as wt, findLatestJsonl as x, runPassChild as xt, migrateWorkQueue as y, PassSpawnError as yt, setDaemonConfig as z };
4415
+ //# sourceMappingURL=context-handover-cache-DcPEW6vf.mjs.map