@poolzin/pool-bot 2026.2.0 → 2026.2.1
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.
- package/dist/agents/bash-tools.exec.js +76 -25
- package/dist/agents/cli-runner/helpers.js +9 -11
- package/dist/agents/identity.js +47 -7
- package/dist/agents/memory-search.js +25 -8
- package/dist/agents/model-selection.js +21 -0
- package/dist/agents/pi-embedded-block-chunker.js +117 -42
- package/dist/agents/pi-embedded-helpers/errors.js +183 -78
- package/dist/agents/pi-embedded-helpers.js +1 -1
- package/dist/agents/pi-embedded-runner/compact.js +1 -0
- package/dist/agents/pi-embedded-runner/model.js +61 -2
- package/dist/agents/pi-embedded-runner/run/attempt.js +21 -11
- package/dist/agents/pi-embedded-runner/run.js +199 -46
- package/dist/agents/pi-embedded-runner/system-prompt.js +10 -2
- package/dist/agents/pi-embedded-subscribe.js +118 -29
- package/dist/agents/pi-tools.js +10 -5
- package/dist/agents/poolbot-tools.js +15 -10
- package/dist/agents/sandbox-paths.js +31 -0
- package/dist/agents/session-tool-result-guard.js +94 -15
- package/dist/agents/shell-utils.js +51 -0
- package/dist/agents/skills/bundled-context.js +23 -0
- package/dist/agents/skills/bundled-dir.js +41 -7
- package/dist/agents/skills-install.js +60 -23
- package/dist/agents/subagent-announce.js +79 -34
- package/dist/agents/tool-policy.conformance.js +14 -0
- package/dist/agents/tool-policy.js +24 -0
- package/dist/agents/tools/cron-tool.js +166 -19
- package/dist/agents/tools/discord-actions-presence.js +78 -0
- package/dist/agents/tools/message-tool.js +56 -2
- package/dist/agents/tools/sessions-history-tool.js +69 -1
- package/dist/agents/tools/web-search.js +211 -42
- package/dist/agents/usage.js +23 -1
- package/dist/agents/workspace-run.js +67 -0
- package/dist/agents/workspace-templates.js +44 -0
- package/dist/auto-reply/command-auth.js +121 -6
- package/dist/auto-reply/envelope.js +50 -72
- package/dist/auto-reply/reply/commands-compact.js +1 -0
- package/dist/auto-reply/reply/commands-context-report.js +1 -0
- package/dist/auto-reply/reply/commands-context.js +1 -0
- package/dist/auto-reply/reply/commands-models.js +107 -60
- package/dist/auto-reply/reply/commands-ptt.js +171 -0
- package/dist/auto-reply/reply/get-reply-run.js +2 -1
- package/dist/auto-reply/reply/inbound-context.js +5 -1
- package/dist/auto-reply/reply/model-selection.js +3 -3
- package/dist/auto-reply/thinking.js +88 -43
- package/dist/browser/bridge-server.js +13 -0
- package/dist/browser/cdp.helpers.js +38 -24
- package/dist/browser/client-fetch.js +50 -7
- package/dist/browser/config.js +1 -10
- package/dist/browser/extension-relay.js +101 -40
- package/dist/browser/pw-ai.js +1 -1
- package/dist/browser/pw-session.js +143 -8
- package/dist/browser/pw-tools-core.interactions.js +125 -27
- package/dist/browser/pw-tools-core.responses.js +1 -1
- package/dist/browser/pw-tools-core.state.js +1 -1
- package/dist/browser/routes/agent.act.js +86 -41
- package/dist/browser/routes/dispatcher.js +4 -4
- package/dist/browser/screenshot.js +1 -1
- package/dist/browser/server.js +13 -0
- package/dist/build-info.json +3 -3
- package/dist/channels/reply-prefix.js +8 -1
- package/dist/cli/cron-cli/register.cron-add.js +61 -40
- package/dist/cli/cron-cli/register.cron-edit.js +60 -34
- package/dist/cli/cron-cli/shared.js +56 -41
- package/dist/cli/dns-cli.js +26 -14
- package/dist/cli/gateway-cli/register.js +37 -19
- package/dist/cli/memory-cli.js +5 -5
- package/dist/cli/parse-bytes.js +37 -0
- package/dist/cli/update-cli.js +173 -52
- package/dist/commands/agent.js +1 -0
- package/dist/commands/doctor-config-flow.js +61 -5
- package/dist/commands/doctor-state-migrations.js +1 -1
- package/dist/commands/health.js +1 -1
- package/dist/commands/model-allowlist.js +29 -0
- package/dist/commands/model-picker.js +2 -1
- package/dist/commands/models/list.status-command.js +43 -23
- package/dist/commands/models/shared.js +15 -0
- package/dist/commands/onboard-custom.js +384 -0
- package/dist/commands/onboard-non-interactive/local/auth-choice-inference.js +35 -0
- package/dist/commands/onboard-non-interactive/local/auth-choice.js +6 -3
- package/dist/commands/onboard-skills.js +63 -38
- package/dist/commands/openai-model-default.js +41 -0
- package/dist/config/defaults.js +3 -2
- package/dist/config/paths.js +136 -35
- package/dist/config/plugin-auto-enable.js +21 -5
- package/dist/config/redact-snapshot.js +153 -0
- package/dist/config/schema.field-metadata.js +590 -0
- package/dist/config/schema.js +2 -2
- package/dist/config/sessions/store.js +291 -23
- package/dist/config/zod-schema.agent-defaults.js +3 -0
- package/dist/config/zod-schema.agent-runtime.js +13 -2
- package/dist/config/zod-schema.providers-core.js +142 -0
- package/dist/config/zod-schema.session.js +3 -0
- package/dist/cron/delivery.js +57 -0
- package/dist/cron/isolated-agent/delivery-target.js +18 -3
- package/dist/cron/isolated-agent/helpers.js +22 -5
- package/dist/cron/isolated-agent/run.js +171 -63
- package/dist/cron/isolated-agent/session.js +2 -0
- package/dist/cron/normalize.js +356 -28
- package/dist/cron/parse.js +10 -5
- package/dist/cron/run-log.js +35 -10
- package/dist/cron/schedule.js +41 -6
- package/dist/cron/service/jobs.js +208 -35
- package/dist/cron/service/ops.js +72 -16
- package/dist/cron/service/state.js +2 -0
- package/dist/cron/service/store.js +386 -14
- package/dist/cron/service/timer.js +390 -147
- package/dist/cron/session-reaper.js +86 -0
- package/dist/cron/store.js +23 -8
- package/dist/cron/validate-timestamp.js +43 -0
- package/dist/discord/monitor/agent-components.js +438 -0
- package/dist/discord/monitor/allow-list.js +28 -5
- package/dist/discord/monitor/gateway-registry.js +29 -0
- package/dist/discord/monitor/native-command.js +44 -23
- package/dist/discord/monitor/sender-identity.js +45 -0
- package/dist/discord/pluralkit.js +27 -0
- package/dist/discord/send.outbound.js +92 -5
- package/dist/discord/send.shared.js +60 -23
- package/dist/discord/targets.js +84 -1
- package/dist/entry.js +15 -9
- package/dist/extensionAPI.js +8 -0
- package/dist/gateway/control-ui.js +8 -1
- package/dist/gateway/hooks-mapping.js +3 -0
- package/dist/gateway/hooks.js +65 -0
- package/dist/gateway/net.js +96 -31
- package/dist/gateway/node-command-policy.js +50 -15
- package/dist/gateway/origin-check.js +56 -0
- package/dist/gateway/protocol/client-info.js +9 -0
- package/dist/gateway/protocol/index.js +9 -2
- package/dist/gateway/protocol/schema/agents-models-skills.js +71 -1
- package/dist/gateway/protocol/schema/cron.js +22 -10
- package/dist/gateway/protocol/schema/protocol-schemas.js +16 -2
- package/dist/gateway/protocol/schema/sessions.js +12 -0
- package/dist/gateway/server/hooks.js +1 -1
- package/dist/gateway/server-broadcast.js +26 -9
- package/dist/gateway/server-chat.js +112 -23
- package/dist/gateway/server-discovery-runtime.js +10 -2
- package/dist/gateway/server-http.js +109 -11
- package/dist/gateway/server-methods/agent-timestamp.js +60 -0
- package/dist/gateway/server-methods/agents.js +321 -2
- package/dist/gateway/server-methods/usage.js +559 -16
- package/dist/gateway/server-runtime-state.js +22 -8
- package/dist/gateway/server-startup-memory.js +16 -0
- package/dist/gateway/server.impl.js +5 -1
- package/dist/gateway/session-utils.fs.js +23 -25
- package/dist/gateway/session-utils.js +20 -10
- package/dist/gateway/sessions-patch.js +7 -22
- package/dist/gateway/test-helpers.server.js +35 -2
- package/dist/imessage/constants.js +2 -0
- package/dist/imessage/monitor/deliver.js +4 -1
- package/dist/imessage/monitor/monitor-provider.js +51 -1
- package/dist/infra/bonjour-discovery.js +131 -70
- package/dist/infra/control-ui-assets.js +134 -12
- package/dist/infra/errors.js +12 -0
- package/dist/infra/exec-approvals.js +266 -57
- package/dist/infra/format-time/format-datetime.js +79 -0
- package/dist/infra/format-time/format-duration.js +81 -0
- package/dist/infra/format-time/format-relative.js +80 -0
- package/dist/infra/heartbeat-runner.js +140 -49
- package/dist/infra/home-dir.js +54 -0
- package/dist/infra/net/fetch-guard.js +122 -0
- package/dist/infra/net/ssrf.js +65 -29
- package/dist/infra/outbound/abort.js +14 -0
- package/dist/infra/outbound/message-action-runner.js +77 -13
- package/dist/infra/outbound/outbound-session.js +143 -37
- package/dist/infra/poolbot-root.js +43 -1
- package/dist/infra/session-cost-usage.js +631 -41
- package/dist/infra/state-migrations.js +317 -47
- package/dist/infra/update-global.js +35 -0
- package/dist/infra/update-runner.js +149 -43
- package/dist/infra/warning-filter.js +65 -0
- package/dist/infra/widearea-dns.js +30 -9
- package/dist/logging/redact-identifier.js +12 -0
- package/dist/media/fetch.js +81 -58
- package/dist/media-understanding/apply.js +403 -3
- package/dist/media-understanding/attachments.js +38 -27
- package/dist/media-understanding/defaults.js +16 -0
- package/dist/media-understanding/providers/deepgram/audio.js +22 -14
- package/dist/media-understanding/providers/google/audio.js +24 -17
- package/dist/media-understanding/providers/google/video.js +24 -17
- package/dist/media-understanding/providers/image.js +2 -2
- package/dist/media-understanding/providers/index.js +4 -1
- package/dist/media-understanding/providers/openai/audio.js +22 -14
- package/dist/media-understanding/providers/shared.js +16 -11
- package/dist/media-understanding/providers/zai/index.js +6 -0
- package/dist/media-understanding/runner.js +158 -90
- package/dist/memory/batch-voyage.js +277 -0
- package/dist/memory/embeddings-voyage.js +75 -0
- package/dist/memory/embeddings.js +28 -16
- package/dist/memory/internal.js +101 -18
- package/dist/memory/manager.js +154 -48
- package/dist/memory/search-manager.js +173 -0
- package/dist/memory/session-files.js +9 -3
- package/dist/node-host/runner.js +34 -24
- package/dist/node-host/with-timeout.js +27 -0
- package/dist/plugins/commands.js +5 -1
- package/dist/plugins/config-state.js +86 -7
- package/dist/plugins/source-display.js +51 -0
- package/dist/process/exec.js +20 -2
- package/dist/routing/resolve-route.js +12 -0
- package/dist/routing/session-key.js +15 -0
- package/dist/runtime.js +2 -0
- package/dist/security/audit-extra.async.js +601 -0
- package/dist/security/audit-extra.js +2 -830
- package/dist/security/audit-extra.sync.js +505 -0
- package/dist/security/channel-metadata.js +34 -0
- package/dist/security/external-content.js +88 -6
- package/dist/security/skill-scanner.js +330 -0
- package/dist/sessions/session-key-utils.js +7 -0
- package/dist/signal/monitor/event-handler.js +80 -1
- package/dist/slack/monitor/media.js +85 -15
- package/dist/tailscale/detect.js +1 -2
- package/dist/telegram/bot/helpers.js +109 -28
- package/dist/telegram/bot-handlers.js +144 -3
- package/dist/telegram/bot-message-context.js +37 -10
- package/dist/telegram/bot-message-dispatch.js +48 -15
- package/dist/telegram/bot-native-commands.js +86 -29
- package/dist/telegram/bot.js +30 -29
- package/dist/telegram/model-buttons.js +163 -0
- package/dist/telegram/monitor.js +110 -85
- package/dist/telegram/send.js +129 -47
- package/dist/terminal/restore.js +45 -0
- package/dist/test-helpers/state-dir-env.js +16 -0
- package/dist/tts/tts.js +12 -6
- package/dist/tui/tui-session-actions.js +166 -54
- package/dist/utils/fetch-timeout.js +20 -0
- package/dist/utils/normalize-secret-input.js +19 -0
- package/dist/utils/transcript-tools.js +58 -0
- package/dist/utils.js +45 -14
- package/dist/version.js +42 -5
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import readline from "node:readline";
|
|
4
4
|
import { normalizeUsage } from "../agents/usage.js";
|
|
5
5
|
import { resolveSessionFilePath, resolveSessionTranscriptsDirForAgent, } from "../config/sessions/paths.js";
|
|
6
|
+
import { countToolResults, extractToolCallNames } from "../utils/transcript-tools.js";
|
|
6
7
|
import { estimateUsageCost, resolveModelCostConfig } from "../utils/usage-format.js";
|
|
7
8
|
const emptyTotals = () => ({
|
|
8
9
|
input: 0,
|
|
@@ -11,65 +12,111 @@ const emptyTotals = () => ({
|
|
|
11
12
|
cacheWrite: 0,
|
|
12
13
|
totalTokens: 0,
|
|
13
14
|
totalCost: 0,
|
|
15
|
+
inputCost: 0,
|
|
16
|
+
outputCost: 0,
|
|
17
|
+
cacheReadCost: 0,
|
|
18
|
+
cacheWriteCost: 0,
|
|
14
19
|
missingCostEntries: 0,
|
|
15
20
|
});
|
|
16
21
|
const toFiniteNumber = (value) => {
|
|
17
|
-
if (typeof value !== "number")
|
|
22
|
+
if (typeof value !== "number") {
|
|
18
23
|
return undefined;
|
|
19
|
-
|
|
24
|
+
}
|
|
25
|
+
if (!Number.isFinite(value)) {
|
|
20
26
|
return undefined;
|
|
27
|
+
}
|
|
21
28
|
return value;
|
|
22
29
|
};
|
|
23
|
-
const
|
|
24
|
-
if (!usageRaw || typeof usageRaw !== "object")
|
|
30
|
+
const extractCostBreakdown = (usageRaw) => {
|
|
31
|
+
if (!usageRaw || typeof usageRaw !== "object") {
|
|
25
32
|
return undefined;
|
|
33
|
+
}
|
|
26
34
|
const record = usageRaw;
|
|
27
35
|
const cost = record.cost;
|
|
28
|
-
|
|
29
|
-
if (total === undefined)
|
|
36
|
+
if (!cost) {
|
|
30
37
|
return undefined;
|
|
31
|
-
|
|
38
|
+
}
|
|
39
|
+
const total = toFiniteNumber(cost.total);
|
|
40
|
+
if (total === undefined || total < 0) {
|
|
32
41
|
return undefined;
|
|
33
|
-
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
total,
|
|
45
|
+
input: toFiniteNumber(cost.input),
|
|
46
|
+
output: toFiniteNumber(cost.output),
|
|
47
|
+
cacheRead: toFiniteNumber(cost.cacheRead),
|
|
48
|
+
cacheWrite: toFiniteNumber(cost.cacheWrite),
|
|
49
|
+
};
|
|
34
50
|
};
|
|
35
51
|
const parseTimestamp = (entry) => {
|
|
36
52
|
const raw = entry.timestamp;
|
|
37
53
|
if (typeof raw === "string") {
|
|
38
54
|
const parsed = new Date(raw);
|
|
39
|
-
if (!Number.isNaN(parsed.valueOf()))
|
|
55
|
+
if (!Number.isNaN(parsed.valueOf())) {
|
|
40
56
|
return parsed;
|
|
57
|
+
}
|
|
41
58
|
}
|
|
42
59
|
const message = entry.message;
|
|
43
60
|
const messageTimestamp = toFiniteNumber(message?.timestamp);
|
|
44
61
|
if (messageTimestamp !== undefined) {
|
|
45
62
|
const parsed = new Date(messageTimestamp);
|
|
46
|
-
if (!Number.isNaN(parsed.valueOf()))
|
|
63
|
+
if (!Number.isNaN(parsed.valueOf())) {
|
|
47
64
|
return parsed;
|
|
65
|
+
}
|
|
48
66
|
}
|
|
49
67
|
return undefined;
|
|
50
68
|
};
|
|
51
|
-
const
|
|
69
|
+
const parseTranscriptEntry = (entry) => {
|
|
52
70
|
const message = entry.message;
|
|
53
|
-
|
|
54
|
-
if (role !== "assistant")
|
|
71
|
+
if (!message || typeof message !== "object") {
|
|
55
72
|
return null;
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
73
|
+
}
|
|
74
|
+
const roleRaw = message.role;
|
|
75
|
+
const role = roleRaw === "user" || roleRaw === "assistant" ? roleRaw : undefined;
|
|
76
|
+
if (!role) {
|
|
59
77
|
return null;
|
|
60
|
-
|
|
78
|
+
}
|
|
79
|
+
const usageRaw = message.usage ?? entry.usage;
|
|
80
|
+
const usage = usageRaw ? (normalizeUsage(usageRaw) ?? undefined) : undefined;
|
|
81
|
+
const provider = (typeof message.provider === "string" ? message.provider : undefined) ??
|
|
61
82
|
(typeof entry.provider === "string" ? entry.provider : undefined);
|
|
62
|
-
const model = (typeof message
|
|
83
|
+
const model = (typeof message.model === "string" ? message.model : undefined) ??
|
|
63
84
|
(typeof entry.model === "string" ? entry.model : undefined);
|
|
85
|
+
const costBreakdown = extractCostBreakdown(usageRaw);
|
|
86
|
+
const stopReason = typeof message.stopReason === "string" ? message.stopReason : undefined;
|
|
87
|
+
const durationMs = toFiniteNumber(message.durationMs ?? entry.durationMs);
|
|
64
88
|
return {
|
|
89
|
+
message,
|
|
90
|
+
role,
|
|
91
|
+
timestamp: parseTimestamp(entry),
|
|
92
|
+
durationMs,
|
|
65
93
|
usage,
|
|
66
|
-
costTotal:
|
|
94
|
+
costTotal: costBreakdown?.total,
|
|
95
|
+
costBreakdown,
|
|
67
96
|
provider,
|
|
68
97
|
model,
|
|
69
|
-
|
|
98
|
+
stopReason,
|
|
99
|
+
toolNames: extractToolCallNames(message),
|
|
100
|
+
toolResultCounts: countToolResults(message),
|
|
70
101
|
};
|
|
71
102
|
};
|
|
72
103
|
const formatDayKey = (date) => date.toLocaleDateString("en-CA", { timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone });
|
|
104
|
+
const computeLatencyStats = (values) => {
|
|
105
|
+
if (!values.length) {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
109
|
+
const total = sorted.reduce((sum, v) => sum + v, 0);
|
|
110
|
+
const count = sorted.length;
|
|
111
|
+
const p95Index = Math.max(0, Math.ceil(count * 0.95) - 1);
|
|
112
|
+
return {
|
|
113
|
+
count,
|
|
114
|
+
avgMs: total / count,
|
|
115
|
+
p95Ms: sorted[p95Index] ?? sorted[count - 1],
|
|
116
|
+
minMs: sorted[0],
|
|
117
|
+
maxMs: sorted[count - 1],
|
|
118
|
+
};
|
|
119
|
+
};
|
|
73
120
|
const applyUsageTotals = (totals, usage) => {
|
|
74
121
|
totals.input += usage.input ?? 0;
|
|
75
122
|
totals.output += usage.output ?? 0;
|
|
@@ -79,6 +126,17 @@ const applyUsageTotals = (totals, usage) => {
|
|
|
79
126
|
(usage.input ?? 0) + (usage.output ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
|
|
80
127
|
totals.totalTokens += totalTokens;
|
|
81
128
|
};
|
|
129
|
+
const applyCostBreakdown = (totals, costBreakdown) => {
|
|
130
|
+
if (costBreakdown === undefined || costBreakdown.total === undefined) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
totals.totalCost += costBreakdown.total;
|
|
134
|
+
totals.inputCost += costBreakdown.input ?? 0;
|
|
135
|
+
totals.outputCost += costBreakdown.output ?? 0;
|
|
136
|
+
totals.cacheReadCost += costBreakdown.cacheRead ?? 0;
|
|
137
|
+
totals.cacheWriteCost += costBreakdown.cacheWrite ?? 0;
|
|
138
|
+
};
|
|
139
|
+
// Legacy function for backwards compatibility (no cost breakdown available)
|
|
82
140
|
const applyCostTotal = (totals, costTotal) => {
|
|
83
141
|
if (costTotal === undefined) {
|
|
84
142
|
totals.missingCostEntries += 1;
|
|
@@ -86,19 +144,21 @@ const applyCostTotal = (totals, costTotal) => {
|
|
|
86
144
|
}
|
|
87
145
|
totals.totalCost += costTotal;
|
|
88
146
|
};
|
|
89
|
-
async function
|
|
147
|
+
async function scanTranscriptFile(params) {
|
|
90
148
|
const fileStream = fs.createReadStream(params.filePath, { encoding: "utf-8" });
|
|
91
149
|
const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
|
|
92
150
|
for await (const line of rl) {
|
|
93
151
|
const trimmed = line.trim();
|
|
94
|
-
if (!trimmed)
|
|
152
|
+
if (!trimmed) {
|
|
95
153
|
continue;
|
|
154
|
+
}
|
|
96
155
|
try {
|
|
97
156
|
const parsed = JSON.parse(trimmed);
|
|
98
|
-
const entry =
|
|
99
|
-
if (!entry)
|
|
157
|
+
const entry = parseTranscriptEntry(parsed);
|
|
158
|
+
if (!entry) {
|
|
100
159
|
continue;
|
|
101
|
-
|
|
160
|
+
}
|
|
161
|
+
if (entry.usage && entry.costTotal === undefined) {
|
|
102
162
|
const cost = resolveModelCostConfig({
|
|
103
163
|
provider: entry.provider,
|
|
104
164
|
model: entry.model,
|
|
@@ -113,12 +173,41 @@ async function scanUsageFile(params) {
|
|
|
113
173
|
}
|
|
114
174
|
}
|
|
115
175
|
}
|
|
176
|
+
async function scanUsageFile(params) {
|
|
177
|
+
await scanTranscriptFile({
|
|
178
|
+
filePath: params.filePath,
|
|
179
|
+
config: params.config,
|
|
180
|
+
onEntry: (entry) => {
|
|
181
|
+
if (!entry.usage) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
params.onEntry({
|
|
185
|
+
usage: entry.usage,
|
|
186
|
+
costTotal: entry.costTotal,
|
|
187
|
+
costBreakdown: entry.costBreakdown,
|
|
188
|
+
provider: entry.provider,
|
|
189
|
+
model: entry.model,
|
|
190
|
+
timestamp: entry.timestamp,
|
|
191
|
+
});
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
}
|
|
116
195
|
export async function loadCostUsageSummary(params) {
|
|
117
|
-
const days = Math.max(1, Math.floor(params?.days ?? 30));
|
|
118
196
|
const now = new Date();
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
197
|
+
let sinceTime;
|
|
198
|
+
let untilTime;
|
|
199
|
+
if (params?.startMs !== undefined && params?.endMs !== undefined) {
|
|
200
|
+
sinceTime = params.startMs;
|
|
201
|
+
untilTime = params.endMs;
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
// Fallback to days-based calculation for backwards compatibility
|
|
205
|
+
const days = Math.max(1, Math.floor(params?.days ?? 30));
|
|
206
|
+
const since = new Date(now);
|
|
207
|
+
since.setDate(since.getDate() - (days - 1));
|
|
208
|
+
sinceTime = since.getTime();
|
|
209
|
+
untilTime = now.getTime();
|
|
210
|
+
}
|
|
122
211
|
const dailyMap = new Map();
|
|
123
212
|
const totals = emptyTotals();
|
|
124
213
|
const sessionsDir = resolveSessionTranscriptsDirForAgent(params?.agentId);
|
|
@@ -128,10 +217,13 @@ export async function loadCostUsageSummary(params) {
|
|
|
128
217
|
.map(async (entry) => {
|
|
129
218
|
const filePath = path.join(sessionsDir, entry.name);
|
|
130
219
|
const stats = await fs.promises.stat(filePath).catch(() => null);
|
|
131
|
-
if (!stats)
|
|
220
|
+
if (!stats) {
|
|
132
221
|
return null;
|
|
133
|
-
|
|
222
|
+
}
|
|
223
|
+
// Include file if it was modified after our start time
|
|
224
|
+
if (stats.mtimeMs < sinceTime) {
|
|
134
225
|
return null;
|
|
226
|
+
}
|
|
135
227
|
return filePath;
|
|
136
228
|
}))).filter((filePath) => Boolean(filePath));
|
|
137
229
|
for (const filePath of files) {
|
|
@@ -140,21 +232,34 @@ export async function loadCostUsageSummary(params) {
|
|
|
140
232
|
config: params?.config,
|
|
141
233
|
onEntry: (entry) => {
|
|
142
234
|
const ts = entry.timestamp?.getTime();
|
|
143
|
-
if (!ts || ts < sinceTime)
|
|
235
|
+
if (!ts || ts < sinceTime || ts > untilTime) {
|
|
144
236
|
return;
|
|
237
|
+
}
|
|
145
238
|
const dayKey = formatDayKey(entry.timestamp ?? now);
|
|
146
239
|
const bucket = dailyMap.get(dayKey) ?? emptyTotals();
|
|
147
240
|
applyUsageTotals(bucket, entry.usage);
|
|
148
|
-
|
|
241
|
+
if (entry.costBreakdown?.total !== undefined) {
|
|
242
|
+
applyCostBreakdown(bucket, entry.costBreakdown);
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
applyCostTotal(bucket, entry.costTotal);
|
|
246
|
+
}
|
|
149
247
|
dailyMap.set(dayKey, bucket);
|
|
150
248
|
applyUsageTotals(totals, entry.usage);
|
|
151
|
-
|
|
249
|
+
if (entry.costBreakdown?.total !== undefined) {
|
|
250
|
+
applyCostBreakdown(totals, entry.costBreakdown);
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
applyCostTotal(totals, entry.costTotal);
|
|
254
|
+
}
|
|
152
255
|
},
|
|
153
256
|
});
|
|
154
257
|
}
|
|
155
258
|
const daily = Array.from(dailyMap.entries())
|
|
156
|
-
.map(([date, bucket]) => ({ date,
|
|
259
|
+
.map(([date, bucket]) => Object.assign({ date }, bucket))
|
|
157
260
|
.sort((a, b) => a.date.localeCompare(b.date));
|
|
261
|
+
// Calculate days for backwards compatibility in response
|
|
262
|
+
const days = Math.ceil((untilTime - sinceTime) / (24 * 60 * 60 * 1000)) + 1;
|
|
158
263
|
return {
|
|
159
264
|
updatedAt: Date.now(),
|
|
160
265
|
days,
|
|
@@ -162,29 +267,514 @@ export async function loadCostUsageSummary(params) {
|
|
|
162
267
|
totals,
|
|
163
268
|
};
|
|
164
269
|
}
|
|
270
|
+
/**
|
|
271
|
+
* Scan all transcript files to discover sessions not in the session store.
|
|
272
|
+
* Returns basic metadata for each discovered session.
|
|
273
|
+
*/
|
|
274
|
+
export async function discoverAllSessions(params) {
|
|
275
|
+
const sessionsDir = resolveSessionTranscriptsDirForAgent(params?.agentId);
|
|
276
|
+
const entries = await fs.promises.readdir(sessionsDir, { withFileTypes: true }).catch(() => []);
|
|
277
|
+
const discovered = [];
|
|
278
|
+
for (const entry of entries) {
|
|
279
|
+
if (!entry.isFile() || !entry.name.endsWith(".jsonl")) {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
const filePath = path.join(sessionsDir, entry.name);
|
|
283
|
+
const stats = await fs.promises.stat(filePath).catch(() => null);
|
|
284
|
+
if (!stats) {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
// Filter by date range if provided
|
|
288
|
+
if (params?.startMs && stats.mtimeMs < params.startMs) {
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
// Do not exclude by endMs: a session can have activity in range even if it continued later.
|
|
292
|
+
// Extract session ID from filename (remove .jsonl)
|
|
293
|
+
const sessionId = entry.name.slice(0, -6);
|
|
294
|
+
// Try to read first user message for label extraction
|
|
295
|
+
let firstUserMessage;
|
|
296
|
+
try {
|
|
297
|
+
const fileStream = fs.createReadStream(filePath, { encoding: "utf-8" });
|
|
298
|
+
const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
|
|
299
|
+
for await (const line of rl) {
|
|
300
|
+
const trimmed = line.trim();
|
|
301
|
+
if (!trimmed) {
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
const parsed = JSON.parse(trimmed);
|
|
306
|
+
const message = parsed.message;
|
|
307
|
+
if (message?.role === "user") {
|
|
308
|
+
const content = message.content;
|
|
309
|
+
if (typeof content === "string") {
|
|
310
|
+
firstUserMessage = content.slice(0, 100);
|
|
311
|
+
}
|
|
312
|
+
else if (Array.isArray(content)) {
|
|
313
|
+
for (const block of content) {
|
|
314
|
+
if (typeof block === "object" &&
|
|
315
|
+
block &&
|
|
316
|
+
block.type === "text") {
|
|
317
|
+
const text = block.text;
|
|
318
|
+
if (typeof text === "string") {
|
|
319
|
+
firstUserMessage = text.slice(0, 100);
|
|
320
|
+
}
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
break; // Found first user message
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
// Skip malformed lines
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
rl.close();
|
|
333
|
+
fileStream.destroy();
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
// Ignore read errors
|
|
337
|
+
}
|
|
338
|
+
discovered.push({
|
|
339
|
+
sessionId,
|
|
340
|
+
sessionFile: filePath,
|
|
341
|
+
mtime: stats.mtimeMs,
|
|
342
|
+
firstUserMessage,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
// Sort by mtime descending (most recent first)
|
|
346
|
+
return discovered.sort((a, b) => b.mtime - a.mtime);
|
|
347
|
+
}
|
|
165
348
|
export async function loadSessionCostSummary(params) {
|
|
166
349
|
const sessionFile = params.sessionFile ??
|
|
167
350
|
(params.sessionId ? resolveSessionFilePath(params.sessionId, params.sessionEntry) : undefined);
|
|
168
|
-
if (!sessionFile || !fs.existsSync(sessionFile))
|
|
351
|
+
if (!sessionFile || !fs.existsSync(sessionFile)) {
|
|
169
352
|
return null;
|
|
353
|
+
}
|
|
170
354
|
const totals = emptyTotals();
|
|
355
|
+
let firstActivity;
|
|
171
356
|
let lastActivity;
|
|
172
|
-
|
|
357
|
+
const activityDatesSet = new Set();
|
|
358
|
+
const dailyMap = new Map();
|
|
359
|
+
const dailyMessageMap = new Map();
|
|
360
|
+
const dailyLatencyMap = new Map();
|
|
361
|
+
const dailyModelUsageMap = new Map();
|
|
362
|
+
const messageCounts = {
|
|
363
|
+
total: 0,
|
|
364
|
+
user: 0,
|
|
365
|
+
assistant: 0,
|
|
366
|
+
toolCalls: 0,
|
|
367
|
+
toolResults: 0,
|
|
368
|
+
errors: 0,
|
|
369
|
+
};
|
|
370
|
+
const toolUsageMap = new Map();
|
|
371
|
+
const modelUsageMap = new Map();
|
|
372
|
+
const errorStopReasons = new Set(["error", "aborted", "timeout"]);
|
|
373
|
+
const latencyValues = [];
|
|
374
|
+
let lastUserTimestamp;
|
|
375
|
+
const MAX_LATENCY_MS = 12 * 60 * 60 * 1000;
|
|
376
|
+
await scanTranscriptFile({
|
|
173
377
|
filePath: sessionFile,
|
|
174
378
|
config: params.config,
|
|
175
379
|
onEntry: (entry) => {
|
|
176
|
-
applyUsageTotals(totals, entry.usage);
|
|
177
|
-
applyCostTotal(totals, entry.costTotal);
|
|
178
380
|
const ts = entry.timestamp?.getTime();
|
|
179
|
-
|
|
180
|
-
|
|
381
|
+
// Filter by date range if specified
|
|
382
|
+
if (params.startMs !== undefined && ts !== undefined && ts < params.startMs) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (params.endMs !== undefined && ts !== undefined && ts > params.endMs) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
if (ts !== undefined) {
|
|
389
|
+
if (!firstActivity || ts < firstActivity) {
|
|
390
|
+
firstActivity = ts;
|
|
391
|
+
}
|
|
392
|
+
if (!lastActivity || ts > lastActivity) {
|
|
393
|
+
lastActivity = ts;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (entry.role === "user") {
|
|
397
|
+
messageCounts.user += 1;
|
|
398
|
+
messageCounts.total += 1;
|
|
399
|
+
if (entry.timestamp) {
|
|
400
|
+
lastUserTimestamp = entry.timestamp.getTime();
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (entry.role === "assistant") {
|
|
404
|
+
messageCounts.assistant += 1;
|
|
405
|
+
messageCounts.total += 1;
|
|
406
|
+
const ts = entry.timestamp?.getTime();
|
|
407
|
+
if (ts !== undefined) {
|
|
408
|
+
const latencyMs = entry.durationMs ??
|
|
409
|
+
(lastUserTimestamp !== undefined ? Math.max(0, ts - lastUserTimestamp) : undefined);
|
|
410
|
+
if (latencyMs !== undefined &&
|
|
411
|
+
Number.isFinite(latencyMs) &&
|
|
412
|
+
latencyMs <= MAX_LATENCY_MS) {
|
|
413
|
+
latencyValues.push(latencyMs);
|
|
414
|
+
const dayKey = formatDayKey(entry.timestamp ?? new Date(ts));
|
|
415
|
+
const dailyLatencies = dailyLatencyMap.get(dayKey) ?? [];
|
|
416
|
+
dailyLatencies.push(latencyMs);
|
|
417
|
+
dailyLatencyMap.set(dayKey, dailyLatencies);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (entry.toolNames.length > 0) {
|
|
422
|
+
messageCounts.toolCalls += entry.toolNames.length;
|
|
423
|
+
for (const name of entry.toolNames) {
|
|
424
|
+
toolUsageMap.set(name, (toolUsageMap.get(name) ?? 0) + 1);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (entry.toolResultCounts.total > 0) {
|
|
428
|
+
messageCounts.toolResults += entry.toolResultCounts.total;
|
|
429
|
+
messageCounts.errors += entry.toolResultCounts.errors;
|
|
430
|
+
}
|
|
431
|
+
if (entry.stopReason && errorStopReasons.has(entry.stopReason)) {
|
|
432
|
+
messageCounts.errors += 1;
|
|
433
|
+
}
|
|
434
|
+
if (entry.timestamp) {
|
|
435
|
+
const dayKey = formatDayKey(entry.timestamp);
|
|
436
|
+
activityDatesSet.add(dayKey);
|
|
437
|
+
const daily = dailyMessageMap.get(dayKey) ?? {
|
|
438
|
+
date: dayKey,
|
|
439
|
+
total: 0,
|
|
440
|
+
user: 0,
|
|
441
|
+
assistant: 0,
|
|
442
|
+
toolCalls: 0,
|
|
443
|
+
toolResults: 0,
|
|
444
|
+
errors: 0,
|
|
445
|
+
};
|
|
446
|
+
daily.total += entry.role === "user" || entry.role === "assistant" ? 1 : 0;
|
|
447
|
+
if (entry.role === "user") {
|
|
448
|
+
daily.user += 1;
|
|
449
|
+
}
|
|
450
|
+
else if (entry.role === "assistant") {
|
|
451
|
+
daily.assistant += 1;
|
|
452
|
+
}
|
|
453
|
+
daily.toolCalls += entry.toolNames.length;
|
|
454
|
+
daily.toolResults += entry.toolResultCounts.total;
|
|
455
|
+
daily.errors += entry.toolResultCounts.errors;
|
|
456
|
+
if (entry.stopReason && errorStopReasons.has(entry.stopReason)) {
|
|
457
|
+
daily.errors += 1;
|
|
458
|
+
}
|
|
459
|
+
dailyMessageMap.set(dayKey, daily);
|
|
460
|
+
}
|
|
461
|
+
if (!entry.usage) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
applyUsageTotals(totals, entry.usage);
|
|
465
|
+
if (entry.costBreakdown?.total !== undefined) {
|
|
466
|
+
applyCostBreakdown(totals, entry.costBreakdown);
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
applyCostTotal(totals, entry.costTotal);
|
|
470
|
+
}
|
|
471
|
+
if (entry.timestamp) {
|
|
472
|
+
const dayKey = formatDayKey(entry.timestamp);
|
|
473
|
+
const entryTokens = (entry.usage.input ?? 0) +
|
|
474
|
+
(entry.usage.output ?? 0) +
|
|
475
|
+
(entry.usage.cacheRead ?? 0) +
|
|
476
|
+
(entry.usage.cacheWrite ?? 0);
|
|
477
|
+
const entryCost = entry.costBreakdown?.total ??
|
|
478
|
+
(entry.costBreakdown
|
|
479
|
+
? (entry.costBreakdown.input ?? 0) +
|
|
480
|
+
(entry.costBreakdown.output ?? 0) +
|
|
481
|
+
(entry.costBreakdown.cacheRead ?? 0) +
|
|
482
|
+
(entry.costBreakdown.cacheWrite ?? 0)
|
|
483
|
+
: (entry.costTotal ?? 0));
|
|
484
|
+
const existing = dailyMap.get(dayKey) ?? { tokens: 0, cost: 0 };
|
|
485
|
+
dailyMap.set(dayKey, {
|
|
486
|
+
tokens: existing.tokens + entryTokens,
|
|
487
|
+
cost: existing.cost + entryCost,
|
|
488
|
+
});
|
|
489
|
+
if (entry.provider || entry.model) {
|
|
490
|
+
const modelKey = `${dayKey}::${entry.provider ?? "unknown"}::${entry.model ?? "unknown"}`;
|
|
491
|
+
const dailyModel = dailyModelUsageMap.get(modelKey) ??
|
|
492
|
+
{
|
|
493
|
+
date: dayKey,
|
|
494
|
+
provider: entry.provider,
|
|
495
|
+
model: entry.model,
|
|
496
|
+
tokens: 0,
|
|
497
|
+
cost: 0,
|
|
498
|
+
count: 0,
|
|
499
|
+
};
|
|
500
|
+
dailyModel.tokens += entryTokens;
|
|
501
|
+
dailyModel.cost += entryCost;
|
|
502
|
+
dailyModel.count += 1;
|
|
503
|
+
dailyModelUsageMap.set(modelKey, dailyModel);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (entry.provider || entry.model) {
|
|
507
|
+
const key = `${entry.provider ?? "unknown"}::${entry.model ?? "unknown"}`;
|
|
508
|
+
const existing = modelUsageMap.get(key) ??
|
|
509
|
+
{
|
|
510
|
+
provider: entry.provider,
|
|
511
|
+
model: entry.model,
|
|
512
|
+
count: 0,
|
|
513
|
+
totals: emptyTotals(),
|
|
514
|
+
};
|
|
515
|
+
existing.count += 1;
|
|
516
|
+
applyUsageTotals(existing.totals, entry.usage);
|
|
517
|
+
if (entry.costBreakdown?.total !== undefined) {
|
|
518
|
+
applyCostBreakdown(existing.totals, entry.costBreakdown);
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
applyCostTotal(existing.totals, entry.costTotal);
|
|
522
|
+
}
|
|
523
|
+
modelUsageMap.set(key, existing);
|
|
181
524
|
}
|
|
182
525
|
},
|
|
183
526
|
});
|
|
527
|
+
// Convert daily map to sorted array
|
|
528
|
+
const dailyBreakdown = Array.from(dailyMap.entries())
|
|
529
|
+
.map(([date, data]) => ({ date, tokens: data.tokens, cost: data.cost }))
|
|
530
|
+
.sort((a, b) => a.date.localeCompare(b.date));
|
|
531
|
+
const dailyMessageCounts = Array.from(dailyMessageMap.values()).sort((a, b) => a.date.localeCompare(b.date));
|
|
532
|
+
const dailyLatency = Array.from(dailyLatencyMap.entries())
|
|
533
|
+
.map(([date, values]) => {
|
|
534
|
+
const stats = computeLatencyStats(values);
|
|
535
|
+
if (!stats) {
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
return { date, ...stats };
|
|
539
|
+
})
|
|
540
|
+
.filter((entry) => Boolean(entry))
|
|
541
|
+
.sort((a, b) => a.date.localeCompare(b.date));
|
|
542
|
+
const dailyModelUsage = Array.from(dailyModelUsageMap.values()).sort((a, b) => a.date.localeCompare(b.date) || b.cost - a.cost);
|
|
543
|
+
const toolUsage = toolUsageMap.size
|
|
544
|
+
? {
|
|
545
|
+
totalCalls: Array.from(toolUsageMap.values()).reduce((sum, count) => sum + count, 0),
|
|
546
|
+
uniqueTools: toolUsageMap.size,
|
|
547
|
+
tools: Array.from(toolUsageMap.entries())
|
|
548
|
+
.map(([name, count]) => ({ name, count }))
|
|
549
|
+
.sort((a, b) => b.count - a.count),
|
|
550
|
+
}
|
|
551
|
+
: undefined;
|
|
552
|
+
const modelUsage = modelUsageMap.size
|
|
553
|
+
? Array.from(modelUsageMap.values()).sort((a, b) => {
|
|
554
|
+
const costDiff = b.totals.totalCost - a.totals.totalCost;
|
|
555
|
+
if (costDiff !== 0) {
|
|
556
|
+
return costDiff;
|
|
557
|
+
}
|
|
558
|
+
return b.totals.totalTokens - a.totals.totalTokens;
|
|
559
|
+
})
|
|
560
|
+
: undefined;
|
|
184
561
|
return {
|
|
185
562
|
sessionId: params.sessionId,
|
|
186
563
|
sessionFile,
|
|
564
|
+
firstActivity,
|
|
187
565
|
lastActivity,
|
|
566
|
+
durationMs: firstActivity !== undefined && lastActivity !== undefined
|
|
567
|
+
? Math.max(0, lastActivity - firstActivity)
|
|
568
|
+
: undefined,
|
|
569
|
+
activityDates: Array.from(activityDatesSet).sort(),
|
|
570
|
+
dailyBreakdown,
|
|
571
|
+
dailyMessageCounts,
|
|
572
|
+
dailyLatency: dailyLatency.length ? dailyLatency : undefined,
|
|
573
|
+
dailyModelUsage: dailyModelUsage.length ? dailyModelUsage : undefined,
|
|
574
|
+
messageCounts,
|
|
575
|
+
toolUsage,
|
|
576
|
+
modelUsage,
|
|
577
|
+
latency: computeLatencyStats(latencyValues),
|
|
188
578
|
...totals,
|
|
189
579
|
};
|
|
190
580
|
}
|
|
581
|
+
export async function loadSessionUsageTimeSeries(params) {
|
|
582
|
+
const sessionFile = params.sessionFile ??
|
|
583
|
+
(params.sessionId ? resolveSessionFilePath(params.sessionId, params.sessionEntry) : undefined);
|
|
584
|
+
if (!sessionFile || !fs.existsSync(sessionFile)) {
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
const points = [];
|
|
588
|
+
let cumulativeTokens = 0;
|
|
589
|
+
let cumulativeCost = 0;
|
|
590
|
+
await scanUsageFile({
|
|
591
|
+
filePath: sessionFile,
|
|
592
|
+
config: params.config,
|
|
593
|
+
onEntry: (entry) => {
|
|
594
|
+
const ts = entry.timestamp?.getTime();
|
|
595
|
+
if (!ts) {
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
const input = entry.usage.input ?? 0;
|
|
599
|
+
const output = entry.usage.output ?? 0;
|
|
600
|
+
const cacheRead = entry.usage.cacheRead ?? 0;
|
|
601
|
+
const cacheWrite = entry.usage.cacheWrite ?? 0;
|
|
602
|
+
const totalTokens = entry.usage.total ?? input + output + cacheRead + cacheWrite;
|
|
603
|
+
const cost = entry.costTotal ?? 0;
|
|
604
|
+
cumulativeTokens += totalTokens;
|
|
605
|
+
cumulativeCost += cost;
|
|
606
|
+
points.push({
|
|
607
|
+
timestamp: ts,
|
|
608
|
+
input,
|
|
609
|
+
output,
|
|
610
|
+
cacheRead,
|
|
611
|
+
cacheWrite,
|
|
612
|
+
totalTokens,
|
|
613
|
+
cost,
|
|
614
|
+
cumulativeTokens,
|
|
615
|
+
cumulativeCost,
|
|
616
|
+
});
|
|
617
|
+
},
|
|
618
|
+
});
|
|
619
|
+
// Sort by timestamp
|
|
620
|
+
const sortedPoints = points.sort((a, b) => a.timestamp - b.timestamp);
|
|
621
|
+
// Optionally downsample if too many points
|
|
622
|
+
const maxPoints = params.maxPoints ?? 100;
|
|
623
|
+
if (sortedPoints.length > maxPoints) {
|
|
624
|
+
const step = Math.ceil(sortedPoints.length / maxPoints);
|
|
625
|
+
const downsampled = [];
|
|
626
|
+
for (let i = 0; i < sortedPoints.length; i += step) {
|
|
627
|
+
downsampled.push(sortedPoints[i]);
|
|
628
|
+
}
|
|
629
|
+
// Always include the last point
|
|
630
|
+
if (downsampled[downsampled.length - 1] !== sortedPoints[sortedPoints.length - 1]) {
|
|
631
|
+
downsampled.push(sortedPoints[sortedPoints.length - 1]);
|
|
632
|
+
}
|
|
633
|
+
return { sessionId: params.sessionId, points: downsampled };
|
|
634
|
+
}
|
|
635
|
+
return { sessionId: params.sessionId, points: sortedPoints };
|
|
636
|
+
}
|
|
637
|
+
export async function loadSessionLogs(params) {
|
|
638
|
+
const sessionFile = params.sessionFile ??
|
|
639
|
+
(params.sessionId ? resolveSessionFilePath(params.sessionId, params.sessionEntry) : undefined);
|
|
640
|
+
if (!sessionFile || !fs.existsSync(sessionFile)) {
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
const logs = [];
|
|
644
|
+
const limit = params.limit ?? 50;
|
|
645
|
+
const fileStream = fs.createReadStream(sessionFile, { encoding: "utf-8" });
|
|
646
|
+
const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
|
|
647
|
+
for await (const line of rl) {
|
|
648
|
+
const trimmed = line.trim();
|
|
649
|
+
if (!trimmed) {
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
try {
|
|
653
|
+
const parsed = JSON.parse(trimmed);
|
|
654
|
+
const message = parsed.message;
|
|
655
|
+
if (!message) {
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
const role = message.role;
|
|
659
|
+
if (role !== "user" && role !== "assistant" && role !== "tool" && role !== "toolResult") {
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
const contentParts = [];
|
|
663
|
+
const rawToolName = message.toolName ?? message.tool_name ?? message.name ?? message.tool;
|
|
664
|
+
const toolName = typeof rawToolName === "string" && rawToolName.trim() ? rawToolName.trim() : undefined;
|
|
665
|
+
if (role === "tool" || role === "toolResult") {
|
|
666
|
+
contentParts.push(`[Tool: ${toolName ?? "tool"}]`);
|
|
667
|
+
contentParts.push("[Tool Result]");
|
|
668
|
+
}
|
|
669
|
+
// Extract content
|
|
670
|
+
const rawContent = message.content;
|
|
671
|
+
if (typeof rawContent === "string") {
|
|
672
|
+
contentParts.push(rawContent);
|
|
673
|
+
}
|
|
674
|
+
else if (Array.isArray(rawContent)) {
|
|
675
|
+
// Handle content blocks (text, tool_use, etc.)
|
|
676
|
+
const contentText = rawContent
|
|
677
|
+
.map((block) => {
|
|
678
|
+
if (typeof block === "string") {
|
|
679
|
+
return block;
|
|
680
|
+
}
|
|
681
|
+
const b = block;
|
|
682
|
+
if (b.type === "text" && typeof b.text === "string") {
|
|
683
|
+
return b.text;
|
|
684
|
+
}
|
|
685
|
+
if (b.type === "tool_use") {
|
|
686
|
+
const name = typeof b.name === "string" ? b.name : "unknown";
|
|
687
|
+
return `[Tool: ${name}]`;
|
|
688
|
+
}
|
|
689
|
+
if (b.type === "tool_result") {
|
|
690
|
+
return `[Tool Result]`;
|
|
691
|
+
}
|
|
692
|
+
return "";
|
|
693
|
+
})
|
|
694
|
+
.filter(Boolean)
|
|
695
|
+
.join("\n");
|
|
696
|
+
if (contentText) {
|
|
697
|
+
contentParts.push(contentText);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
// OpenAI-style tool calls stored outside the content array.
|
|
701
|
+
const rawToolCalls = message.tool_calls ?? message.toolCalls ?? message.function_call ?? message.functionCall;
|
|
702
|
+
const toolCalls = Array.isArray(rawToolCalls)
|
|
703
|
+
? rawToolCalls
|
|
704
|
+
: rawToolCalls
|
|
705
|
+
? [rawToolCalls]
|
|
706
|
+
: [];
|
|
707
|
+
if (toolCalls.length > 0) {
|
|
708
|
+
for (const call of toolCalls) {
|
|
709
|
+
const callObj = call;
|
|
710
|
+
const directName = typeof callObj.name === "string" ? callObj.name : undefined;
|
|
711
|
+
const fn = callObj.function;
|
|
712
|
+
const fnName = typeof fn?.name === "string" ? fn.name : undefined;
|
|
713
|
+
const name = directName ?? fnName ?? "unknown";
|
|
714
|
+
contentParts.push(`[Tool: ${name}]`);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
let content = contentParts.join("\n").trim();
|
|
718
|
+
if (!content) {
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
// Truncate very long content
|
|
722
|
+
const maxLen = 2000;
|
|
723
|
+
if (content.length > maxLen) {
|
|
724
|
+
content = content.slice(0, maxLen) + "\u2026";
|
|
725
|
+
}
|
|
726
|
+
// Get timestamp
|
|
727
|
+
let timestamp = 0;
|
|
728
|
+
if (typeof parsed.timestamp === "string") {
|
|
729
|
+
timestamp = new Date(parsed.timestamp).getTime();
|
|
730
|
+
}
|
|
731
|
+
else if (typeof message.timestamp === "number") {
|
|
732
|
+
timestamp = message.timestamp;
|
|
733
|
+
}
|
|
734
|
+
// Get usage for assistant messages
|
|
735
|
+
let tokens;
|
|
736
|
+
let cost;
|
|
737
|
+
if (role === "assistant") {
|
|
738
|
+
const usageRaw = message.usage;
|
|
739
|
+
const usage = normalizeUsage(usageRaw);
|
|
740
|
+
if (usage) {
|
|
741
|
+
tokens =
|
|
742
|
+
usage.total ??
|
|
743
|
+
(usage.input ?? 0) +
|
|
744
|
+
(usage.output ?? 0) +
|
|
745
|
+
(usage.cacheRead ?? 0) +
|
|
746
|
+
(usage.cacheWrite ?? 0);
|
|
747
|
+
const breakdown = extractCostBreakdown(usageRaw);
|
|
748
|
+
if (breakdown?.total !== undefined) {
|
|
749
|
+
cost = breakdown.total;
|
|
750
|
+
}
|
|
751
|
+
else {
|
|
752
|
+
const costConfig = resolveModelCostConfig({
|
|
753
|
+
provider: message.provider,
|
|
754
|
+
model: message.model,
|
|
755
|
+
config: params.config,
|
|
756
|
+
});
|
|
757
|
+
cost = estimateUsageCost({ usage, cost: costConfig });
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
logs.push({
|
|
762
|
+
timestamp,
|
|
763
|
+
role,
|
|
764
|
+
content,
|
|
765
|
+
tokens,
|
|
766
|
+
cost,
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
catch {
|
|
770
|
+
// Ignore malformed lines
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
// Sort by timestamp and limit
|
|
774
|
+
const sortedLogs = logs.sort((a, b) => a.timestamp - b.timestamp);
|
|
775
|
+
// Return most recent logs
|
|
776
|
+
if (sortedLogs.length > limit) {
|
|
777
|
+
return sortedLogs.slice(-limit);
|
|
778
|
+
}
|
|
779
|
+
return sortedLogs;
|
|
780
|
+
}
|