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
@@ -18,6 +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 { 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';
21
23
  import { calculateNormalizedUsageCost, extractNormalizedUsage, normalizeProviderUsage, } from '../usageNormalization';
22
24
  // ── Defaults ──
23
25
  const DEFAULT_TIMELINE_CAP = 200;
@@ -26,10 +28,100 @@ const DEFAULT_CONTEXT_TIMELINE_CAP = 2_000;
26
28
  const DEFAULT_BURN_WINDOW_MS = 5 * 60_000;
27
29
  const DEFAULT_BURN_SAMPLE_MS = 10_000;
28
30
  const COMPACTION_DROP_THRESHOLD = 0.8; // >20% drop
29
- /** Schema version for serialized snapshots. v6: token totals count each Claude response and Codex call once. */
30
- 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
+ }
31
51
  // ── Goal gate detection regex ──
32
52
  const GOAL_GATE_REGEX = /\b(CRITICAL|MUST|blocker|required|must.?complete|goal.?gate|essential|do.?not.?skip|blocking)\b/i;
53
+ // ── Task notifications delivered as attachments ──
54
+ /**
55
+ * The notification a `system` FollowEvent carries when it stands for a
56
+ * mid-turn task-notification attachment (its `raw` is the attachment line or
57
+ * the SessionEvent `extractSessionEvents` made from it).
58
+ */
59
+ function taskNotificationFromAttachmentFollowEvent(event) {
60
+ const raw = event.raw;
61
+ if (!raw || typeof raw !== 'object')
62
+ return null;
63
+ const prompt = taskNotificationPromptFromAttachment(raw);
64
+ if (prompt !== null)
65
+ return parseTaskNotification(prompt);
66
+ const asEvent = raw;
67
+ return isTaskNotificationSystemEvent(asEvent) ? taskNotificationFromEvent(asEvent) : null;
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
+ }
33
125
  // ── Todo dependency parsing ──
34
126
  /**
35
127
  * Parses dependency references from a todo item's content text.
@@ -115,6 +207,8 @@ export class EventAggregator {
115
207
  // Subagent lifecycle
116
208
  subagents = [];
117
209
  pendingSubagents = new Map(); // toolUseId -> index
210
+ /** Codex `spawn_agent` calls still waiting for their result (always a background launch). */
211
+ pendingCodexSpawns = new Set();
118
212
  // Plan state
119
213
  planExtractor;
120
214
  planState = null;
@@ -168,6 +262,41 @@ export class EventAggregator {
168
262
  // processEvent — main entry for SessionEvent
169
263
  // ═══════════════════════════════════════════════════════════════════════
170
264
  processEvent(event) {
265
+ // Opt-in mid-turn task notifications (`extractSessionEvents` with
266
+ // `includeTaskNotifications`) only finish background subagents and mark
267
+ // the timeline: they are not messages, prompts, context, or activity.
268
+ if (isTaskNotificationSystemEvent(event)) {
269
+ const notification = taskNotificationFromEvent(event);
270
+ if (notification) {
271
+ this.applyTaskNotification(notification, event.timestamp);
272
+ this.addTaskNotificationTimeline(event.timestamp, notification);
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
+ }
280
+ return;
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);
171
300
  // 1. Increment counters, capture timestamps
172
301
  this.eventCount++;
173
302
  if (!this.sessionStartTime) {
@@ -233,12 +362,24 @@ export class EventAggregator {
233
362
  // processFollowEvent — adapter for CLI FollowEvent format
234
363
  // ═══════════════════════════════════════════════════════════════════════
235
364
  processFollowEvent(event) {
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.
368
+ if (event.type === 'system') {
369
+ const record = subagentRecordFromFollowEvent(event);
370
+ if (record) {
371
+ this.applySubagentFollowRecord(record, event.timestamp);
372
+ return;
373
+ }
374
+ }
236
375
  // 1. Increment counters, capture timestamps
237
376
  this.eventCount++;
238
377
  if (!this.sessionStartTime) {
239
378
  this.sessionStartTime = event.timestamp;
240
379
  }
241
380
  this.lastEventTime = event.timestamp;
381
+ // Followed events carry the mode as a plain string.
382
+ this.trackPermissionMode(event);
242
383
  // Provider
243
384
  if (!this._providerId && event.providerId) {
244
385
  this._providerId = event.providerId;
@@ -370,6 +511,10 @@ export class EventAggregator {
370
511
  getSubagents() {
371
512
  return [...this.subagents];
372
513
  }
514
+ /** Timestamp of the last counted event (`getMetrics().lastEventTime`), without building the metrics. */
515
+ getLastEventTime() {
516
+ return this.lastEventTime;
517
+ }
373
518
  getPlan() {
374
519
  return this.planState
375
520
  ? { ...this.planState, steps: this.planState.steps.map((step) => ({ ...step })) }
@@ -466,6 +611,7 @@ export class EventAggregator {
466
611
  this.activeTaskId = null;
467
612
  this.subagents = [];
468
613
  this.pendingSubagents.clear();
614
+ this.pendingCodexSpawns.clear();
469
615
  this.planExtractor.reset();
470
616
  this.planState = null;
471
617
  this.planEnteredAt = undefined;
@@ -599,7 +745,7 @@ export class EventAggregator {
599
745
  this.tasks = new Map(state.tasks);
600
746
  this.activeTaskId = state.activeTaskId;
601
747
  this.subagents = [...state.subagents];
602
- this.currentPermissionMode = state.permissionMode ?? null;
748
+ this.currentPermissionMode = asPermissionMode(state.permissionMode) ?? null;
603
749
  this.permissionModeHistory = state.permissionModeHistory
604
750
  ? [...state.permissionModeHistory]
605
751
  : [];
@@ -626,6 +772,15 @@ export class EventAggregator {
626
772
  this.pendingToolCalls.clear();
627
773
  this.pendingTaskCreates.clear();
628
774
  this.pendingSubagents.clear();
775
+ this.pendingCodexSpawns.clear();
776
+ // A spawn still waiting for its tool_result is resolved by that result,
777
+ // which can land after the snapshot boundary. Background agents need no
778
+ // entry: their notifications are matched against every subagent.
779
+ this.subagents.forEach((agent, idx) => {
780
+ if (agent.status === 'running' && !agent.background && agent.id) {
781
+ this.pendingSubagents.set(agent.id, idx);
782
+ }
783
+ });
629
784
  this.pendingUserRequest = null;
630
785
  this.planExtractor.reset();
631
786
  return true;
@@ -1104,7 +1259,7 @@ export class EventAggregator {
1104
1259
  else if (name === 'UpdatePlan') {
1105
1260
  this.handleUpdatePlan(input, new Date(event.timestamp));
1106
1261
  }
1107
- else if ((name === 'Agent' || name === 'Task') && toolUseId) {
1262
+ else if (isSubagentSpawnTool(name, input) && toolUseId) {
1108
1263
  this.handleAgentSpawn(toolUseId, input, new Date(event.timestamp));
1109
1264
  }
1110
1265
  }
@@ -1114,7 +1269,7 @@ export class EventAggregator {
1114
1269
  continue;
1115
1270
  const isError = block.is_error ?? false;
1116
1271
  this.resolveTaskCreate(toolUseId, block.content ?? block.output, event.timestamp, isError);
1117
- this.resolveAgentResult(toolUseId, isError, event.timestamp);
1272
+ this.resolveAgentResult(toolUseId, isError, event.timestamp, block.content ?? block.output);
1118
1273
  }
1119
1274
  }
1120
1275
  }
@@ -1148,7 +1303,7 @@ export class EventAggregator {
1148
1303
  else if (event.toolName === 'UpdatePlan') {
1149
1304
  this.handleUpdatePlan(input, new Date(event.timestamp));
1150
1305
  }
1151
- else if ((event.toolName === 'Agent' || event.toolName === 'Task') && toolUseId) {
1306
+ else if (isSubagentSpawnTool(event.toolName, input) && toolUseId) {
1152
1307
  this.handleAgentSpawn(toolUseId, input, new Date(event.timestamp));
1153
1308
  }
1154
1309
  }
@@ -1160,7 +1315,7 @@ export class EventAggregator {
1160
1315
  return;
1161
1316
  const isError = raw.is_error ?? false;
1162
1317
  this.resolveTaskCreate(toolUseId, raw.content, event.timestamp, isError);
1163
- this.resolveAgentResult(toolUseId, isError, event.timestamp);
1318
+ this.resolveAgentResult(toolUseId, isError, event.timestamp, raw.content);
1164
1319
  }
1165
1320
  }
1166
1321
  applyTaskUpdate(input) {
@@ -1447,8 +1602,9 @@ export class EventAggregator {
1447
1602
  // ═══════════════════════════════════════════════════════════════════════
1448
1603
  handleAgentSpawn(toolUseId, input, now) {
1449
1604
  const agentTaskId = `agent-${toolUseId}`;
1450
- const description = input.description ? String(input.description) : 'Subagent';
1451
- const subagentType = input.subagent_type || input.subagentType || undefined;
1605
+ const spawn = spawnFields(input);
1606
+ const description = spawn.description ?? 'Subagent';
1607
+ const subagentType = spawn.subagentType;
1452
1608
  const task = {
1453
1609
  taskId: agentTaskId,
1454
1610
  subject: description,
@@ -1465,11 +1621,15 @@ export class EventAggregator {
1465
1621
  };
1466
1622
  this.tasks.set(agentTaskId, task);
1467
1623
  }
1468
- resolveAgentResult(toolUseId, isError, timestamp) {
1624
+ resolveAgentResult(toolUseId, isError, timestamp, content) {
1469
1625
  const agentTaskId = `agent-${toolUseId}`;
1470
1626
  const agentTask = this.tasks.get(agentTaskId);
1471
1627
  if (!agentTask)
1472
1628
  return;
1629
+ // A background launch returns at once; its task notification finishes it.
1630
+ if (spawnResultKind(this.pendingCodexSpawns.has(toolUseId), content, isError) === 'launch') {
1631
+ return;
1632
+ }
1473
1633
  agentTask.status = isError ? 'deleted' : 'completed';
1474
1634
  agentTask.updatedAt = new Date(timestamp);
1475
1635
  }
@@ -1489,31 +1649,25 @@ export class EventAggregator {
1489
1649
  // Private: Subagent Tracking from SessionEvent
1490
1650
  // ═══════════════════════════════════════════════════════════════════════
1491
1651
  extractSubagentFromEvent(event) {
1652
+ const notification = taskNotificationFromEvent(event);
1653
+ if (notification)
1654
+ this.applyTaskNotification(notification, event.timestamp);
1492
1655
  const content = event.message?.content;
1493
1656
  if (!Array.isArray(content))
1494
1657
  return;
1495
1658
  for (const block of content) {
1496
- if (block.type === 'tool_use' && block.name === 'Task') {
1659
+ if (block.type === 'tool_use' && isSubagentSpawnTool(block.name, block.input)) {
1497
1660
  const input = block.input;
1498
1661
  const toolUseId = block.id;
1499
1662
  if (!input || !toolUseId)
1500
1663
  continue;
1501
- const info = {
1502
- id: toolUseId,
1503
- description: input.description || 'Unknown task',
1504
- subagentType: input.subagent_type || input.subagentType || 'general',
1505
- spawnTime: event.timestamp,
1506
- status: 'running',
1507
- };
1508
- const idx = this.subagents.length;
1509
- this.subagents.push(info);
1510
- this.pendingSubagents.set(toolUseId, idx);
1664
+ this.trackSubagentSpawn(toolUseId, input, event.timestamp, isCodexSubagentSpawnTool(block.name, input));
1511
1665
  }
1512
1666
  else if (block.type === 'tool_result') {
1513
1667
  const toolUseId = block.tool_use_id;
1514
1668
  if (!toolUseId)
1515
1669
  continue;
1516
- this.completeSubagent(toolUseId, event.timestamp);
1670
+ this.resolveSubagentResult(toolUseId, block.content ?? block.output, block.is_error === true, event.timestamp);
1517
1671
  }
1518
1672
  }
1519
1673
  }
@@ -1522,23 +1676,10 @@ export class EventAggregator {
1522
1676
  // ═══════════════════════════════════════════════════════════════════════
1523
1677
  extractSubagentFromFollowEvent(event) {
1524
1678
  const raw = event.raw;
1525
- if (event.type === 'tool_use' && event.toolName === 'Task') {
1679
+ if (event.type === 'tool_use' && isSubagentSpawnTool(event.toolName, raw?.input)) {
1526
1680
  if (!raw?.input)
1527
1681
  return;
1528
- const input = raw.input;
1529
- const toolUseId = raw.id || '';
1530
- const info = {
1531
- id: toolUseId,
1532
- description: input.description || 'Unknown task',
1533
- subagentType: input.subagent_type || input.subagentType || 'general',
1534
- spawnTime: event.timestamp,
1535
- status: 'running',
1536
- };
1537
- const idx = this.subagents.length;
1538
- this.subagents.push(info);
1539
- if (toolUseId) {
1540
- this.pendingSubagents.set(toolUseId, idx);
1541
- }
1682
+ this.trackSubagentSpawn(raw.id || '', raw.input, event.timestamp, isCodexSubagentSpawnTool(event.toolName, raw.input));
1542
1683
  }
1543
1684
  else if (event.type === 'tool_result') {
1544
1685
  if (!raw)
@@ -1546,22 +1687,374 @@ export class EventAggregator {
1546
1687
  const toolUseId = raw.tool_use_id;
1547
1688
  if (!toolUseId)
1548
1689
  return;
1549
- this.completeSubagent(toolUseId, event.timestamp);
1690
+ this.resolveSubagentResult(toolUseId, raw.content, raw.is_error === true, event.timestamp);
1691
+ }
1692
+ else if (event.type === 'user' && raw?.type === 'user') {
1693
+ // Claude Code delivers most task notifications as their own user line.
1694
+ const notification = taskNotificationFromEvent(raw);
1695
+ if (notification)
1696
+ this.applyTaskNotification(notification, event.timestamp);
1697
+ }
1698
+ }
1699
+ trackSubagentSpawn(toolUseId, input, timestamp, codex = false) {
1700
+ const spawn = spawnFields(input);
1701
+ const info = {
1702
+ id: toolUseId,
1703
+ description: spawn.description ?? 'Unknown task',
1704
+ subagentType: spawn.subagentType ?? (codex ? 'default' : 'general'),
1705
+ spawnTime: timestamp,
1706
+ status: 'running',
1707
+ };
1708
+ const idx = this.subagents.length;
1709
+ this.subagents.push(info);
1710
+ if (toolUseId) {
1711
+ this.pendingSubagents.set(toolUseId, idx);
1712
+ if (codex)
1713
+ this.pendingCodexSpawns.add(toolUseId);
1550
1714
  }
1551
1715
  }
1552
- completeSubagent(toolUseId, timestamp) {
1716
+ /**
1717
+ * The spawning tool call's result. A foreground agent ends here; a
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.
1721
+ */
1722
+ resolveSubagentResult(toolUseId, content, isError, timestamp) {
1553
1723
  const idx = this.pendingSubagents.get(toolUseId);
1554
1724
  if (idx === undefined)
1555
1725
  return;
1556
- this.pendingSubagents.delete(toolUseId);
1726
+ const codex = this.pendingCodexSpawns.delete(toolUseId);
1727
+ if (spawnResultKind(codex, content, isError) === 'launch') {
1728
+ const launch = parseCodexSpawnLaunch(content) ?? parseAsyncAgentLaunch(content) ?? {};
1729
+ this.markBackgroundLaunch(idx, launch);
1730
+ return;
1731
+ }
1732
+ this.completeSubagent(toolUseId, timestamp, isError ? 'failed' : 'completed');
1733
+ }
1734
+ markBackgroundLaunch(idx, launch) {
1557
1735
  const agent = this.subagents[idx];
1736
+ this.pendingSubagents.delete(agent.id);
1737
+ agent.background = true;
1738
+ if (launch.agentId)
1739
+ agent.agentId = launch.agentId;
1740
+ if (launch.agentPath && !agent.agentPath)
1741
+ agent.agentPath = launch.agentPath;
1742
+ }
1743
+ completeSubagent(toolUseId, timestamp, outcome = 'completed') {
1744
+ const idx = this.pendingSubagents.get(toolUseId);
1745
+ if (idx === undefined)
1746
+ return;
1747
+ this.finishSubagent(this.subagents[idx], timestamp, outcome);
1748
+ }
1749
+ finishSubagent(agent, timestamp, outcome, settledBy) {
1750
+ this.pendingSubagents.delete(agent.id);
1751
+ this.pendingCodexSpawns.delete(agent.id);
1558
1752
  agent.status = 'completed';
1559
1753
  agent.completionTime = timestamp;
1754
+ agent.outcome = outcome;
1755
+ if (settledBy)
1756
+ agent.settledBy = settledBy;
1757
+ else
1758
+ delete agent.settledBy;
1560
1759
  const start = new Date(agent.spawnTime).getTime();
1561
1760
  const end = new Date(timestamp).getTime();
1562
1761
  if (!isNaN(start) && !isNaN(end) && end >= start) {
1563
1762
  agent.durationMs = end - start;
1564
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);
1801
+ }
1802
+ /**
1803
+ * Applies a background task's notification to the subagent it reports on,
1804
+ * matched by the spawning tool_use id (or by agent id when the notification
1805
+ * has none). The latest notification wins: an interim one ("waiting on its
1806
+ * own background work") keeps or puts the agent back to running, a final one
1807
+ * completes it. Notifications for background shell tasks match nothing.
1808
+ */
1809
+ applyTaskNotification(notification, timestamp) {
1810
+ const agent = this.findNotifiedSubagent(notification);
1811
+ const toolUseId = agent?.id || notification.toolUseId;
1812
+ const outcome = taskNotificationOutcome(notification.status);
1813
+ if (agent) {
1814
+ agent.background = true;
1815
+ if (!agent.agentId)
1816
+ agent.agentId = notification.taskId;
1817
+ if (notification.interim) {
1818
+ this.pendingSubagents.delete(agent.id);
1819
+ agent.status = 'running';
1820
+ delete agent.completionTime;
1821
+ delete agent.durationMs;
1822
+ delete agent.outcome;
1823
+ delete agent.settledBy;
1824
+ }
1825
+ else {
1826
+ this.finishSubagent(agent, timestamp, outcome, 'notification');
1827
+ }
1828
+ }
1829
+ this.settleAgentTask(toolUseId, notification.interim ? null : outcome, timestamp);
1830
+ }
1831
+ findNotifiedSubagent(notification) {
1832
+ const { toolUseId, taskId } = notification;
1833
+ for (let i = this.subagents.length - 1; i >= 0; i--) {
1834
+ if (toolUseId && this.subagents[i].id === toolUseId)
1835
+ return this.subagents[i];
1836
+ }
1837
+ for (let i = this.subagents.length - 1; i >= 0; i--) {
1838
+ if (this.subagents[i].agentId === taskId)
1839
+ return this.subagents[i];
1840
+ }
1841
+ return undefined;
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
+ }
2043
+ addTaskNotificationTimeline(timestamp, notification) {
2044
+ let description = notification.summary ?? `Background task ${notification.status ?? 'finished'}`;
2045
+ if (description.length > 200)
2046
+ description = description.substring(0, 197) + '...';
2047
+ this.timeline.push({
2048
+ type: 'tool_result',
2049
+ timestamp,
2050
+ description,
2051
+ noiseLevel: 'system',
2052
+ messageClassification: 'system',
2053
+ ...(notification.toolUseId ? { metadata: { toolUseId: notification.toolUseId } } : {}),
2054
+ });
2055
+ if (this.timeline.length > this.timelineCap) {
2056
+ this.timeline.shift();
2057
+ }
1565
2058
  }
1566
2059
  // ═══════════════════════════════════════════════════════════════════════
1567
2060
  // Private: Plan Extraction from SessionEvent
@@ -1797,9 +2290,9 @@ export class EventAggregator {
1797
2290
  // Private: Permission Mode
1798
2291
  // ═══════════════════════════════════════════════════════════════════════
1799
2292
  trackPermissionMode(event) {
1800
- if (!event.permissionMode)
2293
+ const mode = asPermissionMode(event.permissionMode);
2294
+ if (!mode)
1801
2295
  return;
1802
- const mode = event.permissionMode;
1803
2296
  if (mode !== this.currentPermissionMode) {
1804
2297
  this.permissionModeHistory.push({
1805
2298
  timestamp: event.timestamp,