sidekick-shared 0.29.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +198 -56
  3. package/dist/aggregation/EventAggregator.d.ts +96 -2
  4. package/dist/aggregation/EventAggregator.js +535 -42
  5. package/dist/aggregation/snapshot.d.ts +37 -8
  6. package/dist/aggregation/snapshot.js +31 -9
  7. package/dist/aggregation/types.d.ts +38 -1
  8. package/dist/browser.d.ts +9 -1
  9. package/dist/browser.js +29 -3
  10. package/dist/context/sessionContext.js +8 -0
  11. package/dist/esm/aggregation/EventAggregator.js +535 -42
  12. package/dist/esm/aggregation/snapshot.js +31 -9
  13. package/dist/esm/browser.js +3 -0
  14. package/dist/esm/context/sessionContext.js +8 -0
  15. package/dist/esm/fileContinuity.js +149 -0
  16. package/dist/esm/formatters/toolSummary.js +4 -1
  17. package/dist/esm/index.js +7 -2
  18. package/dist/esm/modelCatalog.js +5 -4
  19. package/dist/esm/modelContext.js +4 -3
  20. package/dist/esm/modelInfo.js +4 -3
  21. package/dist/esm/node.js +3 -1
  22. package/dist/esm/observedContextWindows.js +2 -1
  23. package/dist/esm/ownKey.js +26 -0
  24. package/dist/esm/parsers/codexParser.js +372 -7
  25. package/dist/esm/parsers/openCodeParser.js +2 -1
  26. package/dist/esm/parsers/subagentEndState.js +61 -0
  27. package/dist/esm/parsers/subagentScanner.js +37 -12
  28. package/dist/esm/parsers/subagentTraceParser.js +103 -37
  29. package/dist/esm/pricingCatalog.js +3 -2
  30. package/dist/esm/promptHistory.js +22 -11
  31. package/dist/esm/providers/claudeCode.js +60 -29
  32. package/dist/esm/providers/codex.js +235 -201
  33. package/dist/esm/providers/codexCompressedReader.js +184 -0
  34. package/dist/esm/providers/codexDatabase.js +47 -2
  35. package/dist/esm/providers/codexRollouts.js +20 -2
  36. package/dist/esm/providers/codexSubagents.js +456 -0
  37. package/dist/esm/providers/compressedRollouts.js +330 -0
  38. package/dist/esm/providers/factory.js +4 -1
  39. package/dist/esm/providers/rolloutWalker.js +32 -4
  40. package/dist/esm/schemas/sessionEvent.js +63 -14
  41. package/dist/esm/sessionMonitor.js +7 -1
  42. package/dist/esm/sessionPreviews.js +9 -0
  43. package/dist/esm/sessionPromptHistory.js +5 -3
  44. package/dist/esm/subagentActivity.js +125 -0
  45. package/dist/esm/subagentDisplayState.js +81 -0
  46. package/dist/esm/subagentSpawn.js +363 -0
  47. package/dist/esm/testing/codexSubagentFixtures.js +336 -0
  48. package/dist/esm/toolCallOrigins.js +17 -10
  49. package/dist/esm/toolCategory.js +6 -2
  50. package/dist/esm/transcript.js +8 -0
  51. package/dist/esm/turns/assistantTurn.js +6 -2
  52. package/dist/esm/usage/claudeUsageDedupe.js +68 -0
  53. package/dist/esm/usage/usageEvents.js +4 -1
  54. package/dist/esm/watchers/eventBridge.js +45 -0
  55. package/dist/esm/watchers/factory.js +5 -3
  56. package/dist/esm/watchers/jsonlTail.js +82 -48
  57. package/dist/esm/watchers/jsonlWatcher.js +61 -4
  58. package/dist/esm/watchers/providerReaderWatcher.js +18 -4
  59. package/dist/esm/zaiQuota.js +2 -1
  60. package/dist/fileContinuity.d.ts +95 -0
  61. package/dist/fileContinuity.js +187 -0
  62. package/dist/formatters/toolSummary.js +4 -1
  63. package/dist/index.d.ts +19 -7
  64. package/dist/index.js +48 -10
  65. package/dist/modelCatalog.js +5 -4
  66. package/dist/modelContext.js +4 -3
  67. package/dist/modelInfo.js +4 -3
  68. package/dist/node.d.ts +5 -1
  69. package/dist/node.js +18 -2
  70. package/dist/observedContextWindows.js +2 -1
  71. package/dist/ownKey.d.ts +16 -0
  72. package/dist/ownKey.js +30 -0
  73. package/dist/parsers/codexParser.d.ts +107 -0
  74. package/dist/parsers/codexParser.js +373 -7
  75. package/dist/parsers/openCodeParser.js +2 -1
  76. package/dist/parsers/subagentEndState.d.ts +20 -0
  77. package/dist/parsers/subagentEndState.js +65 -0
  78. package/dist/parsers/subagentScanner.d.ts +19 -6
  79. package/dist/parsers/subagentScanner.js +37 -12
  80. package/dist/parsers/subagentTraceParser.d.ts +33 -5
  81. package/dist/parsers/subagentTraceParser.js +103 -37
  82. package/dist/pricingCatalog.js +3 -2
  83. package/dist/promptHistory.d.ts +13 -3
  84. package/dist/promptHistory.js +22 -11
  85. package/dist/providers/claudeCode.js +59 -28
  86. package/dist/providers/codex.d.ts +35 -5
  87. package/dist/providers/codex.js +232 -198
  88. package/dist/providers/codexCompressedReader.d.ts +58 -0
  89. package/dist/providers/codexCompressedReader.js +221 -0
  90. package/dist/providers/codexDatabase.d.ts +25 -0
  91. package/dist/providers/codexDatabase.js +48 -2
  92. package/dist/providers/codexRollouts.d.ts +16 -2
  93. package/dist/providers/codexRollouts.js +20 -2
  94. package/dist/providers/codexSubagents.d.ts +66 -0
  95. package/dist/providers/codexSubagents.js +495 -0
  96. package/dist/providers/compressedRollouts.d.ts +114 -0
  97. package/dist/providers/compressedRollouts.js +380 -0
  98. package/dist/providers/factory.d.ts +5 -0
  99. package/dist/providers/factory.js +4 -1
  100. package/dist/providers/rolloutWalker.d.ts +15 -2
  101. package/dist/providers/rolloutWalker.js +33 -4
  102. package/dist/providers/types.d.ts +36 -1
  103. package/dist/schemas/index.d.ts +1 -0
  104. package/dist/schemas/sessionEvent.d.ts +51 -10
  105. package/dist/schemas/sessionEvent.js +63 -14
  106. package/dist/sessionMonitor.d.ts +6 -0
  107. package/dist/sessionMonitor.js +7 -1
  108. package/dist/sessionPreviews.js +9 -0
  109. package/dist/sessionPromptHistory.d.ts +5 -0
  110. package/dist/sessionPromptHistory.js +5 -36
  111. package/dist/subagentActivity.d.ts +91 -0
  112. package/dist/subagentActivity.js +132 -0
  113. package/dist/subagentDisplayState.d.ts +81 -0
  114. package/dist/subagentDisplayState.js +86 -0
  115. package/dist/subagentSpawn.d.ts +175 -0
  116. package/dist/subagentSpawn.js +385 -0
  117. package/dist/testing/codexSubagentFixtures.d.ts +48 -0
  118. package/dist/testing/codexSubagentFixtures.js +344 -0
  119. package/dist/toolCallOrigins.d.ts +5 -0
  120. package/dist/toolCallOrigins.js +17 -10
  121. package/dist/toolCategory.js +6 -2
  122. package/dist/transcript.js +8 -0
  123. package/dist/turns/assistantTurn.d.ts +4 -0
  124. package/dist/turns/assistantTurn.js +6 -2
  125. package/dist/types/codex.d.ts +71 -2
  126. package/dist/types/sessionEvent.d.ts +38 -3
  127. package/dist/usage/claudeUsageDedupe.d.ts +31 -0
  128. package/dist/usage/claudeUsageDedupe.js +68 -0
  129. package/dist/usage/usageEvents.d.ts +4 -1
  130. package/dist/usage/usageEvents.js +4 -1
  131. package/dist/watchers/eventBridge.js +45 -0
  132. package/dist/watchers/factory.d.ts +6 -0
  133. package/dist/watchers/factory.js +5 -3
  134. package/dist/watchers/jsonlTail.d.ts +27 -4
  135. package/dist/watchers/jsonlTail.js +84 -48
  136. package/dist/watchers/jsonlWatcher.d.ts +22 -1
  137. package/dist/watchers/jsonlWatcher.js +60 -3
  138. package/dist/watchers/providerReaderWatcher.d.ts +7 -0
  139. package/dist/watchers/providerReaderWatcher.js +18 -4
  140. package/dist/watchers/types.d.ts +12 -0
  141. package/dist/zaiQuota.js +2 -1
  142. package/package.json +1 -1
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Subagent lifecycle records as `system` session events.
3
+ *
4
+ * Codex records its multi-agent lifecycle in the parent's rollout:
5
+ * `item_completed` → `SubAgentActivity` items link a `spawn_agent` call to the
6
+ * child thread it started (`started`), mark each finished child turn
7
+ * (`completed`, Codex 0.150+), and note `followup_task` / `send_message`
8
+ * calls (`interacted`) and `interrupt_agent` calls (`interrupted`); a child's
9
+ * report arrives as an `agent_message` whose text starts
10
+ * `Message Type: FINAL_ANSWER`. With
11
+ * `CodexRolloutParserOptions.includeSubagentActivity`, the parser turns each
12
+ * into one `system` event with `origin: { kind: 'subagent-activity' }` whose
13
+ * `message.content` is a single `{ type: 'subagent_activity', ... }` block.
14
+ *
15
+ * Those events are bookkeeping, not messages: transcripts, context
16
+ * attribution, and message counts skip them, as they do task notifications.
17
+ *
18
+ * Pure: no filesystem access. Browser-safe.
19
+ *
20
+ * @module subagentActivity
21
+ */
22
+ /** `origin.kind` of a subagent-activity `system` event. */
23
+ export const SUBAGENT_ACTIVITY_ORIGIN_KIND = 'subagent-activity';
24
+ /** `type` of the content block that carries the activity. */
25
+ export const SUBAGENT_ACTIVITY_BLOCK_TYPE = 'subagent_activity';
26
+ const KINDS = new Set([
27
+ 'started',
28
+ 'completed',
29
+ 'interacted',
30
+ 'interrupted',
31
+ 'final-answer',
32
+ ]);
33
+ const STRING_FIELDS = [
34
+ 'callId',
35
+ 'tool',
36
+ 'turnId',
37
+ 'agentThreadId',
38
+ 'agentPath',
39
+ 'recipient',
40
+ 'model',
41
+ 'reasoningEffort',
42
+ 'parentThreadId',
43
+ 'parentTurnId',
44
+ 'text',
45
+ ];
46
+ /** Whether an event is a subagent-activity `system` event (see the module docs). */
47
+ export function isSubagentActivitySystemEvent(event) {
48
+ return event.type === 'system' && event.origin?.kind === SUBAGENT_ACTIVITY_ORIGIN_KIND;
49
+ }
50
+ /**
51
+ * The activity a subagent-activity `system` event carries, copied with only
52
+ * its known string fields; `null` for any other event or a malformed block.
53
+ */
54
+ export function subagentActivityFromEvent(event) {
55
+ if (!isSubagentActivitySystemEvent(event))
56
+ return null;
57
+ const content = event.message?.content;
58
+ if (!Array.isArray(content))
59
+ return null;
60
+ for (const block of content) {
61
+ if (!block || typeof block !== 'object')
62
+ continue;
63
+ const raw = block;
64
+ if (raw.type !== SUBAGENT_ACTIVITY_BLOCK_TYPE)
65
+ continue;
66
+ if (typeof raw.kind !== 'string' || !KINDS.has(raw.kind))
67
+ return null;
68
+ const activity = { kind: raw.kind };
69
+ for (const field of STRING_FIELDS) {
70
+ const value = raw[field];
71
+ if (typeof value === 'string' && value.length > 0)
72
+ activity[field] = value;
73
+ }
74
+ return activity;
75
+ }
76
+ return null;
77
+ }
78
+ /** A one-line, content-free description, e.g. `Subagent /root/review started`. */
79
+ export function describeSubagentActivity(activity) {
80
+ const who = activity.agentPath ?? activity.agentThreadId ?? 'subagent';
81
+ const name = `Subagent ${who}`;
82
+ switch (activity.kind) {
83
+ case 'started':
84
+ return `${name} started`;
85
+ case 'completed':
86
+ return `${name} finished a turn`;
87
+ case 'interacted':
88
+ return activity.tool === 'send_message'
89
+ ? `Message sent to subagent ${who}`
90
+ : `${name} given a follow-up task`;
91
+ case 'interrupted':
92
+ return `${name} interrupted`;
93
+ case 'final-answer':
94
+ return `${name} reported its final answer`;
95
+ }
96
+ }
97
+ const MESSAGE_HEADER = /^Message Type:[ \t]*([A-Z_]+)[ \t]*(?:\r?\n|$)/;
98
+ /**
99
+ * Parses a Codex inter-agent message text. Returns `null` unless it starts
100
+ * with a `Message Type:` line.
101
+ */
102
+ export function parseCodexAgentMessageText(text) {
103
+ if (typeof text !== 'string')
104
+ return null;
105
+ const header = MESSAGE_HEADER.exec(text);
106
+ if (!header)
107
+ return null;
108
+ const rest = text.slice(header[0].length);
109
+ const payloadAt = /(?:^|\n)Payload:[ \t]*(?:\r?\n|$)/.exec(rest);
110
+ const head = payloadAt ? rest.slice(0, payloadAt.index) : rest;
111
+ const payload = payloadAt ? rest.slice(payloadAt.index + payloadAt[0].length) : '';
112
+ const field = (name) => {
113
+ const match = new RegExp(`(?:^|\\n)${name}:[ \\t]*([^\\n]*)`).exec(head);
114
+ const value = match?.[1]?.trim();
115
+ return value ? value : undefined;
116
+ };
117
+ const taskName = field('Task name');
118
+ const sender = field('Sender');
119
+ return {
120
+ messageType: header[1],
121
+ ...(taskName ? { taskName } : {}),
122
+ ...(sender ? { sender } : {}),
123
+ payload,
124
+ };
125
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * How to show a subagent: its lifecycle's outcome once something settled
3
+ * it, otherwise `running`, unless its transcript or its parent says it has
4
+ * stopped in all but name.
5
+ *
6
+ * Some background agents never get a final notification (they stopped after
7
+ * an interim "may resume" notice, the user stopped them from the UI, or a
8
+ * restart killed them). Their lifecycles stay `running`. This rule reads
9
+ * them as:
10
+ *
11
+ * - `finished-unreported`: the transcript ended cleanly (last assistant turn
12
+ * `end_turn` / `stop_sequence`, no tool call unanswered) and has been quiet
13
+ * for {@link SUBAGENT_UNREPORTED_QUIET_MS}.
14
+ * - `ended`: neither the parent session nor the transcript has been active for
15
+ * {@link SUBAGENT_PARENT_IDLE_ENDED_MS}.
16
+ *
17
+ * Neither is stored: the state is recomputed from the latest data on every
18
+ * call, so an agent that resumes (its transcript grows, or a later
19
+ * notification reopens its lifecycle) reads as `running` again. Silence alone
20
+ * is never enough for `finished-unreported`: live agents can be quiet for
21
+ * hours.
22
+ *
23
+ * Pure: no filesystem access. Browser-safe.
24
+ *
25
+ * @module subagentDisplayState
26
+ */
27
+ /** Quiet time after a clean end before a running agent reads as finished without a report (2 min). */
28
+ export const SUBAGENT_UNREPORTED_QUIET_MS = 2 * 60_000;
29
+ /** Parent and transcript idle time after which a running agent reads as ended (6 h). */
30
+ export const SUBAGENT_PARENT_IDLE_ENDED_MS = 6 * 60 * 60_000;
31
+ function toMs(value) {
32
+ if (value === null || value === undefined)
33
+ return undefined;
34
+ const ms = value instanceof Date ? value.getTime() : new Date(value).getTime();
35
+ return Number.isFinite(ms) ? ms : undefined;
36
+ }
37
+ /**
38
+ * Resolves how to show a subagent (see the module docs). Settled lifecycles
39
+ * map to their outcome (`completed` when none is recorded). A running one is
40
+ * `stopped` / `completed` when its meta flags say so, `finished-unreported`
41
+ * after a clean end and {@link SUBAGENT_UNREPORTED_QUIET_MS} of quiet,
42
+ * `ended` when the parent and transcript have both been idle for longer than
43
+ * {@link SUBAGENT_PARENT_IDLE_ENDED_MS}, and `running` otherwise.
44
+ */
45
+ export function resolveSubagentDisplayState(lifecycle, trace, options) {
46
+ if (lifecycle.status !== 'running') {
47
+ return { state: lifecycle.outcome ?? 'completed', reason: 'settled' };
48
+ }
49
+ if (trace?.stoppedByUser)
50
+ return { state: 'stopped', reason: 'meta' };
51
+ if (trace?.finished)
52
+ return { state: 'completed', reason: 'meta' };
53
+ const now = toMs(options.now) ?? Date.now();
54
+ const lastActivity = toMs(trace?.lastActivityAt);
55
+ if (trace?.endedCleanly &&
56
+ lastActivity !== undefined &&
57
+ now - lastActivity >= SUBAGENT_UNREPORTED_QUIET_MS) {
58
+ return { state: 'finished-unreported', reason: 'clean-end' };
59
+ }
60
+ const parentLast = toMs(options.parentLastActivity);
61
+ if (parentLast !== undefined) {
62
+ const latest = Math.max(parentLast, lastActivity ?? -Infinity);
63
+ if (now - latest > SUBAGENT_PARENT_IDLE_ENDED_MS) {
64
+ return { state: 'ended', reason: 'parent-idle' };
65
+ }
66
+ }
67
+ return { state: 'running' };
68
+ }
69
+ const LABELS = {
70
+ running: 'running',
71
+ 'finished-unreported': 'finished (no report)',
72
+ ended: 'ended',
73
+ completed: 'completed',
74
+ failed: 'failed',
75
+ stopped: 'stopped',
76
+ killed: 'killed',
77
+ };
78
+ /** A short label: `running`, `finished (no report)`, `ended`, `completed`, `failed`, `stopped`, `killed`. */
79
+ export function subagentDisplayLabel(state) {
80
+ return LABELS[state] ?? state;
81
+ }
@@ -0,0 +1,363 @@
1
+ /**
2
+ * Recognizes subagent spawns, background-task notifications, and the other
3
+ * records that settle a background agent.
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. When no
10
+ * notification comes, a `task_status` attachment, an `agents_killed` system
11
+ * line, or a plain-text "background agents were stopped by the user" notice
12
+ * can still say how it ended.
13
+ *
14
+ * Codex spawns with `spawn_agent` (parsed as `Spawn_agent`); its result
15
+ * (`{"task_name": "/root/x"}`) is always a background launch.
16
+ *
17
+ * Pure: no filesystem access, so it is safe for browser bundles.
18
+ *
19
+ * @module subagentSpawn
20
+ */
21
+ /**
22
+ * Claude Code / OpenCode tool names that spawn a subagent: `Agent` (current)
23
+ * and `Task` (earlier name). Codex's `spawn_agent` is not in this list;
24
+ * {@link isSubagentSpawnTool} recognizes it as well.
25
+ */
26
+ export const SUBAGENT_SPAWN_TOOL_NAMES = Object.freeze(['Agent', 'Task']);
27
+ const SPAWN_TOOLS = new Set(SUBAGENT_SPAWN_TOOL_NAMES);
28
+ /** Codex's raw spawn tool name, and the name the Codex parser normalizes it to. */
29
+ const CODEX_SPAWN_RAW = 'spawn_agent';
30
+ const CODEX_SPAWN_TOOLS = new Set([CODEX_SPAWN_RAW, 'Spawn_agent']);
31
+ /**
32
+ * Whether a tool call is Codex's `spawn_agent`: by name (`Spawn_agent` as
33
+ * parsed, or the raw `spawn_agent`), or by the raw name the Codex parser keeps
34
+ * in the input (`_sidekickRawToolName`).
35
+ */
36
+ export function isCodexSubagentSpawnTool(name, input) {
37
+ if (typeof name === 'string' && CODEX_SPAWN_TOOLS.has(name))
38
+ return true;
39
+ return (!!input &&
40
+ typeof input === 'object' &&
41
+ input._sidekickRawToolName === CODEX_SPAWN_RAW);
42
+ }
43
+ /**
44
+ * Whether a tool call spawns a subagent: one of
45
+ * {@link SUBAGENT_SPAWN_TOOL_NAMES} (`Agent`, `Task`), or Codex's
46
+ * `spawn_agent` (see {@link isCodexSubagentSpawnTool}; pass the call's input
47
+ * to also match on its raw name).
48
+ */
49
+ export function isSubagentSpawnTool(name, input) {
50
+ if (typeof name === 'string' && SPAWN_TOOLS.has(name))
51
+ return true;
52
+ return isCodexSubagentSpawnTool(name, input);
53
+ }
54
+ /**
55
+ * Recognizes a Codex `spawn_agent` result (`{"task_name": "/root/x"}`, as a
56
+ * JSON string or object). `null` for anything else.
57
+ */
58
+ export function parseCodexSpawnLaunch(result) {
59
+ let value = result;
60
+ if (typeof value !== 'string') {
61
+ const text = contentText(value);
62
+ value = text || value;
63
+ }
64
+ if (typeof value === 'string') {
65
+ const trimmed = value.trim();
66
+ if (!trimmed.startsWith('{'))
67
+ return null;
68
+ try {
69
+ value = JSON.parse(trimmed);
70
+ }
71
+ catch {
72
+ return null;
73
+ }
74
+ }
75
+ if (!value || typeof value !== 'object' || Array.isArray(value))
76
+ return null;
77
+ const taskName = value.task_name;
78
+ if (typeof taskName !== 'string')
79
+ return null;
80
+ return taskName ? { agentPath: taskName } : {};
81
+ }
82
+ const ASYNC_LAUNCH_PREFIX = /^\s*Async agent launched successfully\b/;
83
+ const AGENT_ID_LINE = /(?:^|\n)[ \t]*agentId:[ \t]*([A-Za-z0-9][\w.@-]*)/;
84
+ /**
85
+ * Plain text of tool-result or message content: a string as-is, or the text
86
+ * blocks of a content array joined by newlines.
87
+ */
88
+ export function contentText(content) {
89
+ if (typeof content === 'string')
90
+ return content;
91
+ if (!Array.isArray(content))
92
+ return '';
93
+ const parts = [];
94
+ for (const block of content) {
95
+ if (!block || typeof block !== 'object')
96
+ continue;
97
+ const typed = block;
98
+ if (typed.type === 'text' && typeof typed.text === 'string')
99
+ parts.push(typed.text);
100
+ }
101
+ return parts.join('\n');
102
+ }
103
+ /**
104
+ * Recognizes the immediate result of a background spawn
105
+ * (`Async agent launched successfully.` … `agentId: <id> …`). Accepts the
106
+ * result text or the raw tool-result content. Returns `null` for anything
107
+ * else, including a finished foreground agent's result.
108
+ */
109
+ export function parseAsyncAgentLaunch(result) {
110
+ const text = contentText(result);
111
+ if (!ASYNC_LAUNCH_PREFIX.test(text))
112
+ return null;
113
+ const match = AGENT_ID_LINE.exec(text);
114
+ return match ? { agentId: match[1] } : {};
115
+ }
116
+ const NOTIFICATION_OPEN = '<task-notification>';
117
+ const NOTIFICATION_CLOSE = '</task-notification>';
118
+ const NOTIFICATION_PREFIX = /^\s*<task-notification>/;
119
+ const TAG_PATTERNS = {
120
+ taskId: /<task-id>\s*([\s\S]*?)\s*<\/task-id>/,
121
+ toolUseId: /<tool-use-id>\s*([\s\S]*?)\s*<\/tool-use-id>/,
122
+ status: /<status>\s*([\s\S]*?)\s*<\/status>/,
123
+ summary: /<summary>\s*([\s\S]*?)\s*<\/summary>/,
124
+ note: /<note>\s*([\s\S]*?)\s*<\/note>/,
125
+ };
126
+ const INTERIM_TEXT = /waiting on its own background work|background work of its own still running/i;
127
+ function tagValue(body, pattern) {
128
+ const value = pattern.exec(body)?.[1]?.trim();
129
+ return value ? value : undefined;
130
+ }
131
+ /**
132
+ * Parses the first `<task-notification>` block in `text`. Whitespace and
133
+ * newlines between tags, missing fields, and unknown extra fields are
134
+ * tolerated; the block needs a `<task-id>`. Fields are read before the
135
+ * agent's own `<result>` output, which may quote anything.
136
+ */
137
+ export function parseTaskNotification(text) {
138
+ if (typeof text !== 'string')
139
+ return null;
140
+ const start = text.indexOf(NOTIFICATION_OPEN);
141
+ if (start < 0)
142
+ return null;
143
+ let body = text.slice(start + NOTIFICATION_OPEN.length);
144
+ const resultAt = body.indexOf('<result>');
145
+ let header = resultAt >= 0 ? body.slice(0, resultAt) : body;
146
+ if (resultAt < 0) {
147
+ const end = body.indexOf(NOTIFICATION_CLOSE);
148
+ if (end >= 0)
149
+ header = body = body.slice(0, end);
150
+ }
151
+ if (!TAG_PATTERNS.taskId.test(header))
152
+ header = body;
153
+ const taskId = tagValue(header, TAG_PATTERNS.taskId);
154
+ if (!taskId)
155
+ return null;
156
+ const toolUseId = tagValue(header, TAG_PATTERNS.toolUseId);
157
+ const status = tagValue(header, TAG_PATTERNS.status);
158
+ const summary = tagValue(header, TAG_PATTERNS.summary);
159
+ const note = tagValue(header, TAG_PATTERNS.note);
160
+ return {
161
+ taskId,
162
+ ...(toolUseId ? { toolUseId } : {}),
163
+ ...(status ? { status } : {}),
164
+ ...(summary ? { summary } : {}),
165
+ interim: INTERIM_TEXT.test(summary ?? '') || INTERIM_TEXT.test(note ?? ''),
166
+ };
167
+ }
168
+ /**
169
+ * The `<task-notification>` prompt of a raw Claude Code attachment line
170
+ * (`{ type: 'attachment', attachment: { type: 'queued_command', commandMode:
171
+ * 'task-notification', prompt } }`), which is how a notification that lands
172
+ * mid-turn is recorded. `null` for any other value.
173
+ */
174
+ export function taskNotificationPromptFromAttachment(raw) {
175
+ if (!raw || typeof raw !== 'object')
176
+ return null;
177
+ const line = raw;
178
+ if (line.type !== 'attachment' || !line.attachment || typeof line.attachment !== 'object') {
179
+ return null;
180
+ }
181
+ const attachment = line.attachment;
182
+ return attachment.type === 'queued_command' &&
183
+ attachment.commandMode === 'task-notification' &&
184
+ typeof attachment.prompt === 'string'
185
+ ? attachment.prompt
186
+ : null;
187
+ }
188
+ /**
189
+ * The synthetic `system` event `extractSessionEvents(raw, {
190
+ * includeTaskNotifications: true })` makes from a mid-turn task-notification
191
+ * attachment. It is bookkeeping, not a message or a prompt.
192
+ */
193
+ export function isTaskNotificationSystemEvent(event) {
194
+ return event.type === 'system' && event.origin?.kind === 'task-notification';
195
+ }
196
+ /**
197
+ * The task notification an event carries: a user line with
198
+ * `origin.kind === 'task-notification'` or whose text starts with
199
+ * `<task-notification>`, or the opt-in synthetic system event. `null` otherwise.
200
+ */
201
+ export function taskNotificationFromEvent(event) {
202
+ if (event.type !== 'user' && event.type !== 'system')
203
+ return null;
204
+ const flagged = event.origin?.kind === 'task-notification';
205
+ if (event.type === 'system' && !flagged)
206
+ return null;
207
+ const text = contentText(event.message?.content);
208
+ if (!flagged && !NOTIFICATION_PREFIX.test(text))
209
+ return null;
210
+ return parseTaskNotification(text);
211
+ }
212
+ /** Maps a notification `<status>` to an outcome; unknown or missing statuses read as `completed`. */
213
+ export function taskNotificationOutcome(status) {
214
+ switch (status?.toLowerCase()) {
215
+ case 'failed':
216
+ return 'failed';
217
+ case 'killed':
218
+ return 'killed';
219
+ case 'stopped':
220
+ return 'stopped';
221
+ default:
222
+ return 'completed';
223
+ }
224
+ }
225
+ const STOPPED_PLURAL = /^\s*(\d+)\s+background agents? (?:were|was) stopped by the user:\s*([\s\S]*?)\s*$/i;
226
+ const STOPPED_SINGULAR = /^\s*Background agent "([\s\S]*)" was stopped by the user\.?\s*$/i;
227
+ /**
228
+ * Parses Claude Code's plain-text notice that the user stopped background
229
+ * agents: `2 background agents were stopped by the user: "A", "B".` (a
230
+ * `queued_command` attachment's prompt) or `Background agent "A" was stopped
231
+ * by the user.` (a queue-operation line). `null` for any other text.
232
+ */
233
+ export function parseAgentsStoppedNotice(text) {
234
+ if (typeof text !== 'string')
235
+ return null;
236
+ const singular = STOPPED_SINGULAR.exec(text);
237
+ if (singular)
238
+ return { descriptions: [singular[1].trim()].filter(Boolean) };
239
+ const plural = STOPPED_PLURAL.exec(text);
240
+ if (!plural)
241
+ return null;
242
+ let list = plural[2].trim();
243
+ if (list.endsWith('.'))
244
+ list = list.slice(0, -1).trimEnd();
245
+ if (!list.startsWith('"') || !list.endsWith('"') || list.length < 2)
246
+ return null;
247
+ const descriptions = list
248
+ .slice(1, -1)
249
+ .split(/",\s*"/)
250
+ .map((description) => description.trim())
251
+ .filter(Boolean);
252
+ return descriptions.length > 0 ? { descriptions } : null;
253
+ }
254
+ /**
255
+ * The stop notice a task-notification event carries (a `queued_command`
256
+ * prompt that is plain text, not a `<task-notification>` block). `null`
257
+ * otherwise.
258
+ */
259
+ export function agentsStoppedNoticeFromEvent(event) {
260
+ if (!isTaskNotificationSystemEvent(event))
261
+ return null;
262
+ return parseAgentsStoppedNotice(contentText(event.message?.content));
263
+ }
264
+ /** Claude Code's `agents_killed` system line: the user killed every running background agent. */
265
+ export const AGENTS_KILLED_SUBTYPE = 'agents_killed';
266
+ /**
267
+ * Whether an event, or a raw Claude Code JSONL line, is an `agents_killed`
268
+ * system line (`{ type: 'system', subtype: 'agents_killed' }`).
269
+ */
270
+ export function isAgentsKilledSystemEvent(event) {
271
+ if (!event || typeof event !== 'object')
272
+ return false;
273
+ const line = event;
274
+ return line.type === 'system' && line.subtype === AGENTS_KILLED_SUBTYPE;
275
+ }
276
+ /** `origin.kind` of the opt-in `system` event made from a `task_status` attachment. */
277
+ export const TASK_STATUS_ORIGIN_KIND = 'task-status';
278
+ /** `type` of the content block that carries the status. */
279
+ export const TASK_STATUS_BLOCK_TYPE = 'task_status';
280
+ const AGENT_TASK_TYPE = 'local_agent';
281
+ function nonEmpty(value) {
282
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
283
+ }
284
+ /**
285
+ * The background agent's status a raw Claude Code `task_status` attachment
286
+ * line reports (`{ type: 'attachment', attachment: { type: 'task_status',
287
+ * taskId, taskType: 'local_agent', status, description } }`). `null` for any
288
+ * other value, including background shell tasks (`local_bash`).
289
+ */
290
+ export function taskStatusFromAttachment(raw) {
291
+ if (!raw || typeof raw !== 'object')
292
+ return null;
293
+ const line = raw;
294
+ if (line.type !== 'attachment' || !line.attachment || typeof line.attachment !== 'object') {
295
+ return null;
296
+ }
297
+ const attachment = line.attachment;
298
+ if (attachment.type !== 'task_status' || attachment.taskType !== AGENT_TASK_TYPE)
299
+ return null;
300
+ const taskId = nonEmpty(attachment.taskId);
301
+ const status = nonEmpty(attachment.status);
302
+ if (!taskId || !status)
303
+ return null;
304
+ const description = nonEmpty(attachment.description);
305
+ return { taskId, taskType: AGENT_TASK_TYPE, status, ...(description ? { description } : {}) };
306
+ }
307
+ /** Whether a task status is final (anything but `running` / `pending`). */
308
+ export function isFinalTaskStatus(status) {
309
+ if (!status)
310
+ return false;
311
+ const lower = status.toLowerCase();
312
+ return lower !== 'running' && lower !== 'pending';
313
+ }
314
+ /**
315
+ * The synthetic `system` event `extractSessionEvents(raw, {
316
+ * includeTaskNotifications: true })` makes from a background agent's
317
+ * `task_status` attachment. It is bookkeeping, not a message or a prompt.
318
+ */
319
+ export function isTaskStatusSystemEvent(event) {
320
+ return event.type === 'system' && event.origin?.kind === TASK_STATUS_ORIGIN_KIND;
321
+ }
322
+ /** The status an opt-in task-status `system` event carries; `null` for any other event. */
323
+ export function taskStatusFromEvent(event) {
324
+ if (!isTaskStatusSystemEvent(event))
325
+ return null;
326
+ const content = event.message?.content;
327
+ if (!Array.isArray(content))
328
+ return null;
329
+ for (const block of content) {
330
+ if (!block || typeof block !== 'object')
331
+ continue;
332
+ const raw = block;
333
+ if (raw.type !== TASK_STATUS_BLOCK_TYPE)
334
+ continue;
335
+ const taskId = nonEmpty(raw.taskId);
336
+ const status = nonEmpty(raw.status);
337
+ if (!taskId || !status)
338
+ return null;
339
+ const description = nonEmpty(raw.description);
340
+ return {
341
+ taskId,
342
+ taskType: nonEmpty(raw.taskType) ?? AGENT_TASK_TYPE,
343
+ status,
344
+ ...(description ? { description } : {}),
345
+ };
346
+ }
347
+ return null;
348
+ }
349
+ /**
350
+ * A one-line description of a stop: `Background agents stopped by the user (2)`,
351
+ * or, for an `agents_killed` line (`null`), `Background agents stopped by the user`.
352
+ */
353
+ export function describeAgentsStopped(notice) {
354
+ const count = notice?.descriptions.length;
355
+ return count
356
+ ? `Background agents stopped by the user (${count})`
357
+ : 'Background agents stopped by the user';
358
+ }
359
+ /** A one-line description of a task status, e.g. `Background agent "Explore auth" completed`. */
360
+ export function describeTaskStatus(status) {
361
+ const name = status.description ? `Background agent "${status.description}"` : 'Background agent';
362
+ return `${name} ${status.status}`;
363
+ }