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
@@ -22,6 +22,7 @@ const HeatmapTracker_1 = require("./HeatmapTracker");
22
22
  const PatternExtractor_1 = require("./PatternExtractor");
23
23
  const errorTaxonomy_1 = require("../extractors/errorTaxonomy");
24
24
  const tokenEstimation_1 = require("../tokenEstimation");
25
+ const subagentActivity_1 = require("../subagentActivity");
25
26
  const subagentSpawn_1 = require("../subagentSpawn");
26
27
  const usageNormalization_1 = require("../usageNormalization");
27
28
  // ── Defaults ──
@@ -31,8 +32,26 @@ const DEFAULT_CONTEXT_TIMELINE_CAP = 2_000;
31
32
  const DEFAULT_BURN_WINDOW_MS = 5 * 60_000;
32
33
  const DEFAULT_BURN_SAMPLE_MS = 10_000;
33
34
  const COMPACTION_DROP_THRESHOLD = 0.8; // >20% drop
34
- /** Schema version for serialized snapshots. v6: token totals count each Claude response and Codex call once. */
35
- exports.SNAPSHOT_SCHEMA_VERSION = 6;
35
+ /**
36
+ * Schema version for serialized snapshots. v6: token totals count each Claude
37
+ * response and Codex call once. v7: subagent lifecycles track Codex spawns and
38
+ * record what settled a background agent (`settledBy`).
39
+ */
40
+ exports.SNAPSHOT_SCHEMA_VERSION = 7;
41
+ // Checked without the schema module so this file stays free of Zod.
42
+ const PERMISSION_MODES = new Set([
43
+ 'default',
44
+ 'acceptEdits',
45
+ 'bypassPermissions',
46
+ 'plan',
47
+ 'auto',
48
+ 'dontAsk',
49
+ ]);
50
+ function asPermissionMode(value) {
51
+ return typeof value === 'string' && PERMISSION_MODES.has(value)
52
+ ? value
53
+ : null;
54
+ }
36
55
  // ── Goal gate detection regex ──
37
56
  const GOAL_GATE_REGEX = /\b(CRITICAL|MUST|blocker|required|must.?complete|goal.?gate|essential|do.?not.?skip|blocking)\b/i;
38
57
  // ── Task notifications delivered as attachments ──
@@ -51,6 +70,62 @@ function taskNotificationFromAttachmentFollowEvent(event) {
51
70
  const asEvent = raw;
52
71
  return (0, subagentSpawn_1.isTaskNotificationSystemEvent)(asEvent) ? (0, subagentSpawn_1.taskNotificationFromEvent)(asEvent) : null;
53
72
  }
73
+ function subagentRecordFromFollowEvent(event) {
74
+ const raw = event.raw;
75
+ if (!raw || typeof raw !== 'object')
76
+ return null;
77
+ const notification = taskNotificationFromAttachmentFollowEvent(event);
78
+ if (notification)
79
+ return { kind: 'notification', notification };
80
+ const asEvent = raw;
81
+ const prompt = (0, subagentSpawn_1.taskNotificationPromptFromAttachment)(raw);
82
+ const notice = prompt !== null ? (0, subagentSpawn_1.parseAgentsStoppedNotice)(prompt) : (0, subagentSpawn_1.agentsStoppedNoticeFromEvent)(asEvent);
83
+ if (notice)
84
+ return { kind: 'stopped', notice };
85
+ if (prompt !== null || (0, subagentSpawn_1.isTaskNotificationSystemEvent)(asEvent)) {
86
+ // A notification this release cannot read: still bookkeeping.
87
+ return { kind: 'unreadable' };
88
+ }
89
+ const status = (0, subagentSpawn_1.taskStatusFromAttachment)(raw) ?? (0, subagentSpawn_1.taskStatusFromEvent)(asEvent);
90
+ if (status)
91
+ return { kind: 'task-status', status };
92
+ if ((0, subagentActivity_1.isSubagentActivitySystemEvent)(asEvent)) {
93
+ const activity = (0, subagentActivity_1.subagentActivityFromEvent)(asEvent);
94
+ return activity ? { kind: 'activity', activity } : { kind: 'unreadable' };
95
+ }
96
+ if ((0, subagentSpawn_1.isAgentsKilledSystemEvent)(raw))
97
+ return { kind: 'agents-killed' };
98
+ return null;
99
+ }
100
+ // ── Subagent spawn input ──
101
+ /**
102
+ * Description and type of a spawn: Claude Code's `description` /
103
+ * `subagent_type`, or Codex `spawn_agent`'s `task_name` / `agent_type`.
104
+ */
105
+ function spawnFields(input) {
106
+ const text = (value) => typeof value === 'string' && value ? value : undefined;
107
+ const description = text(input.description) ?? text(input.task_name);
108
+ const subagentType = text(input.subagent_type) ?? text(input.subagentType) ?? text(input.agent_type);
109
+ return {
110
+ ...(description ? { description } : {}),
111
+ ...(subagentType ? { subagentType } : {}),
112
+ };
113
+ }
114
+ /**
115
+ * Whether a spawn's tool result launched the agent in the background or
116
+ * ended it. A Codex `spawn_agent` result is always a launch unless it failed
117
+ * ("collab spawn failed: …"); its `{"task_name": …}` output is a launch even
118
+ * when the output's text looked like an error to the parser (an agent path
119
+ * such as `/root/fix_failed_tests`).
120
+ */
121
+ function spawnResultKind(codex, content, isError) {
122
+ // Read first: the `codex` hint does not survive a snapshot boundary.
123
+ if ((0, subagentSpawn_1.parseCodexSpawnLaunch)(content))
124
+ return 'launch';
125
+ if (codex)
126
+ return isError ? 'end' : 'launch';
127
+ return !isError && (0, subagentSpawn_1.parseAsyncAgentLaunch)(content) ? 'launch' : 'end';
128
+ }
54
129
  // ── Todo dependency parsing ──
55
130
  /**
56
131
  * Parses dependency references from a todo item's content text.
@@ -136,6 +211,8 @@ class EventAggregator {
136
211
  // Subagent lifecycle
137
212
  subagents = [];
138
213
  pendingSubagents = new Map(); // toolUseId -> index
214
+ /** Codex `spawn_agent` calls still waiting for their result (always a background launch). */
215
+ pendingCodexSpawns = new Set();
139
216
  // Plan state
140
217
  planExtractor;
141
218
  planState = null;
@@ -198,8 +275,32 @@ class EventAggregator {
198
275
  this.applyTaskNotification(notification, event.timestamp);
199
276
  this.addTaskNotificationTimeline(event.timestamp, notification);
200
277
  }
278
+ else {
279
+ // The plain-text "background agents were stopped by the user" notice.
280
+ const notice = (0, subagentSpawn_1.agentsStoppedNoticeFromEvent)(event);
281
+ if (notice)
282
+ this.applyAgentsStopped(notice, event.timestamp);
283
+ }
201
284
  return;
202
285
  }
286
+ // So are a background agent's final `task_status` (same opt-in) and the
287
+ // opt-in Codex subagent lifecycle records (`subagentActivityFromEvent`).
288
+ if ((0, subagentSpawn_1.isTaskStatusSystemEvent)(event)) {
289
+ const status = (0, subagentSpawn_1.taskStatusFromEvent)(event);
290
+ if (status)
291
+ this.applyTaskStatus(status, event.timestamp);
292
+ return;
293
+ }
294
+ if ((0, subagentActivity_1.isSubagentActivitySystemEvent)(event)) {
295
+ const activity = (0, subagentActivity_1.subagentActivityFromEvent)(event);
296
+ if (activity)
297
+ this.applySubagentActivity(activity, event.timestamp);
298
+ return;
299
+ }
300
+ // The user killed every running background agent. The line itself is
301
+ // counted below as before.
302
+ if ((0, subagentSpawn_1.isAgentsKilledSystemEvent)(event))
303
+ this.applyAgentsKilled(event.timestamp);
203
304
  // 1. Increment counters, capture timestamps
204
305
  this.eventCount++;
205
306
  if (!this.sessionStartTime) {
@@ -265,12 +366,13 @@ class EventAggregator {
265
366
  // processFollowEvent — adapter for CLI FollowEvent format
266
367
  // ═══════════════════════════════════════════════════════════════════════
267
368
  processFollowEvent(event) {
268
- // A mid-turn task-notification attachment, like its SessionEvent form above.
369
+ // Subagent bookkeeping (a mid-turn task-notification or task-status
370
+ // attachment, a stop notice or `agents_killed` line, a Codex lifecycle
371
+ // record), like its SessionEvent form above: not counted as activity.
269
372
  if (event.type === 'system') {
270
- const notification = taskNotificationFromAttachmentFollowEvent(event);
271
- if (notification) {
272
- this.applyTaskNotification(notification, event.timestamp);
273
- this.addTaskNotificationTimeline(event.timestamp, notification);
373
+ const record = subagentRecordFromFollowEvent(event);
374
+ if (record) {
375
+ this.applySubagentFollowRecord(record, event.timestamp);
274
376
  return;
275
377
  }
276
378
  }
@@ -280,6 +382,8 @@ class EventAggregator {
280
382
  this.sessionStartTime = event.timestamp;
281
383
  }
282
384
  this.lastEventTime = event.timestamp;
385
+ // Followed events carry the mode as a plain string.
386
+ this.trackPermissionMode(event);
283
387
  // Provider
284
388
  if (!this._providerId && event.providerId) {
285
389
  this._providerId = event.providerId;
@@ -411,6 +515,10 @@ class EventAggregator {
411
515
  getSubagents() {
412
516
  return [...this.subagents];
413
517
  }
518
+ /** Timestamp of the last counted event (`getMetrics().lastEventTime`), without building the metrics. */
519
+ getLastEventTime() {
520
+ return this.lastEventTime;
521
+ }
414
522
  getPlan() {
415
523
  return this.planState
416
524
  ? { ...this.planState, steps: this.planState.steps.map((step) => ({ ...step })) }
@@ -507,6 +615,7 @@ class EventAggregator {
507
615
  this.activeTaskId = null;
508
616
  this.subagents = [];
509
617
  this.pendingSubagents.clear();
618
+ this.pendingCodexSpawns.clear();
510
619
  this.planExtractor.reset();
511
620
  this.planState = null;
512
621
  this.planEnteredAt = undefined;
@@ -640,7 +749,7 @@ class EventAggregator {
640
749
  this.tasks = new Map(state.tasks);
641
750
  this.activeTaskId = state.activeTaskId;
642
751
  this.subagents = [...state.subagents];
643
- this.currentPermissionMode = state.permissionMode ?? null;
752
+ this.currentPermissionMode = asPermissionMode(state.permissionMode) ?? null;
644
753
  this.permissionModeHistory = state.permissionModeHistory
645
754
  ? [...state.permissionModeHistory]
646
755
  : [];
@@ -667,6 +776,7 @@ class EventAggregator {
667
776
  this.pendingToolCalls.clear();
668
777
  this.pendingTaskCreates.clear();
669
778
  this.pendingSubagents.clear();
779
+ this.pendingCodexSpawns.clear();
670
780
  // A spawn still waiting for its tool_result is resolved by that result,
671
781
  // which can land after the snapshot boundary. Background agents need no
672
782
  // entry: their notifications are matched against every subagent.
@@ -1153,7 +1263,7 @@ class EventAggregator {
1153
1263
  else if (name === 'UpdatePlan') {
1154
1264
  this.handleUpdatePlan(input, new Date(event.timestamp));
1155
1265
  }
1156
- else if ((0, subagentSpawn_1.isSubagentSpawnTool)(name) && toolUseId) {
1266
+ else if ((0, subagentSpawn_1.isSubagentSpawnTool)(name, input) && toolUseId) {
1157
1267
  this.handleAgentSpawn(toolUseId, input, new Date(event.timestamp));
1158
1268
  }
1159
1269
  }
@@ -1197,7 +1307,7 @@ class EventAggregator {
1197
1307
  else if (event.toolName === 'UpdatePlan') {
1198
1308
  this.handleUpdatePlan(input, new Date(event.timestamp));
1199
1309
  }
1200
- else if ((0, subagentSpawn_1.isSubagentSpawnTool)(event.toolName) && toolUseId) {
1310
+ else if ((0, subagentSpawn_1.isSubagentSpawnTool)(event.toolName, input) && toolUseId) {
1201
1311
  this.handleAgentSpawn(toolUseId, input, new Date(event.timestamp));
1202
1312
  }
1203
1313
  }
@@ -1496,8 +1606,9 @@ class EventAggregator {
1496
1606
  // ═══════════════════════════════════════════════════════════════════════
1497
1607
  handleAgentSpawn(toolUseId, input, now) {
1498
1608
  const agentTaskId = `agent-${toolUseId}`;
1499
- const description = input.description ? String(input.description) : 'Subagent';
1500
- const subagentType = input.subagent_type || input.subagentType || undefined;
1609
+ const spawn = spawnFields(input);
1610
+ const description = spawn.description ?? 'Subagent';
1611
+ const subagentType = spawn.subagentType;
1501
1612
  const task = {
1502
1613
  taskId: agentTaskId,
1503
1614
  subject: description,
@@ -1520,8 +1631,9 @@ class EventAggregator {
1520
1631
  if (!agentTask)
1521
1632
  return;
1522
1633
  // A background launch returns at once; its task notification finishes it.
1523
- if (!isError && (0, subagentSpawn_1.parseAsyncAgentLaunch)(content))
1634
+ if (spawnResultKind(this.pendingCodexSpawns.has(toolUseId), content, isError) === 'launch') {
1524
1635
  return;
1636
+ }
1525
1637
  agentTask.status = isError ? 'deleted' : 'completed';
1526
1638
  agentTask.updatedAt = new Date(timestamp);
1527
1639
  }
@@ -1548,12 +1660,12 @@ class EventAggregator {
1548
1660
  if (!Array.isArray(content))
1549
1661
  return;
1550
1662
  for (const block of content) {
1551
- if (block.type === 'tool_use' && (0, subagentSpawn_1.isSubagentSpawnTool)(block.name)) {
1663
+ if (block.type === 'tool_use' && (0, subagentSpawn_1.isSubagentSpawnTool)(block.name, block.input)) {
1552
1664
  const input = block.input;
1553
1665
  const toolUseId = block.id;
1554
1666
  if (!input || !toolUseId)
1555
1667
  continue;
1556
- this.trackSubagentSpawn(toolUseId, input, event.timestamp);
1668
+ this.trackSubagentSpawn(toolUseId, input, event.timestamp, (0, subagentSpawn_1.isCodexSubagentSpawnTool)(block.name, input));
1557
1669
  }
1558
1670
  else if (block.type === 'tool_result') {
1559
1671
  const toolUseId = block.tool_use_id;
@@ -1568,10 +1680,10 @@ class EventAggregator {
1568
1680
  // ═══════════════════════════════════════════════════════════════════════
1569
1681
  extractSubagentFromFollowEvent(event) {
1570
1682
  const raw = event.raw;
1571
- if (event.type === 'tool_use' && (0, subagentSpawn_1.isSubagentSpawnTool)(event.toolName)) {
1683
+ if (event.type === 'tool_use' && (0, subagentSpawn_1.isSubagentSpawnTool)(event.toolName, raw?.input)) {
1572
1684
  if (!raw?.input)
1573
1685
  return;
1574
- this.trackSubagentSpawn(raw.id || '', raw.input, event.timestamp);
1686
+ this.trackSubagentSpawn(raw.id || '', raw.input, event.timestamp, (0, subagentSpawn_1.isCodexSubagentSpawnTool)(event.toolName, raw.input));
1575
1687
  }
1576
1688
  else if (event.type === 'tool_result') {
1577
1689
  if (!raw)
@@ -1588,11 +1700,12 @@ class EventAggregator {
1588
1700
  this.applyTaskNotification(notification, event.timestamp);
1589
1701
  }
1590
1702
  }
1591
- trackSubagentSpawn(toolUseId, input, timestamp) {
1703
+ trackSubagentSpawn(toolUseId, input, timestamp, codex = false) {
1704
+ const spawn = spawnFields(input);
1592
1705
  const info = {
1593
1706
  id: toolUseId,
1594
- description: input.description || 'Unknown task',
1595
- subagentType: input.subagent_type || input.subagentType || 'general',
1707
+ description: spawn.description ?? 'Unknown task',
1708
+ subagentType: spawn.subagentType ?? (codex ? 'default' : 'general'),
1596
1709
  spawnTime: timestamp,
1597
1710
  status: 'running',
1598
1711
  };
@@ -1600,19 +1713,23 @@ class EventAggregator {
1600
1713
  this.subagents.push(info);
1601
1714
  if (toolUseId) {
1602
1715
  this.pendingSubagents.set(toolUseId, idx);
1716
+ if (codex)
1717
+ this.pendingCodexSpawns.add(toolUseId);
1603
1718
  }
1604
1719
  }
1605
1720
  /**
1606
1721
  * The spawning tool call's result. A foreground agent ends here; a
1607
- * background launch only reports the agent id, and the agent keeps running
1608
- * until its task notification arrives.
1722
+ * background launch only reports the agent id (a Codex spawn, its agent
1723
+ * path), and the agent keeps running until a notification or another
1724
+ * record says it ended.
1609
1725
  */
1610
1726
  resolveSubagentResult(toolUseId, content, isError, timestamp) {
1611
1727
  const idx = this.pendingSubagents.get(toolUseId);
1612
1728
  if (idx === undefined)
1613
1729
  return;
1614
- const launch = isError ? null : (0, subagentSpawn_1.parseAsyncAgentLaunch)(content);
1615
- if (launch) {
1730
+ const codex = this.pendingCodexSpawns.delete(toolUseId);
1731
+ if (spawnResultKind(codex, content, isError) === 'launch') {
1732
+ const launch = (0, subagentSpawn_1.parseCodexSpawnLaunch)(content) ?? (0, subagentSpawn_1.parseAsyncAgentLaunch)(content) ?? {};
1616
1733
  this.markBackgroundLaunch(idx, launch);
1617
1734
  return;
1618
1735
  }
@@ -1624,6 +1741,8 @@ class EventAggregator {
1624
1741
  agent.background = true;
1625
1742
  if (launch.agentId)
1626
1743
  agent.agentId = launch.agentId;
1744
+ if (launch.agentPath && !agent.agentPath)
1745
+ agent.agentPath = launch.agentPath;
1627
1746
  }
1628
1747
  completeSubagent(toolUseId, timestamp, outcome = 'completed') {
1629
1748
  const idx = this.pendingSubagents.get(toolUseId);
@@ -1631,16 +1750,58 @@ class EventAggregator {
1631
1750
  return;
1632
1751
  this.finishSubagent(this.subagents[idx], timestamp, outcome);
1633
1752
  }
1634
- finishSubagent(agent, timestamp, outcome) {
1753
+ finishSubagent(agent, timestamp, outcome, settledBy) {
1635
1754
  this.pendingSubagents.delete(agent.id);
1755
+ this.pendingCodexSpawns.delete(agent.id);
1636
1756
  agent.status = 'completed';
1637
1757
  agent.completionTime = timestamp;
1638
1758
  agent.outcome = outcome;
1759
+ if (settledBy)
1760
+ agent.settledBy = settledBy;
1761
+ else
1762
+ delete agent.settledBy;
1639
1763
  const start = new Date(agent.spawnTime).getTime();
1640
1764
  const end = new Date(timestamp).getTime();
1641
1765
  if (!isNaN(start) && !isNaN(end) && end >= start) {
1642
1766
  agent.durationMs = end - start;
1643
1767
  }
1768
+ else {
1769
+ delete agent.durationMs;
1770
+ }
1771
+ }
1772
+ /** Puts a settled agent back to running (it resumed). */
1773
+ reopenSubagent(agent, timestamp) {
1774
+ this.pendingSubagents.delete(agent.id);
1775
+ agent.status = 'running';
1776
+ delete agent.completionTime;
1777
+ delete agent.durationMs;
1778
+ delete agent.outcome;
1779
+ delete agent.settledBy;
1780
+ this.settleAgentTask(agent.id, null, timestamp);
1781
+ }
1782
+ /**
1783
+ * Settles a lifecycle that is still running (and its task card). Agents
1784
+ * already settled are left alone: a stop notice or status can name agents
1785
+ * that finished earlier.
1786
+ */
1787
+ settleRunningSubagent(agent, timestamp, outcome, settledBy) {
1788
+ if (agent.status !== 'running')
1789
+ return false;
1790
+ this.finishSubagent(agent, timestamp, outcome, settledBy);
1791
+ this.settleAgentTask(agent.id, outcome, timestamp);
1792
+ return true;
1793
+ }
1794
+ /**
1795
+ * Moves an agent's task card with its lifecycle: `null` back to in
1796
+ * progress, a failure to deleted, any other outcome to completed.
1797
+ */
1798
+ settleAgentTask(toolUseId, outcome, timestamp) {
1799
+ const agentTask = toolUseId ? this.tasks.get(`agent-${toolUseId}`) : undefined;
1800
+ if (!agentTask?.isSubagent)
1801
+ return;
1802
+ agentTask.status =
1803
+ outcome === null ? 'in_progress' : outcome === 'failed' ? 'deleted' : 'completed';
1804
+ agentTask.updatedAt = new Date(timestamp);
1644
1805
  }
1645
1806
  /**
1646
1807
  * Applies a background task's notification to the subagent it reports on,
@@ -1663,20 +1824,13 @@ class EventAggregator {
1663
1824
  delete agent.completionTime;
1664
1825
  delete agent.durationMs;
1665
1826
  delete agent.outcome;
1827
+ delete agent.settledBy;
1666
1828
  }
1667
1829
  else {
1668
- this.finishSubagent(agent, timestamp, outcome);
1830
+ this.finishSubagent(agent, timestamp, outcome, 'notification');
1669
1831
  }
1670
1832
  }
1671
- const agentTask = toolUseId ? this.tasks.get(`agent-${toolUseId}`) : undefined;
1672
- if (agentTask?.isSubagent) {
1673
- agentTask.status = notification.interim
1674
- ? 'in_progress'
1675
- : outcome === 'failed'
1676
- ? 'deleted'
1677
- : 'completed';
1678
- agentTask.updatedAt = new Date(timestamp);
1679
- }
1833
+ this.settleAgentTask(toolUseId, notification.interim ? null : outcome, timestamp);
1680
1834
  }
1681
1835
  findNotifiedSubagent(notification) {
1682
1836
  const { toolUseId, taskId } = notification;
@@ -1690,6 +1844,206 @@ class EventAggregator {
1690
1844
  }
1691
1845
  return undefined;
1692
1846
  }
1847
+ // ═══════════════════════════════════════════════════════════════════════
1848
+ // Subagent settling: evidence, stop markers, task statuses, Codex records
1849
+ // ═══════════════════════════════════════════════════════════════════════
1850
+ /**
1851
+ * Applies host evidence about a subagent (Claude Code's meta flags):
1852
+ * `stoppedByUser` settles a running agent as `stopped`, `finished` as
1853
+ * `completed` (`settledBy: 'meta'`). Agents already settled some other way
1854
+ * keep their outcome; `finished: false` reopens an agent that only meta
1855
+ * settled. Matches by `toolUseId`, then `agentId` (the newest such agent),
1856
+ * and records the agent id when matched by tool_use id.
1857
+ *
1858
+ * @param timestamp ISO time to record as the completion (the agent's last
1859
+ * activity, when the host knows it)
1860
+ * @returns Whether a lifecycle changed
1861
+ */
1862
+ applySubagentEvidence(evidence, timestamp) {
1863
+ const agent = this.findSubagent(evidence.toolUseId, evidence.agentId);
1864
+ if (!agent)
1865
+ return false;
1866
+ let changed = false;
1867
+ if (evidence.agentId && !agent.agentId) {
1868
+ agent.agentId = evidence.agentId;
1869
+ changed = true;
1870
+ }
1871
+ if (agent.status === 'running') {
1872
+ const outcome = evidence.stoppedByUser
1873
+ ? 'stopped'
1874
+ : evidence.finished
1875
+ ? 'completed'
1876
+ : null;
1877
+ if (outcome)
1878
+ changed = this.settleRunningSubagent(agent, timestamp, outcome, 'meta') || changed;
1879
+ }
1880
+ else if (agent.settledBy === 'meta' &&
1881
+ evidence.finished === false &&
1882
+ evidence.stoppedByUser !== true) {
1883
+ this.reopenSubagent(agent, timestamp);
1884
+ changed = true;
1885
+ }
1886
+ return changed;
1887
+ }
1888
+ /** The newest lifecycle with this spawning tool_use id, else with this agent id. */
1889
+ findSubagent(toolUseId, agentId) {
1890
+ if (toolUseId) {
1891
+ for (let i = this.subagents.length - 1; i >= 0; i--) {
1892
+ if (this.subagents[i].id === toolUseId)
1893
+ return this.subagents[i];
1894
+ }
1895
+ }
1896
+ if (agentId) {
1897
+ for (let i = this.subagents.length - 1; i >= 0; i--) {
1898
+ if (this.subagents[i].agentId === agentId)
1899
+ return this.subagents[i];
1900
+ }
1901
+ }
1902
+ return undefined;
1903
+ }
1904
+ /** `agents_killed`: the user killed every running background agent. */
1905
+ applyAgentsKilled(timestamp) {
1906
+ let settled = 0;
1907
+ for (const agent of this.subagents) {
1908
+ if (!agent.background)
1909
+ continue;
1910
+ if (this.settleRunningSubagent(agent, timestamp, 'stopped', 'stop-marker'))
1911
+ settled++;
1912
+ }
1913
+ if (settled > 0)
1914
+ this.addSubagentTimeline(timestamp, (0, subagentSpawn_1.describeAgentsStopped)(null));
1915
+ }
1916
+ /**
1917
+ * "N background agents were stopped by the user: "A", "B"": settles the
1918
+ * running agents with those descriptions, the newest first for repeats.
1919
+ */
1920
+ applyAgentsStopped(notice, timestamp) {
1921
+ const taken = new Set();
1922
+ for (const description of notice.descriptions) {
1923
+ for (let i = this.subagents.length - 1; i >= 0; i--) {
1924
+ const agent = this.subagents[i];
1925
+ if (taken.has(agent) || agent.status !== 'running')
1926
+ continue;
1927
+ if (agent.description.trim() !== description)
1928
+ continue;
1929
+ taken.add(agent);
1930
+ this.settleRunningSubagent(agent, timestamp, 'stopped', 'stop-marker');
1931
+ break;
1932
+ }
1933
+ }
1934
+ this.addSubagentTimeline(timestamp, (0, subagentSpawn_1.describeAgentsStopped)(notice));
1935
+ }
1936
+ /** A background agent's final `task_status` attachment, matched by agent id. */
1937
+ applyTaskStatus(status, timestamp) {
1938
+ if (!(0, subagentSpawn_1.isFinalTaskStatus)(status.status))
1939
+ return;
1940
+ const agent = this.findSubagent(undefined, status.taskId);
1941
+ if (!agent)
1942
+ return;
1943
+ if (this.settleRunningSubagent(agent, timestamp, (0, subagentSpawn_1.taskNotificationOutcome)(status.status), 'task-status')) {
1944
+ this.addSubagentTimeline(timestamp, (0, subagentSpawn_1.describeTaskStatus)(status), agent.id);
1945
+ }
1946
+ }
1947
+ /**
1948
+ * A Codex subagent lifecycle record: `started` links the spawn to its child
1949
+ * thread; `completed` (a child turn finished) and `final-answer` (the
1950
+ * child's report, matched by its agent path) complete it; `interacted`
1951
+ * after `followup_task` puts it back to running (a `send_message` does
1952
+ * not: it does not start a turn); `interrupted` (`interrupt_agent`) settles
1953
+ * it as `stopped`, since the child keeps its thread and a follow-up can
1954
+ * run it again, like a user stop rather than a kill.
1955
+ */
1956
+ applySubagentActivity(activity, timestamp) {
1957
+ const byThread = () => this.findSubagent(undefined, activity.agentThreadId) ??
1958
+ this.findSubagentByPath(activity.agentPath);
1959
+ let agent;
1960
+ switch (activity.kind) {
1961
+ case 'started': {
1962
+ agent = this.findSubagent(activity.callId, undefined);
1963
+ if (!agent)
1964
+ break;
1965
+ agent.background = true;
1966
+ if (activity.agentThreadId)
1967
+ agent.agentId = activity.agentThreadId;
1968
+ if (activity.agentPath)
1969
+ agent.agentPath = activity.agentPath;
1970
+ break;
1971
+ }
1972
+ case 'completed':
1973
+ agent = byThread();
1974
+ if (agent)
1975
+ this.settleRunningSubagent(agent, timestamp, 'completed', 'notification');
1976
+ break;
1977
+ case 'final-answer':
1978
+ agent = this.findSubagentByPath(activity.agentPath, true);
1979
+ if (agent)
1980
+ this.settleRunningSubagent(agent, timestamp, 'completed', 'notification');
1981
+ break;
1982
+ case 'interacted':
1983
+ agent = byThread();
1984
+ if (agent && activity.tool === 'followup_task' && agent.status === 'completed') {
1985
+ this.reopenSubagent(agent, timestamp);
1986
+ }
1987
+ break;
1988
+ case 'interrupted':
1989
+ agent = byThread();
1990
+ if (agent)
1991
+ this.settleRunningSubagent(agent, timestamp, 'stopped', 'stop-marker');
1992
+ break;
1993
+ }
1994
+ this.addSubagentTimeline(timestamp, (0, subagentActivity_1.describeSubagentActivity)(activity), agent?.id);
1995
+ }
1996
+ /** The newest lifecycle with this Codex agent path (with `runningFirst`, a running one first). */
1997
+ findSubagentByPath(agentPath, runningFirst = false) {
1998
+ if (!agentPath)
1999
+ return undefined;
2000
+ let fallback;
2001
+ for (let i = this.subagents.length - 1; i >= 0; i--) {
2002
+ const agent = this.subagents[i];
2003
+ if (agent.agentPath !== agentPath)
2004
+ continue;
2005
+ if (!runningFirst || agent.status === 'running')
2006
+ return agent;
2007
+ fallback ??= agent;
2008
+ }
2009
+ return fallback;
2010
+ }
2011
+ applySubagentFollowRecord(record, timestamp) {
2012
+ switch (record.kind) {
2013
+ case 'notification':
2014
+ this.applyTaskNotification(record.notification, timestamp);
2015
+ this.addTaskNotificationTimeline(timestamp, record.notification);
2016
+ break;
2017
+ case 'stopped':
2018
+ this.applyAgentsStopped(record.notice, timestamp);
2019
+ break;
2020
+ case 'task-status':
2021
+ this.applyTaskStatus(record.status, timestamp);
2022
+ break;
2023
+ case 'activity':
2024
+ this.applySubagentActivity(record.activity, timestamp);
2025
+ break;
2026
+ case 'agents-killed':
2027
+ this.applyAgentsKilled(timestamp);
2028
+ break;
2029
+ case 'unreadable':
2030
+ break;
2031
+ }
2032
+ }
2033
+ addSubagentTimeline(timestamp, description, toolUseId) {
2034
+ const text = description.length > 200 ? description.substring(0, 197) + '...' : description;
2035
+ this.timeline.push({
2036
+ type: 'tool_result',
2037
+ timestamp,
2038
+ description: text,
2039
+ noiseLevel: 'system',
2040
+ messageClassification: 'system',
2041
+ ...(toolUseId ? { metadata: { toolUseId } } : {}),
2042
+ });
2043
+ if (this.timeline.length > this.timelineCap) {
2044
+ this.timeline.shift();
2045
+ }
2046
+ }
1693
2047
  addTaskNotificationTimeline(timestamp, notification) {
1694
2048
  let description = notification.summary ?? `Background task ${notification.status ?? 'finished'}`;
1695
2049
  if (description.length > 200)
@@ -1940,9 +2294,9 @@ class EventAggregator {
1940
2294
  // Private: Permission Mode
1941
2295
  // ═══════════════════════════════════════════════════════════════════════
1942
2296
  trackPermissionMode(event) {
1943
- if (!event.permissionMode)
2297
+ const mode = asPermissionMode(event.permissionMode);
2298
+ if (!mode)
1944
2299
  return;
1945
- const mode = event.permissionMode;
1946
2300
  if (mode !== this.currentPermissionMode) {
1947
2301
  this.permissionModeHistory.push({
1948
2302
  timestamp: event.timestamp,
@@ -119,9 +119,23 @@ export interface BurnRateInfo {
119
119
  }
120
120
  /** How a subagent ended: a failed spawn, or a background agent's notification status. */
121
121
  export type SubagentOutcome = 'completed' | 'failed' | 'killed' | 'stopped';
122
+ /**
123
+ * What settled a background subagent, when something other than its spawn's
124
+ * own tool result did:
125
+ *
126
+ * - `notification`: its final task notification (Claude Code), or a Codex
127
+ * `SubAgentActivity` `completed` record or `FINAL_ANSWER` report.
128
+ * - `meta`: the host's evidence from Claude Code's `agent-<id>.meta.json`
129
+ * (`finished`, `stoppedByUser`), via `EventAggregator.applySubagentEvidence()`.
130
+ * - `stop-marker`: an in-stream stop: Claude Code's `agents_killed` system
131
+ * line or its "background agents were stopped by the user" notice, or a
132
+ * Codex `interrupt_agent` record.
133
+ * - `task-status`: a Claude Code `task_status` attachment with a final status.
134
+ */
135
+ export type SubagentSettledBy = 'notification' | 'meta' | 'stop-marker' | 'task-status';
122
136
  /** Subagent lifecycle info (for tracking spawn/completion from tool events). */
123
137
  export interface SubagentLifecycle {
124
- /** The spawning tool_use id (`Agent` / `Task`). */
138
+ /** The spawning tool_use id (`Agent` / `Task`, or Codex `spawn_agent`). */
125
139
  id: string;
126
140
  description: string;
127
141
  subagentType: string;
@@ -133,12 +147,22 @@ export interface SubagentLifecycle {
133
147
  */
134
148
  status: 'running' | 'completed';
135
149
  durationMs?: number;
136
- /** Claude Code's agent id (the `agent-<id>.jsonl` transcript name), once known. */
150
+ /**
151
+ * The agent's id, once known: Claude Code's agent id (the `agent-<id>.jsonl`
152
+ * transcript name), or the Codex child thread id (its rollout's session id).
153
+ */
137
154
  agentId?: string;
155
+ /** Codex: the child's agent path (e.g. `/root/review_builder`), once known. */
156
+ agentPath?: string;
138
157
  /** The agent was launched in the background (its spawn returned at once). */
139
158
  background?: boolean;
140
159
  /** How the agent ended, once `status` is `completed`. */
141
160
  outcome?: SubagentOutcome;
161
+ /**
162
+ * What settled a background agent (see {@link SubagentSettledBy}); absent
163
+ * while it runs and when its spawn's own result settled it.
164
+ */
165
+ settledBy?: SubagentSettledBy;
142
166
  }
143
167
  /** Frequency entry for tool/word frequency tracking. */
144
168
  export interface FrequencyMetric {
@@ -178,7 +202,7 @@ export interface AggregatedMetrics {
178
202
  taskState: TaskState;
179
203
  subagents: SubagentLifecycle[];
180
204
  plan: PlanState | null;
181
- /** Current permission mode (default | acceptEdits | bypassPermissions | plan) */
205
+ /** Current permission mode (default | acceptEdits | bypassPermissions | plan | auto | dontAsk) */
182
206
  permissionMode: PermissionMode | null;
183
207
  /** History of permission mode changes */
184
208
  permissionModeHistory: PermissionModeChange[];