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
@@ -46,7 +46,8 @@ exports.installCredentialGuard = installCredentialGuard;
46
46
  * - `HOME` (and the Windows equivalents) point at a per-process temp dir,
47
47
  * removed on exit, and the home overrides (`CLAUDE_CONFIG_DIR`,
48
48
  * `CLAUDE_SECURESTORAGE_CONFIG_DIR`, `CODEX_HOME`, `SIDEKICK_CONFIG_DIR`)
49
- * are unset, so every `os.homedir()`-derived path is isolated structurally;
49
+ * are unset (unless named in `preserveEnv` and pointed at a fixture), so
50
+ * every `os.homedir()`-derived path is isolated structurally;
50
51
  * - the real `child_process` module object is patched, which reaches both ESM
51
52
  * named imports (after `syncBuiltinESMExports`) and CommonJS code loaded
52
53
  * through native `require`; per-file `vi.mock('child_process')` factories
@@ -64,6 +65,13 @@ const os = __importStar(require("node:os"));
64
65
  const path = __importStar(require("node:path"));
65
66
  const GUARD_KEY = '__sidekickCredentialGuard';
66
67
  const DEFAULT_BLOCKED_BINARIES = ['security', 'claude', 'codex'];
68
+ /** Home overrides the guard unsets; only these can be preserved. */
69
+ const SCRUBBED_ENV = [
70
+ 'CLAUDE_CONFIG_DIR',
71
+ 'CLAUDE_SECURESTORAGE_CONFIG_DIR',
72
+ 'CODEX_HOME',
73
+ 'SIDEKICK_CONFIG_DIR',
74
+ ];
67
75
  const CHILD_PROCESS_FUNCTIONS = [
68
76
  'spawn',
69
77
  'spawnSync',
@@ -111,6 +119,8 @@ function installCredentialGuard(options = {}) {
111
119
  // os.userInfo() reads the passwd entry, so it still names the real home
112
120
  // after HOME is redirected below.
113
121
  const realHome = os.userInfo().homedir;
122
+ // Validated before any side effect, so a refused install changes nothing.
123
+ const preserved = checkPreservedEnv(options, realHome, realFs);
114
124
  const testHome = realFs.mkdtempSync(path.join(os.tmpdir(), 'sidekick-test-home-'));
115
125
  const removeTestHome = realFs.rmSync.bind(realFs);
116
126
  process.once('exit', () => {
@@ -139,10 +149,9 @@ function installCredentialGuard(options = {}) {
139
149
  process.env.USERPROFILE = testHome;
140
150
  process.env.APPDATA = path.join(testHome, 'AppData', 'Roaming');
141
151
  process.env.LOCALAPPDATA = path.join(testHome, 'AppData', 'Local');
142
- delete process.env.CLAUDE_CONFIG_DIR;
143
- delete process.env.CLAUDE_SECURESTORAGE_CONFIG_DIR;
144
- delete process.env.CODEX_HOME;
145
- delete process.env.SIDEKICK_CONFIG_DIR;
152
+ for (const name of SCRUBBED_ENV)
153
+ if (!preserved.has(name))
154
+ delete process.env[name];
146
155
  const protectedRoots = path.resolve(realHome) === path.resolve(testHome)
147
156
  ? []
148
157
  : [
@@ -244,6 +253,79 @@ function installCredentialGuard(options = {}) {
244
253
  globalThis[GUARD_KEY] = state;
245
254
  return state;
246
255
  }
256
+ /** The `preserveEnv` names, after refusing unknown names and unsafe values. */
257
+ function checkPreservedEnv(options, realHome, realFs) {
258
+ const preserved = new Set();
259
+ for (const name of options.preserveEnv ?? []) {
260
+ if (!SCRUBBED_ENV.includes(name)) {
261
+ throw new Error(`installCredentialGuard: preserveEnv accepts only ${SCRUBBED_ENV.join(', ')}; got ${JSON.stringify(name)}.`);
262
+ }
263
+ preserved.add(name);
264
+ }
265
+ if (preserved.size === 0)
266
+ return preserved;
267
+ const roots = [
268
+ path.join(realHome, '.claude'),
269
+ path.join(realHome, '.claude.json'),
270
+ path.join(realHome, '.codex'),
271
+ path.join(realHome, '.config', 'sidekick'),
272
+ // The Windows default config dir, read before HOME and APPDATA move.
273
+ ...(process.platform === 'win32' && process.env.APPDATA
274
+ ? [path.join(process.env.APPDATA, 'sidekick')]
275
+ : []),
276
+ ...(options.protectedPaths ?? []),
277
+ ];
278
+ for (const name of preserved) {
279
+ const value = process.env[name]?.trim();
280
+ if (!value)
281
+ continue;
282
+ const hit = protectedLocation(value, roots, realFs);
283
+ if (hit) {
284
+ throw new Error(`installCredentialGuard: preserveEnv ${name}=${value} reaches the real protected location ${hit}. Point it at a temporary fixture instead.`);
285
+ }
286
+ }
287
+ return preserved;
288
+ }
289
+ /** The root `target` equals, contains, or lies inside, comparing plain and symlink-resolved paths. */
290
+ function protectedLocation(target, roots, realFs) {
291
+ // Case-insensitive by default on macOS and Windows; folding can only over-refuse.
292
+ const fold = process.platform === 'darwin' || process.platform === 'win32';
293
+ const normal = (value) => {
294
+ const resolved = path.resolve(value);
295
+ return fold ? resolved.toLowerCase() : resolved;
296
+ };
297
+ const within = (parent, child) => child === parent || child.startsWith(parent.endsWith(path.sep) ? parent : parent + path.sep);
298
+ const related = (a, b) => within(a, b) || within(b, a);
299
+ // Plain paths first, so an obviously unsafe value never reaches the filesystem.
300
+ const plain = normal(target);
301
+ for (const root of roots)
302
+ if (related(normal(root), plain))
303
+ return root;
304
+ const real = normal(realpathOfNearest(target, realFs));
305
+ for (const root of roots) {
306
+ const realRoot = normal(realpathOfNearest(root, realFs));
307
+ if (related(realRoot, real) || related(normal(root), real) || related(realRoot, plain))
308
+ return root;
309
+ }
310
+ return null;
311
+ }
312
+ /** `realpath` of the nearest existing ancestor, with the missing tail re-appended. */
313
+ function realpathOfNearest(target, realFs) {
314
+ let current = path.resolve(target);
315
+ let tail = [];
316
+ for (;;) {
317
+ try {
318
+ return path.join(realFs.realpathSync.native(current), ...tail);
319
+ }
320
+ catch {
321
+ const parent = path.dirname(current);
322
+ if (parent === current)
323
+ return path.resolve(target);
324
+ tail = [path.basename(current), ...tail];
325
+ current = parent;
326
+ }
327
+ }
328
+ }
247
329
  /**
248
330
  * The real module object of a Node built-in: `process.getBuiltinModule`
249
331
  * (Node 20.16+), or `require` in CommonJS on older Node.
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Locates the session record that issued each of a set of tool calls, by id.
3
+ *
4
+ * Given Claude Code `tool_use` ids (`toolu_…`) and/or Codex `call_id`s, finds
5
+ * the session file, session id and time of each call, plus the ordinal of the
6
+ * human prompt the call answered, so a downstream record (an edit, a commit)
7
+ * can be tied back to the prompt that caused it. Scope, listing, session keys
8
+ * and ordinals are the ones `collectPromptHistory()` uses, so results join
9
+ * directly with prompt-history entries.
10
+ *
11
+ * Cost: one listing of the workspace-scoped session files (`since` prunes by
12
+ * modification time), then a streaming pass over each candidate file in which
13
+ * a line is only parsed when it contains one of the wanted ids. Claude Code
14
+ * subagent transcripts of each session are searched too. Stops as soon as
15
+ * every id is found. Prompt ordinals are read only for files that matched.
16
+ *
17
+ * Node-only: reads session files.
18
+ *
19
+ * @module toolCallOrigins
20
+ */
21
+ import { type PromptHistoryProvider } from './promptHistory';
22
+ export interface FindToolCallOriginsOptions {
23
+ /** Tool-call ids to locate: Claude `tool_use` ids and/or Codex `call_id`s. */
24
+ ids: Iterable<string>;
25
+ /** Workspace roots that scope the search (as for `collectPromptHistory`). */
26
+ workspacePaths: string[];
27
+ /** Also search the roots' git worktree siblings. Default true. */
28
+ includeWorktrees?: boolean;
29
+ /** Only session files modified at or after this instant. */
30
+ since?: Date;
31
+ /** Default both. */
32
+ providers?: PromptHistoryProvider[];
33
+ /** Stops between files and lines; the promise rejects with the signal's reason. */
34
+ signal?: AbortSignal;
35
+ }
36
+ export interface ToolCallOrigin {
37
+ provider: PromptHistoryProvider;
38
+ /**
39
+ * Session whose prompt history `afterOrdinal` counts in: the session itself,
40
+ * or its parent for a subagent or fork (Claude subagent transcripts live
41
+ * under the parent; a Codex subagent or fork has its own rollout, named in
42
+ * `subagent.agentId` / `filePath`, and no prompts of its own).
43
+ */
44
+ sessionId: string;
45
+ /** Same key `collectPromptHistory()` reports for that session's file. */
46
+ sessionKey: string;
47
+ /** File the call was recorded in (a subagent transcript for subagent calls). */
48
+ filePath: string;
49
+ /** ISO timestamp of the record that issued the call. */
50
+ timestamp: string;
51
+ /**
52
+ * Ordinal of the last human prompt at or before the call, in the session's
53
+ * prompt history; null when the call precedes every prompt or the session
54
+ * has none (e.g. a Codex subagent whose parent was not found in scope).
55
+ */
56
+ afterOrdinal: number | null;
57
+ toolName?: string;
58
+ /** Set when a subagent issued the call. */
59
+ subagent?: {
60
+ agentId: string;
61
+ parentSessionId: string;
62
+ };
63
+ }
64
+ /**
65
+ * Finds where each tool call was issued. Ids that are not found are absent
66
+ * from the result. When an id appears more than once (a forked or resumed
67
+ * session can replay history), the earliest record wins, and a session's own
68
+ * record wins over a fork's copy at the same instant.
69
+ */
70
+ export declare function findToolCallOrigins(options: FindToolCallOriginsOptions): Promise<Map<string, ToolCallOrigin>>;
@@ -0,0 +1,346 @@
1
+ "use strict";
2
+ /**
3
+ * Locates the session record that issued each of a set of tool calls, by id.
4
+ *
5
+ * Given Claude Code `tool_use` ids (`toolu_…`) and/or Codex `call_id`s, finds
6
+ * the session file, session id and time of each call, plus the ordinal of the
7
+ * human prompt the call answered, so a downstream record (an edit, a commit)
8
+ * can be tied back to the prompt that caused it. Scope, listing, session keys
9
+ * and ordinals are the ones `collectPromptHistory()` uses, so results join
10
+ * directly with prompt-history entries.
11
+ *
12
+ * Cost: one listing of the workspace-scoped session files (`since` prunes by
13
+ * modification time), then a streaming pass over each candidate file in which
14
+ * a line is only parsed when it contains one of the wanted ids. Claude Code
15
+ * subagent transcripts of each session are searched too. Stops as soon as
16
+ * every id is found. Prompt ordinals are read only for files that matched.
17
+ *
18
+ * Node-only: reads session files.
19
+ *
20
+ * @module toolCallOrigins
21
+ */
22
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ var desc = Object.getOwnPropertyDescriptor(m, k);
25
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
26
+ desc = { enumerable: true, get: function() { return m[k]; } };
27
+ }
28
+ Object.defineProperty(o, k2, desc);
29
+ }) : (function(o, m, k, k2) {
30
+ if (k2 === undefined) k2 = k;
31
+ o[k2] = m[k];
32
+ }));
33
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
34
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
35
+ }) : function(o, v) {
36
+ o["default"] = v;
37
+ });
38
+ var __importStar = (this && this.__importStar) || (function () {
39
+ var ownKeys = function(o) {
40
+ ownKeys = Object.getOwnPropertyNames || function (o) {
41
+ var ar = [];
42
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
43
+ return ar;
44
+ };
45
+ return ownKeys(o);
46
+ };
47
+ return function (mod) {
48
+ if (mod && mod.__esModule) return mod;
49
+ var result = {};
50
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
51
+ __setModuleDefault(result, mod);
52
+ return result;
53
+ };
54
+ })();
55
+ Object.defineProperty(exports, "__esModule", { value: true });
56
+ exports.findToolCallOrigins = findToolCallOrigins;
57
+ const fs = __importStar(require("fs"));
58
+ const path = __importStar(require("path"));
59
+ const readline = __importStar(require("readline"));
60
+ const promptHistory_1 = require("./promptHistory");
61
+ const codexRolloutOrigin_1 = require("./parsers/codexRolloutOrigin");
62
+ const workspaceRoots_1 = require("./workspaceRoots");
63
+ /**
64
+ * Finds where each tool call was issued. Ids that are not found are absent
65
+ * from the result. When an id appears more than once (a forked or resumed
66
+ * session can replay history), the earliest record wins, and a session's own
67
+ * record wins over a fork's copy at the same instant.
68
+ */
69
+ async function findToolCallOrigins(options) {
70
+ const wanted = new Set([...options.ids].filter((id) => typeof id === 'string' && id.length > 0));
71
+ const found = new Map();
72
+ if (wanted.size === 0)
73
+ return found;
74
+ throwIfAborted(options.signal);
75
+ const realpaths = new Map();
76
+ const { roots, listingRoots } = (0, promptHistory_1.resolveRoots)(options.workspacePaths, options.includeWorktrees !== false, realpaths);
77
+ if (roots.length === 0)
78
+ return found;
79
+ const providers = options.providers ?? ['claude-code', 'codex'];
80
+ const sinceMs = options.since ? options.since.getTime() : null;
81
+ const candidates = await (0, promptHistory_1.listCandidates)(providers, listingRoots, sinceMs);
82
+ const byProviderSession = new Map();
83
+ for (const candidate of candidates) {
84
+ byProviderSession.set(`${candidate.provider}:${candidate.sessionId}`, candidate);
85
+ }
86
+ const marksCache = new Map();
87
+ const marksFor = (candidate) => {
88
+ let marks = marksCache.get(candidate.key);
89
+ if (!marks) {
90
+ marks = (0, promptHistory_1.readPromptOrdinalMarks)(candidate.provider, candidate.sessionId, candidate.filePath).catch(() => []);
91
+ marksCache.set(candidate.key, marks);
92
+ }
93
+ return marks;
94
+ };
95
+ const remaining = new Set(wanted);
96
+ // Best timestamp seen per id, to prefer the earliest record across files.
97
+ const bestMs = new Map();
98
+ for (const candidate of candidates) {
99
+ if (remaining.size === 0)
100
+ break;
101
+ throwIfAborted(options.signal);
102
+ const targets = [{ candidate, filePath: candidate.filePath }];
103
+ if (candidate.provider === 'claude-code') {
104
+ for (const sub of claudeSubagentTranscripts(candidate.filePath, candidate.sessionId)) {
105
+ targets.push({ candidate, filePath: sub.filePath, agentId: sub.agentId });
106
+ }
107
+ }
108
+ // A Codex fork or spawned subagent answers its parent's prompts.
109
+ let codexParent;
110
+ if (candidate.provider === 'codex') {
111
+ const origin = (0, codexRolloutOrigin_1.codexRolloutOrigin)(await readFirstRecord(candidate.filePath));
112
+ if (origin.parentId)
113
+ codexParent = { id: origin.parentId, fork: !origin.isSubagent };
114
+ }
115
+ for (const target of targets) {
116
+ const hits = await scanForIds(target.filePath, candidate.provider, remaining, roots, realpaths, options.signal);
117
+ for (const hit of hits) {
118
+ const isFork = codexParent?.fork === true;
119
+ const previous = bestMs.get(hit.id);
120
+ if (previous &&
121
+ (previous.ms < hit.timestampMs ||
122
+ (previous.ms === hit.timestampMs && (!previous.fork || isFork)))) {
123
+ continue;
124
+ }
125
+ bestMs.set(hit.id, { ms: hit.timestampMs, fork: isFork });
126
+ let subagent;
127
+ if (target.agentId) {
128
+ subagent = { agentId: target.agentId, parentSessionId: candidate.sessionId };
129
+ }
130
+ else if (codexParent && !codexParent.fork) {
131
+ subagent = { agentId: candidate.sessionId, parentSessionId: codexParent.id };
132
+ }
133
+ // Ordinals come from the session whose prompts the call answered: a
134
+ // Codex fork or subagent has none of its own, so its parent's count.
135
+ const parentSession = codexParent
136
+ ? byProviderSession.get(`codex:${codexParent.id}`)
137
+ : undefined;
138
+ const session = parentSession ?? candidate;
139
+ const afterOrdinal = !codexParent || parentSession
140
+ ? ordinalAt(await marksFor(session), hit.timestampMs)
141
+ : null;
142
+ found.set(hit.id, {
143
+ provider: candidate.provider,
144
+ sessionId: session.sessionId,
145
+ sessionKey: session.key,
146
+ filePath: target.filePath,
147
+ timestamp: hit.timestamp,
148
+ afterOrdinal,
149
+ ...(hit.toolName ? { toolName: hit.toolName } : {}),
150
+ ...(subagent ? { subagent } : {}),
151
+ });
152
+ // A fork's copy might be replaced by the original later; keep looking.
153
+ if (!isFork)
154
+ remaining.delete(hit.id);
155
+ }
156
+ if (remaining.size === 0)
157
+ break;
158
+ }
159
+ }
160
+ return found;
161
+ }
162
+ /** Highest ordinal among prompts at or before `ms`; marks are in file order. */
163
+ function ordinalAt(marks, ms) {
164
+ let best = null;
165
+ for (const mark of marks) {
166
+ if (Number.isFinite(mark.timestampMs) && mark.timestampMs <= ms) {
167
+ if (best === null || mark.ordinal > best)
168
+ best = mark.ordinal;
169
+ }
170
+ }
171
+ return best;
172
+ }
173
+ function claudeSubagentTranscripts(sessionFile, sessionId) {
174
+ const dir = path.join(path.dirname(sessionFile), sessionId, 'subagents');
175
+ let names;
176
+ try {
177
+ names = fs.readdirSync(dir);
178
+ }
179
+ catch {
180
+ return [];
181
+ }
182
+ return names
183
+ .filter((name) => name.endsWith('.jsonl'))
184
+ .sort()
185
+ .map((name) => ({
186
+ filePath: path.join(dir, name),
187
+ agentId: path.basename(name, '.jsonl').replace(/^agent-/, ''),
188
+ }));
189
+ }
190
+ async function readFirstRecord(filePath) {
191
+ let handle;
192
+ try {
193
+ handle = await fs.promises.open(filePath, 'r');
194
+ const buffer = Buffer.alloc(256 * 1024);
195
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
196
+ const text = buffer.subarray(0, bytesRead).toString('utf8');
197
+ const newline = text.indexOf('\n');
198
+ return JSON.parse(newline === -1 ? text : text.slice(0, newline));
199
+ }
200
+ catch {
201
+ return undefined;
202
+ }
203
+ finally {
204
+ await handle?.close();
205
+ }
206
+ }
207
+ /** Fail-closed scope check on a recorded working directory. */
208
+ function inScope(cwd, roots, realpaths) {
209
+ if (typeof cwd !== 'string' || cwd.length === 0)
210
+ return false;
211
+ const real = (0, workspaceRoots_1.realpathOrNull)(cwd, realpaths);
212
+ if (!real)
213
+ return false;
214
+ return roots.some((root) => {
215
+ if (real === root)
216
+ return true;
217
+ const prefix = root.endsWith(path.sep) ? root : root + path.sep;
218
+ return real.startsWith(prefix);
219
+ });
220
+ }
221
+ const ID_PATTERN = /"(?:id|call_id)"\s*:\s*"([^"\\]{1,256})"/g;
222
+ async function scanForIds(filePath, provider, wanted, roots, realpaths, signal) {
223
+ const hits = [];
224
+ let stream;
225
+ try {
226
+ stream = fs.createReadStream(filePath, { encoding: 'utf8' });
227
+ }
228
+ catch {
229
+ return hits;
230
+ }
231
+ const lines = readline.createInterface({ input: stream, crlfDelay: Infinity });
232
+ // Codex records a working directory per session/turn, not per line.
233
+ let codexCwd;
234
+ try {
235
+ for await (const line of lines) {
236
+ throwIfAborted(signal);
237
+ if (provider === 'codex' && line.includes('"cwd"')) {
238
+ const cwd = codexCwdOf(line);
239
+ if (cwd !== undefined)
240
+ codexCwd = cwd;
241
+ }
242
+ let match = false;
243
+ for (const found of line.matchAll(ID_PATTERN)) {
244
+ if (wanted.has(found[1])) {
245
+ match = true;
246
+ break;
247
+ }
248
+ }
249
+ if (!match)
250
+ continue;
251
+ let record;
252
+ try {
253
+ record = JSON.parse(line);
254
+ }
255
+ catch {
256
+ continue;
257
+ }
258
+ const lineHits = provider === 'claude-code' ? claudeToolUses(record, wanted) : codexToolCall(record, wanted);
259
+ if (lineHits.length === 0)
260
+ continue;
261
+ const cwd = provider === 'claude-code' ? record.cwd : codexCwd;
262
+ if (!inScope(cwd, roots, realpaths))
263
+ continue;
264
+ hits.push(...lineHits);
265
+ }
266
+ }
267
+ catch (error) {
268
+ if (signal?.aborted)
269
+ throw error;
270
+ // Unreadable or vanished file: keep what was found.
271
+ }
272
+ finally {
273
+ lines.close();
274
+ stream.destroy();
275
+ }
276
+ return hits;
277
+ }
278
+ function codexCwdOf(line) {
279
+ try {
280
+ const record = JSON.parse(line);
281
+ if (record.type === 'session_meta' || record.type === 'turn_context') {
282
+ return record.payload?.cwd;
283
+ }
284
+ }
285
+ catch {
286
+ // Not a complete record.
287
+ }
288
+ return undefined;
289
+ }
290
+ function timeOf(record) {
291
+ const timestamp = record.timestamp;
292
+ if (typeof timestamp !== 'string')
293
+ return null;
294
+ const timestampMs = Date.parse(timestamp);
295
+ return Number.isFinite(timestampMs) ? { timestamp, timestampMs } : null;
296
+ }
297
+ function claudeToolUses(record, wanted) {
298
+ const row = record;
299
+ if (row?.type !== 'assistant' || !Array.isArray(row.message?.content))
300
+ return [];
301
+ const time = timeOf(record);
302
+ if (!time)
303
+ return [];
304
+ const hits = [];
305
+ for (const block of row.message.content) {
306
+ const item = block;
307
+ if ((item?.type === 'tool_use' || item?.type === 'server_tool_use') &&
308
+ typeof item.id === 'string' &&
309
+ wanted.has(item.id)) {
310
+ hits.push({
311
+ id: item.id,
312
+ ...time,
313
+ ...(typeof item.name === 'string' ? { toolName: item.name } : {}),
314
+ });
315
+ }
316
+ }
317
+ return hits;
318
+ }
319
+ function codexToolCall(record, wanted) {
320
+ const row = record;
321
+ const payloadType = row?.payload?.type;
322
+ if (row?.type !== 'response_item' ||
323
+ typeof payloadType !== 'string' ||
324
+ !payloadType.endsWith('_call')) {
325
+ return [];
326
+ }
327
+ const callId = row.payload?.call_id;
328
+ if (typeof callId !== 'string' || !wanted.has(callId))
329
+ return [];
330
+ const time = timeOf(record);
331
+ if (!time)
332
+ return [];
333
+ const name = row.payload?.name;
334
+ return [
335
+ {
336
+ id: callId,
337
+ ...time,
338
+ toolName: typeof name === 'string' ? name : payloadType.replace(/_call$/, ''),
339
+ },
340
+ ];
341
+ }
342
+ function throwIfAborted(signal) {
343
+ if (signal?.aborted) {
344
+ throw (signal.reason ?? Object.assign(new Error('The operation was aborted'), { name: 'AbortError' }));
345
+ }
346
+ }
@@ -32,6 +32,13 @@ export interface CanonicalTranscriptBlock {
32
32
  command?: string;
33
33
  truncated?: boolean;
34
34
  raw?: unknown;
35
+ /**
36
+ * The provider's own tool name before normalization (for example Codex
37
+ * `exec_command` behind `Bash`), when the parser recorded one.
38
+ */
39
+ rawToolName?: string;
40
+ /** The MCP server that served this tool call, when the parser recorded one. */
41
+ mcpServerName?: string;
35
42
  }
36
43
  export interface CanonicalTranscriptMessage {
37
44
  id?: string;
@@ -51,10 +58,15 @@ export interface CanonicalToolCall {
51
58
  id?: string;
52
59
  name: string;
53
60
  timestamp: string;
61
+ /** Tool input without the parsers' internal `_sidekick*` bookkeeping keys. */
54
62
  input?: unknown;
55
63
  output?: unknown;
56
64
  isError?: boolean;
57
65
  command?: string;
66
+ /** The provider's own tool name before normalization, when recorded. */
67
+ rawToolName?: string;
68
+ /** The MCP server that served this tool call, when recorded. */
69
+ mcpServerName?: string;
58
70
  source: TranscriptSourceProvenance;
59
71
  }
60
72
  export interface CanonicalTranscriptUsageTotals {
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.projectSessionTranscript = projectSessionTranscript;
4
4
  const usageNormalization_1 = require("./usageNormalization");
5
+ const subagentSpawn_1 = require("./subagentSpawn");
5
6
  const EMPTY_TOTALS = {
6
7
  uncachedInputTokens: 0,
7
8
  cacheReadTokens: 0,
@@ -28,6 +29,9 @@ function projectSessionTranscript(events, options = {}) {
28
29
  let cwd;
29
30
  let gitBranch;
30
31
  events.forEach((event, eventIndex) => {
32
+ // A reader's synthetic notice that a background task finished, not a message.
33
+ if ((0, subagentSpawn_1.isTaskNotificationSystemEvent)(event))
34
+ return;
31
35
  if (nonEmpty(event.cwd))
32
36
  cwd = event.cwd;
33
37
  if (nonEmpty(event.gitBranch))
@@ -66,10 +70,12 @@ function projectSessionTranscript(events, options = {}) {
66
70
  blocks.push({ type: 'text', ...boundedText(event.summary, fidelity, maxContentChars) });
67
71
  }
68
72
  if (event.type === 'tool_use') {
73
+ const projected = projectToolInput(event.tool.input);
69
74
  blocks.push({
70
75
  type: 'tool_use',
71
76
  toolName: event.tool.name,
72
- input: boundedValue(event.tool.input, fidelity, maxContentChars),
77
+ input: boundedValue(projected.input, fidelity, maxContentChars),
78
+ ...toolIdentity(projected),
73
79
  });
74
80
  }
75
81
  if (event.type === 'tool_result') {
@@ -90,6 +96,7 @@ function projectSessionTranscript(events, options = {}) {
90
96
  timestamp: event.timestamp,
91
97
  input: block.input,
92
98
  command,
99
+ ...toolIdentity(block),
93
100
  source,
94
101
  };
95
102
  tools.push(call);
@@ -186,12 +193,14 @@ function contentBlocks(content, fidelity, maxChars, includeUnknown = false) {
186
193
  });
187
194
  }
188
195
  else if (block.type === 'tool_use' && typeof block.name === 'string') {
196
+ const projected = projectToolInput(block.input);
189
197
  result.push({
190
198
  type: 'tool_use',
191
199
  toolName: block.name,
192
200
  toolUseId: typeof block.id === 'string' ? block.id : undefined,
193
- input: boundedValue(block.input, fidelity, maxChars),
194
- command: commandFromTool(block.name, block.input),
201
+ input: boundedValue(projected.input, fidelity, maxChars),
202
+ command: commandFromTool(block.name, projected.input),
203
+ ...toolIdentity(projected),
195
204
  });
196
205
  }
197
206
  else if (block.type === 'tool_result') {
@@ -215,6 +224,40 @@ function contentBlocks(content, fidelity, maxChars, includeUnknown = false) {
215
224
  }
216
225
  return result;
217
226
  }
227
+ /** Prefix of the bookkeeping keys session parsers stash inside tool inputs. */
228
+ const SIDEKICK_INPUT_KEY_PREFIX = '_sidekick';
229
+ /**
230
+ * Lifts the parsers' `_sidekickRawToolName` / `_sidekickMcpServerName` out of
231
+ * a tool input and drops every other top-level `_sidekick*` key. Returns a
232
+ * copy when anything is removed; the source event is never modified.
233
+ */
234
+ function projectToolInput(input) {
235
+ if (!input || typeof input !== 'object' || Array.isArray(input))
236
+ return { input };
237
+ const source = input;
238
+ const keys = Object.keys(source);
239
+ if (!keys.some((key) => key.startsWith(SIDEKICK_INPUT_KEY_PREFIX)))
240
+ return { input };
241
+ const clean = {};
242
+ for (const key of keys) {
243
+ if (!key.startsWith(SIDEKICK_INPUT_KEY_PREFIX))
244
+ clean[key] = source[key];
245
+ }
246
+ return {
247
+ input: clean,
248
+ rawToolName: nonEmptyString(source._sidekickRawToolName),
249
+ mcpServerName: nonEmptyString(source._sidekickMcpServerName),
250
+ };
251
+ }
252
+ function toolIdentity(value) {
253
+ return {
254
+ ...(value.rawToolName ? { rawToolName: value.rawToolName } : {}),
255
+ ...(value.mcpServerName ? { mcpServerName: value.mcpServerName } : {}),
256
+ };
257
+ }
258
+ function nonEmptyString(value) {
259
+ return typeof value === 'string' && value.trim().length > 0 ? value : undefined;
260
+ }
218
261
  function boundedText(text, fidelity, maxChars) {
219
262
  if (fidelity === 'full' || text.length <= maxChars)
220
263
  return { text, truncated: false };
@@ -70,6 +70,7 @@ export interface ReasoningSummary {
70
70
  body: string;
71
71
  }
72
72
  export declare function reasoningSummary(text: string): ReasoningSummary;
73
+ /** Whether a tool call spawns a subagent (`Agent`, or its earlier name `Task`). */
73
74
  export declare function isAssistantTurnSubagentTool(toolName: string | undefined): boolean;
74
75
  export declare function segmentAssistantTurn(events: readonly AssistantTurnEvent[], options?: SegmentAssistantTurnOptions): AssistantTurnProjection;
75
76
  export declare function assistantTurnEventsFromSessionEvents(events: readonly SessionEvent[]): AssistantTurnEvent[];