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,336 @@
1
+ /**
2
+ * Redacted rollouts in the shape Codex 0.166 writes for multi-agent sessions:
3
+ * a parent that spawns a child (`spawn_agent` → `SubAgentActivity` started →
4
+ * `{"task_name": …}` output → MESSAGE / FINAL_ANSWER agent messages →
5
+ * `completed`), a failed spawn, `followup_task` / `send_message` /
6
+ * `interrupt_agent` interactions, and a forked child whose rollout embeds the
7
+ * parent's history (and session_meta) before its
8
+ * `subagent_history_start_ordinal`, which itself spawns a grandchild.
9
+ * Payload text is placeholder; ids are synthetic.
10
+ *
11
+ * Test-only helpers.
12
+ *
13
+ * @module testing/codexSubagentFixtures
14
+ */
15
+ export const PARENT_ID = '01a18f87-0000-7000-8000-000000000001';
16
+ export const CHILD_ID = '01a18f9d-0000-7000-8000-000000000002';
17
+ export const GRANDCHILD_ID = '01a18fa0-0000-7000-8000-000000000003';
18
+ export const CALL_SPAWN = 'call_spawn_builder';
19
+ export const CALL_SPAWN_FAILED = 'call_spawn_failed';
20
+ export const CALL_FOLLOWUP = 'call_followup_builder';
21
+ export const CALL_SEND = 'call_send_builder';
22
+ export const CALL_INTERRUPT = 'call_interrupt_builder';
23
+ export const CALL_SPAWN_GRANDCHILD = 'call_spawn_deep_dive';
24
+ export const CHILD_TURN_ID = '01a18f9d-0000-7000-8000-0000000000t1';
25
+ export const CHILD_PATH = '/root/review_builder';
26
+ export const GRANDCHILD_PATH = '/root/review_builder/deep_dive';
27
+ /** `2026-10-31T01:<minute>:<second>.000Z`. */
28
+ export function at(minute, second = 0) {
29
+ return `2026-10-31T01:${String(minute).padStart(2, '0')}:${String(second).padStart(2, '0')}.000Z`;
30
+ }
31
+ function numbered(rows) {
32
+ return rows.map((row, ordinal) => ({ timestamp: row.timestamp, ordinal, ...row }));
33
+ }
34
+ function tokenCount(timestamp, total, last) {
35
+ return {
36
+ timestamp,
37
+ type: 'event_msg',
38
+ payload: {
39
+ type: 'token_count',
40
+ info: {
41
+ total_token_usage: { input_tokens: total, output_tokens: 0, total_tokens: total },
42
+ last_token_usage: { input_tokens: last, output_tokens: 0, total_tokens: last },
43
+ },
44
+ },
45
+ };
46
+ }
47
+ function functionCall(timestamp, callId, name, args, namespace = 'collaboration') {
48
+ return {
49
+ timestamp,
50
+ type: 'response_item',
51
+ payload: {
52
+ type: 'function_call',
53
+ status: 'completed',
54
+ id: `fc_${callId}`,
55
+ name,
56
+ ...(namespace ? { namespace } : {}),
57
+ arguments: JSON.stringify(args),
58
+ call_id: callId,
59
+ },
60
+ };
61
+ }
62
+ function output(timestamp, callId, text) {
63
+ return {
64
+ timestamp,
65
+ type: 'response_item',
66
+ payload: { type: 'function_call_output', call_id: callId, output: text },
67
+ };
68
+ }
69
+ function activity(timestamp, threadId, item) {
70
+ return {
71
+ timestamp,
72
+ type: 'event_msg',
73
+ payload: {
74
+ type: 'item_completed',
75
+ thread_id: threadId,
76
+ turn_id: `${threadId}-turn`,
77
+ item: { type: 'SubAgentActivity', model: null, reasoning_effort: null, ...item },
78
+ },
79
+ };
80
+ }
81
+ function agentMessage(timestamp, id, author, recipient, messageType, payload) {
82
+ return {
83
+ timestamp,
84
+ type: 'response_item',
85
+ payload: {
86
+ type: 'agent_message',
87
+ id,
88
+ author,
89
+ recipient,
90
+ content: [
91
+ {
92
+ type: 'input_text',
93
+ text: `Message Type: ${messageType}\nTask name: ${recipient}\nSender: ${author}\nPayload:\n${payload}`,
94
+ },
95
+ ...(payload ? [] : [{ type: 'encrypted_content', encrypted_content: 'gAAAA-redacted' }]),
96
+ ],
97
+ },
98
+ };
99
+ }
100
+ function exec(timestamp, callId) {
101
+ return functionCall(timestamp, callId, 'exec_command', { cmd: 'rg TODO' }, undefined);
102
+ }
103
+ /** The parent session's rollout (ordinals 0…). */
104
+ export function parentRollout(options) {
105
+ const rows = [
106
+ {
107
+ timestamp: at(15),
108
+ type: 'session_meta',
109
+ payload: {
110
+ id: PARENT_ID,
111
+ session_id: PARENT_ID,
112
+ timestamp: at(15),
113
+ cwd: options.cwd,
114
+ originator: 'codex-tui',
115
+ cli_version: '0.166.0',
116
+ source: 'vscode',
117
+ thread_source: 'user',
118
+ history_mode: 'paginated',
119
+ base_instructions: { text: 'Parent base instructions.' },
120
+ },
121
+ },
122
+ {
123
+ timestamp: at(15, 1),
124
+ type: 'turn_context',
125
+ payload: { model: 'gpt-test', cwd: options.cwd },
126
+ },
127
+ {
128
+ timestamp: at(15, 2),
129
+ type: 'response_item',
130
+ payload: {
131
+ type: 'message',
132
+ role: 'user',
133
+ content: [{ type: 'input_text', text: 'Review the builder.' }],
134
+ },
135
+ },
136
+ tokenCount(at(15, 3), 1000, 1000),
137
+ functionCall(at(40, 33), CALL_SPAWN, 'spawn_agent', {
138
+ task_name: 'review_builder',
139
+ message: 'gAAAA-redacted',
140
+ }),
141
+ options.withoutActivity
142
+ ? null
143
+ : activity(at(40, 33), PARENT_ID, {
144
+ id: CALL_SPAWN,
145
+ kind: 'started',
146
+ model: 'gpt-test',
147
+ reasoning_effort: 'high',
148
+ agent_thread_id: CHILD_ID,
149
+ agent_path: CHILD_PATH,
150
+ }),
151
+ output(at(40, 34), CALL_SPAWN, JSON.stringify({ task_name: CHILD_PATH })),
152
+ functionCall(at(40, 35), CALL_SPAWN_FAILED, 'spawn_agent', {
153
+ task_name: 'review_extra',
154
+ message: 'gAAAA-redacted',
155
+ }),
156
+ output(at(40, 35), CALL_SPAWN_FAILED, 'collab spawn failed: agent thread limit reached'),
157
+ {
158
+ timestamp: at(41, 8),
159
+ type: 'inter_agent_communication_metadata',
160
+ payload: { trigger_turn: false },
161
+ },
162
+ agentMessage(at(41, 8), 'amsg_progress', CHILD_PATH, '/root', 'MESSAGE', ''),
163
+ options.withoutActivity || options.withoutCompleted
164
+ ? null
165
+ : activity(at(42, 21), PARENT_ID, {
166
+ id: `subagent-completed-${CHILD_TURN_ID}`,
167
+ kind: 'completed',
168
+ agent_thread_id: CHILD_ID,
169
+ agent_path: CHILD_PATH,
170
+ }),
171
+ agentMessage(at(42, 25), 'amsg_final', CHILD_PATH, '/root', 'FINAL_ANSWER', 'Found two issues.'),
172
+ functionCall(at(45, 37), CALL_FOLLOWUP, 'followup_task', {
173
+ target: 'review_builder',
174
+ message: 'gAAAA-redacted',
175
+ }),
176
+ options.withoutActivity
177
+ ? null
178
+ : activity(at(45, 37), PARENT_ID, {
179
+ id: CALL_FOLLOWUP,
180
+ kind: 'interacted',
181
+ agent_thread_id: CHILD_ID,
182
+ agent_path: CHILD_PATH,
183
+ }),
184
+ output(at(45, 38), CALL_FOLLOWUP, ''),
185
+ functionCall(at(45, 40), CALL_SEND, 'send_message', {
186
+ target: 'review_builder',
187
+ message: 'gAAAA-redacted',
188
+ }),
189
+ options.withoutActivity
190
+ ? null
191
+ : activity(at(45, 40), PARENT_ID, {
192
+ id: CALL_SEND,
193
+ kind: 'interacted',
194
+ agent_thread_id: CHILD_ID,
195
+ agent_path: CHILD_PATH,
196
+ }),
197
+ functionCall(at(46, 0), CALL_INTERRUPT, 'interrupt_agent', { target: 'review_builder' }),
198
+ options.withoutActivity
199
+ ? null
200
+ : activity(at(46, 0), PARENT_ID, {
201
+ id: CALL_INTERRUPT,
202
+ kind: 'interrupted',
203
+ agent_thread_id: CHILD_ID,
204
+ agent_path: CHILD_PATH,
205
+ }),
206
+ tokenCount(at(46, 5), 2000, 1000),
207
+ ];
208
+ return numbered(rows.filter((row) => row !== null));
209
+ }
210
+ /** Records the child inherits from the parent (re-stamped at fork time, as Codex does). */
211
+ const INHERITED_RECORDS = 5;
212
+ /** The forked child's rollout: its meta, the parent's history, then its own records. */
213
+ export function childRollout(options) {
214
+ const start = 1 + INHERITED_RECORDS;
215
+ return numbered([
216
+ {
217
+ timestamp: at(40, 33),
218
+ type: 'session_meta',
219
+ payload: {
220
+ id: CHILD_ID,
221
+ session_id: PARENT_ID,
222
+ forked_from_id: PARENT_ID,
223
+ parent_thread_id: PARENT_ID,
224
+ timestamp: at(40, 33),
225
+ cwd: options.cwd,
226
+ originator: 'codex-tui',
227
+ cli_version: '0.166.0',
228
+ source: {
229
+ subagent: {
230
+ thread_spawn: {
231
+ parent_thread_id: PARENT_ID,
232
+ depth: 1,
233
+ agent_path: CHILD_PATH,
234
+ agent_nickname: 'Banach',
235
+ agent_role: null,
236
+ },
237
+ },
238
+ },
239
+ thread_source: 'subagent',
240
+ agent_nickname: 'Banach',
241
+ agent_path: CHILD_PATH,
242
+ history_mode: 'paginated',
243
+ subagent_history_start_ordinal: start,
244
+ multi_agent_version: 'v2',
245
+ },
246
+ },
247
+ // The parent's history, embedded by the fork.
248
+ {
249
+ timestamp: at(40, 33),
250
+ type: 'session_meta',
251
+ payload: {
252
+ id: PARENT_ID,
253
+ timestamp: at(15),
254
+ cwd: '/somewhere/else',
255
+ source: 'vscode',
256
+ base_instructions: { text: 'Parent base instructions.' },
257
+ },
258
+ },
259
+ { timestamp: at(40, 33), type: 'turn_context', payload: { model: 'gpt-parent' } },
260
+ exec(at(40, 33), 'call_parent_exec'),
261
+ tokenCount(at(40, 33), 5000, 5000),
262
+ {
263
+ timestamp: at(40, 33),
264
+ type: 'response_item',
265
+ payload: {
266
+ type: 'message',
267
+ role: 'developer',
268
+ content: [{ type: 'input_text', text: 'You are now a newly spawned child agent.' }],
269
+ },
270
+ },
271
+ // The child's own records.
272
+ {
273
+ timestamp: at(40, 34),
274
+ type: 'event_msg',
275
+ payload: { type: 'task_started', turn_id: CHILD_TURN_ID },
276
+ },
277
+ { timestamp: at(40, 34), type: 'turn_context', payload: { model: 'gpt-test' } },
278
+ exec(at(40, 35), 'call_child_exec'),
279
+ output(at(40, 36), 'call_child_exec', 'Process exited with code 0'),
280
+ tokenCount(at(40, 37), 300, 300),
281
+ functionCall(at(40, 38), CALL_SPAWN_GRANDCHILD, 'spawn_agent', {
282
+ task_name: 'deep_dive',
283
+ message: 'gAAAA-redacted',
284
+ }),
285
+ activity(at(40, 38), CHILD_ID, {
286
+ id: CALL_SPAWN_GRANDCHILD,
287
+ kind: 'started',
288
+ agent_thread_id: GRANDCHILD_ID,
289
+ agent_path: GRANDCHILD_PATH,
290
+ }),
291
+ output(at(40, 39), CALL_SPAWN_GRANDCHILD, JSON.stringify({ task_name: GRANDCHILD_PATH })),
292
+ tokenCount(at(42, 20), 500, 200),
293
+ ]);
294
+ }
295
+ /** The grandchild's rollout (depth 2): a fork of the child. */
296
+ export function grandchildRollout(options) {
297
+ return numbered([
298
+ {
299
+ timestamp: at(40, 38),
300
+ type: 'session_meta',
301
+ payload: {
302
+ id: GRANDCHILD_ID,
303
+ session_id: PARENT_ID,
304
+ forked_from_id: CHILD_ID,
305
+ parent_thread_id: CHILD_ID,
306
+ timestamp: at(40, 38),
307
+ cwd: options.cwd,
308
+ cli_version: '0.166.0',
309
+ source: {
310
+ subagent: {
311
+ thread_spawn: {
312
+ parent_thread_id: CHILD_ID,
313
+ depth: 2,
314
+ agent_path: GRANDCHILD_PATH,
315
+ agent_nickname: 'Noether',
316
+ agent_role: 'explorer',
317
+ },
318
+ },
319
+ },
320
+ thread_source: 'subagent',
321
+ agent_nickname: 'Noether',
322
+ agent_path: GRANDCHILD_PATH,
323
+ agent_role: 'explorer',
324
+ subagent_history_start_ordinal: 3,
325
+ },
326
+ },
327
+ { timestamp: at(40, 38), type: 'session_meta', payload: { id: CHILD_ID, cwd: options.cwd } },
328
+ exec(at(40, 38), 'call_child_exec'),
329
+ exec(at(40, 40), 'call_grandchild_exec'),
330
+ tokenCount(at(40, 41), 50, 50),
331
+ ]);
332
+ }
333
+ /** Rows as JSONL text. */
334
+ export function toJsonl(rows) {
335
+ return rows.map((row) => JSON.stringify(row)).join('\n') + '\n';
336
+ }
@@ -21,6 +21,8 @@
21
21
  import * as fs from 'fs';
22
22
  import * as path from 'path';
23
23
  import * as readline from 'readline';
24
+ import { Readable } from 'stream';
25
+ import { isCompressedRolloutPath, readRolloutBufferAsync, readRolloutHeadAsync, } from './providers/compressedRollouts';
24
26
  import { listCandidates, readPromptOrdinalMarks, resolveRoots, } from './promptHistory';
25
27
  import { codexRolloutOrigin } from './parsers/codexRolloutOrigin';
26
28
  import { realpathOrNull } from './workspaceRoots';
@@ -42,7 +44,9 @@ export async function findToolCallOrigins(options) {
42
44
  return found;
43
45
  const providers = options.providers ?? ['claude-code', 'codex'];
44
46
  const sinceMs = options.since ? options.since.getTime() : null;
45
- const candidates = await listCandidates(providers, listingRoots, sinceMs);
47
+ const candidates = await listCandidates(providers, listingRoots, sinceMs, {
48
+ includeCompressedRollouts: options.includeCompressedRollouts,
49
+ });
46
50
  const byProviderSession = new Map();
47
51
  for (const candidate of candidates) {
48
52
  byProviderSession.set(`${candidate.provider}:${candidate.sessionId}`, candidate);
@@ -152,21 +156,15 @@ function claudeSubagentTranscripts(sessionFile, sessionId) {
152
156
  }));
153
157
  }
154
158
  async function readFirstRecord(filePath) {
155
- let handle;
156
159
  try {
157
- handle = await fs.promises.open(filePath, 'r');
158
- const buffer = Buffer.alloc(256 * 1024);
159
- const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
160
- const text = buffer.subarray(0, bytesRead).toString('utf8');
160
+ // A compressed rollout's head is decompressed.
161
+ const text = (await readRolloutHeadAsync(filePath, 256 * 1024)).toString('utf8');
161
162
  const newline = text.indexOf('\n');
162
163
  return JSON.parse(newline === -1 ? text : text.slice(0, newline));
163
164
  }
164
165
  catch {
165
166
  return undefined;
166
167
  }
167
- finally {
168
- await handle?.close();
169
- }
170
168
  }
171
169
  /** Fail-closed scope check on a recorded working directory. */
172
170
  function inScope(cwd, roots, realpaths) {
@@ -187,7 +185,16 @@ async function scanForIds(filePath, provider, wanted, roots, realpaths, signal)
187
185
  const hits = [];
188
186
  let stream;
189
187
  try {
190
- stream = fs.createReadStream(filePath, { encoding: 'utf8' });
188
+ if (isCompressedRolloutPath(filePath)) {
189
+ // Compressed rollouts are complete; search their decompressed content.
190
+ const content = await readRolloutBufferAsync(filePath);
191
+ if (!content)
192
+ return hits;
193
+ stream = Readable.from([content.toString('utf8')]);
194
+ }
195
+ else {
196
+ stream = fs.createReadStream(filePath, { encoding: 'utf8' });
197
+ }
191
198
  }
192
199
  catch {
193
200
  return hits;
@@ -16,7 +16,11 @@ export const TOOL_CATEGORIES = [
16
16
  'mcp',
17
17
  'other',
18
18
  ];
19
- /** Lower-cased tool names of every provider, by category. */
19
+ /**
20
+ * Lower-cased tool names of every provider, by category. Codex's bare `wait`
21
+ * is a code-mode cell wait, not a subagent wait (that is `wait_agent`), so it
22
+ * is `other`.
23
+ */
20
24
  const CATEGORY_BY_NAME = new Map([
21
25
  [
22
26
  'shell',
@@ -50,7 +54,7 @@ const CATEGORY_BY_NAME = new Map([
50
54
  'send_message',
51
55
  'followup_task',
52
56
  'list_agents',
53
- 'wait',
57
+ 'interrupt_agent',
54
58
  ],
55
59
  ],
56
60
  [
@@ -1,4 +1,6 @@
1
1
  import { calculateNormalizedUsageCost, extractNormalizedUsage, } from './usageNormalization';
2
+ import { isTaskNotificationSystemEvent, isTaskStatusSystemEvent } from './subagentSpawn';
3
+ import { isSubagentActivitySystemEvent } from './subagentActivity';
2
4
  const EMPTY_TOTALS = {
3
5
  uncachedInputTokens: 0,
4
6
  cacheReadTokens: 0,
@@ -25,6 +27,12 @@ export function projectSessionTranscript(events, options = {}) {
25
27
  let cwd;
26
28
  let gitBranch;
27
29
  events.forEach((event, eventIndex) => {
30
+ // A reader's synthetic notice that a background task finished, not a message.
31
+ if (isTaskNotificationSystemEvent(event) || isTaskStatusSystemEvent(event))
32
+ return;
33
+ // Likewise a subagent lifecycle record (opt-in Codex parser event).
34
+ if (isSubagentActivitySystemEvent(event))
35
+ return;
28
36
  if (nonEmpty(event.cwd))
29
37
  cwd = event.cwd;
30
38
  if (nonEmpty(event.gitBranch))
@@ -1,10 +1,10 @@
1
1
  import { formatToolSummary, truncate } from '../formatters/toolSummary';
2
+ import { SUBAGENT_SPAWN_TOOL_NAMES } from '../subagentSpawn';
2
3
  const DEFAULT_NARRATION_CHARS = 4000;
3
4
  const DEFAULT_REASONING_CHARS = 8000;
4
5
  const DEFAULT_PROCESS_STEPS = 60;
5
6
  const DEFAULT_REASONING_BLOCKS = 24;
6
7
  const DEFAULT_TOOL_INPUT_CHARS = 200;
7
- const SUBAGENT_TOOL_NAMES = new Set(['Task']);
8
8
  const LABEL_MAX = 80;
9
9
  const TITLE_RE = /^\s*\*\*([^*\n]+)\*\*[ \t]*(?:\n+([\s\S]*))?$/;
10
10
  const TITLE_MAX = 100;
@@ -22,8 +22,12 @@ export function reasoningSummary(text) {
22
22
  }
23
23
  return { title: null, body: trimmed };
24
24
  }
25
+ /**
26
+ * Whether a tool call spawns a subagent (`Agent`, or its earlier name `Task`).
27
+ * Codex `spawn_agent` is not included here (its turns project as before).
28
+ */
25
29
  export function isAssistantTurnSubagentTool(toolName) {
26
- return toolName != null && SUBAGENT_TOOL_NAMES.has(toolName);
30
+ return typeof toolName === 'string' && SUBAGENT_SPAWN_TOOL_NAMES.includes(toolName);
27
31
  }
28
32
  export function segmentAssistantTurn(events, options = {}) {
29
33
  const maxNarrationChars = options.maxNarrationChars ?? DEFAULT_NARRATION_CHARS;
@@ -59,6 +59,25 @@ export function normalizeClaudeUsage(event) {
59
59
  },
60
60
  };
61
61
  }
62
+ const BUCKET_KEYS = ['input', 'cacheRead', 'cacheWrite', 'output', 'reasoning'];
63
+ /** Entries {@link ClaudeUsageDeduper.exportState} keeps by default. */
64
+ const DEFAULT_EXPORT_LIMIT = 256;
65
+ function isCount(value) {
66
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0;
67
+ }
68
+ function bucketsFrom(value) {
69
+ if (!value || typeof value !== 'object' || Array.isArray(value))
70
+ return null;
71
+ const record = value;
72
+ const buckets = {};
73
+ for (const key of BUCKET_KEYS) {
74
+ const count = record[key];
75
+ if (!isCount(count))
76
+ return null;
77
+ buckets[key] = count;
78
+ }
79
+ return buckets;
80
+ }
62
81
  function bucketsOf(usage) {
63
82
  return {
64
83
  input: usage.uncachedInputTokens,
@@ -163,6 +182,55 @@ export class ClaudeUsageDeduper {
163
182
  reset() {
164
183
  this.seen.clear();
165
184
  }
185
+ /**
186
+ * The most recent `limit` message ids and the tokens counted for each,
187
+ * oldest first. Split lines of one response are adjacent, so a reader
188
+ * resumed at the position this was taken at needs only the last few.
189
+ */
190
+ exportState(limit = DEFAULT_EXPORT_LIMIT) {
191
+ const count = limit >= 0 ? Math.min(this.seen.size, Math.floor(limit)) : 0;
192
+ const entries = [];
193
+ if (count > 0) {
194
+ let skip = this.seen.size - count;
195
+ for (const [id, buckets] of this.seen) {
196
+ if (skip > 0) {
197
+ skip -= 1;
198
+ continue;
199
+ }
200
+ entries.push([id, { ...buckets }]);
201
+ }
202
+ }
203
+ return { version: 1, entries };
204
+ }
205
+ /**
206
+ * Replaces this deduper's state with one from {@link exportState}. Returns
207
+ * false, changing nothing, when `state` is not a valid version-1 state
208
+ * (wrong shape, empty ids, or counts that are not finite non-negative
209
+ * numbers). Entries beyond this deduper's capacity drop oldest first.
210
+ */
211
+ importState(state) {
212
+ if (!state || typeof state !== 'object')
213
+ return false;
214
+ const { version, entries } = state;
215
+ if (version !== 1 || !Array.isArray(entries))
216
+ return false;
217
+ const parsed = [];
218
+ for (const entry of entries) {
219
+ if (!Array.isArray(entry) || entry.length !== 2)
220
+ return false;
221
+ const [id, value] = entry;
222
+ if (typeof id !== 'string' || id.length === 0)
223
+ return false;
224
+ const buckets = bucketsFrom(value);
225
+ if (!buckets)
226
+ return false;
227
+ parsed.push([id, buckets]);
228
+ }
229
+ this.seen.clear();
230
+ for (const [id, buckets] of parsed)
231
+ this.remember(id, buckets);
232
+ return true;
233
+ }
166
234
  remember(id, buckets) {
167
235
  this.seen.delete(id);
168
236
  this.seen.set(id, buckets);
@@ -15,7 +15,10 @@
15
15
  * tagged with the subagent, and their sizes and mtimes join the fingerprint.
16
16
  * (Codex spawned subagents and forks are top-level rollouts, already listed
17
17
  * as sessions of their own; their events are tagged with the parent read
18
- * from the rollout's first line.)
18
+ * from the rollout's first line; the parent history a forked child embeds
19
+ * is not counted (it carries no token usage). Compressed rollouts
20
+ * (`….jsonl.zst`) are read when a Codex provider was created with
21
+ * `includeCompressedRollouts`.)
19
22
  *
20
23
  * Each event also carries the ordinal of the human prompt it answered, from
21
24
  * the same scanner as `collectPromptHistory()`, so usage joins prompt history
@@ -8,6 +8,8 @@
8
8
  * FollowEvent is a lossy summary for timeline display and CLI output.
9
9
  */
10
10
  import { formatToolSummary } from '../formatters/toolSummary';
11
+ import { agentsStoppedNoticeFromEvent, describeAgentsStopped, describeTaskStatus, isAgentsKilledSystemEvent, isTaskNotificationSystemEvent, taskNotificationFromEvent, taskStatusFromEvent, } from '../subagentSpawn';
12
+ import { describeSubagentActivity, subagentActivityFromEvent } from '../subagentActivity';
11
13
  /**
12
14
  * Converts a single SessionEvent to one or more FollowEvents.
13
15
  *
@@ -148,6 +150,49 @@ export function toFollowEvents(event, providerId) {
148
150
  break;
149
151
  }
150
152
  case 'system': {
153
+ // A background task's notice (opt-in reader event): its summary, not the raw block.
154
+ const notification = isTaskNotificationSystemEvent(event)
155
+ ? taskNotificationFromEvent(event)
156
+ : null;
157
+ if (notification) {
158
+ events.push({
159
+ providerId,
160
+ type: 'system',
161
+ timestamp: ts,
162
+ summary: notification.summary ?? `Background task ${notification.status ?? 'finished'}`,
163
+ raw: event,
164
+ });
165
+ break;
166
+ }
167
+ // The user stopping background agents, or a background agent's final status.
168
+ const stopped = agentsStoppedNoticeFromEvent(event);
169
+ const taskStatus = stopped ? null : taskStatusFromEvent(event);
170
+ if (stopped || taskStatus || isAgentsKilledSystemEvent(event)) {
171
+ events.push({
172
+ providerId,
173
+ type: 'system',
174
+ timestamp: ts,
175
+ summary: stopped
176
+ ? describeAgentsStopped(stopped)
177
+ : taskStatus
178
+ ? describeTaskStatus(taskStatus)
179
+ : describeAgentsStopped(null),
180
+ raw: event,
181
+ });
182
+ break;
183
+ }
184
+ // A subagent lifecycle record (opt-in Codex parser event): a content-free summary.
185
+ const activity = subagentActivityFromEvent(event);
186
+ if (activity) {
187
+ events.push({
188
+ providerId,
189
+ type: 'system',
190
+ timestamp: ts,
191
+ summary: describeSubagentActivity(activity),
192
+ raw: event,
193
+ });
194
+ break;
195
+ }
151
196
  const text = extractTextContent(event.message?.content);
152
197
  const label = event.message?.sourceLabel || event.message?.role || 'system';
153
198
  const summary = text ||
@@ -41,13 +41,15 @@ export function resolveSessionPath(provider, workspacePath, sessionId) {
41
41
  export function createWatcher(options) {
42
42
  const { provider, workspacePath, sessionId, callbacks } = options;
43
43
  const sessionPath = resolveSessionPath(provider, workspacePath, sessionId);
44
- const watcher = createWatcherForProvider(provider, sessionPath, callbacks);
44
+ const watcher = createWatcherForProvider(provider, sessionPath, callbacks, {
45
+ includeTaskNotifications: options.includeTaskNotifications === true,
46
+ });
45
47
  return { watcher, sessionPath };
46
48
  }
47
- function createWatcherForProvider(provider, sessionPath, callbacks) {
49
+ function createWatcherForProvider(provider, sessionPath, callbacks, jsonlOptions) {
48
50
  switch (provider.id) {
49
51
  case 'claude-code':
50
- return new JsonlSessionWatcher(provider.id, sessionPath, callbacks);
52
+ return new JsonlSessionWatcher(provider.id, sessionPath, callbacks, jsonlOptions);
51
53
  case 'codex':
52
54
  return new ProviderReaderSessionWatcher(provider, sessionPath, callbacks);
53
55
  case 'opencode': {