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
@@ -18,7 +18,8 @@ import { HeatmapTracker } from './HeatmapTracker';
18
18
  import { PatternExtractor } from './PatternExtractor';
19
19
  import { categorizeError } from '../extractors/errorTaxonomy';
20
20
  import { estimateTextTokens } from '../tokenEstimation';
21
- import { isSubagentSpawnTool, isTaskNotificationSystemEvent, parseAsyncAgentLaunch, parseTaskNotification, taskNotificationFromEvent, taskNotificationOutcome, taskNotificationPromptFromAttachment, } from '../subagentSpawn';
21
+ import { describeSubagentActivity, isSubagentActivitySystemEvent, subagentActivityFromEvent, } from '../subagentActivity';
22
+ import { agentsStoppedNoticeFromEvent, describeAgentsStopped, describeTaskStatus, isAgentsKilledSystemEvent, isCodexSubagentSpawnTool, isFinalTaskStatus, isSubagentSpawnTool, isTaskNotificationSystemEvent, isTaskStatusSystemEvent, parseAgentsStoppedNotice, parseAsyncAgentLaunch, parseCodexSpawnLaunch, parseTaskNotification, taskNotificationFromEvent, taskNotificationOutcome, taskNotificationPromptFromAttachment, taskStatusFromAttachment, taskStatusFromEvent, } from '../subagentSpawn';
22
23
  import { calculateNormalizedUsageCost, extractNormalizedUsage, normalizeProviderUsage, } from '../usageNormalization';
23
24
  // ── Defaults ──
24
25
  const DEFAULT_TIMELINE_CAP = 200;
@@ -27,8 +28,26 @@ const DEFAULT_CONTEXT_TIMELINE_CAP = 2_000;
27
28
  const DEFAULT_BURN_WINDOW_MS = 5 * 60_000;
28
29
  const DEFAULT_BURN_SAMPLE_MS = 10_000;
29
30
  const COMPACTION_DROP_THRESHOLD = 0.8; // >20% drop
30
- /** Schema version for serialized snapshots. v6: token totals count each Claude response and Codex call once. */
31
- export const SNAPSHOT_SCHEMA_VERSION = 6;
31
+ /**
32
+ * Schema version for serialized snapshots. v6: token totals count each Claude
33
+ * response and Codex call once. v7: subagent lifecycles track Codex spawns and
34
+ * record what settled a background agent (`settledBy`).
35
+ */
36
+ export const SNAPSHOT_SCHEMA_VERSION = 7;
37
+ // Checked without the schema module so this file stays free of Zod.
38
+ const PERMISSION_MODES = new Set([
39
+ 'default',
40
+ 'acceptEdits',
41
+ 'bypassPermissions',
42
+ 'plan',
43
+ 'auto',
44
+ 'dontAsk',
45
+ ]);
46
+ function asPermissionMode(value) {
47
+ return typeof value === 'string' && PERMISSION_MODES.has(value)
48
+ ? value
49
+ : null;
50
+ }
32
51
  // ── Goal gate detection regex ──
33
52
  const GOAL_GATE_REGEX = /\b(CRITICAL|MUST|blocker|required|must.?complete|goal.?gate|essential|do.?not.?skip|blocking)\b/i;
34
53
  // ── Task notifications delivered as attachments ──
@@ -47,6 +66,62 @@ function taskNotificationFromAttachmentFollowEvent(event) {
47
66
  const asEvent = raw;
48
67
  return isTaskNotificationSystemEvent(asEvent) ? taskNotificationFromEvent(asEvent) : null;
49
68
  }
69
+ function subagentRecordFromFollowEvent(event) {
70
+ const raw = event.raw;
71
+ if (!raw || typeof raw !== 'object')
72
+ return null;
73
+ const notification = taskNotificationFromAttachmentFollowEvent(event);
74
+ if (notification)
75
+ return { kind: 'notification', notification };
76
+ const asEvent = raw;
77
+ const prompt = taskNotificationPromptFromAttachment(raw);
78
+ const notice = prompt !== null ? parseAgentsStoppedNotice(prompt) : agentsStoppedNoticeFromEvent(asEvent);
79
+ if (notice)
80
+ return { kind: 'stopped', notice };
81
+ if (prompt !== null || isTaskNotificationSystemEvent(asEvent)) {
82
+ // A notification this release cannot read: still bookkeeping.
83
+ return { kind: 'unreadable' };
84
+ }
85
+ const status = taskStatusFromAttachment(raw) ?? taskStatusFromEvent(asEvent);
86
+ if (status)
87
+ return { kind: 'task-status', status };
88
+ if (isSubagentActivitySystemEvent(asEvent)) {
89
+ const activity = subagentActivityFromEvent(asEvent);
90
+ return activity ? { kind: 'activity', activity } : { kind: 'unreadable' };
91
+ }
92
+ if (isAgentsKilledSystemEvent(raw))
93
+ return { kind: 'agents-killed' };
94
+ return null;
95
+ }
96
+ // ── Subagent spawn input ──
97
+ /**
98
+ * Description and type of a spawn: Claude Code's `description` /
99
+ * `subagent_type`, or Codex `spawn_agent`'s `task_name` / `agent_type`.
100
+ */
101
+ function spawnFields(input) {
102
+ const text = (value) => typeof value === 'string' && value ? value : undefined;
103
+ const description = text(input.description) ?? text(input.task_name);
104
+ const subagentType = text(input.subagent_type) ?? text(input.subagentType) ?? text(input.agent_type);
105
+ return {
106
+ ...(description ? { description } : {}),
107
+ ...(subagentType ? { subagentType } : {}),
108
+ };
109
+ }
110
+ /**
111
+ * Whether a spawn's tool result launched the agent in the background or
112
+ * ended it. A Codex `spawn_agent` result is always a launch unless it failed
113
+ * ("collab spawn failed: …"); its `{"task_name": …}` output is a launch even
114
+ * when the output's text looked like an error to the parser (an agent path
115
+ * such as `/root/fix_failed_tests`).
116
+ */
117
+ function spawnResultKind(codex, content, isError) {
118
+ // Read first: the `codex` hint does not survive a snapshot boundary.
119
+ if (parseCodexSpawnLaunch(content))
120
+ return 'launch';
121
+ if (codex)
122
+ return isError ? 'end' : 'launch';
123
+ return !isError && parseAsyncAgentLaunch(content) ? 'launch' : 'end';
124
+ }
50
125
  // ── Todo dependency parsing ──
51
126
  /**
52
127
  * Parses dependency references from a todo item's content text.
@@ -132,6 +207,8 @@ export class EventAggregator {
132
207
  // Subagent lifecycle
133
208
  subagents = [];
134
209
  pendingSubagents = new Map(); // toolUseId -> index
210
+ /** Codex `spawn_agent` calls still waiting for their result (always a background launch). */
211
+ pendingCodexSpawns = new Set();
135
212
  // Plan state
136
213
  planExtractor;
137
214
  planState = null;
@@ -194,8 +271,32 @@ export class EventAggregator {
194
271
  this.applyTaskNotification(notification, event.timestamp);
195
272
  this.addTaskNotificationTimeline(event.timestamp, notification);
196
273
  }
274
+ else {
275
+ // The plain-text "background agents were stopped by the user" notice.
276
+ const notice = agentsStoppedNoticeFromEvent(event);
277
+ if (notice)
278
+ this.applyAgentsStopped(notice, event.timestamp);
279
+ }
197
280
  return;
198
281
  }
282
+ // So are a background agent's final `task_status` (same opt-in) and the
283
+ // opt-in Codex subagent lifecycle records (`subagentActivityFromEvent`).
284
+ if (isTaskStatusSystemEvent(event)) {
285
+ const status = taskStatusFromEvent(event);
286
+ if (status)
287
+ this.applyTaskStatus(status, event.timestamp);
288
+ return;
289
+ }
290
+ if (isSubagentActivitySystemEvent(event)) {
291
+ const activity = subagentActivityFromEvent(event);
292
+ if (activity)
293
+ this.applySubagentActivity(activity, event.timestamp);
294
+ return;
295
+ }
296
+ // The user killed every running background agent. The line itself is
297
+ // counted below as before.
298
+ if (isAgentsKilledSystemEvent(event))
299
+ this.applyAgentsKilled(event.timestamp);
199
300
  // 1. Increment counters, capture timestamps
200
301
  this.eventCount++;
201
302
  if (!this.sessionStartTime) {
@@ -261,12 +362,13 @@ export class EventAggregator {
261
362
  // processFollowEvent — adapter for CLI FollowEvent format
262
363
  // ═══════════════════════════════════════════════════════════════════════
263
364
  processFollowEvent(event) {
264
- // A mid-turn task-notification attachment, like its SessionEvent form above.
365
+ // Subagent bookkeeping (a mid-turn task-notification or task-status
366
+ // attachment, a stop notice or `agents_killed` line, a Codex lifecycle
367
+ // record), like its SessionEvent form above: not counted as activity.
265
368
  if (event.type === 'system') {
266
- const notification = taskNotificationFromAttachmentFollowEvent(event);
267
- if (notification) {
268
- this.applyTaskNotification(notification, event.timestamp);
269
- this.addTaskNotificationTimeline(event.timestamp, notification);
369
+ const record = subagentRecordFromFollowEvent(event);
370
+ if (record) {
371
+ this.applySubagentFollowRecord(record, event.timestamp);
270
372
  return;
271
373
  }
272
374
  }
@@ -276,6 +378,8 @@ export class EventAggregator {
276
378
  this.sessionStartTime = event.timestamp;
277
379
  }
278
380
  this.lastEventTime = event.timestamp;
381
+ // Followed events carry the mode as a plain string.
382
+ this.trackPermissionMode(event);
279
383
  // Provider
280
384
  if (!this._providerId && event.providerId) {
281
385
  this._providerId = event.providerId;
@@ -407,6 +511,10 @@ export class EventAggregator {
407
511
  getSubagents() {
408
512
  return [...this.subagents];
409
513
  }
514
+ /** Timestamp of the last counted event (`getMetrics().lastEventTime`), without building the metrics. */
515
+ getLastEventTime() {
516
+ return this.lastEventTime;
517
+ }
410
518
  getPlan() {
411
519
  return this.planState
412
520
  ? { ...this.planState, steps: this.planState.steps.map((step) => ({ ...step })) }
@@ -503,6 +611,7 @@ export class EventAggregator {
503
611
  this.activeTaskId = null;
504
612
  this.subagents = [];
505
613
  this.pendingSubagents.clear();
614
+ this.pendingCodexSpawns.clear();
506
615
  this.planExtractor.reset();
507
616
  this.planState = null;
508
617
  this.planEnteredAt = undefined;
@@ -636,7 +745,7 @@ export class EventAggregator {
636
745
  this.tasks = new Map(state.tasks);
637
746
  this.activeTaskId = state.activeTaskId;
638
747
  this.subagents = [...state.subagents];
639
- this.currentPermissionMode = state.permissionMode ?? null;
748
+ this.currentPermissionMode = asPermissionMode(state.permissionMode) ?? null;
640
749
  this.permissionModeHistory = state.permissionModeHistory
641
750
  ? [...state.permissionModeHistory]
642
751
  : [];
@@ -663,6 +772,7 @@ export class EventAggregator {
663
772
  this.pendingToolCalls.clear();
664
773
  this.pendingTaskCreates.clear();
665
774
  this.pendingSubagents.clear();
775
+ this.pendingCodexSpawns.clear();
666
776
  // A spawn still waiting for its tool_result is resolved by that result,
667
777
  // which can land after the snapshot boundary. Background agents need no
668
778
  // entry: their notifications are matched against every subagent.
@@ -1149,7 +1259,7 @@ export class EventAggregator {
1149
1259
  else if (name === 'UpdatePlan') {
1150
1260
  this.handleUpdatePlan(input, new Date(event.timestamp));
1151
1261
  }
1152
- else if (isSubagentSpawnTool(name) && toolUseId) {
1262
+ else if (isSubagentSpawnTool(name, input) && toolUseId) {
1153
1263
  this.handleAgentSpawn(toolUseId, input, new Date(event.timestamp));
1154
1264
  }
1155
1265
  }
@@ -1193,7 +1303,7 @@ export class EventAggregator {
1193
1303
  else if (event.toolName === 'UpdatePlan') {
1194
1304
  this.handleUpdatePlan(input, new Date(event.timestamp));
1195
1305
  }
1196
- else if (isSubagentSpawnTool(event.toolName) && toolUseId) {
1306
+ else if (isSubagentSpawnTool(event.toolName, input) && toolUseId) {
1197
1307
  this.handleAgentSpawn(toolUseId, input, new Date(event.timestamp));
1198
1308
  }
1199
1309
  }
@@ -1492,8 +1602,9 @@ export class EventAggregator {
1492
1602
  // ═══════════════════════════════════════════════════════════════════════
1493
1603
  handleAgentSpawn(toolUseId, input, now) {
1494
1604
  const agentTaskId = `agent-${toolUseId}`;
1495
- const description = input.description ? String(input.description) : 'Subagent';
1496
- const subagentType = input.subagent_type || input.subagentType || undefined;
1605
+ const spawn = spawnFields(input);
1606
+ const description = spawn.description ?? 'Subagent';
1607
+ const subagentType = spawn.subagentType;
1497
1608
  const task = {
1498
1609
  taskId: agentTaskId,
1499
1610
  subject: description,
@@ -1516,8 +1627,9 @@ export class EventAggregator {
1516
1627
  if (!agentTask)
1517
1628
  return;
1518
1629
  // A background launch returns at once; its task notification finishes it.
1519
- if (!isError && parseAsyncAgentLaunch(content))
1630
+ if (spawnResultKind(this.pendingCodexSpawns.has(toolUseId), content, isError) === 'launch') {
1520
1631
  return;
1632
+ }
1521
1633
  agentTask.status = isError ? 'deleted' : 'completed';
1522
1634
  agentTask.updatedAt = new Date(timestamp);
1523
1635
  }
@@ -1544,12 +1656,12 @@ export class EventAggregator {
1544
1656
  if (!Array.isArray(content))
1545
1657
  return;
1546
1658
  for (const block of content) {
1547
- if (block.type === 'tool_use' && isSubagentSpawnTool(block.name)) {
1659
+ if (block.type === 'tool_use' && isSubagentSpawnTool(block.name, block.input)) {
1548
1660
  const input = block.input;
1549
1661
  const toolUseId = block.id;
1550
1662
  if (!input || !toolUseId)
1551
1663
  continue;
1552
- this.trackSubagentSpawn(toolUseId, input, event.timestamp);
1664
+ this.trackSubagentSpawn(toolUseId, input, event.timestamp, isCodexSubagentSpawnTool(block.name, input));
1553
1665
  }
1554
1666
  else if (block.type === 'tool_result') {
1555
1667
  const toolUseId = block.tool_use_id;
@@ -1564,10 +1676,10 @@ export class EventAggregator {
1564
1676
  // ═══════════════════════════════════════════════════════════════════════
1565
1677
  extractSubagentFromFollowEvent(event) {
1566
1678
  const raw = event.raw;
1567
- if (event.type === 'tool_use' && isSubagentSpawnTool(event.toolName)) {
1679
+ if (event.type === 'tool_use' && isSubagentSpawnTool(event.toolName, raw?.input)) {
1568
1680
  if (!raw?.input)
1569
1681
  return;
1570
- this.trackSubagentSpawn(raw.id || '', raw.input, event.timestamp);
1682
+ this.trackSubagentSpawn(raw.id || '', raw.input, event.timestamp, isCodexSubagentSpawnTool(event.toolName, raw.input));
1571
1683
  }
1572
1684
  else if (event.type === 'tool_result') {
1573
1685
  if (!raw)
@@ -1584,11 +1696,12 @@ export class EventAggregator {
1584
1696
  this.applyTaskNotification(notification, event.timestamp);
1585
1697
  }
1586
1698
  }
1587
- trackSubagentSpawn(toolUseId, input, timestamp) {
1699
+ trackSubagentSpawn(toolUseId, input, timestamp, codex = false) {
1700
+ const spawn = spawnFields(input);
1588
1701
  const info = {
1589
1702
  id: toolUseId,
1590
- description: input.description || 'Unknown task',
1591
- subagentType: input.subagent_type || input.subagentType || 'general',
1703
+ description: spawn.description ?? 'Unknown task',
1704
+ subagentType: spawn.subagentType ?? (codex ? 'default' : 'general'),
1592
1705
  spawnTime: timestamp,
1593
1706
  status: 'running',
1594
1707
  };
@@ -1596,19 +1709,23 @@ export class EventAggregator {
1596
1709
  this.subagents.push(info);
1597
1710
  if (toolUseId) {
1598
1711
  this.pendingSubagents.set(toolUseId, idx);
1712
+ if (codex)
1713
+ this.pendingCodexSpawns.add(toolUseId);
1599
1714
  }
1600
1715
  }
1601
1716
  /**
1602
1717
  * The spawning tool call's result. A foreground agent ends here; a
1603
- * background launch only reports the agent id, and the agent keeps running
1604
- * until its task notification arrives.
1718
+ * background launch only reports the agent id (a Codex spawn, its agent
1719
+ * path), and the agent keeps running until a notification or another
1720
+ * record says it ended.
1605
1721
  */
1606
1722
  resolveSubagentResult(toolUseId, content, isError, timestamp) {
1607
1723
  const idx = this.pendingSubagents.get(toolUseId);
1608
1724
  if (idx === undefined)
1609
1725
  return;
1610
- const launch = isError ? null : parseAsyncAgentLaunch(content);
1611
- if (launch) {
1726
+ const codex = this.pendingCodexSpawns.delete(toolUseId);
1727
+ if (spawnResultKind(codex, content, isError) === 'launch') {
1728
+ const launch = parseCodexSpawnLaunch(content) ?? parseAsyncAgentLaunch(content) ?? {};
1612
1729
  this.markBackgroundLaunch(idx, launch);
1613
1730
  return;
1614
1731
  }
@@ -1620,6 +1737,8 @@ export class EventAggregator {
1620
1737
  agent.background = true;
1621
1738
  if (launch.agentId)
1622
1739
  agent.agentId = launch.agentId;
1740
+ if (launch.agentPath && !agent.agentPath)
1741
+ agent.agentPath = launch.agentPath;
1623
1742
  }
1624
1743
  completeSubagent(toolUseId, timestamp, outcome = 'completed') {
1625
1744
  const idx = this.pendingSubagents.get(toolUseId);
@@ -1627,16 +1746,58 @@ export class EventAggregator {
1627
1746
  return;
1628
1747
  this.finishSubagent(this.subagents[idx], timestamp, outcome);
1629
1748
  }
1630
- finishSubagent(agent, timestamp, outcome) {
1749
+ finishSubagent(agent, timestamp, outcome, settledBy) {
1631
1750
  this.pendingSubagents.delete(agent.id);
1751
+ this.pendingCodexSpawns.delete(agent.id);
1632
1752
  agent.status = 'completed';
1633
1753
  agent.completionTime = timestamp;
1634
1754
  agent.outcome = outcome;
1755
+ if (settledBy)
1756
+ agent.settledBy = settledBy;
1757
+ else
1758
+ delete agent.settledBy;
1635
1759
  const start = new Date(agent.spawnTime).getTime();
1636
1760
  const end = new Date(timestamp).getTime();
1637
1761
  if (!isNaN(start) && !isNaN(end) && end >= start) {
1638
1762
  agent.durationMs = end - start;
1639
1763
  }
1764
+ else {
1765
+ delete agent.durationMs;
1766
+ }
1767
+ }
1768
+ /** Puts a settled agent back to running (it resumed). */
1769
+ reopenSubagent(agent, timestamp) {
1770
+ this.pendingSubagents.delete(agent.id);
1771
+ agent.status = 'running';
1772
+ delete agent.completionTime;
1773
+ delete agent.durationMs;
1774
+ delete agent.outcome;
1775
+ delete agent.settledBy;
1776
+ this.settleAgentTask(agent.id, null, timestamp);
1777
+ }
1778
+ /**
1779
+ * Settles a lifecycle that is still running (and its task card). Agents
1780
+ * already settled are left alone: a stop notice or status can name agents
1781
+ * that finished earlier.
1782
+ */
1783
+ settleRunningSubagent(agent, timestamp, outcome, settledBy) {
1784
+ if (agent.status !== 'running')
1785
+ return false;
1786
+ this.finishSubagent(agent, timestamp, outcome, settledBy);
1787
+ this.settleAgentTask(agent.id, outcome, timestamp);
1788
+ return true;
1789
+ }
1790
+ /**
1791
+ * Moves an agent's task card with its lifecycle: `null` back to in
1792
+ * progress, a failure to deleted, any other outcome to completed.
1793
+ */
1794
+ settleAgentTask(toolUseId, outcome, timestamp) {
1795
+ const agentTask = toolUseId ? this.tasks.get(`agent-${toolUseId}`) : undefined;
1796
+ if (!agentTask?.isSubagent)
1797
+ return;
1798
+ agentTask.status =
1799
+ outcome === null ? 'in_progress' : outcome === 'failed' ? 'deleted' : 'completed';
1800
+ agentTask.updatedAt = new Date(timestamp);
1640
1801
  }
1641
1802
  /**
1642
1803
  * Applies a background task's notification to the subagent it reports on,
@@ -1659,20 +1820,13 @@ export class EventAggregator {
1659
1820
  delete agent.completionTime;
1660
1821
  delete agent.durationMs;
1661
1822
  delete agent.outcome;
1823
+ delete agent.settledBy;
1662
1824
  }
1663
1825
  else {
1664
- this.finishSubagent(agent, timestamp, outcome);
1826
+ this.finishSubagent(agent, timestamp, outcome, 'notification');
1665
1827
  }
1666
1828
  }
1667
- const agentTask = toolUseId ? this.tasks.get(`agent-${toolUseId}`) : undefined;
1668
- if (agentTask?.isSubagent) {
1669
- agentTask.status = notification.interim
1670
- ? 'in_progress'
1671
- : outcome === 'failed'
1672
- ? 'deleted'
1673
- : 'completed';
1674
- agentTask.updatedAt = new Date(timestamp);
1675
- }
1829
+ this.settleAgentTask(toolUseId, notification.interim ? null : outcome, timestamp);
1676
1830
  }
1677
1831
  findNotifiedSubagent(notification) {
1678
1832
  const { toolUseId, taskId } = notification;
@@ -1686,6 +1840,206 @@ export class EventAggregator {
1686
1840
  }
1687
1841
  return undefined;
1688
1842
  }
1843
+ // ═══════════════════════════════════════════════════════════════════════
1844
+ // Subagent settling: evidence, stop markers, task statuses, Codex records
1845
+ // ═══════════════════════════════════════════════════════════════════════
1846
+ /**
1847
+ * Applies host evidence about a subagent (Claude Code's meta flags):
1848
+ * `stoppedByUser` settles a running agent as `stopped`, `finished` as
1849
+ * `completed` (`settledBy: 'meta'`). Agents already settled some other way
1850
+ * keep their outcome; `finished: false` reopens an agent that only meta
1851
+ * settled. Matches by `toolUseId`, then `agentId` (the newest such agent),
1852
+ * and records the agent id when matched by tool_use id.
1853
+ *
1854
+ * @param timestamp ISO time to record as the completion (the agent's last
1855
+ * activity, when the host knows it)
1856
+ * @returns Whether a lifecycle changed
1857
+ */
1858
+ applySubagentEvidence(evidence, timestamp) {
1859
+ const agent = this.findSubagent(evidence.toolUseId, evidence.agentId);
1860
+ if (!agent)
1861
+ return false;
1862
+ let changed = false;
1863
+ if (evidence.agentId && !agent.agentId) {
1864
+ agent.agentId = evidence.agentId;
1865
+ changed = true;
1866
+ }
1867
+ if (agent.status === 'running') {
1868
+ const outcome = evidence.stoppedByUser
1869
+ ? 'stopped'
1870
+ : evidence.finished
1871
+ ? 'completed'
1872
+ : null;
1873
+ if (outcome)
1874
+ changed = this.settleRunningSubagent(agent, timestamp, outcome, 'meta') || changed;
1875
+ }
1876
+ else if (agent.settledBy === 'meta' &&
1877
+ evidence.finished === false &&
1878
+ evidence.stoppedByUser !== true) {
1879
+ this.reopenSubagent(agent, timestamp);
1880
+ changed = true;
1881
+ }
1882
+ return changed;
1883
+ }
1884
+ /** The newest lifecycle with this spawning tool_use id, else with this agent id. */
1885
+ findSubagent(toolUseId, agentId) {
1886
+ if (toolUseId) {
1887
+ for (let i = this.subagents.length - 1; i >= 0; i--) {
1888
+ if (this.subagents[i].id === toolUseId)
1889
+ return this.subagents[i];
1890
+ }
1891
+ }
1892
+ if (agentId) {
1893
+ for (let i = this.subagents.length - 1; i >= 0; i--) {
1894
+ if (this.subagents[i].agentId === agentId)
1895
+ return this.subagents[i];
1896
+ }
1897
+ }
1898
+ return undefined;
1899
+ }
1900
+ /** `agents_killed`: the user killed every running background agent. */
1901
+ applyAgentsKilled(timestamp) {
1902
+ let settled = 0;
1903
+ for (const agent of this.subagents) {
1904
+ if (!agent.background)
1905
+ continue;
1906
+ if (this.settleRunningSubagent(agent, timestamp, 'stopped', 'stop-marker'))
1907
+ settled++;
1908
+ }
1909
+ if (settled > 0)
1910
+ this.addSubagentTimeline(timestamp, describeAgentsStopped(null));
1911
+ }
1912
+ /**
1913
+ * "N background agents were stopped by the user: "A", "B"": settles the
1914
+ * running agents with those descriptions, the newest first for repeats.
1915
+ */
1916
+ applyAgentsStopped(notice, timestamp) {
1917
+ const taken = new Set();
1918
+ for (const description of notice.descriptions) {
1919
+ for (let i = this.subagents.length - 1; i >= 0; i--) {
1920
+ const agent = this.subagents[i];
1921
+ if (taken.has(agent) || agent.status !== 'running')
1922
+ continue;
1923
+ if (agent.description.trim() !== description)
1924
+ continue;
1925
+ taken.add(agent);
1926
+ this.settleRunningSubagent(agent, timestamp, 'stopped', 'stop-marker');
1927
+ break;
1928
+ }
1929
+ }
1930
+ this.addSubagentTimeline(timestamp, describeAgentsStopped(notice));
1931
+ }
1932
+ /** A background agent's final `task_status` attachment, matched by agent id. */
1933
+ applyTaskStatus(status, timestamp) {
1934
+ if (!isFinalTaskStatus(status.status))
1935
+ return;
1936
+ const agent = this.findSubagent(undefined, status.taskId);
1937
+ if (!agent)
1938
+ return;
1939
+ if (this.settleRunningSubagent(agent, timestamp, taskNotificationOutcome(status.status), 'task-status')) {
1940
+ this.addSubagentTimeline(timestamp, describeTaskStatus(status), agent.id);
1941
+ }
1942
+ }
1943
+ /**
1944
+ * A Codex subagent lifecycle record: `started` links the spawn to its child
1945
+ * thread; `completed` (a child turn finished) and `final-answer` (the
1946
+ * child's report, matched by its agent path) complete it; `interacted`
1947
+ * after `followup_task` puts it back to running (a `send_message` does
1948
+ * not: it does not start a turn); `interrupted` (`interrupt_agent`) settles
1949
+ * it as `stopped`, since the child keeps its thread and a follow-up can
1950
+ * run it again, like a user stop rather than a kill.
1951
+ */
1952
+ applySubagentActivity(activity, timestamp) {
1953
+ const byThread = () => this.findSubagent(undefined, activity.agentThreadId) ??
1954
+ this.findSubagentByPath(activity.agentPath);
1955
+ let agent;
1956
+ switch (activity.kind) {
1957
+ case 'started': {
1958
+ agent = this.findSubagent(activity.callId, undefined);
1959
+ if (!agent)
1960
+ break;
1961
+ agent.background = true;
1962
+ if (activity.agentThreadId)
1963
+ agent.agentId = activity.agentThreadId;
1964
+ if (activity.agentPath)
1965
+ agent.agentPath = activity.agentPath;
1966
+ break;
1967
+ }
1968
+ case 'completed':
1969
+ agent = byThread();
1970
+ if (agent)
1971
+ this.settleRunningSubagent(agent, timestamp, 'completed', 'notification');
1972
+ break;
1973
+ case 'final-answer':
1974
+ agent = this.findSubagentByPath(activity.agentPath, true);
1975
+ if (agent)
1976
+ this.settleRunningSubagent(agent, timestamp, 'completed', 'notification');
1977
+ break;
1978
+ case 'interacted':
1979
+ agent = byThread();
1980
+ if (agent && activity.tool === 'followup_task' && agent.status === 'completed') {
1981
+ this.reopenSubagent(agent, timestamp);
1982
+ }
1983
+ break;
1984
+ case 'interrupted':
1985
+ agent = byThread();
1986
+ if (agent)
1987
+ this.settleRunningSubagent(agent, timestamp, 'stopped', 'stop-marker');
1988
+ break;
1989
+ }
1990
+ this.addSubagentTimeline(timestamp, describeSubagentActivity(activity), agent?.id);
1991
+ }
1992
+ /** The newest lifecycle with this Codex agent path (with `runningFirst`, a running one first). */
1993
+ findSubagentByPath(agentPath, runningFirst = false) {
1994
+ if (!agentPath)
1995
+ return undefined;
1996
+ let fallback;
1997
+ for (let i = this.subagents.length - 1; i >= 0; i--) {
1998
+ const agent = this.subagents[i];
1999
+ if (agent.agentPath !== agentPath)
2000
+ continue;
2001
+ if (!runningFirst || agent.status === 'running')
2002
+ return agent;
2003
+ fallback ??= agent;
2004
+ }
2005
+ return fallback;
2006
+ }
2007
+ applySubagentFollowRecord(record, timestamp) {
2008
+ switch (record.kind) {
2009
+ case 'notification':
2010
+ this.applyTaskNotification(record.notification, timestamp);
2011
+ this.addTaskNotificationTimeline(timestamp, record.notification);
2012
+ break;
2013
+ case 'stopped':
2014
+ this.applyAgentsStopped(record.notice, timestamp);
2015
+ break;
2016
+ case 'task-status':
2017
+ this.applyTaskStatus(record.status, timestamp);
2018
+ break;
2019
+ case 'activity':
2020
+ this.applySubagentActivity(record.activity, timestamp);
2021
+ break;
2022
+ case 'agents-killed':
2023
+ this.applyAgentsKilled(timestamp);
2024
+ break;
2025
+ case 'unreadable':
2026
+ break;
2027
+ }
2028
+ }
2029
+ addSubagentTimeline(timestamp, description, toolUseId) {
2030
+ const text = description.length > 200 ? description.substring(0, 197) + '...' : description;
2031
+ this.timeline.push({
2032
+ type: 'tool_result',
2033
+ timestamp,
2034
+ description: text,
2035
+ noiseLevel: 'system',
2036
+ messageClassification: 'system',
2037
+ ...(toolUseId ? { metadata: { toolUseId } } : {}),
2038
+ });
2039
+ if (this.timeline.length > this.timelineCap) {
2040
+ this.timeline.shift();
2041
+ }
2042
+ }
1689
2043
  addTaskNotificationTimeline(timestamp, notification) {
1690
2044
  let description = notification.summary ?? `Background task ${notification.status ?? 'finished'}`;
1691
2045
  if (description.length > 200)
@@ -1936,9 +2290,9 @@ export class EventAggregator {
1936
2290
  // Private: Permission Mode
1937
2291
  // ═══════════════════════════════════════════════════════════════════════
1938
2292
  trackPermissionMode(event) {
1939
- if (!event.permissionMode)
2293
+ const mode = asPermissionMode(event.permissionMode);
2294
+ if (!mode)
1940
2295
  return;
1941
- const mode = event.permissionMode;
1942
2296
  if (mode !== this.currentPermissionMode) {
1943
2297
  this.permissionModeHistory.push({
1944
2298
  timestamp: event.timestamp,
@@ -17,7 +17,8 @@ export { addLocalDays, formatDurationMs, formatLocalDateKey, formatTokenCount, p
17
17
  export { parseMcpToolName } from './parsers/mcpToolName';
18
18
  export { buildSessionContextSnapshot, calculateSessionContextPressure, createSessionContextProjector, } from './context/sessionContext';
19
19
  export { ACCOUNT_PROVIDER_IDS, MODEL_PROVIDER_IDS, QUOTA_PROVIDER_IDS, RUNTIME_QUOTA_PROVIDER_IDS, SESSION_PROVIDER_IDS, } from './providerIds';
20
- export { SUBAGENT_SPAWN_TOOL_NAMES, isSubagentSpawnTool, isTaskNotificationSystemEvent, parseTaskNotification, taskNotificationFromEvent, } from './subagentSpawn';
20
+ export { SUBAGENT_SPAWN_TOOL_NAMES, agentsStoppedNoticeFromEvent, isAgentsKilledSystemEvent, isCodexSubagentSpawnTool, isSubagentSpawnTool, isTaskNotificationSystemEvent, isTaskStatusSystemEvent, parseAgentsStoppedNotice, parseCodexSpawnLaunch, parseTaskNotification, taskNotificationFromEvent, taskStatusFromAttachment, taskStatusFromEvent, } from './subagentSpawn';
21
+ export { SUBAGENT_PARENT_IDLE_ENDED_MS, SUBAGENT_UNREPORTED_QUIET_MS, resolveSubagentDisplayState, subagentDisplayLabel, } from './subagentDisplayState';
21
22
  export { assistantTurnEventsFromSessionEvents, extractTurnSubagents, isAssistantTurnSubagentTool, reasoningSummary, segmentAssistantTurn, } from './turns/assistantTurn';
22
23
  export { parseModelId, getModelPricing, resolveModelPricing, getModelInfo, calculateCost, calculateCostWithPricing, calculateCostWithProvenance, mergeCostSources, shortModelName, getModelDisplayInfo, compareModelIds, sortModelIds, formatCost, canonicalModelId, isMovingModelAlias, registerModelPricing, clearRegisteredModelPricing, getPricingCatalogInfo, costSourceFromPricingProvenance, mergeCostComponents, } from './modelInfo';
23
24
  export { exportResolvedModelCatalog, importResolvedModelCatalog, registerModelAlias, resolveModelAlias, RESOLVED_MODEL_CATALOG_SCHEMA_VERSION, } from './modelCatalog';
@@ -31,6 +32,7 @@ export { isAggregateCodexLimit } from './types/codex';
31
32
  // Codex rollout parsing (pure: no filesystem access)
32
33
  export { CodexRolloutParser, DEFAULT_MAX_TRACKED_TOOL_CALLS, extractPatchFilePaths, normalizeCodexToolName, normalizeCodexToolInput, resolveCodexCallUsage, } from './parsers/codexParser';
33
34
  export { codexRolloutOrigin, codexSpawnParentFromSource } from './parsers/codexRolloutOrigin';
35
+ export { SUBAGENT_ACTIVITY_BLOCK_TYPE, SUBAGENT_ACTIVITY_ORIGIN_KIND, describeSubagentActivity, isSubagentActivitySystemEvent, parseCodexAgentMessageText, subagentActivityFromEvent, } from './subagentActivity';
34
36
  export { HUMAN_CLAUDE_ENTRYPOINTS, HUMAN_CODEX_SOURCES, humanPromptText, isHumanPrompt, } from './humanPrompt';
35
37
  export { renderHandoffUrlTemplate } from './handoffUrl';
36
38
  // Tool categories across providers
@@ -8,7 +8,8 @@ import { EventAggregator } from '../aggregation/EventAggregator';
8
8
  import { getModelContextWindowSize } from '../modelContext';
9
9
  import { parseMcpToolName } from '../parsers/mcpToolName';
10
10
  import { estimateTextTokens } from '../tokenEstimation';
11
- import { isTaskNotificationSystemEvent } from '../subagentSpawn';
11
+ import { isTaskNotificationSystemEvent, isTaskStatusSystemEvent } from '../subagentSpawn';
12
+ import { isSubagentActivitySystemEvent } from '../subagentActivity';
12
13
  const DEFAULT_CONTEXT_SOURCE_LIMIT = 200;
13
14
  const DEFAULT_SNIPPET_MAX_CHARS = 240;
14
15
  const DEFAULT_BODY_MAX_CHARS = 5000;
@@ -207,7 +208,10 @@ function extractSourcesFromEvent(event, state) {
207
208
  }
208
209
  function extractSystemSource(event, state) {
209
210
  // Background-task notices only update subagent lifecycles (like the aggregator).
210
- if (isTaskNotificationSystemEvent(event))
211
+ if (isTaskNotificationSystemEvent(event) || isTaskStatusSystemEvent(event))
212
+ return;
213
+ // So are subagent lifecycle records (opt-in Codex parser events).
214
+ if (isSubagentActivitySystemEvent(event))
211
215
  return;
212
216
  const message = event.message;
213
217
  const text = extractText(message?.content);