sidekick-shared 0.28.3 → 0.29.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/CHANGELOG.md +86 -0
- package/README.md +188 -27
- package/dist/aggregation/EventAggregator.d.ts +19 -0
- package/dist/aggregation/EventAggregator.js +174 -35
- package/dist/aggregation/snapshot.d.ts +37 -8
- package/dist/aggregation/snapshot.js +31 -9
- package/dist/aggregation/types.d.ts +13 -0
- package/dist/authStatus.d.ts +54 -0
- package/dist/authStatus.js +91 -0
- package/dist/browser.d.ts +16 -6
- package/dist/browser.js +24 -2
- package/dist/codex.d.ts +23 -0
- package/dist/codex.js +44 -0
- package/dist/codexProfiles.d.ts +1 -0
- package/dist/codexProfiles.js +8 -11
- package/dist/context/sessionContext.js +4 -0
- package/dist/esm/aggregation/EventAggregator.js +174 -35
- package/dist/esm/aggregation/snapshot.js +31 -9
- package/dist/esm/authStatus.js +87 -0
- package/dist/esm/browser.js +5 -2
- package/dist/esm/codex.js +18 -0
- package/dist/esm/codexProfiles.js +8 -11
- package/dist/esm/context/sessionContext.js +4 -0
- package/dist/esm/formatters/toolSummary.js +4 -1
- package/dist/esm/formatting.js +10 -1
- package/dist/esm/index.js +11 -6
- package/dist/esm/modelCatalog.js +7 -5
- package/dist/esm/modelContext.js +4 -3
- package/dist/esm/modelInfo.js +425 -47
- package/dist/esm/node.js +9 -3
- package/dist/esm/observedContextWindows.js +2 -1
- package/dist/esm/ownKey.js +26 -0
- package/dist/esm/parsers/codexParser.js +351 -54
- package/dist/esm/parsers/codexRolloutOrigin.js +107 -0
- package/dist/esm/parsers/openCodeParser.js +85 -22
- package/dist/esm/parsers/subagentScanner.js +33 -12
- package/dist/esm/parsers/subagentTraceParser.js +97 -37
- package/dist/esm/pricing.js +18 -0
- package/dist/esm/pricingCatalog.js +17 -9
- package/dist/esm/promptHistory.js +66 -4
- package/dist/esm/providerFailure.js +124 -3
- package/dist/esm/providers/claudeCode.js +10 -2
- package/dist/esm/providers/codex.js +84 -62
- package/dist/esm/providers/codexDatabase.js +3 -19
- package/dist/esm/providers/codexRollouts.js +24 -1
- package/dist/esm/providers/openCode.js +9 -17
- package/dist/esm/report/htmlReportGenerator.js +42 -5
- package/dist/esm/report/transcriptParser.js +13 -11
- package/dist/esm/schemas/providerFailure.js +3 -1
- package/dist/esm/schemas/sessionEvent.js +35 -14
- package/dist/esm/schemas/transcript.js +4 -0
- package/dist/esm/sessionPromptHistory.js +2 -2
- package/dist/esm/statusline/payload.js +9 -0
- package/dist/esm/subagentSpawn.js +163 -0
- package/dist/esm/testing/credentialGuard.js +87 -5
- package/dist/esm/toolCallOrigins.js +310 -0
- package/dist/esm/transcript.js +46 -3
- package/dist/esm/turns/assistantTurn.js +3 -2
- package/dist/esm/usage/claudeUsageDedupe.js +68 -0
- package/dist/esm/usage/usageEvents.js +146 -17
- package/dist/esm/usage/usageReports.js +39 -6
- package/dist/esm/watchers/eventBridge.js +15 -0
- package/dist/esm/watchers/factory.js +5 -3
- package/dist/esm/watchers/jsonlTail.js +442 -58
- package/dist/esm/watchers/jsonlWatcher.js +51 -4
- package/dist/esm/watchers/providerReaderWatcher.js +13 -0
- package/dist/esm/zaiQuota.js +2 -1
- package/dist/formatters/toolSummary.js +4 -1
- package/dist/formatting.d.ts +26 -1
- package/dist/formatting.js +11 -1
- package/dist/index.d.ts +22 -11
- package/dist/index.js +44 -12
- package/dist/modelCatalog.js +7 -5
- package/dist/modelContext.js +4 -3
- package/dist/modelInfo.d.ts +185 -16
- package/dist/modelInfo.js +434 -49
- package/dist/node.d.ts +16 -5
- package/dist/node.js +33 -2
- package/dist/observedContextWindows.js +2 -1
- package/dist/ownKey.d.ts +16 -0
- package/dist/ownKey.js +30 -0
- package/dist/parsers/codexParser.d.ts +118 -0
- package/dist/parsers/codexParser.js +352 -54
- package/dist/parsers/codexRolloutOrigin.d.ts +49 -0
- package/dist/parsers/codexRolloutOrigin.js +111 -0
- package/dist/parsers/openCodeParser.d.ts +38 -0
- package/dist/parsers/openCodeParser.js +88 -22
- package/dist/parsers/subagentScanner.d.ts +19 -6
- package/dist/parsers/subagentScanner.js +33 -12
- package/dist/parsers/subagentTraceParser.d.ts +25 -5
- package/dist/parsers/subagentTraceParser.js +97 -37
- package/dist/pricing.d.ts +22 -0
- package/dist/pricing.js +50 -0
- package/dist/pricingCatalog.d.ts +10 -0
- package/dist/pricingCatalog.js +16 -8
- package/dist/promptHistory.d.ts +65 -4
- package/dist/promptHistory.js +68 -5
- package/dist/providerFailure.d.ts +44 -0
- package/dist/providerFailure.js +126 -4
- package/dist/providers/claudeCode.js +9 -1
- package/dist/providers/codex.d.ts +3 -22
- package/dist/providers/codex.js +85 -64
- package/dist/providers/codexDatabase.d.ts +3 -2
- package/dist/providers/codexDatabase.js +4 -22
- package/dist/providers/codexRollouts.d.ts +22 -0
- package/dist/providers/codexRollouts.js +24 -0
- package/dist/providers/openCode.js +8 -16
- package/dist/providers/types.d.ts +14 -0
- package/dist/report/htmlReportGenerator.js +42 -5
- package/dist/report/transcriptParser.d.ts +10 -1
- package/dist/report/transcriptParser.js +13 -11
- package/dist/schemas/index.d.ts +1 -0
- package/dist/schemas/providerFailure.d.ts +39 -0
- package/dist/schemas/providerFailure.js +2 -0
- package/dist/schemas/sessionEvent.d.ts +27 -10
- package/dist/schemas/sessionEvent.js +35 -14
- package/dist/schemas/transcript.d.ts +2 -0
- package/dist/schemas/transcript.js +4 -0
- package/dist/sessionPromptHistory.d.ts +7 -1
- package/dist/sessionPromptHistory.js +1 -1
- package/dist/statusline/payload.d.ts +10 -0
- package/dist/statusline/payload.js +14 -0
- package/dist/subagentSpawn.d.ts +80 -0
- package/dist/subagentSpawn.js +174 -0
- package/dist/testing/credentialGuard.d.ts +14 -0
- package/dist/testing/credentialGuard.js +87 -5
- package/dist/toolCallOrigins.d.ts +70 -0
- package/dist/toolCallOrigins.js +346 -0
- package/dist/transcript.d.ts +12 -0
- package/dist/transcript.js +46 -3
- package/dist/turns/assistantTurn.d.ts +1 -0
- package/dist/turns/assistantTurn.js +3 -2
- package/dist/types/opencode.d.ts +2 -0
- package/dist/usage/claudeUsageDedupe.d.ts +31 -0
- package/dist/usage/claudeUsageDedupe.js +68 -0
- package/dist/usage/usageEvents.d.ts +54 -6
- package/dist/usage/usageEvents.js +146 -17
- package/dist/usage/usageReports.d.ts +35 -4
- package/dist/usage/usageReports.js +39 -6
- package/dist/watchers/eventBridge.js +15 -0
- package/dist/watchers/factory.d.ts +6 -0
- package/dist/watchers/factory.js +5 -3
- package/dist/watchers/jsonlTail.d.ts +124 -4
- package/dist/watchers/jsonlTail.js +446 -59
- package/dist/watchers/jsonlWatcher.d.ts +18 -1
- package/dist/watchers/jsonlWatcher.js +50 -3
- package/dist/watchers/providerReaderWatcher.d.ts +7 -0
- package/dist/watchers/providerReaderWatcher.js +13 -0
- package/dist/watchers/types.d.ts +12 -0
- package/dist/zaiQuota.js +2 -1
- package/package.json +41 -1
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a Codex rollout came from, read from its `session_meta` record:
|
|
3
|
+
* a session a person started, a fork of another session, or a subagent.
|
|
4
|
+
*
|
|
5
|
+
* Codex writes `session_meta.source` as a plain string for sessions started
|
|
6
|
+
* from a surface (`cli`, `vscode`, `exec`, …) and as an object for subagents:
|
|
7
|
+
* `{ subagent: { thread_spawn: { parent_thread_id, … } } }` for a spawned
|
|
8
|
+
* agent, `{ subagent: { other: 'guardian' } }` for internal reviewers, or
|
|
9
|
+
* `{ subagent: 'review' }`. The same JSON appears as a string in the state
|
|
10
|
+
* database's `threads.source`. A forked rollout records `forked_from_id`, and
|
|
11
|
+
* embeds its parent's `session_meta` after its own, so only the first counts.
|
|
12
|
+
*
|
|
13
|
+
* Pure: no filesystem access. Browser-safe.
|
|
14
|
+
*
|
|
15
|
+
* @module parsers/codexRolloutOrigin
|
|
16
|
+
*/
|
|
17
|
+
function record(value) {
|
|
18
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
19
|
+
? value
|
|
20
|
+
: undefined;
|
|
21
|
+
}
|
|
22
|
+
function nonEmpty(value) {
|
|
23
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
24
|
+
}
|
|
25
|
+
/** A `source` value as an object: rollouts store it parsed, the database as JSON text. */
|
|
26
|
+
function sourceObject(source) {
|
|
27
|
+
if (typeof source !== 'string')
|
|
28
|
+
return record(source);
|
|
29
|
+
if (!source.startsWith('{'))
|
|
30
|
+
return undefined;
|
|
31
|
+
try {
|
|
32
|
+
return record(JSON.parse(source));
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Parent thread id of a spawned subagent's `source` (object, or JSON text as
|
|
40
|
+
* stored in `threads.source`): `subagent.thread_spawn.parent_thread_id`.
|
|
41
|
+
*/
|
|
42
|
+
export function codexSpawnParentFromSource(source) {
|
|
43
|
+
const spawn = record(record(sourceObject(source)?.subagent)?.thread_spawn);
|
|
44
|
+
const parent = spawn?.parent_thread_id;
|
|
45
|
+
return typeof parent === 'string' ? parent : undefined;
|
|
46
|
+
}
|
|
47
|
+
function subagentKindOf(subagent) {
|
|
48
|
+
if (typeof subagent === 'string')
|
|
49
|
+
return nonEmpty(subagent);
|
|
50
|
+
const variant = record(subagent);
|
|
51
|
+
if (!variant)
|
|
52
|
+
return undefined;
|
|
53
|
+
const other = nonEmpty(variant.other);
|
|
54
|
+
if (other)
|
|
55
|
+
return other;
|
|
56
|
+
return Object.keys(variant)[0];
|
|
57
|
+
}
|
|
58
|
+
/** The `session_meta` payload of a record, of a bare payload, or of the first in a list. */
|
|
59
|
+
function metaPayload(input) {
|
|
60
|
+
if (Array.isArray(input)) {
|
|
61
|
+
for (const item of input) {
|
|
62
|
+
const row = record(item);
|
|
63
|
+
if (row?.type === 'session_meta')
|
|
64
|
+
return record(row.payload);
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
const row = record(input);
|
|
69
|
+
if (!row)
|
|
70
|
+
return undefined;
|
|
71
|
+
if (row.type === 'session_meta')
|
|
72
|
+
return record(row.payload);
|
|
73
|
+
// A payload on its own, as `readSessionMeta()` returns it.
|
|
74
|
+
if (row.type === undefined && row.payload === undefined)
|
|
75
|
+
return row;
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Origin of a Codex rollout from its `session_meta`. Accepts the parsed
|
|
80
|
+
* `session_meta` record, its payload, or the rollout's records in order (the
|
|
81
|
+
* first `session_meta` wins: a fork embeds its parent's after its own).
|
|
82
|
+
* Anything else yields `{ isSubagent: false }`.
|
|
83
|
+
*/
|
|
84
|
+
export function codexRolloutOrigin(input) {
|
|
85
|
+
const payload = metaPayload(input);
|
|
86
|
+
if (!payload)
|
|
87
|
+
return { isSubagent: false };
|
|
88
|
+
const forkedFromId = nonEmpty(payload.forked_from_id);
|
|
89
|
+
const object = sourceObject(payload.source);
|
|
90
|
+
const isSubagent = object !== undefined && 'subagent' in object;
|
|
91
|
+
const source = typeof payload.source === 'string' && object === undefined
|
|
92
|
+
? nonEmpty(payload.source)
|
|
93
|
+
: isSubagent
|
|
94
|
+
? 'subagent'
|
|
95
|
+
: undefined;
|
|
96
|
+
const parentId = forkedFromId ??
|
|
97
|
+
nonEmpty(codexSpawnParentFromSource(object)) ??
|
|
98
|
+
(isSubagent ? nonEmpty(payload.parent_thread_id) : undefined);
|
|
99
|
+
const subagentKind = isSubagent ? subagentKindOf(object?.subagent) : undefined;
|
|
100
|
+
return {
|
|
101
|
+
...(forkedFromId ? { forkedFromId } : {}),
|
|
102
|
+
...(source ? { source } : {}),
|
|
103
|
+
isSubagent,
|
|
104
|
+
...(parentId ? { parentId } : {}),
|
|
105
|
+
...(subagentKind ? { subagentKind } : {}),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* @module parsers/openCodeParser
|
|
12
12
|
*/
|
|
13
13
|
import { normalizeProviderUsage } from '../usageNormalization';
|
|
14
|
+
import { ownValue } from '../ownKey';
|
|
14
15
|
/**
|
|
15
16
|
* OpenCode → Claude Code tool name mapping.
|
|
16
17
|
* OpenCode uses lowercase names; downstream code expects PascalCase.
|
|
@@ -46,7 +47,7 @@ export function normalizeToolName(name) {
|
|
|
46
47
|
if (!name)
|
|
47
48
|
return name;
|
|
48
49
|
// Check mapping (case-insensitive lookup via lowercase key)
|
|
49
|
-
const mapped = TOOL_NAME_MAP
|
|
50
|
+
const mapped = ownValue(TOOL_NAME_MAP, name.toLowerCase());
|
|
50
51
|
if (mapped)
|
|
51
52
|
return mapped;
|
|
52
53
|
// Already PascalCase (starts with uppercase)? Pass through.
|
|
@@ -115,6 +116,78 @@ function toISOString(time) {
|
|
|
115
116
|
return time;
|
|
116
117
|
return new Date(time).toISOString();
|
|
117
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Token counts from a message in either storage shape: database rows are
|
|
121
|
+
* mapped to flat `cacheRead` / `cacheWrite`, while file-storage messages keep
|
|
122
|
+
* OpenCode's nested `cache: { read, write }`.
|
|
123
|
+
* @internal
|
|
124
|
+
*/
|
|
125
|
+
export function openCodeTokenCounts(tokens) {
|
|
126
|
+
const raw = (tokens ?? {});
|
|
127
|
+
const cache = (raw.cache ?? {});
|
|
128
|
+
const num = (value) => (typeof value === 'number' ? value : undefined);
|
|
129
|
+
return {
|
|
130
|
+
input: num(raw.input),
|
|
131
|
+
output: num(raw.output),
|
|
132
|
+
reasoning: num(raw.reasoning),
|
|
133
|
+
cacheRead: num(raw.cacheRead) ?? num(cache.read),
|
|
134
|
+
cacheWrite: num(raw.cacheWrite) ?? num(cache.write),
|
|
135
|
+
total: num(raw.total),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* How a stored OpenCode message splits its tokens. Current OpenCode stores
|
|
140
|
+
* every bucket disjoint for every provider: `input` excludes cache reads and
|
|
141
|
+
* writes (since 2025-11) and `output` excludes `reasoning` (since 2026-04),
|
|
142
|
+
* with `total` equal to their sum. Older messages either counted cached input
|
|
143
|
+
* in `input` (OpenAI-compatible providers) or reasoning in `output`. The
|
|
144
|
+
* stored `total` tells the layouts apart; without it the current layout is
|
|
145
|
+
* assumed.
|
|
146
|
+
* @internal
|
|
147
|
+
*/
|
|
148
|
+
export function openCodeTokenLayout(tokens) {
|
|
149
|
+
const n = (value) => typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : 0;
|
|
150
|
+
const input = n(tokens.input);
|
|
151
|
+
const output = n(tokens.output);
|
|
152
|
+
const reasoning = n(tokens.reasoning);
|
|
153
|
+
const cache = n(tokens.cacheRead) + n(tokens.cacheWrite);
|
|
154
|
+
const total = n(tokens.total);
|
|
155
|
+
if (total > 0 && total !== input + cache + output + reasoning) {
|
|
156
|
+
if (reasoning > 0 && total === input + cache + output) {
|
|
157
|
+
return { inputIncludesCache: false, reasoningInOutput: true };
|
|
158
|
+
}
|
|
159
|
+
if (cache > 0 && total === input + output + reasoning) {
|
|
160
|
+
return { inputIncludesCache: true, reasoningInOutput: false };
|
|
161
|
+
}
|
|
162
|
+
if (cache > 0 && reasoning > 0 && total === input + output) {
|
|
163
|
+
return { inputIncludesCache: true, reasoningInOutput: true };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return { inputIncludesCache: false, reasoningInOutput: false };
|
|
167
|
+
}
|
|
168
|
+
/** Normalizes one OpenCode message's or step's stored tokens. @internal */
|
|
169
|
+
export function normalizeOpenCodeUsage(tokens, providerID, extra = {}) {
|
|
170
|
+
const layout = openCodeTokenLayout(tokens);
|
|
171
|
+
return normalizeProviderUsage({
|
|
172
|
+
// `openai` semantics subtract cache reads from input; only an old
|
|
173
|
+
// cache-inclusive message needs that.
|
|
174
|
+
semantics: layout.inputIncludesCache
|
|
175
|
+
? 'openai'
|
|
176
|
+
: providerID === 'anthropic'
|
|
177
|
+
? 'anthropic'
|
|
178
|
+
: 'sidekick',
|
|
179
|
+
provider: providerID ?? 'opencode',
|
|
180
|
+
...(extra.source ? { source: extra.source } : {}),
|
|
181
|
+
...(extra.model ? { model: extra.model } : {}),
|
|
182
|
+
inputTokens: tokens.input,
|
|
183
|
+
outputTokens: tokens.output,
|
|
184
|
+
cacheReadTokens: tokens.cacheRead,
|
|
185
|
+
cacheWriteTokens: tokens.cacheWrite,
|
|
186
|
+
reasoningTokens: tokens.reasoning,
|
|
187
|
+
reasoningIncludedInOutput: layout.reasoningInOutput,
|
|
188
|
+
...(extra.reportedCostUsd !== undefined ? { reportedCostUsd: extra.reportedCostUsd } : {}),
|
|
189
|
+
});
|
|
190
|
+
}
|
|
118
191
|
/**
|
|
119
192
|
* Converts an OpenCode message and its parts into SessionEvent array.
|
|
120
193
|
*
|
|
@@ -320,31 +393,20 @@ function convertAssistantMessage(message, parts) {
|
|
|
320
393
|
: stepFinishCost > 0
|
|
321
394
|
? stepFinishCost
|
|
322
395
|
: undefined;
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
output_tokens: message.tokens.output || 0,
|
|
326
|
-
cache_creation_input_tokens: message.tokens.cacheWrite || 0,
|
|
327
|
-
cache_read_input_tokens: message.tokens.cacheRead || 0,
|
|
328
|
-
reported_cost: reportedCost,
|
|
329
|
-
reasoning_tokens: message.tokens.reasoning || 0,
|
|
330
|
-
};
|
|
331
|
-
const normalizedUsage = normalizeProviderUsage({
|
|
332
|
-
semantics: message.providerID === 'openai'
|
|
333
|
-
? 'openai'
|
|
334
|
-
: message.providerID === 'anthropic'
|
|
335
|
-
? 'anthropic'
|
|
336
|
-
: 'sidekick',
|
|
337
|
-
provider: message.providerID ?? 'opencode',
|
|
396
|
+
const counts = openCodeTokenCounts(message.tokens);
|
|
397
|
+
const normalizedUsage = normalizeOpenCodeUsage(counts, message.providerID, {
|
|
338
398
|
source: 'opencode-message',
|
|
339
399
|
model: message.modelID,
|
|
340
|
-
inputTokens: message.tokens.input,
|
|
341
|
-
outputTokens: message.tokens.output,
|
|
342
|
-
cacheReadTokens: message.tokens.cacheRead,
|
|
343
|
-
cacheWriteTokens: message.tokens.cacheWrite,
|
|
344
|
-
reasoningTokens: message.tokens.reasoning,
|
|
345
|
-
reasoningIncludedInOutput: message.providerID === 'openai',
|
|
346
400
|
reportedCostUsd: reportedCost,
|
|
347
401
|
});
|
|
402
|
+
const usage = {
|
|
403
|
+
input_tokens: normalizedUsage.uncachedInputTokens,
|
|
404
|
+
output_tokens: counts.output || 0,
|
|
405
|
+
cache_creation_input_tokens: counts.cacheWrite || 0,
|
|
406
|
+
cache_read_input_tokens: counts.cacheRead || 0,
|
|
407
|
+
reported_cost: reportedCost,
|
|
408
|
+
reasoning_tokens: counts.reasoning || 0,
|
|
409
|
+
};
|
|
348
410
|
const timestamp = toISOString(message.time.completed || message.time.created);
|
|
349
411
|
const retryAttempts = parts
|
|
350
412
|
.filter((part) => part.type === 'retry')
|
|
@@ -483,6 +545,7 @@ export function parseDbMessageData(row) {
|
|
|
483
545
|
cacheRead: cache?.read || 0,
|
|
484
546
|
cacheWrite: cache?.write || 0,
|
|
485
547
|
reasoning: tokens?.reasoning || 0,
|
|
548
|
+
...(typeof tokens?.total === 'number' ? { total: tokens.total } : {}),
|
|
486
549
|
},
|
|
487
550
|
time: {
|
|
488
551
|
created: time?.created || row.time_created,
|
|
@@ -12,28 +12,49 @@ import * as path from 'path';
|
|
|
12
12
|
import { ClaudeUsageDeduper, dedupedRawClaudeUsage } from '../usage/claudeUsageDedupe';
|
|
13
13
|
import { addUsageToSubagent } from '../usage/subagentUsage';
|
|
14
14
|
import { cachedSubagentStats } from './subagentStatsCache';
|
|
15
|
+
import { isSubagentSpawnTool } from '../subagentSpawn';
|
|
15
16
|
/**
|
|
16
17
|
* Pattern for matching subagent JSONL files.
|
|
17
18
|
* Files are named like: agent-<hash>.jsonl
|
|
18
19
|
*/
|
|
19
20
|
const AGENT_FILE_PATTERN = /^agent-(.+)\.jsonl$/;
|
|
21
|
+
function metaString(value) {
|
|
22
|
+
return typeof value === 'string' && value ? value : undefined;
|
|
23
|
+
}
|
|
20
24
|
/**
|
|
21
25
|
* Reads `agent-<id>.meta.json` (written by Claude Code next to the transcript)
|
|
22
|
-
* for the spawn's agent type
|
|
23
|
-
* yield an empty object
|
|
26
|
+
* for the spawn's agent type, description, spawning tool_use id, parent agent,
|
|
27
|
+
* and completion flags. Missing or malformed files yield an empty object;
|
|
28
|
+
* wrongly typed fields are left out.
|
|
24
29
|
*/
|
|
25
30
|
export function readAgentMeta(agentFilePath) {
|
|
26
31
|
try {
|
|
27
32
|
const metaPath = agentFilePath.replace(/\.jsonl$/, '.meta.json');
|
|
28
33
|
const parsed = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
35
|
+
return {};
|
|
36
|
+
const record = parsed;
|
|
37
|
+
const meta = {};
|
|
38
|
+
const agentType = metaString(record.agentType);
|
|
39
|
+
if (agentType)
|
|
40
|
+
meta.agentType = agentType;
|
|
41
|
+
const description = metaString(record.description);
|
|
42
|
+
if (description)
|
|
43
|
+
meta.description = description;
|
|
44
|
+
const toolUseId = metaString(record.toolUseId);
|
|
45
|
+
if (toolUseId)
|
|
46
|
+
meta.toolUseId = toolUseId;
|
|
47
|
+
const parentAgentId = metaString(record.parentAgentId);
|
|
48
|
+
if (parentAgentId)
|
|
49
|
+
meta.parentAgentId = parentAgentId;
|
|
50
|
+
const requestShape = metaString(record.requestShape);
|
|
51
|
+
if (requestShape)
|
|
52
|
+
meta.requestShape = requestShape;
|
|
53
|
+
if (typeof record.finished === 'boolean')
|
|
54
|
+
meta.finished = record.finished;
|
|
55
|
+
if (typeof record.stoppedByUser === 'boolean')
|
|
56
|
+
meta.stoppedByUser = record.stoppedByUser;
|
|
57
|
+
return meta;
|
|
37
58
|
}
|
|
38
59
|
catch {
|
|
39
60
|
return {};
|
|
@@ -208,8 +229,8 @@ function parseAgentFile(filePath, agentId, log) {
|
|
|
208
229
|
}
|
|
209
230
|
}
|
|
210
231
|
toolCalls.push(toolCall);
|
|
211
|
-
// If this is
|
|
212
|
-
if (toolUse.name
|
|
232
|
+
// If this is an Agent/Task call (spawning another subagent), extract info
|
|
233
|
+
if (isSubagentSpawnTool(toolUse.name) && toolUse.input) {
|
|
213
234
|
if (toolUse.input.subagent_type && !agentType) {
|
|
214
235
|
agentType = String(toolUse.input.subagent_type);
|
|
215
236
|
}
|
|
@@ -5,10 +5,13 @@
|
|
|
5
5
|
* (not just stats) for drill-down trace visualization. Applies noise classification
|
|
6
6
|
* and tool summary formatting from the formatters module.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* 1.
|
|
10
|
-
* 2.
|
|
11
|
-
*
|
|
8
|
+
* Parent-child linking, exact links first:
|
|
9
|
+
* 1. Meta `parentAgentId` (Claude Code writes it for nested spawns)
|
|
10
|
+
* 2. The child's spawning tool_use id (meta `toolUseId`) found in another
|
|
11
|
+
* trace's `Agent` / `Task` calls
|
|
12
|
+
* Only traces without that spawn metadata (older Claude Code) fall back to:
|
|
13
|
+
* 3. Team summary: parse <teammate-message> XML blocks
|
|
14
|
+
* 4. Positional fallback: timestamp ordering + proximity
|
|
12
15
|
*
|
|
13
16
|
* @module parsers/subagentTraceParser
|
|
14
17
|
*/
|
|
@@ -19,6 +22,8 @@ import { isHardNoise, classifyMessage, getSoftNoiseReason } from '../formatters/
|
|
|
19
22
|
import { ClaudeUsageDeduper, dedupedRawClaudeUsage } from '../usage/claudeUsageDedupe';
|
|
20
23
|
import { addUsageToSubagent } from '../usage/subagentUsage';
|
|
21
24
|
import { readAgentMeta } from './subagentScanner';
|
|
25
|
+
import { isSubagentSpawnTool } from '../subagentSpawn';
|
|
26
|
+
import { ownValue } from '../ownKey';
|
|
22
27
|
// ── Constants ──
|
|
23
28
|
const AGENT_FILE_PATTERN = /^agent-(.+)\.jsonl$/;
|
|
24
29
|
// ── Public API ──
|
|
@@ -50,10 +55,8 @@ export function scanSubagentTraces(sessionDir, sessionId) {
|
|
|
50
55
|
if (trace)
|
|
51
56
|
traces.push(trace);
|
|
52
57
|
}
|
|
53
|
-
// Link parent-child relationships
|
|
54
|
-
linkTraces(traces);
|
|
55
|
-
// Return only top-level traces (those without parents)
|
|
56
|
-
return traces.filter((t) => !t.parentToolUseId);
|
|
58
|
+
// Link parent-child relationships; returns the traces with no parent agent.
|
|
59
|
+
return linkTraces(traces);
|
|
57
60
|
}
|
|
58
61
|
catch {
|
|
59
62
|
return [];
|
|
@@ -133,8 +136,8 @@ function parseAgentTrace(filePath, agentId) {
|
|
|
133
136
|
input: block.input || {},
|
|
134
137
|
timestamp,
|
|
135
138
|
});
|
|
136
|
-
// Extract agent info from Task
|
|
137
|
-
if (block.name
|
|
139
|
+
// Extract agent info from an Agent/Task spawn
|
|
140
|
+
if (isSubagentSpawnTool(block.name) && block.input) {
|
|
138
141
|
if (block.input.subagent_type && !agentType) {
|
|
139
142
|
agentType = String(block.input.subagent_type);
|
|
140
143
|
}
|
|
@@ -171,6 +174,14 @@ function parseAgentTrace(filePath, agentId) {
|
|
|
171
174
|
agentId,
|
|
172
175
|
agentType,
|
|
173
176
|
description,
|
|
177
|
+
filePath,
|
|
178
|
+
...(meta.toolUseId
|
|
179
|
+
? { spawnToolUseId: meta.toolUseId, parentToolUseId: meta.toolUseId }
|
|
180
|
+
: {}),
|
|
181
|
+
...(meta.parentAgentId ? { parentAgentId: meta.parentAgentId } : {}),
|
|
182
|
+
...(meta.requestShape ? { background: meta.requestShape === 'background' } : {}),
|
|
183
|
+
...(meta.finished !== undefined ? { finished: meta.finished } : {}),
|
|
184
|
+
...(meta.stoppedByUser !== undefined ? { stoppedByUser: meta.stoppedByUser } : {}),
|
|
174
185
|
events,
|
|
175
186
|
children: [],
|
|
176
187
|
stats: {
|
|
@@ -195,23 +206,61 @@ function parseAgentTrace(filePath, agentId) {
|
|
|
195
206
|
}
|
|
196
207
|
}
|
|
197
208
|
// ── Parent-Child Linking ──
|
|
209
|
+
/**
|
|
210
|
+
* Nests child traces under their parents and returns the traces with no
|
|
211
|
+
* parent agent. Exact metadata links come first; the heuristics only place
|
|
212
|
+
* traces Claude Code recorded no spawn metadata for.
|
|
213
|
+
*/
|
|
198
214
|
function linkTraces(traces) {
|
|
199
215
|
const traceById = new Map();
|
|
200
216
|
for (const trace of traces) {
|
|
201
217
|
traceById.set(trace.agentId, trace);
|
|
202
218
|
}
|
|
203
|
-
|
|
204
|
-
|
|
219
|
+
const parentOf = new Map();
|
|
220
|
+
const attach = (parent, child) => {
|
|
221
|
+
if (parent === child || parentOf.has(child))
|
|
222
|
+
return false;
|
|
223
|
+
// Never nest an agent under its own descendant (malformed metadata).
|
|
224
|
+
for (let up = parent; up; up = parentOf.get(up)) {
|
|
225
|
+
if (up === child)
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
parentOf.set(child, parent);
|
|
229
|
+
parent.children.push(child);
|
|
230
|
+
return true;
|
|
231
|
+
};
|
|
232
|
+
// Phase 1: the meta file names the parent agent
|
|
233
|
+
for (const child of traces) {
|
|
234
|
+
const parent = child.parentAgentId ? traceById.get(child.parentAgentId) : undefined;
|
|
235
|
+
if (parent)
|
|
236
|
+
attach(parent, child);
|
|
237
|
+
}
|
|
238
|
+
// Phase 2: the child's spawning call is in another trace
|
|
239
|
+
const spawnOwner = new Map();
|
|
205
240
|
for (const trace of traces) {
|
|
206
|
-
for (const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
// The tool_use_id from this call should appear as a result in the parent
|
|
210
|
-
// but we can link via timestamp proximity to child traces
|
|
211
|
-
}
|
|
241
|
+
for (const id of spawnToolUseIdsOf(trace)) {
|
|
242
|
+
if (!spawnOwner.has(id))
|
|
243
|
+
spawnOwner.set(id, trace);
|
|
212
244
|
}
|
|
213
245
|
}
|
|
214
|
-
|
|
246
|
+
for (const child of traces) {
|
|
247
|
+
if (parentOf.has(child) || !child.spawnToolUseId)
|
|
248
|
+
continue;
|
|
249
|
+
const parent = spawnOwner.get(child.spawnToolUseId);
|
|
250
|
+
if (parent && attach(parent, child))
|
|
251
|
+
child.parentAgentId ??= parent.agentId;
|
|
252
|
+
}
|
|
253
|
+
// Heuristic links set parentAgentId and, as before, parentToolUseId to the
|
|
254
|
+
// parent's agent id.
|
|
255
|
+
const unresolved = (trace) => !parentOf.has(trace) && !trace.spawnToolUseId && !trace.parentAgentId;
|
|
256
|
+
const linkHeuristically = (parent, child) => {
|
|
257
|
+
if (!attach(parent, child))
|
|
258
|
+
return false;
|
|
259
|
+
child.parentAgentId = parent.agentId;
|
|
260
|
+
child.parentToolUseId = parent.agentId;
|
|
261
|
+
return true;
|
|
262
|
+
};
|
|
263
|
+
// Phase 3: Team summary linking
|
|
215
264
|
for (const trace of traces) {
|
|
216
265
|
for (const traceEvent of trace.events) {
|
|
217
266
|
const content = traceEvent.event.message?.content;
|
|
@@ -219,45 +268,56 @@ function linkTraces(traces) {
|
|
|
219
268
|
// Parse teammate agent IDs from XML blocks
|
|
220
269
|
const matches = content.matchAll(/agent[_-]id['":\s]+([a-f0-9]+)/gi);
|
|
221
270
|
for (const match of matches) {
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
child.parentToolUseId = trace.agentId;
|
|
226
|
-
trace.children.push(child);
|
|
227
|
-
}
|
|
271
|
+
const child = traceById.get(match[1]);
|
|
272
|
+
if (child && unresolved(child))
|
|
273
|
+
linkHeuristically(trace, child);
|
|
228
274
|
}
|
|
229
275
|
}
|
|
230
276
|
}
|
|
231
277
|
}
|
|
232
|
-
// Phase
|
|
278
|
+
// Phase 4: Positional fallback — timestamp proximity
|
|
233
279
|
// Traces without parents that fall within another trace's time window
|
|
234
|
-
const
|
|
235
|
-
for (const child of parentless) {
|
|
280
|
+
for (const child of traces.filter(unresolved)) {
|
|
236
281
|
if (!child.stats.startTime)
|
|
237
282
|
continue;
|
|
238
283
|
const childStart = child.stats.startTime.getTime();
|
|
239
|
-
// Find a trace that
|
|
284
|
+
// Find a top-level trace that spawned an agent right before this child started
|
|
240
285
|
for (const potential of traces) {
|
|
241
|
-
if (potential === child)
|
|
286
|
+
if (potential === child || parentOf.has(potential))
|
|
242
287
|
continue;
|
|
243
|
-
if (potential.parentToolUseId)
|
|
244
|
-
continue; // Already linked
|
|
245
288
|
for (const traceEvent of potential.events) {
|
|
246
289
|
const evt = traceEvent.event;
|
|
247
|
-
if (evt.type === 'assistant' && evt.tool?.name
|
|
290
|
+
if (evt.type === 'assistant' && isSubagentSpawnTool(evt.tool?.name)) {
|
|
248
291
|
const taskTime = new Date(evt.timestamp).getTime();
|
|
249
292
|
// Within 2 seconds before child start
|
|
250
293
|
if (taskTime <= childStart && childStart - taskTime < 2000) {
|
|
251
|
-
child
|
|
252
|
-
potential.children.push(child);
|
|
294
|
+
linkHeuristically(potential, child);
|
|
253
295
|
break;
|
|
254
296
|
}
|
|
255
297
|
}
|
|
256
298
|
}
|
|
257
|
-
if (child
|
|
299
|
+
if (parentOf.has(child))
|
|
258
300
|
break;
|
|
259
301
|
}
|
|
260
302
|
}
|
|
303
|
+
return traces.filter((trace) => !parentOf.has(trace));
|
|
304
|
+
}
|
|
305
|
+
/** Ids of the `Agent` / `Task` calls a trace made. */
|
|
306
|
+
function spawnToolUseIdsOf(trace) {
|
|
307
|
+
const ids = [];
|
|
308
|
+
for (const traceEvent of trace.events) {
|
|
309
|
+
const content = traceEvent.event.message?.content;
|
|
310
|
+
if (!Array.isArray(content))
|
|
311
|
+
continue;
|
|
312
|
+
for (const block of content) {
|
|
313
|
+
if (block?.type === 'tool_use' &&
|
|
314
|
+
typeof block.id === 'string' &&
|
|
315
|
+
isSubagentSpawnTool(block.name)) {
|
|
316
|
+
ids.push(block.id);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return ids;
|
|
261
321
|
}
|
|
262
322
|
// ── Helpers ──
|
|
263
323
|
function rawToSessionEvent(raw) {
|
|
@@ -272,7 +332,7 @@ function rawToSessionEvent(raw) {
|
|
|
272
332
|
tool_result: 'tool_result',
|
|
273
333
|
summary: 'summary',
|
|
274
334
|
};
|
|
275
|
-
const mappedType = typeMap
|
|
335
|
+
const mappedType = ownValue(typeMap, type);
|
|
276
336
|
if (!mappedType)
|
|
277
337
|
return null;
|
|
278
338
|
const message = raw.message;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sidekick-shared/pricing` — model pricing, model-id identity, usage
|
|
3
|
+
* normalization and cost, cost provenance, cost/token formatting, and text
|
|
4
|
+
* token estimates, as one small pure entry (no filesystem, network, or Zod).
|
|
5
|
+
*
|
|
6
|
+
* For hosts that price usage or format costs without session reading: it
|
|
7
|
+
* loads a handful of modules instead of the full browser surface. LiteLLM
|
|
8
|
+
* catalog hydration stays Node-only in `sidekick-shared/node`
|
|
9
|
+
* (`hydratePricingCatalog`); once hydrated, lookups here see the catalog.
|
|
10
|
+
* Everything here is also exported from the package root and
|
|
11
|
+
* `sidekick-shared/browser`.
|
|
12
|
+
*
|
|
13
|
+
* @module pricing
|
|
14
|
+
*/
|
|
15
|
+
export { MODEL_PROVIDER_IDS, canonicalModelId, isMovingModelAlias, parseModelId, compareModelIds, sortModelIds, shortModelName, getModelDisplayInfo, getModelInfo, getModelPricing, resolveModelPricing, registerModelPricing, clearRegisteredModelPricing, getPricingCatalogInfo, calculateCost, calculateCostWithPricing, calculateCostWithProvenance, mergeCostSources, costSourceFromPricingProvenance, mergeCostComponents, formatCost, } from './modelInfo';
|
|
16
|
+
export { normalizeProviderUsage, extractNormalizedUsage, calculateNormalizedUsageCost, } from './usageNormalization';
|
|
17
|
+
export { formatTokenCount, formatDurationMs } from './formatting';
|
|
18
|
+
export { ASCII_CHARS_PER_TOKEN, estimateTextTokens, estimateSerializedTokens, } from './tokenEstimation';
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
import { promises as fs } from 'node:fs';
|
|
20
20
|
import * as path from 'node:path';
|
|
21
21
|
import { _setCatalogContextWindows } from './modelContext';
|
|
22
|
-
import { _setPricingOverrides } from './modelInfo';
|
|
22
|
+
import { _setPricingOverrides, getPricingCatalogInfo } from './modelInfo';
|
|
23
|
+
import { setOwnValue } from './ownKey';
|
|
23
24
|
import { getConfigDir } from './paths';
|
|
24
25
|
import { atomicWriteJson } from './writers/atomic';
|
|
25
26
|
// ── Constants ──
|
|
@@ -36,6 +37,10 @@ const DEFAULT_TIMEOUT_MS = 3_000;
|
|
|
36
37
|
* 2. Otherwise attempt a network fetch; on success, persist + apply.
|
|
37
38
|
* 3. On any failure, load a stale cache if present; else leave overrides empty.
|
|
38
39
|
*
|
|
40
|
+
* `getPricingCatalogInfo().source` afterwards reads `litellm` for a fresh fetch
|
|
41
|
+
* or a cache within `ttlMs`, and `stale-cache` for a cache older than `ttlMs`
|
|
42
|
+
* — whether the refresh failed or `offline` skipped it.
|
|
43
|
+
*
|
|
39
44
|
* Always resolves — never throws. Offline-safe.
|
|
40
45
|
*/
|
|
41
46
|
export async function hydratePricingCatalog(options = {}) {
|
|
@@ -44,8 +49,9 @@ export async function hydratePricingCatalog(options = {}) {
|
|
|
44
49
|
const now = Date.now();
|
|
45
50
|
// 1. Fresh cache? (Offline callers take any cache, however old.)
|
|
46
51
|
const cached = await readCache(cachePath);
|
|
47
|
-
|
|
48
|
-
|
|
52
|
+
const cacheFresh = cached !== null && now - Date.parse(cached.fetchedAt) < ttlMs;
|
|
53
|
+
if (cached && (offline || cacheFresh)) {
|
|
54
|
+
return applyCacheFile(cached, 'cache', cacheFresh ? 'litellm' : 'stale-cache');
|
|
49
55
|
}
|
|
50
56
|
// 2. Try network.
|
|
51
57
|
if (fetchImpl && !offline) {
|
|
@@ -58,12 +64,12 @@ export async function hydratePricingCatalog(options = {}) {
|
|
|
58
64
|
contextWindows: normalizeLiteLlmContextWindows(fetched),
|
|
59
65
|
};
|
|
60
66
|
await writeCache(cachePath, payload, logger);
|
|
61
|
-
return applyCacheFile(payload, 'network');
|
|
67
|
+
return applyCacheFile(payload, 'network', 'litellm');
|
|
62
68
|
}
|
|
63
69
|
}
|
|
64
70
|
// 3. Fall back to stale cache if present.
|
|
65
71
|
if (cached) {
|
|
66
|
-
return applyCacheFile(cached, 'cache');
|
|
72
|
+
return applyCacheFile(cached, 'cache', 'stale-cache');
|
|
67
73
|
}
|
|
68
74
|
// 4. Nothing we can do — stay on static tables.
|
|
69
75
|
return {
|
|
@@ -71,6 +77,7 @@ export async function hydratePricingCatalog(options = {}) {
|
|
|
71
77
|
entries: 0,
|
|
72
78
|
contextWindowEntries: 0,
|
|
73
79
|
fetchedAt: new Date(now).toISOString(),
|
|
80
|
+
digest: getPricingCatalogInfo().digest,
|
|
74
81
|
};
|
|
75
82
|
}
|
|
76
83
|
/**
|
|
@@ -78,9 +85,9 @@ export async function hydratePricingCatalog(options = {}) {
|
|
|
78
85
|
* `contextWindows` is absent on caches written before context hydration; those
|
|
79
86
|
* contribute no overrides rather than clearing whatever is already loaded.
|
|
80
87
|
*/
|
|
81
|
-
function applyCacheFile(cache, source) {
|
|
88
|
+
function applyCacheFile(cache, source, basis) {
|
|
82
89
|
const contextWindows = cache.contextWindows ?? {};
|
|
83
|
-
_setPricingOverrides(cache.overrides);
|
|
90
|
+
_setPricingOverrides(cache.overrides, { source: basis, fetchedAt: cache.fetchedAt });
|
|
84
91
|
if (Object.keys(contextWindows).length > 0) {
|
|
85
92
|
_setCatalogContextWindows(contextWindows);
|
|
86
93
|
}
|
|
@@ -89,6 +96,7 @@ function applyCacheFile(cache, source) {
|
|
|
89
96
|
entries: Object.keys(cache.overrides).length,
|
|
90
97
|
contextWindowEntries: Object.keys(contextWindows).length,
|
|
91
98
|
fetchedAt: cache.fetchedAt,
|
|
99
|
+
digest: getPricingCatalogInfo().digest,
|
|
92
100
|
};
|
|
93
101
|
}
|
|
94
102
|
/**
|
|
@@ -110,7 +118,7 @@ export function normalizeLiteLlmCatalog(raw) {
|
|
|
110
118
|
continue;
|
|
111
119
|
// Catalog keys use both `provider/model` and bare `model` forms. Record both
|
|
112
120
|
// the raw key and the bare-model form so lookups work regardless of caller.
|
|
113
|
-
out
|
|
121
|
+
setOwnValue(out, key, pricing);
|
|
114
122
|
const bare = stripProviderPrefix(key);
|
|
115
123
|
if (bare && bare !== key && !out[bare]) {
|
|
116
124
|
out[bare] = pricing;
|
|
@@ -147,7 +155,7 @@ export function normalizeLiteLlmContextWindows(raw) {
|
|
|
147
155
|
const window = entry.max_input_tokens;
|
|
148
156
|
if (typeof window !== 'number' || !Number.isFinite(window) || window <= 0)
|
|
149
157
|
continue;
|
|
150
|
-
out
|
|
158
|
+
setOwnValue(out, key, window);
|
|
151
159
|
const bare = stripProviderPrefix(key);
|
|
152
160
|
if (bare && bare !== key) {
|
|
153
161
|
const seen = aliasWindows.get(bare) ?? new Set();
|