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.
Files changed (151) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +188 -27
  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/authStatus.d.ts +54 -0
  9. package/dist/authStatus.js +91 -0
  10. package/dist/browser.d.ts +16 -6
  11. package/dist/browser.js +24 -2
  12. package/dist/codex.d.ts +23 -0
  13. package/dist/codex.js +44 -0
  14. package/dist/codexProfiles.d.ts +1 -0
  15. package/dist/codexProfiles.js +8 -11
  16. package/dist/context/sessionContext.js +4 -0
  17. package/dist/esm/aggregation/EventAggregator.js +174 -35
  18. package/dist/esm/aggregation/snapshot.js +31 -9
  19. package/dist/esm/authStatus.js +87 -0
  20. package/dist/esm/browser.js +5 -2
  21. package/dist/esm/codex.js +18 -0
  22. package/dist/esm/codexProfiles.js +8 -11
  23. package/dist/esm/context/sessionContext.js +4 -0
  24. package/dist/esm/formatters/toolSummary.js +4 -1
  25. package/dist/esm/formatting.js +10 -1
  26. package/dist/esm/index.js +11 -6
  27. package/dist/esm/modelCatalog.js +7 -5
  28. package/dist/esm/modelContext.js +4 -3
  29. package/dist/esm/modelInfo.js +425 -47
  30. package/dist/esm/node.js +9 -3
  31. package/dist/esm/observedContextWindows.js +2 -1
  32. package/dist/esm/ownKey.js +26 -0
  33. package/dist/esm/parsers/codexParser.js +351 -54
  34. package/dist/esm/parsers/codexRolloutOrigin.js +107 -0
  35. package/dist/esm/parsers/openCodeParser.js +85 -22
  36. package/dist/esm/parsers/subagentScanner.js +33 -12
  37. package/dist/esm/parsers/subagentTraceParser.js +97 -37
  38. package/dist/esm/pricing.js +18 -0
  39. package/dist/esm/pricingCatalog.js +17 -9
  40. package/dist/esm/promptHistory.js +66 -4
  41. package/dist/esm/providerFailure.js +124 -3
  42. package/dist/esm/providers/claudeCode.js +10 -2
  43. package/dist/esm/providers/codex.js +84 -62
  44. package/dist/esm/providers/codexDatabase.js +3 -19
  45. package/dist/esm/providers/codexRollouts.js +24 -1
  46. package/dist/esm/providers/openCode.js +9 -17
  47. package/dist/esm/report/htmlReportGenerator.js +42 -5
  48. package/dist/esm/report/transcriptParser.js +13 -11
  49. package/dist/esm/schemas/providerFailure.js +3 -1
  50. package/dist/esm/schemas/sessionEvent.js +35 -14
  51. package/dist/esm/schemas/transcript.js +4 -0
  52. package/dist/esm/sessionPromptHistory.js +2 -2
  53. package/dist/esm/statusline/payload.js +9 -0
  54. package/dist/esm/subagentSpawn.js +163 -0
  55. package/dist/esm/testing/credentialGuard.js +87 -5
  56. package/dist/esm/toolCallOrigins.js +310 -0
  57. package/dist/esm/transcript.js +46 -3
  58. package/dist/esm/turns/assistantTurn.js +3 -2
  59. package/dist/esm/usage/claudeUsageDedupe.js +68 -0
  60. package/dist/esm/usage/usageEvents.js +146 -17
  61. package/dist/esm/usage/usageReports.js +39 -6
  62. package/dist/esm/watchers/eventBridge.js +15 -0
  63. package/dist/esm/watchers/factory.js +5 -3
  64. package/dist/esm/watchers/jsonlTail.js +442 -58
  65. package/dist/esm/watchers/jsonlWatcher.js +51 -4
  66. package/dist/esm/watchers/providerReaderWatcher.js +13 -0
  67. package/dist/esm/zaiQuota.js +2 -1
  68. package/dist/formatters/toolSummary.js +4 -1
  69. package/dist/formatting.d.ts +26 -1
  70. package/dist/formatting.js +11 -1
  71. package/dist/index.d.ts +22 -11
  72. package/dist/index.js +44 -12
  73. package/dist/modelCatalog.js +7 -5
  74. package/dist/modelContext.js +4 -3
  75. package/dist/modelInfo.d.ts +185 -16
  76. package/dist/modelInfo.js +434 -49
  77. package/dist/node.d.ts +16 -5
  78. package/dist/node.js +33 -2
  79. package/dist/observedContextWindows.js +2 -1
  80. package/dist/ownKey.d.ts +16 -0
  81. package/dist/ownKey.js +30 -0
  82. package/dist/parsers/codexParser.d.ts +118 -0
  83. package/dist/parsers/codexParser.js +352 -54
  84. package/dist/parsers/codexRolloutOrigin.d.ts +49 -0
  85. package/dist/parsers/codexRolloutOrigin.js +111 -0
  86. package/dist/parsers/openCodeParser.d.ts +38 -0
  87. package/dist/parsers/openCodeParser.js +88 -22
  88. package/dist/parsers/subagentScanner.d.ts +19 -6
  89. package/dist/parsers/subagentScanner.js +33 -12
  90. package/dist/parsers/subagentTraceParser.d.ts +25 -5
  91. package/dist/parsers/subagentTraceParser.js +97 -37
  92. package/dist/pricing.d.ts +22 -0
  93. package/dist/pricing.js +50 -0
  94. package/dist/pricingCatalog.d.ts +10 -0
  95. package/dist/pricingCatalog.js +16 -8
  96. package/dist/promptHistory.d.ts +65 -4
  97. package/dist/promptHistory.js +68 -5
  98. package/dist/providerFailure.d.ts +44 -0
  99. package/dist/providerFailure.js +126 -4
  100. package/dist/providers/claudeCode.js +9 -1
  101. package/dist/providers/codex.d.ts +3 -22
  102. package/dist/providers/codex.js +85 -64
  103. package/dist/providers/codexDatabase.d.ts +3 -2
  104. package/dist/providers/codexDatabase.js +4 -22
  105. package/dist/providers/codexRollouts.d.ts +22 -0
  106. package/dist/providers/codexRollouts.js +24 -0
  107. package/dist/providers/openCode.js +8 -16
  108. package/dist/providers/types.d.ts +14 -0
  109. package/dist/report/htmlReportGenerator.js +42 -5
  110. package/dist/report/transcriptParser.d.ts +10 -1
  111. package/dist/report/transcriptParser.js +13 -11
  112. package/dist/schemas/index.d.ts +1 -0
  113. package/dist/schemas/providerFailure.d.ts +39 -0
  114. package/dist/schemas/providerFailure.js +2 -0
  115. package/dist/schemas/sessionEvent.d.ts +27 -10
  116. package/dist/schemas/sessionEvent.js +35 -14
  117. package/dist/schemas/transcript.d.ts +2 -0
  118. package/dist/schemas/transcript.js +4 -0
  119. package/dist/sessionPromptHistory.d.ts +7 -1
  120. package/dist/sessionPromptHistory.js +1 -1
  121. package/dist/statusline/payload.d.ts +10 -0
  122. package/dist/statusline/payload.js +14 -0
  123. package/dist/subagentSpawn.d.ts +80 -0
  124. package/dist/subagentSpawn.js +174 -0
  125. package/dist/testing/credentialGuard.d.ts +14 -0
  126. package/dist/testing/credentialGuard.js +87 -5
  127. package/dist/toolCallOrigins.d.ts +70 -0
  128. package/dist/toolCallOrigins.js +346 -0
  129. package/dist/transcript.d.ts +12 -0
  130. package/dist/transcript.js +46 -3
  131. package/dist/turns/assistantTurn.d.ts +1 -0
  132. package/dist/turns/assistantTurn.js +3 -2
  133. package/dist/types/opencode.d.ts +2 -0
  134. package/dist/usage/claudeUsageDedupe.d.ts +31 -0
  135. package/dist/usage/claudeUsageDedupe.js +68 -0
  136. package/dist/usage/usageEvents.d.ts +54 -6
  137. package/dist/usage/usageEvents.js +146 -17
  138. package/dist/usage/usageReports.d.ts +35 -4
  139. package/dist/usage/usageReports.js +39 -6
  140. package/dist/watchers/eventBridge.js +15 -0
  141. package/dist/watchers/factory.d.ts +6 -0
  142. package/dist/watchers/factory.js +5 -3
  143. package/dist/watchers/jsonlTail.d.ts +124 -4
  144. package/dist/watchers/jsonlTail.js +446 -59
  145. package/dist/watchers/jsonlWatcher.d.ts +18 -1
  146. package/dist/watchers/jsonlWatcher.js +50 -3
  147. package/dist/watchers/providerReaderWatcher.d.ts +7 -0
  148. package/dist/watchers/providerReaderWatcher.js +13 -0
  149. package/dist/watchers/types.d.ts +12 -0
  150. package/dist/zaiQuota.js +2 -1
  151. package/package.json +41 -1
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ /**
3
+ * Where a Codex rollout came from, read from its `session_meta` record:
4
+ * a session a person started, a fork of another session, or a subagent.
5
+ *
6
+ * Codex writes `session_meta.source` as a plain string for sessions started
7
+ * from a surface (`cli`, `vscode`, `exec`, …) and as an object for subagents:
8
+ * `{ subagent: { thread_spawn: { parent_thread_id, … } } }` for a spawned
9
+ * agent, `{ subagent: { other: 'guardian' } }` for internal reviewers, or
10
+ * `{ subagent: 'review' }`. The same JSON appears as a string in the state
11
+ * database's `threads.source`. A forked rollout records `forked_from_id`, and
12
+ * embeds its parent's `session_meta` after its own, so only the first counts.
13
+ *
14
+ * Pure: no filesystem access. Browser-safe.
15
+ *
16
+ * @module parsers/codexRolloutOrigin
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.codexSpawnParentFromSource = codexSpawnParentFromSource;
20
+ exports.codexRolloutOrigin = codexRolloutOrigin;
21
+ function record(value) {
22
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
23
+ ? value
24
+ : undefined;
25
+ }
26
+ function nonEmpty(value) {
27
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
28
+ }
29
+ /** A `source` value as an object: rollouts store it parsed, the database as JSON text. */
30
+ function sourceObject(source) {
31
+ if (typeof source !== 'string')
32
+ return record(source);
33
+ if (!source.startsWith('{'))
34
+ return undefined;
35
+ try {
36
+ return record(JSON.parse(source));
37
+ }
38
+ catch {
39
+ return undefined;
40
+ }
41
+ }
42
+ /**
43
+ * Parent thread id of a spawned subagent's `source` (object, or JSON text as
44
+ * stored in `threads.source`): `subagent.thread_spawn.parent_thread_id`.
45
+ */
46
+ function codexSpawnParentFromSource(source) {
47
+ const spawn = record(record(sourceObject(source)?.subagent)?.thread_spawn);
48
+ const parent = spawn?.parent_thread_id;
49
+ return typeof parent === 'string' ? parent : undefined;
50
+ }
51
+ function subagentKindOf(subagent) {
52
+ if (typeof subagent === 'string')
53
+ return nonEmpty(subagent);
54
+ const variant = record(subagent);
55
+ if (!variant)
56
+ return undefined;
57
+ const other = nonEmpty(variant.other);
58
+ if (other)
59
+ return other;
60
+ return Object.keys(variant)[0];
61
+ }
62
+ /** The `session_meta` payload of a record, of a bare payload, or of the first in a list. */
63
+ function metaPayload(input) {
64
+ if (Array.isArray(input)) {
65
+ for (const item of input) {
66
+ const row = record(item);
67
+ if (row?.type === 'session_meta')
68
+ return record(row.payload);
69
+ }
70
+ return undefined;
71
+ }
72
+ const row = record(input);
73
+ if (!row)
74
+ return undefined;
75
+ if (row.type === 'session_meta')
76
+ return record(row.payload);
77
+ // A payload on its own, as `readSessionMeta()` returns it.
78
+ if (row.type === undefined && row.payload === undefined)
79
+ return row;
80
+ return undefined;
81
+ }
82
+ /**
83
+ * Origin of a Codex rollout from its `session_meta`. Accepts the parsed
84
+ * `session_meta` record, its payload, or the rollout's records in order (the
85
+ * first `session_meta` wins: a fork embeds its parent's after its own).
86
+ * Anything else yields `{ isSubagent: false }`.
87
+ */
88
+ function codexRolloutOrigin(input) {
89
+ const payload = metaPayload(input);
90
+ if (!payload)
91
+ return { isSubagent: false };
92
+ const forkedFromId = nonEmpty(payload.forked_from_id);
93
+ const object = sourceObject(payload.source);
94
+ const isSubagent = object !== undefined && 'subagent' in object;
95
+ const source = typeof payload.source === 'string' && object === undefined
96
+ ? nonEmpty(payload.source)
97
+ : isSubagent
98
+ ? 'subagent'
99
+ : undefined;
100
+ const parentId = forkedFromId ??
101
+ nonEmpty(codexSpawnParentFromSource(object)) ??
102
+ (isSubagent ? nonEmpty(payload.parent_thread_id) : undefined);
103
+ const subagentKind = isSubagent ? subagentKindOf(object?.subagent) : undefined;
104
+ return {
105
+ ...(forkedFromId ? { forkedFromId } : {}),
106
+ ...(source ? { source } : {}),
107
+ isSubagent,
108
+ ...(parentId ? { parentId } : {}),
109
+ ...(subagentKind ? { subagentKind } : {}),
110
+ };
111
+ }
@@ -12,6 +12,7 @@
12
12
  */
13
13
  import type { SessionEvent } from '../types/sessionEvent';
14
14
  import type { OpenCodeMessage, OpenCodePart, DbMessage, DbPart } from '../types/opencode';
15
+ import { type NormalizedUsage } from '../usageNormalization';
15
16
  /**
16
17
  * Normalizes an OpenCode tool name to PascalCase.
17
18
  *
@@ -38,6 +39,43 @@ export declare function detectPlanModeFromText(text: string): 'enter' | 'exit' |
38
39
  * This adds snake_case aliases without removing the originals.
39
40
  */
40
41
  export declare function normalizeToolInput(input: Record<string, unknown>): Record<string, unknown>;
42
+ /** OpenCode token counts as stored on an assistant message or step. */
43
+ export interface OpenCodeTokenCounts {
44
+ input?: number;
45
+ output?: number;
46
+ reasoning?: number;
47
+ cacheRead?: number;
48
+ cacheWrite?: number;
49
+ /** Provider-reported total, stored by OpenCode since late 2025. */
50
+ total?: number;
51
+ }
52
+ /**
53
+ * Token counts from a message in either storage shape: database rows are
54
+ * mapped to flat `cacheRead` / `cacheWrite`, while file-storage messages keep
55
+ * OpenCode's nested `cache: { read, write }`.
56
+ * @internal
57
+ */
58
+ export declare function openCodeTokenCounts(tokens: unknown): OpenCodeTokenCounts;
59
+ /**
60
+ * How a stored OpenCode message splits its tokens. Current OpenCode stores
61
+ * every bucket disjoint for every provider: `input` excludes cache reads and
62
+ * writes (since 2025-11) and `output` excludes `reasoning` (since 2026-04),
63
+ * with `total` equal to their sum. Older messages either counted cached input
64
+ * in `input` (OpenAI-compatible providers) or reasoning in `output`. The
65
+ * stored `total` tells the layouts apart; without it the current layout is
66
+ * assumed.
67
+ * @internal
68
+ */
69
+ export declare function openCodeTokenLayout(tokens: OpenCodeTokenCounts): {
70
+ inputIncludesCache: boolean;
71
+ reasoningInOutput: boolean;
72
+ };
73
+ /** Normalizes one OpenCode message's or step's stored tokens. @internal */
74
+ export declare function normalizeOpenCodeUsage(tokens: OpenCodeTokenCounts, providerID: string | undefined, extra?: {
75
+ source?: string;
76
+ model?: string;
77
+ reportedCostUsd?: number;
78
+ }): NormalizedUsage;
41
79
  /**
42
80
  * Converts an OpenCode message and its parts into SessionEvent array.
43
81
  *
@@ -15,10 +15,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.normalizeToolName = normalizeToolName;
16
16
  exports.detectPlanModeFromText = detectPlanModeFromText;
17
17
  exports.normalizeToolInput = normalizeToolInput;
18
+ exports.openCodeTokenCounts = openCodeTokenCounts;
19
+ exports.openCodeTokenLayout = openCodeTokenLayout;
20
+ exports.normalizeOpenCodeUsage = normalizeOpenCodeUsage;
18
21
  exports.convertOpenCodeMessage = convertOpenCodeMessage;
19
22
  exports.parseDbMessageData = parseDbMessageData;
20
23
  exports.parseDbPartData = parseDbPartData;
21
24
  const usageNormalization_1 = require("../usageNormalization");
25
+ const ownKey_1 = require("../ownKey");
22
26
  /**
23
27
  * OpenCode → Claude Code tool name mapping.
24
28
  * OpenCode uses lowercase names; downstream code expects PascalCase.
@@ -54,7 +58,7 @@ function normalizeToolName(name) {
54
58
  if (!name)
55
59
  return name;
56
60
  // Check mapping (case-insensitive lookup via lowercase key)
57
- const mapped = TOOL_NAME_MAP[name.toLowerCase()];
61
+ const mapped = (0, ownKey_1.ownValue)(TOOL_NAME_MAP, name.toLowerCase());
58
62
  if (mapped)
59
63
  return mapped;
60
64
  // Already PascalCase (starts with uppercase)? Pass through.
@@ -123,6 +127,78 @@ function toISOString(time) {
123
127
  return time;
124
128
  return new Date(time).toISOString();
125
129
  }
130
+ /**
131
+ * Token counts from a message in either storage shape: database rows are
132
+ * mapped to flat `cacheRead` / `cacheWrite`, while file-storage messages keep
133
+ * OpenCode's nested `cache: { read, write }`.
134
+ * @internal
135
+ */
136
+ function openCodeTokenCounts(tokens) {
137
+ const raw = (tokens ?? {});
138
+ const cache = (raw.cache ?? {});
139
+ const num = (value) => (typeof value === 'number' ? value : undefined);
140
+ return {
141
+ input: num(raw.input),
142
+ output: num(raw.output),
143
+ reasoning: num(raw.reasoning),
144
+ cacheRead: num(raw.cacheRead) ?? num(cache.read),
145
+ cacheWrite: num(raw.cacheWrite) ?? num(cache.write),
146
+ total: num(raw.total),
147
+ };
148
+ }
149
+ /**
150
+ * How a stored OpenCode message splits its tokens. Current OpenCode stores
151
+ * every bucket disjoint for every provider: `input` excludes cache reads and
152
+ * writes (since 2025-11) and `output` excludes `reasoning` (since 2026-04),
153
+ * with `total` equal to their sum. Older messages either counted cached input
154
+ * in `input` (OpenAI-compatible providers) or reasoning in `output`. The
155
+ * stored `total` tells the layouts apart; without it the current layout is
156
+ * assumed.
157
+ * @internal
158
+ */
159
+ function openCodeTokenLayout(tokens) {
160
+ const n = (value) => typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : 0;
161
+ const input = n(tokens.input);
162
+ const output = n(tokens.output);
163
+ const reasoning = n(tokens.reasoning);
164
+ const cache = n(tokens.cacheRead) + n(tokens.cacheWrite);
165
+ const total = n(tokens.total);
166
+ if (total > 0 && total !== input + cache + output + reasoning) {
167
+ if (reasoning > 0 && total === input + cache + output) {
168
+ return { inputIncludesCache: false, reasoningInOutput: true };
169
+ }
170
+ if (cache > 0 && total === input + output + reasoning) {
171
+ return { inputIncludesCache: true, reasoningInOutput: false };
172
+ }
173
+ if (cache > 0 && reasoning > 0 && total === input + output) {
174
+ return { inputIncludesCache: true, reasoningInOutput: true };
175
+ }
176
+ }
177
+ return { inputIncludesCache: false, reasoningInOutput: false };
178
+ }
179
+ /** Normalizes one OpenCode message's or step's stored tokens. @internal */
180
+ function normalizeOpenCodeUsage(tokens, providerID, extra = {}) {
181
+ const layout = openCodeTokenLayout(tokens);
182
+ return (0, usageNormalization_1.normalizeProviderUsage)({
183
+ // `openai` semantics subtract cache reads from input; only an old
184
+ // cache-inclusive message needs that.
185
+ semantics: layout.inputIncludesCache
186
+ ? 'openai'
187
+ : providerID === 'anthropic'
188
+ ? 'anthropic'
189
+ : 'sidekick',
190
+ provider: providerID ?? 'opencode',
191
+ ...(extra.source ? { source: extra.source } : {}),
192
+ ...(extra.model ? { model: extra.model } : {}),
193
+ inputTokens: tokens.input,
194
+ outputTokens: tokens.output,
195
+ cacheReadTokens: tokens.cacheRead,
196
+ cacheWriteTokens: tokens.cacheWrite,
197
+ reasoningTokens: tokens.reasoning,
198
+ reasoningIncludedInOutput: layout.reasoningInOutput,
199
+ ...(extra.reportedCostUsd !== undefined ? { reportedCostUsd: extra.reportedCostUsd } : {}),
200
+ });
201
+ }
126
202
  /**
127
203
  * Converts an OpenCode message and its parts into SessionEvent array.
128
204
  *
@@ -328,31 +404,20 @@ function convertAssistantMessage(message, parts) {
328
404
  : stepFinishCost > 0
329
405
  ? stepFinishCost
330
406
  : undefined;
331
- const usage = {
332
- input_tokens: message.tokens.input || 0,
333
- output_tokens: message.tokens.output || 0,
334
- cache_creation_input_tokens: message.tokens.cacheWrite || 0,
335
- cache_read_input_tokens: message.tokens.cacheRead || 0,
336
- reported_cost: reportedCost,
337
- reasoning_tokens: message.tokens.reasoning || 0,
338
- };
339
- const normalizedUsage = (0, usageNormalization_1.normalizeProviderUsage)({
340
- semantics: message.providerID === 'openai'
341
- ? 'openai'
342
- : message.providerID === 'anthropic'
343
- ? 'anthropic'
344
- : 'sidekick',
345
- provider: message.providerID ?? 'opencode',
407
+ const counts = openCodeTokenCounts(message.tokens);
408
+ const normalizedUsage = normalizeOpenCodeUsage(counts, message.providerID, {
346
409
  source: 'opencode-message',
347
410
  model: message.modelID,
348
- inputTokens: message.tokens.input,
349
- outputTokens: message.tokens.output,
350
- cacheReadTokens: message.tokens.cacheRead,
351
- cacheWriteTokens: message.tokens.cacheWrite,
352
- reasoningTokens: message.tokens.reasoning,
353
- reasoningIncludedInOutput: message.providerID === 'openai',
354
411
  reportedCostUsd: reportedCost,
355
412
  });
413
+ const usage = {
414
+ input_tokens: normalizedUsage.uncachedInputTokens,
415
+ output_tokens: counts.output || 0,
416
+ cache_creation_input_tokens: counts.cacheWrite || 0,
417
+ cache_read_input_tokens: counts.cacheRead || 0,
418
+ reported_cost: reportedCost,
419
+ reasoning_tokens: counts.reasoning || 0,
420
+ };
356
421
  const timestamp = toISOString(message.time.completed || message.time.created);
357
422
  const retryAttempts = parts
358
423
  .filter((part) => part.type === 'retry')
@@ -491,6 +556,7 @@ function parseDbMessageData(row) {
491
556
  cacheRead: cache?.read || 0,
492
557
  cacheWrite: cache?.write || 0,
493
558
  reasoning: tokens?.reasoning || 0,
559
+ ...(typeof tokens?.total === 'number' ? { total: tokens.total } : {}),
494
560
  },
495
561
  time: {
496
562
  created: time?.created || row.time_created,
@@ -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.