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
@@ -6,10 +6,13 @@
6
6
  * (not just stats) for drill-down trace visualization. Applies noise classification
7
7
  * and tool summary formatting from the formatters module.
8
8
  *
9
- * Three-phase parent-child linking:
10
- * 1. Result-based: match toolUseResult.agentId → parent tool_use_id
11
- * 2. Team summary: parse <teammate-message> XML blocks
12
- * 3. Positional fallback: timestamp ordering + proximity
9
+ * Parent-child linking, exact links first:
10
+ * 1. Meta `parentAgentId` (Claude Code writes it for nested spawns)
11
+ * 2. The child's spawning tool_use id (meta `toolUseId`) found in another
12
+ * trace's `Agent` / `Task` calls
13
+ * Only traces without that spawn metadata (older Claude Code) fall back to:
14
+ * 3. Team summary: parse <teammate-message> XML blocks
15
+ * 4. Positional fallback: timestamp ordering + proximity
13
16
  *
14
17
  * @module parsers/subagentTraceParser
15
18
  */
@@ -55,6 +58,8 @@ const noiseClassifier_1 = require("../formatters/noiseClassifier");
55
58
  const claudeUsageDedupe_1 = require("../usage/claudeUsageDedupe");
56
59
  const subagentUsage_1 = require("../usage/subagentUsage");
57
60
  const subagentScanner_1 = require("./subagentScanner");
61
+ const subagentSpawn_1 = require("../subagentSpawn");
62
+ const ownKey_1 = require("../ownKey");
58
63
  // ── Constants ──
59
64
  const AGENT_FILE_PATTERN = /^agent-(.+)\.jsonl$/;
60
65
  // ── Public API ──
@@ -86,10 +91,8 @@ function scanSubagentTraces(sessionDir, sessionId) {
86
91
  if (trace)
87
92
  traces.push(trace);
88
93
  }
89
- // Link parent-child relationships
90
- linkTraces(traces);
91
- // Return only top-level traces (those without parents)
92
- return traces.filter((t) => !t.parentToolUseId);
94
+ // Link parent-child relationships; returns the traces with no parent agent.
95
+ return linkTraces(traces);
93
96
  }
94
97
  catch {
95
98
  return [];
@@ -169,8 +172,8 @@ function parseAgentTrace(filePath, agentId) {
169
172
  input: block.input || {},
170
173
  timestamp,
171
174
  });
172
- // Extract agent info from Task tool
173
- if (block.name === 'Task' && block.input) {
175
+ // Extract agent info from an Agent/Task spawn
176
+ if ((0, subagentSpawn_1.isSubagentSpawnTool)(block.name) && block.input) {
174
177
  if (block.input.subagent_type && !agentType) {
175
178
  agentType = String(block.input.subagent_type);
176
179
  }
@@ -207,6 +210,14 @@ function parseAgentTrace(filePath, agentId) {
207
210
  agentId,
208
211
  agentType,
209
212
  description,
213
+ filePath,
214
+ ...(meta.toolUseId
215
+ ? { spawnToolUseId: meta.toolUseId, parentToolUseId: meta.toolUseId }
216
+ : {}),
217
+ ...(meta.parentAgentId ? { parentAgentId: meta.parentAgentId } : {}),
218
+ ...(meta.requestShape ? { background: meta.requestShape === 'background' } : {}),
219
+ ...(meta.finished !== undefined ? { finished: meta.finished } : {}),
220
+ ...(meta.stoppedByUser !== undefined ? { stoppedByUser: meta.stoppedByUser } : {}),
210
221
  events,
211
222
  children: [],
212
223
  stats: {
@@ -231,23 +242,61 @@ function parseAgentTrace(filePath, agentId) {
231
242
  }
232
243
  }
233
244
  // ── Parent-Child Linking ──
245
+ /**
246
+ * Nests child traces under their parents and returns the traces with no
247
+ * parent agent. Exact metadata links come first; the heuristics only place
248
+ * traces Claude Code recorded no spawn metadata for.
249
+ */
234
250
  function linkTraces(traces) {
235
251
  const traceById = new Map();
236
252
  for (const trace of traces) {
237
253
  traceById.set(trace.agentId, trace);
238
254
  }
239
- // Phase 1: Result-based linking
240
- // Look for tool_result events that contain agentId references
255
+ const parentOf = new Map();
256
+ const attach = (parent, child) => {
257
+ if (parent === child || parentOf.has(child))
258
+ return false;
259
+ // Never nest an agent under its own descendant (malformed metadata).
260
+ for (let up = parent; up; up = parentOf.get(up)) {
261
+ if (up === child)
262
+ return false;
263
+ }
264
+ parentOf.set(child, parent);
265
+ parent.children.push(child);
266
+ return true;
267
+ };
268
+ // Phase 1: the meta file names the parent agent
269
+ for (const child of traces) {
270
+ const parent = child.parentAgentId ? traceById.get(child.parentAgentId) : undefined;
271
+ if (parent)
272
+ attach(parent, child);
273
+ }
274
+ // Phase 2: the child's spawning call is in another trace
275
+ const spawnOwner = new Map();
241
276
  for (const trace of traces) {
242
- for (const traceEvent of trace.events) {
243
- const evt = traceEvent.event;
244
- if (evt.type === 'tool_use' && evt.tool?.name === 'Task') {
245
- // The tool_use_id from this call should appear as a result in the parent
246
- // but we can link via timestamp proximity to child traces
247
- }
277
+ for (const id of spawnToolUseIdsOf(trace)) {
278
+ if (!spawnOwner.has(id))
279
+ spawnOwner.set(id, trace);
248
280
  }
249
281
  }
250
- // Phase 2: Team summary linking
282
+ for (const child of traces) {
283
+ if (parentOf.has(child) || !child.spawnToolUseId)
284
+ continue;
285
+ const parent = spawnOwner.get(child.spawnToolUseId);
286
+ if (parent && attach(parent, child))
287
+ child.parentAgentId ??= parent.agentId;
288
+ }
289
+ // Heuristic links set parentAgentId and, as before, parentToolUseId to the
290
+ // parent's agent id.
291
+ const unresolved = (trace) => !parentOf.has(trace) && !trace.spawnToolUseId && !trace.parentAgentId;
292
+ const linkHeuristically = (parent, child) => {
293
+ if (!attach(parent, child))
294
+ return false;
295
+ child.parentAgentId = parent.agentId;
296
+ child.parentToolUseId = parent.agentId;
297
+ return true;
298
+ };
299
+ // Phase 3: Team summary linking
251
300
  for (const trace of traces) {
252
301
  for (const traceEvent of trace.events) {
253
302
  const content = traceEvent.event.message?.content;
@@ -255,45 +304,56 @@ function linkTraces(traces) {
255
304
  // Parse teammate agent IDs from XML blocks
256
305
  const matches = content.matchAll(/agent[_-]id['":\s]+([a-f0-9]+)/gi);
257
306
  for (const match of matches) {
258
- const childId = match[1];
259
- const child = traceById.get(childId);
260
- if (child && child !== trace && !child.parentToolUseId) {
261
- child.parentToolUseId = trace.agentId;
262
- trace.children.push(child);
263
- }
307
+ const child = traceById.get(match[1]);
308
+ if (child && unresolved(child))
309
+ linkHeuristically(trace, child);
264
310
  }
265
311
  }
266
312
  }
267
313
  }
268
- // Phase 3: Positional fallback — timestamp proximity
314
+ // Phase 4: Positional fallback — timestamp proximity
269
315
  // Traces without parents that fall within another trace's time window
270
- const parentless = traces.filter((t) => !t.parentToolUseId);
271
- for (const child of parentless) {
316
+ for (const child of traces.filter(unresolved)) {
272
317
  if (!child.stats.startTime)
273
318
  continue;
274
319
  const childStart = child.stats.startTime.getTime();
275
- // Find a trace that was active (Task tool call) right before this child started
320
+ // Find a top-level trace that spawned an agent right before this child started
276
321
  for (const potential of traces) {
277
- if (potential === child)
322
+ if (potential === child || parentOf.has(potential))
278
323
  continue;
279
- if (potential.parentToolUseId)
280
- continue; // Already linked
281
324
  for (const traceEvent of potential.events) {
282
325
  const evt = traceEvent.event;
283
- if (evt.type === 'assistant' && evt.tool?.name === 'Task') {
326
+ if (evt.type === 'assistant' && (0, subagentSpawn_1.isSubagentSpawnTool)(evt.tool?.name)) {
284
327
  const taskTime = new Date(evt.timestamp).getTime();
285
328
  // Within 2 seconds before child start
286
329
  if (taskTime <= childStart && childStart - taskTime < 2000) {
287
- child.parentToolUseId = potential.agentId;
288
- potential.children.push(child);
330
+ linkHeuristically(potential, child);
289
331
  break;
290
332
  }
291
333
  }
292
334
  }
293
- if (child.parentToolUseId)
335
+ if (parentOf.has(child))
294
336
  break;
295
337
  }
296
338
  }
339
+ return traces.filter((trace) => !parentOf.has(trace));
340
+ }
341
+ /** Ids of the `Agent` / `Task` calls a trace made. */
342
+ function spawnToolUseIdsOf(trace) {
343
+ const ids = [];
344
+ for (const traceEvent of trace.events) {
345
+ const content = traceEvent.event.message?.content;
346
+ if (!Array.isArray(content))
347
+ continue;
348
+ for (const block of content) {
349
+ if (block?.type === 'tool_use' &&
350
+ typeof block.id === 'string' &&
351
+ (0, subagentSpawn_1.isSubagentSpawnTool)(block.name)) {
352
+ ids.push(block.id);
353
+ }
354
+ }
355
+ }
356
+ return ids;
297
357
  }
298
358
  // ── Helpers ──
299
359
  function rawToSessionEvent(raw) {
@@ -308,7 +368,7 @@ function rawToSessionEvent(raw) {
308
368
  tool_result: 'tool_result',
309
369
  summary: 'summary',
310
370
  };
311
- const mappedType = typeMap[type];
371
+ const mappedType = (0, ownKey_1.ownValue)(typeMap, type);
312
372
  if (!mappedType)
313
373
  return null;
314
374
  const message = raw.message;
@@ -59,6 +59,7 @@ const node_fs_1 = require("node:fs");
59
59
  const path = __importStar(require("node:path"));
60
60
  const modelContext_1 = require("./modelContext");
61
61
  const modelInfo_1 = require("./modelInfo");
62
+ const ownKey_1 = require("./ownKey");
62
63
  const paths_1 = require("./paths");
63
64
  const atomic_1 = require("./writers/atomic");
64
65
  // ── Constants ──
@@ -156,7 +157,7 @@ function normalizeLiteLlmCatalog(raw) {
156
157
  continue;
157
158
  // Catalog keys use both `provider/model` and bare `model` forms. Record both
158
159
  // the raw key and the bare-model form so lookups work regardless of caller.
159
- out[key] = pricing;
160
+ (0, ownKey_1.setOwnValue)(out, key, pricing);
160
161
  const bare = stripProviderPrefix(key);
161
162
  if (bare && bare !== key && !out[bare]) {
162
163
  out[bare] = pricing;
@@ -193,7 +194,7 @@ function normalizeLiteLlmContextWindows(raw) {
193
194
  const window = entry.max_input_tokens;
194
195
  if (typeof window !== 'number' || !Number.isFinite(window) || window <= 0)
195
196
  continue;
196
- out[key] = window;
197
+ (0, ownKey_1.setOwnValue)(out, key, window);
197
198
  const bare = stripProviderPrefix(key);
198
199
  if (bare && bare !== key) {
199
200
  const seen = aliasWindows.get(bare) ?? new Set();
@@ -155,7 +155,8 @@ class ClaudeCodeReader {
155
155
  this.usageDeduper = usageDeduper ?? new claudeUsageDedupe_1.ClaudeUsageDeduper();
156
156
  this.parser = new jsonl_1.JsonlParser({
157
157
  onEvent: (raw) => {
158
- for (const event of (0, sessionEvent_1.extractSessionEvents)(raw)) {
158
+ // Mid-turn task notifications finish background subagents.
159
+ for (const event of (0, sessionEvent_1.extractSessionEvents)(raw, { includeTaskNotifications: true })) {
159
160
  this.events.push(this.usageDeduper.apply((0, claudeUsageDedupe_1.normalizeClaudeUsage)(event)));
160
161
  }
161
162
  },
@@ -244,6 +245,13 @@ class ClaudeCodeReader {
244
245
  wasTruncated() {
245
246
  return this._wasTruncated;
246
247
  }
248
+ /** The usage deduper's recent message ids, so a resumed reader does not recount a split response. */
249
+ getResumeState() {
250
+ return this.usageDeduper.exportState();
251
+ }
252
+ restoreResumeState(state) {
253
+ return this.usageDeduper.importState(state);
254
+ }
247
255
  }
248
256
  /**
249
257
  * Session provider for Claude Code CLI.
@@ -306,6 +306,12 @@ class CodexReader {
306
306
  lineBuffer = '';
307
307
  _wasTruncated = false;
308
308
  decoder = new string_decoder_1.StringDecoder('utf8');
309
+ /**
310
+ * Position from `seekTo()` whose prefix still has to be parsed for model,
311
+ * turn, and cumulative-token context; `restoreResumeState()` supplies that
312
+ * context instead and clears it.
313
+ */
314
+ pendingSeek = null;
309
315
  constructor(rolloutPath, onContextWindowLimit, onRateLimits, onTokenUsage) {
310
316
  this.rolloutPath = rolloutPath;
311
317
  this.onContextWindowLimit = onContextWindowLimit;
@@ -315,9 +321,11 @@ class CodexReader {
315
321
  }
316
322
  /** Get the parser's session metadata. */
317
323
  getSessionMeta() {
324
+ this.settleSeek();
318
325
  return this.parser.getSessionMeta();
319
326
  }
320
327
  readNew(endPosition) {
328
+ this.settleSeek();
321
329
  this._wasTruncated = false;
322
330
  const events = [];
323
331
  try {
@@ -371,47 +379,51 @@ class CodexReader {
371
379
  // Skip malformed JSON lines
372
380
  }
373
381
  }
374
- // Propagate model_context_window from token_count events to the provider.
375
- // The model comes along so the value can be persisted per model — the
376
- // window Codex reports is tier-specific, not the published maximum.
377
- const mcw = this.parser.getModelContextWindow();
378
- if (mcw && this.onContextWindowLimit) {
379
- this.onContextWindowLimit(mcw, this.parser.getCurrentModel() ?? undefined);
380
- }
381
- // Propagate rate_limits from token_count events to the provider
382
- const rateLimits = this.parser.getLastRateLimits();
383
- if (rateLimits && this.onRateLimits) {
384
- this.onRateLimits(rateLimits);
385
- }
386
- // Propagate last token usage snapshot to the provider
387
- const lastUsage = this.parser.getLastTokenUsage();
388
- if (lastUsage && this.onTokenUsage) {
389
- const normalized = (0, usageNormalization_1.normalizeProviderUsage)({
390
- semantics: 'openai',
391
- provider: 'openai',
392
- source: 'codex-reader-snapshot',
393
- model: this.parser.getCurrentModel() || undefined,
394
- inputTokens: lastUsage.input_tokens,
395
- outputTokens: lastUsage.output_tokens,
396
- cacheReadTokens: lastUsage.cached_input_tokens,
397
- reasoningTokens: lastUsage.reasoning_output_tokens,
398
- });
399
- this.onTokenUsage({
400
- inputTokens: normalized.uncachedInputTokens,
401
- outputTokens: normalized.outputTokens,
402
- cacheWriteTokens: normalized.cacheWriteTokens,
403
- cacheReadTokens: normalized.cacheReadTokens,
404
- reasoningTokens: normalized.reasoningTokens,
405
- model: this.parser.getCurrentModel() || 'unknown',
406
- timestamp: new Date(),
407
- });
408
- }
382
+ this.propagateParserState();
409
383
  }
410
384
  catch {
411
385
  // Skip read errors
412
386
  }
413
387
  return events;
414
388
  }
389
+ /** Hands the parser's context window, rate limits, and last usage to the provider. */
390
+ propagateParserState() {
391
+ // Propagate model_context_window from token_count events to the provider.
392
+ // The model comes along so the value can be persisted per model — the
393
+ // window Codex reports is tier-specific, not the published maximum.
394
+ const mcw = this.parser.getModelContextWindow();
395
+ if (mcw && this.onContextWindowLimit) {
396
+ this.onContextWindowLimit(mcw, this.parser.getCurrentModel() ?? undefined);
397
+ }
398
+ // Propagate rate_limits from token_count events to the provider
399
+ const rateLimits = this.parser.getLastRateLimits();
400
+ if (rateLimits && this.onRateLimits) {
401
+ this.onRateLimits(rateLimits);
402
+ }
403
+ // Propagate last token usage snapshot to the provider
404
+ const lastUsage = this.parser.getLastTokenUsage();
405
+ if (lastUsage && this.onTokenUsage) {
406
+ const normalized = (0, usageNormalization_1.normalizeProviderUsage)({
407
+ semantics: 'openai',
408
+ provider: 'openai',
409
+ source: 'codex-reader-snapshot',
410
+ model: this.parser.getCurrentModel() || undefined,
411
+ inputTokens: lastUsage.input_tokens,
412
+ outputTokens: lastUsage.output_tokens,
413
+ cacheReadTokens: lastUsage.cached_input_tokens,
414
+ reasoningTokens: lastUsage.reasoning_output_tokens,
415
+ });
416
+ this.onTokenUsage({
417
+ inputTokens: normalized.uncachedInputTokens,
418
+ outputTokens: normalized.outputTokens,
419
+ cacheWriteTokens: normalized.cacheWriteTokens,
420
+ cacheReadTokens: normalized.cacheReadTokens,
421
+ reasoningTokens: normalized.reasoningTokens,
422
+ model: this.parser.getCurrentModel() || 'unknown',
423
+ timestamp: new Date(),
424
+ });
425
+ }
426
+ }
415
427
  readAll() {
416
428
  this.reset();
417
429
  return this.readNew();
@@ -423,11 +435,13 @@ class CodexReader {
423
435
  this.parser.reset();
424
436
  this.decoder = new string_decoder_1.StringDecoder('utf8');
425
437
  this._wasTruncated = false;
438
+ this.pendingSeek = null;
426
439
  }
427
440
  exists() {
428
441
  return fs.existsSync(this.rolloutPath);
429
442
  }
430
443
  flush() {
444
+ this.settleSeek();
431
445
  this.lineBuffer += this.decoder.end();
432
446
  // Process any remaining data in the line buffer
433
447
  if (this.lineBuffer.trim()) {
@@ -445,15 +459,45 @@ class CodexReader {
445
459
  return this.committedPosition;
446
460
  }
447
461
  seekTo(position) {
448
- // Rebuild model, turn, and cumulative-token context before resuming.
449
- // The prefix is parsed only; its events are already in the consumer snapshot.
450
462
  this.reset();
463
+ this.filePosition = position;
464
+ this.committedPosition = position;
465
+ // The prefix's context is rebuilt on the next read, unless
466
+ // restoreResumeState() supplies it first.
451
467
  if (position > 0)
452
- this.readNew(position);
468
+ this.pendingSeek = position;
453
469
  }
454
470
  wasTruncated() {
455
471
  return this._wasTruncated;
456
472
  }
473
+ /** The parser's continuation state (see `CodexRolloutParser.exportResumeState`). */
474
+ getResumeState() {
475
+ this.settleSeek();
476
+ return this.parser.exportResumeState();
477
+ }
478
+ /**
479
+ * Restores parser state from `getResumeState()`; after `seekTo()` this
480
+ * replaces re-parsing the rollout up to that position.
481
+ */
482
+ restoreResumeState(state) {
483
+ if (!this.parser.importResumeState(state))
484
+ return false;
485
+ this.pendingSeek = null;
486
+ this.propagateParserState();
487
+ return true;
488
+ }
489
+ /**
490
+ * Rebuilds model, turn, and cumulative-token context for a pending
491
+ * `seekTo()` position. The prefix is parsed only; its events are already
492
+ * in the consumer snapshot.
493
+ */
494
+ settleSeek() {
495
+ const position = this.pendingSeek;
496
+ if (position === null)
497
+ return;
498
+ this.reset();
499
+ this.readNew(position);
500
+ }
457
501
  }
458
502
  // ---------------------------------------------------------------------------
459
503
  // CodexProvider — full SessionProviderBase implementation
@@ -186,6 +186,20 @@ export interface SessionReader {
186
186
  seekTo(position: number): void;
187
187
  /** Check if file was truncated (size < position). */
188
188
  wasTruncated(): boolean;
189
+ /**
190
+ * Parse state the reader needs, beyond `getPosition()`, to continue
191
+ * correctly after a restart: JSON-serializable data to persist together
192
+ * with that position (for example which Claude responses were already
193
+ * counted). `undefined` when the reader keeps none. Optional.
194
+ */
195
+ getResumeState?(): unknown;
196
+ /**
197
+ * Restores state from `getResumeState()`. Call it right after `seekTo()`
198
+ * the position that state was taken at. Returns false, changing nothing,
199
+ * when `state` is missing or not valid for this reader; the caller should
200
+ * then replay from the start instead. Optional.
201
+ */
202
+ restoreResumeState?(state: unknown): boolean;
189
203
  }
190
204
  /**
191
205
  * Expanded session provider interface (no VS Code dependencies).
@@ -12,6 +12,7 @@
12
12
  */
13
13
  export { ACCOUNT_PROVIDER_IDS, QUOTA_PROVIDER_IDS, RUNTIME_QUOTA_PROVIDER_IDS, SESSION_PROVIDER_IDS, } from '../providerIds';
14
14
  export { messageUsageSchema, sessionMessageSchema, sessionEventSchema, permissionModeSchema, extractSessionEvents, userSessionEventSchema, assistantSessionEventSchema, summarySessionEventSchema, systemSessionEventSchema, toolUseSessionEventSchema, toolResultSessionEventSchema, } from './sessionEvent';
15
+ export type { ExtractSessionEventsOptions } from './sessionEvent';
15
16
  export type { MessageUsage, SessionMessage, SessionEvent, PermissionMode, } from '../types/sessionEvent';
16
17
  export { normalizedUsageSchema, normalizedUsageCostSchema } from './usageNormalization';
17
18
  export type { NormalizedUsage, NormalizedUsageCost } from '../usageNormalization';
@@ -76,12 +76,12 @@ export declare const userSessionEventSchema: z.ZodObject<{
76
76
  promptSource: z.ZodOptional<z.ZodString>;
77
77
  cwd: z.ZodOptional<z.ZodString>;
78
78
  gitBranch: z.ZodOptional<z.ZodString>;
79
- permissionMode: z.ZodOptional<z.ZodEnum<{
79
+ permissionMode: z.ZodCatch<z.ZodOptional<z.ZodEnum<{
80
80
  default: "default";
81
81
  acceptEdits: "acceptEdits";
82
82
  bypassPermissions: "bypassPermissions";
83
83
  plan: "plan";
84
- }>>;
84
+ }>>>;
85
85
  rateLimits: z.ZodOptional<z.ZodObject<{
86
86
  primary: z.ZodOptional<z.ZodObject<{
87
87
  usedPercent: z.ZodNumber;
@@ -165,12 +165,12 @@ export declare const assistantSessionEventSchema: z.ZodObject<{
165
165
  promptSource: z.ZodOptional<z.ZodString>;
166
166
  cwd: z.ZodOptional<z.ZodString>;
167
167
  gitBranch: z.ZodOptional<z.ZodString>;
168
- permissionMode: z.ZodOptional<z.ZodEnum<{
168
+ permissionMode: z.ZodCatch<z.ZodOptional<z.ZodEnum<{
169
169
  default: "default";
170
170
  acceptEdits: "acceptEdits";
171
171
  bypassPermissions: "bypassPermissions";
172
172
  plan: "plan";
173
- }>>;
173
+ }>>>;
174
174
  rateLimits: z.ZodOptional<z.ZodObject<{
175
175
  primary: z.ZodOptional<z.ZodObject<{
176
176
  usedPercent: z.ZodNumber;
@@ -255,12 +255,12 @@ export declare const systemSessionEventSchema: z.ZodObject<{
255
255
  promptSource: z.ZodOptional<z.ZodString>;
256
256
  cwd: z.ZodOptional<z.ZodString>;
257
257
  gitBranch: z.ZodOptional<z.ZodString>;
258
- permissionMode: z.ZodOptional<z.ZodEnum<{
258
+ permissionMode: z.ZodCatch<z.ZodOptional<z.ZodEnum<{
259
259
  default: "default";
260
260
  acceptEdits: "acceptEdits";
261
261
  bypassPermissions: "bypassPermissions";
262
262
  plan: "plan";
263
- }>>;
263
+ }>>>;
264
264
  rateLimits: z.ZodOptional<z.ZodObject<{
265
265
  primary: z.ZodOptional<z.ZodObject<{
266
266
  usedPercent: z.ZodNumber;
@@ -348,12 +348,12 @@ export declare const toolUseSessionEventSchema: z.ZodObject<{
348
348
  promptSource: z.ZodOptional<z.ZodString>;
349
349
  cwd: z.ZodOptional<z.ZodString>;
350
350
  gitBranch: z.ZodOptional<z.ZodString>;
351
- permissionMode: z.ZodOptional<z.ZodEnum<{
351
+ permissionMode: z.ZodCatch<z.ZodOptional<z.ZodEnum<{
352
352
  default: "default";
353
353
  acceptEdits: "acceptEdits";
354
354
  bypassPermissions: "bypassPermissions";
355
355
  plan: "plan";
356
- }>>;
356
+ }>>>;
357
357
  rateLimits: z.ZodOptional<z.ZodObject<{
358
358
  primary: z.ZodOptional<z.ZodObject<{
359
359
  usedPercent: z.ZodNumber;
@@ -442,12 +442,12 @@ export declare const toolResultSessionEventSchema: z.ZodObject<{
442
442
  promptSource: z.ZodOptional<z.ZodString>;
443
443
  cwd: z.ZodOptional<z.ZodString>;
444
444
  gitBranch: z.ZodOptional<z.ZodString>;
445
- permissionMode: z.ZodOptional<z.ZodEnum<{
445
+ permissionMode: z.ZodCatch<z.ZodOptional<z.ZodEnum<{
446
446
  default: "default";
447
447
  acceptEdits: "acceptEdits";
448
448
  bypassPermissions: "bypassPermissions";
449
449
  plan: "plan";
450
- }>>;
450
+ }>>>;
451
451
  rateLimits: z.ZodOptional<z.ZodObject<{
452
452
  primary: z.ZodOptional<z.ZodObject<{
453
453
  usedPercent: z.ZodNumber;
@@ -520,6 +520,19 @@ export declare const toolResultSessionEventSchema: z.ZodObject<{
520
520
  }, z.core.$strip>>;
521
521
  }, z.core.$strip>;
522
522
  export declare const sessionEventSchema: z.ZodType<SessionEvent>;
523
+ /** Options for {@link extractSessionEvents}. */
524
+ export interface ExtractSessionEventsOptions {
525
+ /**
526
+ * Also turn Claude Code's mid-turn task-notification attachments
527
+ * (`{ type: 'attachment', attachment: { type: 'queued_command',
528
+ * commandMode: 'task-notification', prompt } }`) into one `system` event
529
+ * with `origin: { kind: 'task-notification' }`, `message: { role: 'user',
530
+ * sourceLabel: 'task notification', content: <prompt> }`, and the line's
531
+ * timestamp. They report background agents finishing. Default `false`:
532
+ * attachment lines yield no events.
533
+ */
534
+ includeTaskNotifications?: boolean;
535
+ }
523
536
  /**
524
537
  * Extracts canonical SessionEvents from a raw JSONL value.
525
538
  *
@@ -528,5 +541,9 @@ export declare const sessionEventSchema: z.ZodType<SessionEvent>;
528
541
  * `sessionEventSchema` alone rejects. This helper tries a direct parse,
529
542
  * then unwraps progress envelopes (recursively, in case of nesting).
530
543
  * Returns zero events for unrecognized input — never throws.
544
+ *
545
+ * The second argument is either the starting progress depth (the original
546
+ * signature) or an options object.
531
547
  */
532
548
  export declare function extractSessionEvents(raw: unknown, depth?: number): SessionEvent[];
549
+ export declare function extractSessionEvents(raw: unknown, options?: ExtractSessionEventsOptions): SessionEvent[];
@@ -12,6 +12,7 @@ exports.sessionEventSchema = exports.toolResultSessionEventSchema = exports.tool
12
12
  exports.extractSessionEvents = extractSessionEvents;
13
13
  const zod_1 = require("zod");
14
14
  const usageNormalization_1 = require("./usageNormalization");
15
+ const subagentSpawn_1 = require("../subagentSpawn");
15
16
  // ── MessageUsage ──
16
17
  exports.messageUsageSchema = zod_1.z.object({
17
18
  input_tokens: zod_1.z.number(),
@@ -56,7 +57,9 @@ const eventBaseShape = {
56
57
  promptSource: zod_1.z.string().optional(),
57
58
  cwd: zod_1.z.string().optional(),
58
59
  gitBranch: zod_1.z.string().optional(),
59
- permissionMode: exports.permissionModeSchema.optional(),
60
+ // A mode this release doesn't know (Claude Code added `auto`) is dropped
61
+ // rather than rejecting the whole line with its prompt or tool results.
62
+ permissionMode: exports.permissionModeSchema.optional().catch(undefined),
60
63
  rateLimits: zod_1.z
61
64
  .object({
62
65
  primary: zod_1.z
@@ -173,22 +176,21 @@ exports.sessionEventSchema = zod_1.z.union([
173
176
  // ── Progress unwrapping ──
174
177
  /** Recursion bound for nested progress envelopes; real data nests 1 level. */
175
178
  const MAX_PROGRESS_DEPTH = 8;
176
- /**
177
- * Extracts canonical SessionEvents from a raw JSONL value.
178
- *
179
- * Claude Code wraps subagent/SDK events as
180
- * `{ type: 'progress', data: { message: <SessionEvent> } }`, which
181
- * `sessionEventSchema` alone rejects. This helper tries a direct parse,
182
- * then unwraps progress envelopes (recursively, in case of nesting).
183
- * Returns zero events for unrecognized input — never throws.
184
- */
185
- function extractSessionEvents(raw, depth = 0) {
186
- return extractSessionEventsWithProvenance(raw, depth, {});
179
+ function extractSessionEvents(raw, depthOrOptions = 0) {
180
+ if (typeof depthOrOptions === 'number') {
181
+ return extractSessionEventsWithProvenance(raw, depthOrOptions, {}, {});
182
+ }
183
+ return extractSessionEventsWithProvenance(raw, 0, {}, depthOrOptions ?? {});
187
184
  }
188
- function extractSessionEventsWithProvenance(raw, depth, inherited) {
185
+ function extractSessionEventsWithProvenance(raw, depth, inherited, options) {
189
186
  const direct = exports.sessionEventSchema.safeParse(raw);
190
187
  if (direct.success)
191
188
  return [{ ...inherited, ...direct.data }];
189
+ if (options.includeTaskNotifications) {
190
+ const notification = taskNotificationAttachmentEvent(raw);
191
+ if (notification)
192
+ return [{ ...inherited, ...notification }];
193
+ }
192
194
  if (depth >= MAX_PROGRESS_DEPTH)
193
195
  return [];
194
196
  if (typeof raw === 'object' && raw !== null && raw.type === 'progress') {
@@ -197,12 +199,31 @@ function extractSessionEventsWithProvenance(raw, depth, inherited) {
197
199
  if (typeof data === 'object' && data !== null) {
198
200
  const message = data.message;
199
201
  if (typeof message === 'object' && message !== null) {
200
- return extractSessionEventsWithProvenance(message, depth + 1, provenance);
202
+ return extractSessionEventsWithProvenance(message, depth + 1, provenance, options);
201
203
  }
202
204
  }
203
205
  }
204
206
  return [];
205
207
  }
208
+ /** The `system` event for a task-notification attachment line, or `null`. */
209
+ function taskNotificationAttachmentEvent(raw) {
210
+ const prompt = (0, subagentSpawn_1.taskNotificationPromptFromAttachment)(raw);
211
+ if (prompt === null)
212
+ return null;
213
+ const line = raw;
214
+ const candidate = {
215
+ type: 'system',
216
+ timestamp: line.timestamp,
217
+ origin: { kind: 'task-notification' },
218
+ message: { role: 'user', sourceLabel: 'task notification', content: prompt },
219
+ };
220
+ for (const key of ['entrypoint', 'isSidechain', 'cwd', 'gitBranch']) {
221
+ if (line[key] !== undefined)
222
+ candidate[key] = line[key];
223
+ }
224
+ const parsed = exports.systemSessionEventSchema.safeParse(candidate);
225
+ return parsed.success ? parsed.data : null;
226
+ }
206
227
  function progressProvenance(raw) {
207
228
  const value = raw;
208
229
  const provenance = {};