sidekick-shared 0.29.1 → 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 (109) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +171 -60
  3. package/dist/aggregation/EventAggregator.d.ts +79 -4
  4. package/dist/aggregation/EventAggregator.js +391 -37
  5. package/dist/aggregation/types.d.ts +27 -3
  6. package/dist/browser.d.ts +7 -3
  7. package/dist/browser.js +23 -3
  8. package/dist/context/sessionContext.js +5 -1
  9. package/dist/esm/aggregation/EventAggregator.js +392 -38
  10. package/dist/esm/browser.js +3 -1
  11. package/dist/esm/context/sessionContext.js +6 -2
  12. package/dist/esm/fileContinuity.js +149 -0
  13. package/dist/esm/index.js +5 -2
  14. package/dist/esm/node.js +2 -0
  15. package/dist/esm/parsers/codexParser.js +192 -3
  16. package/dist/esm/parsers/subagentEndState.js +61 -0
  17. package/dist/esm/parsers/subagentScanner.js +4 -0
  18. package/dist/esm/parsers/subagentTraceParser.js +6 -0
  19. package/dist/esm/promptHistory.js +22 -11
  20. package/dist/esm/providers/claudeCode.js +50 -27
  21. package/dist/esm/providers/codex.js +153 -163
  22. package/dist/esm/providers/codexCompressedReader.js +184 -0
  23. package/dist/esm/providers/codexDatabase.js +47 -2
  24. package/dist/esm/providers/codexRollouts.js +20 -2
  25. package/dist/esm/providers/codexSubagents.js +456 -0
  26. package/dist/esm/providers/compressedRollouts.js +330 -0
  27. package/dist/esm/providers/factory.js +4 -1
  28. package/dist/esm/providers/rolloutWalker.js +32 -4
  29. package/dist/esm/schemas/sessionEvent.js +32 -4
  30. package/dist/esm/sessionMonitor.js +7 -1
  31. package/dist/esm/sessionPreviews.js +9 -0
  32. package/dist/esm/sessionPromptHistory.js +5 -3
  33. package/dist/esm/subagentActivity.js +125 -0
  34. package/dist/esm/subagentDisplayState.js +81 -0
  35. package/dist/esm/subagentSpawn.js +206 -6
  36. package/dist/esm/testing/codexSubagentFixtures.js +336 -0
  37. package/dist/esm/toolCallOrigins.js +17 -10
  38. package/dist/esm/toolCategory.js +6 -2
  39. package/dist/esm/transcript.js +6 -2
  40. package/dist/esm/turns/assistantTurn.js +6 -3
  41. package/dist/esm/usage/usageEvents.js +4 -1
  42. package/dist/esm/watchers/eventBridge.js +31 -1
  43. package/dist/esm/watchers/jsonlTail.js +11 -42
  44. package/dist/esm/watchers/jsonlWatcher.js +19 -9
  45. package/dist/esm/watchers/providerReaderWatcher.js +5 -4
  46. package/dist/fileContinuity.d.ts +95 -0
  47. package/dist/fileContinuity.js +187 -0
  48. package/dist/index.d.ts +14 -7
  49. package/dist/index.js +39 -10
  50. package/dist/node.d.ts +4 -0
  51. package/dist/node.js +16 -2
  52. package/dist/parsers/codexParser.d.ts +44 -0
  53. package/dist/parsers/codexParser.js +192 -3
  54. package/dist/parsers/subagentEndState.d.ts +20 -0
  55. package/dist/parsers/subagentEndState.js +65 -0
  56. package/dist/parsers/subagentScanner.js +4 -0
  57. package/dist/parsers/subagentTraceParser.d.ts +8 -0
  58. package/dist/parsers/subagentTraceParser.js +6 -0
  59. package/dist/promptHistory.d.ts +13 -3
  60. package/dist/promptHistory.js +22 -11
  61. package/dist/providers/claudeCode.js +50 -27
  62. package/dist/providers/codex.d.ts +35 -5
  63. package/dist/providers/codex.js +150 -160
  64. package/dist/providers/codexCompressedReader.d.ts +58 -0
  65. package/dist/providers/codexCompressedReader.js +221 -0
  66. package/dist/providers/codexDatabase.d.ts +25 -0
  67. package/dist/providers/codexDatabase.js +48 -2
  68. package/dist/providers/codexRollouts.d.ts +16 -2
  69. package/dist/providers/codexRollouts.js +20 -2
  70. package/dist/providers/codexSubagents.d.ts +66 -0
  71. package/dist/providers/codexSubagents.js +495 -0
  72. package/dist/providers/compressedRollouts.d.ts +114 -0
  73. package/dist/providers/compressedRollouts.js +380 -0
  74. package/dist/providers/factory.d.ts +5 -0
  75. package/dist/providers/factory.js +4 -1
  76. package/dist/providers/rolloutWalker.d.ts +15 -2
  77. package/dist/providers/rolloutWalker.js +33 -4
  78. package/dist/providers/types.d.ts +22 -1
  79. package/dist/schemas/sessionEvent.d.ts +26 -2
  80. package/dist/schemas/sessionEvent.js +31 -3
  81. package/dist/sessionMonitor.d.ts +6 -0
  82. package/dist/sessionMonitor.js +7 -1
  83. package/dist/sessionPreviews.js +9 -0
  84. package/dist/sessionPromptHistory.d.ts +5 -0
  85. package/dist/sessionPromptHistory.js +5 -36
  86. package/dist/subagentActivity.d.ts +91 -0
  87. package/dist/subagentActivity.js +132 -0
  88. package/dist/subagentDisplayState.d.ts +81 -0
  89. package/dist/subagentDisplayState.js +86 -0
  90. package/dist/subagentSpawn.d.ts +100 -5
  91. package/dist/subagentSpawn.js +218 -7
  92. package/dist/testing/codexSubagentFixtures.d.ts +48 -0
  93. package/dist/testing/codexSubagentFixtures.js +344 -0
  94. package/dist/toolCallOrigins.d.ts +5 -0
  95. package/dist/toolCallOrigins.js +17 -10
  96. package/dist/toolCategory.js +6 -2
  97. package/dist/transcript.js +5 -1
  98. package/dist/turns/assistantTurn.d.ts +4 -1
  99. package/dist/turns/assistantTurn.js +5 -2
  100. package/dist/types/codex.d.ts +71 -2
  101. package/dist/types/sessionEvent.d.ts +38 -3
  102. package/dist/usage/usageEvents.d.ts +4 -1
  103. package/dist/usage/usageEvents.js +4 -1
  104. package/dist/watchers/eventBridge.js +30 -0
  105. package/dist/watchers/jsonlTail.js +11 -42
  106. package/dist/watchers/jsonlWatcher.d.ts +5 -1
  107. package/dist/watchers/jsonlWatcher.js +18 -8
  108. package/dist/watchers/providerReaderWatcher.js +5 -4
  109. 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,5 +1,6 @@
1
1
  import { calculateNormalizedUsageCost, extractNormalizedUsage, } from './usageNormalization';
2
- import { isTaskNotificationSystemEvent } from './subagentSpawn';
2
+ import { isTaskNotificationSystemEvent, isTaskStatusSystemEvent } from './subagentSpawn';
3
+ import { isSubagentActivitySystemEvent } from './subagentActivity';
3
4
  const EMPTY_TOTALS = {
4
5
  uncachedInputTokens: 0,
5
6
  cacheReadTokens: 0,
@@ -27,7 +28,10 @@ export function projectSessionTranscript(events, options = {}) {
27
28
  let gitBranch;
28
29
  events.forEach((event, eventIndex) => {
29
30
  // A reader's synthetic notice that a background task finished, not a message.
30
- if (isTaskNotificationSystemEvent(event))
31
+ if (isTaskNotificationSystemEvent(event) || isTaskStatusSystemEvent(event))
32
+ return;
33
+ // Likewise a subagent lifecycle record (opt-in Codex parser event).
34
+ if (isSubagentActivitySystemEvent(event))
31
35
  return;
32
36
  if (nonEmpty(event.cwd))
33
37
  cwd = event.cwd;
@@ -1,5 +1,5 @@
1
1
  import { formatToolSummary, truncate } from '../formatters/toolSummary';
2
- import { isSubagentSpawnTool } from '../subagentSpawn';
2
+ import { SUBAGENT_SPAWN_TOOL_NAMES } from '../subagentSpawn';
3
3
  const DEFAULT_NARRATION_CHARS = 4000;
4
4
  const DEFAULT_REASONING_CHARS = 8000;
5
5
  const DEFAULT_PROCESS_STEPS = 60;
@@ -22,9 +22,12 @@ export function reasoningSummary(text) {
22
22
  }
23
23
  return { title: null, body: trimmed };
24
24
  }
25
- /** Whether a tool call spawns a subagent (`Agent`, or its earlier name `Task`). */
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
+ */
26
29
  export function isAssistantTurnSubagentTool(toolName) {
27
- return isSubagentSpawnTool(toolName);
30
+ return typeof toolName === 'string' && SUBAGENT_SPAWN_TOOL_NAMES.includes(toolName);
28
31
  }
29
32
  export function segmentAssistantTurn(events, options = {}) {
30
33
  const maxNarrationChars = options.maxNarrationChars ?? DEFAULT_NARRATION_CHARS;
@@ -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,7 +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 { isTaskNotificationSystemEvent, taskNotificationFromEvent } from '../subagentSpawn';
11
+ import { agentsStoppedNoticeFromEvent, describeAgentsStopped, describeTaskStatus, isAgentsKilledSystemEvent, isTaskNotificationSystemEvent, taskNotificationFromEvent, taskStatusFromEvent, } from '../subagentSpawn';
12
+ import { describeSubagentActivity, subagentActivityFromEvent } from '../subagentActivity';
12
13
  /**
13
14
  * Converts a single SessionEvent to one or more FollowEvents.
14
15
  *
@@ -163,6 +164,35 @@ export function toFollowEvents(event, providerId) {
163
164
  });
164
165
  break;
165
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
+ }
166
196
  const text = extractTextContent(event.message?.content);
167
197
  const label = event.message?.sourceLabel || event.message?.role || 'system';
168
198
  const summary = text ||
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import * as fs from 'fs';
6
6
  import { fileHeadOf, HEAD_BYTES, headMatches } from '../fileHead';
7
+ import { FileContinuity, probeFileContinuity } from '../fileContinuity';
7
8
  import { JsonlParser } from '../parsers/jsonl';
8
9
  const DEFAULT_DEBOUNCE_MS = 100;
9
10
  const DEFAULT_CATCHUP_INTERVAL_MS = 30_000;
@@ -128,10 +129,8 @@ class JsonlTailReader {
128
129
  /** Inside a line over `maxLineBytes`: drop bytes until its newline. */
129
130
  skippingLine = false;
130
131
  currentLine = { offset: 0, length: 0 };
131
- /** Identity and prefix of the file the offsets refer to, once read. */
132
- identity = null;
133
- head = null;
134
- lastMtimeMs = -1;
132
+ /** Identity, mtime, and prefix of the file the offsets refer to, once read. */
133
+ continuity = new FileContinuity();
135
134
  maxReadBytes;
136
135
  maxLineBytes;
137
136
  rewritePolicy;
@@ -339,21 +338,10 @@ class JsonlTailReader {
339
338
  }
340
339
  /** Whether the continuity check needs bytes from the file. */
341
340
  probeNeeded(stat) {
342
- return (this.identity === null ||
343
- stat.dev !== this.identity.dev ||
344
- stat.ino !== this.identity.ino ||
345
- stat.size < this.readOffset ||
346
- stat.mtimeMs !== this.lastMtimeMs ||
347
- this.head === null ||
348
- this.head.bytes < Math.min(HEAD_BYTES, stat.size));
341
+ return this.continuity.needsProbe(stat, this.readOffset);
349
342
  }
350
343
  probeSync(fd, stat) {
351
- return {
352
- prefix: readPrefix(fd, stat),
353
- before: this.committedOffset > 0 && this.committedOffset <= stat.size
354
- ? readAt(fd, this.committedOffset - 1, 1)
355
- : null,
356
- };
344
+ return probeFileContinuity(fd, stat, this.committedOffset);
357
345
  }
358
346
  async probeAsync(handle, stat) {
359
347
  const committedOffset = this.committedOffset;
@@ -370,31 +358,16 @@ class JsonlTailReader {
370
358
  * newline. `null` when it can continue.
371
359
  */
372
360
  resetReason(stat, probe) {
373
- if (this.identity && (stat.dev !== this.identity.dev || stat.ino !== this.identity.ino)) {
374
- return 'replaced';
375
- }
376
- if (stat.size < this.readOffset)
377
- return 'truncated';
378
- if (this.identity && stat.mtimeMs !== this.lastMtimeMs && probe) {
379
- if (this.head && !headMatches(this.head, probe.prefix))
380
- return 'rewritten';
381
- if (probe.before && probe.before[0] !== 0x0a)
382
- return 'rewritten';
383
- }
384
- return null;
361
+ return this.continuity.reasonFor(stat, this.readOffset, probe);
385
362
  }
386
363
  /** Records the identity and prefix of the file the offsets now refer to. */
387
364
  adoptFile(stat, probe) {
388
- this.identity = { dev: stat.dev, ino: stat.ino };
389
- this.lastMtimeMs = stat.mtimeMs;
390
- if (probe && (!this.head || this.head.bytes < Math.min(HEAD_BYTES, stat.size))) {
391
- this.head = fileHeadOf(probe.prefix);
392
- }
365
+ this.continuity.record(stat, probe);
393
366
  }
394
367
  reset(reason, offset) {
395
368
  const previousOffset = this.committedOffset;
396
369
  this.position(offset);
397
- this.head = null;
370
+ this.continuity.discardHead();
398
371
  if (reason === 'replaced' && this.active) {
399
372
  // fs.watch follows the old inode; watch the file now at the path.
400
373
  this.fsWatcher?.close();
@@ -469,7 +442,7 @@ class JsonlTailReader {
469
442
  this.position(Math.max(0, offset));
470
443
  }
471
444
  checkpoint() {
472
- return checkpointOf(this.committedOffset, this.identity, this.head);
445
+ return checkpointOf(this.committedOffset, this.continuity.knownIdentity, this.continuity.knownHead);
473
446
  }
474
447
  resumeFrom(checkpoint) {
475
448
  this.generation += 1;
@@ -487,16 +460,12 @@ class JsonlTailReader {
487
460
  ? lastCompleteLineOffset(fd, stat.size)
488
461
  : 0;
489
462
  this.position(offset);
490
- this.identity = { dev: stat.dev, ino: stat.ino };
491
- this.lastMtimeMs = stat.mtimeMs;
492
- this.head = fileHeadOf(prefix);
463
+ this.continuity.adoptPrefix(stat, prefix);
493
464
  return matches;
494
465
  }
495
466
  catch (error) {
496
467
  this.position(0);
497
- this.identity = null;
498
- this.head = null;
499
- this.lastMtimeMs = -1;
468
+ this.continuity.forget();
500
469
  this.options.onError?.(toError(error));
501
470
  return false;
502
471
  }
@@ -6,7 +6,7 @@ import { createJsonlTail } from './jsonlTail';
6
6
  import { codexTokenUsageFrom, normalizeCodexToolName, resolveCodexCallUsage, } from '../parsers/codexParser';
7
7
  import { formatToolSummary } from '../formatters/toolSummary';
8
8
  import { ClaudeUsageDeduper, normalizeClaudeUsage } from '../usage/claudeUsageDedupe';
9
- import { parseTaskNotification, taskNotificationPromptFromAttachment } from '../subagentSpawn';
9
+ import { describeAgentsStopped, describeTaskStatus, isAgentsKilledSystemEvent, isFinalTaskStatus, parseAgentsStoppedNotice, parseTaskNotification, taskNotificationPromptFromAttachment, taskStatusFromAttachment, } from '../subagentSpawn';
10
10
  // ── Normalizers ──
11
11
  /**
12
12
  * Split lines of one Claude response repeat its usage; run the raw usage
@@ -136,15 +136,25 @@ function normalizeClaudeCodeEvent(raw, deduper, includeTaskNotifications) {
136
136
  }
137
137
  else if (includeTaskNotifications) {
138
138
  // A task notification that landed mid-turn is only an attachment line.
139
- const notification = parseTaskNotification(taskNotificationPromptFromAttachment(raw));
139
+ const prompt = taskNotificationPromptFromAttachment(raw);
140
+ const notification = parseTaskNotification(prompt);
141
+ const stopped = notification ? null : parseAgentsStoppedNotice(prompt);
142
+ const taskStatus = taskStatusFromAttachment(raw);
143
+ let summary = null;
140
144
  if (notification) {
141
- events.push({
142
- providerId: 'claude-code',
143
- type: 'system',
144
- timestamp: ts,
145
- summary: notification.summary ?? `Background task ${notification.status ?? 'finished'}`,
146
- raw,
147
- });
145
+ summary = notification.summary ?? `Background task ${notification.status ?? 'finished'}`;
146
+ }
147
+ else if (stopped) {
148
+ summary = describeAgentsStopped(stopped);
149
+ }
150
+ else if (taskStatus && isFinalTaskStatus(taskStatus.status)) {
151
+ summary = describeTaskStatus(taskStatus);
152
+ }
153
+ else if (isAgentsKilledSystemEvent(raw)) {
154
+ summary = describeAgentsStopped(null);
155
+ }
156
+ if (summary !== null) {
157
+ events.push({ providerId: 'claude-code', type: 'system', timestamp: ts, summary, raw });
148
158
  }
149
159
  }
150
160
  }