@poolzin/pool-bot 2026.2.0 → 2026.2.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 (258) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/README-header.png +0 -0
  3. package/dist/agents/bash-tools.exec.js +76 -25
  4. package/dist/agents/cli-runner/helpers.js +9 -11
  5. package/dist/agents/context.js +1 -1
  6. package/dist/agents/identity.js +47 -7
  7. package/dist/agents/memory-search.js +25 -8
  8. package/dist/agents/model-catalog.js +1 -1
  9. package/dist/agents/model-selection.js +21 -0
  10. package/dist/agents/pi-embedded-block-chunker.js +117 -42
  11. package/dist/agents/pi-embedded-helpers/errors.js +183 -78
  12. package/dist/agents/pi-embedded-helpers.js +1 -1
  13. package/dist/agents/pi-embedded-runner/compact.js +8 -10
  14. package/dist/agents/pi-embedded-runner/model.js +62 -3
  15. package/dist/agents/pi-embedded-runner/run/attempt.js +21 -11
  16. package/dist/agents/pi-embedded-runner/run.js +199 -46
  17. package/dist/agents/pi-embedded-runner/system-prompt.js +10 -2
  18. package/dist/agents/pi-embedded-subscribe.js +118 -29
  19. package/dist/agents/pi-tools.js +10 -5
  20. package/dist/agents/poolbot-tools.js +15 -10
  21. package/dist/agents/sandbox-paths.js +31 -0
  22. package/dist/agents/session-tool-result-guard.js +94 -15
  23. package/dist/agents/shell-utils.js +51 -0
  24. package/dist/agents/skills/bundled-context.js +23 -0
  25. package/dist/agents/skills/bundled-dir.js +41 -7
  26. package/dist/agents/skills-install.js +60 -23
  27. package/dist/agents/subagent-announce.js +79 -34
  28. package/dist/agents/tool-policy.conformance.js +14 -0
  29. package/dist/agents/tool-policy.js +24 -0
  30. package/dist/agents/tools/cron-tool.js +166 -19
  31. package/dist/agents/tools/discord-actions-presence.js +78 -0
  32. package/dist/agents/tools/image-tool.js +1 -1
  33. package/dist/agents/tools/message-tool.js +56 -2
  34. package/dist/agents/tools/sessions-history-tool.js +69 -1
  35. package/dist/agents/tools/web-search.js +211 -42
  36. package/dist/agents/usage.js +23 -1
  37. package/dist/agents/workspace-run.js +67 -0
  38. package/dist/agents/workspace-templates.js +44 -0
  39. package/dist/auto-reply/command-auth.js +121 -6
  40. package/dist/auto-reply/envelope.js +74 -82
  41. package/dist/auto-reply/reply/commands-compact.js +1 -0
  42. package/dist/auto-reply/reply/commands-context-report.js +1 -0
  43. package/dist/auto-reply/reply/commands-context.js +1 -0
  44. package/dist/auto-reply/reply/commands-models.js +107 -60
  45. package/dist/auto-reply/reply/commands-ptt.js +171 -0
  46. package/dist/auto-reply/reply/get-reply-run.js +2 -1
  47. package/dist/auto-reply/reply/inbound-context.js +5 -1
  48. package/dist/auto-reply/reply/mentions.js +1 -1
  49. package/dist/auto-reply/reply/model-selection.js +3 -3
  50. package/dist/auto-reply/thinking.js +88 -43
  51. package/dist/browser/bridge-server.js +13 -0
  52. package/dist/browser/cdp.helpers.js +38 -24
  53. package/dist/browser/client-fetch.js +50 -7
  54. package/dist/browser/config.js +1 -10
  55. package/dist/browser/extension-relay.js +101 -40
  56. package/dist/browser/pw-ai.js +1 -1
  57. package/dist/browser/pw-session.js +143 -8
  58. package/dist/browser/pw-tools-core.interactions.js +125 -27
  59. package/dist/browser/pw-tools-core.responses.js +1 -1
  60. package/dist/browser/pw-tools-core.state.js +1 -1
  61. package/dist/browser/routes/agent.act.js +86 -41
  62. package/dist/browser/routes/dispatcher.js +4 -4
  63. package/dist/browser/screenshot.js +1 -1
  64. package/dist/browser/server.js +13 -0
  65. package/dist/build-info.json +3 -3
  66. package/dist/canvas-host/a2ui/index.html +28 -28
  67. package/dist/channels/reply-prefix.js +8 -1
  68. package/dist/cli/cron-cli/register.cron-add.js +61 -40
  69. package/dist/cli/cron-cli/register.cron-edit.js +60 -34
  70. package/dist/cli/cron-cli/shared.js +56 -41
  71. package/dist/cli/dns-cli.js +26 -14
  72. package/dist/cli/gateway-cli/register.js +37 -19
  73. package/dist/cli/memory-cli.js +5 -5
  74. package/dist/cli/parse-bytes.js +37 -0
  75. package/dist/cli/update-cli.js +173 -52
  76. package/dist/commands/agent.js +1 -0
  77. package/dist/commands/auth-choice.apply.oauth.js +1 -1
  78. package/dist/commands/doctor-config-flow.js +61 -5
  79. package/dist/commands/doctor-state-migrations.js +1 -1
  80. package/dist/commands/health.js +1 -1
  81. package/dist/commands/model-allowlist.js +29 -0
  82. package/dist/commands/model-picker.js +2 -1
  83. package/dist/commands/models/list.registry.js +1 -1
  84. package/dist/commands/models/list.status-command.js +43 -23
  85. package/dist/commands/models/shared.js +15 -0
  86. package/dist/commands/onboard-custom.js +384 -0
  87. package/dist/commands/onboard-non-interactive/local/auth-choice-inference.js +35 -0
  88. package/dist/commands/onboard-non-interactive/local/auth-choice.js +6 -3
  89. package/dist/commands/onboard-skills.js +63 -38
  90. package/dist/commands/openai-model-default.js +41 -0
  91. package/dist/compat/legacy-names.js +2 -0
  92. package/dist/config/defaults.js +3 -2
  93. package/dist/config/paths.js +136 -35
  94. package/dist/config/plugin-auto-enable.js +21 -5
  95. package/dist/config/redact-snapshot.js +153 -0
  96. package/dist/config/schema.field-metadata.js +590 -0
  97. package/dist/config/schema.js +2 -2
  98. package/dist/config/sessions/store.js +291 -23
  99. package/dist/config/zod-schema.agent-defaults.js +3 -0
  100. package/dist/config/zod-schema.agent-runtime.js +13 -2
  101. package/dist/config/zod-schema.providers-core.js +142 -0
  102. package/dist/config/zod-schema.session.js +3 -0
  103. package/dist/control-ui/assets/{index-CIRDm-Lu.css → index-CSfXd2LO.css} +1 -1
  104. package/dist/control-ui/assets/{index-CmNMuoem.js → index-HRr1grwl.js} +446 -413
  105. package/dist/control-ui/assets/index-HRr1grwl.js.map +1 -0
  106. package/dist/control-ui/index.html +4 -4
  107. package/dist/cron/delivery.js +57 -0
  108. package/dist/cron/isolated-agent/delivery-target.js +18 -3
  109. package/dist/cron/isolated-agent/helpers.js +22 -5
  110. package/dist/cron/isolated-agent/run.js +172 -63
  111. package/dist/cron/isolated-agent/session.js +2 -0
  112. package/dist/cron/normalize.js +356 -28
  113. package/dist/cron/parse.js +10 -5
  114. package/dist/cron/run-log.js +35 -10
  115. package/dist/cron/schedule.js +41 -6
  116. package/dist/cron/service/jobs.js +208 -35
  117. package/dist/cron/service/ops.js +72 -16
  118. package/dist/cron/service/state.js +2 -0
  119. package/dist/cron/service/store.js +386 -14
  120. package/dist/cron/service/timer.js +390 -147
  121. package/dist/cron/session-reaper.js +86 -0
  122. package/dist/cron/store.js +23 -8
  123. package/dist/cron/validate-timestamp.js +43 -0
  124. package/dist/discord/monitor/agent-components.js +438 -0
  125. package/dist/discord/monitor/allow-list.js +28 -5
  126. package/dist/discord/monitor/gateway-registry.js +29 -0
  127. package/dist/discord/monitor/native-command.js +44 -23
  128. package/dist/discord/monitor/sender-identity.js +45 -0
  129. package/dist/discord/pluralkit.js +27 -0
  130. package/dist/discord/send.outbound.js +92 -5
  131. package/dist/discord/send.shared.js +60 -23
  132. package/dist/discord/targets.js +84 -1
  133. package/dist/entry.js +15 -9
  134. package/dist/extensionAPI.js +8 -0
  135. package/dist/gateway/control-ui.js +8 -1
  136. package/dist/gateway/hooks-mapping.js +3 -0
  137. package/dist/gateway/hooks.js +65 -0
  138. package/dist/gateway/net.js +96 -31
  139. package/dist/gateway/node-command-policy.js +50 -15
  140. package/dist/gateway/origin-check.js +56 -0
  141. package/dist/gateway/protocol/client-info.js +9 -0
  142. package/dist/gateway/protocol/index.js +9 -2
  143. package/dist/gateway/protocol/schema/agents-models-skills.js +71 -1
  144. package/dist/gateway/protocol/schema/cron.js +22 -10
  145. package/dist/gateway/protocol/schema/protocol-schemas.js +16 -2
  146. package/dist/gateway/protocol/schema/sessions.js +12 -0
  147. package/dist/gateway/server/hooks.js +1 -1
  148. package/dist/gateway/server-broadcast.js +26 -9
  149. package/dist/gateway/server-chat.js +112 -23
  150. package/dist/gateway/server-discovery-runtime.js +10 -2
  151. package/dist/gateway/server-http.js +109 -11
  152. package/dist/gateway/server-methods/agent-timestamp.js +60 -0
  153. package/dist/gateway/server-methods/agents.js +321 -2
  154. package/dist/gateway/server-methods/usage.js +559 -16
  155. package/dist/gateway/server-runtime-state.js +22 -8
  156. package/dist/gateway/server-startup-memory.js +16 -0
  157. package/dist/gateway/server.impl.js +5 -1
  158. package/dist/gateway/session-utils.fs.js +23 -25
  159. package/dist/gateway/session-utils.js +20 -10
  160. package/dist/gateway/sessions-patch.js +7 -22
  161. package/dist/gateway/test-helpers.mocks.js +11 -7
  162. package/dist/gateway/test-helpers.server.js +35 -2
  163. package/dist/imessage/constants.js +2 -0
  164. package/dist/imessage/monitor/deliver.js +4 -1
  165. package/dist/imessage/monitor/monitor-provider.js +51 -1
  166. package/dist/infra/bonjour-discovery.js +131 -70
  167. package/dist/infra/control-ui-assets.js +134 -12
  168. package/dist/infra/errors.js +12 -0
  169. package/dist/infra/exec-approvals.js +266 -57
  170. package/dist/infra/format-time/format-datetime.js +79 -0
  171. package/dist/infra/format-time/format-duration.js +81 -0
  172. package/dist/infra/format-time/format-relative.js +80 -0
  173. package/dist/infra/heartbeat-runner.js +140 -49
  174. package/dist/infra/home-dir.js +54 -0
  175. package/dist/infra/net/fetch-guard.js +122 -0
  176. package/dist/infra/net/ssrf.js +65 -29
  177. package/dist/infra/outbound/abort.js +14 -0
  178. package/dist/infra/outbound/message-action-runner.js +77 -13
  179. package/dist/infra/outbound/outbound-session.js +143 -37
  180. package/dist/infra/poolbot-root.js +43 -1
  181. package/dist/infra/session-cost-usage.js +631 -41
  182. package/dist/infra/state-migrations.js +317 -47
  183. package/dist/infra/update-global.js +35 -0
  184. package/dist/infra/update-runner.js +149 -43
  185. package/dist/infra/warning-filter.js +65 -0
  186. package/dist/infra/widearea-dns.js +30 -9
  187. package/dist/logging/redact-identifier.js +12 -0
  188. package/dist/media/fetch.js +81 -58
  189. package/dist/media/store.js +2 -0
  190. package/dist/media-understanding/apply.js +403 -3
  191. package/dist/media-understanding/attachments.js +38 -27
  192. package/dist/media-understanding/defaults.js +16 -0
  193. package/dist/media-understanding/providers/deepgram/audio.js +22 -14
  194. package/dist/media-understanding/providers/google/audio.js +24 -17
  195. package/dist/media-understanding/providers/google/video.js +24 -17
  196. package/dist/media-understanding/providers/image.js +3 -3
  197. package/dist/media-understanding/providers/index.js +4 -1
  198. package/dist/media-understanding/providers/openai/audio.js +22 -14
  199. package/dist/media-understanding/providers/shared.js +16 -11
  200. package/dist/media-understanding/providers/zai/index.js +6 -0
  201. package/dist/media-understanding/runner.js +158 -90
  202. package/dist/memory/batch-voyage.js +277 -0
  203. package/dist/memory/embeddings-voyage.js +75 -0
  204. package/dist/memory/embeddings.js +28 -16
  205. package/dist/memory/internal.js +101 -18
  206. package/dist/memory/manager.js +154 -48
  207. package/dist/memory/search-manager.js +173 -0
  208. package/dist/memory/session-files.js +9 -3
  209. package/dist/node-host/runner.js +34 -24
  210. package/dist/node-host/with-timeout.js +27 -0
  211. package/dist/plugins/commands.js +5 -1
  212. package/dist/plugins/config-state.js +86 -7
  213. package/dist/plugins/source-display.js +51 -0
  214. package/dist/process/exec.js +20 -2
  215. package/dist/routing/resolve-route.js +12 -0
  216. package/dist/routing/session-key.js +15 -0
  217. package/dist/runtime.js +2 -0
  218. package/dist/security/audit-extra.async.js +601 -0
  219. package/dist/security/audit-extra.js +2 -830
  220. package/dist/security/audit-extra.sync.js +505 -0
  221. package/dist/security/channel-metadata.js +34 -0
  222. package/dist/security/external-content.js +88 -6
  223. package/dist/security/skill-scanner.js +330 -0
  224. package/dist/sessions/session-key-utils.js +7 -0
  225. package/dist/signal/monitor/event-handler.js +80 -1
  226. package/dist/slack/monitor/media.js +85 -15
  227. package/dist/tailscale/detect.js +1 -2
  228. package/dist/telegram/bot/helpers.js +109 -28
  229. package/dist/telegram/bot-handlers.js +144 -3
  230. package/dist/telegram/bot-message-context.js +37 -10
  231. package/dist/telegram/bot-message-dispatch.js +54 -17
  232. package/dist/telegram/bot-native-commands.js +86 -29
  233. package/dist/telegram/bot.js +30 -29
  234. package/dist/telegram/model-buttons.js +163 -0
  235. package/dist/telegram/monitor.js +110 -85
  236. package/dist/telegram/send.js +129 -47
  237. package/dist/terminal/restore.js +45 -0
  238. package/dist/test-helpers/state-dir-env.js +16 -0
  239. package/dist/tts/tts.js +12 -6
  240. package/dist/tui/tui-session-actions.js +166 -54
  241. package/dist/utils/fetch-timeout.js +20 -0
  242. package/dist/utils/normalize-secret-input.js +19 -0
  243. package/dist/utils/transcript-tools.js +58 -0
  244. package/dist/utils.js +45 -14
  245. package/dist/version.js +42 -5
  246. package/dist/wizard/clack-prompter.js +9 -6
  247. package/extensions/googlechat/node_modules/.bin/poolbot +21 -0
  248. package/extensions/googlechat/package.json +2 -2
  249. package/extensions/line/node_modules/.bin/poolbot +21 -0
  250. package/extensions/line/package.json +1 -1
  251. package/extensions/matrix/node_modules/.bin/poolbot +21 -0
  252. package/extensions/matrix/package.json +1 -1
  253. package/extensions/memory-core/node_modules/.bin/poolbot +21 -0
  254. package/extensions/memory-core/package.json +4 -1
  255. package/extensions/twitch/node_modules/.bin/poolbot +21 -0
  256. package/extensions/twitch/package.json +1 -1
  257. package/package.json +183 -24
  258. package/dist/control-ui/assets/index-CmNMuoem.js.map +0 -1
@@ -1,54 +1,134 @@
1
+ import fs from "node:fs";
1
2
  import { loadConfig } from "../../config/config.js";
2
- import { loadCostUsageSummary } from "../../infra/session-cost-usage.js";
3
+ import { resolveSessionFilePath } from "../../config/sessions/paths.js";
3
4
  import { loadProviderUsageSummary } from "../../infra/provider-usage.js";
5
+ import { loadCostUsageSummary, loadSessionCostSummary, loadSessionUsageTimeSeries, discoverAllSessions, } from "../../infra/session-cost-usage.js";
6
+ import { parseAgentSessionKey } from "../../routing/session-key.js";
7
+ import { ErrorCodes, errorShape, formatValidationErrors, validateSessionsUsageParams, } from "../protocol/index.js";
8
+ import { listAgentsForGateway, loadCombinedSessionStoreForGateway, loadSessionEntry, } from "../session-utils.js";
4
9
  const COST_USAGE_CACHE_TTL_MS = 30_000;
5
10
  const costUsageCache = new Map();
11
+ /**
12
+ * Parse a date string (YYYY-MM-DD) to start of day timestamp in UTC.
13
+ * Returns undefined if invalid.
14
+ */
15
+ const parseDateToMs = (raw) => {
16
+ if (typeof raw !== "string" || !raw.trim()) {
17
+ return undefined;
18
+ }
19
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(raw.trim());
20
+ if (!match) {
21
+ return undefined;
22
+ }
23
+ const [, year, month, day] = match;
24
+ // Use UTC to ensure consistent behavior across timezones
25
+ const ms = Date.UTC(parseInt(year), parseInt(month) - 1, parseInt(day));
26
+ if (Number.isNaN(ms)) {
27
+ return undefined;
28
+ }
29
+ return ms;
30
+ };
6
31
  const parseDays = (raw) => {
7
- if (typeof raw === "number" && Number.isFinite(raw))
32
+ if (typeof raw === "number" && Number.isFinite(raw)) {
8
33
  return Math.floor(raw);
34
+ }
9
35
  if (typeof raw === "string" && raw.trim() !== "") {
10
36
  const parsed = Number(raw);
11
- if (Number.isFinite(parsed))
37
+ if (Number.isFinite(parsed)) {
12
38
  return Math.floor(parsed);
39
+ }
40
+ }
41
+ return undefined;
42
+ };
43
+ /**
44
+ * Get date range from params (startDate/endDate or days).
45
+ * Falls back to last 30 days if not provided.
46
+ */
47
+ const parseDateRange = (params) => {
48
+ const now = new Date();
49
+ // Use UTC for consistent date handling
50
+ const todayStartMs = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
51
+ const todayEndMs = todayStartMs + 24 * 60 * 60 * 1000 - 1;
52
+ const startMs = parseDateToMs(params.startDate);
53
+ const endMs = parseDateToMs(params.endDate);
54
+ if (startMs !== undefined && endMs !== undefined) {
55
+ // endMs should be end of day
56
+ return { startMs, endMs: endMs + 24 * 60 * 60 * 1000 - 1 };
57
+ }
58
+ const days = parseDays(params.days);
59
+ if (days !== undefined) {
60
+ const clampedDays = Math.max(1, days);
61
+ const start = todayStartMs - (clampedDays - 1) * 24 * 60 * 60 * 1000;
62
+ return { startMs: start, endMs: todayEndMs };
13
63
  }
14
- return 30;
64
+ // Default to last 30 days
65
+ const defaultStartMs = todayStartMs - 29 * 24 * 60 * 60 * 1000;
66
+ return { startMs: defaultStartMs, endMs: todayEndMs };
15
67
  };
68
+ async function discoverAllSessionsForUsage(params) {
69
+ const agents = listAgentsForGateway(params.config).agents;
70
+ const results = await Promise.all(agents.map(async (agent) => {
71
+ const sessions = await discoverAllSessions({
72
+ agentId: agent.id,
73
+ startMs: params.startMs,
74
+ endMs: params.endMs,
75
+ });
76
+ return sessions.map((session) => ({ ...session, agentId: agent.id }));
77
+ }));
78
+ return results.flat().toSorted((a, b) => b.mtime - a.mtime);
79
+ }
16
80
  async function loadCostUsageSummaryCached(params) {
17
- const days = Math.max(1, params.days);
81
+ const cacheKey = `${params.startMs}-${params.endMs}`;
18
82
  const now = Date.now();
19
- const cached = costUsageCache.get(days);
83
+ const cached = costUsageCache.get(cacheKey);
20
84
  if (cached?.summary && cached.updatedAt && now - cached.updatedAt < COST_USAGE_CACHE_TTL_MS) {
21
85
  return cached.summary;
22
86
  }
23
87
  if (cached?.inFlight) {
24
- if (cached.summary)
88
+ if (cached.summary) {
25
89
  return cached.summary;
90
+ }
26
91
  return await cached.inFlight;
27
92
  }
28
93
  const entry = cached ?? {};
29
- const inFlight = loadCostUsageSummary({ days, config: params.config })
94
+ const inFlight = loadCostUsageSummary({
95
+ startMs: params.startMs,
96
+ endMs: params.endMs,
97
+ config: params.config,
98
+ })
30
99
  .then((summary) => {
31
- costUsageCache.set(days, { summary, updatedAt: Date.now() });
100
+ costUsageCache.set(cacheKey, { summary, updatedAt: Date.now() });
32
101
  return summary;
33
102
  })
34
103
  .catch((err) => {
35
- if (entry.summary)
104
+ if (entry.summary) {
36
105
  return entry.summary;
106
+ }
37
107
  throw err;
38
108
  })
39
109
  .finally(() => {
40
- const current = costUsageCache.get(days);
110
+ const current = costUsageCache.get(cacheKey);
41
111
  if (current?.inFlight === inFlight) {
42
112
  current.inFlight = undefined;
43
- costUsageCache.set(days, current);
113
+ costUsageCache.set(cacheKey, current);
44
114
  }
45
115
  });
46
116
  entry.inFlight = inFlight;
47
- costUsageCache.set(days, entry);
48
- if (entry.summary)
117
+ costUsageCache.set(cacheKey, entry);
118
+ if (entry.summary) {
49
119
  return entry.summary;
120
+ }
50
121
  return await inFlight;
51
122
  }
123
+ // Exposed for unit tests (kept as a single export to avoid widening the public API surface).
124
+ export const __test = {
125
+ parseDateToMs,
126
+ parseDays,
127
+ parseDateRange,
128
+ discoverAllSessionsForUsage,
129
+ loadCostUsageSummaryCached,
130
+ costUsageCache,
131
+ };
52
132
  export const usageHandlers = {
53
133
  "usage.status": async ({ respond }) => {
54
134
  const summary = await loadProviderUsageSummary();
@@ -56,8 +136,471 @@ export const usageHandlers = {
56
136
  },
57
137
  "usage.cost": async ({ respond, params }) => {
58
138
  const config = loadConfig();
59
- const days = parseDays(params?.days);
60
- const summary = await loadCostUsageSummaryCached({ days, config });
139
+ const { startMs, endMs } = parseDateRange({
140
+ startDate: params?.startDate,
141
+ endDate: params?.endDate,
142
+ days: params?.days,
143
+ });
144
+ const summary = await loadCostUsageSummaryCached({ startMs, endMs, config });
61
145
  respond(true, summary, undefined);
62
146
  },
147
+ "sessions.usage": async ({ respond, params }) => {
148
+ if (!validateSessionsUsageParams(params)) {
149
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `invalid sessions.usage params: ${formatValidationErrors(validateSessionsUsageParams.errors)}`));
150
+ return;
151
+ }
152
+ const p = params;
153
+ const config = loadConfig();
154
+ const { startMs, endMs } = parseDateRange({
155
+ startDate: p.startDate,
156
+ endDate: p.endDate,
157
+ });
158
+ const limit = typeof p.limit === "number" && Number.isFinite(p.limit) ? p.limit : 50;
159
+ const includeContextWeight = p.includeContextWeight ?? false;
160
+ const specificKey = typeof p.key === "string" ? p.key.trim() : null;
161
+ // Load session store for named sessions
162
+ const { store } = loadCombinedSessionStoreForGateway(config);
163
+ const now = Date.now();
164
+ const mergedEntries = [];
165
+ // Optimization: If a specific key is requested, skip full directory scan
166
+ if (specificKey) {
167
+ const parsed = parseAgentSessionKey(specificKey);
168
+ const agentIdFromKey = parsed?.agentId;
169
+ const keyRest = parsed?.rest ?? specificKey;
170
+ // Prefer the store entry when available, even if the caller provides a discovered key
171
+ // (`agent:<id>:<sessionId>`) for a session that now has a canonical store key.
172
+ const storeBySessionId = new Map();
173
+ for (const [key, entry] of Object.entries(store)) {
174
+ if (entry?.sessionId) {
175
+ storeBySessionId.set(entry.sessionId, { key, entry });
176
+ }
177
+ }
178
+ const storeMatch = store[specificKey]
179
+ ? { key: specificKey, entry: store[specificKey] }
180
+ : null;
181
+ const storeByIdMatch = storeBySessionId.get(keyRest) ?? null;
182
+ const resolvedStoreKey = storeMatch?.key ?? storeByIdMatch?.key ?? specificKey;
183
+ const storeEntry = storeMatch?.entry ?? storeByIdMatch?.entry;
184
+ const sessionId = storeEntry?.sessionId ?? keyRest;
185
+ // Resolve the session file path
186
+ const sessionFile = resolveSessionFilePath(sessionId, storeEntry, {
187
+ agentId: agentIdFromKey,
188
+ });
189
+ try {
190
+ const stats = fs.statSync(sessionFile);
191
+ if (stats.isFile()) {
192
+ mergedEntries.push({
193
+ key: resolvedStoreKey,
194
+ sessionId,
195
+ sessionFile,
196
+ label: storeEntry?.label,
197
+ updatedAt: storeEntry?.updatedAt ?? stats.mtimeMs,
198
+ storeEntry,
199
+ });
200
+ }
201
+ }
202
+ catch {
203
+ // File doesn't exist - no results for this key
204
+ }
205
+ }
206
+ else {
207
+ // Full discovery for list view
208
+ const discoveredSessions = await discoverAllSessionsForUsage({
209
+ config,
210
+ startMs,
211
+ endMs,
212
+ });
213
+ // Build a map of sessionId -> store entry for quick lookup
214
+ const storeBySessionId = new Map();
215
+ for (const [key, entry] of Object.entries(store)) {
216
+ if (entry?.sessionId) {
217
+ storeBySessionId.set(entry.sessionId, { key, entry });
218
+ }
219
+ }
220
+ for (const discovered of discoveredSessions) {
221
+ const storeMatch = storeBySessionId.get(discovered.sessionId);
222
+ if (storeMatch) {
223
+ // Named session from store
224
+ mergedEntries.push({
225
+ key: storeMatch.key,
226
+ sessionId: discovered.sessionId,
227
+ sessionFile: discovered.sessionFile,
228
+ label: storeMatch.entry.label,
229
+ updatedAt: storeMatch.entry.updatedAt ?? discovered.mtime,
230
+ storeEntry: storeMatch.entry,
231
+ });
232
+ }
233
+ else {
234
+ // Unnamed session - use session ID as key, no label
235
+ mergedEntries.push({
236
+ // Keep agentId in the key so the dashboard can attribute sessions and later fetch logs.
237
+ key: `agent:${discovered.agentId}:${discovered.sessionId}`,
238
+ sessionId: discovered.sessionId,
239
+ sessionFile: discovered.sessionFile,
240
+ label: undefined, // No label for unnamed sessions
241
+ updatedAt: discovered.mtime,
242
+ });
243
+ }
244
+ }
245
+ }
246
+ // Sort by most recent first
247
+ mergedEntries.sort((a, b) => b.updatedAt - a.updatedAt);
248
+ // Apply limit
249
+ const limitedEntries = mergedEntries.slice(0, limit);
250
+ // Load usage for each session
251
+ const sessions = [];
252
+ const aggregateTotals = {
253
+ input: 0,
254
+ output: 0,
255
+ cacheRead: 0,
256
+ cacheWrite: 0,
257
+ totalTokens: 0,
258
+ totalCost: 0,
259
+ inputCost: 0,
260
+ outputCost: 0,
261
+ cacheReadCost: 0,
262
+ cacheWriteCost: 0,
263
+ missingCostEntries: 0,
264
+ };
265
+ const aggregateMessages = {
266
+ total: 0,
267
+ user: 0,
268
+ assistant: 0,
269
+ toolCalls: 0,
270
+ toolResults: 0,
271
+ errors: 0,
272
+ };
273
+ const toolAggregateMap = new Map();
274
+ const byModelMap = new Map();
275
+ const byProviderMap = new Map();
276
+ const byAgentMap = new Map();
277
+ const byChannelMap = new Map();
278
+ const dailyAggregateMap = new Map();
279
+ const latencyTotals = {
280
+ count: 0,
281
+ sum: 0,
282
+ min: Number.POSITIVE_INFINITY,
283
+ max: 0,
284
+ p95Max: 0,
285
+ };
286
+ const dailyLatencyMap = new Map();
287
+ const modelDailyMap = new Map();
288
+ const emptyTotals = () => ({
289
+ input: 0,
290
+ output: 0,
291
+ cacheRead: 0,
292
+ cacheWrite: 0,
293
+ totalTokens: 0,
294
+ totalCost: 0,
295
+ inputCost: 0,
296
+ outputCost: 0,
297
+ cacheReadCost: 0,
298
+ cacheWriteCost: 0,
299
+ missingCostEntries: 0,
300
+ });
301
+ const mergeTotals = (target, source) => {
302
+ target.input += source.input;
303
+ target.output += source.output;
304
+ target.cacheRead += source.cacheRead;
305
+ target.cacheWrite += source.cacheWrite;
306
+ target.totalTokens += source.totalTokens;
307
+ target.totalCost += source.totalCost;
308
+ target.inputCost += source.inputCost;
309
+ target.outputCost += source.outputCost;
310
+ target.cacheReadCost += source.cacheReadCost;
311
+ target.cacheWriteCost += source.cacheWriteCost;
312
+ target.missingCostEntries += source.missingCostEntries;
313
+ };
314
+ for (const merged of limitedEntries) {
315
+ const usage = await loadSessionCostSummary({
316
+ sessionId: merged.sessionId,
317
+ sessionEntry: merged.storeEntry,
318
+ sessionFile: merged.sessionFile,
319
+ config,
320
+ startMs,
321
+ endMs,
322
+ });
323
+ if (usage) {
324
+ aggregateTotals.input += usage.input;
325
+ aggregateTotals.output += usage.output;
326
+ aggregateTotals.cacheRead += usage.cacheRead;
327
+ aggregateTotals.cacheWrite += usage.cacheWrite;
328
+ aggregateTotals.totalTokens += usage.totalTokens;
329
+ aggregateTotals.totalCost += usage.totalCost;
330
+ aggregateTotals.inputCost += usage.inputCost;
331
+ aggregateTotals.outputCost += usage.outputCost;
332
+ aggregateTotals.cacheReadCost += usage.cacheReadCost;
333
+ aggregateTotals.cacheWriteCost += usage.cacheWriteCost;
334
+ aggregateTotals.missingCostEntries += usage.missingCostEntries;
335
+ }
336
+ const agentId = parseAgentSessionKey(merged.key)?.agentId;
337
+ const channel = merged.storeEntry?.channel ?? merged.storeEntry?.origin?.provider;
338
+ const chatType = merged.storeEntry?.chatType ?? merged.storeEntry?.origin?.chatType;
339
+ if (usage) {
340
+ if (usage.messageCounts) {
341
+ aggregateMessages.total += usage.messageCounts.total;
342
+ aggregateMessages.user += usage.messageCounts.user;
343
+ aggregateMessages.assistant += usage.messageCounts.assistant;
344
+ aggregateMessages.toolCalls += usage.messageCounts.toolCalls;
345
+ aggregateMessages.toolResults += usage.messageCounts.toolResults;
346
+ aggregateMessages.errors += usage.messageCounts.errors;
347
+ }
348
+ if (usage.toolUsage) {
349
+ for (const tool of usage.toolUsage.tools) {
350
+ toolAggregateMap.set(tool.name, (toolAggregateMap.get(tool.name) ?? 0) + tool.count);
351
+ }
352
+ }
353
+ if (usage.modelUsage) {
354
+ for (const entry of usage.modelUsage) {
355
+ const modelKey = `${entry.provider ?? "unknown"}::${entry.model ?? "unknown"}`;
356
+ const modelExisting = byModelMap.get(modelKey) ??
357
+ {
358
+ provider: entry.provider,
359
+ model: entry.model,
360
+ count: 0,
361
+ totals: emptyTotals(),
362
+ };
363
+ modelExisting.count += entry.count;
364
+ mergeTotals(modelExisting.totals, entry.totals);
365
+ byModelMap.set(modelKey, modelExisting);
366
+ const providerKey = entry.provider ?? "unknown";
367
+ const providerExisting = byProviderMap.get(providerKey) ??
368
+ {
369
+ provider: entry.provider,
370
+ model: undefined,
371
+ count: 0,
372
+ totals: emptyTotals(),
373
+ };
374
+ providerExisting.count += entry.count;
375
+ mergeTotals(providerExisting.totals, entry.totals);
376
+ byProviderMap.set(providerKey, providerExisting);
377
+ }
378
+ }
379
+ if (usage.latency) {
380
+ const { count, avgMs, minMs, maxMs, p95Ms } = usage.latency;
381
+ if (count > 0) {
382
+ latencyTotals.count += count;
383
+ latencyTotals.sum += avgMs * count;
384
+ latencyTotals.min = Math.min(latencyTotals.min, minMs);
385
+ latencyTotals.max = Math.max(latencyTotals.max, maxMs);
386
+ latencyTotals.p95Max = Math.max(latencyTotals.p95Max, p95Ms);
387
+ }
388
+ }
389
+ if (usage.dailyLatency) {
390
+ for (const day of usage.dailyLatency) {
391
+ const existing = dailyLatencyMap.get(day.date) ?? {
392
+ date: day.date,
393
+ count: 0,
394
+ sum: 0,
395
+ min: Number.POSITIVE_INFINITY,
396
+ max: 0,
397
+ p95Max: 0,
398
+ };
399
+ existing.count += day.count;
400
+ existing.sum += day.avgMs * day.count;
401
+ existing.min = Math.min(existing.min, day.minMs);
402
+ existing.max = Math.max(existing.max, day.maxMs);
403
+ existing.p95Max = Math.max(existing.p95Max, day.p95Ms);
404
+ dailyLatencyMap.set(day.date, existing);
405
+ }
406
+ }
407
+ if (usage.dailyModelUsage) {
408
+ for (const entry of usage.dailyModelUsage) {
409
+ const key = `${entry.date}::${entry.provider ?? "unknown"}::${entry.model ?? "unknown"}`;
410
+ const existing = modelDailyMap.get(key) ??
411
+ {
412
+ date: entry.date,
413
+ provider: entry.provider,
414
+ model: entry.model,
415
+ tokens: 0,
416
+ cost: 0,
417
+ count: 0,
418
+ };
419
+ existing.tokens += entry.tokens;
420
+ existing.cost += entry.cost;
421
+ existing.count += entry.count;
422
+ modelDailyMap.set(key, existing);
423
+ }
424
+ }
425
+ if (agentId) {
426
+ const agentTotals = byAgentMap.get(agentId) ?? emptyTotals();
427
+ mergeTotals(agentTotals, usage);
428
+ byAgentMap.set(agentId, agentTotals);
429
+ }
430
+ if (channel) {
431
+ const channelTotals = byChannelMap.get(channel) ?? emptyTotals();
432
+ mergeTotals(channelTotals, usage);
433
+ byChannelMap.set(channel, channelTotals);
434
+ }
435
+ if (usage.dailyBreakdown) {
436
+ for (const day of usage.dailyBreakdown) {
437
+ const daily = dailyAggregateMap.get(day.date) ?? {
438
+ date: day.date,
439
+ tokens: 0,
440
+ cost: 0,
441
+ messages: 0,
442
+ toolCalls: 0,
443
+ errors: 0,
444
+ };
445
+ daily.tokens += day.tokens;
446
+ daily.cost += day.cost;
447
+ dailyAggregateMap.set(day.date, daily);
448
+ }
449
+ }
450
+ if (usage.dailyMessageCounts) {
451
+ for (const day of usage.dailyMessageCounts) {
452
+ const daily = dailyAggregateMap.get(day.date) ?? {
453
+ date: day.date,
454
+ tokens: 0,
455
+ cost: 0,
456
+ messages: 0,
457
+ toolCalls: 0,
458
+ errors: 0,
459
+ };
460
+ daily.messages += day.total;
461
+ daily.toolCalls += day.toolCalls;
462
+ daily.errors += day.errors;
463
+ dailyAggregateMap.set(day.date, daily);
464
+ }
465
+ }
466
+ }
467
+ sessions.push({
468
+ key: merged.key,
469
+ label: merged.label,
470
+ sessionId: merged.sessionId,
471
+ updatedAt: merged.updatedAt,
472
+ agentId,
473
+ channel,
474
+ chatType,
475
+ origin: merged.storeEntry?.origin,
476
+ modelOverride: merged.storeEntry?.modelOverride,
477
+ providerOverride: merged.storeEntry?.providerOverride,
478
+ modelProvider: merged.storeEntry?.modelProvider,
479
+ model: merged.storeEntry?.model,
480
+ usage,
481
+ contextWeight: includeContextWeight
482
+ ? (merged.storeEntry?.systemPromptReport ?? null)
483
+ : undefined,
484
+ });
485
+ }
486
+ // Format dates back to YYYY-MM-DD strings
487
+ const formatDateStr = (ms) => {
488
+ const d = new Date(ms);
489
+ return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}-${String(d.getUTCDate()).padStart(2, "0")}`;
490
+ };
491
+ const aggregates = {
492
+ messages: aggregateMessages,
493
+ tools: {
494
+ totalCalls: Array.from(toolAggregateMap.values()).reduce((sum, count) => sum + count, 0),
495
+ uniqueTools: toolAggregateMap.size,
496
+ tools: Array.from(toolAggregateMap.entries())
497
+ .map(([name, count]) => ({ name, count }))
498
+ .toSorted((a, b) => b.count - a.count),
499
+ },
500
+ byModel: Array.from(byModelMap.values()).toSorted((a, b) => {
501
+ const costDiff = b.totals.totalCost - a.totals.totalCost;
502
+ if (costDiff !== 0) {
503
+ return costDiff;
504
+ }
505
+ return b.totals.totalTokens - a.totals.totalTokens;
506
+ }),
507
+ byProvider: Array.from(byProviderMap.values()).toSorted((a, b) => {
508
+ const costDiff = b.totals.totalCost - a.totals.totalCost;
509
+ if (costDiff !== 0) {
510
+ return costDiff;
511
+ }
512
+ return b.totals.totalTokens - a.totals.totalTokens;
513
+ }),
514
+ byAgent: Array.from(byAgentMap.entries())
515
+ .map(([id, totals]) => ({ agentId: id, totals }))
516
+ .toSorted((a, b) => b.totals.totalCost - a.totals.totalCost),
517
+ byChannel: Array.from(byChannelMap.entries())
518
+ .map(([name, totals]) => ({ channel: name, totals }))
519
+ .toSorted((a, b) => b.totals.totalCost - a.totals.totalCost),
520
+ latency: latencyTotals.count > 0
521
+ ? {
522
+ count: latencyTotals.count,
523
+ avgMs: latencyTotals.sum / latencyTotals.count,
524
+ minMs: latencyTotals.min === Number.POSITIVE_INFINITY ? 0 : latencyTotals.min,
525
+ maxMs: latencyTotals.max,
526
+ p95Ms: latencyTotals.p95Max,
527
+ }
528
+ : undefined,
529
+ dailyLatency: Array.from(dailyLatencyMap.values())
530
+ .map((entry) => ({
531
+ date: entry.date,
532
+ count: entry.count,
533
+ avgMs: entry.count ? entry.sum / entry.count : 0,
534
+ minMs: entry.min === Number.POSITIVE_INFINITY ? 0 : entry.min,
535
+ maxMs: entry.max,
536
+ p95Ms: entry.p95Max,
537
+ }))
538
+ .toSorted((a, b) => a.date.localeCompare(b.date)),
539
+ modelDaily: Array.from(modelDailyMap.values()).toSorted((a, b) => a.date.localeCompare(b.date) || b.cost - a.cost),
540
+ daily: Array.from(dailyAggregateMap.values()).toSorted((a, b) => a.date.localeCompare(b.date)),
541
+ };
542
+ const result = {
543
+ updatedAt: now,
544
+ startDate: formatDateStr(startMs),
545
+ endDate: formatDateStr(endMs),
546
+ sessions,
547
+ totals: aggregateTotals,
548
+ aggregates,
549
+ };
550
+ respond(true, result, undefined);
551
+ },
552
+ "sessions.usage.timeseries": async ({ respond, params }) => {
553
+ const key = typeof params?.key === "string" ? params.key.trim() : null;
554
+ if (!key) {
555
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "key is required for timeseries"));
556
+ return;
557
+ }
558
+ const config = loadConfig();
559
+ const { entry } = loadSessionEntry(key);
560
+ // For discovered sessions (not in store), try using key as sessionId directly
561
+ const parsed = parseAgentSessionKey(key);
562
+ const agentId = parsed?.agentId;
563
+ const rawSessionId = parsed?.rest ?? key;
564
+ const sessionId = entry?.sessionId ?? rawSessionId;
565
+ const sessionFile = entry?.sessionFile ?? resolveSessionFilePath(rawSessionId, entry, { agentId });
566
+ const timeseries = await loadSessionUsageTimeSeries({
567
+ sessionId,
568
+ sessionEntry: entry,
569
+ sessionFile,
570
+ config,
571
+ maxPoints: 200,
572
+ });
573
+ if (!timeseries) {
574
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `No transcript found for session: ${key}`));
575
+ return;
576
+ }
577
+ respond(true, timeseries, undefined);
578
+ },
579
+ "sessions.usage.logs": async ({ respond, params }) => {
580
+ const key = typeof params?.key === "string" ? params.key.trim() : null;
581
+ if (!key) {
582
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "key is required for logs"));
583
+ return;
584
+ }
585
+ const limit = typeof params?.limit === "number" && Number.isFinite(params.limit)
586
+ ? Math.min(params.limit, 1000)
587
+ : 200;
588
+ const config = loadConfig();
589
+ const { entry } = loadSessionEntry(key);
590
+ // For discovered sessions (not in store), try using key as sessionId directly
591
+ const parsed = parseAgentSessionKey(key);
592
+ const agentId = parsed?.agentId;
593
+ const rawSessionId = parsed?.rest ?? key;
594
+ const sessionId = entry?.sessionId ?? rawSessionId;
595
+ const sessionFile = entry?.sessionFile ?? resolveSessionFilePath(rawSessionId, entry, { agentId });
596
+ const { loadSessionLogs } = await import("../../infra/session-cost-usage.js");
597
+ const logs = await loadSessionLogs({
598
+ sessionId,
599
+ sessionEntry: entry,
600
+ sessionFile,
601
+ config,
602
+ limit,
603
+ });
604
+ respond(true, { logs: logs ?? [] }, undefined);
605
+ },
63
606
  };