sidekick-shared 0.29.0 → 0.30.0

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 (142) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +198 -56
  3. package/dist/aggregation/EventAggregator.d.ts +96 -2
  4. package/dist/aggregation/EventAggregator.js +535 -42
  5. package/dist/aggregation/snapshot.d.ts +37 -8
  6. package/dist/aggregation/snapshot.js +31 -9
  7. package/dist/aggregation/types.d.ts +38 -1
  8. package/dist/browser.d.ts +9 -1
  9. package/dist/browser.js +29 -3
  10. package/dist/context/sessionContext.js +8 -0
  11. package/dist/esm/aggregation/EventAggregator.js +535 -42
  12. package/dist/esm/aggregation/snapshot.js +31 -9
  13. package/dist/esm/browser.js +3 -0
  14. package/dist/esm/context/sessionContext.js +8 -0
  15. package/dist/esm/fileContinuity.js +149 -0
  16. package/dist/esm/formatters/toolSummary.js +4 -1
  17. package/dist/esm/index.js +7 -2
  18. package/dist/esm/modelCatalog.js +5 -4
  19. package/dist/esm/modelContext.js +4 -3
  20. package/dist/esm/modelInfo.js +4 -3
  21. package/dist/esm/node.js +3 -1
  22. package/dist/esm/observedContextWindows.js +2 -1
  23. package/dist/esm/ownKey.js +26 -0
  24. package/dist/esm/parsers/codexParser.js +372 -7
  25. package/dist/esm/parsers/openCodeParser.js +2 -1
  26. package/dist/esm/parsers/subagentEndState.js +61 -0
  27. package/dist/esm/parsers/subagentScanner.js +37 -12
  28. package/dist/esm/parsers/subagentTraceParser.js +103 -37
  29. package/dist/esm/pricingCatalog.js +3 -2
  30. package/dist/esm/promptHistory.js +22 -11
  31. package/dist/esm/providers/claudeCode.js +60 -29
  32. package/dist/esm/providers/codex.js +235 -201
  33. package/dist/esm/providers/codexCompressedReader.js +184 -0
  34. package/dist/esm/providers/codexDatabase.js +47 -2
  35. package/dist/esm/providers/codexRollouts.js +20 -2
  36. package/dist/esm/providers/codexSubagents.js +456 -0
  37. package/dist/esm/providers/compressedRollouts.js +330 -0
  38. package/dist/esm/providers/factory.js +4 -1
  39. package/dist/esm/providers/rolloutWalker.js +32 -4
  40. package/dist/esm/schemas/sessionEvent.js +63 -14
  41. package/dist/esm/sessionMonitor.js +7 -1
  42. package/dist/esm/sessionPreviews.js +9 -0
  43. package/dist/esm/sessionPromptHistory.js +5 -3
  44. package/dist/esm/subagentActivity.js +125 -0
  45. package/dist/esm/subagentDisplayState.js +81 -0
  46. package/dist/esm/subagentSpawn.js +363 -0
  47. package/dist/esm/testing/codexSubagentFixtures.js +336 -0
  48. package/dist/esm/toolCallOrigins.js +17 -10
  49. package/dist/esm/toolCategory.js +6 -2
  50. package/dist/esm/transcript.js +8 -0
  51. package/dist/esm/turns/assistantTurn.js +6 -2
  52. package/dist/esm/usage/claudeUsageDedupe.js +68 -0
  53. package/dist/esm/usage/usageEvents.js +4 -1
  54. package/dist/esm/watchers/eventBridge.js +45 -0
  55. package/dist/esm/watchers/factory.js +5 -3
  56. package/dist/esm/watchers/jsonlTail.js +82 -48
  57. package/dist/esm/watchers/jsonlWatcher.js +61 -4
  58. package/dist/esm/watchers/providerReaderWatcher.js +18 -4
  59. package/dist/esm/zaiQuota.js +2 -1
  60. package/dist/fileContinuity.d.ts +95 -0
  61. package/dist/fileContinuity.js +187 -0
  62. package/dist/formatters/toolSummary.js +4 -1
  63. package/dist/index.d.ts +19 -7
  64. package/dist/index.js +48 -10
  65. package/dist/modelCatalog.js +5 -4
  66. package/dist/modelContext.js +4 -3
  67. package/dist/modelInfo.js +4 -3
  68. package/dist/node.d.ts +5 -1
  69. package/dist/node.js +18 -2
  70. package/dist/observedContextWindows.js +2 -1
  71. package/dist/ownKey.d.ts +16 -0
  72. package/dist/ownKey.js +30 -0
  73. package/dist/parsers/codexParser.d.ts +107 -0
  74. package/dist/parsers/codexParser.js +373 -7
  75. package/dist/parsers/openCodeParser.js +2 -1
  76. package/dist/parsers/subagentEndState.d.ts +20 -0
  77. package/dist/parsers/subagentEndState.js +65 -0
  78. package/dist/parsers/subagentScanner.d.ts +19 -6
  79. package/dist/parsers/subagentScanner.js +37 -12
  80. package/dist/parsers/subagentTraceParser.d.ts +33 -5
  81. package/dist/parsers/subagentTraceParser.js +103 -37
  82. package/dist/pricingCatalog.js +3 -2
  83. package/dist/promptHistory.d.ts +13 -3
  84. package/dist/promptHistory.js +22 -11
  85. package/dist/providers/claudeCode.js +59 -28
  86. package/dist/providers/codex.d.ts +35 -5
  87. package/dist/providers/codex.js +232 -198
  88. package/dist/providers/codexCompressedReader.d.ts +58 -0
  89. package/dist/providers/codexCompressedReader.js +221 -0
  90. package/dist/providers/codexDatabase.d.ts +25 -0
  91. package/dist/providers/codexDatabase.js +48 -2
  92. package/dist/providers/codexRollouts.d.ts +16 -2
  93. package/dist/providers/codexRollouts.js +20 -2
  94. package/dist/providers/codexSubagents.d.ts +66 -0
  95. package/dist/providers/codexSubagents.js +495 -0
  96. package/dist/providers/compressedRollouts.d.ts +114 -0
  97. package/dist/providers/compressedRollouts.js +380 -0
  98. package/dist/providers/factory.d.ts +5 -0
  99. package/dist/providers/factory.js +4 -1
  100. package/dist/providers/rolloutWalker.d.ts +15 -2
  101. package/dist/providers/rolloutWalker.js +33 -4
  102. package/dist/providers/types.d.ts +36 -1
  103. package/dist/schemas/index.d.ts +1 -0
  104. package/dist/schemas/sessionEvent.d.ts +51 -10
  105. package/dist/schemas/sessionEvent.js +63 -14
  106. package/dist/sessionMonitor.d.ts +6 -0
  107. package/dist/sessionMonitor.js +7 -1
  108. package/dist/sessionPreviews.js +9 -0
  109. package/dist/sessionPromptHistory.d.ts +5 -0
  110. package/dist/sessionPromptHistory.js +5 -36
  111. package/dist/subagentActivity.d.ts +91 -0
  112. package/dist/subagentActivity.js +132 -0
  113. package/dist/subagentDisplayState.d.ts +81 -0
  114. package/dist/subagentDisplayState.js +86 -0
  115. package/dist/subagentSpawn.d.ts +175 -0
  116. package/dist/subagentSpawn.js +385 -0
  117. package/dist/testing/codexSubagentFixtures.d.ts +48 -0
  118. package/dist/testing/codexSubagentFixtures.js +344 -0
  119. package/dist/toolCallOrigins.d.ts +5 -0
  120. package/dist/toolCallOrigins.js +17 -10
  121. package/dist/toolCategory.js +6 -2
  122. package/dist/transcript.js +8 -0
  123. package/dist/turns/assistantTurn.d.ts +4 -0
  124. package/dist/turns/assistantTurn.js +6 -2
  125. package/dist/types/codex.d.ts +71 -2
  126. package/dist/types/sessionEvent.d.ts +38 -3
  127. package/dist/usage/claudeUsageDedupe.d.ts +31 -0
  128. package/dist/usage/claudeUsageDedupe.js +68 -0
  129. package/dist/usage/usageEvents.d.ts +4 -1
  130. package/dist/usage/usageEvents.js +4 -1
  131. package/dist/watchers/eventBridge.js +45 -0
  132. package/dist/watchers/factory.d.ts +6 -0
  133. package/dist/watchers/factory.js +5 -3
  134. package/dist/watchers/jsonlTail.d.ts +27 -4
  135. package/dist/watchers/jsonlTail.js +84 -48
  136. package/dist/watchers/jsonlWatcher.d.ts +22 -1
  137. package/dist/watchers/jsonlWatcher.js +60 -3
  138. package/dist/watchers/providerReaderWatcher.d.ts +7 -0
  139. package/dist/watchers/providerReaderWatcher.js +18 -4
  140. package/dist/watchers/types.d.ts +12 -0
  141. package/dist/zaiQuota.js +2 -1
  142. package/package.json +1 -1
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ /**
3
+ * Whether a Claude Code subagent transcript ended cleanly, read line by line.
4
+ *
5
+ * Clean means: the last user or assistant line is an assistant line that
6
+ * stopped with `end_turn` or `stop_sequence`, and every `tool_use` in the
7
+ * transcript has its `tool_result`. A user line after that (a resumed agent's
8
+ * new prompt) makes it unfinished again.
9
+ *
10
+ * Pure. Internal to the subagent parsers.
11
+ *
12
+ * @module parsers/subagentEndState
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.SubagentEndTracker = void 0;
16
+ const ENDED_STOP_REASONS = new Set(['end_turn', 'stop_sequence']);
17
+ /** Bounds the unanswered-call set for pathological transcripts. */
18
+ const MAX_PENDING_TOOL_USES = 4096;
19
+ class SubagentEndTracker {
20
+ unanswered = new Set();
21
+ lastWasCleanAssistant = false;
22
+ /** Feeds one parsed JSONL line, in file order. */
23
+ observe(raw) {
24
+ if (!raw || typeof raw !== 'object')
25
+ return;
26
+ const line = raw;
27
+ if (line.type !== 'assistant' && line.type !== 'user')
28
+ return;
29
+ const message = line.message && typeof line.message === 'object'
30
+ ? line.message
31
+ : undefined;
32
+ const content = Array.isArray(message?.content) ? message.content : [];
33
+ if (line.type === 'user') {
34
+ for (const block of content) {
35
+ if (!block || typeof block !== 'object')
36
+ continue;
37
+ const typed = block;
38
+ if (typed.type === 'tool_result' && typeof typed.tool_use_id === 'string') {
39
+ this.unanswered.delete(typed.tool_use_id);
40
+ }
41
+ }
42
+ this.lastWasCleanAssistant = false;
43
+ return;
44
+ }
45
+ let callsTool = false;
46
+ for (const block of content) {
47
+ if (!block || typeof block !== 'object')
48
+ continue;
49
+ const typed = block;
50
+ if (typed.type !== 'tool_use')
51
+ continue;
52
+ callsTool = true;
53
+ if (typeof typed.id === 'string' && this.unanswered.size < MAX_PENDING_TOOL_USES) {
54
+ this.unanswered.add(typed.id);
55
+ }
56
+ }
57
+ const stop = typeof message?.stop_reason === 'string' ? message.stop_reason : '';
58
+ this.lastWasCleanAssistant = !callsTool && ENDED_STOP_REASONS.has(stop);
59
+ }
60
+ /** Whether the lines so far end cleanly. */
61
+ endedCleanly() {
62
+ return this.lastWasCleanAssistant && this.unanswered.size === 0;
63
+ }
64
+ }
65
+ exports.SubagentEndTracker = SubagentEndTracker;
@@ -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,50 @@ 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");
55
+ const subagentEndState_1 = require("./subagentEndState");
54
56
  /**
55
57
  * Pattern for matching subagent JSONL files.
56
58
  * Files are named like: agent-<hash>.jsonl
57
59
  */
58
60
  const AGENT_FILE_PATTERN = /^agent-(.+)\.jsonl$/;
61
+ function metaString(value) {
62
+ return typeof value === 'string' && value ? value : undefined;
63
+ }
59
64
  /**
60
65
  * 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.
66
+ * for the spawn's agent type, description, spawning tool_use id, parent agent,
67
+ * and completion flags. Missing or malformed files yield an empty object;
68
+ * wrongly typed fields are left out.
63
69
  */
64
70
  function readAgentMeta(agentFilePath) {
65
71
  try {
66
72
  const metaPath = agentFilePath.replace(/\.jsonl$/, '.meta.json');
67
73
  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
- };
74
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
75
+ return {};
76
+ const record = parsed;
77
+ const meta = {};
78
+ const agentType = metaString(record.agentType);
79
+ if (agentType)
80
+ meta.agentType = agentType;
81
+ const description = metaString(record.description);
82
+ if (description)
83
+ meta.description = description;
84
+ const toolUseId = metaString(record.toolUseId);
85
+ if (toolUseId)
86
+ meta.toolUseId = toolUseId;
87
+ const parentAgentId = metaString(record.parentAgentId);
88
+ if (parentAgentId)
89
+ meta.parentAgentId = parentAgentId;
90
+ const requestShape = metaString(record.requestShape);
91
+ if (requestShape)
92
+ meta.requestShape = requestShape;
93
+ if (typeof record.finished === 'boolean')
94
+ meta.finished = record.finished;
95
+ if (typeof record.stoppedByUser === 'boolean')
96
+ meta.stoppedByUser = record.stoppedByUser;
97
+ return meta;
76
98
  }
77
99
  catch {
78
100
  return {};
@@ -178,6 +200,7 @@ function parseAgentFile(filePath, agentId, log) {
178
200
  let description;
179
201
  const tokens = { agentId, toolCalls: [], inputTokens: 0, outputTokens: 0 };
180
202
  const usageDeduper = new claudeUsageDedupe_1.ClaudeUsageDeduper();
203
+ const endTracker = new subagentEndState_1.SubagentEndTracker();
181
204
  let startTime;
182
205
  let endTime;
183
206
  // Task tracking state
@@ -194,6 +217,7 @@ function parseAgentFile(filePath, agentId, log) {
194
217
  if (!startTime)
195
218
  startTime = eventTimestamp;
196
219
  endTime = eventTimestamp;
220
+ endTracker.observe(event);
197
221
  // Extract token usage from assistant messages. Split lines of one
198
222
  // response repeat its usage, so count each response once.
199
223
  const usage = (0, claudeUsageDedupe_1.dedupedRawClaudeUsage)(event, usageDeduper);
@@ -247,8 +271,8 @@ function parseAgentFile(filePath, agentId, log) {
247
271
  }
248
272
  }
249
273
  toolCalls.push(toolCall);
250
- // If this is a Task tool call (spawning another subagent), extract info
251
- if (toolUse.name === 'Task' && toolUse.input) {
274
+ // If this is an Agent/Task call (spawning another subagent), extract info
275
+ if ((0, subagentSpawn_1.isSubagentSpawnTool)(toolUse.name) && toolUse.input) {
252
276
  if (toolUse.input.subagent_type && !agentType) {
253
277
  agentType = String(toolUse.input.subagent_type);
254
278
  }
@@ -316,6 +340,7 @@ function parseAgentFile(filePath, agentId, log) {
316
340
  startTime,
317
341
  endTime,
318
342
  durationMs,
343
+ endedCleanly: endTracker.endedCleanly(),
319
344
  };
320
345
  }
321
346
  return null;
@@ -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,33 @@ 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;
66
+ /** Time of the transcript's last record (the same as `stats.endTime`). */
67
+ lastActivityAt?: Date;
68
+ /**
69
+ * The transcript ended cleanly: its last assistant turn stopped with
70
+ * `end_turn` / `stop_sequence` and no tool call is unanswered. A resumed
71
+ * agent's next prompt clears it.
72
+ */
73
+ endedCleanly?: boolean;
46
74
  }
47
75
  /**
48
76
  * Scans and parses subagent traces with full conversation events.
@@ -6,10 +6,13 @@
6
6
  * (not just stats) for drill-down trace visualization. Applies noise classification
7
7
  * and tool summary formatting from the formatters module.
8
8
  *
9
- * Three-phase parent-child linking:
10
- * 1. Result-based: match toolUseResult.agentId → parent tool_use_id
11
- * 2. Team summary: parse <teammate-message> XML blocks
12
- * 3. Positional fallback: timestamp ordering + proximity
9
+ * Parent-child linking, exact links first:
10
+ * 1. Meta `parentAgentId` (Claude Code writes it for nested spawns)
11
+ * 2. The child's spawning tool_use id (meta `toolUseId`) found in another
12
+ * trace's `Agent` / `Task` calls
13
+ * Only traces without that spawn metadata (older Claude Code) fall back to:
14
+ * 3. Team summary: parse <teammate-message> XML blocks
15
+ * 4. Positional fallback: timestamp ordering + proximity
13
16
  *
14
17
  * @module parsers/subagentTraceParser
15
18
  */
@@ -55,6 +58,9 @@ const noiseClassifier_1 = require("../formatters/noiseClassifier");
55
58
  const claudeUsageDedupe_1 = require("../usage/claudeUsageDedupe");
56
59
  const subagentUsage_1 = require("../usage/subagentUsage");
57
60
  const subagentScanner_1 = require("./subagentScanner");
61
+ const subagentEndState_1 = require("./subagentEndState");
62
+ const subagentSpawn_1 = require("../subagentSpawn");
63
+ const ownKey_1 = require("../ownKey");
58
64
  // ── Constants ──
59
65
  const AGENT_FILE_PATTERN = /^agent-(.+)\.jsonl$/;
60
66
  // ── Public API ──
@@ -86,10 +92,8 @@ function scanSubagentTraces(sessionDir, sessionId) {
86
92
  if (trace)
87
93
  traces.push(trace);
88
94
  }
89
- // Link parent-child relationships
90
- linkTraces(traces);
91
- // Return only top-level traces (those without parents)
92
- return traces.filter((t) => !t.parentToolUseId);
95
+ // Link parent-child relationships; returns the traces with no parent agent.
96
+ return linkTraces(traces);
93
97
  }
94
98
  catch {
95
99
  return [];
@@ -106,6 +110,7 @@ function parseAgentTrace(filePath, agentId) {
106
110
  let description;
107
111
  const tokens = { agentId, toolCalls: [], inputTokens: 0, outputTokens: 0 };
108
112
  const usageDeduper = new claudeUsageDedupe_1.ClaudeUsageDeduper();
113
+ const endTracker = new subagentEndState_1.SubagentEndTracker();
109
114
  let startTime;
110
115
  let endTime;
111
116
  for (const line of lines) {
@@ -115,6 +120,7 @@ function parseAgentTrace(filePath, agentId) {
115
120
  if (!startTime)
116
121
  startTime = timestamp;
117
122
  endTime = timestamp;
123
+ endTracker.observe(raw);
118
124
  // Build SessionEvent from raw JSONL
119
125
  const sessionEvent = rawToSessionEvent(raw);
120
126
  if (!sessionEvent)
@@ -169,8 +175,8 @@ function parseAgentTrace(filePath, agentId) {
169
175
  input: block.input || {},
170
176
  timestamp,
171
177
  });
172
- // Extract agent info from Task tool
173
- if (block.name === 'Task' && block.input) {
178
+ // Extract agent info from an Agent/Task spawn
179
+ if ((0, subagentSpawn_1.isSubagentSpawnTool)(block.name) && block.input) {
174
180
  if (block.input.subagent_type && !agentType) {
175
181
  agentType = String(block.input.subagent_type);
176
182
  }
@@ -207,6 +213,16 @@ function parseAgentTrace(filePath, agentId) {
207
213
  agentId,
208
214
  agentType,
209
215
  description,
216
+ filePath,
217
+ ...(meta.toolUseId
218
+ ? { spawnToolUseId: meta.toolUseId, parentToolUseId: meta.toolUseId }
219
+ : {}),
220
+ ...(meta.parentAgentId ? { parentAgentId: meta.parentAgentId } : {}),
221
+ ...(meta.requestShape ? { background: meta.requestShape === 'background' } : {}),
222
+ ...(meta.finished !== undefined ? { finished: meta.finished } : {}),
223
+ ...(meta.stoppedByUser !== undefined ? { stoppedByUser: meta.stoppedByUser } : {}),
224
+ ...(endTime ? { lastActivityAt: endTime } : {}),
225
+ endedCleanly: endTracker.endedCleanly(),
210
226
  events,
211
227
  children: [],
212
228
  stats: {
@@ -223,6 +239,7 @@ function parseAgentTrace(filePath, agentId) {
223
239
  startTime,
224
240
  endTime,
225
241
  durationMs,
242
+ endedCleanly: endTracker.endedCleanly(),
226
243
  },
227
244
  };
228
245
  }
@@ -231,23 +248,61 @@ function parseAgentTrace(filePath, agentId) {
231
248
  }
232
249
  }
233
250
  // ── Parent-Child Linking ──
251
+ /**
252
+ * Nests child traces under their parents and returns the traces with no
253
+ * parent agent. Exact metadata links come first; the heuristics only place
254
+ * traces Claude Code recorded no spawn metadata for.
255
+ */
234
256
  function linkTraces(traces) {
235
257
  const traceById = new Map();
236
258
  for (const trace of traces) {
237
259
  traceById.set(trace.agentId, trace);
238
260
  }
239
- // Phase 1: Result-based linking
240
- // Look for tool_result events that contain agentId references
261
+ const parentOf = new Map();
262
+ const attach = (parent, child) => {
263
+ if (parent === child || parentOf.has(child))
264
+ return false;
265
+ // Never nest an agent under its own descendant (malformed metadata).
266
+ for (let up = parent; up; up = parentOf.get(up)) {
267
+ if (up === child)
268
+ return false;
269
+ }
270
+ parentOf.set(child, parent);
271
+ parent.children.push(child);
272
+ return true;
273
+ };
274
+ // Phase 1: the meta file names the parent agent
275
+ for (const child of traces) {
276
+ const parent = child.parentAgentId ? traceById.get(child.parentAgentId) : undefined;
277
+ if (parent)
278
+ attach(parent, child);
279
+ }
280
+ // Phase 2: the child's spawning call is in another trace
281
+ const spawnOwner = new Map();
241
282
  for (const trace of traces) {
242
- for (const traceEvent of trace.events) {
243
- const evt = traceEvent.event;
244
- if (evt.type === 'tool_use' && evt.tool?.name === 'Task') {
245
- // The tool_use_id from this call should appear as a result in the parent
246
- // but we can link via timestamp proximity to child traces
247
- }
283
+ for (const id of spawnToolUseIdsOf(trace)) {
284
+ if (!spawnOwner.has(id))
285
+ spawnOwner.set(id, trace);
248
286
  }
249
287
  }
250
- // Phase 2: Team summary linking
288
+ for (const child of traces) {
289
+ if (parentOf.has(child) || !child.spawnToolUseId)
290
+ continue;
291
+ const parent = spawnOwner.get(child.spawnToolUseId);
292
+ if (parent && attach(parent, child))
293
+ child.parentAgentId ??= parent.agentId;
294
+ }
295
+ // Heuristic links set parentAgentId and, as before, parentToolUseId to the
296
+ // parent's agent id.
297
+ const unresolved = (trace) => !parentOf.has(trace) && !trace.spawnToolUseId && !trace.parentAgentId;
298
+ const linkHeuristically = (parent, child) => {
299
+ if (!attach(parent, child))
300
+ return false;
301
+ child.parentAgentId = parent.agentId;
302
+ child.parentToolUseId = parent.agentId;
303
+ return true;
304
+ };
305
+ // Phase 3: Team summary linking
251
306
  for (const trace of traces) {
252
307
  for (const traceEvent of trace.events) {
253
308
  const content = traceEvent.event.message?.content;
@@ -255,45 +310,56 @@ function linkTraces(traces) {
255
310
  // Parse teammate agent IDs from XML blocks
256
311
  const matches = content.matchAll(/agent[_-]id['":\s]+([a-f0-9]+)/gi);
257
312
  for (const match of matches) {
258
- const childId = match[1];
259
- const child = traceById.get(childId);
260
- if (child && child !== trace && !child.parentToolUseId) {
261
- child.parentToolUseId = trace.agentId;
262
- trace.children.push(child);
263
- }
313
+ const child = traceById.get(match[1]);
314
+ if (child && unresolved(child))
315
+ linkHeuristically(trace, child);
264
316
  }
265
317
  }
266
318
  }
267
319
  }
268
- // Phase 3: Positional fallback — timestamp proximity
320
+ // Phase 4: Positional fallback — timestamp proximity
269
321
  // Traces without parents that fall within another trace's time window
270
- const parentless = traces.filter((t) => !t.parentToolUseId);
271
- for (const child of parentless) {
322
+ for (const child of traces.filter(unresolved)) {
272
323
  if (!child.stats.startTime)
273
324
  continue;
274
325
  const childStart = child.stats.startTime.getTime();
275
- // Find a trace that was active (Task tool call) right before this child started
326
+ // Find a top-level trace that spawned an agent right before this child started
276
327
  for (const potential of traces) {
277
- if (potential === child)
328
+ if (potential === child || parentOf.has(potential))
278
329
  continue;
279
- if (potential.parentToolUseId)
280
- continue; // Already linked
281
330
  for (const traceEvent of potential.events) {
282
331
  const evt = traceEvent.event;
283
- if (evt.type === 'assistant' && evt.tool?.name === 'Task') {
332
+ if (evt.type === 'assistant' && (0, subagentSpawn_1.isSubagentSpawnTool)(evt.tool?.name)) {
284
333
  const taskTime = new Date(evt.timestamp).getTime();
285
334
  // Within 2 seconds before child start
286
335
  if (taskTime <= childStart && childStart - taskTime < 2000) {
287
- child.parentToolUseId = potential.agentId;
288
- potential.children.push(child);
336
+ linkHeuristically(potential, child);
289
337
  break;
290
338
  }
291
339
  }
292
340
  }
293
- if (child.parentToolUseId)
341
+ if (parentOf.has(child))
294
342
  break;
295
343
  }
296
344
  }
345
+ return traces.filter((trace) => !parentOf.has(trace));
346
+ }
347
+ /** Ids of the `Agent` / `Task` calls a trace made. */
348
+ function spawnToolUseIdsOf(trace) {
349
+ const ids = [];
350
+ for (const traceEvent of trace.events) {
351
+ const content = traceEvent.event.message?.content;
352
+ if (!Array.isArray(content))
353
+ continue;
354
+ for (const block of content) {
355
+ if (block?.type === 'tool_use' &&
356
+ typeof block.id === 'string' &&
357
+ (0, subagentSpawn_1.isSubagentSpawnTool)(block.name)) {
358
+ ids.push(block.id);
359
+ }
360
+ }
361
+ }
362
+ return ids;
297
363
  }
298
364
  // ── Helpers ──
299
365
  function rawToSessionEvent(raw) {
@@ -308,7 +374,7 @@ function rawToSessionEvent(raw) {
308
374
  tool_result: 'tool_result',
309
375
  summary: 'summary',
310
376
  };
311
- const mappedType = typeMap[type];
377
+ const mappedType = (0, ownKey_1.ownValue)(typeMap, type);
312
378
  if (!mappedType)
313
379
  return null;
314
380
  const message = raw.message;
@@ -59,6 +59,7 @@ const node_fs_1 = require("node:fs");
59
59
  const path = __importStar(require("node:path"));
60
60
  const modelContext_1 = require("./modelContext");
61
61
  const modelInfo_1 = require("./modelInfo");
62
+ const ownKey_1 = require("./ownKey");
62
63
  const paths_1 = require("./paths");
63
64
  const atomic_1 = require("./writers/atomic");
64
65
  // ── Constants ──
@@ -156,7 +157,7 @@ function normalizeLiteLlmCatalog(raw) {
156
157
  continue;
157
158
  // Catalog keys use both `provider/model` and bare `model` forms. Record both
158
159
  // the raw key and the bare-model form so lookups work regardless of caller.
159
- out[key] = pricing;
160
+ (0, ownKey_1.setOwnValue)(out, key, pricing);
160
161
  const bare = stripProviderPrefix(key);
161
162
  if (bare && bare !== key && !out[bare]) {
162
163
  out[bare] = pricing;
@@ -193,7 +194,7 @@ function normalizeLiteLlmContextWindows(raw) {
193
194
  const window = entry.max_input_tokens;
194
195
  if (typeof window !== 'number' || !Number.isFinite(window) || window <= 0)
195
196
  continue;
196
- out[key] = window;
197
+ (0, ownKey_1.setOwnValue)(out, key, window);
197
198
  const bare = stripProviderPrefix(key);
198
199
  if (bare && bare !== key) {
199
200
  const seen = aliasWindows.get(bare) ?? new Set();
@@ -23,8 +23,8 @@
23
23
  * the prompt is returned again under the same identity once its first
24
24
  * answering call brings model or usage (`metadataStatus` says how settled).
25
25
  */
26
- import * as fs from 'fs';
27
26
  import { type HumanPromptProvider } from './humanPrompt';
27
+ import { type RolloutContentStat } from './providers/compressedRollouts';
28
28
  export type PromptHistoryProvider = HumanPromptProvider;
29
29
  export type PromptHistoryBound = 'maxSessions' | 'maxFileBytes' | 'maxTotalBytes' | 'maxEntries' | 'deadline' | 'aborted';
30
30
  export interface PromptHistoryBounds {
@@ -235,6 +235,14 @@ export interface CollectPromptHistoryOptions {
235
235
  * both modes, so a cursor written in one mode resumes in the other.
236
236
  */
237
237
  includeText?: boolean;
238
+ /**
239
+ * Codex: also scan compressed rollouts (`….jsonl.zst`, decompressed when
240
+ * the runtime has `zlib.zstdDecompressSync`; skipped otherwise). Offsets
241
+ * and sizes in the cursor are then those of the decompressed content, so
242
+ * a session whose rollout Codex compressed continues where it stopped.
243
+ * Default false.
244
+ */
245
+ includeCompressedRollouts?: boolean;
238
246
  }
239
247
  export interface PromptHistoryResult {
240
248
  /** Ordered by provider, session, then ordinal. */
@@ -375,6 +383,8 @@ export declare function resolveRoots(workspacePaths: string[], includeWorktrees:
375
383
  listingRoots: string[];
376
384
  };
377
385
  /** @internal */
378
- export declare function listCandidates(providers: PromptHistoryProvider[], listingRoots: string[], sinceMs: number | null): Promise<Candidate[]>;
386
+ export declare function listCandidates(providers: PromptHistoryProvider[], listingRoots: string[], sinceMs: number | null, options?: {
387
+ includeCompressedRollouts?: boolean;
388
+ }): Promise<Candidate[]>;
379
389
  /** @internal */
380
- export declare function scanFile(candidate: Candidate, stat: fs.Stats, prior: PromptHistoryCursorSession | undefined, budget: FileBudget, context: ScanContext): Promise<FileOutcome>;
390
+ export declare function scanFile(candidate: Candidate, fileStat: RolloutContentStat, prior: PromptHistoryCursorSession | undefined, budget: FileBudget, context: ScanContext): Promise<FileOutcome>;
@@ -69,11 +69,11 @@ exports.resolveRoots = resolveRoots;
69
69
  exports.listCandidates = listCandidates;
70
70
  exports.scanFile = scanFile;
71
71
  const crypto_1 = require("crypto");
72
- const fs = __importStar(require("fs"));
73
72
  const path = __importStar(require("path"));
74
73
  const humanPrompt_1 = require("./humanPrompt");
75
74
  const claudeListing_1 = require("./providers/claudeListing");
76
75
  const codexRollouts_1 = require("./providers/codexRollouts");
76
+ const compressedRollouts_1 = require("./providers/compressedRollouts");
77
77
  const directoryListingCache_1 = require("./providers/directoryListingCache");
78
78
  const workspaceRoots_1 = require("./workspaceRoots");
79
79
  const fileHead_1 = require("./fileHead");
@@ -150,7 +150,9 @@ async function collectPromptHistory(options) {
150
150
  return finish();
151
151
  const sinceMs = options.since ? options.since.getTime() : null;
152
152
  const providers = options.providers ?? ALL_PROVIDERS;
153
- const candidates = rotate(await listCandidates(providers, listingRoots, sinceMs), previousNext);
153
+ const candidates = rotate(await listCandidates(providers, listingRoots, sinceMs, {
154
+ includeCompressedRollouts: options.includeCompressedRollouts,
155
+ }), previousNext);
154
156
  const moved = movedSessionKeys(previous, candidates);
155
157
  const context = {
156
158
  roots,
@@ -186,7 +188,8 @@ async function collectPromptHistory(options) {
186
188
  }
187
189
  let stat;
188
190
  try {
189
- stat = await fs.promises.stat(candidate.filePath);
191
+ // A compressed rollout's size is its decompressed content's.
192
+ stat = await (0, compressedRollouts_1.statRolloutContent)(candidate.filePath);
190
193
  }
191
194
  catch {
192
195
  continue;
@@ -291,7 +294,7 @@ async function collectPromptHistory(options) {
291
294
  */
292
295
  async function readPromptOrdinalMarks(provider, sessionId, filePath) {
293
296
  const marks = [];
294
- const stat = await fs.promises.stat(filePath);
297
+ const stat = await (0, compressedRollouts_1.statRolloutContent)(filePath);
295
298
  const unbounded = Number.MAX_SAFE_INTEGER;
296
299
  const context = {
297
300
  roots: [],
@@ -480,19 +483,24 @@ function scopedCwd(cwd, roots, realpaths) {
480
483
  return null;
481
484
  }
482
485
  /** @internal */
483
- async function listCandidates(providers, listingRoots, sinceMs) {
486
+ async function listCandidates(providers, listingRoots, sinceMs, options = {}) {
484
487
  const byPath = new Map();
485
488
  const listOptions = sinceMs !== null ? { since: sinceMs } : {};
486
489
  for (const provider of [...new Set(providers)]) {
487
490
  if (!ALL_PROVIDERS.includes(provider))
488
491
  continue;
489
492
  // Listings are only candidate supersets; scope is re-checked per prompt.
490
- const list = provider === 'claude-code' ? claudeListing_1.listClaudeSessionFilesAsync : codexRollouts_1.listCodexSessionFilesAsync;
491
493
  const cache = new directoryListingCache_1.DirectoryListingCache();
492
494
  for (const root of listingRoots) {
493
495
  let files;
494
496
  try {
495
- files = await list(root, listOptions, cache);
497
+ files =
498
+ provider === 'claude-code'
499
+ ? await (0, claudeListing_1.listClaudeSessionFilesAsync)(root, listOptions, cache)
500
+ : await (0, codexRollouts_1.listCodexSessionFilesAsync)(root, {
501
+ ...listOptions,
502
+ includeCompressed: options.includeCompressedRollouts === true && (0, compressedRollouts_1.canReadCompressedRollouts)(),
503
+ }, cache);
496
504
  }
497
505
  catch {
498
506
  files = [];
@@ -514,11 +522,11 @@ async function listCandidates(providers, listingRoots, sinceMs) {
514
522
  compareStrings(left.sessionId, right.sessionId) ||
515
523
  compareStrings(left.key, right.key));
516
524
  }
517
- /** True when the file's first bytes still match a recorded head. */
525
+ /** True when the file's first bytes (a compressed rollout's content) still match a recorded head. */
518
526
  async function fileStartsWith(filePath, head) {
519
527
  let handle;
520
528
  try {
521
- handle = await fs.promises.open(filePath, 'r');
529
+ ({ handle } = await (0, compressedRollouts_1.openRolloutReadHandle)(filePath));
522
530
  return (0, fileHead_1.headMatches)(head, await readAt(handle, 0, Math.min(fileHead_1.HEAD_BYTES, head.bytes)));
523
531
  }
524
532
  catch {
@@ -591,8 +599,11 @@ async function verifyFile(handle, size, session) {
591
599
  return { head: (0, fileHead_1.fileHeadOf)(head), bytesRead };
592
600
  }
593
601
  /** @internal */
594
- async function scanFile(candidate, stat, prior, budget, context) {
595
- const handle = await fs.promises.open(candidate.filePath, 'r');
602
+ async function scanFile(candidate, fileStat, prior, budget, context) {
603
+ // A compressed rollout is scanned over its decompressed content.
604
+ const opened = await (0, compressedRollouts_1.openRolloutReadHandle)(candidate.filePath);
605
+ const handle = opened.handle;
606
+ const stat = opened.size === null ? fileStat : { size: opened.size, mtimeMs: fileStat.mtimeMs };
596
607
  try {
597
608
  let bytesRead = 0;
598
609
  const verified = await verifyFile(handle, stat.size, prior);