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
@@ -22,6 +22,8 @@ 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");
26
+ const subagentSpawn_1 = require("../subagentSpawn");
25
27
  const usageNormalization_1 = require("../usageNormalization");
26
28
  // ── Defaults ──
27
29
  const DEFAULT_TIMELINE_CAP = 200;
@@ -30,10 +32,100 @@ const DEFAULT_CONTEXT_TIMELINE_CAP = 2_000;
30
32
  const DEFAULT_BURN_WINDOW_MS = 5 * 60_000;
31
33
  const DEFAULT_BURN_SAMPLE_MS = 10_000;
32
34
  const COMPACTION_DROP_THRESHOLD = 0.8; // >20% drop
33
- /** Schema version for serialized snapshots. v6: token totals count each Claude response and Codex call once. */
34
- 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
+ }
35
55
  // ── Goal gate detection regex ──
36
56
  const GOAL_GATE_REGEX = /\b(CRITICAL|MUST|blocker|required|must.?complete|goal.?gate|essential|do.?not.?skip|blocking)\b/i;
57
+ // ── Task notifications delivered as attachments ──
58
+ /**
59
+ * The notification a `system` FollowEvent carries when it stands for a
60
+ * mid-turn task-notification attachment (its `raw` is the attachment line or
61
+ * the SessionEvent `extractSessionEvents` made from it).
62
+ */
63
+ function taskNotificationFromAttachmentFollowEvent(event) {
64
+ const raw = event.raw;
65
+ if (!raw || typeof raw !== 'object')
66
+ return null;
67
+ const prompt = (0, subagentSpawn_1.taskNotificationPromptFromAttachment)(raw);
68
+ if (prompt !== null)
69
+ return (0, subagentSpawn_1.parseTaskNotification)(prompt);
70
+ const asEvent = raw;
71
+ return (0, subagentSpawn_1.isTaskNotificationSystemEvent)(asEvent) ? (0, subagentSpawn_1.taskNotificationFromEvent)(asEvent) : null;
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
+ }
37
129
  // ── Todo dependency parsing ──
38
130
  /**
39
131
  * Parses dependency references from a todo item's content text.
@@ -119,6 +211,8 @@ class EventAggregator {
119
211
  // Subagent lifecycle
120
212
  subagents = [];
121
213
  pendingSubagents = new Map(); // toolUseId -> index
214
+ /** Codex `spawn_agent` calls still waiting for their result (always a background launch). */
215
+ pendingCodexSpawns = new Set();
122
216
  // Plan state
123
217
  planExtractor;
124
218
  planState = null;
@@ -172,6 +266,41 @@ class EventAggregator {
172
266
  // processEvent — main entry for SessionEvent
173
267
  // ═══════════════════════════════════════════════════════════════════════
174
268
  processEvent(event) {
269
+ // Opt-in mid-turn task notifications (`extractSessionEvents` with
270
+ // `includeTaskNotifications`) only finish background subagents and mark
271
+ // the timeline: they are not messages, prompts, context, or activity.
272
+ if ((0, subagentSpawn_1.isTaskNotificationSystemEvent)(event)) {
273
+ const notification = (0, subagentSpawn_1.taskNotificationFromEvent)(event);
274
+ if (notification) {
275
+ this.applyTaskNotification(notification, event.timestamp);
276
+ this.addTaskNotificationTimeline(event.timestamp, notification);
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
+ }
284
+ return;
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);
175
304
  // 1. Increment counters, capture timestamps
176
305
  this.eventCount++;
177
306
  if (!this.sessionStartTime) {
@@ -237,12 +366,24 @@ class EventAggregator {
237
366
  // processFollowEvent — adapter for CLI FollowEvent format
238
367
  // ═══════════════════════════════════════════════════════════════════════
239
368
  processFollowEvent(event) {
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.
372
+ if (event.type === 'system') {
373
+ const record = subagentRecordFromFollowEvent(event);
374
+ if (record) {
375
+ this.applySubagentFollowRecord(record, event.timestamp);
376
+ return;
377
+ }
378
+ }
240
379
  // 1. Increment counters, capture timestamps
241
380
  this.eventCount++;
242
381
  if (!this.sessionStartTime) {
243
382
  this.sessionStartTime = event.timestamp;
244
383
  }
245
384
  this.lastEventTime = event.timestamp;
385
+ // Followed events carry the mode as a plain string.
386
+ this.trackPermissionMode(event);
246
387
  // Provider
247
388
  if (!this._providerId && event.providerId) {
248
389
  this._providerId = event.providerId;
@@ -374,6 +515,10 @@ class EventAggregator {
374
515
  getSubagents() {
375
516
  return [...this.subagents];
376
517
  }
518
+ /** Timestamp of the last counted event (`getMetrics().lastEventTime`), without building the metrics. */
519
+ getLastEventTime() {
520
+ return this.lastEventTime;
521
+ }
377
522
  getPlan() {
378
523
  return this.planState
379
524
  ? { ...this.planState, steps: this.planState.steps.map((step) => ({ ...step })) }
@@ -470,6 +615,7 @@ class EventAggregator {
470
615
  this.activeTaskId = null;
471
616
  this.subagents = [];
472
617
  this.pendingSubagents.clear();
618
+ this.pendingCodexSpawns.clear();
473
619
  this.planExtractor.reset();
474
620
  this.planState = null;
475
621
  this.planEnteredAt = undefined;
@@ -603,7 +749,7 @@ class EventAggregator {
603
749
  this.tasks = new Map(state.tasks);
604
750
  this.activeTaskId = state.activeTaskId;
605
751
  this.subagents = [...state.subagents];
606
- this.currentPermissionMode = state.permissionMode ?? null;
752
+ this.currentPermissionMode = asPermissionMode(state.permissionMode) ?? null;
607
753
  this.permissionModeHistory = state.permissionModeHistory
608
754
  ? [...state.permissionModeHistory]
609
755
  : [];
@@ -630,6 +776,15 @@ class EventAggregator {
630
776
  this.pendingToolCalls.clear();
631
777
  this.pendingTaskCreates.clear();
632
778
  this.pendingSubagents.clear();
779
+ this.pendingCodexSpawns.clear();
780
+ // A spawn still waiting for its tool_result is resolved by that result,
781
+ // which can land after the snapshot boundary. Background agents need no
782
+ // entry: their notifications are matched against every subagent.
783
+ this.subagents.forEach((agent, idx) => {
784
+ if (agent.status === 'running' && !agent.background && agent.id) {
785
+ this.pendingSubagents.set(agent.id, idx);
786
+ }
787
+ });
633
788
  this.pendingUserRequest = null;
634
789
  this.planExtractor.reset();
635
790
  return true;
@@ -1108,7 +1263,7 @@ class EventAggregator {
1108
1263
  else if (name === 'UpdatePlan') {
1109
1264
  this.handleUpdatePlan(input, new Date(event.timestamp));
1110
1265
  }
1111
- else if ((name === 'Agent' || name === 'Task') && toolUseId) {
1266
+ else if ((0, subagentSpawn_1.isSubagentSpawnTool)(name, input) && toolUseId) {
1112
1267
  this.handleAgentSpawn(toolUseId, input, new Date(event.timestamp));
1113
1268
  }
1114
1269
  }
@@ -1118,7 +1273,7 @@ class EventAggregator {
1118
1273
  continue;
1119
1274
  const isError = block.is_error ?? false;
1120
1275
  this.resolveTaskCreate(toolUseId, block.content ?? block.output, event.timestamp, isError);
1121
- this.resolveAgentResult(toolUseId, isError, event.timestamp);
1276
+ this.resolveAgentResult(toolUseId, isError, event.timestamp, block.content ?? block.output);
1122
1277
  }
1123
1278
  }
1124
1279
  }
@@ -1152,7 +1307,7 @@ class EventAggregator {
1152
1307
  else if (event.toolName === 'UpdatePlan') {
1153
1308
  this.handleUpdatePlan(input, new Date(event.timestamp));
1154
1309
  }
1155
- else if ((event.toolName === 'Agent' || event.toolName === 'Task') && toolUseId) {
1310
+ else if ((0, subagentSpawn_1.isSubagentSpawnTool)(event.toolName, input) && toolUseId) {
1156
1311
  this.handleAgentSpawn(toolUseId, input, new Date(event.timestamp));
1157
1312
  }
1158
1313
  }
@@ -1164,7 +1319,7 @@ class EventAggregator {
1164
1319
  return;
1165
1320
  const isError = raw.is_error ?? false;
1166
1321
  this.resolveTaskCreate(toolUseId, raw.content, event.timestamp, isError);
1167
- this.resolveAgentResult(toolUseId, isError, event.timestamp);
1322
+ this.resolveAgentResult(toolUseId, isError, event.timestamp, raw.content);
1168
1323
  }
1169
1324
  }
1170
1325
  applyTaskUpdate(input) {
@@ -1451,8 +1606,9 @@ class EventAggregator {
1451
1606
  // ═══════════════════════════════════════════════════════════════════════
1452
1607
  handleAgentSpawn(toolUseId, input, now) {
1453
1608
  const agentTaskId = `agent-${toolUseId}`;
1454
- const description = input.description ? String(input.description) : 'Subagent';
1455
- const subagentType = input.subagent_type || input.subagentType || undefined;
1609
+ const spawn = spawnFields(input);
1610
+ const description = spawn.description ?? 'Subagent';
1611
+ const subagentType = spawn.subagentType;
1456
1612
  const task = {
1457
1613
  taskId: agentTaskId,
1458
1614
  subject: description,
@@ -1469,11 +1625,15 @@ class EventAggregator {
1469
1625
  };
1470
1626
  this.tasks.set(agentTaskId, task);
1471
1627
  }
1472
- resolveAgentResult(toolUseId, isError, timestamp) {
1628
+ resolveAgentResult(toolUseId, isError, timestamp, content) {
1473
1629
  const agentTaskId = `agent-${toolUseId}`;
1474
1630
  const agentTask = this.tasks.get(agentTaskId);
1475
1631
  if (!agentTask)
1476
1632
  return;
1633
+ // A background launch returns at once; its task notification finishes it.
1634
+ if (spawnResultKind(this.pendingCodexSpawns.has(toolUseId), content, isError) === 'launch') {
1635
+ return;
1636
+ }
1477
1637
  agentTask.status = isError ? 'deleted' : 'completed';
1478
1638
  agentTask.updatedAt = new Date(timestamp);
1479
1639
  }
@@ -1493,31 +1653,25 @@ class EventAggregator {
1493
1653
  // Private: Subagent Tracking from SessionEvent
1494
1654
  // ═══════════════════════════════════════════════════════════════════════
1495
1655
  extractSubagentFromEvent(event) {
1656
+ const notification = (0, subagentSpawn_1.taskNotificationFromEvent)(event);
1657
+ if (notification)
1658
+ this.applyTaskNotification(notification, event.timestamp);
1496
1659
  const content = event.message?.content;
1497
1660
  if (!Array.isArray(content))
1498
1661
  return;
1499
1662
  for (const block of content) {
1500
- if (block.type === 'tool_use' && block.name === 'Task') {
1663
+ if (block.type === 'tool_use' && (0, subagentSpawn_1.isSubagentSpawnTool)(block.name, block.input)) {
1501
1664
  const input = block.input;
1502
1665
  const toolUseId = block.id;
1503
1666
  if (!input || !toolUseId)
1504
1667
  continue;
1505
- const info = {
1506
- id: toolUseId,
1507
- description: input.description || 'Unknown task',
1508
- subagentType: input.subagent_type || input.subagentType || 'general',
1509
- spawnTime: event.timestamp,
1510
- status: 'running',
1511
- };
1512
- const idx = this.subagents.length;
1513
- this.subagents.push(info);
1514
- this.pendingSubagents.set(toolUseId, idx);
1668
+ this.trackSubagentSpawn(toolUseId, input, event.timestamp, (0, subagentSpawn_1.isCodexSubagentSpawnTool)(block.name, input));
1515
1669
  }
1516
1670
  else if (block.type === 'tool_result') {
1517
1671
  const toolUseId = block.tool_use_id;
1518
1672
  if (!toolUseId)
1519
1673
  continue;
1520
- this.completeSubagent(toolUseId, event.timestamp);
1674
+ this.resolveSubagentResult(toolUseId, block.content ?? block.output, block.is_error === true, event.timestamp);
1521
1675
  }
1522
1676
  }
1523
1677
  }
@@ -1526,23 +1680,10 @@ class EventAggregator {
1526
1680
  // ═══════════════════════════════════════════════════════════════════════
1527
1681
  extractSubagentFromFollowEvent(event) {
1528
1682
  const raw = event.raw;
1529
- if (event.type === 'tool_use' && event.toolName === 'Task') {
1683
+ if (event.type === 'tool_use' && (0, subagentSpawn_1.isSubagentSpawnTool)(event.toolName, raw?.input)) {
1530
1684
  if (!raw?.input)
1531
1685
  return;
1532
- const input = raw.input;
1533
- const toolUseId = raw.id || '';
1534
- const info = {
1535
- id: toolUseId,
1536
- description: input.description || 'Unknown task',
1537
- subagentType: input.subagent_type || input.subagentType || 'general',
1538
- spawnTime: event.timestamp,
1539
- status: 'running',
1540
- };
1541
- const idx = this.subagents.length;
1542
- this.subagents.push(info);
1543
- if (toolUseId) {
1544
- this.pendingSubagents.set(toolUseId, idx);
1545
- }
1686
+ this.trackSubagentSpawn(raw.id || '', raw.input, event.timestamp, (0, subagentSpawn_1.isCodexSubagentSpawnTool)(event.toolName, raw.input));
1546
1687
  }
1547
1688
  else if (event.type === 'tool_result') {
1548
1689
  if (!raw)
@@ -1550,22 +1691,374 @@ class EventAggregator {
1550
1691
  const toolUseId = raw.tool_use_id;
1551
1692
  if (!toolUseId)
1552
1693
  return;
1553
- this.completeSubagent(toolUseId, event.timestamp);
1694
+ this.resolveSubagentResult(toolUseId, raw.content, raw.is_error === true, event.timestamp);
1695
+ }
1696
+ else if (event.type === 'user' && raw?.type === 'user') {
1697
+ // Claude Code delivers most task notifications as their own user line.
1698
+ const notification = (0, subagentSpawn_1.taskNotificationFromEvent)(raw);
1699
+ if (notification)
1700
+ this.applyTaskNotification(notification, event.timestamp);
1701
+ }
1702
+ }
1703
+ trackSubagentSpawn(toolUseId, input, timestamp, codex = false) {
1704
+ const spawn = spawnFields(input);
1705
+ const info = {
1706
+ id: toolUseId,
1707
+ description: spawn.description ?? 'Unknown task',
1708
+ subagentType: spawn.subagentType ?? (codex ? 'default' : 'general'),
1709
+ spawnTime: timestamp,
1710
+ status: 'running',
1711
+ };
1712
+ const idx = this.subagents.length;
1713
+ this.subagents.push(info);
1714
+ if (toolUseId) {
1715
+ this.pendingSubagents.set(toolUseId, idx);
1716
+ if (codex)
1717
+ this.pendingCodexSpawns.add(toolUseId);
1554
1718
  }
1555
1719
  }
1556
- completeSubagent(toolUseId, timestamp) {
1720
+ /**
1721
+ * The spawning tool call's result. A foreground agent ends here; a
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.
1725
+ */
1726
+ resolveSubagentResult(toolUseId, content, isError, timestamp) {
1557
1727
  const idx = this.pendingSubagents.get(toolUseId);
1558
1728
  if (idx === undefined)
1559
1729
  return;
1560
- this.pendingSubagents.delete(toolUseId);
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) ?? {};
1733
+ this.markBackgroundLaunch(idx, launch);
1734
+ return;
1735
+ }
1736
+ this.completeSubagent(toolUseId, timestamp, isError ? 'failed' : 'completed');
1737
+ }
1738
+ markBackgroundLaunch(idx, launch) {
1561
1739
  const agent = this.subagents[idx];
1740
+ this.pendingSubagents.delete(agent.id);
1741
+ agent.background = true;
1742
+ if (launch.agentId)
1743
+ agent.agentId = launch.agentId;
1744
+ if (launch.agentPath && !agent.agentPath)
1745
+ agent.agentPath = launch.agentPath;
1746
+ }
1747
+ completeSubagent(toolUseId, timestamp, outcome = 'completed') {
1748
+ const idx = this.pendingSubagents.get(toolUseId);
1749
+ if (idx === undefined)
1750
+ return;
1751
+ this.finishSubagent(this.subagents[idx], timestamp, outcome);
1752
+ }
1753
+ finishSubagent(agent, timestamp, outcome, settledBy) {
1754
+ this.pendingSubagents.delete(agent.id);
1755
+ this.pendingCodexSpawns.delete(agent.id);
1562
1756
  agent.status = 'completed';
1563
1757
  agent.completionTime = timestamp;
1758
+ agent.outcome = outcome;
1759
+ if (settledBy)
1760
+ agent.settledBy = settledBy;
1761
+ else
1762
+ delete agent.settledBy;
1564
1763
  const start = new Date(agent.spawnTime).getTime();
1565
1764
  const end = new Date(timestamp).getTime();
1566
1765
  if (!isNaN(start) && !isNaN(end) && end >= start) {
1567
1766
  agent.durationMs = end - start;
1568
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);
1805
+ }
1806
+ /**
1807
+ * Applies a background task's notification to the subagent it reports on,
1808
+ * matched by the spawning tool_use id (or by agent id when the notification
1809
+ * has none). The latest notification wins: an interim one ("waiting on its
1810
+ * own background work") keeps or puts the agent back to running, a final one
1811
+ * completes it. Notifications for background shell tasks match nothing.
1812
+ */
1813
+ applyTaskNotification(notification, timestamp) {
1814
+ const agent = this.findNotifiedSubagent(notification);
1815
+ const toolUseId = agent?.id || notification.toolUseId;
1816
+ const outcome = (0, subagentSpawn_1.taskNotificationOutcome)(notification.status);
1817
+ if (agent) {
1818
+ agent.background = true;
1819
+ if (!agent.agentId)
1820
+ agent.agentId = notification.taskId;
1821
+ if (notification.interim) {
1822
+ this.pendingSubagents.delete(agent.id);
1823
+ agent.status = 'running';
1824
+ delete agent.completionTime;
1825
+ delete agent.durationMs;
1826
+ delete agent.outcome;
1827
+ delete agent.settledBy;
1828
+ }
1829
+ else {
1830
+ this.finishSubagent(agent, timestamp, outcome, 'notification');
1831
+ }
1832
+ }
1833
+ this.settleAgentTask(toolUseId, notification.interim ? null : outcome, timestamp);
1834
+ }
1835
+ findNotifiedSubagent(notification) {
1836
+ const { toolUseId, taskId } = notification;
1837
+ for (let i = this.subagents.length - 1; i >= 0; i--) {
1838
+ if (toolUseId && this.subagents[i].id === toolUseId)
1839
+ return this.subagents[i];
1840
+ }
1841
+ for (let i = this.subagents.length - 1; i >= 0; i--) {
1842
+ if (this.subagents[i].agentId === taskId)
1843
+ return this.subagents[i];
1844
+ }
1845
+ return undefined;
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
+ }
2047
+ addTaskNotificationTimeline(timestamp, notification) {
2048
+ let description = notification.summary ?? `Background task ${notification.status ?? 'finished'}`;
2049
+ if (description.length > 200)
2050
+ description = description.substring(0, 197) + '...';
2051
+ this.timeline.push({
2052
+ type: 'tool_result',
2053
+ timestamp,
2054
+ description,
2055
+ noiseLevel: 'system',
2056
+ messageClassification: 'system',
2057
+ ...(notification.toolUseId ? { metadata: { toolUseId: notification.toolUseId } } : {}),
2058
+ });
2059
+ if (this.timeline.length > this.timelineCap) {
2060
+ this.timeline.shift();
2061
+ }
1569
2062
  }
1570
2063
  // ═══════════════════════════════════════════════════════════════════════
1571
2064
  // Private: Plan Extraction from SessionEvent
@@ -1801,9 +2294,9 @@ class EventAggregator {
1801
2294
  // Private: Permission Mode
1802
2295
  // ═══════════════════════════════════════════════════════════════════════
1803
2296
  trackPermissionMode(event) {
1804
- if (!event.permissionMode)
2297
+ const mode = asPermissionMode(event.permissionMode);
2298
+ if (!mode)
1805
2299
  return;
1806
- const mode = event.permissionMode;
1807
2300
  if (mode !== this.currentPermissionMode) {
1808
2301
  this.permissionModeHistory.push({
1809
2302
  timestamp: event.timestamp,