sidekick-shared 0.29.0 → 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 +22 -0
- package/README.md +56 -25
- 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/browser.d.ts +5 -1
- package/dist/browser.js +9 -3
- 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/browser.js +1 -0
- package/dist/esm/context/sessionContext.js +4 -0
- package/dist/esm/formatters/toolSummary.js +4 -1
- package/dist/esm/index.js +3 -1
- package/dist/esm/modelCatalog.js +5 -4
- package/dist/esm/modelContext.js +4 -3
- package/dist/esm/modelInfo.js +4 -3
- package/dist/esm/node.js +1 -1
- package/dist/esm/observedContextWindows.js +2 -1
- package/dist/esm/ownKey.js +26 -0
- package/dist/esm/parsers/codexParser.js +180 -4
- package/dist/esm/parsers/openCodeParser.js +2 -1
- package/dist/esm/parsers/subagentScanner.js +33 -12
- package/dist/esm/parsers/subagentTraceParser.js +97 -37
- package/dist/esm/pricingCatalog.js +3 -2
- package/dist/esm/providers/claudeCode.js +10 -2
- package/dist/esm/providers/codex.js +82 -38
- package/dist/esm/schemas/sessionEvent.js +35 -14
- package/dist/esm/subagentSpawn.js +163 -0
- package/dist/esm/transcript.js +4 -0
- package/dist/esm/turns/assistantTurn.js +3 -2
- package/dist/esm/usage/claudeUsageDedupe.js +68 -0
- package/dist/esm/watchers/eventBridge.js +15 -0
- package/dist/esm/watchers/factory.js +5 -3
- package/dist/esm/watchers/jsonlTail.js +73 -8
- 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/index.d.ts +8 -3
- package/dist/index.js +18 -9
- package/dist/modelCatalog.js +5 -4
- package/dist/modelContext.js +4 -3
- package/dist/modelInfo.js +4 -3
- package/dist/node.d.ts +1 -1
- package/dist/node.js +3 -1
- 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 +63 -0
- package/dist/parsers/codexParser.js +181 -4
- package/dist/parsers/openCodeParser.js +2 -1
- 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/pricingCatalog.js +3 -2
- package/dist/providers/claudeCode.js +9 -1
- package/dist/providers/codex.js +82 -38
- package/dist/providers/types.d.ts +14 -0
- package/dist/schemas/index.d.ts +1 -0
- package/dist/schemas/sessionEvent.d.ts +27 -10
- package/dist/schemas/sessionEvent.js +35 -14
- package/dist/subagentSpawn.d.ts +80 -0
- package/dist/subagentSpawn.js +174 -0
- package/dist/transcript.js +4 -0
- package/dist/turns/assistantTurn.d.ts +1 -0
- package/dist/turns/assistantTurn.js +3 -2
- package/dist/usage/claudeUsageDedupe.d.ts +31 -0
- package/dist/usage/claudeUsageDedupe.js +68 -0
- 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 +27 -4
- package/dist/watchers/jsonlTail.js +75 -8
- 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 +1 -1
|
@@ -171,6 +171,61 @@ export function extractPatchFilePaths(input) {
|
|
|
171
171
|
}
|
|
172
172
|
/** Default cap on each piece of per-call correlation state the parser keeps. */
|
|
173
173
|
export const DEFAULT_MAX_TRACKED_TOOL_CALLS = 4096;
|
|
174
|
+
/** Entries per table {@link CodexRolloutParser.exportResumeState} keeps by default. */
|
|
175
|
+
const DEFAULT_RESUME_STATE_LIMIT = 256;
|
|
176
|
+
function isPlainObject(value) {
|
|
177
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
178
|
+
}
|
|
179
|
+
function cloneJson(value) {
|
|
180
|
+
return JSON.parse(JSON.stringify(value));
|
|
181
|
+
}
|
|
182
|
+
function isOptionalString(value) {
|
|
183
|
+
return value === undefined || typeof value === 'string';
|
|
184
|
+
}
|
|
185
|
+
function isStringArray(value) {
|
|
186
|
+
return Array.isArray(value) && value.every((item) => typeof item === 'string');
|
|
187
|
+
}
|
|
188
|
+
function isTokenCount(value) {
|
|
189
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
|
190
|
+
}
|
|
191
|
+
/** A valid `CodexTokenUsage` (copied), `null` for `null`, or `undefined` when invalid. Internal. */
|
|
192
|
+
export function codexTokenUsageFrom(value) {
|
|
193
|
+
if (value === null)
|
|
194
|
+
return null;
|
|
195
|
+
if (!isPlainObject(value))
|
|
196
|
+
return undefined;
|
|
197
|
+
if (!isTokenCount(value.input_tokens) || !isTokenCount(value.output_tokens))
|
|
198
|
+
return undefined;
|
|
199
|
+
const usage = {
|
|
200
|
+
input_tokens: value.input_tokens,
|
|
201
|
+
output_tokens: value.output_tokens,
|
|
202
|
+
};
|
|
203
|
+
for (const field of ['cached_input_tokens', 'reasoning_output_tokens', 'total_tokens']) {
|
|
204
|
+
const count = value[field];
|
|
205
|
+
if (count === undefined)
|
|
206
|
+
continue;
|
|
207
|
+
if (!isTokenCount(count))
|
|
208
|
+
return undefined;
|
|
209
|
+
usage[field] = count;
|
|
210
|
+
}
|
|
211
|
+
return usage;
|
|
212
|
+
}
|
|
213
|
+
/** Last `limit` items of an insertion-ordered collection, oldest first. */
|
|
214
|
+
function newest(items, size, limit) {
|
|
215
|
+
const count = limit >= 0 ? Math.min(size, Math.floor(limit)) : 0;
|
|
216
|
+
const kept = [];
|
|
217
|
+
let skip = size - count;
|
|
218
|
+
for (const item of items) {
|
|
219
|
+
if (kept.length >= count)
|
|
220
|
+
break;
|
|
221
|
+
if (skip > 0) {
|
|
222
|
+
skip -= 1;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
kept.push(item);
|
|
226
|
+
}
|
|
227
|
+
return kept;
|
|
228
|
+
}
|
|
174
229
|
/** Fixed per-entry cost added to each pending call's byte estimate. */
|
|
175
230
|
const PENDING_ENTRY_OVERHEAD_BYTES = 64;
|
|
176
231
|
/** Prefix of the bookkeeping keys the parser stashes inside tool inputs. */
|
|
@@ -282,6 +337,118 @@ export class CodexRolloutParser {
|
|
|
282
337
|
getLastRateLimits() {
|
|
283
338
|
return this.lastRateLimits;
|
|
284
339
|
}
|
|
340
|
+
/**
|
|
341
|
+
* The state later lines depend on — session metadata, current model,
|
|
342
|
+
* previous cumulative token total, context window, rate limits, plan mode,
|
|
343
|
+
* and the tool-call correlation tables (newest `limit` entries each) — as
|
|
344
|
+
* JSON-serializable data. Eviction counters are not included.
|
|
345
|
+
*/
|
|
346
|
+
exportResumeState(limit = DEFAULT_RESUME_STATE_LIMIT) {
|
|
347
|
+
return cloneJson({
|
|
348
|
+
version: 1,
|
|
349
|
+
sessionMeta: this.sessionMeta,
|
|
350
|
+
currentModel: this.currentModel,
|
|
351
|
+
prevTotalUsage: this.prevTotalUsage,
|
|
352
|
+
lastTokenUsage: this.lastTokenUsage,
|
|
353
|
+
modelContextWindow: this.modelContextWindow,
|
|
354
|
+
lastRateLimits: this.lastRateLimits,
|
|
355
|
+
hasAggregateRateLimits: this.hasAggregateRateLimits,
|
|
356
|
+
inPlanMode: this.inPlanMode,
|
|
357
|
+
emittedToolUseIds: newest(this.emittedToolUseIds, this.emittedToolUseIds.size, limit),
|
|
358
|
+
patchExpandedToolUseIds: newest(this.patchExpandedToolUseIds, this.patchExpandedToolUseIds.size, limit),
|
|
359
|
+
pendingExecCommands: newest(this.pendingExecCommands.values(), this.pendingExecCommands.size, limit),
|
|
360
|
+
pendingMcpToolCalls: newest(this.pendingMcpToolCalls.values(), this.pendingMcpToolCalls.size, limit),
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Replaces this parser's state with one from {@link exportResumeState}, as
|
|
365
|
+
* if it had parsed the rollout up to where that state was taken. Returns
|
|
366
|
+
* false, changing nothing, when `state` is not a valid version-1 state.
|
|
367
|
+
* This parser's own `maxTrackedToolCalls`, `maxPendingBytes`, and
|
|
368
|
+
* `metadataOnly` options apply to the imported tables.
|
|
369
|
+
*/
|
|
370
|
+
importResumeState(state) {
|
|
371
|
+
if (!isPlainObject(state) || state.version !== 1)
|
|
372
|
+
return false;
|
|
373
|
+
const s = state;
|
|
374
|
+
const prevTotalUsage = codexTokenUsageFrom(s.prevTotalUsage);
|
|
375
|
+
const lastTokenUsage = codexTokenUsageFrom(s.lastTokenUsage);
|
|
376
|
+
if (prevTotalUsage === undefined || lastTokenUsage === undefined)
|
|
377
|
+
return false;
|
|
378
|
+
if (s.sessionMeta !== null && !isPlainObject(s.sessionMeta))
|
|
379
|
+
return false;
|
|
380
|
+
if (s.lastRateLimits !== null && !isPlainObject(s.lastRateLimits))
|
|
381
|
+
return false;
|
|
382
|
+
if (s.currentModel !== null && typeof s.currentModel !== 'string')
|
|
383
|
+
return false;
|
|
384
|
+
const contextWindow = s.modelContextWindow;
|
|
385
|
+
if (contextWindow !== null &&
|
|
386
|
+
!(typeof contextWindow === 'number' && Number.isFinite(contextWindow) && contextWindow > 0)) {
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
if (typeof s.hasAggregateRateLimits !== 'boolean' || typeof s.inPlanMode !== 'boolean') {
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
if (!isStringArray(s.emittedToolUseIds))
|
|
393
|
+
return false;
|
|
394
|
+
const patches = s.patchExpandedToolUseIds;
|
|
395
|
+
if (!Array.isArray(patches) ||
|
|
396
|
+
!patches.every((entry) => Array.isArray(entry) &&
|
|
397
|
+
entry.length === 2 &&
|
|
398
|
+
typeof entry[0] === 'string' &&
|
|
399
|
+
isStringArray(entry[1]))) {
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
const execs = s.pendingExecCommands;
|
|
403
|
+
if (!Array.isArray(execs) ||
|
|
404
|
+
!execs.every((entry) => isPlainObject(entry) &&
|
|
405
|
+
typeof entry.call_id === 'string' &&
|
|
406
|
+
isStringArray(entry.command) &&
|
|
407
|
+
isOptionalString(entry.workdir) &&
|
|
408
|
+
typeof entry.timestamp === 'string')) {
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
const mcps = s.pendingMcpToolCalls;
|
|
412
|
+
if (!Array.isArray(mcps) ||
|
|
413
|
+
!mcps.every((entry) => isPlainObject(entry) &&
|
|
414
|
+
typeof entry.call_id === 'string' &&
|
|
415
|
+
typeof entry.tool_name === 'string' &&
|
|
416
|
+
isOptionalString(entry.server_name) &&
|
|
417
|
+
(entry.arguments === undefined || isPlainObject(entry.arguments)) &&
|
|
418
|
+
typeof entry.timestamp === 'string')) {
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
const copy = cloneJson({
|
|
422
|
+
sessionMeta: s.sessionMeta,
|
|
423
|
+
lastRateLimits: s.lastRateLimits,
|
|
424
|
+
patches: patches,
|
|
425
|
+
execs: execs,
|
|
426
|
+
mcps: mcps,
|
|
427
|
+
});
|
|
428
|
+
this.reset();
|
|
429
|
+
this.sessionMeta = copy.sessionMeta;
|
|
430
|
+
this.currentModel = s.currentModel;
|
|
431
|
+
this.prevTotalUsage = prevTotalUsage;
|
|
432
|
+
this.lastTokenUsage = lastTokenUsage;
|
|
433
|
+
this.modelContextWindow = contextWindow;
|
|
434
|
+
this.lastRateLimits = copy.lastRateLimits;
|
|
435
|
+
this.hasAggregateRateLimits = s.hasAggregateRateLimits;
|
|
436
|
+
this.inPlanMode = s.inPlanMode;
|
|
437
|
+
for (const id of s.emittedToolUseIds) {
|
|
438
|
+
rememberBounded(this.emittedToolUseIds, id, this.maxTrackedToolCalls);
|
|
439
|
+
}
|
|
440
|
+
for (const [callId, toolUseIds] of copy.patches)
|
|
441
|
+
this.putPendingPatch(callId, toolUseIds);
|
|
442
|
+
for (const pending of copy.execs) {
|
|
443
|
+
this.putPendingExec(this.metadataOnly
|
|
444
|
+
? { call_id: pending.call_id, command: [], timestamp: pending.timestamp }
|
|
445
|
+
: pending);
|
|
446
|
+
}
|
|
447
|
+
for (const pending of copy.mcps) {
|
|
448
|
+
this.putPendingMcp(this.metadataOnly ? { ...pending, arguments: undefined } : pending);
|
|
449
|
+
}
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
285
452
|
/**
|
|
286
453
|
* Converts a single rollout line to zero or more session events.
|
|
287
454
|
*/
|
|
@@ -353,6 +520,16 @@ export class CodexRolloutParser {
|
|
|
353
520
|
this.evictedByBytes += 1;
|
|
354
521
|
}
|
|
355
522
|
}
|
|
523
|
+
putPendingExec(pending) {
|
|
524
|
+
this.putPending(this.pendingExecCommands, pending.call_id, pending, () => stringBytes(pending.call_id, pending.workdir, pending.timestamp, ...(Array.isArray(pending.command) ? pending.command : [])));
|
|
525
|
+
}
|
|
526
|
+
putPendingMcp(pending) {
|
|
527
|
+
this.putPending(this.pendingMcpToolCalls, pending.call_id, pending, () => stringBytes(pending.call_id, pending.server_name, pending.tool_name, pending.timestamp) +
|
|
528
|
+
approxJsonBytes(pending.arguments));
|
|
529
|
+
}
|
|
530
|
+
putPendingPatch(callId, toolUseIds) {
|
|
531
|
+
this.putPending(this.patchExpandedToolUseIds, callId, toolUseIds, () => stringBytes(callId, ...toolUseIds));
|
|
532
|
+
}
|
|
356
533
|
/** Removes and returns a pending call. */
|
|
357
534
|
takePending(table, callId) {
|
|
358
535
|
const value = table.get(callId);
|
|
@@ -582,7 +759,7 @@ export class CodexRolloutParser {
|
|
|
582
759
|
if (filePaths.length === 0)
|
|
583
760
|
return [];
|
|
584
761
|
const toolUseIds = filePaths.map((fp) => `${item.call_id}-${fp}`);
|
|
585
|
-
this.
|
|
762
|
+
this.putPendingPatch(item.call_id, toolUseIds);
|
|
586
763
|
for (const id of toolUseIds) {
|
|
587
764
|
rememberBounded(this.emittedToolUseIds, id, this.maxTrackedToolCalls);
|
|
588
765
|
}
|
|
@@ -732,7 +909,7 @@ export class CodexRolloutParser {
|
|
|
732
909
|
const pending = this.metadataOnly
|
|
733
910
|
? { call_id: e.call_id, command: [], timestamp }
|
|
734
911
|
: { call_id: e.call_id, command: e.command, workdir: e.workdir, timestamp };
|
|
735
|
-
this.
|
|
912
|
+
this.putPendingExec(pending);
|
|
736
913
|
return [];
|
|
737
914
|
}
|
|
738
915
|
case 'exec_command_end': {
|
|
@@ -795,8 +972,7 @@ export class CodexRolloutParser {
|
|
|
795
972
|
arguments: this.metadataOnly ? undefined : e.arguments,
|
|
796
973
|
timestamp,
|
|
797
974
|
};
|
|
798
|
-
this.
|
|
799
|
-
approxJsonBytes(pending.arguments));
|
|
975
|
+
this.putPendingMcp(pending);
|
|
800
976
|
return [];
|
|
801
977
|
}
|
|
802
978
|
case 'mcp_tool_call_end': {
|
|
@@ -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.
|
|
@@ -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;
|
|
@@ -20,6 +20,7 @@ import { promises as fs } from 'node:fs';
|
|
|
20
20
|
import * as path from 'node:path';
|
|
21
21
|
import { _setCatalogContextWindows } from './modelContext';
|
|
22
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 ──
|
|
@@ -117,7 +118,7 @@ export function normalizeLiteLlmCatalog(raw) {
|
|
|
117
118
|
continue;
|
|
118
119
|
// Catalog keys use both `provider/model` and bare `model` forms. Record both
|
|
119
120
|
// the raw key and the bare-model form so lookups work regardless of caller.
|
|
120
|
-
out
|
|
121
|
+
setOwnValue(out, key, pricing);
|
|
121
122
|
const bare = stripProviderPrefix(key);
|
|
122
123
|
if (bare && bare !== key && !out[bare]) {
|
|
123
124
|
out[bare] = pricing;
|
|
@@ -154,7 +155,7 @@ export function normalizeLiteLlmContextWindows(raw) {
|
|
|
154
155
|
const window = entry.max_input_tokens;
|
|
155
156
|
if (typeof window !== 'number' || !Number.isFinite(window) || window <= 0)
|
|
156
157
|
continue;
|
|
157
|
-
out
|
|
158
|
+
setOwnValue(out, key, window);
|
|
158
159
|
const bare = stripProviderPrefix(key);
|
|
159
160
|
if (bare && bare !== key) {
|
|
160
161
|
const seen = aliasWindows.get(bare) ?? new Set();
|
|
@@ -13,7 +13,7 @@ import { StringDecoder } from 'string_decoder';
|
|
|
13
13
|
import { readSessionContextSnapshot } from '../context/sessionContext';
|
|
14
14
|
import { readSessionFileStats } from '../sessionStats';
|
|
15
15
|
import { JsonlParser } from '../parsers/jsonl';
|
|
16
|
-
import { ClaudeUsageDeduper, normalizeClaudeUsage } from '../usage/claudeUsageDedupe';
|
|
16
|
+
import { ClaudeUsageDeduper, normalizeClaudeUsage, } from '../usage/claudeUsageDedupe';
|
|
17
17
|
import { ProviderDiagnosticTracker } from './diagnostics';
|
|
18
18
|
import { DirectoryListingCache, splitWatchedPath } from './directoryListingCache';
|
|
19
19
|
import { encodeWorkspacePath as encodeWsPath, getSessionDirectory as getSessionDir, discoverSessionDirectory as discoverSessionDir, findActiveSession as findActiveSessionPath, findAllSessions as findAllSessionPaths, findSessionsInDirectory as findSessionsInDir, decodeEncodedPath, getAllProjectFolders as getAllProjectFoldersRaw, findSessionFilesWithStats, } from '../parsers/sessionPathResolver';
|
|
@@ -119,7 +119,8 @@ class ClaudeCodeReader {
|
|
|
119
119
|
this.usageDeduper = usageDeduper ?? new ClaudeUsageDeduper();
|
|
120
120
|
this.parser = new JsonlParser({
|
|
121
121
|
onEvent: (raw) => {
|
|
122
|
-
|
|
122
|
+
// Mid-turn task notifications finish background subagents.
|
|
123
|
+
for (const event of extractSessionEvents(raw, { includeTaskNotifications: true })) {
|
|
123
124
|
this.events.push(this.usageDeduper.apply(normalizeClaudeUsage(event)));
|
|
124
125
|
}
|
|
125
126
|
},
|
|
@@ -208,6 +209,13 @@ class ClaudeCodeReader {
|
|
|
208
209
|
wasTruncated() {
|
|
209
210
|
return this._wasTruncated;
|
|
210
211
|
}
|
|
212
|
+
/** The usage deduper's recent message ids, so a resumed reader does not recount a split response. */
|
|
213
|
+
getResumeState() {
|
|
214
|
+
return this.usageDeduper.exportState();
|
|
215
|
+
}
|
|
216
|
+
restoreResumeState(state) {
|
|
217
|
+
return this.usageDeduper.importState(state);
|
|
218
|
+
}
|
|
211
219
|
}
|
|
212
220
|
/**
|
|
213
221
|
* Session provider for Claude Code CLI.
|