@tekmidian/pai 0.66.0 → 0.66.2

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 (165) hide show
  1. package/dist/{aibroker-client-B8c42Lh8.mjs → aibroker-client-CHEEvJZW.mjs} +2 -2
  2. package/dist/{aibroker-client-C5Fw7DNz.mjs → aibroker-client-Dfv7j1Od.mjs} +3 -3
  3. package/dist/{aibroker-client-C5Fw7DNz.mjs.map → aibroker-client-Dfv7j1Od.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-D4PuQ2c-.mjs → chain-D5876UQX.mjs} +3 -4
  8. package/dist/{chain-D4PuQ2c-.mjs.map → chain-D5876UQX.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-BbLFD7Uf.mjs → config-C-LiGlop.mjs} +2 -2
  13. package/dist/{config-BbLFD7Uf.mjs.map → config-C-LiGlop.mjs.map} +1 -1
  14. package/dist/{config-YinjgXEJ.mjs → config-DKIA7wgF.mjs} +1 -1
  15. package/dist/{context-handover-cache-pkHzmL3c.mjs → context-handover-cache-9PGvXRIw.mjs} +984 -10
  16. package/dist/context-handover-cache-9PGvXRIw.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-BUgNvMUh.mjs +5029 -0
  20. package/dist/daemon-BUgNvMUh.mjs.map +1 -0
  21. package/dist/daemon-DL0eglMJ.mjs +18 -0
  22. package/dist/daemon-mcp/index.mjs +15 -10
  23. package/dist/daemon-mcp/index.mjs.map +1 -1
  24. package/dist/{embeddings-CcscYWwk.mjs → embeddings-CEBGrzwu.mjs} +1 -1
  25. package/dist/{embeddings-Bx3q0QOY.mjs → embeddings-DOLZnT1X.mjs} +1 -1
  26. package/dist/{embeddings-Bx3q0QOY.mjs.map → embeddings-DOLZnT1X.mjs.map} +1 -1
  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-DrBg24UT.mjs +8 -0
  30. package/dist/factory-Ypf8r0fK.mjs +2892 -0
  31. package/dist/factory-Ypf8r0fK.mjs.map +1 -0
  32. package/dist/{fallback-B5svk75f.mjs → fallback-D51R4shY.mjs} +27 -13
  33. package/dist/fallback-D51R4shY.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.map +2 -2
  37. package/dist/hooks/load-project-context.mjs.map +2 -2
  38. package/dist/hooks/worker-guard.mjs +5 -0
  39. package/dist/hooks/worker-guard.mjs.map +2 -2
  40. package/dist/hooks/worker-status-line.mjs +1 -0
  41. package/dist/hooks/worker-status-line.mjs.map +2 -2
  42. package/dist/index.mjs +6 -7
  43. package/dist/{ipc-client-D16Xw6Uo.mjs → ipc-client-BLYX51nG.mjs} +2 -2
  44. package/dist/{ipc-client-D16Xw6Uo.mjs.map → ipc-client-BLYX51nG.mjs.map} +1 -1
  45. package/dist/main-resolver-BYgP86C1.mjs +7 -0
  46. package/dist/{main-resolver-DPPwHNtn.mjs → main-resolver-kVkMJZUD.mjs} +6 -6
  47. package/dist/{main-resolver-DPPwHNtn.mjs.map → main-resolver-kVkMJZUD.mjs.map} +1 -1
  48. package/dist/{migrate-BD7D8EEh.mjs → migrate-Bq0esMOa.mjs} +2 -2
  49. package/dist/{migrate-BD7D8EEh.mjs.map → migrate-Bq0esMOa.mjs.map} +1 -1
  50. package/dist/{pai-marker-D1MMswkz.mjs → pai-marker-DXVpFsYz.mjs} +1 -1
  51. package/dist/{pai-marker-D1MMswkz.mjs.map → pai-marker-DXVpFsYz.mjs.map} +1 -1
  52. package/dist/{planner-EJPZnnif.mjs → planner-D5yt0EfA.mjs} +10 -7
  53. package/dist/{planner-EJPZnnif.mjs.map → planner-D5yt0EfA.mjs.map} +1 -1
  54. package/dist/postgres-BmLr0MUm.mjs +5 -0
  55. package/dist/{postgres-Ceqsa64C.mjs → postgres-D3xc2RB4.mjs} +364 -9
  56. package/dist/postgres-D3xc2RB4.mjs.map +1 -0
  57. package/dist/{program-DSETzrWg.mjs → program-DIXRtAFy.mjs} +80 -70
  58. package/dist/program-DIXRtAFy.mjs.map +1 -0
  59. package/dist/{query-feedback-BIaZTTFO.mjs → query-feedback-B7FYE4JR.mjs} +1 -1
  60. package/dist/{query-feedback-BIaZTTFO.mjs.map → query-feedback-B7FYE4JR.mjs.map} +1 -1
  61. package/dist/{reranker-DKv80KO5.mjs → reranker-3lnggwgq.mjs} +1 -1
  62. package/dist/{reranker-DKv80KO5.mjs.map → reranker-3lnggwgq.mjs.map} +1 -1
  63. package/dist/{reranker-CFiEzHQu.mjs → reranker-CZ2mP4cf.mjs} +1 -1
  64. package/dist/router-BK-hFeQ6.mjs +3 -0
  65. package/dist/{router-a5G7q8_3.mjs → router-BaTbc9VX.mjs} +2 -2
  66. package/dist/{router-a5G7q8_3.mjs.map → router-BaTbc9VX.mjs.map} +1 -1
  67. package/dist/{run-Z4akoa8G.mjs → run-CLYTOm0x.mjs} +764 -25
  68. package/dist/run-CLYTOm0x.mjs.map +1 -0
  69. package/dist/{run-env-DRcp7A8K.mjs → run-env-Bnmf6bRI.mjs} +2 -2
  70. package/dist/{run-env-DRcp7A8K.mjs.map → run-env-Bnmf6bRI.mjs.map} +1 -1
  71. package/dist/{runtime-paths-CHTg3ywb.mjs → runtime-paths-D3FKAm69.mjs} +1 -1
  72. package/dist/{runtime-paths-CHTg3ywb.mjs.map → runtime-paths-D3FKAm69.mjs.map} +1 -1
  73. package/dist/{search-BOSphCJ1.mjs → search-CNAGTiJP.mjs} +2 -2
  74. package/dist/{search-BOSphCJ1.mjs.map → search-CNAGTiJP.mjs.map} +1 -1
  75. package/dist/{sources-CijVso3n.mjs → sources-CM2g-CLT.mjs} +2 -2
  76. package/dist/{sources-CijVso3n.mjs.map → sources-CM2g-CLT.mjs.map} +1 -1
  77. package/dist/{stop-words-BdQuaE9K.mjs → stop-words-DtxaTWU_.mjs} +1 -1
  78. package/dist/{stop-words-BdQuaE9K.mjs.map → stop-words-DtxaTWU_.mjs.map} +1 -1
  79. package/dist/{utils-C9HsYDpO.mjs → utils-DddRwMsG.mjs} +2 -2
  80. package/dist/{utils-C9HsYDpO.mjs.map → utils-DddRwMsG.mjs.map} +1 -1
  81. package/dist/{zettelkasten-CtGHQWPU.mjs → zettelkasten-BhZMvmoK.mjs} +149 -7
  82. package/dist/zettelkasten-BhZMvmoK.mjs.map +1 -0
  83. package/dist/zettelkasten-BwV1pluL.mjs +5 -0
  84. package/docs/commands/worker.md +2 -0
  85. package/package.json +1 -1
  86. package/src/hooks/ts/lib/worker-guard.test.ts +12 -0
  87. package/src/hooks/ts/lib/worker-guard.ts +9 -0
  88. package/dist/async-C2Bm_Lal.mjs +0 -300
  89. package/dist/async-C2Bm_Lal.mjs.map +0 -1
  90. package/dist/auto-route-o0BOsXn7.mjs.map +0 -1
  91. package/dist/chunker-BH4i-F2b.mjs.map +0 -1
  92. package/dist/clusters-BCtD3fbe.mjs +0 -169
  93. package/dist/clusters-BCtD3fbe.mjs.map +0 -1
  94. package/dist/context-handover-cache-pkHzmL3c.mjs.map +0 -1
  95. package/dist/daemon-BEoVgVfJ.mjs +0 -1741
  96. package/dist/daemon-BEoVgVfJ.mjs.map +0 -1
  97. package/dist/daemon-DPAYU9hp.mjs +0 -19
  98. package/dist/detector-BEPFsINR.mjs +0 -3
  99. package/dist/detector-BRTtAWrM.mjs +0 -65
  100. package/dist/detector-BRTtAWrM.mjs.map +0 -1
  101. package/dist/factory-CNpPQQ-5.mjs +0 -249
  102. package/dist/factory-CNpPQQ-5.mjs.map +0 -1
  103. package/dist/factory-DY7x73mE.mjs +0 -3
  104. package/dist/fallback-B5svk75f.mjs.map +0 -1
  105. package/dist/federation-db-BTyoufBh.mjs +0 -139
  106. package/dist/federation-db-BTyoufBh.mjs.map +0 -1
  107. package/dist/federation-db-HfIFc7FG.mjs +0 -3
  108. package/dist/helpers-BRCJg0G3.mjs +0 -223
  109. package/dist/helpers-BRCJg0G3.mjs.map +0 -1
  110. package/dist/indexer-backend-DFF2FrYx.mjs +0 -5
  111. package/dist/indexer-backend-isSLg6yE.mjs +0 -1
  112. package/dist/kg-entity-DCOcsVFD.mjs +0 -29
  113. package/dist/kg-entity-DCOcsVFD.mjs.map +0 -1
  114. package/dist/latent-ideas-wwAXquGe.mjs +0 -191
  115. package/dist/latent-ideas-wwAXquGe.mjs.map +0 -1
  116. package/dist/link-boost-CnI7UVMJ.mjs +0 -34
  117. package/dist/link-boost-CnI7UVMJ.mjs.map +0 -1
  118. package/dist/main-resolver-BKz_OuEh.mjs +0 -7
  119. package/dist/merge-2gqRPFu2.mjs +0 -3
  120. package/dist/merge-DgU9OgZy.mjs +0 -6
  121. package/dist/merge-DgU9OgZy.mjs.map +0 -1
  122. package/dist/module-paths-DdRzbkUI.mjs +0 -44
  123. package/dist/module-paths-DdRzbkUI.mjs.map +0 -1
  124. package/dist/neighborhood-2FsmzoxG.mjs +0 -114
  125. package/dist/neighborhood-2FsmzoxG.mjs.map +0 -1
  126. package/dist/note-context-De63k8na.mjs +0 -106
  127. package/dist/note-context-De63k8na.mjs.map +0 -1
  128. package/dist/postgres-Ceqsa64C.mjs.map +0 -1
  129. package/dist/program-DSETzrWg.mjs.map +0 -1
  130. package/dist/query-feedback-B_iigYj-.mjs +0 -3
  131. package/dist/registry-db-C7voqML9.mjs +0 -213
  132. package/dist/registry-db-C7voqML9.mjs.map +0 -1
  133. package/dist/registry-db-JHPhA8vF.mjs +0 -3
  134. package/dist/registry-postgres-nOfyBSIW.mjs +0 -795
  135. package/dist/registry-postgres-nOfyBSIW.mjs.map +0 -1
  136. package/dist/registry-sqlite-DrCW1aRK.mjs +0 -593
  137. package/dist/registry-sqlite-DrCW1aRK.mjs.map +0 -1
  138. package/dist/router-CXUGsv85.mjs +0 -3
  139. package/dist/run-Z4akoa8G.mjs.map +0 -1
  140. package/dist/search-Bf3Kub3F.mjs +0 -3
  141. package/dist/server-DgmAHyFK.mjs +0 -403
  142. package/dist/server-DgmAHyFK.mjs.map +0 -1
  143. package/dist/session-keepalive-BDhytXEo.mjs +0 -582
  144. package/dist/session-keepalive-BDhytXEo.mjs.map +0 -1
  145. package/dist/sqlite-CqsTy6xo.mjs +0 -936
  146. package/dist/sqlite-CqsTy6xo.mjs.map +0 -1
  147. package/dist/state-DW8zdweW.mjs +0 -3
  148. package/dist/state-HyjqTihC.mjs +0 -76
  149. package/dist/state-HyjqTihC.mjs.map +0 -1
  150. package/dist/themes-CTaOj3e1.mjs +0 -148
  151. package/dist/themes-CTaOj3e1.mjs.map +0 -1
  152. package/dist/tools-BcOKPBtg.mjs +0 -1489
  153. package/dist/tools-BcOKPBtg.mjs.map +0 -1
  154. package/dist/tools-C-_l0o8Z.mjs +0 -6
  155. package/dist/trace-CtV0RO6n.mjs +0 -137
  156. package/dist/trace-CtV0RO6n.mjs.map +0 -1
  157. package/dist/utils-BrX9FLeK.mjs +0 -3
  158. package/dist/vault-indexer-BG1sEVyM.mjs +0 -537
  159. package/dist/vault-indexer-BG1sEVyM.mjs.map +0 -1
  160. package/dist/wakeup-D8n33hYV.mjs +0 -335
  161. package/dist/wakeup-D8n33hYV.mjs.map +0 -1
  162. package/dist/work-queue-worker-4tLa5gpQ.mjs +0 -567
  163. package/dist/work-queue-worker-4tLa5gpQ.mjs.map +0 -1
  164. package/dist/work-queue-worker-rFZvTO3p.mjs +0 -11
  165. package/dist/zettelkasten-CtGHQWPU.mjs.map +0 -1
@@ -1,582 +0,0 @@
1
- import { a as paiHomePath } from "./pai-home-Cm9rcJgX.mjs";
2
- import { y as writeJsonAtomic } from "./config-BbLFD7Uf.mjs";
3
- import { D as readWorkersSection } from "./run-env-DRcp7A8K.mjs";
4
- import { A as worktreesDir, j as appendLedger, x as sessionMapPath, y as itermUuid } from "./run-Z4akoa8G.mjs";
5
- import { v as workersLogDir } from "./server-DgmAHyFK.mjs";
6
- import { i as sendToSession, n as fetchLiveSessions } from "./aibroker-client-C5Fw7DNz.mjs";
7
- import { createReadStream, existsSync, readFileSync, readdirSync, statSync } from "node:fs";
8
- import { homedir } from "node:os";
9
- import { join } from "node:path";
10
- import { createInterface } from "node:readline";
11
-
12
- //#region src/audit/session-usage.ts
13
- /**
14
- * session-usage.ts — parse the usage numbers out of a Claude Code session
15
- * (or subagent, or pai-worker event-mirror) JSONL transcript.
16
- *
17
- * All three log shapes share the same assistant-message envelope
18
- * ({ type: "assistant", message: { id, model, usage } }), so one parser
19
- * covers `pai audit tokens session` and the per-log readings feeding
20
- * `pai audit tokens spawn`.
21
- *
22
- * Streaming writes one JSONL line per content block of the same logical
23
- * turn, repeating message.id and usage each time — summing every line would
24
- * multiply usage by the block count. Keeping only the first line seen per
25
- * message.id is what the reference script (and this parser) count instead.
26
- */
27
- const USAGE_KEYS = [
28
- "cache_read_input_tokens",
29
- "cache_creation_input_tokens",
30
- "input_tokens",
31
- "output_tokens"
32
- ];
33
- function emptyTotals() {
34
- return {
35
- cache_read_input_tokens: 0,
36
- cache_creation_input_tokens: 0,
37
- input_tokens: 0,
38
- output_tokens: 0
39
- };
40
- }
41
- /**
42
- * `type:"system"` `subtype:"compact_boundary"` lines mark a compaction.
43
- * turnIndex is the count of assistant turns already folded when it fired,
44
- * so it lines up with the turn numbering the text/JSON report prints.
45
- */
46
- function isCompactBoundary(line) {
47
- return line.type === "system" && line.subtype === "compact_boundary";
48
- }
49
- function parseCompactionEvent(line, turnIndex) {
50
- const meta = line.compactMetadata;
51
- if (!meta || typeof meta.preTokens !== "number") return null;
52
- return {
53
- trigger: meta.trigger ?? "unknown",
54
- preTokens: meta.preTokens,
55
- turnIndex
56
- };
57
- }
58
- /**
59
- * `type:"system"` `subtype:"model_refusal_fallback"` lines mark an
60
- * automatic model switch fired by an API safety refusal (e.g. "cyber"
61
- * category), not a user or config choice — worth flagging separately since
62
- * it can rebuild the whole prompt cache mid-session.
63
- */
64
- function isModelFallback(line) {
65
- return line.type === "system" && line.subtype === "model_refusal_fallback";
66
- }
67
- function parseFallbackEvent(line, turnIndex) {
68
- return {
69
- turnIndex,
70
- from: line.originalModel ?? "unknown",
71
- to: line.fallbackModel ?? "unknown",
72
- category: line.apiRefusalCategory ?? "unknown",
73
- scope: line.scope ?? "unknown"
74
- };
75
- }
76
- /**
77
- * Fold one already-parsed JSONL line into a report being accumulated.
78
- * Exported separately so both the streaming file reader below and tests
79
- * (which build fixtures as arrays of objects, not files) share one path.
80
- * Returns the turn's context value when a new turn was counted, else null
81
- * (non-assistant line, no usage, or a duplicate message.id already seen).
82
- */
83
- function foldAssistantLine(report, seenIds, line, threshold) {
84
- if (line.type !== "assistant") return null;
85
- const message = line.message;
86
- const usage = message?.usage;
87
- if (!usage) return null;
88
- const id = message?.id ?? line.uuid;
89
- if (id) {
90
- if (seenIds.has(id)) return null;
91
- seenIds.add(id);
92
- }
93
- report.turns++;
94
- for (const key of USAGE_KEYS) {
95
- const v = usage[key];
96
- if (typeof v === "number") report.totals[key] += v;
97
- }
98
- const context = (Number(usage.cache_read_input_tokens) || 0) + (Number(usage.cache_creation_input_tokens) || 0) + (Number(usage.input_tokens) || 0);
99
- if (report.firstTurnContext === null) {
100
- report.firstTurnContext = context;
101
- report.firstTurnAt = line.timestamp ?? null;
102
- }
103
- const atMs = line.timestamp ? Date.parse(line.timestamp) : NaN;
104
- if (!Number.isNaN(atMs)) {
105
- if (report.lastTurnAtMs !== null && atMs - report.lastTurnAtMs > 3600 * 1e3) report.idleGapsOver60min++;
106
- report.lastTurnAtMs = atMs;
107
- }
108
- report.lastTurnContext = context;
109
- report.maxContext = report.maxContext === null ? context : Math.max(report.maxContext, context);
110
- if (context > threshold) report.turnsAboveThreshold++;
111
- if ((Number(usage.cache_creation_input_tokens) || 0) > 2e4) report.cacheRebuildTurns++;
112
- const model = message?.model ?? "unknown";
113
- report.models[model] = (report.models[model] ?? 0) + 1;
114
- if (report.lastModel !== null && report.lastModel !== model) report.modelSwitches.push({
115
- turnIndex: report.turns,
116
- from: report.lastModel,
117
- to: model,
118
- cacheRead: Number(usage.cache_read_input_tokens) || 0,
119
- cacheCreation: Number(usage.cache_creation_input_tokens) || 0
120
- });
121
- report.lastModel = model;
122
- const split = usage.cache_creation;
123
- if (split) {
124
- report.cacheCreationSplit.ephemeral5m += Number(split.ephemeral_5m_input_tokens) || 0;
125
- report.cacheCreationSplit.ephemeral1h += Number(split.ephemeral_1h_input_tokens) || 0;
126
- }
127
- return context;
128
- }
129
- /**
130
- * True for a real human-authored `type:"user"` prompt line: content is a
131
- * plain string, or a content array with no `tool_result` block. Claude Code
132
- * encodes tool results as `type:"user"` messages whose content array is
133
- * entirely (or partly) tool_result blocks — those must not count as prompts.
134
- */
135
- function isRealUserPrompt(line) {
136
- if (line.type !== "user") return false;
137
- if (line.isMeta) return false;
138
- if (line.origin?.kind && line.origin.kind !== "human") return false;
139
- const content = line.message?.content;
140
- if (typeof content === "string") return true;
141
- if (Array.isArray(content)) return !content.some((block) => block?.type === "tool_result");
142
- return false;
143
- }
144
- /** Plain text of a real user prompt: the string content, or its first text block. */
145
- function extractUserPromptText(line) {
146
- const content = line.message?.content;
147
- if (typeof content === "string") return content;
148
- if (Array.isArray(content)) return content.find((b) => b?.type === "text")?.text ?? "";
149
- return "";
150
- }
151
- /**
152
- * Parse a session/subagent/worker-event JSONL file into a usage report.
153
- * `keepaliveWord`, when given, counts real user prompts whose text exactly
154
- * matches it (trimmed) — the sessions.cacheKeepalive beat prompt — into
155
- * `keepaliveBeats`; omitted, `keepaliveBeats` stays null.
156
- */
157
- async function parseSessionUsage(path, threshold = 2e5, keepaliveWord) {
158
- const report = {
159
- path,
160
- sizeBytes: existsSync(path) ? readFileSync(path).byteLength : 0,
161
- turns: 0,
162
- totals: emptyTotals(),
163
- models: {},
164
- firstTurnContext: null,
165
- lastTurnContext: null,
166
- cacheCreationSplit: {
167
- ephemeral5m: 0,
168
- ephemeral1h: 0
169
- },
170
- avgContext: null,
171
- maxContext: null,
172
- turnsAboveThreshold: 0,
173
- cacheRebuildTurns: 0,
174
- userPrompts: 0,
175
- promptExposure: 0,
176
- compactions: [],
177
- firstTurnAt: null,
178
- lastModel: null,
179
- modelSwitches: [],
180
- fallbacks: [],
181
- lastTurnAtMs: null,
182
- idleGapsOver60min: 0,
183
- keepaliveBeats: keepaliveWord ? 0 : null
184
- };
185
- const seenIds = /* @__PURE__ */ new Set();
186
- let contextSum = 0;
187
- const rl = createInterface({
188
- input: createReadStream(path, "utf8"),
189
- crlfDelay: Infinity
190
- });
191
- for await (const raw of rl) {
192
- if (!raw.trim()) continue;
193
- let obj;
194
- try {
195
- obj = JSON.parse(raw);
196
- } catch {
197
- continue;
198
- }
199
- if (obj.type === "user") {
200
- if (isRealUserPrompt(obj)) {
201
- report.userPrompts++;
202
- if (keepaliveWord && extractUserPromptText(obj).trim() === keepaliveWord) report.keepaliveBeats = (report.keepaliveBeats ?? 0) + 1;
203
- }
204
- continue;
205
- }
206
- if (isCompactBoundary(obj)) {
207
- const event = parseCompactionEvent(obj, report.turns);
208
- if (event) report.compactions.push(event);
209
- continue;
210
- }
211
- if (isModelFallback(obj)) {
212
- report.fallbacks.push(parseFallbackEvent(obj, report.turns));
213
- continue;
214
- }
215
- const context = foldAssistantLine(report, seenIds, obj, threshold);
216
- if (context !== null) {
217
- contextSum += context;
218
- report.promptExposure += report.userPrompts;
219
- }
220
- }
221
- report.avgContext = report.turns > 0 ? Math.round(contextSum / report.turns) : null;
222
- return report;
223
- }
224
- function totalUsageTokens(totals) {
225
- return totals.cache_read_input_tokens + totals.cache_creation_input_tokens + totals.input_tokens + totals.output_tokens;
226
- }
227
-
228
- //#endregion
229
- //#region src/daemon/session-keepalive.ts
230
- /**
231
- * session-keepalive.ts — idle-triggered prompt-cache keepalive beat for live
232
- * interactive Claude Code sessions (`sessions.cacheKeepalive`, see config.ts
233
- * and docs/cache-keepalive.md, "Interactive sessions").
234
- *
235
- * One beat = one trivial prompt typed into a session through AIBroker's
236
- * send_to_session (src/cli/lib/aibroker-client.ts), sent with `noReply: true`
237
- * so the target sees only the bare word typed into its input line and
238
- * nothing is queued into its mailbox as a peer message demanding a reply.
239
- * A cache READ refreshes the provider's ephemeral prompt-cache TTL at
240
- * roughly 0.1x the cost of the 2x rewrite a cold cache forces on the next
241
- * real prompt.
242
- *
243
- * Distinct from workers/keepalive.ts, which beats a *worker provider's*
244
- * cache on a fixed timer regardless of activity: this only beats a session
245
- * that has actually been idle long enough to be at risk, and only inside a
246
- * configured working-hours window, so it never fires while the user is
247
- * present at the keyboard or overnight when nobody will read the reply
248
- * before the cache would have expired anyway.
249
- */
250
- function sessionKeepaliveStatePath() {
251
- return paiHomePath("session-keepalive.json");
252
- }
253
- function emptyEntry() {
254
- return {
255
- beats: 0,
256
- lastRealPromptKey: null,
257
- lastBeatAt: null
258
- };
259
- }
260
- function loadSessionKeepaliveState(path = sessionKeepaliveStatePath()) {
261
- if (!existsSync(path)) return {};
262
- try {
263
- return JSON.parse(readFileSync(path, "utf8"));
264
- } catch {
265
- return {};
266
- }
267
- }
268
- function saveSessionKeepaliveState(state, path = sessionKeepaliveStatePath()) {
269
- writeJsonAtomic(path, state, {
270
- backup: false,
271
- label: path
272
- });
273
- }
274
- /** Parse "HH:MM-HH:MM" into minutes-since-midnight. Throws on a malformed
275
- * spec — an explicit config error beats a window that is silently always
276
- * on or always off. */
277
- function parseActiveHours(spec) {
278
- const m = spec.match(/^(\d{1,2}):(\d{2})-(\d{1,2}):(\d{2})$/);
279
- if (!m) throw new Error(`sessions.cacheKeepalive.activeHours: invalid "${spec}" (want "HH:MM-HH:MM")`);
280
- return {
281
- startMin: Number(m[1]) * 60 + Number(m[2]),
282
- endMin: Number(m[3]) * 60 + Number(m[4])
283
- };
284
- }
285
- /**
286
- * Whether `now` (local time) sits inside the window. A window that wraps
287
- * midnight (e.g. "22:00-06:00") is honoured by inverting the test instead of
288
- * requiring startMin < endMin.
289
- */
290
- function isWithinActiveHours(now, spec) {
291
- const { startMin, endMin } = parseActiveHours(spec);
292
- const nowMin = now.getHours() * 60 + now.getMinutes();
293
- if (startMin <= endMin) return nowMin >= startMin && nowMin < endMin;
294
- return nowMin >= startMin || nowMin < endMin;
295
- }
296
- /**
297
- * Full path to a live session's transcript under ~/.claude/projects, or null
298
- * when none is found — the case for a session too new to have written a file
299
- * yet. A worker running in a worktree DOES write a transcript here (Claude
300
- * Code encodes its worktree cwd as the project dir name), so workers are not
301
- * excluded "for free" — see isWorkerSession.
302
- */
303
- function findSessionTranscript(sessionId, projectsDir = join(homedir(), ".claude", "projects")) {
304
- let projectDirs;
305
- try {
306
- projectDirs = readdirSync(projectsDir);
307
- } catch {
308
- return null;
309
- }
310
- for (const projectDir of projectDirs) {
311
- const candidate = join(projectsDir, projectDir, `${sessionId}.jsonl`);
312
- if (existsSync(candidate)) return candidate;
313
- }
314
- return null;
315
- }
316
- /**
317
- * AIBroker's `fetchLiveSessions()` identifies a session by its iTerm2 pane id
318
- * (e.g. an AIBroker/iTerm UUID), not the Claude session id `<uuid>.jsonl`
319
- * transcripts are named after — the two are unrelated identifiers. The
320
- * status line bridges them on every refresh via `claude-session-map.json`
321
- * (see `recordSessionMapEntry` in ../workers/scope.ts): this picks, among the
322
- * map entries whose `term` pane UUID matches, the most recently written one.
323
- * Returns null when the pane has no (fresh enough) mapped Claude session.
324
- */
325
- function resolveClaudeSessionIdFromMap(paneId, logDir) {
326
- const path = sessionMapPath(logDir);
327
- if (!paneId || !existsSync(path)) return null;
328
- let map;
329
- try {
330
- map = JSON.parse(readFileSync(path, "utf8"));
331
- } catch {
332
- return null;
333
- }
334
- let best = null;
335
- for (const entry of Object.values(map)) {
336
- if (!entry.term || itermUuid(entry.term) !== paneId) continue;
337
- if (!best || entry.ts > best.ts) best = entry;
338
- }
339
- return best?.session ?? null;
340
- }
341
- /** Claude Code's project-dir encoding of a cwd: every "/" becomes "-". */
342
- function encodeProjectDirName(cwd) {
343
- return cwd.replace(/\//g, "-");
344
- }
345
- /**
346
- * Is this live "claude"-kind session actually a worker pane rather than an
347
- * interactive one? `claude -p` workers running in a worktree write their
348
- * transcript under ~/.claude/projects too (encoded cwd = the worktree dir),
349
- * so a missing transcript is NOT how workers get excluded — this predicate
350
- * is. Either signal is enough:
351
- * (a) the transcript's project-dir name is the encoded form of a path
352
- * under <logDir>/worktrees (every worker worktree lives there), or
353
- * (b) the broker's session name/paiName names a path under the workers
354
- * log dir or one of its worktrees (best-effort: AIBroker does not
355
- * guarantee this, but honors it when present).
356
- */
357
- function isWorkerSession(meta, transcriptPath, logDir) {
358
- const worktreesPrefix = encodeProjectDirName(worktreesDir(logDir));
359
- if (transcriptPath) {
360
- if ((transcriptPath.split("/").slice(0, -1).pop() ?? "").startsWith(worktreesPrefix)) return true;
361
- }
362
- const rawPrefix = worktreesDir(logDir);
363
- for (const field of [meta.name, meta.paiName]) if (field && (field.includes(rawPrefix) || field.includes(logDir))) return true;
364
- return false;
365
- }
366
- function isMidTurn(line) {
367
- if (!line) return false;
368
- if (line.type === "user" && isRealUserPrompt(line)) return true;
369
- if (line.type === "assistant") {
370
- const stopReason = line.message?.stop_reason;
371
- if (stopReason === void 0 || stopReason === null) return true;
372
- return stopReason !== "end_turn" && stopReason !== "stop_sequence";
373
- }
374
- return false;
375
- }
376
- function readTranscriptSnapshot(path) {
377
- let lines;
378
- try {
379
- lines = readFileSync(path, "utf8").split("\n");
380
- } catch {
381
- return {
382
- context: null,
383
- midTurn: false,
384
- lastRealPrompt: null
385
- };
386
- }
387
- let lastLine = null;
388
- let context = null;
389
- let lastRealPrompt = null;
390
- for (let i = lines.length - 1; i >= 0; i--) {
391
- const raw = lines[i].trim();
392
- if (!raw) continue;
393
- let obj;
394
- try {
395
- obj = JSON.parse(raw);
396
- } catch {
397
- continue;
398
- }
399
- if (lastLine === null) lastLine = obj;
400
- if (context === null && obj.type === "assistant" && obj.message?.usage) {
401
- const usage = obj.message.usage;
402
- context = (Number(usage.cache_read_input_tokens) || 0) + (Number(usage.cache_creation_input_tokens) || 0) + (Number(usage.input_tokens) || 0);
403
- }
404
- if (lastRealPrompt === null && isRealUserPrompt(obj)) lastRealPrompt = {
405
- key: obj.uuid ?? obj.timestamp ?? String(i),
406
- text: extractUserPromptText(obj).trim()
407
- };
408
- if (context !== null && lastRealPrompt !== null) break;
409
- }
410
- return {
411
- context,
412
- midTurn: isMidTurn(lastLine),
413
- lastRealPrompt
414
- };
415
- }
416
- /** Ledger event tag; `pai daemon keepalive` and any log-tailer key off this. */
417
- const SESSION_KEEPALIVE_EVENT = "SESSION-KEEPALIVE";
418
- function sessionKeepaliveLedgerPath() {
419
- const { workers } = readWorkersSection();
420
- return join(workersLogDir(workers), "ledger.log");
421
- }
422
- /** The counts/last lines `pai daemon keepalive` prints, scoped to this event. */
423
- function sessionKeepaliveLedgerSummary(path = sessionKeepaliveLedgerPath(), lastN = 10) {
424
- if (!existsSync(path)) return {
425
- sent: 0,
426
- skipped: 0,
427
- lastLines: []
428
- };
429
- const lines = readFileSync(path, "utf8").split("\n").filter((l) => l.includes(SESSION_KEEPALIVE_EVENT));
430
- const sent = lines.filter((l) => / result=sent(\s|$)/.test(l)).length;
431
- return {
432
- sent,
433
- skipped: lines.length - sent,
434
- lastLines: lines.slice(-lastN)
435
- };
436
- }
437
- function defaultDeps() {
438
- const { workers } = readWorkersSection();
439
- const logDir = workersLogDir(workers);
440
- return {
441
- now: () => /* @__PURE__ */ new Date(),
442
- fetchLiveSessions,
443
- resolveClaudeSessionId: (paneId) => resolveClaudeSessionIdFromMap(paneId, logDir),
444
- findTranscript: (id) => findSessionTranscript(id),
445
- mtimeMs: (p) => {
446
- try {
447
- return statSync(p).mtimeMs;
448
- } catch {
449
- return null;
450
- }
451
- },
452
- readSnapshot: readTranscriptSnapshot,
453
- sendBeat: (id, text) => sendToSession(id, text, void 0, { noReply: true }),
454
- loadState: () => loadSessionKeepaliveState(),
455
- saveState: (s) => saveSessionKeepaliveState(s),
456
- ledger: (kv) => appendLedger(sessionKeepaliveLedgerPath(), SESSION_KEEPALIVE_EVENT, kv)
457
- };
458
- }
459
- /**
460
- * One tick over every live interactive session: beat the ones that qualify,
461
- * skip (with a reason) the ones that don't. Returns one result per live
462
- * "claude"-kind session for tests to assert against; writes the ledger line
463
- * and (when any counter changed) the state file as side effects.
464
- */
465
- async function runSessionKeepaliveTick(config, overrides = {}) {
466
- if (!config.enabled) return [];
467
- const d = {
468
- ...defaultDeps(),
469
- ...overrides
470
- };
471
- const sessions = await d.fetchLiveSessions();
472
- const state = d.loadState();
473
- const now = d.now();
474
- const { workers } = readWorkersSection();
475
- const logDir = workersLogDir(workers);
476
- const results = [];
477
- let anyChanged = false;
478
- for (const s of sessions) {
479
- if (s.kind !== "claude") continue;
480
- const paneId = s.sessionId;
481
- const claudeId = d.resolveClaudeSessionId(paneId);
482
- if (!claudeId) {
483
- d.ledger({
484
- session: null,
485
- pane: paneId,
486
- result: "skipped:unmapped"
487
- });
488
- results.push({
489
- sessionId: paneId,
490
- result: "skipped:unmapped"
491
- });
492
- continue;
493
- }
494
- const sessionId = claudeId;
495
- const entry = { ...state[sessionId] ?? emptyEntry() };
496
- let changed = false;
497
- const transcript = d.findTranscript(sessionId);
498
- if (isWorkerSession(s, transcript, logDir)) {
499
- d.ledger({
500
- session: sessionId,
501
- pane: paneId,
502
- result: "skipped:worker"
503
- });
504
- results.push({
505
- sessionId,
506
- result: "skipped:worker"
507
- });
508
- continue;
509
- }
510
- if (!transcript) {
511
- d.ledger({
512
- session: sessionId,
513
- pane: paneId,
514
- result: "skipped:no-transcript"
515
- });
516
- results.push({
517
- sessionId,
518
- result: "skipped:no-transcript"
519
- });
520
- continue;
521
- }
522
- const snapshot = d.readSnapshot(transcript);
523
- if (snapshot.lastRealPrompt && snapshot.lastRealPrompt.key !== entry.lastRealPromptKey) {
524
- entry.lastRealPromptKey = snapshot.lastRealPrompt.key;
525
- if (snapshot.lastRealPrompt.text !== config.prompt) entry.beats = 0;
526
- changed = true;
527
- }
528
- const mtime = d.mtimeMs(transcript);
529
- const idleMin = mtime === null ? null : (now.getTime() - mtime) / 6e4;
530
- 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;
531
- const ledgerBase = {
532
- session: sessionId,
533
- pane: paneId,
534
- idle_min: idleMin === null ? null : idleMin.toFixed(1),
535
- context: snapshot.context
536
- };
537
- if (skip) {
538
- d.ledger({
539
- ...ledgerBase,
540
- beat: `${entry.beats}/${config.maxBeats}`,
541
- result: `skipped:${skip}`
542
- });
543
- results.push({
544
- sessionId,
545
- result: `skipped:${skip}`
546
- });
547
- } else if ((await d.sendBeat(paneId, config.prompt)).ok) {
548
- entry.beats += 1;
549
- entry.lastBeatAt = now.toISOString();
550
- changed = true;
551
- d.ledger({
552
- ...ledgerBase,
553
- beat: `${entry.beats}/${config.maxBeats}`,
554
- result: "sent"
555
- });
556
- results.push({
557
- sessionId,
558
- result: "sent"
559
- });
560
- } else {
561
- d.ledger({
562
- ...ledgerBase,
563
- beat: `${entry.beats}/${config.maxBeats}`,
564
- result: "skipped:send-failed"
565
- });
566
- results.push({
567
- sessionId,
568
- result: "skipped:send-failed"
569
- });
570
- }
571
- if (changed) {
572
- state[sessionId] = entry;
573
- anyChanged = true;
574
- }
575
- }
576
- if (anyChanged) d.saveState(state);
577
- return results;
578
- }
579
-
580
- //#endregion
581
- export { totalUsageTokens as a, parseSessionUsage as i, runSessionKeepaliveTick as n, sessionKeepaliveLedgerSummary as r, loadSessionKeepaliveState as t };
582
- //# sourceMappingURL=session-keepalive-BDhytXEo.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"session-keepalive-BDhytXEo.mjs","names":["fetchLiveSessionsDefault","sendToSessionDefault"],"sources":["../src/audit/session-usage.ts","../src/daemon/session-keepalive.ts"],"sourcesContent":["/**\n * session-usage.ts — parse the usage numbers out of a Claude Code session\n * (or subagent, or pai-worker event-mirror) JSONL transcript.\n *\n * All three log shapes share the same assistant-message envelope\n * ({ type: \"assistant\", message: { id, model, usage } }), so one parser\n * covers `pai audit tokens session` and the per-log readings feeding\n * `pai audit tokens spawn`.\n *\n * Streaming writes one JSONL line per content block of the same logical\n * turn, repeating message.id and usage each time — summing every line would\n * multiply usage by the block count. Keeping only the first line seen per\n * message.id is what the reference script (and this parser) count instead.\n */\n\nimport { createReadStream, existsSync } from \"node:fs\";\nimport { createInterface } from \"node:readline\";\nimport { readFileSync } from \"node:fs\";\n\nexport interface UsageTotals {\n cache_read_input_tokens: number;\n cache_creation_input_tokens: number;\n input_tokens: number;\n output_tokens: number;\n}\n\nexport interface CacheCreationSplit {\n ephemeral5m: number;\n ephemeral1h: number;\n}\n\nexport interface CompactionEvent {\n trigger: string;\n preTokens: number;\n turnIndex: number;\n}\n\nexport interface ModelSwitch {\n turnIndex: number;\n from: string;\n to: string;\n cacheRead: number;\n cacheCreation: number;\n}\n\nexport interface FallbackEvent {\n turnIndex: number;\n from: string;\n to: string;\n category: string;\n scope: string;\n}\n\nexport interface SessionUsageReport {\n path: string;\n sizeBytes: number;\n turns: number;\n totals: UsageTotals;\n /** Per-model assistant-turn counts. */\n models: Record<string, number>;\n /** cache_read + cache_creation + input on the first / last assistant turn seen. */\n firstTurnContext: number | null;\n lastTurnContext: number | null;\n cacheCreationSplit: CacheCreationSplit;\n /** Average / max of the per-turn context value across all turns; null if no turns. */\n avgContext: number | null;\n maxContext: number | null;\n /** Turns whose per-turn context value exceeds the report's threshold. */\n turnsAboveThreshold: number;\n /** Turns whose cache_creation_input_tokens exceeds 20000. */\n cacheRebuildTurns: number;\n /** Real human-authored user prompts (excludes tool-result-only \"user\" lines). */\n userPrompts: number;\n /**\n * Sum over assistant turns of the real user prompts seen before that turn;\n * multiplied by per-prompt hook tokens it gives the tokens UserPromptSubmit\n * output occupied across the whole session.\n */\n promptExposure: number;\n compactions: CompactionEvent[];\n /** ISO timestamp of the first assistant turn. */\n firstTurnAt: string | null;\n /** Model of the last folded turn; drives switch detection. */\n lastModel: string | null;\n modelSwitches: ModelSwitch[];\n fallbacks: FallbackEvent[];\n /** Timestamp (ms) of the last folded assistant turn; drives idle-gap detection. */\n lastTurnAtMs: number | null;\n /** Gaps between consecutive assistant turns exceeding 60 minutes — evidence\n * for whether the session ever went idle long enough to risk its cache TTL\n * (see sessions.cacheKeepalive, src/daemon/config.ts). */\n idleGapsOver60min: number;\n /** Real user prompts whose text is exactly the configured keepalive word\n * (set only when parseSessionUsage is called with one); null otherwise. */\n keepaliveBeats: number | null;\n}\n\nconst USAGE_KEYS: (keyof UsageTotals)[] = [\n \"cache_read_input_tokens\",\n \"cache_creation_input_tokens\",\n \"input_tokens\",\n \"output_tokens\",\n];\n\nfunction emptyTotals(): UsageTotals {\n return { cache_read_input_tokens: 0, cache_creation_input_tokens: 0, input_tokens: 0, output_tokens: 0 };\n}\n\nexport interface AssistantLine {\n type?: string;\n uuid?: string;\n subtype?: string;\n /** Claude Code marks skill expansions and injected system reminders isMeta; they fire no UserPromptSubmit hook. */\n isMeta?: boolean;\n /** Present on newer logs: { kind: \"human\" } for a typed prompt. */\n origin?: { kind?: string };\n compactMetadata?: { trigger?: string; preTokens?: number };\n timestamp?: string;\n originalModel?: string;\n fallbackModel?: string;\n apiRefusalCategory?: string;\n scope?: string;\n message?: {\n id?: string;\n model?: string;\n usage?: Record<string, unknown> & {\n cache_creation?: { ephemeral_5m_input_tokens?: number; ephemeral_1h_input_tokens?: number };\n };\n content?: string | Array<{ type?: string }>;\n stop_reason?: string | null;\n };\n}\n\n/**\n * `type:\"system\"` `subtype:\"compact_boundary\"` lines mark a compaction.\n * turnIndex is the count of assistant turns already folded when it fired,\n * so it lines up with the turn numbering the text/JSON report prints.\n */\nexport function isCompactBoundary(line: AssistantLine): boolean {\n return line.type === \"system\" && line.subtype === \"compact_boundary\";\n}\n\nexport function parseCompactionEvent(line: AssistantLine, turnIndex: number): CompactionEvent | null {\n const meta = line.compactMetadata;\n if (!meta || typeof meta.preTokens !== \"number\") return null;\n return { trigger: meta.trigger ?? \"unknown\", preTokens: meta.preTokens, turnIndex };\n}\n\n/**\n * `type:\"system\"` `subtype:\"model_refusal_fallback\"` lines mark an\n * automatic model switch fired by an API safety refusal (e.g. \"cyber\"\n * category), not a user or config choice — worth flagging separately since\n * it can rebuild the whole prompt cache mid-session.\n */\nexport function isModelFallback(line: AssistantLine): boolean {\n return line.type === \"system\" && line.subtype === \"model_refusal_fallback\";\n}\n\nexport function parseFallbackEvent(line: AssistantLine, turnIndex: number): FallbackEvent {\n return {\n turnIndex,\n from: line.originalModel ?? \"unknown\",\n to: line.fallbackModel ?? \"unknown\",\n category: line.apiRefusalCategory ?? \"unknown\",\n scope: line.scope ?? \"unknown\",\n };\n}\n\n/**\n * Fold one already-parsed JSONL line into a report being accumulated.\n * Exported separately so both the streaming file reader below and tests\n * (which build fixtures as arrays of objects, not files) share one path.\n * Returns the turn's context value when a new turn was counted, else null\n * (non-assistant line, no usage, or a duplicate message.id already seen).\n */\nexport function foldAssistantLine(\n report: Pick<\n SessionUsageReport,\n | \"turns\"\n | \"totals\"\n | \"models\"\n | \"firstTurnContext\"\n | \"lastTurnContext\"\n | \"cacheCreationSplit\"\n | \"maxContext\"\n | \"turnsAboveThreshold\"\n | \"cacheRebuildTurns\"\n | \"firstTurnAt\"\n | \"lastModel\"\n | \"modelSwitches\"\n | \"lastTurnAtMs\"\n | \"idleGapsOver60min\"\n >,\n seenIds: Set<string>,\n line: AssistantLine,\n threshold: number\n): number | null {\n if (line.type !== \"assistant\") return null;\n const message = line.message;\n const usage = message?.usage;\n if (!usage) return null;\n const id = message?.id ?? line.uuid;\n if (id) {\n if (seenIds.has(id)) return null;\n seenIds.add(id);\n }\n report.turns++;\n for (const key of USAGE_KEYS) {\n const v = usage[key];\n if (typeof v === \"number\") report.totals[key] += v;\n }\n const context =\n (Number(usage.cache_read_input_tokens) || 0) +\n (Number(usage.cache_creation_input_tokens) || 0) +\n (Number(usage.input_tokens) || 0);\n if (report.firstTurnContext === null) {\n report.firstTurnContext = context;\n report.firstTurnAt = line.timestamp ?? null;\n }\n const atMs = line.timestamp ? Date.parse(line.timestamp) : NaN;\n if (!Number.isNaN(atMs)) {\n if (report.lastTurnAtMs !== null && atMs - report.lastTurnAtMs > 60 * 60 * 1000) {\n report.idleGapsOver60min++;\n }\n report.lastTurnAtMs = atMs;\n }\n report.lastTurnContext = context;\n report.maxContext = report.maxContext === null ? context : Math.max(report.maxContext, context);\n if (context > threshold) report.turnsAboveThreshold++;\n if ((Number(usage.cache_creation_input_tokens) || 0) > 20000) report.cacheRebuildTurns++;\n const model = message?.model ?? \"unknown\";\n report.models[model] = (report.models[model] ?? 0) + 1;\n if (report.lastModel !== null && report.lastModel !== model) {\n report.modelSwitches.push({\n turnIndex: report.turns,\n from: report.lastModel,\n to: model,\n cacheRead: Number(usage.cache_read_input_tokens) || 0,\n cacheCreation: Number(usage.cache_creation_input_tokens) || 0,\n });\n }\n report.lastModel = model;\n const split = usage.cache_creation;\n if (split) {\n report.cacheCreationSplit.ephemeral5m += Number(split.ephemeral_5m_input_tokens) || 0;\n report.cacheCreationSplit.ephemeral1h += Number(split.ephemeral_1h_input_tokens) || 0;\n }\n return context;\n}\n\n/**\n * True for a real human-authored `type:\"user\"` prompt line: content is a\n * plain string, or a content array with no `tool_result` block. Claude Code\n * encodes tool results as `type:\"user\"` messages whose content array is\n * entirely (or partly) tool_result blocks — those must not count as prompts.\n */\nexport function isRealUserPrompt(line: AssistantLine): boolean {\n if (line.type !== \"user\") return false;\n if (line.isMeta) return false;\n if (line.origin?.kind && line.origin.kind !== \"human\") return false;\n const content = line.message?.content;\n if (typeof content === \"string\") return true;\n if (Array.isArray(content)) return !content.some((block) => block?.type === \"tool_result\");\n return false;\n}\n\n/** Plain text of a real user prompt: the string content, or its first text block. */\nexport function extractUserPromptText(line: AssistantLine): string {\n const content = line.message?.content;\n if (typeof content === \"string\") return content;\n if (Array.isArray(content)) {\n const block = content.find((b) => b?.type === \"text\") as { text?: string } | undefined;\n return block?.text ?? \"\";\n }\n return \"\";\n}\n\n/**\n * Parse a session/subagent/worker-event JSONL file into a usage report.\n * `keepaliveWord`, when given, counts real user prompts whose text exactly\n * matches it (trimmed) — the sessions.cacheKeepalive beat prompt — into\n * `keepaliveBeats`; omitted, `keepaliveBeats` stays null.\n */\nexport async function parseSessionUsage(\n path: string,\n threshold = 200_000,\n keepaliveWord?: string\n): Promise<SessionUsageReport> {\n const report: SessionUsageReport = {\n path,\n sizeBytes: existsSync(path) ? readFileSync(path).byteLength : 0,\n turns: 0,\n totals: emptyTotals(),\n models: {},\n firstTurnContext: null,\n lastTurnContext: null,\n cacheCreationSplit: { ephemeral5m: 0, ephemeral1h: 0 },\n avgContext: null,\n maxContext: null,\n turnsAboveThreshold: 0,\n cacheRebuildTurns: 0,\n userPrompts: 0,\n promptExposure: 0,\n compactions: [],\n firstTurnAt: null,\n lastModel: null,\n modelSwitches: [],\n fallbacks: [],\n lastTurnAtMs: null,\n idleGapsOver60min: 0,\n keepaliveBeats: keepaliveWord ? 0 : null,\n };\n const seenIds = new Set<string>();\n let contextSum = 0;\n\n const rl = createInterface({ input: createReadStream(path, \"utf8\"), crlfDelay: Infinity });\n for await (const raw of rl) {\n if (!raw.trim()) continue;\n let obj: AssistantLine;\n try {\n obj = JSON.parse(raw) as AssistantLine;\n } catch {\n continue;\n }\n if (obj.type === \"user\") {\n if (isRealUserPrompt(obj)) {\n report.userPrompts++;\n if (keepaliveWord && extractUserPromptText(obj).trim() === keepaliveWord) {\n report.keepaliveBeats = (report.keepaliveBeats ?? 0) + 1;\n }\n }\n continue;\n }\n if (isCompactBoundary(obj)) {\n const event = parseCompactionEvent(obj, report.turns);\n if (event) report.compactions.push(event);\n continue;\n }\n if (isModelFallback(obj)) {\n report.fallbacks.push(parseFallbackEvent(obj, report.turns));\n continue;\n }\n const context = foldAssistantLine(report, seenIds, obj, threshold);\n if (context !== null) {\n contextSum += context;\n report.promptExposure += report.userPrompts;\n }\n }\n report.avgContext = report.turns > 0 ? Math.round(contextSum / report.turns) : null;\n return report;\n}\n\nexport function totalUsageTokens(totals: UsageTotals): number {\n return totals.cache_read_input_tokens + totals.cache_creation_input_tokens + totals.input_tokens + totals.output_tokens;\n}\n","/**\n * session-keepalive.ts — idle-triggered prompt-cache keepalive beat for live\n * interactive Claude Code sessions (`sessions.cacheKeepalive`, see config.ts\n * and docs/cache-keepalive.md, \"Interactive sessions\").\n *\n * One beat = one trivial prompt typed into a session through AIBroker's\n * send_to_session (src/cli/lib/aibroker-client.ts), sent with `noReply: true`\n * so the target sees only the bare word typed into its input line and\n * nothing is queued into its mailbox as a peer message demanding a reply.\n * A cache READ refreshes the provider's ephemeral prompt-cache TTL at\n * roughly 0.1x the cost of the 2x rewrite a cold cache forces on the next\n * real prompt.\n *\n * Distinct from workers/keepalive.ts, which beats a *worker provider's*\n * cache on a fixed timer regardless of activity: this only beats a session\n * that has actually been idle long enough to be at risk, and only inside a\n * configured working-hours window, so it never fires while the user is\n * present at the keyboard or overnight when nobody will read the reply\n * before the cache would have expired anyway.\n */\n\nimport { existsSync, readFileSync, readdirSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport {\n fetchLiveSessions as fetchLiveSessionsDefault,\n sendToSession as sendToSessionDefault,\n type AiBrokerSessionMeta,\n} from \"../cli/lib/aibroker-client.js\";\nimport { writeJsonAtomic } from \"../config/json-store.js\";\nimport { paiHomePath } from \"../config/pai-home.js\";\nimport { appendLedger } from \"../workers/ledger.js\";\nimport { readWorkersSection } from \"../workers/config.js\";\nimport { workersLogDir } from \"../workers/paths.js\";\nimport { worktreesDir } from \"../workers/worktree.js\";\nimport { itermUuid, sessionMapPath, type SessionMapEntry } from \"../workers/scope.js\";\nimport { isRealUserPrompt, extractUserPromptText, type AssistantLine } from \"../audit/session-usage.js\";\nimport type { SessionsCacheKeepaliveConfig } from \"./config.js\";\n\n// ---------------------------------------------------------------------------\n// State file — per-session beat counters, rebuildable (see json-store.ts on\n// when NOT to use readJsonStrict: a damaged file here just resets counters,\n// never blocks the feature).\n// ---------------------------------------------------------------------------\n\nexport interface SessionKeepaliveEntry {\n /** Beats sent since the last real (non-keepalive) user prompt. */\n beats: number;\n /** Identity of the last real user prompt observed, so a fresh one can be\n * told apart from the keepalive's own echo landing back in the transcript. */\n lastRealPromptKey: string | null;\n /** ISO stamp of the last beat sent. */\n lastBeatAt: string | null;\n}\n\nexport type SessionKeepaliveState = Record<string, SessionKeepaliveEntry>;\n\nexport function sessionKeepaliveStatePath(): string {\n return paiHomePath(\"session-keepalive.json\");\n}\n\nfunction emptyEntry(): SessionKeepaliveEntry {\n return { beats: 0, lastRealPromptKey: null, lastBeatAt: null };\n}\n\nexport function loadSessionKeepaliveState(path: string = sessionKeepaliveStatePath()): SessionKeepaliveState {\n if (!existsSync(path)) return {};\n try {\n return JSON.parse(readFileSync(path, \"utf8\")) as SessionKeepaliveState;\n } catch {\n return {};\n }\n}\n\nexport function saveSessionKeepaliveState(\n state: SessionKeepaliveState,\n path: string = sessionKeepaliveStatePath()\n): void {\n writeJsonAtomic(path, state, { backup: false, label: path });\n}\n\n// ---------------------------------------------------------------------------\n// Active-hours window\n// ---------------------------------------------------------------------------\n\n/** Parse \"HH:MM-HH:MM\" into minutes-since-midnight. Throws on a malformed\n * spec — an explicit config error beats a window that is silently always\n * on or always off. */\nexport function parseActiveHours(spec: string): { startMin: number; endMin: number } {\n const m = spec.match(/^(\\d{1,2}):(\\d{2})-(\\d{1,2}):(\\d{2})$/);\n if (!m) {\n throw new Error(`sessions.cacheKeepalive.activeHours: invalid \"${spec}\" (want \"HH:MM-HH:MM\")`);\n }\n return {\n startMin: Number(m[1]) * 60 + Number(m[2]),\n endMin: Number(m[3]) * 60 + Number(m[4]),\n };\n}\n\n/**\n * Whether `now` (local time) sits inside the window. A window that wraps\n * midnight (e.g. \"22:00-06:00\") is honoured by inverting the test instead of\n * requiring startMin < endMin.\n */\nexport function isWithinActiveHours(now: Date, spec: string): boolean {\n const { startMin, endMin } = parseActiveHours(spec);\n const nowMin = now.getHours() * 60 + now.getMinutes();\n if (startMin <= endMin) return nowMin >= startMin && nowMin < endMin;\n return nowMin >= startMin || nowMin < endMin;\n}\n\n// ---------------------------------------------------------------------------\n// Transcript lookup\n// ---------------------------------------------------------------------------\n\n/**\n * Full path to a live session's transcript under ~/.claude/projects, or null\n * when none is found — the case for a session too new to have written a file\n * yet. A worker running in a worktree DOES write a transcript here (Claude\n * Code encodes its worktree cwd as the project dir name), so workers are not\n * excluded \"for free\" — see isWorkerSession.\n */\nexport function findSessionTranscript(\n sessionId: string,\n projectsDir: string = join(homedir(), \".claude\", \"projects\")\n): string | null {\n let projectDirs: string[];\n try {\n projectDirs = readdirSync(projectsDir);\n } catch {\n return null;\n }\n for (const projectDir of projectDirs) {\n const candidate = join(projectsDir, projectDir, `${sessionId}.jsonl`);\n if (existsSync(candidate)) return candidate;\n }\n return null;\n}\n\n/**\n * AIBroker's `fetchLiveSessions()` identifies a session by its iTerm2 pane id\n * (e.g. an AIBroker/iTerm UUID), not the Claude session id `<uuid>.jsonl`\n * transcripts are named after — the two are unrelated identifiers. The\n * status line bridges them on every refresh via `claude-session-map.json`\n * (see `recordSessionMapEntry` in ../workers/scope.ts): this picks, among the\n * map entries whose `term` pane UUID matches, the most recently written one.\n * Returns null when the pane has no (fresh enough) mapped Claude session.\n */\nexport function resolveClaudeSessionIdFromMap(paneId: string, logDir: string): string | null {\n const path = sessionMapPath(logDir);\n if (!paneId || !existsSync(path)) return null;\n let map: Record<string, SessionMapEntry>;\n try {\n map = JSON.parse(readFileSync(path, \"utf8\")) as Record<string, SessionMapEntry>;\n } catch {\n return null;\n }\n let best: SessionMapEntry | null = null;\n for (const entry of Object.values(map)) {\n if (!entry.term || itermUuid(entry.term) !== paneId) continue;\n if (!best || entry.ts > best.ts) best = entry;\n }\n return best?.session ?? null;\n}\n\n/** Claude Code's project-dir encoding of a cwd: every \"/\" becomes \"-\". */\nfunction encodeProjectDirName(cwd: string): string {\n return cwd.replace(/\\//g, \"-\");\n}\n\n/**\n * Is this live \"claude\"-kind session actually a worker pane rather than an\n * interactive one? `claude -p` workers running in a worktree write their\n * transcript under ~/.claude/projects too (encoded cwd = the worktree dir),\n * so a missing transcript is NOT how workers get excluded — this predicate\n * is. Either signal is enough:\n * (a) the transcript's project-dir name is the encoded form of a path\n * under <logDir>/worktrees (every worker worktree lives there), or\n * (b) the broker's session name/paiName names a path under the workers\n * log dir or one of its worktrees (best-effort: AIBroker does not\n * guarantee this, but honors it when present).\n */\nexport function isWorkerSession(\n meta: AiBrokerSessionMeta,\n transcriptPath: string | null,\n logDir: string\n): boolean {\n const worktreesPrefix = encodeProjectDirName(worktreesDir(logDir));\n if (transcriptPath) {\n const projectDirName = transcriptPath.split(\"/\").slice(0, -1).pop() ?? \"\";\n if (projectDirName.startsWith(worktreesPrefix)) return true;\n }\n const rawPrefix = worktreesDir(logDir);\n for (const field of [meta.name, meta.paiName]) {\n if (field && (field.includes(rawPrefix) || field.includes(logDir))) return true;\n }\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// Tail snapshot: last turn's context, mid-turn flag, last real prompt\n// ---------------------------------------------------------------------------\n\nexport interface TranscriptSnapshot {\n /** cache_read + cache_creation + input on the most recent assistant turn seen. */\n context: number | null;\n /** True when the last line in the file shows the session still working: an\n * assistant turn with no usage yet (streaming) or a stop_reason other than\n * end_turn/stop_sequence (e.g. mid tool-call), or a real user prompt with\n * no reply behind it yet. A beat sent now would land mid-generation. */\n midTurn: boolean;\n /** The last real (human-authored) user prompt seen, if any. */\n lastRealPrompt: { key: string; text: string } | null;\n}\n\nfunction isMidTurn(line: AssistantLine | null): boolean {\n if (!line) return false;\n if (line.type === \"user\" && isRealUserPrompt(line)) return true;\n if (line.type === \"assistant\") {\n const stopReason = line.message?.stop_reason;\n if (stopReason === undefined || stopReason === null) return true;\n return stopReason !== \"end_turn\" && stopReason !== \"stop_sequence\";\n }\n return false;\n}\n\nexport function readTranscriptSnapshot(path: string): TranscriptSnapshot {\n let lines: string[];\n try {\n lines = readFileSync(path, \"utf8\").split(\"\\n\");\n } catch {\n return { context: null, midTurn: false, lastRealPrompt: null };\n }\n\n let lastLine: AssistantLine | null = null;\n let context: number | null = null;\n let lastRealPrompt: { key: string; text: string } | null = null;\n\n for (let i = lines.length - 1; i >= 0; i--) {\n const raw = lines[i].trim();\n if (!raw) continue;\n let obj: AssistantLine;\n try {\n obj = JSON.parse(raw) as AssistantLine;\n } catch {\n continue;\n }\n if (lastLine === null) lastLine = obj;\n\n if (context === null && obj.type === \"assistant\" && obj.message?.usage) {\n const usage = obj.message.usage;\n context =\n (Number(usage.cache_read_input_tokens) || 0) +\n (Number(usage.cache_creation_input_tokens) || 0) +\n (Number(usage.input_tokens) || 0);\n }\n\n if (lastRealPrompt === null && isRealUserPrompt(obj)) {\n const key = obj.uuid ?? obj.timestamp ?? String(i);\n lastRealPrompt = { key, text: extractUserPromptText(obj).trim() };\n }\n\n if (context !== null && lastRealPrompt !== null) break;\n }\n\n return { context, midTurn: isMidTurn(lastLine), lastRealPrompt };\n}\n\n// ---------------------------------------------------------------------------\n// Ledger — same file WORKER-KEEPALIVE lines go to (workers.logDir/ledger.log)\n// ---------------------------------------------------------------------------\n\n/** Ledger event tag; `pai daemon keepalive` and any log-tailer key off this. */\nexport const SESSION_KEEPALIVE_EVENT = \"SESSION-KEEPALIVE\";\n\nexport function sessionKeepaliveLedgerPath(): string {\n const { workers } = readWorkersSection();\n return join(workersLogDir(workers), \"ledger.log\");\n}\n\n/** The counts/last lines `pai daemon keepalive` prints, scoped to this event. */\nexport function sessionKeepaliveLedgerSummary(\n path: string = sessionKeepaliveLedgerPath(),\n lastN = 10\n): { sent: number; skipped: number; lastLines: string[] } {\n if (!existsSync(path)) return { sent: 0, skipped: 0, lastLines: [] };\n const lines = readFileSync(path, \"utf8\")\n .split(\"\\n\")\n .filter((l) => l.includes(SESSION_KEEPALIVE_EVENT));\n const sent = lines.filter((l) => / result=sent(\\s|$)/.test(l)).length;\n return { sent, skipped: lines.length - sent, lastLines: lines.slice(-lastN) };\n}\n\n// ---------------------------------------------------------------------------\n// The tick\n// ---------------------------------------------------------------------------\n\nexport interface SessionKeepaliveDeps {\n now: () => Date;\n fetchLiveSessions: () => Promise<AiBrokerSessionMeta[]>;\n /** AIBroker pane id (fetchLiveSessions' `sessionId`) -> Claude transcript\n * session id, via the status line's claude-session-map.json bridge. */\n resolveClaudeSessionId: (paneId: string) => string | null;\n findTranscript: (sessionId: string) => string | null;\n mtimeMs: (path: string) => number | null;\n readSnapshot: (path: string) => TranscriptSnapshot;\n sendBeat: (sessionId: string, text: string) => Promise<{ ok: boolean; error?: string }>;\n loadState: () => SessionKeepaliveState;\n saveState: (s: SessionKeepaliveState) => void;\n ledger: (kv: Record<string, string | number | null | undefined>) => void;\n}\n\nexport interface SessionKeepaliveResult {\n sessionId: string;\n result: \"sent\" | string; // \"skipped:<reason>\"\n}\n\nfunction defaultDeps(): SessionKeepaliveDeps {\n const { workers } = readWorkersSection();\n const logDir = workersLogDir(workers);\n return {\n now: () => new Date(),\n fetchLiveSessions: fetchLiveSessionsDefault,\n resolveClaudeSessionId: (paneId) => resolveClaudeSessionIdFromMap(paneId, logDir),\n findTranscript: (id) => findSessionTranscript(id),\n mtimeMs: (p) => {\n try {\n return statSync(p).mtimeMs;\n } catch {\n return null;\n }\n },\n readSnapshot: readTranscriptSnapshot,\n sendBeat: (id, text) => sendToSessionDefault(id, text, undefined, { noReply: true }),\n loadState: () => loadSessionKeepaliveState(),\n saveState: (s) => saveSessionKeepaliveState(s),\n ledger: (kv) => appendLedger(sessionKeepaliveLedgerPath(), SESSION_KEEPALIVE_EVENT, kv),\n };\n}\n\n/**\n * One tick over every live interactive session: beat the ones that qualify,\n * skip (with a reason) the ones that don't. Returns one result per live\n * \"claude\"-kind session for tests to assert against; writes the ledger line\n * and (when any counter changed) the state file as side effects.\n */\nexport async function runSessionKeepaliveTick(\n config: SessionsCacheKeepaliveConfig,\n overrides: Partial<SessionKeepaliveDeps> = {}\n): Promise<SessionKeepaliveResult[]> {\n if (!config.enabled) return [];\n\n const d: SessionKeepaliveDeps = { ...defaultDeps(), ...overrides };\n const sessions = await d.fetchLiveSessions();\n const state = d.loadState();\n const now = d.now();\n const { workers } = readWorkersSection();\n const logDir = workersLogDir(workers);\n const results: SessionKeepaliveResult[] = [];\n let anyChanged = false;\n\n for (const s of sessions) {\n if (s.kind !== \"claude\") continue;\n const paneId = s.sessionId;\n\n const claudeId = d.resolveClaudeSessionId(paneId);\n if (!claudeId) {\n d.ledger({ session: null, pane: paneId, result: \"skipped:unmapped\" });\n results.push({ sessionId: paneId, result: \"skipped:unmapped\" });\n continue;\n }\n const sessionId = claudeId;\n const entry = { ...(state[sessionId] ?? emptyEntry()) };\n let changed = false;\n\n const transcript = d.findTranscript(sessionId);\n\n if (isWorkerSession(s, transcript, logDir)) {\n d.ledger({ session: sessionId, pane: paneId, result: \"skipped:worker\" });\n results.push({ sessionId, result: \"skipped:worker\" });\n continue;\n }\n\n if (!transcript) {\n d.ledger({ session: sessionId, pane: paneId, result: \"skipped:no-transcript\" });\n results.push({ sessionId, result: \"skipped:no-transcript\" });\n continue;\n }\n\n const snapshot = d.readSnapshot(transcript);\n\n // A fresh real prompt (not the keepalive's own echo) resets the beat\n // count — the idle stretch it capped is over.\n if (snapshot.lastRealPrompt && snapshot.lastRealPrompt.key !== entry.lastRealPromptKey) {\n entry.lastRealPromptKey = snapshot.lastRealPrompt.key;\n if (snapshot.lastRealPrompt.text !== config.prompt) entry.beats = 0;\n changed = true;\n }\n\n const mtime = d.mtimeMs(transcript);\n const idleMin = mtime === null ? null : (now.getTime() - mtime) / 60_000;\n\n const skip: string | null =\n mtime === null || idleMin === null\n ? \"no-mtime\"\n : idleMin < config.idleMinutes\n ? `idle:${idleMin.toFixed(1)}min`\n : !isWithinActiveHours(now, config.activeHours)\n ? \"hours\"\n : snapshot.context === null || snapshot.context < config.minContextTokens\n ? \"context\"\n : snapshot.midTurn\n ? \"mid-turn\"\n : entry.beats >= config.maxBeats\n ? \"max-beats\"\n : null;\n\n const ledgerBase = {\n session: sessionId,\n pane: paneId,\n idle_min: idleMin === null ? null : idleMin.toFixed(1),\n context: snapshot.context,\n };\n\n if (skip) {\n d.ledger({ ...ledgerBase, beat: `${entry.beats}/${config.maxBeats}`, result: `skipped:${skip}` });\n results.push({ sessionId, result: `skipped:${skip}` });\n } else {\n const sent = await d.sendBeat(paneId, config.prompt);\n if (sent.ok) {\n entry.beats += 1;\n entry.lastBeatAt = now.toISOString();\n changed = true;\n d.ledger({ ...ledgerBase, beat: `${entry.beats}/${config.maxBeats}`, result: \"sent\" });\n results.push({ sessionId, result: \"sent\" });\n } else {\n d.ledger({ ...ledgerBase, beat: `${entry.beats}/${config.maxBeats}`, result: \"skipped:send-failed\" });\n results.push({ sessionId, result: \"skipped:send-failed\" });\n }\n }\n\n if (changed) {\n state[sessionId] = entry;\n anyChanged = true;\n }\n }\n\n if (anyChanged) d.saveState(state);\n return results;\n}\n\n"],"mappings":"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"}