sidekick-shared 0.29.0 → 0.29.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +56 -25
- package/dist/aggregation/EventAggregator.d.ts +19 -0
- package/dist/aggregation/EventAggregator.js +174 -35
- package/dist/aggregation/snapshot.d.ts +37 -8
- package/dist/aggregation/snapshot.js +31 -9
- package/dist/aggregation/types.d.ts +13 -0
- package/dist/browser.d.ts +5 -1
- package/dist/browser.js +9 -3
- package/dist/context/sessionContext.js +4 -0
- package/dist/esm/aggregation/EventAggregator.js +174 -35
- package/dist/esm/aggregation/snapshot.js +31 -9
- package/dist/esm/browser.js +1 -0
- package/dist/esm/context/sessionContext.js +4 -0
- package/dist/esm/formatters/toolSummary.js +4 -1
- package/dist/esm/index.js +3 -1
- 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 +1 -1
- package/dist/esm/observedContextWindows.js +2 -1
- package/dist/esm/ownKey.js +26 -0
- package/dist/esm/parsers/codexParser.js +180 -4
- package/dist/esm/parsers/openCodeParser.js +2 -1
- package/dist/esm/parsers/subagentScanner.js +33 -12
- package/dist/esm/parsers/subagentTraceParser.js +97 -37
- package/dist/esm/pricingCatalog.js +3 -2
- package/dist/esm/providers/claudeCode.js +10 -2
- package/dist/esm/providers/codex.js +82 -38
- package/dist/esm/schemas/sessionEvent.js +35 -14
- package/dist/esm/subagentSpawn.js +163 -0
- package/dist/esm/transcript.js +4 -0
- package/dist/esm/turns/assistantTurn.js +3 -2
- package/dist/esm/usage/claudeUsageDedupe.js +68 -0
- package/dist/esm/watchers/eventBridge.js +15 -0
- package/dist/esm/watchers/factory.js +5 -3
- package/dist/esm/watchers/jsonlTail.js +73 -8
- package/dist/esm/watchers/jsonlWatcher.js +51 -4
- package/dist/esm/watchers/providerReaderWatcher.js +13 -0
- package/dist/esm/zaiQuota.js +2 -1
- package/dist/formatters/toolSummary.js +4 -1
- package/dist/index.d.ts +8 -3
- package/dist/index.js +18 -9
- package/dist/modelCatalog.js +5 -4
- package/dist/modelContext.js +4 -3
- package/dist/modelInfo.js +4 -3
- package/dist/node.d.ts +1 -1
- package/dist/node.js +3 -1
- 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 +63 -0
- package/dist/parsers/codexParser.js +181 -4
- package/dist/parsers/openCodeParser.js +2 -1
- package/dist/parsers/subagentScanner.d.ts +19 -6
- package/dist/parsers/subagentScanner.js +33 -12
- package/dist/parsers/subagentTraceParser.d.ts +25 -5
- package/dist/parsers/subagentTraceParser.js +97 -37
- package/dist/pricingCatalog.js +3 -2
- package/dist/providers/claudeCode.js +9 -1
- package/dist/providers/codex.js +82 -38
- package/dist/providers/types.d.ts +14 -0
- package/dist/schemas/index.d.ts +1 -0
- package/dist/schemas/sessionEvent.d.ts +27 -10
- package/dist/schemas/sessionEvent.js +35 -14
- package/dist/subagentSpawn.d.ts +80 -0
- package/dist/subagentSpawn.js +174 -0
- package/dist/transcript.js +4 -0
- package/dist/turns/assistantTurn.d.ts +1 -0
- package/dist/turns/assistantTurn.js +3 -2
- package/dist/usage/claudeUsageDedupe.d.ts +31 -0
- package/dist/usage/claudeUsageDedupe.js +68 -0
- package/dist/watchers/eventBridge.js +15 -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 +75 -8
- package/dist/watchers/jsonlWatcher.d.ts +18 -1
- package/dist/watchers/jsonlWatcher.js +50 -3
- package/dist/watchers/providerReaderWatcher.d.ts +7 -0
- package/dist/watchers/providerReaderWatcher.js +13 -0
- package/dist/watchers/types.d.ts +12 -0
- package/dist/zaiQuota.js +2 -1
- package/package.json +1 -1
|
@@ -270,6 +270,12 @@ class CodexReader {
|
|
|
270
270
|
lineBuffer = '';
|
|
271
271
|
_wasTruncated = false;
|
|
272
272
|
decoder = new StringDecoder('utf8');
|
|
273
|
+
/**
|
|
274
|
+
* Position from `seekTo()` whose prefix still has to be parsed for model,
|
|
275
|
+
* turn, and cumulative-token context; `restoreResumeState()` supplies that
|
|
276
|
+
* context instead and clears it.
|
|
277
|
+
*/
|
|
278
|
+
pendingSeek = null;
|
|
273
279
|
constructor(rolloutPath, onContextWindowLimit, onRateLimits, onTokenUsage) {
|
|
274
280
|
this.rolloutPath = rolloutPath;
|
|
275
281
|
this.onContextWindowLimit = onContextWindowLimit;
|
|
@@ -279,9 +285,11 @@ class CodexReader {
|
|
|
279
285
|
}
|
|
280
286
|
/** Get the parser's session metadata. */
|
|
281
287
|
getSessionMeta() {
|
|
288
|
+
this.settleSeek();
|
|
282
289
|
return this.parser.getSessionMeta();
|
|
283
290
|
}
|
|
284
291
|
readNew(endPosition) {
|
|
292
|
+
this.settleSeek();
|
|
285
293
|
this._wasTruncated = false;
|
|
286
294
|
const events = [];
|
|
287
295
|
try {
|
|
@@ -335,47 +343,51 @@ class CodexReader {
|
|
|
335
343
|
// Skip malformed JSON lines
|
|
336
344
|
}
|
|
337
345
|
}
|
|
338
|
-
|
|
339
|
-
// The model comes along so the value can be persisted per model — the
|
|
340
|
-
// window Codex reports is tier-specific, not the published maximum.
|
|
341
|
-
const mcw = this.parser.getModelContextWindow();
|
|
342
|
-
if (mcw && this.onContextWindowLimit) {
|
|
343
|
-
this.onContextWindowLimit(mcw, this.parser.getCurrentModel() ?? undefined);
|
|
344
|
-
}
|
|
345
|
-
// Propagate rate_limits from token_count events to the provider
|
|
346
|
-
const rateLimits = this.parser.getLastRateLimits();
|
|
347
|
-
if (rateLimits && this.onRateLimits) {
|
|
348
|
-
this.onRateLimits(rateLimits);
|
|
349
|
-
}
|
|
350
|
-
// Propagate last token usage snapshot to the provider
|
|
351
|
-
const lastUsage = this.parser.getLastTokenUsage();
|
|
352
|
-
if (lastUsage && this.onTokenUsage) {
|
|
353
|
-
const normalized = normalizeProviderUsage({
|
|
354
|
-
semantics: 'openai',
|
|
355
|
-
provider: 'openai',
|
|
356
|
-
source: 'codex-reader-snapshot',
|
|
357
|
-
model: this.parser.getCurrentModel() || undefined,
|
|
358
|
-
inputTokens: lastUsage.input_tokens,
|
|
359
|
-
outputTokens: lastUsage.output_tokens,
|
|
360
|
-
cacheReadTokens: lastUsage.cached_input_tokens,
|
|
361
|
-
reasoningTokens: lastUsage.reasoning_output_tokens,
|
|
362
|
-
});
|
|
363
|
-
this.onTokenUsage({
|
|
364
|
-
inputTokens: normalized.uncachedInputTokens,
|
|
365
|
-
outputTokens: normalized.outputTokens,
|
|
366
|
-
cacheWriteTokens: normalized.cacheWriteTokens,
|
|
367
|
-
cacheReadTokens: normalized.cacheReadTokens,
|
|
368
|
-
reasoningTokens: normalized.reasoningTokens,
|
|
369
|
-
model: this.parser.getCurrentModel() || 'unknown',
|
|
370
|
-
timestamp: new Date(),
|
|
371
|
-
});
|
|
372
|
-
}
|
|
346
|
+
this.propagateParserState();
|
|
373
347
|
}
|
|
374
348
|
catch {
|
|
375
349
|
// Skip read errors
|
|
376
350
|
}
|
|
377
351
|
return events;
|
|
378
352
|
}
|
|
353
|
+
/** Hands the parser's context window, rate limits, and last usage to the provider. */
|
|
354
|
+
propagateParserState() {
|
|
355
|
+
// Propagate model_context_window from token_count events to the provider.
|
|
356
|
+
// The model comes along so the value can be persisted per model — the
|
|
357
|
+
// window Codex reports is tier-specific, not the published maximum.
|
|
358
|
+
const mcw = this.parser.getModelContextWindow();
|
|
359
|
+
if (mcw && this.onContextWindowLimit) {
|
|
360
|
+
this.onContextWindowLimit(mcw, this.parser.getCurrentModel() ?? undefined);
|
|
361
|
+
}
|
|
362
|
+
// Propagate rate_limits from token_count events to the provider
|
|
363
|
+
const rateLimits = this.parser.getLastRateLimits();
|
|
364
|
+
if (rateLimits && this.onRateLimits) {
|
|
365
|
+
this.onRateLimits(rateLimits);
|
|
366
|
+
}
|
|
367
|
+
// Propagate last token usage snapshot to the provider
|
|
368
|
+
const lastUsage = this.parser.getLastTokenUsage();
|
|
369
|
+
if (lastUsage && this.onTokenUsage) {
|
|
370
|
+
const normalized = normalizeProviderUsage({
|
|
371
|
+
semantics: 'openai',
|
|
372
|
+
provider: 'openai',
|
|
373
|
+
source: 'codex-reader-snapshot',
|
|
374
|
+
model: this.parser.getCurrentModel() || undefined,
|
|
375
|
+
inputTokens: lastUsage.input_tokens,
|
|
376
|
+
outputTokens: lastUsage.output_tokens,
|
|
377
|
+
cacheReadTokens: lastUsage.cached_input_tokens,
|
|
378
|
+
reasoningTokens: lastUsage.reasoning_output_tokens,
|
|
379
|
+
});
|
|
380
|
+
this.onTokenUsage({
|
|
381
|
+
inputTokens: normalized.uncachedInputTokens,
|
|
382
|
+
outputTokens: normalized.outputTokens,
|
|
383
|
+
cacheWriteTokens: normalized.cacheWriteTokens,
|
|
384
|
+
cacheReadTokens: normalized.cacheReadTokens,
|
|
385
|
+
reasoningTokens: normalized.reasoningTokens,
|
|
386
|
+
model: this.parser.getCurrentModel() || 'unknown',
|
|
387
|
+
timestamp: new Date(),
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
}
|
|
379
391
|
readAll() {
|
|
380
392
|
this.reset();
|
|
381
393
|
return this.readNew();
|
|
@@ -387,11 +399,13 @@ class CodexReader {
|
|
|
387
399
|
this.parser.reset();
|
|
388
400
|
this.decoder = new StringDecoder('utf8');
|
|
389
401
|
this._wasTruncated = false;
|
|
402
|
+
this.pendingSeek = null;
|
|
390
403
|
}
|
|
391
404
|
exists() {
|
|
392
405
|
return fs.existsSync(this.rolloutPath);
|
|
393
406
|
}
|
|
394
407
|
flush() {
|
|
408
|
+
this.settleSeek();
|
|
395
409
|
this.lineBuffer += this.decoder.end();
|
|
396
410
|
// Process any remaining data in the line buffer
|
|
397
411
|
if (this.lineBuffer.trim()) {
|
|
@@ -409,15 +423,45 @@ class CodexReader {
|
|
|
409
423
|
return this.committedPosition;
|
|
410
424
|
}
|
|
411
425
|
seekTo(position) {
|
|
412
|
-
// Rebuild model, turn, and cumulative-token context before resuming.
|
|
413
|
-
// The prefix is parsed only; its events are already in the consumer snapshot.
|
|
414
426
|
this.reset();
|
|
427
|
+
this.filePosition = position;
|
|
428
|
+
this.committedPosition = position;
|
|
429
|
+
// The prefix's context is rebuilt on the next read, unless
|
|
430
|
+
// restoreResumeState() supplies it first.
|
|
415
431
|
if (position > 0)
|
|
416
|
-
this.
|
|
432
|
+
this.pendingSeek = position;
|
|
417
433
|
}
|
|
418
434
|
wasTruncated() {
|
|
419
435
|
return this._wasTruncated;
|
|
420
436
|
}
|
|
437
|
+
/** The parser's continuation state (see `CodexRolloutParser.exportResumeState`). */
|
|
438
|
+
getResumeState() {
|
|
439
|
+
this.settleSeek();
|
|
440
|
+
return this.parser.exportResumeState();
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Restores parser state from `getResumeState()`; after `seekTo()` this
|
|
444
|
+
* replaces re-parsing the rollout up to that position.
|
|
445
|
+
*/
|
|
446
|
+
restoreResumeState(state) {
|
|
447
|
+
if (!this.parser.importResumeState(state))
|
|
448
|
+
return false;
|
|
449
|
+
this.pendingSeek = null;
|
|
450
|
+
this.propagateParserState();
|
|
451
|
+
return true;
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Rebuilds model, turn, and cumulative-token context for a pending
|
|
455
|
+
* `seekTo()` position. The prefix is parsed only; its events are already
|
|
456
|
+
* in the consumer snapshot.
|
|
457
|
+
*/
|
|
458
|
+
settleSeek() {
|
|
459
|
+
const position = this.pendingSeek;
|
|
460
|
+
if (position === null)
|
|
461
|
+
return;
|
|
462
|
+
this.reset();
|
|
463
|
+
this.readNew(position);
|
|
464
|
+
}
|
|
421
465
|
}
|
|
422
466
|
// ---------------------------------------------------------------------------
|
|
423
467
|
// CodexProvider — full SessionProviderBase implementation
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { z } from 'zod';
|
|
10
10
|
import { normalizedUsageSchema } from './usageNormalization';
|
|
11
|
+
import { taskNotificationPromptFromAttachment } from '../subagentSpawn';
|
|
11
12
|
// ── MessageUsage ──
|
|
12
13
|
export const messageUsageSchema = z.object({
|
|
13
14
|
input_tokens: z.number(),
|
|
@@ -52,7 +53,9 @@ const eventBaseShape = {
|
|
|
52
53
|
promptSource: z.string().optional(),
|
|
53
54
|
cwd: z.string().optional(),
|
|
54
55
|
gitBranch: z.string().optional(),
|
|
55
|
-
|
|
56
|
+
// A mode this release doesn't know (Claude Code added `auto`) is dropped
|
|
57
|
+
// rather than rejecting the whole line with its prompt or tool results.
|
|
58
|
+
permissionMode: permissionModeSchema.optional().catch(undefined),
|
|
56
59
|
rateLimits: z
|
|
57
60
|
.object({
|
|
58
61
|
primary: z
|
|
@@ -169,22 +172,21 @@ export const sessionEventSchema = z.union([
|
|
|
169
172
|
// ── Progress unwrapping ──
|
|
170
173
|
/** Recursion bound for nested progress envelopes; real data nests 1 level. */
|
|
171
174
|
const MAX_PROGRESS_DEPTH = 8;
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
* `sessionEventSchema` alone rejects. This helper tries a direct parse,
|
|
178
|
-
* then unwraps progress envelopes (recursively, in case of nesting).
|
|
179
|
-
* Returns zero events for unrecognized input — never throws.
|
|
180
|
-
*/
|
|
181
|
-
export function extractSessionEvents(raw, depth = 0) {
|
|
182
|
-
return extractSessionEventsWithProvenance(raw, depth, {});
|
|
175
|
+
export function extractSessionEvents(raw, depthOrOptions = 0) {
|
|
176
|
+
if (typeof depthOrOptions === 'number') {
|
|
177
|
+
return extractSessionEventsWithProvenance(raw, depthOrOptions, {}, {});
|
|
178
|
+
}
|
|
179
|
+
return extractSessionEventsWithProvenance(raw, 0, {}, depthOrOptions ?? {});
|
|
183
180
|
}
|
|
184
|
-
function extractSessionEventsWithProvenance(raw, depth, inherited) {
|
|
181
|
+
function extractSessionEventsWithProvenance(raw, depth, inherited, options) {
|
|
185
182
|
const direct = sessionEventSchema.safeParse(raw);
|
|
186
183
|
if (direct.success)
|
|
187
184
|
return [{ ...inherited, ...direct.data }];
|
|
185
|
+
if (options.includeTaskNotifications) {
|
|
186
|
+
const notification = taskNotificationAttachmentEvent(raw);
|
|
187
|
+
if (notification)
|
|
188
|
+
return [{ ...inherited, ...notification }];
|
|
189
|
+
}
|
|
188
190
|
if (depth >= MAX_PROGRESS_DEPTH)
|
|
189
191
|
return [];
|
|
190
192
|
if (typeof raw === 'object' && raw !== null && raw.type === 'progress') {
|
|
@@ -193,12 +195,31 @@ function extractSessionEventsWithProvenance(raw, depth, inherited) {
|
|
|
193
195
|
if (typeof data === 'object' && data !== null) {
|
|
194
196
|
const message = data.message;
|
|
195
197
|
if (typeof message === 'object' && message !== null) {
|
|
196
|
-
return extractSessionEventsWithProvenance(message, depth + 1, provenance);
|
|
198
|
+
return extractSessionEventsWithProvenance(message, depth + 1, provenance, options);
|
|
197
199
|
}
|
|
198
200
|
}
|
|
199
201
|
}
|
|
200
202
|
return [];
|
|
201
203
|
}
|
|
204
|
+
/** The `system` event for a task-notification attachment line, or `null`. */
|
|
205
|
+
function taskNotificationAttachmentEvent(raw) {
|
|
206
|
+
const prompt = taskNotificationPromptFromAttachment(raw);
|
|
207
|
+
if (prompt === null)
|
|
208
|
+
return null;
|
|
209
|
+
const line = raw;
|
|
210
|
+
const candidate = {
|
|
211
|
+
type: 'system',
|
|
212
|
+
timestamp: line.timestamp,
|
|
213
|
+
origin: { kind: 'task-notification' },
|
|
214
|
+
message: { role: 'user', sourceLabel: 'task notification', content: prompt },
|
|
215
|
+
};
|
|
216
|
+
for (const key of ['entrypoint', 'isSidechain', 'cwd', 'gitBranch']) {
|
|
217
|
+
if (line[key] !== undefined)
|
|
218
|
+
candidate[key] = line[key];
|
|
219
|
+
}
|
|
220
|
+
const parsed = systemSessionEventSchema.safeParse(candidate);
|
|
221
|
+
return parsed.success ? parsed.data : null;
|
|
222
|
+
}
|
|
202
223
|
function progressProvenance(raw) {
|
|
203
224
|
const value = raw;
|
|
204
225
|
const provenance = {};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recognizes Claude Code subagent spawns and background-task notifications.
|
|
3
|
+
*
|
|
4
|
+
* Claude Code's subagent tool is `Agent`; `Task` is its earlier name (and the
|
|
5
|
+
* name OpenCode's `task` tool is normalized to). A background agent's spawning
|
|
6
|
+
* tool call gets its result right away ("Async agent launched successfully.")
|
|
7
|
+
* and its completion arrives later as a `<task-notification>` block, delivered
|
|
8
|
+
* either as a user line or as a mid-turn `queued_command` attachment.
|
|
9
|
+
*
|
|
10
|
+
* Pure: no filesystem access, so it is safe for browser bundles.
|
|
11
|
+
*
|
|
12
|
+
* @module subagentSpawn
|
|
13
|
+
*/
|
|
14
|
+
/** Tool names that spawn a subagent: `Agent` (current) and `Task` (earlier name). */
|
|
15
|
+
export const SUBAGENT_SPAWN_TOOL_NAMES = Object.freeze(['Agent', 'Task']);
|
|
16
|
+
const SPAWN_TOOLS = new Set(SUBAGENT_SPAWN_TOOL_NAMES);
|
|
17
|
+
/** Whether a tool name spawns a subagent (`Agent` or `Task`). */
|
|
18
|
+
export function isSubagentSpawnTool(name) {
|
|
19
|
+
return typeof name === 'string' && SPAWN_TOOLS.has(name);
|
|
20
|
+
}
|
|
21
|
+
const ASYNC_LAUNCH_PREFIX = /^\s*Async agent launched successfully\b/;
|
|
22
|
+
const AGENT_ID_LINE = /(?:^|\n)[ \t]*agentId:[ \t]*([A-Za-z0-9][\w.@-]*)/;
|
|
23
|
+
/**
|
|
24
|
+
* Plain text of tool-result or message content: a string as-is, or the text
|
|
25
|
+
* blocks of a content array joined by newlines.
|
|
26
|
+
*/
|
|
27
|
+
export function contentText(content) {
|
|
28
|
+
if (typeof content === 'string')
|
|
29
|
+
return content;
|
|
30
|
+
if (!Array.isArray(content))
|
|
31
|
+
return '';
|
|
32
|
+
const parts = [];
|
|
33
|
+
for (const block of content) {
|
|
34
|
+
if (!block || typeof block !== 'object')
|
|
35
|
+
continue;
|
|
36
|
+
const typed = block;
|
|
37
|
+
if (typed.type === 'text' && typeof typed.text === 'string')
|
|
38
|
+
parts.push(typed.text);
|
|
39
|
+
}
|
|
40
|
+
return parts.join('\n');
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Recognizes the immediate result of a background spawn
|
|
44
|
+
* (`Async agent launched successfully.` … `agentId: <id> …`). Accepts the
|
|
45
|
+
* result text or the raw tool-result content. Returns `null` for anything
|
|
46
|
+
* else, including a finished foreground agent's result.
|
|
47
|
+
*/
|
|
48
|
+
export function parseAsyncAgentLaunch(result) {
|
|
49
|
+
const text = contentText(result);
|
|
50
|
+
if (!ASYNC_LAUNCH_PREFIX.test(text))
|
|
51
|
+
return null;
|
|
52
|
+
const match = AGENT_ID_LINE.exec(text);
|
|
53
|
+
return match ? { agentId: match[1] } : {};
|
|
54
|
+
}
|
|
55
|
+
const NOTIFICATION_OPEN = '<task-notification>';
|
|
56
|
+
const NOTIFICATION_CLOSE = '</task-notification>';
|
|
57
|
+
const NOTIFICATION_PREFIX = /^\s*<task-notification>/;
|
|
58
|
+
const TAG_PATTERNS = {
|
|
59
|
+
taskId: /<task-id>\s*([\s\S]*?)\s*<\/task-id>/,
|
|
60
|
+
toolUseId: /<tool-use-id>\s*([\s\S]*?)\s*<\/tool-use-id>/,
|
|
61
|
+
status: /<status>\s*([\s\S]*?)\s*<\/status>/,
|
|
62
|
+
summary: /<summary>\s*([\s\S]*?)\s*<\/summary>/,
|
|
63
|
+
note: /<note>\s*([\s\S]*?)\s*<\/note>/,
|
|
64
|
+
};
|
|
65
|
+
const INTERIM_TEXT = /waiting on its own background work|background work of its own still running/i;
|
|
66
|
+
function tagValue(body, pattern) {
|
|
67
|
+
const value = pattern.exec(body)?.[1]?.trim();
|
|
68
|
+
return value ? value : undefined;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Parses the first `<task-notification>` block in `text`. Whitespace and
|
|
72
|
+
* newlines between tags, missing fields, and unknown extra fields are
|
|
73
|
+
* tolerated; the block needs a `<task-id>`. Fields are read before the
|
|
74
|
+
* agent's own `<result>` output, which may quote anything.
|
|
75
|
+
*/
|
|
76
|
+
export function parseTaskNotification(text) {
|
|
77
|
+
if (typeof text !== 'string')
|
|
78
|
+
return null;
|
|
79
|
+
const start = text.indexOf(NOTIFICATION_OPEN);
|
|
80
|
+
if (start < 0)
|
|
81
|
+
return null;
|
|
82
|
+
let body = text.slice(start + NOTIFICATION_OPEN.length);
|
|
83
|
+
const resultAt = body.indexOf('<result>');
|
|
84
|
+
let header = resultAt >= 0 ? body.slice(0, resultAt) : body;
|
|
85
|
+
if (resultAt < 0) {
|
|
86
|
+
const end = body.indexOf(NOTIFICATION_CLOSE);
|
|
87
|
+
if (end >= 0)
|
|
88
|
+
header = body = body.slice(0, end);
|
|
89
|
+
}
|
|
90
|
+
if (!TAG_PATTERNS.taskId.test(header))
|
|
91
|
+
header = body;
|
|
92
|
+
const taskId = tagValue(header, TAG_PATTERNS.taskId);
|
|
93
|
+
if (!taskId)
|
|
94
|
+
return null;
|
|
95
|
+
const toolUseId = tagValue(header, TAG_PATTERNS.toolUseId);
|
|
96
|
+
const status = tagValue(header, TAG_PATTERNS.status);
|
|
97
|
+
const summary = tagValue(header, TAG_PATTERNS.summary);
|
|
98
|
+
const note = tagValue(header, TAG_PATTERNS.note);
|
|
99
|
+
return {
|
|
100
|
+
taskId,
|
|
101
|
+
...(toolUseId ? { toolUseId } : {}),
|
|
102
|
+
...(status ? { status } : {}),
|
|
103
|
+
...(summary ? { summary } : {}),
|
|
104
|
+
interim: INTERIM_TEXT.test(summary ?? '') || INTERIM_TEXT.test(note ?? ''),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The `<task-notification>` prompt of a raw Claude Code attachment line
|
|
109
|
+
* (`{ type: 'attachment', attachment: { type: 'queued_command', commandMode:
|
|
110
|
+
* 'task-notification', prompt } }`), which is how a notification that lands
|
|
111
|
+
* mid-turn is recorded. `null` for any other value.
|
|
112
|
+
*/
|
|
113
|
+
export function taskNotificationPromptFromAttachment(raw) {
|
|
114
|
+
if (!raw || typeof raw !== 'object')
|
|
115
|
+
return null;
|
|
116
|
+
const line = raw;
|
|
117
|
+
if (line.type !== 'attachment' || !line.attachment || typeof line.attachment !== 'object') {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const attachment = line.attachment;
|
|
121
|
+
return attachment.type === 'queued_command' &&
|
|
122
|
+
attachment.commandMode === 'task-notification' &&
|
|
123
|
+
typeof attachment.prompt === 'string'
|
|
124
|
+
? attachment.prompt
|
|
125
|
+
: null;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The synthetic `system` event `extractSessionEvents(raw, {
|
|
129
|
+
* includeTaskNotifications: true })` makes from a mid-turn task-notification
|
|
130
|
+
* attachment. It is bookkeeping, not a message or a prompt.
|
|
131
|
+
*/
|
|
132
|
+
export function isTaskNotificationSystemEvent(event) {
|
|
133
|
+
return event.type === 'system' && event.origin?.kind === 'task-notification';
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The task notification an event carries: a user line with
|
|
137
|
+
* `origin.kind === 'task-notification'` or whose text starts with
|
|
138
|
+
* `<task-notification>`, or the opt-in synthetic system event. `null` otherwise.
|
|
139
|
+
*/
|
|
140
|
+
export function taskNotificationFromEvent(event) {
|
|
141
|
+
if (event.type !== 'user' && event.type !== 'system')
|
|
142
|
+
return null;
|
|
143
|
+
const flagged = event.origin?.kind === 'task-notification';
|
|
144
|
+
if (event.type === 'system' && !flagged)
|
|
145
|
+
return null;
|
|
146
|
+
const text = contentText(event.message?.content);
|
|
147
|
+
if (!flagged && !NOTIFICATION_PREFIX.test(text))
|
|
148
|
+
return null;
|
|
149
|
+
return parseTaskNotification(text);
|
|
150
|
+
}
|
|
151
|
+
/** Maps a notification `<status>` to an outcome; unknown or missing statuses read as `completed`. */
|
|
152
|
+
export function taskNotificationOutcome(status) {
|
|
153
|
+
switch (status?.toLowerCase()) {
|
|
154
|
+
case 'failed':
|
|
155
|
+
return 'failed';
|
|
156
|
+
case 'killed':
|
|
157
|
+
return 'killed';
|
|
158
|
+
case 'stopped':
|
|
159
|
+
return 'stopped';
|
|
160
|
+
default:
|
|
161
|
+
return 'completed';
|
|
162
|
+
}
|
|
163
|
+
}
|
package/dist/esm/transcript.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { calculateNormalizedUsageCost, extractNormalizedUsage, } from './usageNormalization';
|
|
2
|
+
import { isTaskNotificationSystemEvent } from './subagentSpawn';
|
|
2
3
|
const EMPTY_TOTALS = {
|
|
3
4
|
uncachedInputTokens: 0,
|
|
4
5
|
cacheReadTokens: 0,
|
|
@@ -25,6 +26,9 @@ export function projectSessionTranscript(events, options = {}) {
|
|
|
25
26
|
let cwd;
|
|
26
27
|
let gitBranch;
|
|
27
28
|
events.forEach((event, eventIndex) => {
|
|
29
|
+
// A reader's synthetic notice that a background task finished, not a message.
|
|
30
|
+
if (isTaskNotificationSystemEvent(event))
|
|
31
|
+
return;
|
|
28
32
|
if (nonEmpty(event.cwd))
|
|
29
33
|
cwd = event.cwd;
|
|
30
34
|
if (nonEmpty(event.gitBranch))
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { formatToolSummary, truncate } from '../formatters/toolSummary';
|
|
2
|
+
import { isSubagentSpawnTool } from '../subagentSpawn';
|
|
2
3
|
const DEFAULT_NARRATION_CHARS = 4000;
|
|
3
4
|
const DEFAULT_REASONING_CHARS = 8000;
|
|
4
5
|
const DEFAULT_PROCESS_STEPS = 60;
|
|
5
6
|
const DEFAULT_REASONING_BLOCKS = 24;
|
|
6
7
|
const DEFAULT_TOOL_INPUT_CHARS = 200;
|
|
7
|
-
const SUBAGENT_TOOL_NAMES = new Set(['Task']);
|
|
8
8
|
const LABEL_MAX = 80;
|
|
9
9
|
const TITLE_RE = /^\s*\*\*([^*\n]+)\*\*[ \t]*(?:\n+([\s\S]*))?$/;
|
|
10
10
|
const TITLE_MAX = 100;
|
|
@@ -22,8 +22,9 @@ export function reasoningSummary(text) {
|
|
|
22
22
|
}
|
|
23
23
|
return { title: null, body: trimmed };
|
|
24
24
|
}
|
|
25
|
+
/** Whether a tool call spawns a subagent (`Agent`, or its earlier name `Task`). */
|
|
25
26
|
export function isAssistantTurnSubagentTool(toolName) {
|
|
26
|
-
return
|
|
27
|
+
return isSubagentSpawnTool(toolName);
|
|
27
28
|
}
|
|
28
29
|
export function segmentAssistantTurn(events, options = {}) {
|
|
29
30
|
const maxNarrationChars = options.maxNarrationChars ?? DEFAULT_NARRATION_CHARS;
|
|
@@ -59,6 +59,25 @@ export function normalizeClaudeUsage(event) {
|
|
|
59
59
|
},
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
|
+
const BUCKET_KEYS = ['input', 'cacheRead', 'cacheWrite', 'output', 'reasoning'];
|
|
63
|
+
/** Entries {@link ClaudeUsageDeduper.exportState} keeps by default. */
|
|
64
|
+
const DEFAULT_EXPORT_LIMIT = 256;
|
|
65
|
+
function isCount(value) {
|
|
66
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
|
67
|
+
}
|
|
68
|
+
function bucketsFrom(value) {
|
|
69
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
70
|
+
return null;
|
|
71
|
+
const record = value;
|
|
72
|
+
const buckets = {};
|
|
73
|
+
for (const key of BUCKET_KEYS) {
|
|
74
|
+
const count = record[key];
|
|
75
|
+
if (!isCount(count))
|
|
76
|
+
return null;
|
|
77
|
+
buckets[key] = count;
|
|
78
|
+
}
|
|
79
|
+
return buckets;
|
|
80
|
+
}
|
|
62
81
|
function bucketsOf(usage) {
|
|
63
82
|
return {
|
|
64
83
|
input: usage.uncachedInputTokens,
|
|
@@ -163,6 +182,55 @@ export class ClaudeUsageDeduper {
|
|
|
163
182
|
reset() {
|
|
164
183
|
this.seen.clear();
|
|
165
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* The most recent `limit` message ids and the tokens counted for each,
|
|
187
|
+
* oldest first. Split lines of one response are adjacent, so a reader
|
|
188
|
+
* resumed at the position this was taken at needs only the last few.
|
|
189
|
+
*/
|
|
190
|
+
exportState(limit = DEFAULT_EXPORT_LIMIT) {
|
|
191
|
+
const count = limit >= 0 ? Math.min(this.seen.size, Math.floor(limit)) : 0;
|
|
192
|
+
const entries = [];
|
|
193
|
+
if (count > 0) {
|
|
194
|
+
let skip = this.seen.size - count;
|
|
195
|
+
for (const [id, buckets] of this.seen) {
|
|
196
|
+
if (skip > 0) {
|
|
197
|
+
skip -= 1;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
entries.push([id, { ...buckets }]);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return { version: 1, entries };
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Replaces this deduper's state with one from {@link exportState}. Returns
|
|
207
|
+
* false, changing nothing, when `state` is not a valid version-1 state
|
|
208
|
+
* (wrong shape, empty ids, or counts that are not finite non-negative
|
|
209
|
+
* numbers). Entries beyond this deduper's capacity drop oldest first.
|
|
210
|
+
*/
|
|
211
|
+
importState(state) {
|
|
212
|
+
if (!state || typeof state !== 'object')
|
|
213
|
+
return false;
|
|
214
|
+
const { version, entries } = state;
|
|
215
|
+
if (version !== 1 || !Array.isArray(entries))
|
|
216
|
+
return false;
|
|
217
|
+
const parsed = [];
|
|
218
|
+
for (const entry of entries) {
|
|
219
|
+
if (!Array.isArray(entry) || entry.length !== 2)
|
|
220
|
+
return false;
|
|
221
|
+
const [id, value] = entry;
|
|
222
|
+
if (typeof id !== 'string' || id.length === 0)
|
|
223
|
+
return false;
|
|
224
|
+
const buckets = bucketsFrom(value);
|
|
225
|
+
if (!buckets)
|
|
226
|
+
return false;
|
|
227
|
+
parsed.push([id, buckets]);
|
|
228
|
+
}
|
|
229
|
+
this.seen.clear();
|
|
230
|
+
for (const [id, buckets] of parsed)
|
|
231
|
+
this.remember(id, buckets);
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
166
234
|
remember(id, buckets) {
|
|
167
235
|
this.seen.delete(id);
|
|
168
236
|
this.seen.set(id, buckets);
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* FollowEvent is a lossy summary for timeline display and CLI output.
|
|
9
9
|
*/
|
|
10
10
|
import { formatToolSummary } from '../formatters/toolSummary';
|
|
11
|
+
import { isTaskNotificationSystemEvent, taskNotificationFromEvent } from '../subagentSpawn';
|
|
11
12
|
/**
|
|
12
13
|
* Converts a single SessionEvent to one or more FollowEvents.
|
|
13
14
|
*
|
|
@@ -148,6 +149,20 @@ export function toFollowEvents(event, providerId) {
|
|
|
148
149
|
break;
|
|
149
150
|
}
|
|
150
151
|
case 'system': {
|
|
152
|
+
// A background task's notice (opt-in reader event): its summary, not the raw block.
|
|
153
|
+
const notification = isTaskNotificationSystemEvent(event)
|
|
154
|
+
? taskNotificationFromEvent(event)
|
|
155
|
+
: null;
|
|
156
|
+
if (notification) {
|
|
157
|
+
events.push({
|
|
158
|
+
providerId,
|
|
159
|
+
type: 'system',
|
|
160
|
+
timestamp: ts,
|
|
161
|
+
summary: notification.summary ?? `Background task ${notification.status ?? 'finished'}`,
|
|
162
|
+
raw: event,
|
|
163
|
+
});
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
151
166
|
const text = extractTextContent(event.message?.content);
|
|
152
167
|
const label = event.message?.sourceLabel || event.message?.role || 'system';
|
|
153
168
|
const summary = text ||
|
|
@@ -41,13 +41,15 @@ export function resolveSessionPath(provider, workspacePath, sessionId) {
|
|
|
41
41
|
export function createWatcher(options) {
|
|
42
42
|
const { provider, workspacePath, sessionId, callbacks } = options;
|
|
43
43
|
const sessionPath = resolveSessionPath(provider, workspacePath, sessionId);
|
|
44
|
-
const watcher = createWatcherForProvider(provider, sessionPath, callbacks
|
|
44
|
+
const watcher = createWatcherForProvider(provider, sessionPath, callbacks, {
|
|
45
|
+
includeTaskNotifications: options.includeTaskNotifications === true,
|
|
46
|
+
});
|
|
45
47
|
return { watcher, sessionPath };
|
|
46
48
|
}
|
|
47
|
-
function createWatcherForProvider(provider, sessionPath, callbacks) {
|
|
49
|
+
function createWatcherForProvider(provider, sessionPath, callbacks, jsonlOptions) {
|
|
48
50
|
switch (provider.id) {
|
|
49
51
|
case 'claude-code':
|
|
50
|
-
return new JsonlSessionWatcher(provider.id, sessionPath, callbacks);
|
|
52
|
+
return new JsonlSessionWatcher(provider.id, sessionPath, callbacks, jsonlOptions);
|
|
51
53
|
case 'codex':
|
|
52
54
|
return new ProviderReaderSessionWatcher(provider, sessionPath, callbacks);
|
|
53
55
|
case 'opencode': {
|