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.
- package/CHANGELOG.md +48 -0
- package/README.md +198 -56
- package/dist/aggregation/EventAggregator.d.ts +96 -2
- package/dist/aggregation/EventAggregator.js +535 -42
- package/dist/aggregation/snapshot.d.ts +37 -8
- package/dist/aggregation/snapshot.js +31 -9
- package/dist/aggregation/types.d.ts +38 -1
- package/dist/browser.d.ts +9 -1
- package/dist/browser.js +29 -3
- package/dist/context/sessionContext.js +8 -0
- package/dist/esm/aggregation/EventAggregator.js +535 -42
- package/dist/esm/aggregation/snapshot.js +31 -9
- package/dist/esm/browser.js +3 -0
- package/dist/esm/context/sessionContext.js +8 -0
- package/dist/esm/fileContinuity.js +149 -0
- package/dist/esm/formatters/toolSummary.js +4 -1
- package/dist/esm/index.js +7 -2
- package/dist/esm/modelCatalog.js +5 -4
- package/dist/esm/modelContext.js +4 -3
- package/dist/esm/modelInfo.js +4 -3
- package/dist/esm/node.js +3 -1
- package/dist/esm/observedContextWindows.js +2 -1
- package/dist/esm/ownKey.js +26 -0
- package/dist/esm/parsers/codexParser.js +372 -7
- package/dist/esm/parsers/openCodeParser.js +2 -1
- package/dist/esm/parsers/subagentEndState.js +61 -0
- package/dist/esm/parsers/subagentScanner.js +37 -12
- package/dist/esm/parsers/subagentTraceParser.js +103 -37
- package/dist/esm/pricingCatalog.js +3 -2
- package/dist/esm/promptHistory.js +22 -11
- package/dist/esm/providers/claudeCode.js +60 -29
- package/dist/esm/providers/codex.js +235 -201
- package/dist/esm/providers/codexCompressedReader.js +184 -0
- package/dist/esm/providers/codexDatabase.js +47 -2
- package/dist/esm/providers/codexRollouts.js +20 -2
- package/dist/esm/providers/codexSubagents.js +456 -0
- package/dist/esm/providers/compressedRollouts.js +330 -0
- package/dist/esm/providers/factory.js +4 -1
- package/dist/esm/providers/rolloutWalker.js +32 -4
- package/dist/esm/schemas/sessionEvent.js +63 -14
- package/dist/esm/sessionMonitor.js +7 -1
- package/dist/esm/sessionPreviews.js +9 -0
- package/dist/esm/sessionPromptHistory.js +5 -3
- package/dist/esm/subagentActivity.js +125 -0
- package/dist/esm/subagentDisplayState.js +81 -0
- package/dist/esm/subagentSpawn.js +363 -0
- package/dist/esm/testing/codexSubagentFixtures.js +336 -0
- package/dist/esm/toolCallOrigins.js +17 -10
- package/dist/esm/toolCategory.js +6 -2
- package/dist/esm/transcript.js +8 -0
- package/dist/esm/turns/assistantTurn.js +6 -2
- package/dist/esm/usage/claudeUsageDedupe.js +68 -0
- package/dist/esm/usage/usageEvents.js +4 -1
- package/dist/esm/watchers/eventBridge.js +45 -0
- package/dist/esm/watchers/factory.js +5 -3
- package/dist/esm/watchers/jsonlTail.js +82 -48
- package/dist/esm/watchers/jsonlWatcher.js +61 -4
- package/dist/esm/watchers/providerReaderWatcher.js +18 -4
- package/dist/esm/zaiQuota.js +2 -1
- package/dist/fileContinuity.d.ts +95 -0
- package/dist/fileContinuity.js +187 -0
- package/dist/formatters/toolSummary.js +4 -1
- package/dist/index.d.ts +19 -7
- package/dist/index.js +48 -10
- package/dist/modelCatalog.js +5 -4
- package/dist/modelContext.js +4 -3
- package/dist/modelInfo.js +4 -3
- package/dist/node.d.ts +5 -1
- package/dist/node.js +18 -2
- package/dist/observedContextWindows.js +2 -1
- package/dist/ownKey.d.ts +16 -0
- package/dist/ownKey.js +30 -0
- package/dist/parsers/codexParser.d.ts +107 -0
- package/dist/parsers/codexParser.js +373 -7
- package/dist/parsers/openCodeParser.js +2 -1
- package/dist/parsers/subagentEndState.d.ts +20 -0
- package/dist/parsers/subagentEndState.js +65 -0
- package/dist/parsers/subagentScanner.d.ts +19 -6
- package/dist/parsers/subagentScanner.js +37 -12
- package/dist/parsers/subagentTraceParser.d.ts +33 -5
- package/dist/parsers/subagentTraceParser.js +103 -37
- package/dist/pricingCatalog.js +3 -2
- package/dist/promptHistory.d.ts +13 -3
- package/dist/promptHistory.js +22 -11
- package/dist/providers/claudeCode.js +59 -28
- package/dist/providers/codex.d.ts +35 -5
- package/dist/providers/codex.js +232 -198
- package/dist/providers/codexCompressedReader.d.ts +58 -0
- package/dist/providers/codexCompressedReader.js +221 -0
- package/dist/providers/codexDatabase.d.ts +25 -0
- package/dist/providers/codexDatabase.js +48 -2
- package/dist/providers/codexRollouts.d.ts +16 -2
- package/dist/providers/codexRollouts.js +20 -2
- package/dist/providers/codexSubagents.d.ts +66 -0
- package/dist/providers/codexSubagents.js +495 -0
- package/dist/providers/compressedRollouts.d.ts +114 -0
- package/dist/providers/compressedRollouts.js +380 -0
- package/dist/providers/factory.d.ts +5 -0
- package/dist/providers/factory.js +4 -1
- package/dist/providers/rolloutWalker.d.ts +15 -2
- package/dist/providers/rolloutWalker.js +33 -4
- package/dist/providers/types.d.ts +36 -1
- package/dist/schemas/index.d.ts +1 -0
- package/dist/schemas/sessionEvent.d.ts +51 -10
- package/dist/schemas/sessionEvent.js +63 -14
- package/dist/sessionMonitor.d.ts +6 -0
- package/dist/sessionMonitor.js +7 -1
- package/dist/sessionPreviews.js +9 -0
- package/dist/sessionPromptHistory.d.ts +5 -0
- package/dist/sessionPromptHistory.js +5 -36
- package/dist/subagentActivity.d.ts +91 -0
- package/dist/subagentActivity.js +132 -0
- package/dist/subagentDisplayState.d.ts +81 -0
- package/dist/subagentDisplayState.js +86 -0
- package/dist/subagentSpawn.d.ts +175 -0
- package/dist/subagentSpawn.js +385 -0
- package/dist/testing/codexSubagentFixtures.d.ts +48 -0
- package/dist/testing/codexSubagentFixtures.js +344 -0
- package/dist/toolCallOrigins.d.ts +5 -0
- package/dist/toolCallOrigins.js +17 -10
- package/dist/toolCategory.js +6 -2
- package/dist/transcript.js +8 -0
- package/dist/turns/assistantTurn.d.ts +4 -0
- package/dist/turns/assistantTurn.js +6 -2
- package/dist/types/codex.d.ts +71 -2
- package/dist/types/sessionEvent.d.ts +38 -3
- package/dist/usage/claudeUsageDedupe.d.ts +31 -0
- package/dist/usage/claudeUsageDedupe.js +68 -0
- package/dist/usage/usageEvents.d.ts +4 -1
- package/dist/usage/usageEvents.js +4 -1
- package/dist/watchers/eventBridge.js +45 -0
- package/dist/watchers/factory.d.ts +6 -0
- package/dist/watchers/factory.js +5 -3
- package/dist/watchers/jsonlTail.d.ts +27 -4
- package/dist/watchers/jsonlTail.js +84 -48
- package/dist/watchers/jsonlWatcher.d.ts +22 -1
- package/dist/watchers/jsonlWatcher.js +60 -3
- package/dist/watchers/providerReaderWatcher.d.ts +7 -0
- package/dist/watchers/providerReaderWatcher.js +18 -4
- package/dist/watchers/types.d.ts +12 -0
- package/dist/zaiQuota.js +2 -1
- package/package.json +1 -1
|
@@ -85,7 +85,7 @@ export interface SessionEventBase {
|
|
|
85
85
|
/** Provider-reported git branch for this event/session. */
|
|
86
86
|
gitBranch?: string;
|
|
87
87
|
/** Permission mode active when this event occurred */
|
|
88
|
-
permissionMode?:
|
|
88
|
+
permissionMode?: PermissionMode;
|
|
89
89
|
/** Subscription rate limits, when provided by the session source. */
|
|
90
90
|
rateLimits?: {
|
|
91
91
|
primary?: {
|
|
@@ -166,6 +166,11 @@ export interface SystemSessionEvent extends SessionEventBase {
|
|
|
166
166
|
type: 'system';
|
|
167
167
|
/** Bookkeeping-only system rows legitimately have no message. */
|
|
168
168
|
message?: SessionMessage;
|
|
169
|
+
/**
|
|
170
|
+
* The provider's system-line subtype, when it wrote one (Claude Code:
|
|
171
|
+
* `turn_duration`, `compact_boundary`, `agents_killed`, …).
|
|
172
|
+
*/
|
|
173
|
+
subtype?: string;
|
|
169
174
|
}
|
|
170
175
|
export interface ToolUseSessionEvent extends SessionEventBase {
|
|
171
176
|
type: 'tool_use';
|
|
@@ -187,9 +192,11 @@ export interface ToolResultSessionEvent extends SessionEventBase {
|
|
|
187
192
|
/** Canonical provider-neutral event contract. */
|
|
188
193
|
export type SessionEvent = UserSessionEvent | AssistantSessionEvent | SummarySessionEvent | SystemSessionEvent | ToolUseSessionEvent | ToolResultSessionEvent;
|
|
189
194
|
/**
|
|
190
|
-
* Permission mode values from Claude Code JSONL.
|
|
195
|
+
* Permission mode values from Claude Code JSONL. `auto` approves or denies
|
|
196
|
+
* permission prompts with a model classifier; `dontAsk` denies anything not
|
|
197
|
+
* pre-approved. Unknown future modes are dropped by the schema, not rejected.
|
|
191
198
|
*/
|
|
192
|
-
export type PermissionMode = 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan';
|
|
199
|
+
export type PermissionMode = 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan' | 'auto' | 'dontAsk';
|
|
193
200
|
/**
|
|
194
201
|
* A permission mode change event in the session timeline.
|
|
195
202
|
*/
|
|
@@ -423,6 +430,34 @@ export interface SubagentStats {
|
|
|
423
430
|
endTime?: Date;
|
|
424
431
|
/** Duration in milliseconds */
|
|
425
432
|
durationMs?: number;
|
|
433
|
+
/**
|
|
434
|
+
* Codex: the subagent's rollout file — the `.jsonl`, or the `.jsonl.zst`
|
|
435
|
+
* when only the compressed one exists (with `includeCompressedRollouts`).
|
|
436
|
+
*/
|
|
437
|
+
filePath?: string;
|
|
438
|
+
/**
|
|
439
|
+
* Codex: the parent's spawn call (`spawn_agent` call id) that started this
|
|
440
|
+
* subagent — the tool-use id of that call in the parent's events.
|
|
441
|
+
*/
|
|
442
|
+
spawnToolUseId?: string;
|
|
443
|
+
/** Codex: the display nickname Codex gave the subagent (e.g. `Banach`). */
|
|
444
|
+
agentNickname?: string;
|
|
445
|
+
/** Codex: the subagent's agent path (e.g. `/root/review_builder`). */
|
|
446
|
+
agentPath?: string;
|
|
447
|
+
/**
|
|
448
|
+
* Codex: the thread that spawned this subagent — the session's id for its
|
|
449
|
+
* own children, another subagent's `agentId` below that.
|
|
450
|
+
*/
|
|
451
|
+
parentAgentId?: string;
|
|
452
|
+
/** Codex: spawn depth — 1 for the session's own children, 2 for theirs, … */
|
|
453
|
+
depth?: number;
|
|
454
|
+
/**
|
|
455
|
+
* The subagent's transcript ended cleanly: Claude Code, its last assistant
|
|
456
|
+
* turn stopped with `end_turn` / `stop_sequence` and left no tool call
|
|
457
|
+
* unanswered; Codex, its last own turn ended with `task_complete`. A later
|
|
458
|
+
* prompt (a resumed agent) clears it. Absent when unknown.
|
|
459
|
+
*/
|
|
460
|
+
endedCleanly?: boolean;
|
|
426
461
|
}
|
|
427
462
|
/**
|
|
428
463
|
* Pending user request awaiting assistant response.
|
|
@@ -22,6 +22,24 @@ import type { SessionEvent } from '../types/sessionEvent';
|
|
|
22
22
|
export declare const SYNTHETIC_MODEL = "<synthetic>";
|
|
23
23
|
/** Attach Anthropic-semantics normalized usage and Claude Code provenance to an event. */
|
|
24
24
|
export declare function normalizeClaudeUsage(event: SessionEvent): SessionEvent;
|
|
25
|
+
/** Tokens a {@link ClaudeUsageDeduper} has already counted for one message id. */
|
|
26
|
+
export interface ClaudeUsageDedupeBuckets {
|
|
27
|
+
input: number;
|
|
28
|
+
cacheRead: number;
|
|
29
|
+
cacheWrite: number;
|
|
30
|
+
output: number;
|
|
31
|
+
reasoning: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* JSON-serializable state of a {@link ClaudeUsageDeduper}, from
|
|
35
|
+
* `exportState()`, for `importState()` in a reader resumed at the same
|
|
36
|
+
* position (for example from a persisted session snapshot).
|
|
37
|
+
*/
|
|
38
|
+
export interface ClaudeUsageDedupeState {
|
|
39
|
+
version: 1;
|
|
40
|
+
/** `[message id, counted tokens]` pairs, oldest first. */
|
|
41
|
+
entries: Array<[string, ClaudeUsageDedupeBuckets]>;
|
|
42
|
+
}
|
|
25
43
|
/**
|
|
26
44
|
* Per-reader dedupe state. Split lines of one response are adjacent, so a
|
|
27
45
|
* bounded map (oldest keys evicted first) is enough.
|
|
@@ -40,6 +58,19 @@ export declare class ClaudeUsageDeduper {
|
|
|
40
58
|
/** Whether a message id has already been counted by this deduper. */
|
|
41
59
|
has(id: string): boolean;
|
|
42
60
|
reset(): void;
|
|
61
|
+
/**
|
|
62
|
+
* The most recent `limit` message ids and the tokens counted for each,
|
|
63
|
+
* oldest first. Split lines of one response are adjacent, so a reader
|
|
64
|
+
* resumed at the position this was taken at needs only the last few.
|
|
65
|
+
*/
|
|
66
|
+
exportState(limit?: number): ClaudeUsageDedupeState;
|
|
67
|
+
/**
|
|
68
|
+
* Replaces this deduper's state with one from {@link exportState}. Returns
|
|
69
|
+
* false, changing nothing, when `state` is not a valid version-1 state
|
|
70
|
+
* (wrong shape, empty ids, or counts that are not finite non-negative
|
|
71
|
+
* numbers). Entries beyond this deduper's capacity drop oldest first.
|
|
72
|
+
*/
|
|
73
|
+
importState(state: unknown): boolean;
|
|
43
74
|
private remember;
|
|
44
75
|
}
|
|
45
76
|
/**
|
|
@@ -64,6 +64,25 @@ function normalizeClaudeUsage(event) {
|
|
|
64
64
|
},
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
|
+
const BUCKET_KEYS = ['input', 'cacheRead', 'cacheWrite', 'output', 'reasoning'];
|
|
68
|
+
/** Entries {@link ClaudeUsageDeduper.exportState} keeps by default. */
|
|
69
|
+
const DEFAULT_EXPORT_LIMIT = 256;
|
|
70
|
+
function isCount(value) {
|
|
71
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
|
72
|
+
}
|
|
73
|
+
function bucketsFrom(value) {
|
|
74
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
75
|
+
return null;
|
|
76
|
+
const record = value;
|
|
77
|
+
const buckets = {};
|
|
78
|
+
for (const key of BUCKET_KEYS) {
|
|
79
|
+
const count = record[key];
|
|
80
|
+
if (!isCount(count))
|
|
81
|
+
return null;
|
|
82
|
+
buckets[key] = count;
|
|
83
|
+
}
|
|
84
|
+
return buckets;
|
|
85
|
+
}
|
|
67
86
|
function bucketsOf(usage) {
|
|
68
87
|
return {
|
|
69
88
|
input: usage.uncachedInputTokens,
|
|
@@ -168,6 +187,55 @@ class ClaudeUsageDeduper {
|
|
|
168
187
|
reset() {
|
|
169
188
|
this.seen.clear();
|
|
170
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* The most recent `limit` message ids and the tokens counted for each,
|
|
192
|
+
* oldest first. Split lines of one response are adjacent, so a reader
|
|
193
|
+
* resumed at the position this was taken at needs only the last few.
|
|
194
|
+
*/
|
|
195
|
+
exportState(limit = DEFAULT_EXPORT_LIMIT) {
|
|
196
|
+
const count = limit >= 0 ? Math.min(this.seen.size, Math.floor(limit)) : 0;
|
|
197
|
+
const entries = [];
|
|
198
|
+
if (count > 0) {
|
|
199
|
+
let skip = this.seen.size - count;
|
|
200
|
+
for (const [id, buckets] of this.seen) {
|
|
201
|
+
if (skip > 0) {
|
|
202
|
+
skip -= 1;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
entries.push([id, { ...buckets }]);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return { version: 1, entries };
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Replaces this deduper's state with one from {@link exportState}. Returns
|
|
212
|
+
* false, changing nothing, when `state` is not a valid version-1 state
|
|
213
|
+
* (wrong shape, empty ids, or counts that are not finite non-negative
|
|
214
|
+
* numbers). Entries beyond this deduper's capacity drop oldest first.
|
|
215
|
+
*/
|
|
216
|
+
importState(state) {
|
|
217
|
+
if (!state || typeof state !== 'object')
|
|
218
|
+
return false;
|
|
219
|
+
const { version, entries } = state;
|
|
220
|
+
if (version !== 1 || !Array.isArray(entries))
|
|
221
|
+
return false;
|
|
222
|
+
const parsed = [];
|
|
223
|
+
for (const entry of entries) {
|
|
224
|
+
if (!Array.isArray(entry) || entry.length !== 2)
|
|
225
|
+
return false;
|
|
226
|
+
const [id, value] = entry;
|
|
227
|
+
if (typeof id !== 'string' || id.length === 0)
|
|
228
|
+
return false;
|
|
229
|
+
const buckets = bucketsFrom(value);
|
|
230
|
+
if (!buckets)
|
|
231
|
+
return false;
|
|
232
|
+
parsed.push([id, buckets]);
|
|
233
|
+
}
|
|
234
|
+
this.seen.clear();
|
|
235
|
+
for (const [id, buckets] of parsed)
|
|
236
|
+
this.remember(id, buckets);
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
171
239
|
remember(id, buckets) {
|
|
172
240
|
this.seen.delete(id);
|
|
173
241
|
this.seen.set(id, buckets);
|
|
@@ -15,7 +15,10 @@
|
|
|
15
15
|
* tagged with the subagent, and their sizes and mtimes join the fingerprint.
|
|
16
16
|
* (Codex spawned subagents and forks are top-level rollouts, already listed
|
|
17
17
|
* as sessions of their own; their events are tagged with the parent read
|
|
18
|
-
* from the rollout's first line
|
|
18
|
+
* from the rollout's first line; the parent history a forked child embeds
|
|
19
|
+
* is not counted (it carries no token usage). Compressed rollouts
|
|
20
|
+
* (`….jsonl.zst`) are read when a Codex provider was created with
|
|
21
|
+
* `includeCompressedRollouts`.)
|
|
19
22
|
*
|
|
20
23
|
* Each event also carries the ordinal of the human prompt it answered, from
|
|
21
24
|
* the same scanner as `collectPromptHistory()`, so usage joins prompt history
|
|
@@ -16,7 +16,10 @@
|
|
|
16
16
|
* tagged with the subagent, and their sizes and mtimes join the fingerprint.
|
|
17
17
|
* (Codex spawned subagents and forks are top-level rollouts, already listed
|
|
18
18
|
* as sessions of their own; their events are tagged with the parent read
|
|
19
|
-
* from the rollout's first line
|
|
19
|
+
* from the rollout's first line; the parent history a forked child embeds
|
|
20
|
+
* is not counted (it carries no token usage). Compressed rollouts
|
|
21
|
+
* (`….jsonl.zst`) are read when a Codex provider was created with
|
|
22
|
+
* `includeCompressedRollouts`.)
|
|
20
23
|
*
|
|
21
24
|
* Each event also carries the ordinal of the human prompt it answered, from
|
|
22
25
|
* the same scanner as `collectPromptHistory()`, so usage joins prompt history
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.toFollowEvents = toFollowEvents;
|
|
13
13
|
const toolSummary_1 = require("../formatters/toolSummary");
|
|
14
|
+
const subagentSpawn_1 = require("../subagentSpawn");
|
|
15
|
+
const subagentActivity_1 = require("../subagentActivity");
|
|
14
16
|
/**
|
|
15
17
|
* Converts a single SessionEvent to one or more FollowEvents.
|
|
16
18
|
*
|
|
@@ -151,6 +153,49 @@ function toFollowEvents(event, providerId) {
|
|
|
151
153
|
break;
|
|
152
154
|
}
|
|
153
155
|
case 'system': {
|
|
156
|
+
// A background task's notice (opt-in reader event): its summary, not the raw block.
|
|
157
|
+
const notification = (0, subagentSpawn_1.isTaskNotificationSystemEvent)(event)
|
|
158
|
+
? (0, subagentSpawn_1.taskNotificationFromEvent)(event)
|
|
159
|
+
: null;
|
|
160
|
+
if (notification) {
|
|
161
|
+
events.push({
|
|
162
|
+
providerId,
|
|
163
|
+
type: 'system',
|
|
164
|
+
timestamp: ts,
|
|
165
|
+
summary: notification.summary ?? `Background task ${notification.status ?? 'finished'}`,
|
|
166
|
+
raw: event,
|
|
167
|
+
});
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
// The user stopping background agents, or a background agent's final status.
|
|
171
|
+
const stopped = (0, subagentSpawn_1.agentsStoppedNoticeFromEvent)(event);
|
|
172
|
+
const taskStatus = stopped ? null : (0, subagentSpawn_1.taskStatusFromEvent)(event);
|
|
173
|
+
if (stopped || taskStatus || (0, subagentSpawn_1.isAgentsKilledSystemEvent)(event)) {
|
|
174
|
+
events.push({
|
|
175
|
+
providerId,
|
|
176
|
+
type: 'system',
|
|
177
|
+
timestamp: ts,
|
|
178
|
+
summary: stopped
|
|
179
|
+
? (0, subagentSpawn_1.describeAgentsStopped)(stopped)
|
|
180
|
+
: taskStatus
|
|
181
|
+
? (0, subagentSpawn_1.describeTaskStatus)(taskStatus)
|
|
182
|
+
: (0, subagentSpawn_1.describeAgentsStopped)(null),
|
|
183
|
+
raw: event,
|
|
184
|
+
});
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
// A subagent lifecycle record (opt-in Codex parser event): a content-free summary.
|
|
188
|
+
const activity = (0, subagentActivity_1.subagentActivityFromEvent)(event);
|
|
189
|
+
if (activity) {
|
|
190
|
+
events.push({
|
|
191
|
+
providerId,
|
|
192
|
+
type: 'system',
|
|
193
|
+
timestamp: ts,
|
|
194
|
+
summary: (0, subagentActivity_1.describeSubagentActivity)(activity),
|
|
195
|
+
raw: event,
|
|
196
|
+
});
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
154
199
|
const text = extractTextContent(event.message?.content);
|
|
155
200
|
const label = event.message?.sourceLabel || event.message?.role || 'system';
|
|
156
201
|
const summary = text ||
|
|
@@ -8,6 +8,12 @@ export interface CreateWatcherOptions {
|
|
|
8
8
|
workspacePath: string;
|
|
9
9
|
sessionId?: string;
|
|
10
10
|
callbacks: SessionWatcherCallbacks;
|
|
11
|
+
/**
|
|
12
|
+
* Claude Code only: also emit a `system` event for each mid-turn
|
|
13
|
+
* task-notification attachment, so an `EventAggregator` fed by this watcher
|
|
14
|
+
* can finish background subagents. Default `false`.
|
|
15
|
+
*/
|
|
16
|
+
includeTaskNotifications?: boolean;
|
|
11
17
|
}
|
|
12
18
|
/**
|
|
13
19
|
* Resolve the session a command should act on: an exact or unique-prefix
|
package/dist/watchers/factory.js
CHANGED
|
@@ -78,13 +78,15 @@ function resolveSessionPath(provider, workspacePath, sessionId) {
|
|
|
78
78
|
function createWatcher(options) {
|
|
79
79
|
const { provider, workspacePath, sessionId, callbacks } = options;
|
|
80
80
|
const sessionPath = resolveSessionPath(provider, workspacePath, sessionId);
|
|
81
|
-
const watcher = createWatcherForProvider(provider, sessionPath, callbacks
|
|
81
|
+
const watcher = createWatcherForProvider(provider, sessionPath, callbacks, {
|
|
82
|
+
includeTaskNotifications: options.includeTaskNotifications === true,
|
|
83
|
+
});
|
|
82
84
|
return { watcher, sessionPath };
|
|
83
85
|
}
|
|
84
|
-
function createWatcherForProvider(provider, sessionPath, callbacks) {
|
|
86
|
+
function createWatcherForProvider(provider, sessionPath, callbacks, jsonlOptions) {
|
|
85
87
|
switch (provider.id) {
|
|
86
88
|
case 'claude-code':
|
|
87
|
-
return new jsonlWatcher_1.JsonlSessionWatcher(provider.id, sessionPath, callbacks);
|
|
89
|
+
return new jsonlWatcher_1.JsonlSessionWatcher(provider.id, sessionPath, callbacks, jsonlOptions);
|
|
88
90
|
case 'codex':
|
|
89
91
|
return new providerReaderWatcher_1.ProviderReaderSessionWatcher(provider, sessionPath, callbacks);
|
|
90
92
|
case 'opencode': {
|
|
@@ -53,10 +53,11 @@ export interface JsonlTailReadResult<T> extends JsonlTailBatch {
|
|
|
53
53
|
reset?: JsonlTailReset;
|
|
54
54
|
}
|
|
55
55
|
/**
|
|
56
|
-
* A JSON-serializable resume point from {@link JsonlTail.checkpoint}
|
|
57
|
-
* identifies the file by device and inode
|
|
58
|
-
* so a new tail (in this or a later
|
|
59
|
-
*
|
|
56
|
+
* A JSON-serializable resume point from {@link JsonlTail.checkpoint} or
|
|
57
|
+
* {@link captureJsonlCheckpoint}. It identifies the file by device and inode
|
|
58
|
+
* plus a digest of its first bytes, so a new tail (in this or a later
|
|
59
|
+
* process) or {@link verifyJsonlCheckpoint} can confirm the file is still the
|
|
60
|
+
* same, append-only one before anything continues from `offset`.
|
|
60
61
|
*/
|
|
61
62
|
export interface JsonlTailCheckpoint {
|
|
62
63
|
version: typeof JSONL_TAIL_CHECKPOINT_VERSION;
|
|
@@ -151,3 +152,25 @@ export interface JsonlTail<T = unknown> {
|
|
|
151
152
|
resumeFrom(checkpoint: unknown): boolean;
|
|
152
153
|
}
|
|
153
154
|
export declare function createJsonlTail<T>(options: JsonlTailOptions<T>): JsonlTail<T>;
|
|
155
|
+
/**
|
|
156
|
+
* Records a {@link JsonlTailCheckpoint} for byte `offset` of `filePath`
|
|
157
|
+
* without a tail, for readers that keep their own offset (a snapshot that
|
|
158
|
+
* stores a reader position, for example). The checkpoint carries the file's
|
|
159
|
+
* current device and inode and a digest of its first bytes, so
|
|
160
|
+
* {@link verifyJsonlCheckpoint} (or {@link JsonlTail.resumeFrom}) can later
|
|
161
|
+
* tell whether `offset` still refers to the same, append-only content.
|
|
162
|
+
*
|
|
163
|
+
* Returns `null` when the file is missing or unreadable, or when `offset` is
|
|
164
|
+
* not a resume-safe position in it: negative, past the end, or not 0 and not
|
|
165
|
+
* just after a newline. Follows symbolic links.
|
|
166
|
+
*/
|
|
167
|
+
export declare function captureJsonlCheckpoint(filePath: string, offset: number): JsonlTailCheckpoint | null;
|
|
168
|
+
/**
|
|
169
|
+
* Whether `checkpoint` (any value, typically parsed from JSON) is a valid
|
|
170
|
+
* {@link JsonlTailCheckpoint} that still describes `filePath`: same device
|
|
171
|
+
* and inode, the recorded first bytes unchanged, and its offset within the
|
|
172
|
+
* file and just after a newline (or 0). This is the check
|
|
173
|
+
* {@link JsonlTail.resumeFrom} applies. `false` on any error, including a
|
|
174
|
+
* missing file. Follows symbolic links.
|
|
175
|
+
*/
|
|
176
|
+
export declare function verifyJsonlCheckpoint(filePath: string, checkpoint: unknown): boolean;
|
|
@@ -39,8 +39,11 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
39
39
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
40
|
exports.JsonlTailSymlinkError = exports.JSONL_TAIL_CHECKPOINT_VERSION = exports.DEFAULT_JSONL_TAIL_MAX_LINE_BYTES = exports.DEFAULT_JSONL_TAIL_READ_BYTES = void 0;
|
|
41
41
|
exports.createJsonlTail = createJsonlTail;
|
|
42
|
+
exports.captureJsonlCheckpoint = captureJsonlCheckpoint;
|
|
43
|
+
exports.verifyJsonlCheckpoint = verifyJsonlCheckpoint;
|
|
42
44
|
const fs = __importStar(require("fs"));
|
|
43
45
|
const fileHead_1 = require("../fileHead");
|
|
46
|
+
const fileContinuity_1 = require("../fileContinuity");
|
|
44
47
|
const jsonl_1 = require("../parsers/jsonl");
|
|
45
48
|
const DEFAULT_DEBOUNCE_MS = 100;
|
|
46
49
|
const DEFAULT_CATCHUP_INTERVAL_MS = 30_000;
|
|
@@ -98,6 +101,63 @@ exports.JsonlTailSymlinkError = JsonlTailSymlinkError;
|
|
|
98
101
|
function createJsonlTail(options) {
|
|
99
102
|
return new JsonlTailReader(options);
|
|
100
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Records a {@link JsonlTailCheckpoint} for byte `offset` of `filePath`
|
|
106
|
+
* without a tail, for readers that keep their own offset (a snapshot that
|
|
107
|
+
* stores a reader position, for example). The checkpoint carries the file's
|
|
108
|
+
* current device and inode and a digest of its first bytes, so
|
|
109
|
+
* {@link verifyJsonlCheckpoint} (or {@link JsonlTail.resumeFrom}) can later
|
|
110
|
+
* tell whether `offset` still refers to the same, append-only content.
|
|
111
|
+
*
|
|
112
|
+
* Returns `null` when the file is missing or unreadable, or when `offset` is
|
|
113
|
+
* not a resume-safe position in it: negative, past the end, or not 0 and not
|
|
114
|
+
* just after a newline. Follows symbolic links.
|
|
115
|
+
*/
|
|
116
|
+
function captureJsonlCheckpoint(filePath, offset) {
|
|
117
|
+
if (!Number.isSafeInteger(offset) || offset < 0)
|
|
118
|
+
return null;
|
|
119
|
+
let fd = null;
|
|
120
|
+
try {
|
|
121
|
+
fd = fs.openSync(filePath, 'r');
|
|
122
|
+
const stat = fs.fstatSync(fd);
|
|
123
|
+
if (offset > stat.size)
|
|
124
|
+
return null;
|
|
125
|
+
if (offset > 0 && readAt(fd, offset - 1, 1)[0] !== 0x0a)
|
|
126
|
+
return null;
|
|
127
|
+
return checkpointOf(offset, stat, (0, fileHead_1.fileHeadOf)(readPrefix(fd, stat)));
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
closeQuietly(fd);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Whether `checkpoint` (any value, typically parsed from JSON) is a valid
|
|
138
|
+
* {@link JsonlTailCheckpoint} that still describes `filePath`: same device
|
|
139
|
+
* and inode, the recorded first bytes unchanged, and its offset within the
|
|
140
|
+
* file and just after a newline (or 0). This is the check
|
|
141
|
+
* {@link JsonlTail.resumeFrom} applies. `false` on any error, including a
|
|
142
|
+
* missing file. Follows symbolic links.
|
|
143
|
+
*/
|
|
144
|
+
function verifyJsonlCheckpoint(filePath, checkpoint) {
|
|
145
|
+
const parsed = parseCheckpoint(checkpoint);
|
|
146
|
+
if (parsed === null)
|
|
147
|
+
return false;
|
|
148
|
+
let fd = null;
|
|
149
|
+
try {
|
|
150
|
+
fd = fs.openSync(filePath, 'r');
|
|
151
|
+
const stat = fs.fstatSync(fd);
|
|
152
|
+
return checkpointMatches(parsed, fd, stat, readPrefix(fd, stat));
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
closeQuietly(fd);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
101
161
|
class JsonlTailReader {
|
|
102
162
|
options;
|
|
103
163
|
active = false;
|
|
@@ -109,10 +169,8 @@ class JsonlTailReader {
|
|
|
109
169
|
/** Inside a line over `maxLineBytes`: drop bytes until its newline. */
|
|
110
170
|
skippingLine = false;
|
|
111
171
|
currentLine = { offset: 0, length: 0 };
|
|
112
|
-
/** Identity and prefix of the file the offsets refer to, once read. */
|
|
113
|
-
|
|
114
|
-
head = null;
|
|
115
|
-
lastMtimeMs = -1;
|
|
172
|
+
/** Identity, mtime, and prefix of the file the offsets refer to, once read. */
|
|
173
|
+
continuity = new fileContinuity_1.FileContinuity();
|
|
116
174
|
maxReadBytes;
|
|
117
175
|
maxLineBytes;
|
|
118
176
|
rewritePolicy;
|
|
@@ -320,21 +378,10 @@ class JsonlTailReader {
|
|
|
320
378
|
}
|
|
321
379
|
/** Whether the continuity check needs bytes from the file. */
|
|
322
380
|
probeNeeded(stat) {
|
|
323
|
-
return (this.
|
|
324
|
-
stat.dev !== this.identity.dev ||
|
|
325
|
-
stat.ino !== this.identity.ino ||
|
|
326
|
-
stat.size < this.readOffset ||
|
|
327
|
-
stat.mtimeMs !== this.lastMtimeMs ||
|
|
328
|
-
this.head === null ||
|
|
329
|
-
this.head.bytes < Math.min(fileHead_1.HEAD_BYTES, stat.size));
|
|
381
|
+
return this.continuity.needsProbe(stat, this.readOffset);
|
|
330
382
|
}
|
|
331
383
|
probeSync(fd, stat) {
|
|
332
|
-
return
|
|
333
|
-
prefix: readAt(fd, 0, Math.min(fileHead_1.HEAD_BYTES, stat.size)),
|
|
334
|
-
before: this.committedOffset > 0 && this.committedOffset <= stat.size
|
|
335
|
-
? readAt(fd, this.committedOffset - 1, 1)
|
|
336
|
-
: null,
|
|
337
|
-
};
|
|
384
|
+
return (0, fileContinuity_1.probeFileContinuity)(fd, stat, this.committedOffset);
|
|
338
385
|
}
|
|
339
386
|
async probeAsync(handle, stat) {
|
|
340
387
|
const committedOffset = this.committedOffset;
|
|
@@ -351,31 +398,16 @@ class JsonlTailReader {
|
|
|
351
398
|
* newline. `null` when it can continue.
|
|
352
399
|
*/
|
|
353
400
|
resetReason(stat, probe) {
|
|
354
|
-
|
|
355
|
-
return 'replaced';
|
|
356
|
-
}
|
|
357
|
-
if (stat.size < this.readOffset)
|
|
358
|
-
return 'truncated';
|
|
359
|
-
if (this.identity && stat.mtimeMs !== this.lastMtimeMs && probe) {
|
|
360
|
-
if (this.head && !(0, fileHead_1.headMatches)(this.head, probe.prefix))
|
|
361
|
-
return 'rewritten';
|
|
362
|
-
if (probe.before && probe.before[0] !== 0x0a)
|
|
363
|
-
return 'rewritten';
|
|
364
|
-
}
|
|
365
|
-
return null;
|
|
401
|
+
return this.continuity.reasonFor(stat, this.readOffset, probe);
|
|
366
402
|
}
|
|
367
403
|
/** Records the identity and prefix of the file the offsets now refer to. */
|
|
368
404
|
adoptFile(stat, probe) {
|
|
369
|
-
this.
|
|
370
|
-
this.lastMtimeMs = stat.mtimeMs;
|
|
371
|
-
if (probe && (!this.head || this.head.bytes < Math.min(fileHead_1.HEAD_BYTES, stat.size))) {
|
|
372
|
-
this.head = (0, fileHead_1.fileHeadOf)(probe.prefix);
|
|
373
|
-
}
|
|
405
|
+
this.continuity.record(stat, probe);
|
|
374
406
|
}
|
|
375
407
|
reset(reason, offset) {
|
|
376
408
|
const previousOffset = this.committedOffset;
|
|
377
409
|
this.position(offset);
|
|
378
|
-
this.
|
|
410
|
+
this.continuity.discardHead();
|
|
379
411
|
if (reason === 'replaced' && this.active) {
|
|
380
412
|
// fs.watch follows the old inode; watch the file now at the path.
|
|
381
413
|
this.fsWatcher?.close();
|
|
@@ -450,12 +482,7 @@ class JsonlTailReader {
|
|
|
450
482
|
this.position(Math.max(0, offset));
|
|
451
483
|
}
|
|
452
484
|
checkpoint() {
|
|
453
|
-
return
|
|
454
|
-
version: exports.JSONL_TAIL_CHECKPOINT_VERSION,
|
|
455
|
-
offset: this.committedOffset,
|
|
456
|
-
identity: this.identity ? { dev: this.identity.dev, ino: this.identity.ino } : null,
|
|
457
|
-
prefixDigest: this.head ? { bytes: this.head.bytes, hash: this.head.hash } : null,
|
|
458
|
-
};
|
|
485
|
+
return checkpointOf(this.committedOffset, this.continuity.knownIdentity, this.continuity.knownHead);
|
|
459
486
|
}
|
|
460
487
|
resumeFrom(checkpoint) {
|
|
461
488
|
this.generation += 1;
|
|
@@ -465,7 +492,7 @@ class JsonlTailReader {
|
|
|
465
492
|
try {
|
|
466
493
|
fd = this.openSync();
|
|
467
494
|
const stat = fs.fstatSync(fd);
|
|
468
|
-
const prefix =
|
|
495
|
+
const prefix = readPrefix(fd, stat);
|
|
469
496
|
const matches = parsed !== null && checkpointMatches(parsed, fd, stat, prefix);
|
|
470
497
|
const offset = matches
|
|
471
498
|
? parsed.offset
|
|
@@ -473,16 +500,12 @@ class JsonlTailReader {
|
|
|
473
500
|
? lastCompleteLineOffset(fd, stat.size)
|
|
474
501
|
: 0;
|
|
475
502
|
this.position(offset);
|
|
476
|
-
this.
|
|
477
|
-
this.lastMtimeMs = stat.mtimeMs;
|
|
478
|
-
this.head = (0, fileHead_1.fileHeadOf)(prefix);
|
|
503
|
+
this.continuity.adoptPrefix(stat, prefix);
|
|
479
504
|
return matches;
|
|
480
505
|
}
|
|
481
506
|
catch (error) {
|
|
482
507
|
this.position(0);
|
|
483
|
-
this.
|
|
484
|
-
this.head = null;
|
|
485
|
-
this.lastMtimeMs = -1;
|
|
508
|
+
this.continuity.forget();
|
|
486
509
|
this.options.onError?.(toError(error));
|
|
487
510
|
return false;
|
|
488
511
|
}
|
|
@@ -597,6 +620,15 @@ function parseCheckpoint(value) {
|
|
|
597
620
|
prefixDigest: parsedDigest,
|
|
598
621
|
};
|
|
599
622
|
}
|
|
623
|
+
/** A fresh checkpoint for `offset` in the file with `identity` and recorded `head`. */
|
|
624
|
+
function checkpointOf(offset, identity, head) {
|
|
625
|
+
return {
|
|
626
|
+
version: exports.JSONL_TAIL_CHECKPOINT_VERSION,
|
|
627
|
+
offset,
|
|
628
|
+
identity: identity ? { dev: identity.dev, ino: identity.ino } : null,
|
|
629
|
+
prefixDigest: head ? { bytes: head.bytes, hash: head.hash } : null,
|
|
630
|
+
};
|
|
631
|
+
}
|
|
600
632
|
/** Whether `checkpoint` still describes the open file. */
|
|
601
633
|
function checkpointMatches(checkpoint, fd, stat, prefix) {
|
|
602
634
|
const { offset, identity, prefixDigest } = checkpoint;
|
|
@@ -639,6 +671,10 @@ function readAt(fd, position, length) {
|
|
|
639
671
|
const count = length > 0 ? fs.readSync(fd, buffer, 0, length, position) : 0;
|
|
640
672
|
return buffer.subarray(0, count);
|
|
641
673
|
}
|
|
674
|
+
/** The file's first `min(HEAD_BYTES, size)` bytes: what a checkpoint digests. */
|
|
675
|
+
function readPrefix(fd, stat) {
|
|
676
|
+
return readAt(fd, 0, Math.min(fileHead_1.HEAD_BYTES, stat.size));
|
|
677
|
+
}
|
|
642
678
|
async function readAtAsync(handle, position, length) {
|
|
643
679
|
const buffer = Buffer.alloc(length);
|
|
644
680
|
const count = length > 0 ? (await handle.read(buffer, 0, length, position)).bytesRead : 0;
|
|
@@ -4,22 +4,43 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { ProviderId } from '../providers/types';
|
|
6
6
|
import type { SessionWatcher, SessionWatcherCallbacks } from './types';
|
|
7
|
+
/** Options for {@link JsonlSessionWatcher}. */
|
|
8
|
+
export interface JsonlSessionWatcherOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Emit a `system` event (summary from the notification, `raw` the line) for
|
|
11
|
+
* each Claude Code mid-turn task-notification attachment, and for the other
|
|
12
|
+
* lines that settle background agents: the plain-text "background agents
|
|
13
|
+
* were stopped by the user" notice, a background agent's final
|
|
14
|
+
* `task_status` attachment, and an `agents_killed` system line. Default
|
|
15
|
+
* `false`.
|
|
16
|
+
*/
|
|
17
|
+
includeTaskNotifications?: boolean;
|
|
18
|
+
}
|
|
7
19
|
export declare class JsonlSessionWatcher implements SessionWatcher {
|
|
8
20
|
private readonly providerId;
|
|
9
21
|
private readonly sessionPath;
|
|
10
22
|
private readonly callbacks;
|
|
23
|
+
private readonly options;
|
|
11
24
|
private tail;
|
|
12
25
|
private startOffset;
|
|
13
26
|
private codexInPlanMode;
|
|
14
27
|
private readonly claudeUsageDeduper;
|
|
15
28
|
/** Last Codex cumulative usage seen; repeated `token_count` events carry the same value. */
|
|
16
29
|
private codexPrevTotal;
|
|
17
|
-
constructor(providerId: ProviderId, sessionPath: string, callbacks: SessionWatcherCallbacks);
|
|
30
|
+
constructor(providerId: ProviderId, sessionPath: string, callbacks: SessionWatcherCallbacks, options?: JsonlSessionWatcherOptions);
|
|
18
31
|
get isActive(): boolean;
|
|
19
32
|
/** Seek to a committed byte offset before starting replay. */
|
|
20
33
|
seekTo(position: number): void;
|
|
21
34
|
/** Position after the last complete JSONL line. */
|
|
22
35
|
getPosition(): number;
|
|
36
|
+
/**
|
|
37
|
+
* Parse state for {@link getPosition}: for Claude Code, the usage deduper's
|
|
38
|
+
* recent message ids (a `ClaudeUsageDedupeState`); for Codex, the previous
|
|
39
|
+
* cumulative token total and plan mode.
|
|
40
|
+
*/
|
|
41
|
+
getResumeState(): unknown;
|
|
42
|
+
/** Restores state from {@link getResumeState} after `seekTo()` and before `start()`. */
|
|
43
|
+
restoreResumeState(state: unknown): boolean;
|
|
23
44
|
start(replay: boolean): void;
|
|
24
45
|
stop(): void;
|
|
25
46
|
private handleRawEvent;
|