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
@@ -14,6 +14,7 @@
14
14
  import { isAggregateCodexLimit } from '../types/codex';
15
15
  import { normalizeToolName } from './openCodeParser';
16
16
  import { normalizeProviderUsage } from '../usageNormalization';
17
+ import { SUBAGENT_ACTIVITY_BLOCK_TYPE, SUBAGENT_ACTIVITY_ORIGIN_KIND, parseCodexAgentMessageText, } from '../subagentActivity';
17
18
  function reportedCompaction(tokensBefore, tokensAfter) {
18
19
  if (typeof tokensBefore !== 'number' ||
19
20
  typeof tokensAfter !== 'number' ||
@@ -171,6 +172,84 @@ export function extractPatchFilePaths(input) {
171
172
  }
172
173
  /** Default cap on each piece of per-call correlation state the parser keeps. */
173
174
  export const DEFAULT_MAX_TRACKED_TOOL_CALLS = 4096;
175
+ /** Entries per table {@link CodexRolloutParser.exportResumeState} keeps by default. */
176
+ const DEFAULT_RESUME_STATE_LIMIT = 256;
177
+ /** Raw names of Codex's multi-agent tools (namespace `collaboration`). */
178
+ const COLLABORATION_TOOL_NAMES = new Set([
179
+ 'spawn_agent',
180
+ 'followup_task',
181
+ 'send_message',
182
+ 'interrupt_agent',
183
+ 'wait_agent',
184
+ 'list_agents',
185
+ ]);
186
+ const SUBAGENT_ACTIVITY_KINDS = new Set([
187
+ 'started',
188
+ 'completed',
189
+ 'interacted',
190
+ 'interrupted',
191
+ ]);
192
+ function nonEmptyString(value) {
193
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
194
+ }
195
+ /** `subagent_history_start_ordinal` of a session_meta payload, when valid. */
196
+ function historyStartOrdinalOf(meta) {
197
+ const value = meta?.subagent_history_start_ordinal;
198
+ return typeof value === 'number' && Number.isInteger(value) && value > 0 ? value : null;
199
+ }
200
+ function isPlainObject(value) {
201
+ return !!value && typeof value === 'object' && !Array.isArray(value);
202
+ }
203
+ function cloneJson(value) {
204
+ return JSON.parse(JSON.stringify(value));
205
+ }
206
+ function isOptionalString(value) {
207
+ return value === undefined || typeof value === 'string';
208
+ }
209
+ function isStringArray(value) {
210
+ return Array.isArray(value) && value.every((item) => typeof item === 'string');
211
+ }
212
+ function isTokenCount(value) {
213
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0;
214
+ }
215
+ /** A valid `CodexTokenUsage` (copied), `null` for `null`, or `undefined` when invalid. Internal. */
216
+ export function codexTokenUsageFrom(value) {
217
+ if (value === null)
218
+ return null;
219
+ if (!isPlainObject(value))
220
+ return undefined;
221
+ if (!isTokenCount(value.input_tokens) || !isTokenCount(value.output_tokens))
222
+ return undefined;
223
+ const usage = {
224
+ input_tokens: value.input_tokens,
225
+ output_tokens: value.output_tokens,
226
+ };
227
+ for (const field of ['cached_input_tokens', 'reasoning_output_tokens', 'total_tokens']) {
228
+ const count = value[field];
229
+ if (count === undefined)
230
+ continue;
231
+ if (!isTokenCount(count))
232
+ return undefined;
233
+ usage[field] = count;
234
+ }
235
+ return usage;
236
+ }
237
+ /** Last `limit` items of an insertion-ordered collection, oldest first. */
238
+ function newest(items, size, limit) {
239
+ const count = limit >= 0 ? Math.min(size, Math.floor(limit)) : 0;
240
+ const kept = [];
241
+ let skip = size - count;
242
+ for (const item of items) {
243
+ if (kept.length >= count)
244
+ break;
245
+ if (skip > 0) {
246
+ skip -= 1;
247
+ continue;
248
+ }
249
+ kept.push(item);
250
+ }
251
+ return kept;
252
+ }
174
253
  /** Fixed per-entry cost added to each pending call's byte estimate. */
175
254
  const PENDING_ENTRY_OVERHEAD_BYTES = 64;
176
255
  /** Prefix of the bookkeeping keys the parser stashes inside tool inputs. */
@@ -235,6 +314,15 @@ export class CodexRolloutParser {
235
314
  /** Byte budget for pending calls; `null` = unlimited (no accounting). */
236
315
  maxPendingBytes;
237
316
  metadataOnly;
317
+ includeSubagentActivity;
318
+ /**
319
+ * Set once a forked child's rollout embeds an ancestor's session_meta: its
320
+ * own `subagent_history_start_ordinal`, below which records are the
321
+ * parent's history and are skipped. `null` while nothing is skipped.
322
+ */
323
+ historyStartOrdinal = null;
324
+ /** Multi-agent tool calls (call id → raw name), only with `includeSubagentActivity`. */
325
+ collaborationCalls = new Map();
238
326
  /** Pending calls in insertion order, for the byte budget. */
239
327
  pendingLedger = new Map();
240
328
  pendingBytes = 0;
@@ -250,6 +338,7 @@ export class CodexRolloutParser {
250
338
  ? Math.floor(maxBytes)
251
339
  : null;
252
340
  this.metadataOnly = options.metadataOnly === true;
341
+ this.includeSubagentActivity = options.includeSubagentActivity === true;
253
342
  }
254
343
  /** Counters for the pending tool-call state and what the caps evicted. */
255
344
  getPendingStats() {
@@ -282,13 +371,172 @@ export class CodexRolloutParser {
282
371
  getLastRateLimits() {
283
372
  return this.lastRateLimits;
284
373
  }
374
+ /**
375
+ * In a forked subagent's rollout, the ordinal of its first own record:
376
+ * records with a lower `ordinal` are the parent history the fork embeds
377
+ * and produce no events. Known once the rollout's second line (an embedded
378
+ * ancestor `session_meta`) was parsed; `null` otherwise, and for every
379
+ * rollout that is not such a fork.
380
+ */
381
+ getInheritedHistoryEnd() {
382
+ return this.historyStartOrdinal;
383
+ }
384
+ /**
385
+ * The state later lines depend on — session metadata, current model,
386
+ * previous cumulative token total, context window, rate limits, plan mode,
387
+ * and the tool-call correlation tables (newest `limit` entries each) — as
388
+ * JSON-serializable data. Eviction counters are not included.
389
+ */
390
+ exportResumeState(limit = DEFAULT_RESUME_STATE_LIMIT) {
391
+ return cloneJson({
392
+ version: 1,
393
+ sessionMeta: this.sessionMeta,
394
+ currentModel: this.currentModel,
395
+ prevTotalUsage: this.prevTotalUsage,
396
+ lastTokenUsage: this.lastTokenUsage,
397
+ modelContextWindow: this.modelContextWindow,
398
+ lastRateLimits: this.lastRateLimits,
399
+ hasAggregateRateLimits: this.hasAggregateRateLimits,
400
+ inPlanMode: this.inPlanMode,
401
+ emittedToolUseIds: newest(this.emittedToolUseIds, this.emittedToolUseIds.size, limit),
402
+ patchExpandedToolUseIds: newest(this.patchExpandedToolUseIds, this.patchExpandedToolUseIds.size, limit),
403
+ pendingExecCommands: newest(this.pendingExecCommands.values(), this.pendingExecCommands.size, limit),
404
+ pendingMcpToolCalls: newest(this.pendingMcpToolCalls.values(), this.pendingMcpToolCalls.size, limit),
405
+ ...(this.historyStartOrdinal !== null
406
+ ? { inheritedHistoryEnd: this.historyStartOrdinal }
407
+ : {}),
408
+ ...(this.collaborationCalls.size > 0
409
+ ? {
410
+ collaborationCalls: newest(this.collaborationCalls, this.collaborationCalls.size, limit),
411
+ }
412
+ : {}),
413
+ });
414
+ }
415
+ /**
416
+ * Replaces this parser's state with one from {@link exportResumeState}, as
417
+ * if it had parsed the rollout up to where that state was taken. Returns
418
+ * false, changing nothing, when `state` is not a valid version-1 state.
419
+ * This parser's own `maxTrackedToolCalls`, `maxPendingBytes`, and
420
+ * `metadataOnly` options apply to the imported tables.
421
+ */
422
+ importResumeState(state) {
423
+ if (!isPlainObject(state) || state.version !== 1)
424
+ return false;
425
+ const s = state;
426
+ const prevTotalUsage = codexTokenUsageFrom(s.prevTotalUsage);
427
+ const lastTokenUsage = codexTokenUsageFrom(s.lastTokenUsage);
428
+ if (prevTotalUsage === undefined || lastTokenUsage === undefined)
429
+ return false;
430
+ if (s.sessionMeta !== null && !isPlainObject(s.sessionMeta))
431
+ return false;
432
+ if (s.lastRateLimits !== null && !isPlainObject(s.lastRateLimits))
433
+ return false;
434
+ if (s.currentModel !== null && typeof s.currentModel !== 'string')
435
+ return false;
436
+ const contextWindow = s.modelContextWindow;
437
+ if (contextWindow !== null &&
438
+ !(typeof contextWindow === 'number' && Number.isFinite(contextWindow) && contextWindow > 0)) {
439
+ return false;
440
+ }
441
+ if (typeof s.hasAggregateRateLimits !== 'boolean' || typeof s.inPlanMode !== 'boolean') {
442
+ return false;
443
+ }
444
+ if (!isStringArray(s.emittedToolUseIds))
445
+ return false;
446
+ const patches = s.patchExpandedToolUseIds;
447
+ if (!Array.isArray(patches) ||
448
+ !patches.every((entry) => Array.isArray(entry) &&
449
+ entry.length === 2 &&
450
+ typeof entry[0] === 'string' &&
451
+ isStringArray(entry[1]))) {
452
+ return false;
453
+ }
454
+ const execs = s.pendingExecCommands;
455
+ if (!Array.isArray(execs) ||
456
+ !execs.every((entry) => isPlainObject(entry) &&
457
+ typeof entry.call_id === 'string' &&
458
+ isStringArray(entry.command) &&
459
+ isOptionalString(entry.workdir) &&
460
+ typeof entry.timestamp === 'string')) {
461
+ return false;
462
+ }
463
+ const mcps = s.pendingMcpToolCalls;
464
+ if (!Array.isArray(mcps) ||
465
+ !mcps.every((entry) => isPlainObject(entry) &&
466
+ typeof entry.call_id === 'string' &&
467
+ typeof entry.tool_name === 'string' &&
468
+ isOptionalString(entry.server_name) &&
469
+ (entry.arguments === undefined || isPlainObject(entry.arguments)) &&
470
+ typeof entry.timestamp === 'string')) {
471
+ return false;
472
+ }
473
+ const inheritedEnd = s.inheritedHistoryEnd;
474
+ if (inheritedEnd !== undefined &&
475
+ inheritedEnd !== null &&
476
+ !(typeof inheritedEnd === 'number' && Number.isInteger(inheritedEnd) && inheritedEnd > 0)) {
477
+ return false;
478
+ }
479
+ const collaborationCalls = s.collaborationCalls;
480
+ if (collaborationCalls !== undefined &&
481
+ !(Array.isArray(collaborationCalls) &&
482
+ collaborationCalls.every((entry) => Array.isArray(entry) &&
483
+ entry.length === 2 &&
484
+ typeof entry[0] === 'string' &&
485
+ typeof entry[1] === 'string'))) {
486
+ return false;
487
+ }
488
+ const copy = cloneJson({
489
+ sessionMeta: s.sessionMeta,
490
+ lastRateLimits: s.lastRateLimits,
491
+ patches: patches,
492
+ execs: execs,
493
+ mcps: mcps,
494
+ });
495
+ this.reset();
496
+ this.sessionMeta = copy.sessionMeta;
497
+ this.historyStartOrdinal = typeof inheritedEnd === 'number' ? inheritedEnd : null;
498
+ this.currentModel = s.currentModel;
499
+ this.prevTotalUsage = prevTotalUsage;
500
+ this.lastTokenUsage = lastTokenUsage;
501
+ this.modelContextWindow = contextWindow;
502
+ this.lastRateLimits = copy.lastRateLimits;
503
+ this.hasAggregateRateLimits = s.hasAggregateRateLimits;
504
+ this.inPlanMode = s.inPlanMode;
505
+ for (const id of s.emittedToolUseIds) {
506
+ rememberBounded(this.emittedToolUseIds, id, this.maxTrackedToolCalls);
507
+ }
508
+ for (const [callId, toolUseIds] of copy.patches)
509
+ this.putPendingPatch(callId, toolUseIds);
510
+ for (const pending of copy.execs) {
511
+ this.putPendingExec(this.metadataOnly
512
+ ? { call_id: pending.call_id, command: [], timestamp: pending.timestamp }
513
+ : pending);
514
+ }
515
+ for (const pending of copy.mcps) {
516
+ this.putPendingMcp(this.metadataOnly ? { ...pending, arguments: undefined } : pending);
517
+ }
518
+ if (this.includeSubagentActivity && Array.isArray(collaborationCalls)) {
519
+ for (const [callId, name] of collaborationCalls) {
520
+ this.rememberCollaborationCall(callId, name);
521
+ }
522
+ }
523
+ return true;
524
+ }
285
525
  /**
286
526
  * Converts a single rollout line to zero or more session events.
287
527
  */
288
528
  convertLine(line) {
529
+ if (line.type === 'session_meta') {
530
+ return this.handleSessionMeta(line.timestamp, line.payload, line.ordinal);
531
+ }
532
+ // A forked subagent rollout embeds the parent's history before its own
533
+ // records; that history is the parent's, not the child's.
534
+ if (this.historyStartOrdinal !== null &&
535
+ typeof line.ordinal === 'number' &&
536
+ line.ordinal < this.historyStartOrdinal) {
537
+ return [];
538
+ }
289
539
  switch (line.type) {
290
- case 'session_meta':
291
- return this.handleSessionMeta(line.timestamp, line.payload);
292
540
  case 'response_item':
293
541
  return this.handleResponseItem(line.timestamp, line.payload);
294
542
  case 'compacted':
@@ -304,6 +552,8 @@ export class CodexRolloutParser {
304
552
  /** Reset all parser state. */
305
553
  reset() {
306
554
  this.sessionMeta = null;
555
+ this.historyStartOrdinal = null;
556
+ this.collaborationCalls.clear();
307
557
  this.currentModel = null;
308
558
  this.pendingExecCommands.clear();
309
559
  this.pendingMcpToolCalls.clear();
@@ -353,6 +603,16 @@ export class CodexRolloutParser {
353
603
  this.evictedByBytes += 1;
354
604
  }
355
605
  }
606
+ putPendingExec(pending) {
607
+ this.putPending(this.pendingExecCommands, pending.call_id, pending, () => stringBytes(pending.call_id, pending.workdir, pending.timestamp, ...(Array.isArray(pending.command) ? pending.command : [])));
608
+ }
609
+ putPendingMcp(pending) {
610
+ this.putPending(this.pendingMcpToolCalls, pending.call_id, pending, () => stringBytes(pending.call_id, pending.server_name, pending.tool_name, pending.timestamp) +
611
+ approxJsonBytes(pending.arguments));
612
+ }
613
+ putPendingPatch(callId, toolUseIds) {
614
+ this.putPending(this.patchExpandedToolUseIds, callId, toolUseIds, () => stringBytes(callId, ...toolUseIds));
615
+ }
356
616
  /** Removes and returns a pending call. */
357
617
  takePending(table, callId) {
358
618
  const value = table.get(callId);
@@ -383,7 +643,25 @@ export class CodexRolloutParser {
383
643
  return this.metadataOnly ? '' : content;
384
644
  }
385
645
  // --- Handlers ---
386
- handleSessionMeta(timestamp, payload) {
646
+ handleSessionMeta(timestamp, payload, ordinal) {
647
+ if (this.sessionMeta) {
648
+ // Only the rollout's own (first) session_meta counts. A forked child
649
+ // embeds its parent's right after its own, followed by the parent's
650
+ // history up to the child's `subagent_history_start_ordinal`; from
651
+ // here on, records below that ordinal are skipped. (Children whose
652
+ // rollout has no embedded meta keep every record: in some, Codex
653
+ // recorded a start ordinal past their own records.)
654
+ const start = historyStartOrdinalOf(this.sessionMeta);
655
+ if (this.historyStartOrdinal === null &&
656
+ start !== null &&
657
+ typeof ordinal === 'number' &&
658
+ ordinal < start) {
659
+ this.historyStartOrdinal = start;
660
+ }
661
+ return [];
662
+ }
663
+ if (!payload || typeof payload !== 'object')
664
+ return [];
387
665
  this.sessionMeta = payload;
388
666
  const text = payload.base_instructions?.text?.trim();
389
667
  return [
@@ -423,10 +701,91 @@ export class CodexRolloutParser {
423
701
  return this.handleCustomToolCall(timestamp, payload);
424
702
  case 'custom_tool_call_output':
425
703
  return this.handleCustomToolCallOutput(timestamp, payload);
704
+ case 'agent_message':
705
+ return this.handleAgentMessage(timestamp, payload);
426
706
  default:
427
707
  return [];
428
708
  }
429
709
  }
710
+ // --- Subagent activity ---
711
+ rememberCollaborationCall(callId, name) {
712
+ this.collaborationCalls.delete(callId);
713
+ this.collaborationCalls.set(callId, name);
714
+ while (this.collaborationCalls.size > this.maxTrackedToolCalls) {
715
+ this.collaborationCalls.delete(this.collaborationCalls.keys().next().value);
716
+ }
717
+ }
718
+ subagentActivityEvent(timestamp, id, activity) {
719
+ return {
720
+ type: 'system',
721
+ timestamp,
722
+ origin: { kind: SUBAGENT_ACTIVITY_ORIGIN_KIND },
723
+ message: {
724
+ role: 'system',
725
+ id: `subagent-activity:${id}`,
726
+ sourceLabel: 'subagent activity',
727
+ content: [{ type: SUBAGENT_ACTIVITY_BLOCK_TYPE, ...activity }],
728
+ },
729
+ providerMetadata: { providerId: 'codex', source: 'codex-rollout' },
730
+ };
731
+ }
732
+ /** A child's `FINAL_ANSWER` message to its parent (opt-in). */
733
+ handleAgentMessage(timestamp, item) {
734
+ if (!this.includeSubagentActivity || !Array.isArray(item.content))
735
+ return [];
736
+ const first = item.content.find((part) => !!part && typeof part === 'object' && typeof part.text === 'string');
737
+ const parsed = parseCodexAgentMessageText(first?.text);
738
+ if (!parsed || parsed.messageType !== 'FINAL_ANSWER')
739
+ return [];
740
+ const agentPath = nonEmptyString(item.author) ?? parsed.sender;
741
+ const recipient = nonEmptyString(item.recipient) ?? parsed.taskName;
742
+ const text = this.metadataOnly ? undefined : nonEmptyString(parsed.payload.trim());
743
+ const activity = {
744
+ kind: 'final-answer',
745
+ ...(agentPath ? { agentPath } : {}),
746
+ ...(recipient ? { recipient } : {}),
747
+ ...(text ? { text } : {}),
748
+ };
749
+ return [
750
+ this.subagentActivityEvent(timestamp, nonEmptyString(item.id) ?? `final-answer-${timestamp}-${agentPath ?? ''}`, activity),
751
+ ];
752
+ }
753
+ /** A `SubAgentActivity` turn item (opt-in). */
754
+ handleItemCompleted(timestamp, event) {
755
+ if (!this.includeSubagentActivity)
756
+ return [];
757
+ const item = event.item;
758
+ if (!item || item.type !== 'SubAgentActivity')
759
+ return [];
760
+ const kind = typeof item.kind === 'string' ? item.kind : '';
761
+ if (!SUBAGENT_ACTIVITY_KINDS.has(kind))
762
+ return [];
763
+ const itemId = nonEmptyString(item.id);
764
+ const completedTurn = kind === 'completed' && itemId?.startsWith('subagent-completed-')
765
+ ? nonEmptyString(itemId.slice('subagent-completed-'.length))
766
+ : undefined;
767
+ const callId = kind === 'completed' ? undefined : itemId;
768
+ const tool = callId ? this.collaborationCalls.get(callId) : undefined;
769
+ const fields = {
770
+ callId,
771
+ tool,
772
+ turnId: completedTurn,
773
+ agentThreadId: nonEmptyString(item.agent_thread_id),
774
+ agentPath: nonEmptyString(item.agent_path),
775
+ model: nonEmptyString(item.model),
776
+ reasoningEffort: nonEmptyString(item.reasoning_effort),
777
+ parentThreadId: nonEmptyString(event.thread_id),
778
+ parentTurnId: nonEmptyString(event.turn_id),
779
+ };
780
+ const activity = { kind: kind };
781
+ for (const [key, value] of Object.entries(fields)) {
782
+ if (value !== undefined)
783
+ activity[key] = value;
784
+ }
785
+ return [
786
+ this.subagentActivityEvent(timestamp, itemId ?? `${kind}-${timestamp}-${activity.agentThreadId ?? ''}`, activity),
787
+ ];
788
+ }
430
789
  handleMessage(timestamp, item) {
431
790
  const text = extractTextFromContent(item.content);
432
791
  if (!text)
@@ -508,6 +867,11 @@ export class CodexRolloutParser {
508
867
  const canonicalName = normalizeCodexToolName(item.name);
509
868
  const normalizedInput = normalizeCodexToolInput(canonicalName, item.name, parsedArgs);
510
869
  rememberBounded(this.emittedToolUseIds, item.call_id, this.maxTrackedToolCalls);
870
+ if (this.includeSubagentActivity &&
871
+ typeof item.call_id === 'string' &&
872
+ (item.namespace === 'collaboration' || COLLABORATION_TOOL_NAMES.has(item.name))) {
873
+ this.rememberCollaborationCall(item.call_id, item.name);
874
+ }
511
875
  return [
512
876
  {
513
877
  type: 'assistant',
@@ -582,7 +946,7 @@ export class CodexRolloutParser {
582
946
  if (filePaths.length === 0)
583
947
  return [];
584
948
  const toolUseIds = filePaths.map((fp) => `${item.call_id}-${fp}`);
585
- this.putPending(this.patchExpandedToolUseIds, item.call_id, toolUseIds, () => stringBytes(item.call_id, ...toolUseIds));
949
+ this.putPendingPatch(item.call_id, toolUseIds);
586
950
  for (const id of toolUseIds) {
587
951
  rememberBounded(this.emittedToolUseIds, id, this.maxTrackedToolCalls);
588
952
  }
@@ -732,7 +1096,7 @@ export class CodexRolloutParser {
732
1096
  const pending = this.metadataOnly
733
1097
  ? { call_id: e.call_id, command: [], timestamp }
734
1098
  : { call_id: e.call_id, command: e.command, workdir: e.workdir, timestamp };
735
- this.putPending(this.pendingExecCommands, e.call_id, pending, () => stringBytes(pending.call_id, pending.workdir, pending.timestamp, ...(Array.isArray(pending.command) ? pending.command : [])));
1099
+ this.putPendingExec(pending);
736
1100
  return [];
737
1101
  }
738
1102
  case 'exec_command_end': {
@@ -795,8 +1159,7 @@ export class CodexRolloutParser {
795
1159
  arguments: this.metadataOnly ? undefined : e.arguments,
796
1160
  timestamp,
797
1161
  };
798
- this.putPending(this.pendingMcpToolCalls, e.call_id, pending, () => stringBytes(pending.call_id, pending.server_name, pending.tool_name, timestamp) +
799
- approxJsonBytes(pending.arguments));
1162
+ this.putPendingMcp(pending);
800
1163
  return [];
801
1164
  }
802
1165
  case 'mcp_tool_call_end': {
@@ -965,6 +1328,8 @@ export class CodexRolloutParser {
965
1328
  timestamp,
966
1329
  },
967
1330
  ];
1331
+ case 'item_completed':
1332
+ return this.handleItemCompleted(timestamp, event);
968
1333
  case 'turn_aborted':
969
1334
  case 'agent_reasoning':
970
1335
  case 'agent_message':
@@ -11,6 +11,7 @@
11
11
  * @module parsers/openCodeParser
12
12
  */
13
13
  import { normalizeProviderUsage } from '../usageNormalization';
14
+ import { ownValue } from '../ownKey';
14
15
  /**
15
16
  * OpenCode → Claude Code tool name mapping.
16
17
  * OpenCode uses lowercase names; downstream code expects PascalCase.
@@ -46,7 +47,7 @@ export function normalizeToolName(name) {
46
47
  if (!name)
47
48
  return name;
48
49
  // Check mapping (case-insensitive lookup via lowercase key)
49
- const mapped = TOOL_NAME_MAP[name.toLowerCase()];
50
+ const mapped = ownValue(TOOL_NAME_MAP, name.toLowerCase());
50
51
  if (mapped)
51
52
  return mapped;
52
53
  // Already PascalCase (starts with uppercase)? Pass through.
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Whether a Claude Code subagent transcript ended cleanly, read line by line.
3
+ *
4
+ * Clean means: the last user or assistant line is an assistant line that
5
+ * stopped with `end_turn` or `stop_sequence`, and every `tool_use` in the
6
+ * transcript has its `tool_result`. A user line after that (a resumed agent's
7
+ * new prompt) makes it unfinished again.
8
+ *
9
+ * Pure. Internal to the subagent parsers.
10
+ *
11
+ * @module parsers/subagentEndState
12
+ */
13
+ const ENDED_STOP_REASONS = new Set(['end_turn', 'stop_sequence']);
14
+ /** Bounds the unanswered-call set for pathological transcripts. */
15
+ const MAX_PENDING_TOOL_USES = 4096;
16
+ export class SubagentEndTracker {
17
+ unanswered = new Set();
18
+ lastWasCleanAssistant = false;
19
+ /** Feeds one parsed JSONL line, in file order. */
20
+ observe(raw) {
21
+ if (!raw || typeof raw !== 'object')
22
+ return;
23
+ const line = raw;
24
+ if (line.type !== 'assistant' && line.type !== 'user')
25
+ return;
26
+ const message = line.message && typeof line.message === 'object'
27
+ ? line.message
28
+ : undefined;
29
+ const content = Array.isArray(message?.content) ? message.content : [];
30
+ if (line.type === 'user') {
31
+ for (const block of content) {
32
+ if (!block || typeof block !== 'object')
33
+ continue;
34
+ const typed = block;
35
+ if (typed.type === 'tool_result' && typeof typed.tool_use_id === 'string') {
36
+ this.unanswered.delete(typed.tool_use_id);
37
+ }
38
+ }
39
+ this.lastWasCleanAssistant = false;
40
+ return;
41
+ }
42
+ let callsTool = false;
43
+ for (const block of content) {
44
+ if (!block || typeof block !== 'object')
45
+ continue;
46
+ const typed = block;
47
+ if (typed.type !== 'tool_use')
48
+ continue;
49
+ callsTool = true;
50
+ if (typeof typed.id === 'string' && this.unanswered.size < MAX_PENDING_TOOL_USES) {
51
+ this.unanswered.add(typed.id);
52
+ }
53
+ }
54
+ const stop = typeof message?.stop_reason === 'string' ? message.stop_reason : '';
55
+ this.lastWasCleanAssistant = !callsTool && ENDED_STOP_REASONS.has(stop);
56
+ }
57
+ /** Whether the lines so far end cleanly. */
58
+ endedCleanly() {
59
+ return this.lastWasCleanAssistant && this.unanswered.size === 0;
60
+ }
61
+ }
@@ -12,28 +12,50 @@ import * as path from 'path';
12
12
  import { ClaudeUsageDeduper, dedupedRawClaudeUsage } from '../usage/claudeUsageDedupe';
13
13
  import { addUsageToSubagent } from '../usage/subagentUsage';
14
14
  import { cachedSubagentStats } from './subagentStatsCache';
15
+ import { isSubagentSpawnTool } from '../subagentSpawn';
16
+ import { SubagentEndTracker } from './subagentEndState';
15
17
  /**
16
18
  * Pattern for matching subagent JSONL files.
17
19
  * Files are named like: agent-<hash>.jsonl
18
20
  */
19
21
  const AGENT_FILE_PATTERN = /^agent-(.+)\.jsonl$/;
22
+ function metaString(value) {
23
+ return typeof value === 'string' && value ? value : undefined;
24
+ }
20
25
  /**
21
26
  * Reads `agent-<id>.meta.json` (written by Claude Code next to the transcript)
22
- * for the spawn's agent type and description. Missing or malformed files
23
- * yield an empty object.
27
+ * for the spawn's agent type, description, spawning tool_use id, parent agent,
28
+ * and completion flags. Missing or malformed files yield an empty object;
29
+ * wrongly typed fields are left out.
24
30
  */
25
31
  export function readAgentMeta(agentFilePath) {
26
32
  try {
27
33
  const metaPath = agentFilePath.replace(/\.jsonl$/, '.meta.json');
28
34
  const parsed = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
29
- return {
30
- ...(typeof parsed.agentType === 'string' && parsed.agentType
31
- ? { agentType: parsed.agentType }
32
- : {}),
33
- ...(typeof parsed.description === 'string' && parsed.description
34
- ? { description: parsed.description }
35
- : {}),
36
- };
35
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
36
+ return {};
37
+ const record = parsed;
38
+ const meta = {};
39
+ const agentType = metaString(record.agentType);
40
+ if (agentType)
41
+ meta.agentType = agentType;
42
+ const description = metaString(record.description);
43
+ if (description)
44
+ meta.description = description;
45
+ const toolUseId = metaString(record.toolUseId);
46
+ if (toolUseId)
47
+ meta.toolUseId = toolUseId;
48
+ const parentAgentId = metaString(record.parentAgentId);
49
+ if (parentAgentId)
50
+ meta.parentAgentId = parentAgentId;
51
+ const requestShape = metaString(record.requestShape);
52
+ if (requestShape)
53
+ meta.requestShape = requestShape;
54
+ if (typeof record.finished === 'boolean')
55
+ meta.finished = record.finished;
56
+ if (typeof record.stoppedByUser === 'boolean')
57
+ meta.stoppedByUser = record.stoppedByUser;
58
+ return meta;
37
59
  }
38
60
  catch {
39
61
  return {};
@@ -139,6 +161,7 @@ function parseAgentFile(filePath, agentId, log) {
139
161
  let description;
140
162
  const tokens = { agentId, toolCalls: [], inputTokens: 0, outputTokens: 0 };
141
163
  const usageDeduper = new ClaudeUsageDeduper();
164
+ const endTracker = new SubagentEndTracker();
142
165
  let startTime;
143
166
  let endTime;
144
167
  // Task tracking state
@@ -155,6 +178,7 @@ function parseAgentFile(filePath, agentId, log) {
155
178
  if (!startTime)
156
179
  startTime = eventTimestamp;
157
180
  endTime = eventTimestamp;
181
+ endTracker.observe(event);
158
182
  // Extract token usage from assistant messages. Split lines of one
159
183
  // response repeat its usage, so count each response once.
160
184
  const usage = dedupedRawClaudeUsage(event, usageDeduper);
@@ -208,8 +232,8 @@ function parseAgentFile(filePath, agentId, log) {
208
232
  }
209
233
  }
210
234
  toolCalls.push(toolCall);
211
- // If this is a Task tool call (spawning another subagent), extract info
212
- if (toolUse.name === 'Task' && toolUse.input) {
235
+ // If this is an Agent/Task call (spawning another subagent), extract info
236
+ if (isSubagentSpawnTool(toolUse.name) && toolUse.input) {
213
237
  if (toolUse.input.subagent_type && !agentType) {
214
238
  agentType = String(toolUse.input.subagent_type);
215
239
  }
@@ -277,6 +301,7 @@ function parseAgentFile(filePath, agentId, log) {
277
301
  startTime,
278
302
  endTime,
279
303
  durationMs,
304
+ endedCleanly: endTracker.endedCleanly(),
280
305
  };
281
306
  }
282
307
  return null;