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.
Files changed (84) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +56 -25
  3. package/dist/aggregation/EventAggregator.d.ts +19 -0
  4. package/dist/aggregation/EventAggregator.js +174 -35
  5. package/dist/aggregation/snapshot.d.ts +37 -8
  6. package/dist/aggregation/snapshot.js +31 -9
  7. package/dist/aggregation/types.d.ts +13 -0
  8. package/dist/browser.d.ts +5 -1
  9. package/dist/browser.js +9 -3
  10. package/dist/context/sessionContext.js +4 -0
  11. package/dist/esm/aggregation/EventAggregator.js +174 -35
  12. package/dist/esm/aggregation/snapshot.js +31 -9
  13. package/dist/esm/browser.js +1 -0
  14. package/dist/esm/context/sessionContext.js +4 -0
  15. package/dist/esm/formatters/toolSummary.js +4 -1
  16. package/dist/esm/index.js +3 -1
  17. package/dist/esm/modelCatalog.js +5 -4
  18. package/dist/esm/modelContext.js +4 -3
  19. package/dist/esm/modelInfo.js +4 -3
  20. package/dist/esm/node.js +1 -1
  21. package/dist/esm/observedContextWindows.js +2 -1
  22. package/dist/esm/ownKey.js +26 -0
  23. package/dist/esm/parsers/codexParser.js +180 -4
  24. package/dist/esm/parsers/openCodeParser.js +2 -1
  25. package/dist/esm/parsers/subagentScanner.js +33 -12
  26. package/dist/esm/parsers/subagentTraceParser.js +97 -37
  27. package/dist/esm/pricingCatalog.js +3 -2
  28. package/dist/esm/providers/claudeCode.js +10 -2
  29. package/dist/esm/providers/codex.js +82 -38
  30. package/dist/esm/schemas/sessionEvent.js +35 -14
  31. package/dist/esm/subagentSpawn.js +163 -0
  32. package/dist/esm/transcript.js +4 -0
  33. package/dist/esm/turns/assistantTurn.js +3 -2
  34. package/dist/esm/usage/claudeUsageDedupe.js +68 -0
  35. package/dist/esm/watchers/eventBridge.js +15 -0
  36. package/dist/esm/watchers/factory.js +5 -3
  37. package/dist/esm/watchers/jsonlTail.js +73 -8
  38. package/dist/esm/watchers/jsonlWatcher.js +51 -4
  39. package/dist/esm/watchers/providerReaderWatcher.js +13 -0
  40. package/dist/esm/zaiQuota.js +2 -1
  41. package/dist/formatters/toolSummary.js +4 -1
  42. package/dist/index.d.ts +8 -3
  43. package/dist/index.js +18 -9
  44. package/dist/modelCatalog.js +5 -4
  45. package/dist/modelContext.js +4 -3
  46. package/dist/modelInfo.js +4 -3
  47. package/dist/node.d.ts +1 -1
  48. package/dist/node.js +3 -1
  49. package/dist/observedContextWindows.js +2 -1
  50. package/dist/ownKey.d.ts +16 -0
  51. package/dist/ownKey.js +30 -0
  52. package/dist/parsers/codexParser.d.ts +63 -0
  53. package/dist/parsers/codexParser.js +181 -4
  54. package/dist/parsers/openCodeParser.js +2 -1
  55. package/dist/parsers/subagentScanner.d.ts +19 -6
  56. package/dist/parsers/subagentScanner.js +33 -12
  57. package/dist/parsers/subagentTraceParser.d.ts +25 -5
  58. package/dist/parsers/subagentTraceParser.js +97 -37
  59. package/dist/pricingCatalog.js +3 -2
  60. package/dist/providers/claudeCode.js +9 -1
  61. package/dist/providers/codex.js +82 -38
  62. package/dist/providers/types.d.ts +14 -0
  63. package/dist/schemas/index.d.ts +1 -0
  64. package/dist/schemas/sessionEvent.d.ts +27 -10
  65. package/dist/schemas/sessionEvent.js +35 -14
  66. package/dist/subagentSpawn.d.ts +80 -0
  67. package/dist/subagentSpawn.js +174 -0
  68. package/dist/transcript.js +4 -0
  69. package/dist/turns/assistantTurn.d.ts +1 -0
  70. package/dist/turns/assistantTurn.js +3 -2
  71. package/dist/usage/claudeUsageDedupe.d.ts +31 -0
  72. package/dist/usage/claudeUsageDedupe.js +68 -0
  73. package/dist/watchers/eventBridge.js +15 -0
  74. package/dist/watchers/factory.d.ts +6 -0
  75. package/dist/watchers/factory.js +5 -3
  76. package/dist/watchers/jsonlTail.d.ts +27 -4
  77. package/dist/watchers/jsonlTail.js +75 -8
  78. package/dist/watchers/jsonlWatcher.d.ts +18 -1
  79. package/dist/watchers/jsonlWatcher.js +50 -3
  80. package/dist/watchers/providerReaderWatcher.d.ts +7 -0
  81. package/dist/watchers/providerReaderWatcher.js +13 -0
  82. package/dist/watchers/types.d.ts +12 -0
  83. package/dist/zaiQuota.js +2 -1
  84. package/package.json +1 -1
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Own-property lookups for plain-object tables indexed by external strings.
3
+ *
4
+ * `table[key]` also finds inherited members, so a model id, tool name, or
5
+ * error code such as `constructor`, `toString`, or `__proto__` would return an
6
+ * `Object.prototype` member instead of `undefined`.
7
+ *
8
+ * @module ownKey
9
+ */
10
+ /** The value stored under `key` in `table` itself, or `undefined`. */
11
+ export declare function ownValue<T>(table: Readonly<Record<string, T>>, key: string): T | undefined;
12
+ /**
13
+ * Store `value` as an own, enumerable property, even for `__proto__`, which a
14
+ * plain assignment would treat as a prototype change.
15
+ */
16
+ export declare function setOwnValue<T>(table: Record<string, T>, key: string, value: T): void;
package/dist/ownKey.js ADDED
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ /**
3
+ * Own-property lookups for plain-object tables indexed by external strings.
4
+ *
5
+ * `table[key]` also finds inherited members, so a model id, tool name, or
6
+ * error code such as `constructor`, `toString`, or `__proto__` would return an
7
+ * `Object.prototype` member instead of `undefined`.
8
+ *
9
+ * @module ownKey
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.ownValue = ownValue;
13
+ exports.setOwnValue = setOwnValue;
14
+ const hasOwn = Object.prototype.hasOwnProperty;
15
+ /** The value stored under `key` in `table` itself, or `undefined`. */
16
+ function ownValue(table, key) {
17
+ return hasOwn.call(table, key) ? table[key] : undefined;
18
+ }
19
+ /**
20
+ * Store `value` as an own, enumerable property, even for `__proto__`, which a
21
+ * plain assignment would treat as a prototype change.
22
+ */
23
+ function setOwnValue(table, key, value) {
24
+ Object.defineProperty(table, key, {
25
+ value,
26
+ enumerable: true,
27
+ writable: true,
28
+ configurable: true,
29
+ });
30
+ }
@@ -13,6 +13,21 @@
13
13
  */
14
14
  import type { SessionEvent } from '../types/sessionEvent';
15
15
  import type { CodexRolloutLine, CodexSessionMeta, CodexTokenUsage, CodexRateLimits } from '../types/codex';
16
+ /** Pending exec command awaiting its end event. */
17
+ interface PendingExecCommand {
18
+ call_id: string;
19
+ command: string[];
20
+ workdir?: string;
21
+ timestamp: string;
22
+ }
23
+ /** Pending MCP tool call awaiting its end event. */
24
+ interface PendingMcpToolCall {
25
+ call_id: string;
26
+ server_name?: string;
27
+ tool_name: string;
28
+ arguments?: Record<string, unknown>;
29
+ timestamp: string;
30
+ }
16
31
  /**
17
32
  * Resolve the usage of the single call a Codex `token_count` event reports.
18
33
  *
@@ -85,6 +100,35 @@ export interface CodexPendingStats {
85
100
  /** Pending calls forgotten because `maxPendingBytes` was exceeded. */
86
101
  evictedByBytes: number;
87
102
  }
103
+ /**
104
+ * JSON-serializable continuation state of a {@link CodexRolloutParser}, from
105
+ * `exportResumeState()`, for `importResumeState()` in a parser that resumes
106
+ * at the same rollout position (for example from a persisted session
107
+ * snapshot). Every table lists its entries oldest first.
108
+ */
109
+ export interface CodexRolloutResumeState {
110
+ version: 1;
111
+ sessionMeta: CodexSessionMeta | null;
112
+ /** Model from the latest `turn_context`, attributed to later events. */
113
+ currentModel: string | null;
114
+ /** Cumulative total of the previous token_count event, to skip its duplicates. */
115
+ prevTotalUsage: CodexTokenUsage | null;
116
+ lastTokenUsage: CodexTokenUsage | null;
117
+ modelContextWindow: number | null;
118
+ lastRateLimits: CodexRateLimits | null;
119
+ hasAggregateRateLimits: boolean;
120
+ inPlanMode: boolean;
121
+ /** Call ids whose tool use was already emitted, so their end event does not emit another. */
122
+ emittedToolUseIds: string[];
123
+ /** `apply_patch` calls awaiting output: call id and its per-file tool-use ids. */
124
+ patchExpandedToolUseIds: Array<[string, string[]]>;
125
+ /** `exec_command_begin` events awaiting their end. */
126
+ pendingExecCommands: PendingExecCommand[];
127
+ /** `mcp_tool_call_begin` events awaiting their end. */
128
+ pendingMcpToolCalls: PendingMcpToolCall[];
129
+ }
130
+ /** A valid `CodexTokenUsage` (copied), `null` for `null`, or `undefined` when invalid. Internal. */
131
+ export declare function codexTokenUsageFrom(value: unknown): CodexTokenUsage | null | undefined;
88
132
  /**
89
133
  * Stateful parser for Codex JSONL rollout lines.
90
134
  *
@@ -130,6 +174,21 @@ export declare class CodexRolloutParser {
130
174
  getModelContextWindow(): number | null;
131
175
  /** Get the last observed rate limits from token_count events. */
132
176
  getLastRateLimits(): CodexRateLimits | null;
177
+ /**
178
+ * The state later lines depend on — session metadata, current model,
179
+ * previous cumulative token total, context window, rate limits, plan mode,
180
+ * and the tool-call correlation tables (newest `limit` entries each) — as
181
+ * JSON-serializable data. Eviction counters are not included.
182
+ */
183
+ exportResumeState(limit?: number): CodexRolloutResumeState;
184
+ /**
185
+ * Replaces this parser's state with one from {@link exportResumeState}, as
186
+ * if it had parsed the rollout up to where that state was taken. Returns
187
+ * false, changing nothing, when `state` is not a valid version-1 state.
188
+ * This parser's own `maxTrackedToolCalls`, `maxPendingBytes`, and
189
+ * `metadataOnly` options apply to the imported tables.
190
+ */
191
+ importResumeState(state: unknown): boolean;
133
192
  /**
134
193
  * Converts a single rollout line to zero or more session events.
135
194
  */
@@ -142,6 +201,9 @@ export declare class CodexRolloutParser {
142
201
  * first.
143
202
  */
144
203
  private putPending;
204
+ private putPendingExec;
205
+ private putPendingMcp;
206
+ private putPendingPatch;
145
207
  /** Removes and returns a pending call. */
146
208
  private takePending;
147
209
  private forgetPendingBytes;
@@ -164,3 +226,4 @@ export declare class CodexRolloutParser {
164
226
  private handleEventMsg;
165
227
  private handleTokenCount;
166
228
  }
229
+ export {};
@@ -18,6 +18,7 @@ exports.resolveCodexCallUsage = resolveCodexCallUsage;
18
18
  exports.normalizeCodexToolName = normalizeCodexToolName;
19
19
  exports.normalizeCodexToolInput = normalizeCodexToolInput;
20
20
  exports.extractPatchFilePaths = extractPatchFilePaths;
21
+ exports.codexTokenUsageFrom = codexTokenUsageFrom;
21
22
  const codex_1 = require("../types/codex");
22
23
  const openCodeParser_1 = require("./openCodeParser");
23
24
  const usageNormalization_1 = require("../usageNormalization");
@@ -178,6 +179,61 @@ function extractPatchFilePaths(input) {
178
179
  }
179
180
  /** Default cap on each piece of per-call correlation state the parser keeps. */
180
181
  exports.DEFAULT_MAX_TRACKED_TOOL_CALLS = 4096;
182
+ /** Entries per table {@link CodexRolloutParser.exportResumeState} keeps by default. */
183
+ const DEFAULT_RESUME_STATE_LIMIT = 256;
184
+ function isPlainObject(value) {
185
+ return !!value && typeof value === 'object' && !Array.isArray(value);
186
+ }
187
+ function cloneJson(value) {
188
+ return JSON.parse(JSON.stringify(value));
189
+ }
190
+ function isOptionalString(value) {
191
+ return value === undefined || typeof value === 'string';
192
+ }
193
+ function isStringArray(value) {
194
+ return Array.isArray(value) && value.every((item) => typeof item === 'string');
195
+ }
196
+ function isTokenCount(value) {
197
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0;
198
+ }
199
+ /** A valid `CodexTokenUsage` (copied), `null` for `null`, or `undefined` when invalid. Internal. */
200
+ function codexTokenUsageFrom(value) {
201
+ if (value === null)
202
+ return null;
203
+ if (!isPlainObject(value))
204
+ return undefined;
205
+ if (!isTokenCount(value.input_tokens) || !isTokenCount(value.output_tokens))
206
+ return undefined;
207
+ const usage = {
208
+ input_tokens: value.input_tokens,
209
+ output_tokens: value.output_tokens,
210
+ };
211
+ for (const field of ['cached_input_tokens', 'reasoning_output_tokens', 'total_tokens']) {
212
+ const count = value[field];
213
+ if (count === undefined)
214
+ continue;
215
+ if (!isTokenCount(count))
216
+ return undefined;
217
+ usage[field] = count;
218
+ }
219
+ return usage;
220
+ }
221
+ /** Last `limit` items of an insertion-ordered collection, oldest first. */
222
+ function newest(items, size, limit) {
223
+ const count = limit >= 0 ? Math.min(size, Math.floor(limit)) : 0;
224
+ const kept = [];
225
+ let skip = size - count;
226
+ for (const item of items) {
227
+ if (kept.length >= count)
228
+ break;
229
+ if (skip > 0) {
230
+ skip -= 1;
231
+ continue;
232
+ }
233
+ kept.push(item);
234
+ }
235
+ return kept;
236
+ }
181
237
  /** Fixed per-entry cost added to each pending call's byte estimate. */
182
238
  const PENDING_ENTRY_OVERHEAD_BYTES = 64;
183
239
  /** Prefix of the bookkeeping keys the parser stashes inside tool inputs. */
@@ -289,6 +345,118 @@ class CodexRolloutParser {
289
345
  getLastRateLimits() {
290
346
  return this.lastRateLimits;
291
347
  }
348
+ /**
349
+ * The state later lines depend on — session metadata, current model,
350
+ * previous cumulative token total, context window, rate limits, plan mode,
351
+ * and the tool-call correlation tables (newest `limit` entries each) — as
352
+ * JSON-serializable data. Eviction counters are not included.
353
+ */
354
+ exportResumeState(limit = DEFAULT_RESUME_STATE_LIMIT) {
355
+ return cloneJson({
356
+ version: 1,
357
+ sessionMeta: this.sessionMeta,
358
+ currentModel: this.currentModel,
359
+ prevTotalUsage: this.prevTotalUsage,
360
+ lastTokenUsage: this.lastTokenUsage,
361
+ modelContextWindow: this.modelContextWindow,
362
+ lastRateLimits: this.lastRateLimits,
363
+ hasAggregateRateLimits: this.hasAggregateRateLimits,
364
+ inPlanMode: this.inPlanMode,
365
+ emittedToolUseIds: newest(this.emittedToolUseIds, this.emittedToolUseIds.size, limit),
366
+ patchExpandedToolUseIds: newest(this.patchExpandedToolUseIds, this.patchExpandedToolUseIds.size, limit),
367
+ pendingExecCommands: newest(this.pendingExecCommands.values(), this.pendingExecCommands.size, limit),
368
+ pendingMcpToolCalls: newest(this.pendingMcpToolCalls.values(), this.pendingMcpToolCalls.size, limit),
369
+ });
370
+ }
371
+ /**
372
+ * Replaces this parser's state with one from {@link exportResumeState}, as
373
+ * if it had parsed the rollout up to where that state was taken. Returns
374
+ * false, changing nothing, when `state` is not a valid version-1 state.
375
+ * This parser's own `maxTrackedToolCalls`, `maxPendingBytes`, and
376
+ * `metadataOnly` options apply to the imported tables.
377
+ */
378
+ importResumeState(state) {
379
+ if (!isPlainObject(state) || state.version !== 1)
380
+ return false;
381
+ const s = state;
382
+ const prevTotalUsage = codexTokenUsageFrom(s.prevTotalUsage);
383
+ const lastTokenUsage = codexTokenUsageFrom(s.lastTokenUsage);
384
+ if (prevTotalUsage === undefined || lastTokenUsage === undefined)
385
+ return false;
386
+ if (s.sessionMeta !== null && !isPlainObject(s.sessionMeta))
387
+ return false;
388
+ if (s.lastRateLimits !== null && !isPlainObject(s.lastRateLimits))
389
+ return false;
390
+ if (s.currentModel !== null && typeof s.currentModel !== 'string')
391
+ return false;
392
+ const contextWindow = s.modelContextWindow;
393
+ if (contextWindow !== null &&
394
+ !(typeof contextWindow === 'number' && Number.isFinite(contextWindow) && contextWindow > 0)) {
395
+ return false;
396
+ }
397
+ if (typeof s.hasAggregateRateLimits !== 'boolean' || typeof s.inPlanMode !== 'boolean') {
398
+ return false;
399
+ }
400
+ if (!isStringArray(s.emittedToolUseIds))
401
+ return false;
402
+ const patches = s.patchExpandedToolUseIds;
403
+ if (!Array.isArray(patches) ||
404
+ !patches.every((entry) => Array.isArray(entry) &&
405
+ entry.length === 2 &&
406
+ typeof entry[0] === 'string' &&
407
+ isStringArray(entry[1]))) {
408
+ return false;
409
+ }
410
+ const execs = s.pendingExecCommands;
411
+ if (!Array.isArray(execs) ||
412
+ !execs.every((entry) => isPlainObject(entry) &&
413
+ typeof entry.call_id === 'string' &&
414
+ isStringArray(entry.command) &&
415
+ isOptionalString(entry.workdir) &&
416
+ typeof entry.timestamp === 'string')) {
417
+ return false;
418
+ }
419
+ const mcps = s.pendingMcpToolCalls;
420
+ if (!Array.isArray(mcps) ||
421
+ !mcps.every((entry) => isPlainObject(entry) &&
422
+ typeof entry.call_id === 'string' &&
423
+ typeof entry.tool_name === 'string' &&
424
+ isOptionalString(entry.server_name) &&
425
+ (entry.arguments === undefined || isPlainObject(entry.arguments)) &&
426
+ typeof entry.timestamp === 'string')) {
427
+ return false;
428
+ }
429
+ const copy = cloneJson({
430
+ sessionMeta: s.sessionMeta,
431
+ lastRateLimits: s.lastRateLimits,
432
+ patches: patches,
433
+ execs: execs,
434
+ mcps: mcps,
435
+ });
436
+ this.reset();
437
+ this.sessionMeta = copy.sessionMeta;
438
+ this.currentModel = s.currentModel;
439
+ this.prevTotalUsage = prevTotalUsage;
440
+ this.lastTokenUsage = lastTokenUsage;
441
+ this.modelContextWindow = contextWindow;
442
+ this.lastRateLimits = copy.lastRateLimits;
443
+ this.hasAggregateRateLimits = s.hasAggregateRateLimits;
444
+ this.inPlanMode = s.inPlanMode;
445
+ for (const id of s.emittedToolUseIds) {
446
+ rememberBounded(this.emittedToolUseIds, id, this.maxTrackedToolCalls);
447
+ }
448
+ for (const [callId, toolUseIds] of copy.patches)
449
+ this.putPendingPatch(callId, toolUseIds);
450
+ for (const pending of copy.execs) {
451
+ this.putPendingExec(this.metadataOnly
452
+ ? { call_id: pending.call_id, command: [], timestamp: pending.timestamp }
453
+ : pending);
454
+ }
455
+ for (const pending of copy.mcps) {
456
+ this.putPendingMcp(this.metadataOnly ? { ...pending, arguments: undefined } : pending);
457
+ }
458
+ return true;
459
+ }
292
460
  /**
293
461
  * Converts a single rollout line to zero or more session events.
294
462
  */
@@ -360,6 +528,16 @@ class CodexRolloutParser {
360
528
  this.evictedByBytes += 1;
361
529
  }
362
530
  }
531
+ putPendingExec(pending) {
532
+ this.putPending(this.pendingExecCommands, pending.call_id, pending, () => stringBytes(pending.call_id, pending.workdir, pending.timestamp, ...(Array.isArray(pending.command) ? pending.command : [])));
533
+ }
534
+ putPendingMcp(pending) {
535
+ this.putPending(this.pendingMcpToolCalls, pending.call_id, pending, () => stringBytes(pending.call_id, pending.server_name, pending.tool_name, pending.timestamp) +
536
+ approxJsonBytes(pending.arguments));
537
+ }
538
+ putPendingPatch(callId, toolUseIds) {
539
+ this.putPending(this.patchExpandedToolUseIds, callId, toolUseIds, () => stringBytes(callId, ...toolUseIds));
540
+ }
363
541
  /** Removes and returns a pending call. */
364
542
  takePending(table, callId) {
365
543
  const value = table.get(callId);
@@ -589,7 +767,7 @@ class CodexRolloutParser {
589
767
  if (filePaths.length === 0)
590
768
  return [];
591
769
  const toolUseIds = filePaths.map((fp) => `${item.call_id}-${fp}`);
592
- this.putPending(this.patchExpandedToolUseIds, item.call_id, toolUseIds, () => stringBytes(item.call_id, ...toolUseIds));
770
+ this.putPendingPatch(item.call_id, toolUseIds);
593
771
  for (const id of toolUseIds) {
594
772
  rememberBounded(this.emittedToolUseIds, id, this.maxTrackedToolCalls);
595
773
  }
@@ -739,7 +917,7 @@ class CodexRolloutParser {
739
917
  const pending = this.metadataOnly
740
918
  ? { call_id: e.call_id, command: [], timestamp }
741
919
  : { call_id: e.call_id, command: e.command, workdir: e.workdir, timestamp };
742
- this.putPending(this.pendingExecCommands, e.call_id, pending, () => stringBytes(pending.call_id, pending.workdir, pending.timestamp, ...(Array.isArray(pending.command) ? pending.command : [])));
920
+ this.putPendingExec(pending);
743
921
  return [];
744
922
  }
745
923
  case 'exec_command_end': {
@@ -802,8 +980,7 @@ class CodexRolloutParser {
802
980
  arguments: this.metadataOnly ? undefined : e.arguments,
803
981
  timestamp,
804
982
  };
805
- this.putPending(this.pendingMcpToolCalls, e.call_id, pending, () => stringBytes(pending.call_id, pending.server_name, pending.tool_name, timestamp) +
806
- approxJsonBytes(pending.arguments));
983
+ this.putPendingMcp(pending);
807
984
  return [];
808
985
  }
809
986
  case 'mcp_tool_call_end': {
@@ -22,6 +22,7 @@ exports.convertOpenCodeMessage = convertOpenCodeMessage;
22
22
  exports.parseDbMessageData = parseDbMessageData;
23
23
  exports.parseDbPartData = parseDbPartData;
24
24
  const usageNormalization_1 = require("../usageNormalization");
25
+ const ownKey_1 = require("../ownKey");
25
26
  /**
26
27
  * OpenCode → Claude Code tool name mapping.
27
28
  * OpenCode uses lowercase names; downstream code expects PascalCase.
@@ -57,7 +58,7 @@ function normalizeToolName(name) {
57
58
  if (!name)
58
59
  return name;
59
60
  // Check mapping (case-insensitive lookup via lowercase key)
60
- const mapped = TOOL_NAME_MAP[name.toLowerCase()];
61
+ const mapped = (0, ownKey_1.ownValue)(TOOL_NAME_MAP, name.toLowerCase());
61
62
  if (mapped)
62
63
  return mapped;
63
64
  // Already PascalCase (starts with uppercase)? Pass through.
@@ -8,15 +8,28 @@
8
8
  * @module parsers/subagentScanner
9
9
  */
10
10
  import type { SubagentStats, TaskState } from '../types/sessionEvent';
11
+ /** Fields of a Claude Code `agent-<id>.meta.json`; each is present only when well-typed. */
12
+ export interface AgentMeta {
13
+ agentType?: string;
14
+ description?: string;
15
+ /** The tool_use id of the `Agent` / `Task` call that spawned this agent. */
16
+ toolUseId?: string;
17
+ /** The agent that spawned this one, for spawns nested inside a subagent. */
18
+ parentAgentId?: string;
19
+ /** `'background'` for an agent launched in the background. */
20
+ requestShape?: string;
21
+ /** Written as `true` once a background agent is done (newer Claude Code). */
22
+ finished?: boolean;
23
+ /** The user stopped the agent. */
24
+ stoppedByUser?: boolean;
25
+ }
11
26
  /**
12
27
  * Reads `agent-<id>.meta.json` (written by Claude Code next to the transcript)
13
- * for the spawn's agent type and description. Missing or malformed files
14
- * yield an empty object.
28
+ * for the spawn's agent type, description, spawning tool_use id, parent agent,
29
+ * and completion flags. Missing or malformed files yield an empty object;
30
+ * wrongly typed fields are left out.
15
31
  */
16
- export declare function readAgentMeta(agentFilePath: string): {
17
- agentType?: string;
18
- description?: string;
19
- };
32
+ export declare function readAgentMeta(agentFilePath: string): AgentMeta;
20
33
  /**
21
34
  * Scans subagent directory for agent JSONL files and extracts tool calls.
22
35
  *
@@ -51,28 +51,49 @@ const path = __importStar(require("path"));
51
51
  const claudeUsageDedupe_1 = require("../usage/claudeUsageDedupe");
52
52
  const subagentUsage_1 = require("../usage/subagentUsage");
53
53
  const subagentStatsCache_1 = require("./subagentStatsCache");
54
+ const subagentSpawn_1 = require("../subagentSpawn");
54
55
  /**
55
56
  * Pattern for matching subagent JSONL files.
56
57
  * Files are named like: agent-<hash>.jsonl
57
58
  */
58
59
  const AGENT_FILE_PATTERN = /^agent-(.+)\.jsonl$/;
60
+ function metaString(value) {
61
+ return typeof value === 'string' && value ? value : undefined;
62
+ }
59
63
  /**
60
64
  * Reads `agent-<id>.meta.json` (written by Claude Code next to the transcript)
61
- * for the spawn's agent type and description. Missing or malformed files
62
- * yield an empty object.
65
+ * for the spawn's agent type, description, spawning tool_use id, parent agent,
66
+ * and completion flags. Missing or malformed files yield an empty object;
67
+ * wrongly typed fields are left out.
63
68
  */
64
69
  function readAgentMeta(agentFilePath) {
65
70
  try {
66
71
  const metaPath = agentFilePath.replace(/\.jsonl$/, '.meta.json');
67
72
  const parsed = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
68
- return {
69
- ...(typeof parsed.agentType === 'string' && parsed.agentType
70
- ? { agentType: parsed.agentType }
71
- : {}),
72
- ...(typeof parsed.description === 'string' && parsed.description
73
- ? { description: parsed.description }
74
- : {}),
75
- };
73
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
74
+ return {};
75
+ const record = parsed;
76
+ const meta = {};
77
+ const agentType = metaString(record.agentType);
78
+ if (agentType)
79
+ meta.agentType = agentType;
80
+ const description = metaString(record.description);
81
+ if (description)
82
+ meta.description = description;
83
+ const toolUseId = metaString(record.toolUseId);
84
+ if (toolUseId)
85
+ meta.toolUseId = toolUseId;
86
+ const parentAgentId = metaString(record.parentAgentId);
87
+ if (parentAgentId)
88
+ meta.parentAgentId = parentAgentId;
89
+ const requestShape = metaString(record.requestShape);
90
+ if (requestShape)
91
+ meta.requestShape = requestShape;
92
+ if (typeof record.finished === 'boolean')
93
+ meta.finished = record.finished;
94
+ if (typeof record.stoppedByUser === 'boolean')
95
+ meta.stoppedByUser = record.stoppedByUser;
96
+ return meta;
76
97
  }
77
98
  catch {
78
99
  return {};
@@ -247,8 +268,8 @@ function parseAgentFile(filePath, agentId, log) {
247
268
  }
248
269
  }
249
270
  toolCalls.push(toolCall);
250
- // If this is a Task tool call (spawning another subagent), extract info
251
- if (toolUse.name === 'Task' && toolUse.input) {
271
+ // If this is an Agent/Task call (spawning another subagent), extract info
272
+ if ((0, subagentSpawn_1.isSubagentSpawnTool)(toolUse.name) && toolUse.input) {
252
273
  if (toolUse.input.subagent_type && !agentType) {
253
274
  agentType = String(toolUse.input.subagent_type);
254
275
  }
@@ -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
- * Three-phase parent-child linking:
9
- * 1. Result-based: match toolUseResult.agentId → parent tool_use_id
10
- * 2. Team summary: parse <teammate-message> XML blocks
11
- * 3. Positional fallback: timestamp ordering + proximity
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
  */
@@ -41,8 +44,25 @@ export interface SubagentTrace {
41
44
  children: SubagentTrace[];
42
45
  /** Basic stats (tokens, duration, etc.) */
43
46
  stats: SubagentStats;
44
- /** Parent tool_use_id that spawned this agent */
47
+ /**
48
+ * Parent tool_use_id that spawned this agent: the spawning `Agent` / `Task`
49
+ * call's id when Claude Code recorded it (also for top-level traces, whose
50
+ * spawn is in the main session). For a heuristic link without that
51
+ * metadata it holds the parent's agent id, as before; see `parentAgentId`.
52
+ */
45
53
  parentToolUseId?: string;
54
+ /** Path of this agent's `agent-<id>.jsonl` transcript. */
55
+ filePath?: string;
56
+ /** The tool_use id of the call that spawned this agent (meta `toolUseId`). */
57
+ spawnToolUseId?: string;
58
+ /** Agent id of the parent trace, for agents spawned by another subagent. */
59
+ parentAgentId?: string;
60
+ /** Launched in the background (meta `requestShape: 'background'`). */
61
+ background?: boolean;
62
+ /** Claude Code marked the background agent done (meta `finished`). */
63
+ finished?: boolean;
64
+ /** The user stopped the agent (meta `stoppedByUser`). */
65
+ stoppedByUser?: boolean;
46
66
  }
47
67
  /**
48
68
  * Scans and parses subagent traces with full conversation events.