sidekick-shared 0.28.3 → 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 (151) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +188 -27
  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/authStatus.d.ts +54 -0
  9. package/dist/authStatus.js +91 -0
  10. package/dist/browser.d.ts +16 -6
  11. package/dist/browser.js +24 -2
  12. package/dist/codex.d.ts +23 -0
  13. package/dist/codex.js +44 -0
  14. package/dist/codexProfiles.d.ts +1 -0
  15. package/dist/codexProfiles.js +8 -11
  16. package/dist/context/sessionContext.js +4 -0
  17. package/dist/esm/aggregation/EventAggregator.js +174 -35
  18. package/dist/esm/aggregation/snapshot.js +31 -9
  19. package/dist/esm/authStatus.js +87 -0
  20. package/dist/esm/browser.js +5 -2
  21. package/dist/esm/codex.js +18 -0
  22. package/dist/esm/codexProfiles.js +8 -11
  23. package/dist/esm/context/sessionContext.js +4 -0
  24. package/dist/esm/formatters/toolSummary.js +4 -1
  25. package/dist/esm/formatting.js +10 -1
  26. package/dist/esm/index.js +11 -6
  27. package/dist/esm/modelCatalog.js +7 -5
  28. package/dist/esm/modelContext.js +4 -3
  29. package/dist/esm/modelInfo.js +425 -47
  30. package/dist/esm/node.js +9 -3
  31. package/dist/esm/observedContextWindows.js +2 -1
  32. package/dist/esm/ownKey.js +26 -0
  33. package/dist/esm/parsers/codexParser.js +351 -54
  34. package/dist/esm/parsers/codexRolloutOrigin.js +107 -0
  35. package/dist/esm/parsers/openCodeParser.js +85 -22
  36. package/dist/esm/parsers/subagentScanner.js +33 -12
  37. package/dist/esm/parsers/subagentTraceParser.js +97 -37
  38. package/dist/esm/pricing.js +18 -0
  39. package/dist/esm/pricingCatalog.js +17 -9
  40. package/dist/esm/promptHistory.js +66 -4
  41. package/dist/esm/providerFailure.js +124 -3
  42. package/dist/esm/providers/claudeCode.js +10 -2
  43. package/dist/esm/providers/codex.js +84 -62
  44. package/dist/esm/providers/codexDatabase.js +3 -19
  45. package/dist/esm/providers/codexRollouts.js +24 -1
  46. package/dist/esm/providers/openCode.js +9 -17
  47. package/dist/esm/report/htmlReportGenerator.js +42 -5
  48. package/dist/esm/report/transcriptParser.js +13 -11
  49. package/dist/esm/schemas/providerFailure.js +3 -1
  50. package/dist/esm/schemas/sessionEvent.js +35 -14
  51. package/dist/esm/schemas/transcript.js +4 -0
  52. package/dist/esm/sessionPromptHistory.js +2 -2
  53. package/dist/esm/statusline/payload.js +9 -0
  54. package/dist/esm/subagentSpawn.js +163 -0
  55. package/dist/esm/testing/credentialGuard.js +87 -5
  56. package/dist/esm/toolCallOrigins.js +310 -0
  57. package/dist/esm/transcript.js +46 -3
  58. package/dist/esm/turns/assistantTurn.js +3 -2
  59. package/dist/esm/usage/claudeUsageDedupe.js +68 -0
  60. package/dist/esm/usage/usageEvents.js +146 -17
  61. package/dist/esm/usage/usageReports.js +39 -6
  62. package/dist/esm/watchers/eventBridge.js +15 -0
  63. package/dist/esm/watchers/factory.js +5 -3
  64. package/dist/esm/watchers/jsonlTail.js +442 -58
  65. package/dist/esm/watchers/jsonlWatcher.js +51 -4
  66. package/dist/esm/watchers/providerReaderWatcher.js +13 -0
  67. package/dist/esm/zaiQuota.js +2 -1
  68. package/dist/formatters/toolSummary.js +4 -1
  69. package/dist/formatting.d.ts +26 -1
  70. package/dist/formatting.js +11 -1
  71. package/dist/index.d.ts +22 -11
  72. package/dist/index.js +44 -12
  73. package/dist/modelCatalog.js +7 -5
  74. package/dist/modelContext.js +4 -3
  75. package/dist/modelInfo.d.ts +185 -16
  76. package/dist/modelInfo.js +434 -49
  77. package/dist/node.d.ts +16 -5
  78. package/dist/node.js +33 -2
  79. package/dist/observedContextWindows.js +2 -1
  80. package/dist/ownKey.d.ts +16 -0
  81. package/dist/ownKey.js +30 -0
  82. package/dist/parsers/codexParser.d.ts +118 -0
  83. package/dist/parsers/codexParser.js +352 -54
  84. package/dist/parsers/codexRolloutOrigin.d.ts +49 -0
  85. package/dist/parsers/codexRolloutOrigin.js +111 -0
  86. package/dist/parsers/openCodeParser.d.ts +38 -0
  87. package/dist/parsers/openCodeParser.js +88 -22
  88. package/dist/parsers/subagentScanner.d.ts +19 -6
  89. package/dist/parsers/subagentScanner.js +33 -12
  90. package/dist/parsers/subagentTraceParser.d.ts +25 -5
  91. package/dist/parsers/subagentTraceParser.js +97 -37
  92. package/dist/pricing.d.ts +22 -0
  93. package/dist/pricing.js +50 -0
  94. package/dist/pricingCatalog.d.ts +10 -0
  95. package/dist/pricingCatalog.js +16 -8
  96. package/dist/promptHistory.d.ts +65 -4
  97. package/dist/promptHistory.js +68 -5
  98. package/dist/providerFailure.d.ts +44 -0
  99. package/dist/providerFailure.js +126 -4
  100. package/dist/providers/claudeCode.js +9 -1
  101. package/dist/providers/codex.d.ts +3 -22
  102. package/dist/providers/codex.js +85 -64
  103. package/dist/providers/codexDatabase.d.ts +3 -2
  104. package/dist/providers/codexDatabase.js +4 -22
  105. package/dist/providers/codexRollouts.d.ts +22 -0
  106. package/dist/providers/codexRollouts.js +24 -0
  107. package/dist/providers/openCode.js +8 -16
  108. package/dist/providers/types.d.ts +14 -0
  109. package/dist/report/htmlReportGenerator.js +42 -5
  110. package/dist/report/transcriptParser.d.ts +10 -1
  111. package/dist/report/transcriptParser.js +13 -11
  112. package/dist/schemas/index.d.ts +1 -0
  113. package/dist/schemas/providerFailure.d.ts +39 -0
  114. package/dist/schemas/providerFailure.js +2 -0
  115. package/dist/schemas/sessionEvent.d.ts +27 -10
  116. package/dist/schemas/sessionEvent.js +35 -14
  117. package/dist/schemas/transcript.d.ts +2 -0
  118. package/dist/schemas/transcript.js +4 -0
  119. package/dist/sessionPromptHistory.d.ts +7 -1
  120. package/dist/sessionPromptHistory.js +1 -1
  121. package/dist/statusline/payload.d.ts +10 -0
  122. package/dist/statusline/payload.js +14 -0
  123. package/dist/subagentSpawn.d.ts +80 -0
  124. package/dist/subagentSpawn.js +174 -0
  125. package/dist/testing/credentialGuard.d.ts +14 -0
  126. package/dist/testing/credentialGuard.js +87 -5
  127. package/dist/toolCallOrigins.d.ts +70 -0
  128. package/dist/toolCallOrigins.js +346 -0
  129. package/dist/transcript.d.ts +12 -0
  130. package/dist/transcript.js +46 -3
  131. package/dist/turns/assistantTurn.d.ts +1 -0
  132. package/dist/turns/assistantTurn.js +3 -2
  133. package/dist/types/opencode.d.ts +2 -0
  134. package/dist/usage/claudeUsageDedupe.d.ts +31 -0
  135. package/dist/usage/claudeUsageDedupe.js +68 -0
  136. package/dist/usage/usageEvents.d.ts +54 -6
  137. package/dist/usage/usageEvents.js +146 -17
  138. package/dist/usage/usageReports.d.ts +35 -4
  139. package/dist/usage/usageReports.js +39 -6
  140. package/dist/watchers/eventBridge.js +15 -0
  141. package/dist/watchers/factory.d.ts +6 -0
  142. package/dist/watchers/factory.js +5 -3
  143. package/dist/watchers/jsonlTail.d.ts +124 -4
  144. package/dist/watchers/jsonlTail.js +446 -59
  145. package/dist/watchers/jsonlWatcher.d.ts +18 -1
  146. package/dist/watchers/jsonlWatcher.js +50 -3
  147. package/dist/watchers/providerReaderWatcher.d.ts +7 -0
  148. package/dist/watchers/providerReaderWatcher.js +13 -0
  149. package/dist/watchers/types.d.ts +12 -0
  150. package/dist/zaiQuota.js +2 -1
  151. package/package.json +41 -1
@@ -18,6 +18,7 @@ import { HeatmapTracker } from './HeatmapTracker';
18
18
  import { PatternExtractor } from './PatternExtractor';
19
19
  import { categorizeError } from '../extractors/errorTaxonomy';
20
20
  import { estimateTextTokens } from '../tokenEstimation';
21
+ import { isSubagentSpawnTool, isTaskNotificationSystemEvent, parseAsyncAgentLaunch, parseTaskNotification, taskNotificationFromEvent, taskNotificationOutcome, taskNotificationPromptFromAttachment, } from '../subagentSpawn';
21
22
  import { calculateNormalizedUsageCost, extractNormalizedUsage, normalizeProviderUsage, } from '../usageNormalization';
22
23
  // ── Defaults ──
23
24
  const DEFAULT_TIMELINE_CAP = 200;
@@ -30,6 +31,22 @@ const COMPACTION_DROP_THRESHOLD = 0.8; // >20% drop
30
31
  export const SNAPSHOT_SCHEMA_VERSION = 6;
31
32
  // ── Goal gate detection regex ──
32
33
  const GOAL_GATE_REGEX = /\b(CRITICAL|MUST|blocker|required|must.?complete|goal.?gate|essential|do.?not.?skip|blocking)\b/i;
34
+ // ── Task notifications delivered as attachments ──
35
+ /**
36
+ * The notification a `system` FollowEvent carries when it stands for a
37
+ * mid-turn task-notification attachment (its `raw` is the attachment line or
38
+ * the SessionEvent `extractSessionEvents` made from it).
39
+ */
40
+ function taskNotificationFromAttachmentFollowEvent(event) {
41
+ const raw = event.raw;
42
+ if (!raw || typeof raw !== 'object')
43
+ return null;
44
+ const prompt = taskNotificationPromptFromAttachment(raw);
45
+ if (prompt !== null)
46
+ return parseTaskNotification(prompt);
47
+ const asEvent = raw;
48
+ return isTaskNotificationSystemEvent(asEvent) ? taskNotificationFromEvent(asEvent) : null;
49
+ }
33
50
  // ── Todo dependency parsing ──
34
51
  /**
35
52
  * Parses dependency references from a todo item's content text.
@@ -168,6 +185,17 @@ export class EventAggregator {
168
185
  // processEvent — main entry for SessionEvent
169
186
  // ═══════════════════════════════════════════════════════════════════════
170
187
  processEvent(event) {
188
+ // Opt-in mid-turn task notifications (`extractSessionEvents` with
189
+ // `includeTaskNotifications`) only finish background subagents and mark
190
+ // the timeline: they are not messages, prompts, context, or activity.
191
+ if (isTaskNotificationSystemEvent(event)) {
192
+ const notification = taskNotificationFromEvent(event);
193
+ if (notification) {
194
+ this.applyTaskNotification(notification, event.timestamp);
195
+ this.addTaskNotificationTimeline(event.timestamp, notification);
196
+ }
197
+ return;
198
+ }
171
199
  // 1. Increment counters, capture timestamps
172
200
  this.eventCount++;
173
201
  if (!this.sessionStartTime) {
@@ -233,6 +261,15 @@ export class EventAggregator {
233
261
  // processFollowEvent — adapter for CLI FollowEvent format
234
262
  // ═══════════════════════════════════════════════════════════════════════
235
263
  processFollowEvent(event) {
264
+ // A mid-turn task-notification attachment, like its SessionEvent form above.
265
+ if (event.type === 'system') {
266
+ const notification = taskNotificationFromAttachmentFollowEvent(event);
267
+ if (notification) {
268
+ this.applyTaskNotification(notification, event.timestamp);
269
+ this.addTaskNotificationTimeline(event.timestamp, notification);
270
+ return;
271
+ }
272
+ }
236
273
  // 1. Increment counters, capture timestamps
237
274
  this.eventCount++;
238
275
  if (!this.sessionStartTime) {
@@ -626,6 +663,14 @@ export class EventAggregator {
626
663
  this.pendingToolCalls.clear();
627
664
  this.pendingTaskCreates.clear();
628
665
  this.pendingSubagents.clear();
666
+ // A spawn still waiting for its tool_result is resolved by that result,
667
+ // which can land after the snapshot boundary. Background agents need no
668
+ // entry: their notifications are matched against every subagent.
669
+ this.subagents.forEach((agent, idx) => {
670
+ if (agent.status === 'running' && !agent.background && agent.id) {
671
+ this.pendingSubagents.set(agent.id, idx);
672
+ }
673
+ });
629
674
  this.pendingUserRequest = null;
630
675
  this.planExtractor.reset();
631
676
  return true;
@@ -1104,7 +1149,7 @@ export class EventAggregator {
1104
1149
  else if (name === 'UpdatePlan') {
1105
1150
  this.handleUpdatePlan(input, new Date(event.timestamp));
1106
1151
  }
1107
- else if ((name === 'Agent' || name === 'Task') && toolUseId) {
1152
+ else if (isSubagentSpawnTool(name) && toolUseId) {
1108
1153
  this.handleAgentSpawn(toolUseId, input, new Date(event.timestamp));
1109
1154
  }
1110
1155
  }
@@ -1114,7 +1159,7 @@ export class EventAggregator {
1114
1159
  continue;
1115
1160
  const isError = block.is_error ?? false;
1116
1161
  this.resolveTaskCreate(toolUseId, block.content ?? block.output, event.timestamp, isError);
1117
- this.resolveAgentResult(toolUseId, isError, event.timestamp);
1162
+ this.resolveAgentResult(toolUseId, isError, event.timestamp, block.content ?? block.output);
1118
1163
  }
1119
1164
  }
1120
1165
  }
@@ -1148,7 +1193,7 @@ export class EventAggregator {
1148
1193
  else if (event.toolName === 'UpdatePlan') {
1149
1194
  this.handleUpdatePlan(input, new Date(event.timestamp));
1150
1195
  }
1151
- else if ((event.toolName === 'Agent' || event.toolName === 'Task') && toolUseId) {
1196
+ else if (isSubagentSpawnTool(event.toolName) && toolUseId) {
1152
1197
  this.handleAgentSpawn(toolUseId, input, new Date(event.timestamp));
1153
1198
  }
1154
1199
  }
@@ -1160,7 +1205,7 @@ export class EventAggregator {
1160
1205
  return;
1161
1206
  const isError = raw.is_error ?? false;
1162
1207
  this.resolveTaskCreate(toolUseId, raw.content, event.timestamp, isError);
1163
- this.resolveAgentResult(toolUseId, isError, event.timestamp);
1208
+ this.resolveAgentResult(toolUseId, isError, event.timestamp, raw.content);
1164
1209
  }
1165
1210
  }
1166
1211
  applyTaskUpdate(input) {
@@ -1465,11 +1510,14 @@ export class EventAggregator {
1465
1510
  };
1466
1511
  this.tasks.set(agentTaskId, task);
1467
1512
  }
1468
- resolveAgentResult(toolUseId, isError, timestamp) {
1513
+ resolveAgentResult(toolUseId, isError, timestamp, content) {
1469
1514
  const agentTaskId = `agent-${toolUseId}`;
1470
1515
  const agentTask = this.tasks.get(agentTaskId);
1471
1516
  if (!agentTask)
1472
1517
  return;
1518
+ // A background launch returns at once; its task notification finishes it.
1519
+ if (!isError && parseAsyncAgentLaunch(content))
1520
+ return;
1473
1521
  agentTask.status = isError ? 'deleted' : 'completed';
1474
1522
  agentTask.updatedAt = new Date(timestamp);
1475
1523
  }
@@ -1489,31 +1537,25 @@ export class EventAggregator {
1489
1537
  // Private: Subagent Tracking from SessionEvent
1490
1538
  // ═══════════════════════════════════════════════════════════════════════
1491
1539
  extractSubagentFromEvent(event) {
1540
+ const notification = taskNotificationFromEvent(event);
1541
+ if (notification)
1542
+ this.applyTaskNotification(notification, event.timestamp);
1492
1543
  const content = event.message?.content;
1493
1544
  if (!Array.isArray(content))
1494
1545
  return;
1495
1546
  for (const block of content) {
1496
- if (block.type === 'tool_use' && block.name === 'Task') {
1547
+ if (block.type === 'tool_use' && isSubagentSpawnTool(block.name)) {
1497
1548
  const input = block.input;
1498
1549
  const toolUseId = block.id;
1499
1550
  if (!input || !toolUseId)
1500
1551
  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);
1552
+ this.trackSubagentSpawn(toolUseId, input, event.timestamp);
1511
1553
  }
1512
1554
  else if (block.type === 'tool_result') {
1513
1555
  const toolUseId = block.tool_use_id;
1514
1556
  if (!toolUseId)
1515
1557
  continue;
1516
- this.completeSubagent(toolUseId, event.timestamp);
1558
+ this.resolveSubagentResult(toolUseId, block.content ?? block.output, block.is_error === true, event.timestamp);
1517
1559
  }
1518
1560
  }
1519
1561
  }
@@ -1522,23 +1564,10 @@ export class EventAggregator {
1522
1564
  // ═══════════════════════════════════════════════════════════════════════
1523
1565
  extractSubagentFromFollowEvent(event) {
1524
1566
  const raw = event.raw;
1525
- if (event.type === 'tool_use' && event.toolName === 'Task') {
1567
+ if (event.type === 'tool_use' && isSubagentSpawnTool(event.toolName)) {
1526
1568
  if (!raw?.input)
1527
1569
  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
- }
1570
+ this.trackSubagentSpawn(raw.id || '', raw.input, event.timestamp);
1542
1571
  }
1543
1572
  else if (event.type === 'tool_result') {
1544
1573
  if (!raw)
@@ -1546,23 +1575,133 @@ export class EventAggregator {
1546
1575
  const toolUseId = raw.tool_use_id;
1547
1576
  if (!toolUseId)
1548
1577
  return;
1549
- this.completeSubagent(toolUseId, event.timestamp);
1578
+ this.resolveSubagentResult(toolUseId, raw.content, raw.is_error === true, event.timestamp);
1579
+ }
1580
+ else if (event.type === 'user' && raw?.type === 'user') {
1581
+ // Claude Code delivers most task notifications as their own user line.
1582
+ const notification = taskNotificationFromEvent(raw);
1583
+ if (notification)
1584
+ this.applyTaskNotification(notification, event.timestamp);
1585
+ }
1586
+ }
1587
+ trackSubagentSpawn(toolUseId, input, timestamp) {
1588
+ const info = {
1589
+ id: toolUseId,
1590
+ description: input.description || 'Unknown task',
1591
+ subagentType: input.subagent_type || input.subagentType || 'general',
1592
+ spawnTime: timestamp,
1593
+ status: 'running',
1594
+ };
1595
+ const idx = this.subagents.length;
1596
+ this.subagents.push(info);
1597
+ if (toolUseId) {
1598
+ this.pendingSubagents.set(toolUseId, idx);
1550
1599
  }
1551
1600
  }
1552
- completeSubagent(toolUseId, timestamp) {
1601
+ /**
1602
+ * The spawning tool call's result. A foreground agent ends here; a
1603
+ * background launch only reports the agent id, and the agent keeps running
1604
+ * until its task notification arrives.
1605
+ */
1606
+ resolveSubagentResult(toolUseId, content, isError, timestamp) {
1553
1607
  const idx = this.pendingSubagents.get(toolUseId);
1554
1608
  if (idx === undefined)
1555
1609
  return;
1556
- this.pendingSubagents.delete(toolUseId);
1610
+ const launch = isError ? null : parseAsyncAgentLaunch(content);
1611
+ if (launch) {
1612
+ this.markBackgroundLaunch(idx, launch);
1613
+ return;
1614
+ }
1615
+ this.completeSubagent(toolUseId, timestamp, isError ? 'failed' : 'completed');
1616
+ }
1617
+ markBackgroundLaunch(idx, launch) {
1557
1618
  const agent = this.subagents[idx];
1619
+ this.pendingSubagents.delete(agent.id);
1620
+ agent.background = true;
1621
+ if (launch.agentId)
1622
+ agent.agentId = launch.agentId;
1623
+ }
1624
+ completeSubagent(toolUseId, timestamp, outcome = 'completed') {
1625
+ const idx = this.pendingSubagents.get(toolUseId);
1626
+ if (idx === undefined)
1627
+ return;
1628
+ this.finishSubagent(this.subagents[idx], timestamp, outcome);
1629
+ }
1630
+ finishSubagent(agent, timestamp, outcome) {
1631
+ this.pendingSubagents.delete(agent.id);
1558
1632
  agent.status = 'completed';
1559
1633
  agent.completionTime = timestamp;
1634
+ agent.outcome = outcome;
1560
1635
  const start = new Date(agent.spawnTime).getTime();
1561
1636
  const end = new Date(timestamp).getTime();
1562
1637
  if (!isNaN(start) && !isNaN(end) && end >= start) {
1563
1638
  agent.durationMs = end - start;
1564
1639
  }
1565
1640
  }
1641
+ /**
1642
+ * Applies a background task's notification to the subagent it reports on,
1643
+ * matched by the spawning tool_use id (or by agent id when the notification
1644
+ * has none). The latest notification wins: an interim one ("waiting on its
1645
+ * own background work") keeps or puts the agent back to running, a final one
1646
+ * completes it. Notifications for background shell tasks match nothing.
1647
+ */
1648
+ applyTaskNotification(notification, timestamp) {
1649
+ const agent = this.findNotifiedSubagent(notification);
1650
+ const toolUseId = agent?.id || notification.toolUseId;
1651
+ const outcome = taskNotificationOutcome(notification.status);
1652
+ if (agent) {
1653
+ agent.background = true;
1654
+ if (!agent.agentId)
1655
+ agent.agentId = notification.taskId;
1656
+ if (notification.interim) {
1657
+ this.pendingSubagents.delete(agent.id);
1658
+ agent.status = 'running';
1659
+ delete agent.completionTime;
1660
+ delete agent.durationMs;
1661
+ delete agent.outcome;
1662
+ }
1663
+ else {
1664
+ this.finishSubagent(agent, timestamp, outcome);
1665
+ }
1666
+ }
1667
+ const agentTask = toolUseId ? this.tasks.get(`agent-${toolUseId}`) : undefined;
1668
+ if (agentTask?.isSubagent) {
1669
+ agentTask.status = notification.interim
1670
+ ? 'in_progress'
1671
+ : outcome === 'failed'
1672
+ ? 'deleted'
1673
+ : 'completed';
1674
+ agentTask.updatedAt = new Date(timestamp);
1675
+ }
1676
+ }
1677
+ findNotifiedSubagent(notification) {
1678
+ const { toolUseId, taskId } = notification;
1679
+ for (let i = this.subagents.length - 1; i >= 0; i--) {
1680
+ if (toolUseId && this.subagents[i].id === toolUseId)
1681
+ return this.subagents[i];
1682
+ }
1683
+ for (let i = this.subagents.length - 1; i >= 0; i--) {
1684
+ if (this.subagents[i].agentId === taskId)
1685
+ return this.subagents[i];
1686
+ }
1687
+ return undefined;
1688
+ }
1689
+ addTaskNotificationTimeline(timestamp, notification) {
1690
+ let description = notification.summary ?? `Background task ${notification.status ?? 'finished'}`;
1691
+ if (description.length > 200)
1692
+ description = description.substring(0, 197) + '...';
1693
+ this.timeline.push({
1694
+ type: 'tool_result',
1695
+ timestamp,
1696
+ description,
1697
+ noiseLevel: 'system',
1698
+ messageClassification: 'system',
1699
+ ...(notification.toolUseId ? { metadata: { toolUseId: notification.toolUseId } } : {}),
1700
+ });
1701
+ if (this.timeline.length > this.timelineCap) {
1702
+ this.timeline.shift();
1703
+ }
1704
+ }
1566
1705
  // ═══════════════════════════════════════════════════════════════════════
1567
1706
  // Private: Plan Extraction from SessionEvent
1568
1707
  // ═══════════════════════════════════════════════════════════════════════
@@ -2,14 +2,18 @@
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
  */
10
13
  import * as fs from 'fs';
11
14
  import * as path from 'path';
12
15
  import { getConfigDir } from '../paths';
16
+ import { verifyJsonlCheckpoint } from '../watchers/jsonlTail';
13
17
  import { atomicWriteJsonSync } from '../writers/atomic';
14
18
  import { SNAPSHOT_SCHEMA_VERSION } from './EventAggregator';
15
19
  /** Current schema version — bump when format changes. */
@@ -101,8 +105,8 @@ export function pruneSnapshots(keep = MAX_SNAPSHOT_FILES) {
101
105
  * - Schema version doesn't match
102
106
  * - JSON parse fails
103
107
  *
104
- * The caller is responsible for staleness checks (comparing sourceSize
105
- * and readerPosition against the actual file).
108
+ * The caller is responsible for staleness checks against the actual file:
109
+ * `isSnapshotValid(snapshot, currentSize, sourcePath)`.
106
110
  */
107
111
  export function loadSnapshot(sessionId) {
108
112
  try {
@@ -142,13 +146,21 @@ export function deleteSnapshot(sessionId) {
142
146
  /**
143
147
  * Validates whether a snapshot is still usable for a given source file.
144
148
  *
145
- * For JSONL providers: checks that the file hasn't been truncated
146
- * (current size >= snapshot position) and that the size matches or has grown.
147
- *
148
149
  * For DB-backed providers: sourceSize is 0, so we always consider valid
149
150
  * (the DB reader's timestamp-based cursor handles staleness naturally).
151
+ *
152
+ * For JSONL providers: the file must not have shrunk below the snapshot
153
+ * position. With `sourcePath` the check is also content-aware: the snapshot
154
+ * must carry a {@link SessionSnapshot.sourceCheckpoint} for exactly
155
+ * `readerPosition`, and that checkpoint must still match the file at
156
+ * `sourcePath` (same device and inode, same first bytes, position just after
157
+ * a newline), so a file replaced by another one or rewritten to the same or
158
+ * a larger size is not resumed mid-content. A snapshot without a checkpoint
159
+ * (written before 0.29.1) is invalid when `sourcePath` is given, which costs
160
+ * one full replay. Without `sourcePath` only the size check applies, as it
161
+ * did before.
150
162
  */
151
- export function isSnapshotValid(snapshot, currentSourceSize) {
163
+ export function isSnapshotValid(snapshot, currentSourceSize, sourcePath) {
152
164
  // DB-backed providers store sourceSize=0 — always valid (cursor is a timestamp)
153
165
  if (snapshot.sourceSize === 0) {
154
166
  return true;
@@ -157,5 +169,15 @@ export function isSnapshotValid(snapshot, currentSourceSize) {
157
169
  if (currentSourceSize < snapshot.readerPosition) {
158
170
  return false;
159
171
  }
160
- return true;
172
+ if (typeof sourcePath !== 'string') {
173
+ return true;
174
+ }
175
+ // Snapshot content comes from disk: guard the shape before trusting it.
176
+ const checkpoint = snapshot.sourceCheckpoint;
177
+ if (!checkpoint ||
178
+ typeof checkpoint !== 'object' ||
179
+ checkpoint.offset !== snapshot.readerPosition) {
180
+ return false;
181
+ }
182
+ return verifyJsonlCheckpoint(sourcePath, checkpoint);
161
183
  }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Pure parsers for the provider CLIs' read-only sign-in status commands,
3
+ * `claude auth status --json` and `codex login status`. They do no I/O. The
4
+ * caller runs the command with its own environment, home, and timeout, and
5
+ * passes the output here.
6
+ *
7
+ * A status command reports which credential the CLI would use. It does not
8
+ * prove that a request will authenticate: an expired or revoked token can
9
+ * still read as logged in.
10
+ *
11
+ * @module authStatus
12
+ */
13
+ /** Larger outputs are not a status report; reading stops there. */
14
+ const MAX_STATUS_OUTPUT = 256 * 1024;
15
+ const MAX_DETAIL = 200;
16
+ function stringField(record, key) {
17
+ const value = record[key];
18
+ if (typeof value !== 'string')
19
+ return undefined;
20
+ const trimmed = value.trim();
21
+ return trimmed && trimmed.length <= 512 ? trimmed : undefined;
22
+ }
23
+ function parseJsonObject(output) {
24
+ const trimmed = output.trim();
25
+ const candidates = [trimmed];
26
+ // Tolerate warning lines printed ahead of the JSON document, which must
27
+ // then start a line and run to the end of the output.
28
+ const start = trimmed.search(/\n[ \t]*\{/);
29
+ if (start >= 0)
30
+ candidates.push(trimmed.slice(start).trim());
31
+ for (const candidate of candidates) {
32
+ if (!candidate.startsWith('{') || !candidate.endsWith('}'))
33
+ continue;
34
+ try {
35
+ const parsed = JSON.parse(candidate);
36
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
37
+ return parsed;
38
+ }
39
+ catch {
40
+ /* Try the next candidate. */
41
+ }
42
+ }
43
+ return undefined;
44
+ }
45
+ /**
46
+ * Parse the stdout of `claude auth status --json`. Claude Code exits 1 while
47
+ * printing valid signed-out JSON, so parse stdout whatever the exit status
48
+ * was. Only a JSON `"loggedIn": true` counts as logged in.
49
+ */
50
+ export function parseClaudeAuthStatus(stdout) {
51
+ const raw = typeof stdout === 'string' && stdout.length <= MAX_STATUS_OUTPUT
52
+ ? parseJsonObject(stdout)
53
+ : undefined;
54
+ if (!raw)
55
+ return { loggedIn: false };
56
+ const method = stringField(raw, 'authMethod');
57
+ const email = stringField(raw, 'email');
58
+ const subscriptionType = stringField(raw, 'subscriptionType');
59
+ return {
60
+ loggedIn: raw.loggedIn === true,
61
+ ...(method ? { method } : {}),
62
+ ...(email ? { email } : {}),
63
+ ...(subscriptionType ? { subscriptionType } : {}),
64
+ raw,
65
+ };
66
+ }
67
+ /**
68
+ * Parse the output of `codex login status`. Codex prints its status line to
69
+ * stderr (`Logged in using ChatGPT`, `Logged in using an API key - sk-…`,
70
+ * `Not logged in`), so pass both streams. The exit status is not consulted:
71
+ * callers that require exit 0 check it themselves.
72
+ */
73
+ export function parseCodexLoginStatus(stdout, stderr) {
74
+ const combined = `${typeof stdout === 'string' ? stdout : ''}\n${typeof stderr === 'string' ? stderr : ''}`.trim();
75
+ const firstLine = combined.split(/\r?\n/, 1)[0]?.trim() ?? '';
76
+ const detail = (/^(Logged in using .*?API key)\s+-\s.*$/i.exec(firstLine)?.[1] ?? firstLine)
77
+ .slice(0, MAX_DETAIL)
78
+ .trim();
79
+ const withDetail = detail ? { detail } : {};
80
+ if (!/^Logged in/i.test(combined))
81
+ return { loggedIn: false, ...withDetail };
82
+ if (/API key/i.test(combined))
83
+ return { loggedIn: true, method: 'api-key', ...withDetail };
84
+ if (/ChatGPT/i.test(combined))
85
+ return { loggedIn: true, method: 'chatgpt', ...withDetail };
86
+ return { loggedIn: true, ...withDetail };
87
+ }
@@ -17,17 +17,20 @@ export { addLocalDays, formatDurationMs, formatLocalDateKey, formatTokenCount, p
17
17
  export { parseMcpToolName } from './parsers/mcpToolName';
18
18
  export { buildSessionContextSnapshot, calculateSessionContextPressure, createSessionContextProjector, } from './context/sessionContext';
19
19
  export { ACCOUNT_PROVIDER_IDS, MODEL_PROVIDER_IDS, QUOTA_PROVIDER_IDS, RUNTIME_QUOTA_PROVIDER_IDS, SESSION_PROVIDER_IDS, } from './providerIds';
20
+ export { SUBAGENT_SPAWN_TOOL_NAMES, isSubagentSpawnTool, isTaskNotificationSystemEvent, parseTaskNotification, taskNotificationFromEvent, } from './subagentSpawn';
20
21
  export { assistantTurnEventsFromSessionEvents, extractTurnSubagents, isAssistantTurnSubagentTool, reasoningSummary, segmentAssistantTurn, } from './turns/assistantTurn';
21
- export { parseModelId, getModelPricing, resolveModelPricing, getModelInfo, calculateCost, calculateCostWithPricing, calculateCostWithProvenance, mergeCostSources, shortModelName, getModelDisplayInfo, compareModelIds, sortModelIds, formatCost, } from './modelInfo';
22
+ export { parseModelId, getModelPricing, resolveModelPricing, getModelInfo, calculateCost, calculateCostWithPricing, calculateCostWithProvenance, mergeCostSources, shortModelName, getModelDisplayInfo, compareModelIds, sortModelIds, formatCost, canonicalModelId, isMovingModelAlias, registerModelPricing, clearRegisteredModelPricing, getPricingCatalogInfo, costSourceFromPricingProvenance, mergeCostComponents, } from './modelInfo';
22
23
  export { exportResolvedModelCatalog, importResolvedModelCatalog, registerModelAlias, resolveModelAlias, RESOLVED_MODEL_CATALOG_SCHEMA_VERSION, } from './modelCatalog';
23
24
  export { BILLING_BLOCK_DURATION_MS, computeBillingBlocks, findActiveBillingBlock, } from './usage/billingBlocks';
24
25
  export { bucketUsage, summarizeUsageRows, usageBucketKey, weekKey } from './usage/usageReports';
25
26
  export { applySessionSummary, isFileImported, markFileImported, removeSessionSummary, sessionSummaryFromStats, } from './historicalStore';
26
27
  export { FAILING_TOOL_TREND_ARROWS, failingToolTrend, mergeFailingToolWindows, } from './failingTools';
27
- export { diagnoseProviderFailure, PROVIDER_AUTHENTICATION_SOURCES, PROVIDER_AUTHENTICATION_STATES, PROVIDER_CREDENTIAL_KINDS, PROVIDER_FAILURE_CODES, PROVIDER_FAILURE_EVIDENCE_SOURCES, PROVIDER_RECOVERY_ACTIONS, } from './providerFailure';
28
+ export { diagnoseProviderFailure, PROVIDER_AUTHENTICATION_SOURCES, PROVIDER_AUTHENTICATION_STATES, PROVIDER_CREDENTIAL_KINDS, PROVIDER_FAILURE_CODES, PROVIDER_FAILURE_EVIDENCE_SOURCES, PROVIDER_RECOVERY_ACTIONS, CODEX_ERROR_INFO_KINDS, decodeCodexErrorInfo, } from './providerFailure';
29
+ export { parseClaudeAuthStatus, parseCodexLoginStatus } from './authStatus';
28
30
  export { isAggregateCodexLimit } from './types/codex';
29
31
  // Codex rollout parsing (pure: no filesystem access)
30
32
  export { CodexRolloutParser, DEFAULT_MAX_TRACKED_TOOL_CALLS, extractPatchFilePaths, normalizeCodexToolName, normalizeCodexToolInput, resolveCodexCallUsage, } from './parsers/codexParser';
33
+ export { codexRolloutOrigin, codexSpawnParentFromSource } from './parsers/codexRolloutOrigin';
31
34
  export { HUMAN_CLAUDE_ENTRYPOINTS, HUMAN_CODEX_SOURCES, humanPromptText, isHumanPrompt, } from './humanPrompt';
32
35
  export { renderHandoffUrlTemplate } from './handoffUrl';
33
36
  // Tool categories across providers
@@ -0,0 +1,18 @@
1
+ /**
2
+ * `sidekick-shared/codex` — a small Node-only entry for Codex homes, the
3
+ * child-process environment, `auth.json` helpers, credential-store mode,
4
+ * rollout lookup, and the atomic JSON writers those helpers rely on.
5
+ *
6
+ * Loads a handful of modules and no Zod, so a host that only launches or
7
+ * locates Codex does not pay for the full `sidekick-shared/node` surface.
8
+ * Everything here is also exported from the package root and from
9
+ * `sidekick-shared/node`.
10
+ *
11
+ * @module codex
12
+ */
13
+ export { getSystemCodexHome, resolveSidekickCodexHome, getCodexExecutionEnv, getCodexMonitoringHomes, getCodexSessionsDirs, getCodexCredentialStoreMode, forceFileCredentialStore, CODEX_SESSIONS_SUBDIR, CODEX_ARCHIVED_SESSIONS_SUBDIR, } from './codexPaths';
14
+ export { parseAuthJson as parseCodexAuthJson, readAuthIdentityFromRaw as readCodexAuthIdentity, readLastRefresh as readCodexLastRefresh, readAccessTokenExpiry as readCodexAccessTokenExpiry, identitiesMatch as codexIdentitiesMatch, codexRefreshFingerprint, compareCodexAuthFreshness, } from './codexAuth';
15
+ export { findCodexRolloutFile } from './providers/codexRollouts';
16
+ export { atomicWriteJson, atomicWriteJsonSync } from './writers/atomic';
17
+ export { parseClaudeAuthStatus, parseCodexLoginStatus } from './authStatus';
18
+ export { codexRolloutOrigin, codexSpawnParentFromSource } from './parsers/codexRolloutOrigin';
@@ -7,6 +7,7 @@ import { getAccountsDir, getActiveSavedAccount, listSavedAccountProfiles, remove
7
7
  // re-serializing would drop fields added by newer codex versions, and the
8
8
  // rotated refresh token inside is only valid in its freshest form.
9
9
  import { atomicWriteFileSync as atomicWriteFile, atomicWriteJsonSync as atomicWriteJson, withFileLockSync, } from './writers/atomic';
10
+ import { parseCodexLoginStatus } from './authStatus';
10
11
  import { identitiesMatch, codexRefreshFingerprint, compareCodexAuthFreshness, readAuthIdentityFromRaw, readLastRefresh, STALE_AUTH_THRESHOLD_MS, } from './codexAuth';
11
12
  import { ensureCodexProfileDirs, forceFileCredentialStore, getCodexCredentialStoreMode, getCodexProfileDir, getCodexProfileHome, getCodexProfileStatePath, getCodexBackupPath, getCodexLoginDir, getCodexLoginsDir, getCodexLoginHome, getCodexLoginStatePath, getCodexProfilesDir, getSystemCodexHome, readFileOrNull, resolveSidekickCodexHome, } from './codexPaths';
12
13
  import { getAccountHealth, readHealthSidecar, writeCodexHealthSidecar, } from './accountHealth';
@@ -206,18 +207,14 @@ function readMetadataFromLegacyCredentials(codexHome) {
206
207
  return {};
207
208
  }
208
209
  const PROBE_TIMEOUT_MS = 4000;
210
+ /** `codex login status` counts only on exit 0; the text is read by {@link parseCodexLoginStatus}. */
209
211
  export function parseCodexLoginStatusOutput(status, stdout) {
210
- const trimmed = stdout.trim();
211
- if (status === 0 && /^Logged in/i.test(trimmed)) {
212
- if (/API key/i.test(trimmed)) {
213
- return { loggedIn: true, authMode: 'api-key' };
214
- }
215
- if (/ChatGPT/i.test(trimmed)) {
216
- return { loggedIn: true, authMode: 'chatgpt' };
217
- }
218
- return { loggedIn: true };
219
- }
220
- return { loggedIn: false };
212
+ if (status !== 0)
213
+ return { loggedIn: false };
214
+ const parsed = parseCodexLoginStatus(stdout);
215
+ if (!parsed.loggedIn)
216
+ return { loggedIn: false };
217
+ return parsed.method ? { loggedIn: true, authMode: parsed.method } : { loggedIn: true };
221
218
  }
222
219
  /**
223
220
  * Blocks the caller's event loop for up to {@link PROBE_TIMEOUT_MS}. Async code
@@ -8,6 +8,7 @@ import { EventAggregator } from '../aggregation/EventAggregator';
8
8
  import { getModelContextWindowSize } from '../modelContext';
9
9
  import { parseMcpToolName } from '../parsers/mcpToolName';
10
10
  import { estimateTextTokens } from '../tokenEstimation';
11
+ import { isTaskNotificationSystemEvent } from '../subagentSpawn';
11
12
  const DEFAULT_CONTEXT_SOURCE_LIMIT = 200;
12
13
  const DEFAULT_SNIPPET_MAX_CHARS = 240;
13
14
  const DEFAULT_BODY_MAX_CHARS = 5000;
@@ -205,6 +206,9 @@ function extractSourcesFromEvent(event, state) {
205
206
  }
206
207
  }
207
208
  function extractSystemSource(event, state) {
209
+ // Background-task notices only update subagent lifecycles (like the aggregator).
210
+ if (isTaskNotificationSystemEvent(event))
211
+ return;
208
212
  const message = event.message;
209
213
  const text = extractText(message?.content);
210
214
  const label = message?.sourceLabel ?? message?.role ?? 'system';
@@ -7,6 +7,7 @@
7
7
  *
8
8
  * @module formatters/toolSummary
9
9
  */
10
+ import { ownValue } from '../ownKey';
10
11
  // ── Helpers ──
11
12
  function truncate(text, maxLen) {
12
13
  const clean = text.replace(/\s+/g, ' ').trim();
@@ -171,6 +172,8 @@ const TOOL_FORMATTERS = {
171
172
  Bash: formatBash,
172
173
  Grep: formatGrep,
173
174
  Glob: formatGlob,
175
+ // Claude Code's subagent tool; `Task` is its earlier name.
176
+ Agent: formatTask,
174
177
  Task: formatTask,
175
178
  WebFetch: formatWebFetch,
176
179
  WebSearch: formatWebSearch,
@@ -214,7 +217,7 @@ function genericFallback(input) {
214
217
  export function formatToolSummary(toolName, input) {
215
218
  // Strip MCP prefixes: "mcp__server__tool" → use "tool" for matching
216
219
  const baseName = toolName.includes('__') ? (toolName.split('__').pop() ?? toolName) : toolName;
217
- const formatter = TOOL_FORMATTERS[toolName] ?? TOOL_FORMATTERS[baseName];
220
+ const formatter = ownValue(TOOL_FORMATTERS, toolName) ?? ownValue(TOOL_FORMATTERS, baseName);
218
221
  if (formatter) {
219
222
  const result = formatter(input);
220
223
  if (result)
@@ -2,10 +2,19 @@
2
2
  * Pure display formatting helpers shared by CLI, extension host, webviews,
3
3
  * and downstream apps.
4
4
  *
5
- * This module intentionally has no Node-only imports.
5
+ * This module intentionally has no Node-only imports, and no runtime imports
6
+ * at all: bundles that only format values must not retain the pricing tables
7
+ * in `modelInfo.ts`. Type-only imports are fine.
6
8
  *
7
9
  * @module formatting
8
10
  */
11
+ export function formatCost(cost, options) {
12
+ const source = options?.source;
13
+ if (cost === null || cost === undefined || source === 'unpriced')
14
+ return '—';
15
+ const formatted = cost < 0.01 ? `$${cost.toFixed(4)}` : `$${cost.toFixed(2)}`;
16
+ return source === 'estimated' ? `${formatted} (est.)` : formatted;
17
+ }
9
18
  /** Format a token count with compact `k` / `M` suffixes. */
10
19
  export function formatTokenCount(value, options = {}) {
11
20
  if (!Number.isFinite(value))