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
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Session reader for a compressed Codex rollout (`rollout-…jsonl.zst`).
3
+ *
4
+ * Codex compresses only rollouts it no longer writes, so the content is
5
+ * decompressed and parsed whole on the first read; later reads cost one
6
+ * stat and return nothing until the file changes (then the rollout is read
7
+ * again from the start, reported as `replaced`). Positions are offsets in
8
+ * the decompressed content. Live tailing stays with the `.jsonl` reader.
9
+ *
10
+ * Node-only: reads files.
11
+ *
12
+ * @module providers/codexCompressedReader
13
+ */
14
+ import { type CodexRolloutResumeState } from '../parsers/codexParser';
15
+ import type { CodexRateLimits, CodexSessionMeta } from '../types/codex';
16
+ import type { SessionEvent, TokenUsage } from '../types/sessionEvent';
17
+ import { type ReadRolloutOptions } from './compressedRollouts';
18
+ import type { SessionReader } from './types';
19
+ type ReaderReset = NonNullable<ReturnType<NonNullable<SessionReader['lastReset']>>>;
20
+ export interface CompressedCodexReaderCallbacks {
21
+ onContextWindowLimit?: (limit: number, modelId?: string) => void;
22
+ onRateLimits?: (limits: CodexRateLimits) => void;
23
+ onTokenUsage?: (usage: TokenUsage) => void;
24
+ onDiagnostic?: ReadRolloutOptions['onDiagnostic'];
25
+ }
26
+ /** Reads a `.jsonl.zst` rollout through a {@link CodexRolloutParser}. */
27
+ export declare class CompressedCodexRolloutReader implements SessionReader {
28
+ private readonly rolloutPath;
29
+ private readonly callbacks;
30
+ private parser;
31
+ /** Offset after the last line parsed (the content is complete, so its end). */
32
+ private position;
33
+ /** Size and mtime of the compressed file the position refers to. */
34
+ private source;
35
+ private reset_;
36
+ /** Position from `seekTo()` whose prefix still has to be parsed for context. */
37
+ private pendingSeek;
38
+ constructor(rolloutPath: string, callbacks?: CompressedCodexReaderCallbacks);
39
+ getSessionMeta(): CodexSessionMeta | null;
40
+ readNew(): SessionEvent[];
41
+ readAll(): SessionEvent[];
42
+ reset(): void;
43
+ exists(): boolean;
44
+ flush(): void;
45
+ getPosition(): number;
46
+ seekTo(position: number): void;
47
+ wasTruncated(): boolean;
48
+ lastReset(): ReaderReset | null;
49
+ getResumeState(): CodexRolloutResumeState;
50
+ restoreResumeState(state: unknown): boolean;
51
+ /** Parses the content past `position`; `emit` false only rebuilds parser state. */
52
+ private readFrom;
53
+ /** Rebuilds parser context for a pending `seekTo()` position. */
54
+ private settleSeek;
55
+ /** Hands the parser's context window, rate limits, and last usage to the provider. */
56
+ private propagateParserState;
57
+ }
58
+ export {};
@@ -0,0 +1,221 @@
1
+ "use strict";
2
+ /**
3
+ * Session reader for a compressed Codex rollout (`rollout-…jsonl.zst`).
4
+ *
5
+ * Codex compresses only rollouts it no longer writes, so the content is
6
+ * decompressed and parsed whole on the first read; later reads cost one
7
+ * stat and return nothing until the file changes (then the rollout is read
8
+ * again from the start, reported as `replaced`). Positions are offsets in
9
+ * the decompressed content. Live tailing stays with the `.jsonl` reader.
10
+ *
11
+ * Node-only: reads files.
12
+ *
13
+ * @module providers/codexCompressedReader
14
+ */
15
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ var desc = Object.getOwnPropertyDescriptor(m, k);
18
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
19
+ desc = { enumerable: true, get: function() { return m[k]; } };
20
+ }
21
+ Object.defineProperty(o, k2, desc);
22
+ }) : (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ o[k2] = m[k];
25
+ }));
26
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
27
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
28
+ }) : function(o, v) {
29
+ o["default"] = v;
30
+ });
31
+ var __importStar = (this && this.__importStar) || (function () {
32
+ var ownKeys = function(o) {
33
+ ownKeys = Object.getOwnPropertyNames || function (o) {
34
+ var ar = [];
35
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
36
+ return ar;
37
+ };
38
+ return ownKeys(o);
39
+ };
40
+ return function (mod) {
41
+ if (mod && mod.__esModule) return mod;
42
+ var result = {};
43
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
44
+ __setModuleDefault(result, mod);
45
+ return result;
46
+ };
47
+ })();
48
+ Object.defineProperty(exports, "__esModule", { value: true });
49
+ exports.CompressedCodexRolloutReader = void 0;
50
+ const fs = __importStar(require("fs"));
51
+ const codexParser_1 = require("../parsers/codexParser");
52
+ const usageNormalization_1 = require("../usageNormalization");
53
+ const compressedRollouts_1 = require("./compressedRollouts");
54
+ /** Reads a `.jsonl.zst` rollout through a {@link CodexRolloutParser}. */
55
+ class CompressedCodexRolloutReader {
56
+ rolloutPath;
57
+ callbacks;
58
+ parser = new codexParser_1.CodexRolloutParser({ includeSubagentActivity: true });
59
+ /** Offset after the last line parsed (the content is complete, so its end). */
60
+ position = 0;
61
+ /** Size and mtime of the compressed file the position refers to. */
62
+ source = null;
63
+ reset_ = null;
64
+ /** Position from `seekTo()` whose prefix still has to be parsed for context. */
65
+ pendingSeek = null;
66
+ constructor(rolloutPath, callbacks = {}) {
67
+ this.rolloutPath = rolloutPath;
68
+ this.callbacks = callbacks;
69
+ }
70
+ getSessionMeta() {
71
+ this.settleSeek();
72
+ return this.parser.getSessionMeta();
73
+ }
74
+ readNew() {
75
+ this.settleSeek();
76
+ this.reset_ = null;
77
+ return this.readFrom(true);
78
+ }
79
+ readAll() {
80
+ this.reset();
81
+ return this.readNew();
82
+ }
83
+ reset() {
84
+ this.parser.reset();
85
+ this.position = 0;
86
+ this.source = null;
87
+ this.reset_ = null;
88
+ this.pendingSeek = null;
89
+ }
90
+ exists() {
91
+ return fs.existsSync(this.rolloutPath);
92
+ }
93
+ flush() {
94
+ // Nothing is buffered: each read parses through the end of the content.
95
+ }
96
+ getPosition() {
97
+ return this.position;
98
+ }
99
+ seekTo(position) {
100
+ this.reset();
101
+ this.position = Math.max(0, position);
102
+ if (this.position > 0)
103
+ this.pendingSeek = this.position;
104
+ }
105
+ wasTruncated() {
106
+ return this.reset_ !== null;
107
+ }
108
+ lastReset() {
109
+ return this.reset_ ? { ...this.reset_ } : null;
110
+ }
111
+ getResumeState() {
112
+ this.settleSeek();
113
+ return this.parser.exportResumeState();
114
+ }
115
+ restoreResumeState(state) {
116
+ if (!this.parser.importResumeState(state))
117
+ return false;
118
+ this.pendingSeek = null;
119
+ this.propagateParserState();
120
+ return true;
121
+ }
122
+ /** Parses the content past `position`; `emit` false only rebuilds parser state. */
123
+ readFrom(emit, endPosition) {
124
+ let stat;
125
+ try {
126
+ stat = fs.statSync(this.rolloutPath);
127
+ }
128
+ catch {
129
+ return [];
130
+ }
131
+ const current = { size: stat.size, mtimeMs: stat.mtimeMs };
132
+ if (this.source &&
133
+ (this.source.size !== current.size || this.source.mtimeMs !== current.mtimeMs)) {
134
+ // Another file has the path: what was read no longer describes it.
135
+ this.reset_ = { reason: 'replaced', previousPosition: this.position };
136
+ this.parser.reset();
137
+ this.position = 0;
138
+ }
139
+ else if (this.source) {
140
+ return [];
141
+ }
142
+ const content = (0, compressedRollouts_1.readRolloutBuffer)(this.rolloutPath, {
143
+ onDiagnostic: this.callbacks.onDiagnostic,
144
+ });
145
+ if (!content)
146
+ return [];
147
+ this.source = current;
148
+ const end = Math.min(content.length, endPosition ?? content.length);
149
+ if (this.position > end) {
150
+ // The position sought lies past this content: start over.
151
+ this.reset_ = { reason: 'truncated', previousPosition: this.position };
152
+ this.parser.reset();
153
+ this.position = 0;
154
+ }
155
+ const events = [];
156
+ const text = content.toString('utf8', this.position, end);
157
+ for (const line of text.split('\n')) {
158
+ const trimmed = line.trim();
159
+ if (!trimmed)
160
+ continue;
161
+ try {
162
+ const converted = this.parser.convertLine(JSON.parse(trimmed));
163
+ if (emit)
164
+ events.push(...converted);
165
+ }
166
+ catch {
167
+ // Skip malformed lines.
168
+ }
169
+ }
170
+ this.position = end;
171
+ if (end < content.length)
172
+ this.source = null;
173
+ this.propagateParserState();
174
+ return events;
175
+ }
176
+ /** Rebuilds parser context for a pending `seekTo()` position. */
177
+ settleSeek() {
178
+ const position = this.pendingSeek;
179
+ if (position === null)
180
+ return;
181
+ this.pendingSeek = null;
182
+ this.parser.reset();
183
+ this.position = 0;
184
+ this.source = null;
185
+ this.readFrom(false, position);
186
+ }
187
+ /** Hands the parser's context window, rate limits, and last usage to the provider. */
188
+ propagateParserState() {
189
+ const { onContextWindowLimit, onRateLimits, onTokenUsage } = this.callbacks;
190
+ const window = this.parser.getModelContextWindow();
191
+ if (window && onContextWindowLimit) {
192
+ onContextWindowLimit(window, this.parser.getCurrentModel() ?? undefined);
193
+ }
194
+ const rateLimits = this.parser.getLastRateLimits();
195
+ if (rateLimits && onRateLimits)
196
+ onRateLimits(rateLimits);
197
+ const lastUsage = this.parser.getLastTokenUsage();
198
+ if (lastUsage && onTokenUsage) {
199
+ const normalized = (0, usageNormalization_1.normalizeProviderUsage)({
200
+ semantics: 'openai',
201
+ provider: 'openai',
202
+ source: 'codex-reader-snapshot',
203
+ model: this.parser.getCurrentModel() || undefined,
204
+ inputTokens: lastUsage.input_tokens,
205
+ outputTokens: lastUsage.output_tokens,
206
+ cacheReadTokens: lastUsage.cached_input_tokens,
207
+ reasoningTokens: lastUsage.reasoning_output_tokens,
208
+ });
209
+ onTokenUsage({
210
+ inputTokens: normalized.uncachedInputTokens,
211
+ outputTokens: normalized.outputTokens,
212
+ cacheWriteTokens: normalized.cacheWriteTokens,
213
+ cacheReadTokens: normalized.cacheReadTokens,
214
+ reasoningTokens: normalized.reasoningTokens,
215
+ model: this.parser.getCurrentModel() || 'unknown',
216
+ timestamp: new Date(),
217
+ });
218
+ }
219
+ }
220
+ }
221
+ exports.CompressedCodexRolloutReader = CompressedCodexRolloutReader;
@@ -7,6 +7,21 @@ import type { ProviderRuntimeStatus } from './types';
7
7
  import { codexSpawnParentFromSource as spawnParentFromSource } from '../parsers/codexRolloutOrigin';
8
8
  /** Parent thread id of a spawned subagent's `source`; one rule shared with rollout parsing. */
9
9
  export { spawnParentFromSource };
10
+ /** Default levels {@link CodexDatabase.getSpawnedDescendantThreads} follows. */
11
+ export declare const DEFAULT_SUBAGENT_TREE_MAX_DEPTH = 8;
12
+ /** A spawned thread below a session, with the thread that spawned it. */
13
+ export interface CodexSpawnedThread {
14
+ thread: CodexDbThread;
15
+ /** The thread that spawned it: the session for depth 1, else another descendant. */
16
+ parentId: string;
17
+ /** 1 for the session's own children, 2 for theirs, … */
18
+ depth: number;
19
+ }
20
+ /**
21
+ * Whether a thread row is a subagent's rather than a session a person
22
+ * started: `thread_source: 'subagent'`, or a `{ subagent: … }` source.
23
+ */
24
+ export declare function isCodexSubagentThread(thread: Pick<CodexDbThread, 'source' | 'thread_source'>): boolean;
10
25
  export declare class CodexDatabase {
11
26
  private readonly dbPath;
12
27
  private sqlite3Available;
@@ -20,6 +35,10 @@ export declare class CodexDatabase {
20
35
  private queryAsync;
21
36
  private queryOne;
22
37
  getThreadsByCwd(cwd: string): CodexDbThread[];
38
+ /**
39
+ * The workspace's most recently updated thread that a person started:
40
+ * subagent threads (which share their parent's cwd) are skipped.
41
+ */
23
42
  getMostRecentThread(cwd: string): CodexDbThread | null;
24
43
  getAllDistinctCwds(): Array<{
25
44
  cwd: string;
@@ -47,6 +66,12 @@ export declare class CodexDatabase {
47
66
  * the LIKE narrows the scan and the JSON parse confirms the match.
48
67
  */
49
68
  getSpawnedChildThreads(parentId: string): CodexDbThread[];
69
+ /**
70
+ * Every subagent thread below a session, level by level (one query per
71
+ * level, in chunks of ids), oldest first within a level: its children,
72
+ * their children, and so on up to `maxDepth` levels.
73
+ */
74
+ getSpawnedDescendantThreads(rootId: string, maxDepth?: number): CodexSpawnedThread[];
50
75
  /** Get the database file's mtime (ms epoch). Returns 0 if unavailable. */
51
76
  getDbMtime(): number;
52
77
  }
@@ -37,7 +37,8 @@ var __importStar = (this && this.__importStar) || (function () {
37
37
  };
38
38
  })();
39
39
  Object.defineProperty(exports, "__esModule", { value: true });
40
- exports.CodexDatabase = exports.spawnParentFromSource = void 0;
40
+ exports.CodexDatabase = exports.DEFAULT_SUBAGENT_TREE_MAX_DEPTH = exports.spawnParentFromSource = void 0;
41
+ exports.isCodexSubagentThread = isCodexSubagentThread;
41
42
  const fs = __importStar(require("fs"));
42
43
  const path = __importStar(require("path"));
43
44
  const child_process_1 = require("child_process");
@@ -48,6 +49,17 @@ Object.defineProperty(exports, "spawnParentFromSource", { enumerable: true, get:
48
49
  * element to sqlite3, so an unbounded list would hit E2BIG on large stores.
49
50
  */
50
51
  const SQL_ID_CHUNK_SIZE = 400;
52
+ /** Default levels {@link CodexDatabase.getSpawnedDescendantThreads} follows. */
53
+ exports.DEFAULT_SUBAGENT_TREE_MAX_DEPTH = 8;
54
+ /**
55
+ * Whether a thread row is a subagent's rather than a session a person
56
+ * started: `thread_source: 'subagent'`, or a `{ subagent: … }` source.
57
+ */
58
+ function isCodexSubagentThread(thread) {
59
+ if (thread.thread_source === 'subagent')
60
+ return true;
61
+ return (0, codexRolloutOrigin_1.codexRolloutOrigin)({ source: thread.source }).isSubagent;
62
+ }
51
63
  class CodexDatabase {
52
64
  dbPath;
53
65
  sqlite3Available = null;
@@ -176,8 +188,12 @@ class CodexDatabase {
176
188
  threadCwd.startsWith(normalized + path.sep));
177
189
  });
178
190
  }
191
+ /**
192
+ * The workspace's most recently updated thread that a person started:
193
+ * subagent threads (which share their parent's cwd) are skipped.
194
+ */
179
195
  getMostRecentThread(cwd) {
180
- return this.getThreadsByCwd(cwd)[0] ?? null;
196
+ return this.getThreadsByCwd(cwd).find((thread) => !isCodexSubagentThread(thread)) ?? null;
181
197
  }
182
198
  getAllDistinctCwds() {
183
199
  return this.query('SELECT cwd, COUNT(*) as count, MAX(updated_at) as lastUpdated FROM threads GROUP BY cwd ORDER BY lastUpdated DESC');
@@ -230,6 +246,36 @@ class CodexDatabase {
230
246
  const rows = this.query('SELECT * FROM threads WHERE source LIKE ? ORDER BY created_at ASC', [`%${parentId}%`]);
231
247
  return rows.filter((row) => (0, codexRolloutOrigin_1.codexSpawnParentFromSource)(row.source) === parentId);
232
248
  }
249
+ /**
250
+ * Every subagent thread below a session, level by level (one query per
251
+ * level, in chunks of ids), oldest first within a level: its children,
252
+ * their children, and so on up to `maxDepth` levels.
253
+ */
254
+ getSpawnedDescendantThreads(rootId, maxDepth = exports.DEFAULT_SUBAGENT_TREE_MAX_DEPTH) {
255
+ if (!this.getThreadColumns().has('source'))
256
+ return [];
257
+ const found = [];
258
+ const seen = new Set([rootId]);
259
+ let level = [rootId];
260
+ for (let depth = 1; depth <= maxDepth && level.length > 0; depth++) {
261
+ const parents = new Set(level);
262
+ const next = [];
263
+ for (let i = 0; i < level.length; i += SQL_ID_CHUNK_SIZE) {
264
+ const chunk = level.slice(i, i + SQL_ID_CHUNK_SIZE);
265
+ const rows = this.query(`SELECT * FROM threads WHERE ${chunk.map(() => 'source LIKE ?').join(' OR ')} ORDER BY created_at ASC`, chunk.map((id) => `%${id}%`));
266
+ for (const row of rows) {
267
+ const parentId = (0, codexRolloutOrigin_1.codexSpawnParentFromSource)(row.source);
268
+ if (!parentId || !parents.has(parentId) || seen.has(row.id))
269
+ continue;
270
+ seen.add(row.id);
271
+ found.push({ thread: row, parentId, depth });
272
+ next.push(row.id);
273
+ }
274
+ }
275
+ level = next;
276
+ }
277
+ return found;
278
+ }
233
279
  /** Get the database file's mtime (ms epoch). Returns 0 if unavailable. */
234
280
  getDbMtime() {
235
281
  try {
@@ -11,7 +11,8 @@ import type { ListSessionFilesOptions, SessionFileInfo } from './types';
11
11
  export declare function getSessionsDirs(): string[];
12
12
  /**
13
13
  * Read the first N complete lines from a file using chunked reads.
14
- * Handles arbitrarily long lines (Codex session_meta can be 10KB+).
14
+ * Handles arbitrarily long lines (Codex session_meta can be 10KB+), and
15
+ * compressed rollouts (`.jsonl.zst`, decompressed when the runtime can).
15
16
  */
16
17
  export declare function readFirstLines(filePath: string, maxLines: number, maxBytes?: number): string[];
17
18
  /** Read session_meta from the beginning of a rollout file. */
@@ -36,12 +37,20 @@ export declare function initializeCounters(stats: ListSessionFilesOptions['stats
36
37
  export declare function cwdMatches(sessionCwd: string, workspacePath: string): boolean;
37
38
  /** Normalize a path using realpathSync. */
38
39
  export declare function normalizePath(input: string): string;
40
+ /** Options for {@link listCodexSessionFilesAsync}. */
41
+ export interface ListCodexSessionFilesOptions extends ListSessionFilesOptions {
42
+ /**
43
+ * Also list compressed rollouts (`.jsonl.zst`; default false). Do not
44
+ * reuse one `cache` across calls that differ in this option.
45
+ */
46
+ includeCompressed?: boolean;
47
+ }
39
48
  /**
40
49
  * List rollout files across the monitored Codex homes, newest first. With
41
50
  * `workspacePath`, keeps rollouts whose session_meta cwd equals, contains, or
42
51
  * sits inside it.
43
52
  */
44
- export declare function listCodexSessionFilesAsync(workspacePath?: string, options?: ListSessionFilesOptions, cache?: DirectoryListingCache): Promise<SessionFileInfo[]>;
53
+ export declare function listCodexSessionFilesAsync(workspacePath?: string, options?: ListCodexSessionFilesOptions, cache?: DirectoryListingCache): Promise<SessionFileInfo[]>;
45
54
  export interface FindCodexRolloutFileOptions {
46
55
  /**
47
56
  * Codex home whose `sessions/` subtree to search. Defaults to every
@@ -49,6 +58,11 @@ export interface FindCodexRolloutFileOptions {
49
58
  * profile homes that recorded sessions.
50
59
  */
51
60
  codexHome?: string;
61
+ /**
62
+ * Also match a compressed rollout (`.jsonl.zst`) when no `.jsonl` for the
63
+ * id exists (default false).
64
+ */
65
+ includeCompressed?: boolean;
52
66
  }
53
67
  /**
54
68
  * Locates the rollout file for a Codex session id by walking
@@ -56,15 +56,19 @@ const path = __importStar(require("path"));
56
56
  const codexPaths_1 = require("../codexPaths");
57
57
  const directoryListingCache_1 = require("./directoryListingCache");
58
58
  const rolloutWalker_1 = require("./rolloutWalker");
59
+ const compressedRollouts_1 = require("./compressedRollouts");
59
60
  /** Every directory across the monitored homes that may hold rollouts, archived ones included. */
60
61
  function getSessionsDirs() {
61
62
  return (0, codexPaths_1.getCodexSessionsDirs)();
62
63
  }
63
64
  /**
64
65
  * Read the first N complete lines from a file using chunked reads.
65
- * Handles arbitrarily long lines (Codex session_meta can be 10KB+).
66
+ * Handles arbitrarily long lines (Codex session_meta can be 10KB+), and
67
+ * compressed rollouts (`.jsonl.zst`, decompressed when the runtime can).
66
68
  */
67
69
  function readFirstLines(filePath, maxLines, maxBytes = 256 * 1024) {
70
+ if ((0, compressedRollouts_1.isCompressedRolloutPath)(filePath))
71
+ return (0, compressedRollouts_1.readRolloutFirstLines)(filePath, maxLines, maxBytes);
68
72
  const lines = [];
69
73
  let fd;
70
74
  try {
@@ -128,6 +132,14 @@ async function readFirstLineAsync(rolloutPath) {
128
132
  // `instructions` and can far exceed a single chunk.
129
133
  const maxBytes = 256 * 1024;
130
134
  const chunkSize = 64 * 1024;
135
+ if ((0, compressedRollouts_1.isCompressedRolloutPath)(rolloutPath)) {
136
+ // Compressed rollouts are complete: their first line is final.
137
+ const head = (0, compressedRollouts_1.readRolloutHead)(rolloutPath, maxBytes);
138
+ if (head.length === 0)
139
+ return { line: null, complete: false };
140
+ const firstLine = head.toString('utf8').split('\n', 1)[0]?.trim();
141
+ return { line: firstLine || null, complete: true };
142
+ }
131
143
  let handle = null;
132
144
  try {
133
145
  handle = await fs.promises.open(rolloutPath, 'r');
@@ -245,6 +257,7 @@ async function listCodexSessionFilesAsync(workspacePath, options = {}, cache = n
245
257
  cache,
246
258
  revalidateRecent: options.revalidateRecent,
247
259
  stats: options.stats,
260
+ includeCompressed: options.includeCompressed,
248
261
  });
249
262
  for (const file of files) {
250
263
  if (options.limit !== undefined && results.length >= options.limit)
@@ -281,5 +294,10 @@ function findCodexRolloutFile(sessionId, options = {}) {
281
294
  ? (0, codexPaths_1.getCodexSessionsDirs)([options.codexHome])
282
295
  : getSessionsDirs();
283
296
  // Name-only filter, one walk across the homes, newest modification first.
284
- return (0, rolloutWalker_1.walkRolloutFiles)(sessionsDirs, { sessionId: normalizedId })[0]?.path ?? null;
297
+ const matches = (0, rolloutWalker_1.walkRolloutFiles)(sessionsDirs, {
298
+ sessionId: normalizedId,
299
+ includeCompressed: options.includeCompressed,
300
+ });
301
+ // A live `.jsonl` wins over a compressed copy found in another home.
302
+ return (matches.find((file) => !(0, compressedRollouts_1.isCompressedRolloutPath)(file.path)) ?? matches[0])?.path ?? null;
285
303
  }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Codex subagent discovery for `CodexProvider.scanSubagents()`.
3
+ *
4
+ * Every spawned thread below a session (children, their children, …), each
5
+ * with its rollout, the parent's spawn call that started it, its nickname and
6
+ * agent path, and stats from the child's own records. A forked child's
7
+ * rollout embeds the parent's session_meta and history before
8
+ * `session_meta.subagent_history_start_ordinal`; that prefix is the parent's
9
+ * and is not counted (the parser skips it, and so do the timestamps here).
10
+ *
11
+ * The spawn link comes from the parent's rollout: the `item_completed` →
12
+ * `SubAgentActivity` record with `kind: 'started'` names the spawn call id
13
+ * and the child thread. When a parent has no such record (older Codex), the
14
+ * spawn call whose output named the child's agent path
15
+ * (`{"task_name":"/root/x"}`) is used. Parent scans are cached and read
16
+ * incrementally as the parent rollout grows.
17
+ *
18
+ * Node-only: reads files.
19
+ *
20
+ * @module providers/codexSubagents
21
+ */
22
+ import type { SubagentStats } from '../types/sessionEvent';
23
+ import type { CodexDatabase } from './codexDatabase';
24
+ import { type ReadRolloutOptions } from './compressedRollouts';
25
+ import type { RolloutFileInfo } from './rolloutWalker';
26
+ /** The spawn calls a rollout records, from its own (not inherited) records. */
27
+ export interface CodexSpawnLinks {
28
+ /** Child thread id → the spawn call that started it (`SubAgentActivity` `started`). */
29
+ started: Map<string, {
30
+ callId: string;
31
+ agentPath?: string;
32
+ }>;
33
+ /** Agent path → the spawn calls whose output named it, in file order. */
34
+ spawnedPaths: Map<string, string[]>;
35
+ }
36
+ /** @internal Test hook. */
37
+ export declare function clearCodexSpawnLinkCache(): void;
38
+ /**
39
+ * The spawn calls a rollout records (see {@link CodexSpawnLinks}). Cached per
40
+ * path; a growing `.jsonl` is read only past what was read before, a
41
+ * compressed one whole. `null` when the rollout cannot be read.
42
+ */
43
+ export declare function readCodexSpawnLinks(filePath: string, options?: ReadRolloutOptions): CodexSpawnLinks | null;
44
+ /**
45
+ * Stats of one subagent rollout from its own records: tool calls, tokens,
46
+ * and first/last timestamps after the embedded parent history (the child's
47
+ * own `session_meta` counts). Reads `.jsonl.zst` when the runtime can.
48
+ * `null` when the rollout is missing or cannot be read.
49
+ */
50
+ export declare function buildCodexSubagentStats(agentId: string, rolloutPath: string, description?: string | null, options?: ReadRolloutOptions): SubagentStats | null;
51
+ export interface DiscoverCodexSubagentsOptions extends ReadRolloutOptions {
52
+ /** The session's state database, when available. */
53
+ db: CodexDatabase | null;
54
+ /** Read compressed rollouts (`.jsonl.zst`) for children whose `.jsonl` is gone. */
55
+ includeCompressed: boolean;
56
+ /** The session's own rollout, or a lookup for it (needed for spawn links). */
57
+ sessionRolloutPath: () => string | null;
58
+ /** Rollouts to inspect when the database has no answer (newest first). */
59
+ fallbackFiles: () => RolloutFileInfo[];
60
+ }
61
+ /**
62
+ * Every subagent below a session with links and own-record stats. Children
63
+ * whose rollout is gone (or compressed, without `includeCompressed`) are
64
+ * left out, as before.
65
+ */
66
+ export declare function discoverCodexSubagents(sessionId: string, options: DiscoverCodexSubagentsOptions): SubagentStats[];