sidekick-shared 0.29.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +198 -56
  3. package/dist/aggregation/EventAggregator.d.ts +96 -2
  4. package/dist/aggregation/EventAggregator.js +535 -42
  5. package/dist/aggregation/snapshot.d.ts +37 -8
  6. package/dist/aggregation/snapshot.js +31 -9
  7. package/dist/aggregation/types.d.ts +38 -1
  8. package/dist/browser.d.ts +9 -1
  9. package/dist/browser.js +29 -3
  10. package/dist/context/sessionContext.js +8 -0
  11. package/dist/esm/aggregation/EventAggregator.js +535 -42
  12. package/dist/esm/aggregation/snapshot.js +31 -9
  13. package/dist/esm/browser.js +3 -0
  14. package/dist/esm/context/sessionContext.js +8 -0
  15. package/dist/esm/fileContinuity.js +149 -0
  16. package/dist/esm/formatters/toolSummary.js +4 -1
  17. package/dist/esm/index.js +7 -2
  18. package/dist/esm/modelCatalog.js +5 -4
  19. package/dist/esm/modelContext.js +4 -3
  20. package/dist/esm/modelInfo.js +4 -3
  21. package/dist/esm/node.js +3 -1
  22. package/dist/esm/observedContextWindows.js +2 -1
  23. package/dist/esm/ownKey.js +26 -0
  24. package/dist/esm/parsers/codexParser.js +372 -7
  25. package/dist/esm/parsers/openCodeParser.js +2 -1
  26. package/dist/esm/parsers/subagentEndState.js +61 -0
  27. package/dist/esm/parsers/subagentScanner.js +37 -12
  28. package/dist/esm/parsers/subagentTraceParser.js +103 -37
  29. package/dist/esm/pricingCatalog.js +3 -2
  30. package/dist/esm/promptHistory.js +22 -11
  31. package/dist/esm/providers/claudeCode.js +60 -29
  32. package/dist/esm/providers/codex.js +235 -201
  33. package/dist/esm/providers/codexCompressedReader.js +184 -0
  34. package/dist/esm/providers/codexDatabase.js +47 -2
  35. package/dist/esm/providers/codexRollouts.js +20 -2
  36. package/dist/esm/providers/codexSubagents.js +456 -0
  37. package/dist/esm/providers/compressedRollouts.js +330 -0
  38. package/dist/esm/providers/factory.js +4 -1
  39. package/dist/esm/providers/rolloutWalker.js +32 -4
  40. package/dist/esm/schemas/sessionEvent.js +63 -14
  41. package/dist/esm/sessionMonitor.js +7 -1
  42. package/dist/esm/sessionPreviews.js +9 -0
  43. package/dist/esm/sessionPromptHistory.js +5 -3
  44. package/dist/esm/subagentActivity.js +125 -0
  45. package/dist/esm/subagentDisplayState.js +81 -0
  46. package/dist/esm/subagentSpawn.js +363 -0
  47. package/dist/esm/testing/codexSubagentFixtures.js +336 -0
  48. package/dist/esm/toolCallOrigins.js +17 -10
  49. package/dist/esm/toolCategory.js +6 -2
  50. package/dist/esm/transcript.js +8 -0
  51. package/dist/esm/turns/assistantTurn.js +6 -2
  52. package/dist/esm/usage/claudeUsageDedupe.js +68 -0
  53. package/dist/esm/usage/usageEvents.js +4 -1
  54. package/dist/esm/watchers/eventBridge.js +45 -0
  55. package/dist/esm/watchers/factory.js +5 -3
  56. package/dist/esm/watchers/jsonlTail.js +82 -48
  57. package/dist/esm/watchers/jsonlWatcher.js +61 -4
  58. package/dist/esm/watchers/providerReaderWatcher.js +18 -4
  59. package/dist/esm/zaiQuota.js +2 -1
  60. package/dist/fileContinuity.d.ts +95 -0
  61. package/dist/fileContinuity.js +187 -0
  62. package/dist/formatters/toolSummary.js +4 -1
  63. package/dist/index.d.ts +19 -7
  64. package/dist/index.js +48 -10
  65. package/dist/modelCatalog.js +5 -4
  66. package/dist/modelContext.js +4 -3
  67. package/dist/modelInfo.js +4 -3
  68. package/dist/node.d.ts +5 -1
  69. package/dist/node.js +18 -2
  70. package/dist/observedContextWindows.js +2 -1
  71. package/dist/ownKey.d.ts +16 -0
  72. package/dist/ownKey.js +30 -0
  73. package/dist/parsers/codexParser.d.ts +107 -0
  74. package/dist/parsers/codexParser.js +373 -7
  75. package/dist/parsers/openCodeParser.js +2 -1
  76. package/dist/parsers/subagentEndState.d.ts +20 -0
  77. package/dist/parsers/subagentEndState.js +65 -0
  78. package/dist/parsers/subagentScanner.d.ts +19 -6
  79. package/dist/parsers/subagentScanner.js +37 -12
  80. package/dist/parsers/subagentTraceParser.d.ts +33 -5
  81. package/dist/parsers/subagentTraceParser.js +103 -37
  82. package/dist/pricingCatalog.js +3 -2
  83. package/dist/promptHistory.d.ts +13 -3
  84. package/dist/promptHistory.js +22 -11
  85. package/dist/providers/claudeCode.js +59 -28
  86. package/dist/providers/codex.d.ts +35 -5
  87. package/dist/providers/codex.js +232 -198
  88. package/dist/providers/codexCompressedReader.d.ts +58 -0
  89. package/dist/providers/codexCompressedReader.js +221 -0
  90. package/dist/providers/codexDatabase.d.ts +25 -0
  91. package/dist/providers/codexDatabase.js +48 -2
  92. package/dist/providers/codexRollouts.d.ts +16 -2
  93. package/dist/providers/codexRollouts.js +20 -2
  94. package/dist/providers/codexSubagents.d.ts +66 -0
  95. package/dist/providers/codexSubagents.js +495 -0
  96. package/dist/providers/compressedRollouts.d.ts +114 -0
  97. package/dist/providers/compressedRollouts.js +380 -0
  98. package/dist/providers/factory.d.ts +5 -0
  99. package/dist/providers/factory.js +4 -1
  100. package/dist/providers/rolloutWalker.d.ts +15 -2
  101. package/dist/providers/rolloutWalker.js +33 -4
  102. package/dist/providers/types.d.ts +36 -1
  103. package/dist/schemas/index.d.ts +1 -0
  104. package/dist/schemas/sessionEvent.d.ts +51 -10
  105. package/dist/schemas/sessionEvent.js +63 -14
  106. package/dist/sessionMonitor.d.ts +6 -0
  107. package/dist/sessionMonitor.js +7 -1
  108. package/dist/sessionPreviews.js +9 -0
  109. package/dist/sessionPromptHistory.d.ts +5 -0
  110. package/dist/sessionPromptHistory.js +5 -36
  111. package/dist/subagentActivity.d.ts +91 -0
  112. package/dist/subagentActivity.js +132 -0
  113. package/dist/subagentDisplayState.d.ts +81 -0
  114. package/dist/subagentDisplayState.js +86 -0
  115. package/dist/subagentSpawn.d.ts +175 -0
  116. package/dist/subagentSpawn.js +385 -0
  117. package/dist/testing/codexSubagentFixtures.d.ts +48 -0
  118. package/dist/testing/codexSubagentFixtures.js +344 -0
  119. package/dist/toolCallOrigins.d.ts +5 -0
  120. package/dist/toolCallOrigins.js +17 -10
  121. package/dist/toolCategory.js +6 -2
  122. package/dist/transcript.js +8 -0
  123. package/dist/turns/assistantTurn.d.ts +4 -0
  124. package/dist/turns/assistantTurn.js +6 -2
  125. package/dist/types/codex.d.ts +71 -2
  126. package/dist/types/sessionEvent.d.ts +38 -3
  127. package/dist/usage/claudeUsageDedupe.d.ts +31 -0
  128. package/dist/usage/claudeUsageDedupe.js +68 -0
  129. package/dist/usage/usageEvents.d.ts +4 -1
  130. package/dist/usage/usageEvents.js +4 -1
  131. package/dist/watchers/eventBridge.js +45 -0
  132. package/dist/watchers/factory.d.ts +6 -0
  133. package/dist/watchers/factory.js +5 -3
  134. package/dist/watchers/jsonlTail.d.ts +27 -4
  135. package/dist/watchers/jsonlTail.js +84 -48
  136. package/dist/watchers/jsonlWatcher.d.ts +22 -1
  137. package/dist/watchers/jsonlWatcher.js +60 -3
  138. package/dist/watchers/providerReaderWatcher.d.ts +7 -0
  139. package/dist/watchers/providerReaderWatcher.js +18 -4
  140. package/dist/watchers/types.d.ts +12 -0
  141. package/dist/zaiQuota.js +2 -1
  142. package/package.json +1 -1
@@ -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
  }
@@ -17,6 +17,8 @@ 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, agentsStoppedNoticeFromEvent, isAgentsKilledSystemEvent, isCodexSubagentSpawnTool, isSubagentSpawnTool, isTaskNotificationSystemEvent, isTaskStatusSystemEvent, parseAgentsStoppedNotice, parseCodexSpawnLaunch, parseTaskNotification, taskNotificationFromEvent, taskStatusFromAttachment, taskStatusFromEvent, } from './subagentSpawn';
21
+ export { SUBAGENT_PARENT_IDLE_ENDED_MS, SUBAGENT_UNREPORTED_QUIET_MS, resolveSubagentDisplayState, subagentDisplayLabel, } from './subagentDisplayState';
20
22
  export { assistantTurnEventsFromSessionEvents, extractTurnSubagents, isAssistantTurnSubagentTool, reasoningSummary, segmentAssistantTurn, } from './turns/assistantTurn';
21
23
  export { parseModelId, getModelPricing, resolveModelPricing, getModelInfo, calculateCost, calculateCostWithPricing, calculateCostWithProvenance, mergeCostSources, shortModelName, getModelDisplayInfo, compareModelIds, sortModelIds, formatCost, canonicalModelId, isMovingModelAlias, registerModelPricing, clearRegisteredModelPricing, getPricingCatalogInfo, costSourceFromPricingProvenance, mergeCostComponents, } from './modelInfo';
22
24
  export { exportResolvedModelCatalog, importResolvedModelCatalog, registerModelAlias, resolveModelAlias, RESOLVED_MODEL_CATALOG_SCHEMA_VERSION, } from './modelCatalog';
@@ -30,6 +32,7 @@ export { isAggregateCodexLimit } from './types/codex';
30
32
  // Codex rollout parsing (pure: no filesystem access)
31
33
  export { CodexRolloutParser, DEFAULT_MAX_TRACKED_TOOL_CALLS, extractPatchFilePaths, normalizeCodexToolName, normalizeCodexToolInput, resolveCodexCallUsage, } from './parsers/codexParser';
32
34
  export { codexRolloutOrigin, codexSpawnParentFromSource } from './parsers/codexRolloutOrigin';
35
+ export { SUBAGENT_ACTIVITY_BLOCK_TYPE, SUBAGENT_ACTIVITY_ORIGIN_KIND, describeSubagentActivity, isSubagentActivitySystemEvent, parseCodexAgentMessageText, subagentActivityFromEvent, } from './subagentActivity';
33
36
  export { HUMAN_CLAUDE_ENTRYPOINTS, HUMAN_CODEX_SOURCES, humanPromptText, isHumanPrompt, } from './humanPrompt';
34
37
  export { renderHandoffUrlTemplate } from './handoffUrl';
35
38
  // Tool categories across providers
@@ -8,6 +8,8 @@ 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, isTaskStatusSystemEvent } from '../subagentSpawn';
12
+ import { isSubagentActivitySystemEvent } from '../subagentActivity';
11
13
  const DEFAULT_CONTEXT_SOURCE_LIMIT = 200;
12
14
  const DEFAULT_SNIPPET_MAX_CHARS = 240;
13
15
  const DEFAULT_BODY_MAX_CHARS = 5000;
@@ -205,6 +207,12 @@ function extractSourcesFromEvent(event, state) {
205
207
  }
206
208
  }
207
209
  function extractSystemSource(event, state) {
210
+ // Background-task notices only update subagent lifecycles (like the aggregator).
211
+ if (isTaskNotificationSystemEvent(event) || isTaskStatusSystemEvent(event))
212
+ return;
213
+ // So are subagent lifecycle records (opt-in Codex parser events).
214
+ if (isSubagentActivitySystemEvent(event))
215
+ return;
208
216
  const message = event.message;
209
217
  const text = extractText(message?.content);
210
218
  const label = message?.sourceLabel ?? message?.role ?? 'system';
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Continuity of a file read incrementally by byte offset: whether the file
3
+ * now at a path is still the content earlier reads consumed, or was
4
+ * truncated, rewritten, or replaced since.
5
+ *
6
+ * {@link FileContinuity} holds the rules `createJsonlTail` applies, so every
7
+ * incremental reader detects the same cases the same way:
8
+ *
9
+ * - `replaced`: another file (by device and inode, read from the open
10
+ * descriptor, so a rename between a stat and an open cannot mix two files)
11
+ * now has the path.
12
+ * - `truncated`: the file is shorter than the read position.
13
+ * - `rewritten`: the mtime changed and either the first {@link HEAD_BYTES}
14
+ * bytes differ from those recorded, or the byte before the committed
15
+ * (resume-safe) offset is no longer a newline.
16
+ *
17
+ * Replacement and truncation are checked on every read; the content probe
18
+ * only when the mtime changed or the recorded head is still short.
19
+ *
20
+ * Known limits: a rewrite that leaves the mtime unchanged (coarse mtime
21
+ * resolution, or an mtime restored on purpose) or that changes only bytes
22
+ * past the first 4 KiB while the resume point still follows a newline goes
23
+ * unnoticed; on filesystems without stable inode numbers (some SMB mounts,
24
+ * Windows) an unchanged file can read as replaced, which costs a replay.
25
+ *
26
+ * @module fileContinuity
27
+ */
28
+ import * as fs from 'fs';
29
+ import { fileHeadOf, HEAD_BYTES, headMatches } from './fileHead';
30
+ /**
31
+ * What one reader knows about the file its offsets refer to: identity,
32
+ * mtime, and a digest of the first bytes, recorded by {@link adopt} after
33
+ * each read.
34
+ *
35
+ * Synchronous readers call {@link check} on the open descriptor, restart
36
+ * from offset 0 when it returns a reason, then call {@link adopt}. A reader
37
+ * that repositions without reading (a seek to a stored offset, a reset)
38
+ * calls {@link forget}, so its next read adopts the file instead of
39
+ * comparing it with the old one. The lower-level methods ({@link needsProbe},
40
+ * {@link reasonFor}, {@link record}) serve asynchronous readers that probe
41
+ * on their own.
42
+ */
43
+ export class FileContinuity {
44
+ identity = null;
45
+ head = null;
46
+ lastMtimeMs = -1;
47
+ /** What the last {@link check} probed and decided, for {@link adopt}. */
48
+ checked = null;
49
+ /** Device and inode of the adopted file; `null` before the first read and after {@link forget}. */
50
+ get knownIdentity() {
51
+ return this.identity;
52
+ }
53
+ /** Length and digest of the adopted file's first bytes, once probed. */
54
+ get knownHead() {
55
+ return this.head;
56
+ }
57
+ /** Whether {@link reasonFor} needs bytes from the file to decide. */
58
+ needsProbe(stat, readOffset) {
59
+ return (this.identity === null ||
60
+ stat.dev !== this.identity.dev ||
61
+ stat.ino !== this.identity.ino ||
62
+ stat.size < readOffset ||
63
+ stat.mtimeMs !== this.lastMtimeMs ||
64
+ this.head === null ||
65
+ this.head.bytes < Math.min(HEAD_BYTES, stat.size));
66
+ }
67
+ /**
68
+ * Why the reader must start over: the path now names another file, the
69
+ * file shrank below `readOffset`, its first bytes changed, or the resume
70
+ * point no longer follows a newline. `null` when it can continue.
71
+ */
72
+ reasonFor(stat, readOffset, probe) {
73
+ if (this.identity && (stat.dev !== this.identity.dev || stat.ino !== this.identity.ino)) {
74
+ return 'replaced';
75
+ }
76
+ if (stat.size < readOffset)
77
+ return 'truncated';
78
+ if (this.identity && stat.mtimeMs !== this.lastMtimeMs && probe) {
79
+ if (this.head && !headMatches(this.head, probe.prefix))
80
+ return 'rewritten';
81
+ if (probe.before && probe.before[0] !== 0x0a)
82
+ return 'rewritten';
83
+ }
84
+ return null;
85
+ }
86
+ /** Records the identity, mtime, and (from `probe`) first bytes of the file the offsets now refer to. */
87
+ record(stat, probe) {
88
+ this.identity = { dev: stat.dev, ino: stat.ino };
89
+ this.lastMtimeMs = stat.mtimeMs;
90
+ if (probe && (!this.head || this.head.bytes < Math.min(HEAD_BYTES, stat.size))) {
91
+ this.head = fileHeadOf(probe.prefix);
92
+ }
93
+ }
94
+ /** Drops the recorded first bytes (the reader restarted); the next {@link record} with a probe sets them again. */
95
+ discardHead() {
96
+ this.head = null;
97
+ }
98
+ /** Records the file as known from `prefix`, its first `min(HEAD_BYTES, size)` bytes. */
99
+ adoptPrefix(stat, prefix) {
100
+ this.identity = { dev: stat.dev, ino: stat.ino };
101
+ this.lastMtimeMs = stat.mtimeMs;
102
+ this.head = fileHeadOf(prefix);
103
+ }
104
+ /** Forgets the file, so the next {@link check} adopts whatever file has the path. */
105
+ forget() {
106
+ this.identity = null;
107
+ this.head = null;
108
+ this.lastMtimeMs = -1;
109
+ this.checked = null;
110
+ }
111
+ /**
112
+ * Applies the rules to the file open on `fd` (`stat` from `fstat` on that
113
+ * descriptor), reading the bytes they need. `readOffset` is how far the
114
+ * reader has read; `committedOffset` (default `readOffset`) is its
115
+ * resume-safe position just after a newline. Returns why the reader must
116
+ * restart from offset 0, or `null`. Call {@link adopt} with the same
117
+ * `stat` once the reader has acted on the answer.
118
+ */
119
+ check(fd, stat, readOffset, committedOffset = readOffset) {
120
+ const probe = this.needsProbe(stat, readOffset)
121
+ ? probeFileContinuity(fd, stat, committedOffset)
122
+ : null;
123
+ const reason = this.reasonFor(stat, readOffset, probe);
124
+ this.checked = { probe, reset: reason !== null };
125
+ return reason;
126
+ }
127
+ /** Records the file just checked as the one the reader's offsets now refer to. */
128
+ adopt(stat) {
129
+ const checked = this.checked;
130
+ this.checked = null;
131
+ if (checked?.reset)
132
+ this.head = null;
133
+ this.record(stat, checked?.probe ?? null);
134
+ }
135
+ }
136
+ /** Reads the bytes {@link FileContinuity.reasonFor} compares, from the file open on `fd`. */
137
+ export function probeFileContinuity(fd, stat, committedOffset) {
138
+ return {
139
+ prefix: readAt(fd, 0, Math.min(HEAD_BYTES, stat.size)),
140
+ before: committedOffset > 0 && committedOffset <= stat.size
141
+ ? readAt(fd, committedOffset - 1, 1)
142
+ : null,
143
+ };
144
+ }
145
+ function readAt(fd, position, length) {
146
+ const buffer = Buffer.alloc(length);
147
+ const count = length > 0 ? fs.readSync(fd, buffer, 0, length, position) : 0;
148
+ return buffer.subarray(0, count);
149
+ }
@@ -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)
package/dist/esm/index.js CHANGED
@@ -51,6 +51,7 @@ export { ClaudeCodeProvider } from './providers/claudeCode';
51
51
  export { OpenCodeProvider, getOpenCodeDataDir } from './providers/openCode';
52
52
  export { CodexProvider, findCodexRolloutFile } from './providers/codex';
53
53
  export { walkRolloutFiles, walkRolloutFilesAsync } from './providers/rolloutWalker';
54
+ export { COMPRESSED_ROLLOUT_SUFFIX, isCompressedRolloutPath, isZstdDecompressionAvailable, readRolloutBuffer, readRolloutText, resolveRolloutPath, } from './providers/compressedRollouts';
54
55
  // Parsers — JSONL
55
56
  export { JsonlParser, TRUNCATION_PATTERNS } from './parsers/jsonl';
56
57
  // Parsers — OpenCode
@@ -58,6 +59,7 @@ export { normalizeToolName, normalizeToolInput, detectPlanModeFromText, convertO
58
59
  // Parsers — Codex
59
60
  export { CodexRolloutParser, DEFAULT_MAX_TRACKED_TOOL_CALLS, extractPatchFilePaths, normalizeCodexToolName, normalizeCodexToolInput, resolveCodexCallUsage, } from './parsers/codexParser';
60
61
  export { codexRolloutOrigin, codexSpawnParentFromSource } from './parsers/codexRolloutOrigin';
62
+ export { SUBAGENT_ACTIVITY_BLOCK_TYPE, SUBAGENT_ACTIVITY_ORIGIN_KIND, describeSubagentActivity, isSubagentActivitySystemEvent, parseCodexAgentMessageText, subagentActivityFromEvent, } from './subagentActivity';
61
63
  // Parsers — Subagent scanning
62
64
  export { scanSubagentDir, extractTaskInfo, readAgentMeta } from './parsers/subagentScanner';
63
65
  // Usage — split-line dedupe, subagent tokens, session totals
@@ -80,13 +82,16 @@ export { listSessionPreviews, listSessionPreviewsAsync, readSessionPreview, read
80
82
  export { parseDebugLog, filterByLevel, collapseDuplicates, discoverDebugLogs, } from './parsers/debugLogParser';
81
83
  // Database wrappers
82
84
  export { OpenCodeDatabase } from './providers/openCodeDatabase';
83
- export { CodexDatabase } from './providers/codexDatabase';
85
+ export { CodexDatabase, isCodexSubagentThread } from './providers/codexDatabase';
84
86
  // Search
85
87
  export { searchSessions } from './search/sessionSearch';
86
88
  export { FilterEngine } from './search/advancedFilter';
87
89
  // Context
88
90
  export { composeContext } from './context/composer';
89
91
  export { buildSessionContextSnapshot, calculateSessionContextPressure, createSessionContextProjector, readSessionContextSnapshot, } from './context/sessionContext';
92
+ // Subagent spawns (`Agent` / `Task`) and background-task notifications
93
+ export { SUBAGENT_SPAWN_TOOL_NAMES, agentsStoppedNoticeFromEvent, isAgentsKilledSystemEvent, isCodexSubagentSpawnTool, isSubagentSpawnTool, isTaskNotificationSystemEvent, isTaskStatusSystemEvent, parseAgentsStoppedNotice, parseCodexSpawnLaunch, parseTaskNotification, taskNotificationFromEvent, taskStatusFromAttachment, taskStatusFromEvent, } from './subagentSpawn';
94
+ export { SUBAGENT_PARENT_IDLE_ENDED_MS, SUBAGENT_UNREPORTED_QUIET_MS, resolveSubagentDisplayState, subagentDisplayLabel, } from './subagentDisplayState';
90
95
  // Assistant turn projection
91
96
  export { assistantTurnEventsFromSessionEvents, extractTurnSubagents, isAssistantTurnSubagentTool, reasoningSummary, segmentAssistantTurn, } from './turns/assistantTurn';
92
97
  // Plan Extraction
@@ -95,7 +100,7 @@ export { PlanExtractor, parsePlanMarkdown as parsePlanMarkdownShared, extractPro
95
100
  export { parseChangelog } from './parsers/changelogParser';
96
101
  export { createWatcher, resolveSessionPath } from './watchers/factory';
97
102
  export { toFollowEvents } from './watchers/eventBridge';
98
- export { createJsonlTail, DEFAULT_JSONL_TAIL_MAX_LINE_BYTES, DEFAULT_JSONL_TAIL_READ_BYTES, JSONL_TAIL_CHECKPOINT_VERSION, JsonlTailSymlinkError, } from './watchers/jsonlTail';
103
+ export { captureJsonlCheckpoint, createJsonlTail, DEFAULT_JSONL_TAIL_MAX_LINE_BYTES, DEFAULT_JSONL_TAIL_READ_BYTES, JSONL_TAIL_CHECKPOINT_VERSION, JsonlTailSymlinkError, verifyJsonlCheckpoint, } from './watchers/jsonlTail';
99
104
  // Formatters
100
105
  export { addLocalDays, formatDurationMs, formatLocalDateKey, formatTokenCount, parseLocalDateKey, } from './formatting';
101
106
  export { formatToolSummary } from './formatters/toolSummary';
@@ -1,3 +1,4 @@
1
+ import { setOwnValue } from './ownKey';
1
2
  import { _getModelContextIds, _setImportedResolvedContextWindows, resolveModelContextWindow, } from './modelContext';
2
3
  import { _getModelPricingIds, _setImportedResolvedPricing, resolveModelPricing, } from './modelInfo';
3
4
  import { _getModelAliases, _importModelAliases, registerModelAlias, resolveModelAlias, } from './modelAliases';
@@ -24,13 +25,13 @@ export function exportResolvedModelCatalog(ids) {
24
25
  const selectedIds = normalizedIds(ids);
25
26
  for (const id of selectedIds) {
26
27
  const canonicalId = resolveModelAlias(id);
27
- models[id] = {
28
+ setOwnValue(models, id, {
28
29
  id,
29
30
  canonicalId,
30
31
  contextWindow: resolveModelContextWindow(id),
31
32
  // Registered prices are this realm's local configuration, never exported.
32
33
  pricing: resolveModelPricing(id, { includeRegistered: false }),
33
- };
34
+ });
34
35
  }
35
36
  const selectedCanonicalIds = new Set(Object.values(models).flatMap(({ id, canonicalId }) => [id, canonicalId]));
36
37
  const aliases = Object.fromEntries(Object.entries(_getModelAliases()).filter(([alias, canonicalId]) => selectedCanonicalIds.has(alias) || selectedCanonicalIds.has(canonicalId)));
@@ -75,9 +76,9 @@ export function importResolvedModelCatalog(snapshot) {
75
76
  if (!id)
76
77
  continue;
77
78
  if (contextResolved)
78
- contexts[id] = context;
79
+ setOwnValue(contexts, id, context);
79
80
  if (pricingResolved)
80
- prices[id] = pricing;
81
+ setOwnValue(prices, id, pricing);
81
82
  }
82
83
  imported++;
83
84
  }
@@ -28,6 +28,7 @@
28
28
  * webviews. Same pattern as `_setPricingOverrides` in `modelInfo.ts`.
29
29
  */
30
30
  import { resolveModelAlias } from './modelAliases';
31
+ import { ownValue, setOwnValue } from './ownKey';
31
32
  /** Known model context window sizes (in tokens); current models verified 2026-09-05. */
32
33
  const MODEL_CONTEXT_SIZES = {
33
34
  // Published API limits verified 2026-10-30; GLM's API documents 1M even
@@ -144,7 +145,7 @@ function normalizeOverrides(overrides) {
144
145
  for (const [key, value] of Object.entries(overrides)) {
145
146
  if (!Number.isFinite(value) || value <= 0)
146
147
  continue;
147
- out[key.trim().toLowerCase()] = value;
148
+ setOwnValue(out, key.trim().toLowerCase(), value);
148
149
  }
149
150
  return out;
150
151
  }
@@ -208,7 +209,7 @@ export function _getModelContextIds() {
208
209
  // ── Lookup ──
209
210
  /** Exact match against one table. */
210
211
  function exactWindow(table, modelId) {
211
- const hit = table[modelId];
212
+ const hit = ownValue(table, modelId);
212
213
  return hit === undefined ? null : hit;
213
214
  }
214
215
  function exactWindowMatch(table, modelId) {
@@ -256,7 +257,7 @@ export function resolveModelContextWindow(modelId) {
256
257
  // Imported resolutions rank below local observed data: a window this realm
257
258
  // saw first-hand is fresher than a snapshot transferred from another realm.
258
259
  if (!observedExact) {
259
- const imported = importedTable[stripped] ?? importedTable[canonicalModelId];
260
+ const imported = ownValue(importedTable, stripped) ?? ownValue(importedTable, canonicalModelId);
260
261
  if (imported) {
261
262
  return { ...imported, modelId: rawModelId, canonicalModelId };
262
263
  }
@@ -23,6 +23,7 @@
23
23
  */
24
24
  import { getModelContextWindowSize } from './modelContext';
25
25
  import { resolveModelAlias } from './modelAliases';
26
+ import { ownValue } from './ownKey';
26
27
  export { MODEL_PROVIDER_IDS } from './providerIds';
27
28
  export { formatCost } from './formatting';
28
29
  // ── Static Pricing Table ──
@@ -1078,11 +1079,11 @@ export function resolveModelPricing(modelId, options) {
1078
1079
  });
1079
1080
  if (registered && !registered.entry.onlyIfMissing)
1080
1081
  return registeredResult(registered);
1081
- const imported = importedTable[rawId] ?? importedTable[aliasId];
1082
+ const imported = ownValue(importedTable, rawId) ?? ownValue(importedTable, aliasId);
1082
1083
  if (imported)
1083
1084
  return { ...imported, modelId, canonicalModelId: aliasId };
1084
1085
  for (const id of candidateIds) {
1085
- const exactOverride = overrideTable[id];
1086
+ const exactOverride = ownValue(overrideTable, id);
1086
1087
  if (exactOverride) {
1087
1088
  return {
1088
1089
  modelId,
@@ -1098,7 +1099,7 @@ export function resolveModelPricing(modelId, options) {
1098
1099
  }
1099
1100
  }
1100
1101
  for (const id of candidateIds) {
1101
- const exactStatic = PRICING_TABLE[id];
1102
+ const exactStatic = ownValue(PRICING_TABLE, id);
1102
1103
  if (exactStatic) {
1103
1104
  return {
1104
1105
  modelId,
package/dist/esm/node.js CHANGED
@@ -24,6 +24,7 @@ export { ACTIVE_SESSION_MTIME_THRESHOLD_MS, importSessionHistory, } from './usag
24
24
  export { bucketUsage, summarizeUsageRows, usageBucketKey, weekKey } from './usage/usageReports';
25
25
  export { findCodexRolloutFile } from './providers/codex';
26
26
  export { walkRolloutFiles, walkRolloutFilesAsync } from './providers/rolloutWalker';
27
+ export { COMPRESSED_ROLLOUT_SUFFIX, isCompressedRolloutPath, isZstdDecompressionAvailable, readRolloutBuffer, readRolloutText, resolveRolloutPath, } from './providers/compressedRollouts';
27
28
  export { readCodexHistory } from './parsers/codexHistory';
28
29
  export { listSessionPreviews, listSessionPreviewsAsync, readSessionPreview, readSessionPreviewAsync, } from './sessionPreviews';
29
30
  export { ObservedSessionCollector, observedSessionSourceFromProvider, fileFingerprint, fileFingerprintParts, } from './observedSessionCollector';
@@ -41,6 +42,7 @@ export { parseAuthJson as parseCodexAuthJson, readAuthIdentityFromRaw as readCod
41
42
  export { atomicWriteJson, atomicWriteJsonSync } from './writers/atomic';
42
43
  export { parseClaudeAuthStatus, parseCodexLoginStatus } from './authStatus';
43
44
  export { codexRolloutOrigin, codexSpawnParentFromSource } from './parsers/codexRolloutOrigin';
45
+ export { SUBAGENT_ACTIVITY_BLOCK_TYPE, SUBAGENT_ACTIVITY_ORIGIN_KIND, describeSubagentActivity, isSubagentActivitySystemEvent, parseCodexAgentMessageText, subagentActivityFromEvent, } from './subagentActivity';
44
46
  export { getPricingCatalogInfo, registerModelPricing, clearRegisteredModelPricing, } from './modelInfo';
45
47
  export { fetchProviderServiceStatus } from './providerServiceStatus';
46
48
  // Tool categories across providers
@@ -49,4 +51,4 @@ export { classifyTool, TOOL_CATEGORIES } from './toolCategory';
49
51
  export { resolveWorkspaceRoots } from './workspaceRoots';
50
52
  export { discoverWorktreeSiblings, resolveWorktreeMainRepo } from './parsers/sessionPathResolver';
51
53
  // Bounded JSONL tail reader
52
- export { createJsonlTail, DEFAULT_JSONL_TAIL_MAX_LINE_BYTES, DEFAULT_JSONL_TAIL_READ_BYTES, JSONL_TAIL_CHECKPOINT_VERSION, JsonlTailSymlinkError, } from './watchers/jsonlTail';
54
+ export { captureJsonlCheckpoint, createJsonlTail, DEFAULT_JSONL_TAIL_MAX_LINE_BYTES, DEFAULT_JSONL_TAIL_READ_BYTES, JSONL_TAIL_CHECKPOINT_VERSION, JsonlTailSymlinkError, verifyJsonlCheckpoint, } from './watchers/jsonlTail';
@@ -32,6 +32,7 @@
32
32
  import { promises as fs } from 'node:fs';
33
33
  import * as path from 'node:path';
34
34
  import { _setObservedContextWindows, _getObservedContextWindows } from './modelContext';
35
+ import { setOwnValue } from './ownKey';
35
36
  import { getConfigDir } from './paths';
36
37
  import { updateJsonStoreAtomic } from './writers/atomic';
37
38
  // ── Constants ──
@@ -80,7 +81,7 @@ function toOverrideMap(store, now = Date.now()) {
80
81
  const observedAt = typeof entry.observedAt === 'string' ? Date.parse(entry.observedAt) : NaN;
81
82
  if (Number.isFinite(observedAt) && now - observedAt > OBSERVED_CONTEXT_WINDOW_TTL_MS)
82
83
  continue;
83
- out[modelId] = window;
84
+ setOwnValue(out, modelId, window);
84
85
  }
85
86
  return out;
86
87
  }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Own-property lookups for plain-object tables indexed by external strings.
3
+ *
4
+ * `table[key]` also finds inherited members, so a model id, tool name, or
5
+ * error code such as `constructor`, `toString`, or `__proto__` would return an
6
+ * `Object.prototype` member instead of `undefined`.
7
+ *
8
+ * @module ownKey
9
+ */
10
+ const hasOwn = Object.prototype.hasOwnProperty;
11
+ /** The value stored under `key` in `table` itself, or `undefined`. */
12
+ export function ownValue(table, key) {
13
+ return hasOwn.call(table, key) ? table[key] : undefined;
14
+ }
15
+ /**
16
+ * Store `value` as an own, enumerable property, even for `__proto__`, which a
17
+ * plain assignment would treat as a prototype change.
18
+ */
19
+ export function setOwnValue(table, key, value) {
20
+ Object.defineProperty(table, key, {
21
+ value,
22
+ enumerable: true,
23
+ writable: true,
24
+ configurable: true,
25
+ });
26
+ }