sidekick-shared 0.29.0 → 0.29.1

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 (84) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +56 -25
  3. package/dist/aggregation/EventAggregator.d.ts +19 -0
  4. package/dist/aggregation/EventAggregator.js +174 -35
  5. package/dist/aggregation/snapshot.d.ts +37 -8
  6. package/dist/aggregation/snapshot.js +31 -9
  7. package/dist/aggregation/types.d.ts +13 -0
  8. package/dist/browser.d.ts +5 -1
  9. package/dist/browser.js +9 -3
  10. package/dist/context/sessionContext.js +4 -0
  11. package/dist/esm/aggregation/EventAggregator.js +174 -35
  12. package/dist/esm/aggregation/snapshot.js +31 -9
  13. package/dist/esm/browser.js +1 -0
  14. package/dist/esm/context/sessionContext.js +4 -0
  15. package/dist/esm/formatters/toolSummary.js +4 -1
  16. package/dist/esm/index.js +3 -1
  17. package/dist/esm/modelCatalog.js +5 -4
  18. package/dist/esm/modelContext.js +4 -3
  19. package/dist/esm/modelInfo.js +4 -3
  20. package/dist/esm/node.js +1 -1
  21. package/dist/esm/observedContextWindows.js +2 -1
  22. package/dist/esm/ownKey.js +26 -0
  23. package/dist/esm/parsers/codexParser.js +180 -4
  24. package/dist/esm/parsers/openCodeParser.js +2 -1
  25. package/dist/esm/parsers/subagentScanner.js +33 -12
  26. package/dist/esm/parsers/subagentTraceParser.js +97 -37
  27. package/dist/esm/pricingCatalog.js +3 -2
  28. package/dist/esm/providers/claudeCode.js +10 -2
  29. package/dist/esm/providers/codex.js +82 -38
  30. package/dist/esm/schemas/sessionEvent.js +35 -14
  31. package/dist/esm/subagentSpawn.js +163 -0
  32. package/dist/esm/transcript.js +4 -0
  33. package/dist/esm/turns/assistantTurn.js +3 -2
  34. package/dist/esm/usage/claudeUsageDedupe.js +68 -0
  35. package/dist/esm/watchers/eventBridge.js +15 -0
  36. package/dist/esm/watchers/factory.js +5 -3
  37. package/dist/esm/watchers/jsonlTail.js +73 -8
  38. package/dist/esm/watchers/jsonlWatcher.js +51 -4
  39. package/dist/esm/watchers/providerReaderWatcher.js +13 -0
  40. package/dist/esm/zaiQuota.js +2 -1
  41. package/dist/formatters/toolSummary.js +4 -1
  42. package/dist/index.d.ts +8 -3
  43. package/dist/index.js +18 -9
  44. package/dist/modelCatalog.js +5 -4
  45. package/dist/modelContext.js +4 -3
  46. package/dist/modelInfo.js +4 -3
  47. package/dist/node.d.ts +1 -1
  48. package/dist/node.js +3 -1
  49. package/dist/observedContextWindows.js +2 -1
  50. package/dist/ownKey.d.ts +16 -0
  51. package/dist/ownKey.js +30 -0
  52. package/dist/parsers/codexParser.d.ts +63 -0
  53. package/dist/parsers/codexParser.js +181 -4
  54. package/dist/parsers/openCodeParser.js +2 -1
  55. package/dist/parsers/subagentScanner.d.ts +19 -6
  56. package/dist/parsers/subagentScanner.js +33 -12
  57. package/dist/parsers/subagentTraceParser.d.ts +25 -5
  58. package/dist/parsers/subagentTraceParser.js +97 -37
  59. package/dist/pricingCatalog.js +3 -2
  60. package/dist/providers/claudeCode.js +9 -1
  61. package/dist/providers/codex.js +82 -38
  62. package/dist/providers/types.d.ts +14 -0
  63. package/dist/schemas/index.d.ts +1 -0
  64. package/dist/schemas/sessionEvent.d.ts +27 -10
  65. package/dist/schemas/sessionEvent.js +35 -14
  66. package/dist/subagentSpawn.d.ts +80 -0
  67. package/dist/subagentSpawn.js +174 -0
  68. package/dist/transcript.js +4 -0
  69. package/dist/turns/assistantTurn.d.ts +1 -0
  70. package/dist/turns/assistantTurn.js +3 -2
  71. package/dist/usage/claudeUsageDedupe.d.ts +31 -0
  72. package/dist/usage/claudeUsageDedupe.js +68 -0
  73. package/dist/watchers/eventBridge.js +15 -0
  74. package/dist/watchers/factory.d.ts +6 -0
  75. package/dist/watchers/factory.js +5 -3
  76. package/dist/watchers/jsonlTail.d.ts +27 -4
  77. package/dist/watchers/jsonlTail.js +75 -8
  78. package/dist/watchers/jsonlWatcher.d.ts +18 -1
  79. package/dist/watchers/jsonlWatcher.js +50 -3
  80. package/dist/watchers/providerReaderWatcher.d.ts +7 -0
  81. package/dist/watchers/providerReaderWatcher.js +13 -0
  82. package/dist/watchers/types.d.ts +12 -0
  83. package/dist/zaiQuota.js +2 -1
  84. 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 subagentSpawn_1 = require("../subagentSpawn");
25
26
  const usageNormalization_1 = require("../usageNormalization");
26
27
  // ── Defaults ──
27
28
  const DEFAULT_TIMELINE_CAP = 200;
@@ -34,6 +35,22 @@ const COMPACTION_DROP_THRESHOLD = 0.8; // >20% drop
34
35
  exports.SNAPSHOT_SCHEMA_VERSION = 6;
35
36
  // ── Goal gate detection regex ──
36
37
  const GOAL_GATE_REGEX = /\b(CRITICAL|MUST|blocker|required|must.?complete|goal.?gate|essential|do.?not.?skip|blocking)\b/i;
38
+ // ── Task notifications delivered as attachments ──
39
+ /**
40
+ * The notification a `system` FollowEvent carries when it stands for a
41
+ * mid-turn task-notification attachment (its `raw` is the attachment line or
42
+ * the SessionEvent `extractSessionEvents` made from it).
43
+ */
44
+ function taskNotificationFromAttachmentFollowEvent(event) {
45
+ const raw = event.raw;
46
+ if (!raw || typeof raw !== 'object')
47
+ return null;
48
+ const prompt = (0, subagentSpawn_1.taskNotificationPromptFromAttachment)(raw);
49
+ if (prompt !== null)
50
+ return (0, subagentSpawn_1.parseTaskNotification)(prompt);
51
+ const asEvent = raw;
52
+ return (0, subagentSpawn_1.isTaskNotificationSystemEvent)(asEvent) ? (0, subagentSpawn_1.taskNotificationFromEvent)(asEvent) : null;
53
+ }
37
54
  // ── Todo dependency parsing ──
38
55
  /**
39
56
  * Parses dependency references from a todo item's content text.
@@ -172,6 +189,17 @@ class EventAggregator {
172
189
  // processEvent — main entry for SessionEvent
173
190
  // ═══════════════════════════════════════════════════════════════════════
174
191
  processEvent(event) {
192
+ // Opt-in mid-turn task notifications (`extractSessionEvents` with
193
+ // `includeTaskNotifications`) only finish background subagents and mark
194
+ // the timeline: they are not messages, prompts, context, or activity.
195
+ if ((0, subagentSpawn_1.isTaskNotificationSystemEvent)(event)) {
196
+ const notification = (0, subagentSpawn_1.taskNotificationFromEvent)(event);
197
+ if (notification) {
198
+ this.applyTaskNotification(notification, event.timestamp);
199
+ this.addTaskNotificationTimeline(event.timestamp, notification);
200
+ }
201
+ return;
202
+ }
175
203
  // 1. Increment counters, capture timestamps
176
204
  this.eventCount++;
177
205
  if (!this.sessionStartTime) {
@@ -237,6 +265,15 @@ class EventAggregator {
237
265
  // processFollowEvent — adapter for CLI FollowEvent format
238
266
  // ═══════════════════════════════════════════════════════════════════════
239
267
  processFollowEvent(event) {
268
+ // A mid-turn task-notification attachment, like its SessionEvent form above.
269
+ if (event.type === 'system') {
270
+ const notification = taskNotificationFromAttachmentFollowEvent(event);
271
+ if (notification) {
272
+ this.applyTaskNotification(notification, event.timestamp);
273
+ this.addTaskNotificationTimeline(event.timestamp, notification);
274
+ return;
275
+ }
276
+ }
240
277
  // 1. Increment counters, capture timestamps
241
278
  this.eventCount++;
242
279
  if (!this.sessionStartTime) {
@@ -630,6 +667,14 @@ class EventAggregator {
630
667
  this.pendingToolCalls.clear();
631
668
  this.pendingTaskCreates.clear();
632
669
  this.pendingSubagents.clear();
670
+ // A spawn still waiting for its tool_result is resolved by that result,
671
+ // which can land after the snapshot boundary. Background agents need no
672
+ // entry: their notifications are matched against every subagent.
673
+ this.subagents.forEach((agent, idx) => {
674
+ if (agent.status === 'running' && !agent.background && agent.id) {
675
+ this.pendingSubagents.set(agent.id, idx);
676
+ }
677
+ });
633
678
  this.pendingUserRequest = null;
634
679
  this.planExtractor.reset();
635
680
  return true;
@@ -1108,7 +1153,7 @@ class EventAggregator {
1108
1153
  else if (name === 'UpdatePlan') {
1109
1154
  this.handleUpdatePlan(input, new Date(event.timestamp));
1110
1155
  }
1111
- else if ((name === 'Agent' || name === 'Task') && toolUseId) {
1156
+ else if ((0, subagentSpawn_1.isSubagentSpawnTool)(name) && toolUseId) {
1112
1157
  this.handleAgentSpawn(toolUseId, input, new Date(event.timestamp));
1113
1158
  }
1114
1159
  }
@@ -1118,7 +1163,7 @@ class EventAggregator {
1118
1163
  continue;
1119
1164
  const isError = block.is_error ?? false;
1120
1165
  this.resolveTaskCreate(toolUseId, block.content ?? block.output, event.timestamp, isError);
1121
- this.resolveAgentResult(toolUseId, isError, event.timestamp);
1166
+ this.resolveAgentResult(toolUseId, isError, event.timestamp, block.content ?? block.output);
1122
1167
  }
1123
1168
  }
1124
1169
  }
@@ -1152,7 +1197,7 @@ class EventAggregator {
1152
1197
  else if (event.toolName === 'UpdatePlan') {
1153
1198
  this.handleUpdatePlan(input, new Date(event.timestamp));
1154
1199
  }
1155
- else if ((event.toolName === 'Agent' || event.toolName === 'Task') && toolUseId) {
1200
+ else if ((0, subagentSpawn_1.isSubagentSpawnTool)(event.toolName) && toolUseId) {
1156
1201
  this.handleAgentSpawn(toolUseId, input, new Date(event.timestamp));
1157
1202
  }
1158
1203
  }
@@ -1164,7 +1209,7 @@ class EventAggregator {
1164
1209
  return;
1165
1210
  const isError = raw.is_error ?? false;
1166
1211
  this.resolveTaskCreate(toolUseId, raw.content, event.timestamp, isError);
1167
- this.resolveAgentResult(toolUseId, isError, event.timestamp);
1212
+ this.resolveAgentResult(toolUseId, isError, event.timestamp, raw.content);
1168
1213
  }
1169
1214
  }
1170
1215
  applyTaskUpdate(input) {
@@ -1469,11 +1514,14 @@ class EventAggregator {
1469
1514
  };
1470
1515
  this.tasks.set(agentTaskId, task);
1471
1516
  }
1472
- resolveAgentResult(toolUseId, isError, timestamp) {
1517
+ resolveAgentResult(toolUseId, isError, timestamp, content) {
1473
1518
  const agentTaskId = `agent-${toolUseId}`;
1474
1519
  const agentTask = this.tasks.get(agentTaskId);
1475
1520
  if (!agentTask)
1476
1521
  return;
1522
+ // A background launch returns at once; its task notification finishes it.
1523
+ if (!isError && (0, subagentSpawn_1.parseAsyncAgentLaunch)(content))
1524
+ return;
1477
1525
  agentTask.status = isError ? 'deleted' : 'completed';
1478
1526
  agentTask.updatedAt = new Date(timestamp);
1479
1527
  }
@@ -1493,31 +1541,25 @@ class EventAggregator {
1493
1541
  // Private: Subagent Tracking from SessionEvent
1494
1542
  // ═══════════════════════════════════════════════════════════════════════
1495
1543
  extractSubagentFromEvent(event) {
1544
+ const notification = (0, subagentSpawn_1.taskNotificationFromEvent)(event);
1545
+ if (notification)
1546
+ this.applyTaskNotification(notification, event.timestamp);
1496
1547
  const content = event.message?.content;
1497
1548
  if (!Array.isArray(content))
1498
1549
  return;
1499
1550
  for (const block of content) {
1500
- if (block.type === 'tool_use' && block.name === 'Task') {
1551
+ if (block.type === 'tool_use' && (0, subagentSpawn_1.isSubagentSpawnTool)(block.name)) {
1501
1552
  const input = block.input;
1502
1553
  const toolUseId = block.id;
1503
1554
  if (!input || !toolUseId)
1504
1555
  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);
1556
+ this.trackSubagentSpawn(toolUseId, input, event.timestamp);
1515
1557
  }
1516
1558
  else if (block.type === 'tool_result') {
1517
1559
  const toolUseId = block.tool_use_id;
1518
1560
  if (!toolUseId)
1519
1561
  continue;
1520
- this.completeSubagent(toolUseId, event.timestamp);
1562
+ this.resolveSubagentResult(toolUseId, block.content ?? block.output, block.is_error === true, event.timestamp);
1521
1563
  }
1522
1564
  }
1523
1565
  }
@@ -1526,23 +1568,10 @@ class EventAggregator {
1526
1568
  // ═══════════════════════════════════════════════════════════════════════
1527
1569
  extractSubagentFromFollowEvent(event) {
1528
1570
  const raw = event.raw;
1529
- if (event.type === 'tool_use' && event.toolName === 'Task') {
1571
+ if (event.type === 'tool_use' && (0, subagentSpawn_1.isSubagentSpawnTool)(event.toolName)) {
1530
1572
  if (!raw?.input)
1531
1573
  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
- }
1574
+ this.trackSubagentSpawn(raw.id || '', raw.input, event.timestamp);
1546
1575
  }
1547
1576
  else if (event.type === 'tool_result') {
1548
1577
  if (!raw)
@@ -1550,23 +1579,133 @@ class EventAggregator {
1550
1579
  const toolUseId = raw.tool_use_id;
1551
1580
  if (!toolUseId)
1552
1581
  return;
1553
- this.completeSubagent(toolUseId, event.timestamp);
1582
+ this.resolveSubagentResult(toolUseId, raw.content, raw.is_error === true, event.timestamp);
1583
+ }
1584
+ else if (event.type === 'user' && raw?.type === 'user') {
1585
+ // Claude Code delivers most task notifications as their own user line.
1586
+ const notification = (0, subagentSpawn_1.taskNotificationFromEvent)(raw);
1587
+ if (notification)
1588
+ this.applyTaskNotification(notification, event.timestamp);
1589
+ }
1590
+ }
1591
+ trackSubagentSpawn(toolUseId, input, timestamp) {
1592
+ const info = {
1593
+ id: toolUseId,
1594
+ description: input.description || 'Unknown task',
1595
+ subagentType: input.subagent_type || input.subagentType || 'general',
1596
+ spawnTime: timestamp,
1597
+ status: 'running',
1598
+ };
1599
+ const idx = this.subagents.length;
1600
+ this.subagents.push(info);
1601
+ if (toolUseId) {
1602
+ this.pendingSubagents.set(toolUseId, idx);
1554
1603
  }
1555
1604
  }
1556
- completeSubagent(toolUseId, timestamp) {
1605
+ /**
1606
+ * 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.
1609
+ */
1610
+ resolveSubagentResult(toolUseId, content, isError, timestamp) {
1557
1611
  const idx = this.pendingSubagents.get(toolUseId);
1558
1612
  if (idx === undefined)
1559
1613
  return;
1560
- this.pendingSubagents.delete(toolUseId);
1614
+ const launch = isError ? null : (0, subagentSpawn_1.parseAsyncAgentLaunch)(content);
1615
+ if (launch) {
1616
+ this.markBackgroundLaunch(idx, launch);
1617
+ return;
1618
+ }
1619
+ this.completeSubagent(toolUseId, timestamp, isError ? 'failed' : 'completed');
1620
+ }
1621
+ markBackgroundLaunch(idx, launch) {
1561
1622
  const agent = this.subagents[idx];
1623
+ this.pendingSubagents.delete(agent.id);
1624
+ agent.background = true;
1625
+ if (launch.agentId)
1626
+ agent.agentId = launch.agentId;
1627
+ }
1628
+ completeSubagent(toolUseId, timestamp, outcome = 'completed') {
1629
+ const idx = this.pendingSubagents.get(toolUseId);
1630
+ if (idx === undefined)
1631
+ return;
1632
+ this.finishSubagent(this.subagents[idx], timestamp, outcome);
1633
+ }
1634
+ finishSubagent(agent, timestamp, outcome) {
1635
+ this.pendingSubagents.delete(agent.id);
1562
1636
  agent.status = 'completed';
1563
1637
  agent.completionTime = timestamp;
1638
+ agent.outcome = outcome;
1564
1639
  const start = new Date(agent.spawnTime).getTime();
1565
1640
  const end = new Date(timestamp).getTime();
1566
1641
  if (!isNaN(start) && !isNaN(end) && end >= start) {
1567
1642
  agent.durationMs = end - start;
1568
1643
  }
1569
1644
  }
1645
+ /**
1646
+ * Applies a background task's notification to the subagent it reports on,
1647
+ * matched by the spawning tool_use id (or by agent id when the notification
1648
+ * has none). The latest notification wins: an interim one ("waiting on its
1649
+ * own background work") keeps or puts the agent back to running, a final one
1650
+ * completes it. Notifications for background shell tasks match nothing.
1651
+ */
1652
+ applyTaskNotification(notification, timestamp) {
1653
+ const agent = this.findNotifiedSubagent(notification);
1654
+ const toolUseId = agent?.id || notification.toolUseId;
1655
+ const outcome = (0, subagentSpawn_1.taskNotificationOutcome)(notification.status);
1656
+ if (agent) {
1657
+ agent.background = true;
1658
+ if (!agent.agentId)
1659
+ agent.agentId = notification.taskId;
1660
+ if (notification.interim) {
1661
+ this.pendingSubagents.delete(agent.id);
1662
+ agent.status = 'running';
1663
+ delete agent.completionTime;
1664
+ delete agent.durationMs;
1665
+ delete agent.outcome;
1666
+ }
1667
+ else {
1668
+ this.finishSubagent(agent, timestamp, outcome);
1669
+ }
1670
+ }
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
+ }
1680
+ }
1681
+ findNotifiedSubagent(notification) {
1682
+ const { toolUseId, taskId } = notification;
1683
+ for (let i = this.subagents.length - 1; i >= 0; i--) {
1684
+ if (toolUseId && this.subagents[i].id === toolUseId)
1685
+ return this.subagents[i];
1686
+ }
1687
+ for (let i = this.subagents.length - 1; i >= 0; i--) {
1688
+ if (this.subagents[i].agentId === taskId)
1689
+ return this.subagents[i];
1690
+ }
1691
+ return undefined;
1692
+ }
1693
+ addTaskNotificationTimeline(timestamp, notification) {
1694
+ let description = notification.summary ?? `Background task ${notification.status ?? 'finished'}`;
1695
+ if (description.length > 200)
1696
+ description = description.substring(0, 197) + '...';
1697
+ this.timeline.push({
1698
+ type: 'tool_result',
1699
+ timestamp,
1700
+ description,
1701
+ noiseLevel: 'system',
1702
+ messageClassification: 'system',
1703
+ ...(notification.toolUseId ? { metadata: { toolUseId: notification.toolUseId } } : {}),
1704
+ });
1705
+ if (this.timeline.length > this.timelineCap) {
1706
+ this.timeline.shift();
1707
+ }
1708
+ }
1570
1709
  // ═══════════════════════════════════════════════════════════════════════
1571
1710
  // Private: Plan Extraction from SessionEvent
1572
1711
  // ═══════════════════════════════════════════════════════════════════════
@@ -2,11 +2,15 @@
2
2
  * Session snapshot sidecar — persists aggregator state + reader position
3
3
  * so that re-attaching to a session skips full replay.
4
4
  *
5
- * Snapshots are stored in ~/.config/sidekick/snapshots/{sessionId}.json.
6
- * They are invalidated when the source file changes beyond the snapshot position.
5
+ * Snapshots are stored in `<config dir>/snapshots/{sessionId}.json` (see
6
+ * `getConfigDir()`). They are invalidated when the source file shrank below
7
+ * the snapshot position or, for file-backed sessions that recorded a
8
+ * {@link SessionSnapshot.sourceCheckpoint}, when the file is no longer the
9
+ * same append-only file.
7
10
  *
8
11
  * @module aggregation/snapshot
9
12
  */
13
+ import { type JsonlTailCheckpoint } from '../watchers/jsonlTail';
10
14
  import { type SerializedAggregatorState } from './EventAggregator';
11
15
  /** Newest snapshots kept on disk; older ones are pruned opportunistically. */
12
16
  export declare const MAX_SNAPSHOT_FILES = 200;
@@ -22,6 +26,23 @@ export interface SessionSnapshot {
22
26
  readerPosition: number;
23
27
  /** Source file size at snapshot time (for staleness check; 0 for DB-backed). */
24
28
  sourceSize: number;
29
+ /**
30
+ * Identity of the source file at `readerPosition`, from
31
+ * `captureJsonlCheckpoint(sourcePath, readerPosition)`: its device and
32
+ * inode plus a digest of its first bytes. Present for file-backed sessions
33
+ * written by 0.29.1 and later; absent for DB-backed sessions and older
34
+ * snapshots. {@link isSnapshotValid} checks it when given the source path.
35
+ */
36
+ sourceCheckpoint?: JsonlTailCheckpoint;
37
+ /**
38
+ * The reader's parse state at `readerPosition`, from
39
+ * `SessionReader.getResumeState()` or `SessionWatcher.getResumeState()`
40
+ * (for Claude Code, which split-line responses were already counted).
41
+ * After seeking to `readerPosition`, pass it to `restoreResumeState()` and
42
+ * replay from the start when that returns false. Absent when the reader
43
+ * keeps none, and in snapshots written before 0.29.1.
44
+ */
45
+ readerState?: unknown;
25
46
  /** ISO timestamp when snapshot was written. */
26
47
  createdAt: string;
27
48
  /** Serialized EventAggregator state. */
@@ -55,8 +76,8 @@ export declare function pruneSnapshots(keep?: number): number;
55
76
  * - Schema version doesn't match
56
77
  * - JSON parse fails
57
78
  *
58
- * The caller is responsible for staleness checks (comparing sourceSize
59
- * and readerPosition against the actual file).
79
+ * The caller is responsible for staleness checks against the actual file:
80
+ * `isSnapshotValid(snapshot, currentSize, sourcePath)`.
60
81
  */
61
82
  export declare function loadSnapshot(sessionId: string): SessionSnapshot | null;
62
83
  /**
@@ -66,10 +87,18 @@ export declare function deleteSnapshot(sessionId: string): void;
66
87
  /**
67
88
  * Validates whether a snapshot is still usable for a given source file.
68
89
  *
69
- * For JSONL providers: checks that the file hasn't been truncated
70
- * (current size >= snapshot position) and that the size matches or has grown.
71
- *
72
90
  * For DB-backed providers: sourceSize is 0, so we always consider valid
73
91
  * (the DB reader's timestamp-based cursor handles staleness naturally).
92
+ *
93
+ * For JSONL providers: the file must not have shrunk below the snapshot
94
+ * position. With `sourcePath` the check is also content-aware: the snapshot
95
+ * must carry a {@link SessionSnapshot.sourceCheckpoint} for exactly
96
+ * `readerPosition`, and that checkpoint must still match the file at
97
+ * `sourcePath` (same device and inode, same first bytes, position just after
98
+ * a newline), so a file replaced by another one or rewritten to the same or
99
+ * a larger size is not resumed mid-content. A snapshot without a checkpoint
100
+ * (written before 0.29.1) is invalid when `sourcePath` is given, which costs
101
+ * one full replay. Without `sourcePath` only the size check applies, as it
102
+ * did before.
74
103
  */
75
- export declare function isSnapshotValid(snapshot: SessionSnapshot, currentSourceSize: number): boolean;
104
+ export declare function isSnapshotValid(snapshot: SessionSnapshot, currentSourceSize: number, sourcePath?: string): boolean;
@@ -3,8 +3,11 @@
3
3
  * Session snapshot sidecar — persists aggregator state + reader position
4
4
  * so that re-attaching to a session skips full replay.
5
5
  *
6
- * Snapshots are stored in ~/.config/sidekick/snapshots/{sessionId}.json.
7
- * They are invalidated when the source file changes beyond the snapshot position.
6
+ * Snapshots are stored in `<config dir>/snapshots/{sessionId}.json` (see
7
+ * `getConfigDir()`). They are invalidated when the source file shrank below
8
+ * the snapshot position or, for file-backed sessions that recorded a
9
+ * {@link SessionSnapshot.sourceCheckpoint}, when the file is no longer the
10
+ * same append-only file.
8
11
  *
9
12
  * @module aggregation/snapshot
10
13
  */
@@ -52,6 +55,7 @@ exports.isSnapshotValid = isSnapshotValid;
52
55
  const fs = __importStar(require("fs"));
53
56
  const path = __importStar(require("path"));
54
57
  const paths_1 = require("../paths");
58
+ const jsonlTail_1 = require("../watchers/jsonlTail");
55
59
  const atomic_1 = require("../writers/atomic");
56
60
  const EventAggregator_1 = require("./EventAggregator");
57
61
  /** Current schema version — bump when format changes. */
@@ -143,8 +147,8 @@ function pruneSnapshots(keep = exports.MAX_SNAPSHOT_FILES) {
143
147
  * - Schema version doesn't match
144
148
  * - JSON parse fails
145
149
  *
146
- * The caller is responsible for staleness checks (comparing sourceSize
147
- * and readerPosition against the actual file).
150
+ * The caller is responsible for staleness checks against the actual file:
151
+ * `isSnapshotValid(snapshot, currentSize, sourcePath)`.
148
152
  */
149
153
  function loadSnapshot(sessionId) {
150
154
  try {
@@ -184,13 +188,21 @@ function deleteSnapshot(sessionId) {
184
188
  /**
185
189
  * Validates whether a snapshot is still usable for a given source file.
186
190
  *
187
- * For JSONL providers: checks that the file hasn't been truncated
188
- * (current size >= snapshot position) and that the size matches or has grown.
189
- *
190
191
  * For DB-backed providers: sourceSize is 0, so we always consider valid
191
192
  * (the DB reader's timestamp-based cursor handles staleness naturally).
193
+ *
194
+ * For JSONL providers: the file must not have shrunk below the snapshot
195
+ * position. With `sourcePath` the check is also content-aware: the snapshot
196
+ * must carry a {@link SessionSnapshot.sourceCheckpoint} for exactly
197
+ * `readerPosition`, and that checkpoint must still match the file at
198
+ * `sourcePath` (same device and inode, same first bytes, position just after
199
+ * a newline), so a file replaced by another one or rewritten to the same or
200
+ * a larger size is not resumed mid-content. A snapshot without a checkpoint
201
+ * (written before 0.29.1) is invalid when `sourcePath` is given, which costs
202
+ * one full replay. Without `sourcePath` only the size check applies, as it
203
+ * did before.
192
204
  */
193
- function isSnapshotValid(snapshot, currentSourceSize) {
205
+ function isSnapshotValid(snapshot, currentSourceSize, sourcePath) {
194
206
  // DB-backed providers store sourceSize=0 — always valid (cursor is a timestamp)
195
207
  if (snapshot.sourceSize === 0) {
196
208
  return true;
@@ -199,5 +211,15 @@ function isSnapshotValid(snapshot, currentSourceSize) {
199
211
  if (currentSourceSize < snapshot.readerPosition) {
200
212
  return false;
201
213
  }
202
- return true;
214
+ if (typeof sourcePath !== 'string') {
215
+ return true;
216
+ }
217
+ // Snapshot content comes from disk: guard the shape before trusting it.
218
+ const checkpoint = snapshot.sourceCheckpoint;
219
+ if (!checkpoint ||
220
+ typeof checkpoint !== 'object' ||
221
+ checkpoint.offset !== snapshot.readerPosition) {
222
+ return false;
223
+ }
224
+ return (0, jsonlTail_1.verifyJsonlCheckpoint)(sourcePath, checkpoint);
203
225
  }
@@ -117,15 +117,28 @@ export interface BurnRateInfo {
117
117
  /** Number of samples in the window. */
118
118
  sampleCount: number;
119
119
  }
120
+ /** How a subagent ended: a failed spawn, or a background agent's notification status. */
121
+ export type SubagentOutcome = 'completed' | 'failed' | 'killed' | 'stopped';
120
122
  /** Subagent lifecycle info (for tracking spawn/completion from tool events). */
121
123
  export interface SubagentLifecycle {
124
+ /** The spawning tool_use id (`Agent` / `Task`). */
122
125
  id: string;
123
126
  description: string;
124
127
  subagentType: string;
125
128
  spawnTime: string;
126
129
  completionTime?: string;
130
+ /**
131
+ * A background agent stays `running` after its spawn's immediate result
132
+ * until its task notification arrives.
133
+ */
127
134
  status: 'running' | 'completed';
128
135
  durationMs?: number;
136
+ /** Claude Code's agent id (the `agent-<id>.jsonl` transcript name), once known. */
137
+ agentId?: string;
138
+ /** The agent was launched in the background (its spawn returned at once). */
139
+ background?: boolean;
140
+ /** How the agent ended, once `status` is `completed`. */
141
+ outcome?: SubagentOutcome;
129
142
  }
130
143
  /** Frequency entry for tool/word frequency tracking. */
131
144
  export interface FrequencyMetric {
package/dist/browser.d.ts CHANGED
@@ -14,6 +14,7 @@ export type { UsageProvider, UsageSemantics, UsageNormalizationProvenance, Provi
14
14
  export { formatTokenBreakdown, summarizeTokens, sumTokenTotals, TOKEN_CONTEXT_LABEL, TOKEN_MAIN_THREAD_LABEL, TOKEN_SESSION_TOTAL_LABEL, TOKEN_SUBAGENTS_LABEL, TOKEN_TOTAL_LABEL, } from './tokenSummary';
15
15
  export type { TokenSummary, TokenTotalsLike } from './tokenSummary';
16
16
  export { ClaudeUsageDeduper, SYNTHETIC_MODEL } from './usage/claudeUsageDedupe';
17
+ export type { ClaudeUsageDedupeBuckets, ClaudeUsageDedupeState } from './usage/claudeUsageDedupe';
17
18
  export { ASCII_CHARS_PER_TOKEN, estimateTextTokens, estimateSerializedTokens, } from './tokenEstimation';
18
19
  export type { ExactTokenCounterContext, ExactTokenCounter, TokenEstimationMethod, TokenEstimationConfidence, TokenEstimationOptions, TokenEstimate, } from './tokenEstimation';
19
20
  export { projectSessionTranscript } from './transcript';
@@ -28,6 +29,9 @@ export type { AccountProviderId, ModelProviderId, QuotaProviderId, RuntimeQuotaP
28
29
  /** Same type as `ProviderId` from the package root and `sidekick-shared/node`. */
29
30
  SessionProviderId as ProviderId, } from './providerIds';
30
31
  export type { BuildSessionContextSnapshotOptions, SessionContextCapabilities, SessionContextLayerBreakdown, SessionContextPressure, SessionContextProjector, SessionContextSnapshot, SessionContextSource, SessionContextSourceType, } from './context/sessionContext';
32
+ export { SUBAGENT_SPAWN_TOOL_NAMES, isSubagentSpawnTool, isTaskNotificationSystemEvent, parseTaskNotification, taskNotificationFromEvent, } from './subagentSpawn';
33
+ export type { TaskNotification } from './subagentSpawn';
34
+ export type { SubagentOutcome } from './aggregation/types';
31
35
  export { assistantTurnEventsFromSessionEvents, extractTurnSubagents, isAssistantTurnSubagentTool, reasoningSummary, segmentAssistantTurn, } from './turns/assistantTurn';
32
36
  export type { AssistantTurnEvent, AssistantTurnEventType, AssistantTurnProcess, AssistantTurnProcessStep, AssistantTurnProjection, AssistantTurnSubagent, AssistantTurnSubagentStatus, AssistantTurnTimelineItem, AssistantTurnToolRef, ExtractTurnSubagentsOptions, ReasoningSummary, SegmentAssistantTurnOptions, } from './turns/assistantTurn';
33
37
  export { parseModelId, getModelPricing, resolveModelPricing, getModelInfo, calculateCost, calculateCostWithPricing, calculateCostWithProvenance, mergeCostSources, shortModelName, getModelDisplayInfo, compareModelIds, sortModelIds, formatCost, canonicalModelId, isMovingModelAlias, registerModelPricing, clearRegisteredModelPricing, getPricingCatalogInfo, costSourceFromPricingProvenance, mergeCostComponents, } from './modelInfo';
@@ -51,7 +55,7 @@ export type { SessionEvent, ClaudeSessionEvent, MessageUsage, SessionMessage, Se
51
55
  export type { CodexRolloutLine, CodexSessionMeta, CodexResponseItem, CodexMessageItem, CodexContentPart, CodexReasoningItem, CodexFunctionCallItem, CodexFunctionCallOutputItem, CodexLocalShellCallItem, CodexWebSearchCallItem, CodexCustomToolCallItem, CodexCustomToolCallOutputItem, CodexCompacted, CodexTurnContext, CodexEventMsg, CodexEvent, CodexTurnStartedEvent, CodexTurnCompleteEvent, CodexTaskStartedEvent, CodexTaskCompleteEvent, CodexTurnAbortedEvent, CodexTokenCountEvent, CodexTokenUsage, CodexRateLimits, CodexAgentMessageEvent, CodexAgentReasoningEvent, CodexUserMessageEvent, CodexExecCommandBeginEvent, CodexExecCommandEndEvent, CodexMcpToolCallBeginEvent, CodexMcpToolCallEndEvent, CodexErrorEvent, CodexContextCompactedEvent, CodexPatchAppliedEvent, CodexBackgroundEvent, CodexGenericEvent, CodexDbThread, CodexReasoningSummary, } from './types/codex';
52
56
  export { isAggregateCodexLimit } from './types/codex';
53
57
  export { CodexRolloutParser, DEFAULT_MAX_TRACKED_TOOL_CALLS, extractPatchFilePaths, normalizeCodexToolName, normalizeCodexToolInput, resolveCodexCallUsage, } from './parsers/codexParser';
54
- export type { CodexPendingStats, CodexRolloutParserOptions } from './parsers/codexParser';
58
+ export type { CodexPendingStats, CodexRolloutParserOptions, CodexRolloutResumeState, } from './parsers/codexParser';
55
59
  export { codexRolloutOrigin, codexSpawnParentFromSource } from './parsers/codexRolloutOrigin';
56
60
  export type { CodexRolloutOrigin } from './parsers/codexRolloutOrigin';
57
61
  export { HUMAN_CLAUDE_ENTRYPOINTS, HUMAN_CODEX_SOURCES, humanPromptText, isHumanPrompt, } from './humanPrompt';
package/dist/browser.js CHANGED
@@ -9,9 +9,9 @@
9
9
  * For pricing hydration (LiteLLM catalog refresh), use `sidekick-shared/node`.
10
10
  */
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.compareModelIds = exports.getModelDisplayInfo = exports.shortModelName = exports.mergeCostSources = exports.calculateCostWithProvenance = exports.calculateCostWithPricing = exports.calculateCost = exports.getModelInfo = exports.resolveModelPricing = exports.getModelPricing = exports.parseModelId = exports.segmentAssistantTurn = exports.reasoningSummary = exports.isAssistantTurnSubagentTool = exports.extractTurnSubagents = exports.assistantTurnEventsFromSessionEvents = exports.SESSION_PROVIDER_IDS = exports.RUNTIME_QUOTA_PROVIDER_IDS = exports.QUOTA_PROVIDER_IDS = exports.MODEL_PROVIDER_IDS = exports.ACCOUNT_PROVIDER_IDS = exports.createSessionContextProjector = exports.calculateSessionContextPressure = exports.buildSessionContextSnapshot = exports.parseMcpToolName = exports.parseLocalDateKey = exports.formatTokenCount = exports.formatLocalDateKey = exports.formatDurationMs = exports.addLocalDays = exports.projectSessionTranscript = exports.estimateSerializedTokens = exports.estimateTextTokens = exports.ASCII_CHARS_PER_TOKEN = exports.SYNTHETIC_MODEL = exports.ClaudeUsageDeduper = exports.TOKEN_TOTAL_LABEL = exports.TOKEN_SUBAGENTS_LABEL = exports.TOKEN_SESSION_TOTAL_LABEL = exports.TOKEN_MAIN_THREAD_LABEL = exports.TOKEN_CONTEXT_LABEL = exports.sumTokenTotals = exports.summarizeTokens = exports.formatTokenBreakdown = exports.calculateNormalizedUsageCost = exports.extractNormalizedUsage = exports.normalizeProviderUsage = exports.DEFAULT_CONTEXT_WINDOW = exports.resolveModelContextWindow = exports.getModelContextWindowSize = void 0;
13
- exports.HUMAN_CLAUDE_ENTRYPOINTS = exports.codexSpawnParentFromSource = exports.codexRolloutOrigin = exports.resolveCodexCallUsage = exports.normalizeCodexToolInput = exports.normalizeCodexToolName = exports.extractPatchFilePaths = exports.DEFAULT_MAX_TRACKED_TOOL_CALLS = exports.CodexRolloutParser = exports.isAggregateCodexLimit = exports.parseCodexLoginStatus = exports.parseClaudeAuthStatus = exports.decodeCodexErrorInfo = exports.CODEX_ERROR_INFO_KINDS = exports.PROVIDER_RECOVERY_ACTIONS = exports.PROVIDER_FAILURE_EVIDENCE_SOURCES = exports.PROVIDER_FAILURE_CODES = exports.PROVIDER_CREDENTIAL_KINDS = exports.PROVIDER_AUTHENTICATION_STATES = exports.PROVIDER_AUTHENTICATION_SOURCES = exports.diagnoseProviderFailure = exports.mergeFailingToolWindows = exports.failingToolTrend = exports.FAILING_TOOL_TREND_ARROWS = exports.sessionSummaryFromStats = exports.removeSessionSummary = exports.markFileImported = exports.isFileImported = exports.applySessionSummary = exports.weekKey = exports.usageBucketKey = exports.summarizeUsageRows = exports.bucketUsage = exports.findActiveBillingBlock = exports.computeBillingBlocks = exports.BILLING_BLOCK_DURATION_MS = exports.RESOLVED_MODEL_CATALOG_SCHEMA_VERSION = exports.resolveModelAlias = exports.registerModelAlias = exports.importResolvedModelCatalog = exports.exportResolvedModelCatalog = exports.mergeCostComponents = exports.costSourceFromPricingProvenance = exports.getPricingCatalogInfo = exports.clearRegisteredModelPricing = exports.registerModelPricing = exports.isMovingModelAlias = exports.canonicalModelId = exports.formatCost = exports.sortModelIds = void 0;
14
- exports.quotaWindowShortLabel = exports.quotaWindowLabel = exports.quotaWindowHasData = exports.quotaWindowApplies = exports.formatExtraUsage = exports.extraQuotaRows = exports.claudePlanLabel = exports.TOOL_CATEGORIES = exports.classifyTool = exports.renderHandoffUrlTemplate = exports.isHumanPrompt = exports.humanPromptText = exports.HUMAN_CODEX_SOURCES = void 0;
12
+ exports.calculateCostWithPricing = exports.calculateCost = exports.getModelInfo = exports.resolveModelPricing = exports.getModelPricing = exports.parseModelId = exports.segmentAssistantTurn = exports.reasoningSummary = exports.isAssistantTurnSubagentTool = exports.extractTurnSubagents = exports.assistantTurnEventsFromSessionEvents = exports.taskNotificationFromEvent = exports.parseTaskNotification = exports.isTaskNotificationSystemEvent = exports.isSubagentSpawnTool = exports.SUBAGENT_SPAWN_TOOL_NAMES = exports.SESSION_PROVIDER_IDS = exports.RUNTIME_QUOTA_PROVIDER_IDS = exports.QUOTA_PROVIDER_IDS = exports.MODEL_PROVIDER_IDS = exports.ACCOUNT_PROVIDER_IDS = exports.createSessionContextProjector = exports.calculateSessionContextPressure = exports.buildSessionContextSnapshot = exports.parseMcpToolName = exports.parseLocalDateKey = exports.formatTokenCount = exports.formatLocalDateKey = exports.formatDurationMs = exports.addLocalDays = exports.projectSessionTranscript = exports.estimateSerializedTokens = exports.estimateTextTokens = exports.ASCII_CHARS_PER_TOKEN = exports.SYNTHETIC_MODEL = exports.ClaudeUsageDeduper = exports.TOKEN_TOTAL_LABEL = exports.TOKEN_SUBAGENTS_LABEL = exports.TOKEN_SESSION_TOTAL_LABEL = exports.TOKEN_MAIN_THREAD_LABEL = exports.TOKEN_CONTEXT_LABEL = exports.sumTokenTotals = exports.summarizeTokens = exports.formatTokenBreakdown = exports.calculateNormalizedUsageCost = exports.extractNormalizedUsage = exports.normalizeProviderUsage = exports.DEFAULT_CONTEXT_WINDOW = exports.resolveModelContextWindow = exports.getModelContextWindowSize = void 0;
13
+ exports.normalizeCodexToolName = exports.extractPatchFilePaths = exports.DEFAULT_MAX_TRACKED_TOOL_CALLS = exports.CodexRolloutParser = exports.isAggregateCodexLimit = exports.parseCodexLoginStatus = exports.parseClaudeAuthStatus = exports.decodeCodexErrorInfo = exports.CODEX_ERROR_INFO_KINDS = exports.PROVIDER_RECOVERY_ACTIONS = exports.PROVIDER_FAILURE_EVIDENCE_SOURCES = exports.PROVIDER_FAILURE_CODES = exports.PROVIDER_CREDENTIAL_KINDS = exports.PROVIDER_AUTHENTICATION_STATES = exports.PROVIDER_AUTHENTICATION_SOURCES = exports.diagnoseProviderFailure = exports.mergeFailingToolWindows = exports.failingToolTrend = exports.FAILING_TOOL_TREND_ARROWS = exports.sessionSummaryFromStats = exports.removeSessionSummary = exports.markFileImported = exports.isFileImported = exports.applySessionSummary = exports.weekKey = exports.usageBucketKey = exports.summarizeUsageRows = exports.bucketUsage = exports.findActiveBillingBlock = exports.computeBillingBlocks = exports.BILLING_BLOCK_DURATION_MS = exports.RESOLVED_MODEL_CATALOG_SCHEMA_VERSION = exports.resolveModelAlias = exports.registerModelAlias = exports.importResolvedModelCatalog = exports.exportResolvedModelCatalog = exports.mergeCostComponents = exports.costSourceFromPricingProvenance = exports.getPricingCatalogInfo = exports.clearRegisteredModelPricing = exports.registerModelPricing = exports.isMovingModelAlias = exports.canonicalModelId = exports.formatCost = exports.sortModelIds = exports.compareModelIds = exports.getModelDisplayInfo = exports.shortModelName = exports.mergeCostSources = exports.calculateCostWithProvenance = void 0;
14
+ exports.quotaWindowShortLabel = exports.quotaWindowLabel = exports.quotaWindowHasData = exports.quotaWindowApplies = exports.formatExtraUsage = exports.extraQuotaRows = exports.claudePlanLabel = exports.TOOL_CATEGORIES = exports.classifyTool = exports.renderHandoffUrlTemplate = exports.isHumanPrompt = exports.humanPromptText = exports.HUMAN_CODEX_SOURCES = exports.HUMAN_CLAUDE_ENTRYPOINTS = exports.codexSpawnParentFromSource = exports.codexRolloutOrigin = exports.resolveCodexCallUsage = exports.normalizeCodexToolInput = void 0;
15
15
  var modelContext_1 = require("./modelContext");
16
16
  Object.defineProperty(exports, "getModelContextWindowSize", { enumerable: true, get: function () { return modelContext_1.getModelContextWindowSize; } });
17
17
  Object.defineProperty(exports, "resolveModelContextWindow", { enumerable: true, get: function () { return modelContext_1.resolveModelContextWindow; } });
@@ -56,6 +56,12 @@ Object.defineProperty(exports, "MODEL_PROVIDER_IDS", { enumerable: true, get: fu
56
56
  Object.defineProperty(exports, "QUOTA_PROVIDER_IDS", { enumerable: true, get: function () { return providerIds_1.QUOTA_PROVIDER_IDS; } });
57
57
  Object.defineProperty(exports, "RUNTIME_QUOTA_PROVIDER_IDS", { enumerable: true, get: function () { return providerIds_1.RUNTIME_QUOTA_PROVIDER_IDS; } });
58
58
  Object.defineProperty(exports, "SESSION_PROVIDER_IDS", { enumerable: true, get: function () { return providerIds_1.SESSION_PROVIDER_IDS; } });
59
+ var subagentSpawn_1 = require("./subagentSpawn");
60
+ Object.defineProperty(exports, "SUBAGENT_SPAWN_TOOL_NAMES", { enumerable: true, get: function () { return subagentSpawn_1.SUBAGENT_SPAWN_TOOL_NAMES; } });
61
+ Object.defineProperty(exports, "isSubagentSpawnTool", { enumerable: true, get: function () { return subagentSpawn_1.isSubagentSpawnTool; } });
62
+ Object.defineProperty(exports, "isTaskNotificationSystemEvent", { enumerable: true, get: function () { return subagentSpawn_1.isTaskNotificationSystemEvent; } });
63
+ Object.defineProperty(exports, "parseTaskNotification", { enumerable: true, get: function () { return subagentSpawn_1.parseTaskNotification; } });
64
+ Object.defineProperty(exports, "taskNotificationFromEvent", { enumerable: true, get: function () { return subagentSpawn_1.taskNotificationFromEvent; } });
59
65
  var assistantTurn_1 = require("./turns/assistantTurn");
60
66
  Object.defineProperty(exports, "assistantTurnEventsFromSessionEvents", { enumerable: true, get: function () { return assistantTurn_1.assistantTurnEventsFromSessionEvents; } });
61
67
  Object.defineProperty(exports, "extractTurnSubagents", { enumerable: true, get: function () { return assistantTurn_1.extractTurnSubagents; } });
@@ -14,6 +14,7 @@ const EventAggregator_1 = require("../aggregation/EventAggregator");
14
14
  const modelContext_1 = require("../modelContext");
15
15
  const mcpToolName_1 = require("../parsers/mcpToolName");
16
16
  const tokenEstimation_1 = require("../tokenEstimation");
17
+ const subagentSpawn_1 = require("../subagentSpawn");
17
18
  const DEFAULT_CONTEXT_SOURCE_LIMIT = 200;
18
19
  const DEFAULT_SNIPPET_MAX_CHARS = 240;
19
20
  const DEFAULT_BODY_MAX_CHARS = 5000;
@@ -211,6 +212,9 @@ function extractSourcesFromEvent(event, state) {
211
212
  }
212
213
  }
213
214
  function extractSystemSource(event, state) {
215
+ // Background-task notices only update subagent lifecycles (like the aggregator).
216
+ if ((0, subagentSpawn_1.isTaskNotificationSystemEvent)(event))
217
+ return;
214
218
  const message = event.message;
215
219
  const text = extractText(message?.content);
216
220
  const label = message?.sourceLabel ?? message?.role ?? 'system';