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
|
@@ -0,0 +1,80 @@
|
|
|
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
|
+
import type { SubagentOutcome } from './aggregation/types';
|
|
15
|
+
import type { SessionEvent } from './types/sessionEvent';
|
|
16
|
+
/** Tool names that spawn a subagent: `Agent` (current) and `Task` (earlier name). */
|
|
17
|
+
export declare const SUBAGENT_SPAWN_TOOL_NAMES: readonly string[];
|
|
18
|
+
/** Whether a tool name spawns a subagent (`Agent` or `Task`). */
|
|
19
|
+
export declare function isSubagentSpawnTool(name: string | null | undefined): boolean;
|
|
20
|
+
/** A spawning tool result that launched the agent in the background. */
|
|
21
|
+
export interface AsyncAgentLaunch {
|
|
22
|
+
/** The agent id Claude Code reported (`agentId: <id>`), when present. */
|
|
23
|
+
agentId?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Plain text of tool-result or message content: a string as-is, or the text
|
|
27
|
+
* blocks of a content array joined by newlines.
|
|
28
|
+
*/
|
|
29
|
+
export declare function contentText(content: unknown): string;
|
|
30
|
+
/**
|
|
31
|
+
* Recognizes the immediate result of a background spawn
|
|
32
|
+
* (`Async agent launched successfully.` … `agentId: <id> …`). Accepts the
|
|
33
|
+
* result text or the raw tool-result content. Returns `null` for anything
|
|
34
|
+
* else, including a finished foreground agent's result.
|
|
35
|
+
*/
|
|
36
|
+
export declare function parseAsyncAgentLaunch(result: unknown): AsyncAgentLaunch | null;
|
|
37
|
+
/** Fields of a `<task-notification>` block. */
|
|
38
|
+
export interface TaskNotification {
|
|
39
|
+
/** `<task-id>`: the background agent's id (or a background shell task's id). */
|
|
40
|
+
taskId: string;
|
|
41
|
+
/** `<tool-use-id>`: the tool call that started the background work. */
|
|
42
|
+
toolUseId?: string;
|
|
43
|
+
/** `<status>` as written, e.g. `completed`, `failed`, `killed`, `stopped`. */
|
|
44
|
+
status?: string;
|
|
45
|
+
/** `<summary>`, e.g. `Agent "Explore auth" finished`. */
|
|
46
|
+
summary?: string;
|
|
47
|
+
/**
|
|
48
|
+
* The agent stopped while background work of its own is still running; it
|
|
49
|
+
* may resume, and the same task id notifies again.
|
|
50
|
+
*/
|
|
51
|
+
interim: boolean;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Parses the first `<task-notification>` block in `text`. Whitespace and
|
|
55
|
+
* newlines between tags, missing fields, and unknown extra fields are
|
|
56
|
+
* tolerated; the block needs a `<task-id>`. Fields are read before the
|
|
57
|
+
* agent's own `<result>` output, which may quote anything.
|
|
58
|
+
*/
|
|
59
|
+
export declare function parseTaskNotification(text: unknown): TaskNotification | null;
|
|
60
|
+
/**
|
|
61
|
+
* The `<task-notification>` prompt of a raw Claude Code attachment line
|
|
62
|
+
* (`{ type: 'attachment', attachment: { type: 'queued_command', commandMode:
|
|
63
|
+
* 'task-notification', prompt } }`), which is how a notification that lands
|
|
64
|
+
* mid-turn is recorded. `null` for any other value.
|
|
65
|
+
*/
|
|
66
|
+
export declare function taskNotificationPromptFromAttachment(raw: unknown): string | null;
|
|
67
|
+
/**
|
|
68
|
+
* The synthetic `system` event `extractSessionEvents(raw, {
|
|
69
|
+
* includeTaskNotifications: true })` makes from a mid-turn task-notification
|
|
70
|
+
* attachment. It is bookkeeping, not a message or a prompt.
|
|
71
|
+
*/
|
|
72
|
+
export declare function isTaskNotificationSystemEvent(event: SessionEvent): boolean;
|
|
73
|
+
/**
|
|
74
|
+
* The task notification an event carries: a user line with
|
|
75
|
+
* `origin.kind === 'task-notification'` or whose text starts with
|
|
76
|
+
* `<task-notification>`, or the opt-in synthetic system event. `null` otherwise.
|
|
77
|
+
*/
|
|
78
|
+
export declare function taskNotificationFromEvent(event: SessionEvent): TaskNotification | null;
|
|
79
|
+
/** Maps a notification `<status>` to an outcome; unknown or missing statuses read as `completed`. */
|
|
80
|
+
export declare function taskNotificationOutcome(status: string | undefined): SubagentOutcome;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Recognizes Claude Code subagent spawns and background-task notifications.
|
|
4
|
+
*
|
|
5
|
+
* Claude Code's subagent tool is `Agent`; `Task` is its earlier name (and the
|
|
6
|
+
* name OpenCode's `task` tool is normalized to). A background agent's spawning
|
|
7
|
+
* tool call gets its result right away ("Async agent launched successfully.")
|
|
8
|
+
* and its completion arrives later as a `<task-notification>` block, delivered
|
|
9
|
+
* either as a user line or as a mid-turn `queued_command` attachment.
|
|
10
|
+
*
|
|
11
|
+
* Pure: no filesystem access, so it is safe for browser bundles.
|
|
12
|
+
*
|
|
13
|
+
* @module subagentSpawn
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.SUBAGENT_SPAWN_TOOL_NAMES = void 0;
|
|
17
|
+
exports.isSubagentSpawnTool = isSubagentSpawnTool;
|
|
18
|
+
exports.contentText = contentText;
|
|
19
|
+
exports.parseAsyncAgentLaunch = parseAsyncAgentLaunch;
|
|
20
|
+
exports.parseTaskNotification = parseTaskNotification;
|
|
21
|
+
exports.taskNotificationPromptFromAttachment = taskNotificationPromptFromAttachment;
|
|
22
|
+
exports.isTaskNotificationSystemEvent = isTaskNotificationSystemEvent;
|
|
23
|
+
exports.taskNotificationFromEvent = taskNotificationFromEvent;
|
|
24
|
+
exports.taskNotificationOutcome = taskNotificationOutcome;
|
|
25
|
+
/** Tool names that spawn a subagent: `Agent` (current) and `Task` (earlier name). */
|
|
26
|
+
exports.SUBAGENT_SPAWN_TOOL_NAMES = Object.freeze(['Agent', 'Task']);
|
|
27
|
+
const SPAWN_TOOLS = new Set(exports.SUBAGENT_SPAWN_TOOL_NAMES);
|
|
28
|
+
/** Whether a tool name spawns a subagent (`Agent` or `Task`). */
|
|
29
|
+
function isSubagentSpawnTool(name) {
|
|
30
|
+
return typeof name === 'string' && SPAWN_TOOLS.has(name);
|
|
31
|
+
}
|
|
32
|
+
const ASYNC_LAUNCH_PREFIX = /^\s*Async agent launched successfully\b/;
|
|
33
|
+
const AGENT_ID_LINE = /(?:^|\n)[ \t]*agentId:[ \t]*([A-Za-z0-9][\w.@-]*)/;
|
|
34
|
+
/**
|
|
35
|
+
* Plain text of tool-result or message content: a string as-is, or the text
|
|
36
|
+
* blocks of a content array joined by newlines.
|
|
37
|
+
*/
|
|
38
|
+
function contentText(content) {
|
|
39
|
+
if (typeof content === 'string')
|
|
40
|
+
return content;
|
|
41
|
+
if (!Array.isArray(content))
|
|
42
|
+
return '';
|
|
43
|
+
const parts = [];
|
|
44
|
+
for (const block of content) {
|
|
45
|
+
if (!block || typeof block !== 'object')
|
|
46
|
+
continue;
|
|
47
|
+
const typed = block;
|
|
48
|
+
if (typed.type === 'text' && typeof typed.text === 'string')
|
|
49
|
+
parts.push(typed.text);
|
|
50
|
+
}
|
|
51
|
+
return parts.join('\n');
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Recognizes the immediate result of a background spawn
|
|
55
|
+
* (`Async agent launched successfully.` … `agentId: <id> …`). Accepts the
|
|
56
|
+
* result text or the raw tool-result content. Returns `null` for anything
|
|
57
|
+
* else, including a finished foreground agent's result.
|
|
58
|
+
*/
|
|
59
|
+
function parseAsyncAgentLaunch(result) {
|
|
60
|
+
const text = contentText(result);
|
|
61
|
+
if (!ASYNC_LAUNCH_PREFIX.test(text))
|
|
62
|
+
return null;
|
|
63
|
+
const match = AGENT_ID_LINE.exec(text);
|
|
64
|
+
return match ? { agentId: match[1] } : {};
|
|
65
|
+
}
|
|
66
|
+
const NOTIFICATION_OPEN = '<task-notification>';
|
|
67
|
+
const NOTIFICATION_CLOSE = '</task-notification>';
|
|
68
|
+
const NOTIFICATION_PREFIX = /^\s*<task-notification>/;
|
|
69
|
+
const TAG_PATTERNS = {
|
|
70
|
+
taskId: /<task-id>\s*([\s\S]*?)\s*<\/task-id>/,
|
|
71
|
+
toolUseId: /<tool-use-id>\s*([\s\S]*?)\s*<\/tool-use-id>/,
|
|
72
|
+
status: /<status>\s*([\s\S]*?)\s*<\/status>/,
|
|
73
|
+
summary: /<summary>\s*([\s\S]*?)\s*<\/summary>/,
|
|
74
|
+
note: /<note>\s*([\s\S]*?)\s*<\/note>/,
|
|
75
|
+
};
|
|
76
|
+
const INTERIM_TEXT = /waiting on its own background work|background work of its own still running/i;
|
|
77
|
+
function tagValue(body, pattern) {
|
|
78
|
+
const value = pattern.exec(body)?.[1]?.trim();
|
|
79
|
+
return value ? value : undefined;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Parses the first `<task-notification>` block in `text`. Whitespace and
|
|
83
|
+
* newlines between tags, missing fields, and unknown extra fields are
|
|
84
|
+
* tolerated; the block needs a `<task-id>`. Fields are read before the
|
|
85
|
+
* agent's own `<result>` output, which may quote anything.
|
|
86
|
+
*/
|
|
87
|
+
function parseTaskNotification(text) {
|
|
88
|
+
if (typeof text !== 'string')
|
|
89
|
+
return null;
|
|
90
|
+
const start = text.indexOf(NOTIFICATION_OPEN);
|
|
91
|
+
if (start < 0)
|
|
92
|
+
return null;
|
|
93
|
+
let body = text.slice(start + NOTIFICATION_OPEN.length);
|
|
94
|
+
const resultAt = body.indexOf('<result>');
|
|
95
|
+
let header = resultAt >= 0 ? body.slice(0, resultAt) : body;
|
|
96
|
+
if (resultAt < 0) {
|
|
97
|
+
const end = body.indexOf(NOTIFICATION_CLOSE);
|
|
98
|
+
if (end >= 0)
|
|
99
|
+
header = body = body.slice(0, end);
|
|
100
|
+
}
|
|
101
|
+
if (!TAG_PATTERNS.taskId.test(header))
|
|
102
|
+
header = body;
|
|
103
|
+
const taskId = tagValue(header, TAG_PATTERNS.taskId);
|
|
104
|
+
if (!taskId)
|
|
105
|
+
return null;
|
|
106
|
+
const toolUseId = tagValue(header, TAG_PATTERNS.toolUseId);
|
|
107
|
+
const status = tagValue(header, TAG_PATTERNS.status);
|
|
108
|
+
const summary = tagValue(header, TAG_PATTERNS.summary);
|
|
109
|
+
const note = tagValue(header, TAG_PATTERNS.note);
|
|
110
|
+
return {
|
|
111
|
+
taskId,
|
|
112
|
+
...(toolUseId ? { toolUseId } : {}),
|
|
113
|
+
...(status ? { status } : {}),
|
|
114
|
+
...(summary ? { summary } : {}),
|
|
115
|
+
interim: INTERIM_TEXT.test(summary ?? '') || INTERIM_TEXT.test(note ?? ''),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The `<task-notification>` prompt of a raw Claude Code attachment line
|
|
120
|
+
* (`{ type: 'attachment', attachment: { type: 'queued_command', commandMode:
|
|
121
|
+
* 'task-notification', prompt } }`), which is how a notification that lands
|
|
122
|
+
* mid-turn is recorded. `null` for any other value.
|
|
123
|
+
*/
|
|
124
|
+
function taskNotificationPromptFromAttachment(raw) {
|
|
125
|
+
if (!raw || typeof raw !== 'object')
|
|
126
|
+
return null;
|
|
127
|
+
const line = raw;
|
|
128
|
+
if (line.type !== 'attachment' || !line.attachment || typeof line.attachment !== 'object') {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
const attachment = line.attachment;
|
|
132
|
+
return attachment.type === 'queued_command' &&
|
|
133
|
+
attachment.commandMode === 'task-notification' &&
|
|
134
|
+
typeof attachment.prompt === 'string'
|
|
135
|
+
? attachment.prompt
|
|
136
|
+
: null;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* The synthetic `system` event `extractSessionEvents(raw, {
|
|
140
|
+
* includeTaskNotifications: true })` makes from a mid-turn task-notification
|
|
141
|
+
* attachment. It is bookkeeping, not a message or a prompt.
|
|
142
|
+
*/
|
|
143
|
+
function isTaskNotificationSystemEvent(event) {
|
|
144
|
+
return event.type === 'system' && event.origin?.kind === 'task-notification';
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The task notification an event carries: a user line with
|
|
148
|
+
* `origin.kind === 'task-notification'` or whose text starts with
|
|
149
|
+
* `<task-notification>`, or the opt-in synthetic system event. `null` otherwise.
|
|
150
|
+
*/
|
|
151
|
+
function taskNotificationFromEvent(event) {
|
|
152
|
+
if (event.type !== 'user' && event.type !== 'system')
|
|
153
|
+
return null;
|
|
154
|
+
const flagged = event.origin?.kind === 'task-notification';
|
|
155
|
+
if (event.type === 'system' && !flagged)
|
|
156
|
+
return null;
|
|
157
|
+
const text = contentText(event.message?.content);
|
|
158
|
+
if (!flagged && !NOTIFICATION_PREFIX.test(text))
|
|
159
|
+
return null;
|
|
160
|
+
return parseTaskNotification(text);
|
|
161
|
+
}
|
|
162
|
+
/** Maps a notification `<status>` to an outcome; unknown or missing statuses read as `completed`. */
|
|
163
|
+
function taskNotificationOutcome(status) {
|
|
164
|
+
switch (status?.toLowerCase()) {
|
|
165
|
+
case 'failed':
|
|
166
|
+
return 'failed';
|
|
167
|
+
case 'killed':
|
|
168
|
+
return 'killed';
|
|
169
|
+
case 'stopped':
|
|
170
|
+
return 'stopped';
|
|
171
|
+
default:
|
|
172
|
+
return 'completed';
|
|
173
|
+
}
|
|
174
|
+
}
|
package/dist/transcript.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.projectSessionTranscript = projectSessionTranscript;
|
|
4
4
|
const usageNormalization_1 = require("./usageNormalization");
|
|
5
|
+
const subagentSpawn_1 = require("./subagentSpawn");
|
|
5
6
|
const EMPTY_TOTALS = {
|
|
6
7
|
uncachedInputTokens: 0,
|
|
7
8
|
cacheReadTokens: 0,
|
|
@@ -28,6 +29,9 @@ function projectSessionTranscript(events, options = {}) {
|
|
|
28
29
|
let cwd;
|
|
29
30
|
let gitBranch;
|
|
30
31
|
events.forEach((event, eventIndex) => {
|
|
32
|
+
// A reader's synthetic notice that a background task finished, not a message.
|
|
33
|
+
if ((0, subagentSpawn_1.isTaskNotificationSystemEvent)(event))
|
|
34
|
+
return;
|
|
31
35
|
if (nonEmpty(event.cwd))
|
|
32
36
|
cwd = event.cwd;
|
|
33
37
|
if (nonEmpty(event.gitBranch))
|
|
@@ -70,6 +70,7 @@ export interface ReasoningSummary {
|
|
|
70
70
|
body: string;
|
|
71
71
|
}
|
|
72
72
|
export declare function reasoningSummary(text: string): ReasoningSummary;
|
|
73
|
+
/** Whether a tool call spawns a subagent (`Agent`, or its earlier name `Task`). */
|
|
73
74
|
export declare function isAssistantTurnSubagentTool(toolName: string | undefined): boolean;
|
|
74
75
|
export declare function segmentAssistantTurn(events: readonly AssistantTurnEvent[], options?: SegmentAssistantTurnOptions): AssistantTurnProjection;
|
|
75
76
|
export declare function assistantTurnEventsFromSessionEvents(events: readonly SessionEvent[]): AssistantTurnEvent[];
|
|
@@ -6,12 +6,12 @@ exports.segmentAssistantTurn = segmentAssistantTurn;
|
|
|
6
6
|
exports.assistantTurnEventsFromSessionEvents = assistantTurnEventsFromSessionEvents;
|
|
7
7
|
exports.extractTurnSubagents = extractTurnSubagents;
|
|
8
8
|
const toolSummary_1 = require("../formatters/toolSummary");
|
|
9
|
+
const subagentSpawn_1 = require("../subagentSpawn");
|
|
9
10
|
const DEFAULT_NARRATION_CHARS = 4000;
|
|
10
11
|
const DEFAULT_REASONING_CHARS = 8000;
|
|
11
12
|
const DEFAULT_PROCESS_STEPS = 60;
|
|
12
13
|
const DEFAULT_REASONING_BLOCKS = 24;
|
|
13
14
|
const DEFAULT_TOOL_INPUT_CHARS = 200;
|
|
14
|
-
const SUBAGENT_TOOL_NAMES = new Set(['Task']);
|
|
15
15
|
const LABEL_MAX = 80;
|
|
16
16
|
const TITLE_RE = /^\s*\*\*([^*\n]+)\*\*[ \t]*(?:\n+([\s\S]*))?$/;
|
|
17
17
|
const TITLE_MAX = 100;
|
|
@@ -29,8 +29,9 @@ function reasoningSummary(text) {
|
|
|
29
29
|
}
|
|
30
30
|
return { title: null, body: trimmed };
|
|
31
31
|
}
|
|
32
|
+
/** Whether a tool call spawns a subagent (`Agent`, or its earlier name `Task`). */
|
|
32
33
|
function isAssistantTurnSubagentTool(toolName) {
|
|
33
|
-
return
|
|
34
|
+
return (0, subagentSpawn_1.isSubagentSpawnTool)(toolName);
|
|
34
35
|
}
|
|
35
36
|
function segmentAssistantTurn(events, options = {}) {
|
|
36
37
|
const maxNarrationChars = options.maxNarrationChars ?? DEFAULT_NARRATION_CHARS;
|
|
@@ -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);
|
|
@@ -11,6 +11,7 @@
|
|
|
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");
|
|
14
15
|
/**
|
|
15
16
|
* Converts a single SessionEvent to one or more FollowEvents.
|
|
16
17
|
*
|
|
@@ -151,6 +152,20 @@ function toFollowEvents(event, providerId) {
|
|
|
151
152
|
break;
|
|
152
153
|
}
|
|
153
154
|
case 'system': {
|
|
155
|
+
// A background task's notice (opt-in reader event): its summary, not the raw block.
|
|
156
|
+
const notification = (0, subagentSpawn_1.isTaskNotificationSystemEvent)(event)
|
|
157
|
+
? (0, subagentSpawn_1.taskNotificationFromEvent)(event)
|
|
158
|
+
: null;
|
|
159
|
+
if (notification) {
|
|
160
|
+
events.push({
|
|
161
|
+
providerId,
|
|
162
|
+
type: 'system',
|
|
163
|
+
timestamp: ts,
|
|
164
|
+
summary: notification.summary ?? `Background task ${notification.status ?? 'finished'}`,
|
|
165
|
+
raw: event,
|
|
166
|
+
});
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
154
169
|
const text = extractTextContent(event.message?.content);
|
|
155
170
|
const label = event.message?.sourceLabel || event.message?.role || 'system';
|
|
156
171
|
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,6 +39,8 @@ 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");
|
|
44
46
|
const jsonl_1 = require("../parsers/jsonl");
|
|
@@ -98,6 +100,63 @@ exports.JsonlTailSymlinkError = JsonlTailSymlinkError;
|
|
|
98
100
|
function createJsonlTail(options) {
|
|
99
101
|
return new JsonlTailReader(options);
|
|
100
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Records a {@link JsonlTailCheckpoint} for byte `offset` of `filePath`
|
|
105
|
+
* without a tail, for readers that keep their own offset (a snapshot that
|
|
106
|
+
* stores a reader position, for example). The checkpoint carries the file's
|
|
107
|
+
* current device and inode and a digest of its first bytes, so
|
|
108
|
+
* {@link verifyJsonlCheckpoint} (or {@link JsonlTail.resumeFrom}) can later
|
|
109
|
+
* tell whether `offset` still refers to the same, append-only content.
|
|
110
|
+
*
|
|
111
|
+
* Returns `null` when the file is missing or unreadable, or when `offset` is
|
|
112
|
+
* not a resume-safe position in it: negative, past the end, or not 0 and not
|
|
113
|
+
* just after a newline. Follows symbolic links.
|
|
114
|
+
*/
|
|
115
|
+
function captureJsonlCheckpoint(filePath, offset) {
|
|
116
|
+
if (!Number.isSafeInteger(offset) || offset < 0)
|
|
117
|
+
return null;
|
|
118
|
+
let fd = null;
|
|
119
|
+
try {
|
|
120
|
+
fd = fs.openSync(filePath, 'r');
|
|
121
|
+
const stat = fs.fstatSync(fd);
|
|
122
|
+
if (offset > stat.size)
|
|
123
|
+
return null;
|
|
124
|
+
if (offset > 0 && readAt(fd, offset - 1, 1)[0] !== 0x0a)
|
|
125
|
+
return null;
|
|
126
|
+
return checkpointOf(offset, stat, (0, fileHead_1.fileHeadOf)(readPrefix(fd, stat)));
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
closeQuietly(fd);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Whether `checkpoint` (any value, typically parsed from JSON) is a valid
|
|
137
|
+
* {@link JsonlTailCheckpoint} that still describes `filePath`: same device
|
|
138
|
+
* and inode, the recorded first bytes unchanged, and its offset within the
|
|
139
|
+
* file and just after a newline (or 0). This is the check
|
|
140
|
+
* {@link JsonlTail.resumeFrom} applies. `false` on any error, including a
|
|
141
|
+
* missing file. Follows symbolic links.
|
|
142
|
+
*/
|
|
143
|
+
function verifyJsonlCheckpoint(filePath, checkpoint) {
|
|
144
|
+
const parsed = parseCheckpoint(checkpoint);
|
|
145
|
+
if (parsed === null)
|
|
146
|
+
return false;
|
|
147
|
+
let fd = null;
|
|
148
|
+
try {
|
|
149
|
+
fd = fs.openSync(filePath, 'r');
|
|
150
|
+
const stat = fs.fstatSync(fd);
|
|
151
|
+
return checkpointMatches(parsed, fd, stat, readPrefix(fd, stat));
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
closeQuietly(fd);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
101
160
|
class JsonlTailReader {
|
|
102
161
|
options;
|
|
103
162
|
active = false;
|
|
@@ -330,7 +389,7 @@ class JsonlTailReader {
|
|
|
330
389
|
}
|
|
331
390
|
probeSync(fd, stat) {
|
|
332
391
|
return {
|
|
333
|
-
prefix:
|
|
392
|
+
prefix: readPrefix(fd, stat),
|
|
334
393
|
before: this.committedOffset > 0 && this.committedOffset <= stat.size
|
|
335
394
|
? readAt(fd, this.committedOffset - 1, 1)
|
|
336
395
|
: null,
|
|
@@ -450,12 +509,7 @@ class JsonlTailReader {
|
|
|
450
509
|
this.position(Math.max(0, offset));
|
|
451
510
|
}
|
|
452
511
|
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
|
-
};
|
|
512
|
+
return checkpointOf(this.committedOffset, this.identity, this.head);
|
|
459
513
|
}
|
|
460
514
|
resumeFrom(checkpoint) {
|
|
461
515
|
this.generation += 1;
|
|
@@ -465,7 +519,7 @@ class JsonlTailReader {
|
|
|
465
519
|
try {
|
|
466
520
|
fd = this.openSync();
|
|
467
521
|
const stat = fs.fstatSync(fd);
|
|
468
|
-
const prefix =
|
|
522
|
+
const prefix = readPrefix(fd, stat);
|
|
469
523
|
const matches = parsed !== null && checkpointMatches(parsed, fd, stat, prefix);
|
|
470
524
|
const offset = matches
|
|
471
525
|
? parsed.offset
|
|
@@ -597,6 +651,15 @@ function parseCheckpoint(value) {
|
|
|
597
651
|
prefixDigest: parsedDigest,
|
|
598
652
|
};
|
|
599
653
|
}
|
|
654
|
+
/** A fresh checkpoint for `offset` in the file with `identity` and recorded `head`. */
|
|
655
|
+
function checkpointOf(offset, identity, head) {
|
|
656
|
+
return {
|
|
657
|
+
version: exports.JSONL_TAIL_CHECKPOINT_VERSION,
|
|
658
|
+
offset,
|
|
659
|
+
identity: identity ? { dev: identity.dev, ino: identity.ino } : null,
|
|
660
|
+
prefixDigest: head ? { bytes: head.bytes, hash: head.hash } : null,
|
|
661
|
+
};
|
|
662
|
+
}
|
|
600
663
|
/** Whether `checkpoint` still describes the open file. */
|
|
601
664
|
function checkpointMatches(checkpoint, fd, stat, prefix) {
|
|
602
665
|
const { offset, identity, prefixDigest } = checkpoint;
|
|
@@ -639,6 +702,10 @@ function readAt(fd, position, length) {
|
|
|
639
702
|
const count = length > 0 ? fs.readSync(fd, buffer, 0, length, position) : 0;
|
|
640
703
|
return buffer.subarray(0, count);
|
|
641
704
|
}
|
|
705
|
+
/** The file's first `min(HEAD_BYTES, size)` bytes: what a checkpoint digests. */
|
|
706
|
+
function readPrefix(fd, stat) {
|
|
707
|
+
return readAt(fd, 0, Math.min(fileHead_1.HEAD_BYTES, stat.size));
|
|
708
|
+
}
|
|
642
709
|
async function readAtAsync(handle, position, length) {
|
|
643
710
|
const buffer = Buffer.alloc(length);
|
|
644
711
|
const count = length > 0 ? (await handle.read(buffer, 0, length, position)).bytesRead : 0;
|