sidekick-shared 0.29.1 → 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 (109) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +171 -60
  3. package/dist/aggregation/EventAggregator.d.ts +79 -4
  4. package/dist/aggregation/EventAggregator.js +391 -37
  5. package/dist/aggregation/types.d.ts +27 -3
  6. package/dist/browser.d.ts +7 -3
  7. package/dist/browser.js +23 -3
  8. package/dist/context/sessionContext.js +5 -1
  9. package/dist/esm/aggregation/EventAggregator.js +392 -38
  10. package/dist/esm/browser.js +3 -1
  11. package/dist/esm/context/sessionContext.js +6 -2
  12. package/dist/esm/fileContinuity.js +149 -0
  13. package/dist/esm/index.js +5 -2
  14. package/dist/esm/node.js +2 -0
  15. package/dist/esm/parsers/codexParser.js +192 -3
  16. package/dist/esm/parsers/subagentEndState.js +61 -0
  17. package/dist/esm/parsers/subagentScanner.js +4 -0
  18. package/dist/esm/parsers/subagentTraceParser.js +6 -0
  19. package/dist/esm/promptHistory.js +22 -11
  20. package/dist/esm/providers/claudeCode.js +50 -27
  21. package/dist/esm/providers/codex.js +153 -163
  22. package/dist/esm/providers/codexCompressedReader.js +184 -0
  23. package/dist/esm/providers/codexDatabase.js +47 -2
  24. package/dist/esm/providers/codexRollouts.js +20 -2
  25. package/dist/esm/providers/codexSubagents.js +456 -0
  26. package/dist/esm/providers/compressedRollouts.js +330 -0
  27. package/dist/esm/providers/factory.js +4 -1
  28. package/dist/esm/providers/rolloutWalker.js +32 -4
  29. package/dist/esm/schemas/sessionEvent.js +32 -4
  30. package/dist/esm/sessionMonitor.js +7 -1
  31. package/dist/esm/sessionPreviews.js +9 -0
  32. package/dist/esm/sessionPromptHistory.js +5 -3
  33. package/dist/esm/subagentActivity.js +125 -0
  34. package/dist/esm/subagentDisplayState.js +81 -0
  35. package/dist/esm/subagentSpawn.js +206 -6
  36. package/dist/esm/testing/codexSubagentFixtures.js +336 -0
  37. package/dist/esm/toolCallOrigins.js +17 -10
  38. package/dist/esm/toolCategory.js +6 -2
  39. package/dist/esm/transcript.js +6 -2
  40. package/dist/esm/turns/assistantTurn.js +6 -3
  41. package/dist/esm/usage/usageEvents.js +4 -1
  42. package/dist/esm/watchers/eventBridge.js +31 -1
  43. package/dist/esm/watchers/jsonlTail.js +11 -42
  44. package/dist/esm/watchers/jsonlWatcher.js +19 -9
  45. package/dist/esm/watchers/providerReaderWatcher.js +5 -4
  46. package/dist/fileContinuity.d.ts +95 -0
  47. package/dist/fileContinuity.js +187 -0
  48. package/dist/index.d.ts +14 -7
  49. package/dist/index.js +39 -10
  50. package/dist/node.d.ts +4 -0
  51. package/dist/node.js +16 -2
  52. package/dist/parsers/codexParser.d.ts +44 -0
  53. package/dist/parsers/codexParser.js +192 -3
  54. package/dist/parsers/subagentEndState.d.ts +20 -0
  55. package/dist/parsers/subagentEndState.js +65 -0
  56. package/dist/parsers/subagentScanner.js +4 -0
  57. package/dist/parsers/subagentTraceParser.d.ts +8 -0
  58. package/dist/parsers/subagentTraceParser.js +6 -0
  59. package/dist/promptHistory.d.ts +13 -3
  60. package/dist/promptHistory.js +22 -11
  61. package/dist/providers/claudeCode.js +50 -27
  62. package/dist/providers/codex.d.ts +35 -5
  63. package/dist/providers/codex.js +150 -160
  64. package/dist/providers/codexCompressedReader.d.ts +58 -0
  65. package/dist/providers/codexCompressedReader.js +221 -0
  66. package/dist/providers/codexDatabase.d.ts +25 -0
  67. package/dist/providers/codexDatabase.js +48 -2
  68. package/dist/providers/codexRollouts.d.ts +16 -2
  69. package/dist/providers/codexRollouts.js +20 -2
  70. package/dist/providers/codexSubagents.d.ts +66 -0
  71. package/dist/providers/codexSubagents.js +495 -0
  72. package/dist/providers/compressedRollouts.d.ts +114 -0
  73. package/dist/providers/compressedRollouts.js +380 -0
  74. package/dist/providers/factory.d.ts +5 -0
  75. package/dist/providers/factory.js +4 -1
  76. package/dist/providers/rolloutWalker.d.ts +15 -2
  77. package/dist/providers/rolloutWalker.js +33 -4
  78. package/dist/providers/types.d.ts +22 -1
  79. package/dist/schemas/sessionEvent.d.ts +26 -2
  80. package/dist/schemas/sessionEvent.js +31 -3
  81. package/dist/sessionMonitor.d.ts +6 -0
  82. package/dist/sessionMonitor.js +7 -1
  83. package/dist/sessionPreviews.js +9 -0
  84. package/dist/sessionPromptHistory.d.ts +5 -0
  85. package/dist/sessionPromptHistory.js +5 -36
  86. package/dist/subagentActivity.d.ts +91 -0
  87. package/dist/subagentActivity.js +132 -0
  88. package/dist/subagentDisplayState.d.ts +81 -0
  89. package/dist/subagentDisplayState.js +86 -0
  90. package/dist/subagentSpawn.d.ts +100 -5
  91. package/dist/subagentSpawn.js +218 -7
  92. package/dist/testing/codexSubagentFixtures.d.ts +48 -0
  93. package/dist/testing/codexSubagentFixtures.js +344 -0
  94. package/dist/toolCallOrigins.d.ts +5 -0
  95. package/dist/toolCallOrigins.js +17 -10
  96. package/dist/toolCategory.js +6 -2
  97. package/dist/transcript.js +5 -1
  98. package/dist/turns/assistantTurn.d.ts +4 -1
  99. package/dist/turns/assistantTurn.js +5 -2
  100. package/dist/types/codex.d.ts +71 -2
  101. package/dist/types/sessionEvent.d.ts +38 -3
  102. package/dist/usage/usageEvents.d.ts +4 -1
  103. package/dist/usage/usageEvents.js +4 -1
  104. package/dist/watchers/eventBridge.js +30 -0
  105. package/dist/watchers/jsonlTail.js +11 -42
  106. package/dist/watchers/jsonlWatcher.d.ts +5 -1
  107. package/dist/watchers/jsonlWatcher.js +18 -8
  108. package/dist/watchers/providerReaderWatcher.js +5 -4
  109. package/package.json +1 -1
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ /**
3
+ * How to show a subagent: its lifecycle's outcome once something settled
4
+ * it, otherwise `running`, unless its transcript or its parent says it has
5
+ * stopped in all but name.
6
+ *
7
+ * Some background agents never get a final notification (they stopped after
8
+ * an interim "may resume" notice, the user stopped them from the UI, or a
9
+ * restart killed them). Their lifecycles stay `running`. This rule reads
10
+ * them as:
11
+ *
12
+ * - `finished-unreported`: the transcript ended cleanly (last assistant turn
13
+ * `end_turn` / `stop_sequence`, no tool call unanswered) and has been quiet
14
+ * for {@link SUBAGENT_UNREPORTED_QUIET_MS}.
15
+ * - `ended`: neither the parent session nor the transcript has been active for
16
+ * {@link SUBAGENT_PARENT_IDLE_ENDED_MS}.
17
+ *
18
+ * Neither is stored: the state is recomputed from the latest data on every
19
+ * call, so an agent that resumes (its transcript grows, or a later
20
+ * notification reopens its lifecycle) reads as `running` again. Silence alone
21
+ * is never enough for `finished-unreported`: live agents can be quiet for
22
+ * hours.
23
+ *
24
+ * Pure: no filesystem access. Browser-safe.
25
+ *
26
+ * @module subagentDisplayState
27
+ */
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.SUBAGENT_PARENT_IDLE_ENDED_MS = exports.SUBAGENT_UNREPORTED_QUIET_MS = void 0;
30
+ exports.resolveSubagentDisplayState = resolveSubagentDisplayState;
31
+ exports.subagentDisplayLabel = subagentDisplayLabel;
32
+ /** Quiet time after a clean end before a running agent reads as finished without a report (2 min). */
33
+ exports.SUBAGENT_UNREPORTED_QUIET_MS = 2 * 60_000;
34
+ /** Parent and transcript idle time after which a running agent reads as ended (6 h). */
35
+ exports.SUBAGENT_PARENT_IDLE_ENDED_MS = 6 * 60 * 60_000;
36
+ function toMs(value) {
37
+ if (value === null || value === undefined)
38
+ return undefined;
39
+ const ms = value instanceof Date ? value.getTime() : new Date(value).getTime();
40
+ return Number.isFinite(ms) ? ms : undefined;
41
+ }
42
+ /**
43
+ * Resolves how to show a subagent (see the module docs). Settled lifecycles
44
+ * map to their outcome (`completed` when none is recorded). A running one is
45
+ * `stopped` / `completed` when its meta flags say so, `finished-unreported`
46
+ * after a clean end and {@link SUBAGENT_UNREPORTED_QUIET_MS} of quiet,
47
+ * `ended` when the parent and transcript have both been idle for longer than
48
+ * {@link SUBAGENT_PARENT_IDLE_ENDED_MS}, and `running` otherwise.
49
+ */
50
+ function resolveSubagentDisplayState(lifecycle, trace, options) {
51
+ if (lifecycle.status !== 'running') {
52
+ return { state: lifecycle.outcome ?? 'completed', reason: 'settled' };
53
+ }
54
+ if (trace?.stoppedByUser)
55
+ return { state: 'stopped', reason: 'meta' };
56
+ if (trace?.finished)
57
+ return { state: 'completed', reason: 'meta' };
58
+ const now = toMs(options.now) ?? Date.now();
59
+ const lastActivity = toMs(trace?.lastActivityAt);
60
+ if (trace?.endedCleanly &&
61
+ lastActivity !== undefined &&
62
+ now - lastActivity >= exports.SUBAGENT_UNREPORTED_QUIET_MS) {
63
+ return { state: 'finished-unreported', reason: 'clean-end' };
64
+ }
65
+ const parentLast = toMs(options.parentLastActivity);
66
+ if (parentLast !== undefined) {
67
+ const latest = Math.max(parentLast, lastActivity ?? -Infinity);
68
+ if (now - latest > exports.SUBAGENT_PARENT_IDLE_ENDED_MS) {
69
+ return { state: 'ended', reason: 'parent-idle' };
70
+ }
71
+ }
72
+ return { state: 'running' };
73
+ }
74
+ const LABELS = {
75
+ running: 'running',
76
+ 'finished-unreported': 'finished (no report)',
77
+ ended: 'ended',
78
+ completed: 'completed',
79
+ failed: 'failed',
80
+ stopped: 'stopped',
81
+ killed: 'killed',
82
+ };
83
+ /** A short label: `running`, `finished (no report)`, `ended`, `completed`, `failed`, `stopped`, `killed`. */
84
+ function subagentDisplayLabel(state) {
85
+ return LABELS[state] ?? state;
86
+ }
@@ -1,11 +1,18 @@
1
1
  /**
2
- * Recognizes Claude Code subagent spawns and background-task notifications.
2
+ * Recognizes subagent spawns, background-task notifications, and the other
3
+ * records that settle a background agent.
3
4
  *
4
5
  * Claude Code's subagent tool is `Agent`; `Task` is its earlier name (and the
5
6
  * name OpenCode's `task` tool is normalized to). A background agent's spawning
6
7
  * tool call gets its result right away ("Async agent launched successfully.")
7
8
  * 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
+ * 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.
9
16
  *
10
17
  * Pure: no filesystem access, so it is safe for browser bundles.
11
18
  *
@@ -13,10 +20,35 @@
13
20
  */
14
21
  import type { SubagentOutcome } from './aggregation/types';
15
22
  import type { SessionEvent } from './types/sessionEvent';
16
- /** Tool names that spawn a subagent: `Agent` (current) and `Task` (earlier name). */
23
+ /**
24
+ * Claude Code / OpenCode tool names that spawn a subagent: `Agent` (current)
25
+ * and `Task` (earlier name). Codex's `spawn_agent` is not in this list;
26
+ * {@link isSubagentSpawnTool} recognizes it as well.
27
+ */
17
28
  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;
29
+ /**
30
+ * Whether a tool call is Codex's `spawn_agent`: by name (`Spawn_agent` as
31
+ * parsed, or the raw `spawn_agent`), or by the raw name the Codex parser keeps
32
+ * in the input (`_sidekickRawToolName`).
33
+ */
34
+ export declare function isCodexSubagentSpawnTool(name: string | null | undefined, input?: unknown): boolean;
35
+ /**
36
+ * Whether a tool call spawns a subagent: one of
37
+ * {@link SUBAGENT_SPAWN_TOOL_NAMES} (`Agent`, `Task`), or Codex's
38
+ * `spawn_agent` (see {@link isCodexSubagentSpawnTool}; pass the call's input
39
+ * to also match on its raw name).
40
+ */
41
+ export declare function isSubagentSpawnTool(name: string | null | undefined, input?: unknown): boolean;
42
+ /** A Codex `spawn_agent` result: the child was started in the background. */
43
+ export interface CodexSpawnLaunch {
44
+ /** The child's agent path (`task_name` in the result), e.g. `/root/review_builder`. */
45
+ agentPath?: string;
46
+ }
47
+ /**
48
+ * Recognizes a Codex `spawn_agent` result (`{"task_name": "/root/x"}`, as a
49
+ * JSON string or object). `null` for anything else.
50
+ */
51
+ export declare function parseCodexSpawnLaunch(result: unknown): CodexSpawnLaunch | null;
20
52
  /** A spawning tool result that launched the agent in the background. */
21
53
  export interface AsyncAgentLaunch {
22
54
  /** The agent id Claude Code reported (`agentId: <id>`), when present. */
@@ -78,3 +110,66 @@ export declare function isTaskNotificationSystemEvent(event: SessionEvent): bool
78
110
  export declare function taskNotificationFromEvent(event: SessionEvent): TaskNotification | null;
79
111
  /** Maps a notification `<status>` to an outcome; unknown or missing statuses read as `completed`. */
80
112
  export declare function taskNotificationOutcome(status: string | undefined): SubagentOutcome;
113
+ /** A plain-text notice that the user stopped background agents. */
114
+ export interface AgentsStoppedNotice {
115
+ /** The stopped agents' descriptions, as the notice quotes them. */
116
+ descriptions: string[];
117
+ }
118
+ /**
119
+ * Parses Claude Code's plain-text notice that the user stopped background
120
+ * agents: `2 background agents were stopped by the user: "A", "B".` (a
121
+ * `queued_command` attachment's prompt) or `Background agent "A" was stopped
122
+ * by the user.` (a queue-operation line). `null` for any other text.
123
+ */
124
+ export declare function parseAgentsStoppedNotice(text: unknown): AgentsStoppedNotice | null;
125
+ /**
126
+ * The stop notice a task-notification event carries (a `queued_command`
127
+ * prompt that is plain text, not a `<task-notification>` block). `null`
128
+ * otherwise.
129
+ */
130
+ export declare function agentsStoppedNoticeFromEvent(event: SessionEvent): AgentsStoppedNotice | null;
131
+ /** Claude Code's `agents_killed` system line: the user killed every running background agent. */
132
+ export declare const AGENTS_KILLED_SUBTYPE = "agents_killed";
133
+ /**
134
+ * Whether an event, or a raw Claude Code JSONL line, is an `agents_killed`
135
+ * system line (`{ type: 'system', subtype: 'agents_killed' }`).
136
+ */
137
+ export declare function isAgentsKilledSystemEvent(event: unknown): boolean;
138
+ /** A background agent's status from a Claude Code `task_status` attachment. */
139
+ export interface SubagentTaskStatus {
140
+ /** The background agent's id. */
141
+ taskId: string;
142
+ /** `local_agent` for background agents. */
143
+ taskType: string;
144
+ /** As written: `running`, `completed`, `failed`, `killed`, `stopped`, … */
145
+ status: string;
146
+ description?: string;
147
+ }
148
+ /** `origin.kind` of the opt-in `system` event made from a `task_status` attachment. */
149
+ export declare const TASK_STATUS_ORIGIN_KIND = "task-status";
150
+ /** `type` of the content block that carries the status. */
151
+ export declare const TASK_STATUS_BLOCK_TYPE = "task_status";
152
+ /**
153
+ * The background agent's status a raw Claude Code `task_status` attachment
154
+ * line reports (`{ type: 'attachment', attachment: { type: 'task_status',
155
+ * taskId, taskType: 'local_agent', status, description } }`). `null` for any
156
+ * other value, including background shell tasks (`local_bash`).
157
+ */
158
+ export declare function taskStatusFromAttachment(raw: unknown): SubagentTaskStatus | null;
159
+ /** Whether a task status is final (anything but `running` / `pending`). */
160
+ export declare function isFinalTaskStatus(status: string | undefined): boolean;
161
+ /**
162
+ * The synthetic `system` event `extractSessionEvents(raw, {
163
+ * includeTaskNotifications: true })` makes from a background agent's
164
+ * `task_status` attachment. It is bookkeeping, not a message or a prompt.
165
+ */
166
+ export declare function isTaskStatusSystemEvent(event: SessionEvent): boolean;
167
+ /** The status an opt-in task-status `system` event carries; `null` for any other event. */
168
+ export declare function taskStatusFromEvent(event: SessionEvent): SubagentTaskStatus | null;
169
+ /**
170
+ * A one-line description of a stop: `Background agents stopped by the user (2)`,
171
+ * or, for an `agents_killed` line (`null`), `Background agents stopped by the user`.
172
+ */
173
+ export declare function describeAgentsStopped(notice: AgentsStoppedNotice | null): string;
174
+ /** A one-line description of a task status, e.g. `Background agent "Explore auth" completed`. */
175
+ export declare function describeTaskStatus(status: SubagentTaskStatus): string;
@@ -1,20 +1,29 @@
1
1
  "use strict";
2
2
  /**
3
- * Recognizes Claude Code subagent spawns and background-task notifications.
3
+ * Recognizes subagent spawns, background-task notifications, and the other
4
+ * records that settle a background agent.
4
5
  *
5
6
  * Claude Code's subagent tool is `Agent`; `Task` is its earlier name (and the
6
7
  * name OpenCode's `task` tool is normalized to). A background agent's spawning
7
8
  * tool call gets its result right away ("Async agent launched successfully.")
8
9
  * 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
+ * either as a user line or as a mid-turn `queued_command` attachment. When no
11
+ * notification comes, a `task_status` attachment, an `agents_killed` system
12
+ * line, or a plain-text "background agents were stopped by the user" notice
13
+ * can still say how it ended.
14
+ *
15
+ * Codex spawns with `spawn_agent` (parsed as `Spawn_agent`); its result
16
+ * (`{"task_name": "/root/x"}`) is always a background launch.
10
17
  *
11
18
  * Pure: no filesystem access, so it is safe for browser bundles.
12
19
  *
13
20
  * @module subagentSpawn
14
21
  */
15
22
  Object.defineProperty(exports, "__esModule", { value: true });
16
- exports.SUBAGENT_SPAWN_TOOL_NAMES = void 0;
23
+ exports.TASK_STATUS_BLOCK_TYPE = exports.TASK_STATUS_ORIGIN_KIND = exports.AGENTS_KILLED_SUBTYPE = exports.SUBAGENT_SPAWN_TOOL_NAMES = void 0;
24
+ exports.isCodexSubagentSpawnTool = isCodexSubagentSpawnTool;
17
25
  exports.isSubagentSpawnTool = isSubagentSpawnTool;
26
+ exports.parseCodexSpawnLaunch = parseCodexSpawnLaunch;
18
27
  exports.contentText = contentText;
19
28
  exports.parseAsyncAgentLaunch = parseAsyncAgentLaunch;
20
29
  exports.parseTaskNotification = parseTaskNotification;
@@ -22,12 +31,75 @@ exports.taskNotificationPromptFromAttachment = taskNotificationPromptFromAttachm
22
31
  exports.isTaskNotificationSystemEvent = isTaskNotificationSystemEvent;
23
32
  exports.taskNotificationFromEvent = taskNotificationFromEvent;
24
33
  exports.taskNotificationOutcome = taskNotificationOutcome;
25
- /** Tool names that spawn a subagent: `Agent` (current) and `Task` (earlier name). */
34
+ exports.parseAgentsStoppedNotice = parseAgentsStoppedNotice;
35
+ exports.agentsStoppedNoticeFromEvent = agentsStoppedNoticeFromEvent;
36
+ exports.isAgentsKilledSystemEvent = isAgentsKilledSystemEvent;
37
+ exports.taskStatusFromAttachment = taskStatusFromAttachment;
38
+ exports.isFinalTaskStatus = isFinalTaskStatus;
39
+ exports.isTaskStatusSystemEvent = isTaskStatusSystemEvent;
40
+ exports.taskStatusFromEvent = taskStatusFromEvent;
41
+ exports.describeAgentsStopped = describeAgentsStopped;
42
+ exports.describeTaskStatus = describeTaskStatus;
43
+ /**
44
+ * Claude Code / OpenCode tool names that spawn a subagent: `Agent` (current)
45
+ * and `Task` (earlier name). Codex's `spawn_agent` is not in this list;
46
+ * {@link isSubagentSpawnTool} recognizes it as well.
47
+ */
26
48
  exports.SUBAGENT_SPAWN_TOOL_NAMES = Object.freeze(['Agent', 'Task']);
27
49
  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);
50
+ /** Codex's raw spawn tool name, and the name the Codex parser normalizes it to. */
51
+ const CODEX_SPAWN_RAW = 'spawn_agent';
52
+ const CODEX_SPAWN_TOOLS = new Set([CODEX_SPAWN_RAW, 'Spawn_agent']);
53
+ /**
54
+ * Whether a tool call is Codex's `spawn_agent`: by name (`Spawn_agent` as
55
+ * parsed, or the raw `spawn_agent`), or by the raw name the Codex parser keeps
56
+ * in the input (`_sidekickRawToolName`).
57
+ */
58
+ function isCodexSubagentSpawnTool(name, input) {
59
+ if (typeof name === 'string' && CODEX_SPAWN_TOOLS.has(name))
60
+ return true;
61
+ return (!!input &&
62
+ typeof input === 'object' &&
63
+ input._sidekickRawToolName === CODEX_SPAWN_RAW);
64
+ }
65
+ /**
66
+ * Whether a tool call spawns a subagent: one of
67
+ * {@link SUBAGENT_SPAWN_TOOL_NAMES} (`Agent`, `Task`), or Codex's
68
+ * `spawn_agent` (see {@link isCodexSubagentSpawnTool}; pass the call's input
69
+ * to also match on its raw name).
70
+ */
71
+ function isSubagentSpawnTool(name, input) {
72
+ if (typeof name === 'string' && SPAWN_TOOLS.has(name))
73
+ return true;
74
+ return isCodexSubagentSpawnTool(name, input);
75
+ }
76
+ /**
77
+ * Recognizes a Codex `spawn_agent` result (`{"task_name": "/root/x"}`, as a
78
+ * JSON string or object). `null` for anything else.
79
+ */
80
+ function parseCodexSpawnLaunch(result) {
81
+ let value = result;
82
+ if (typeof value !== 'string') {
83
+ const text = contentText(value);
84
+ value = text || value;
85
+ }
86
+ if (typeof value === 'string') {
87
+ const trimmed = value.trim();
88
+ if (!trimmed.startsWith('{'))
89
+ return null;
90
+ try {
91
+ value = JSON.parse(trimmed);
92
+ }
93
+ catch {
94
+ return null;
95
+ }
96
+ }
97
+ if (!value || typeof value !== 'object' || Array.isArray(value))
98
+ return null;
99
+ const taskName = value.task_name;
100
+ if (typeof taskName !== 'string')
101
+ return null;
102
+ return taskName ? { agentPath: taskName } : {};
31
103
  }
32
104
  const ASYNC_LAUNCH_PREFIX = /^\s*Async agent launched successfully\b/;
33
105
  const AGENT_ID_LINE = /(?:^|\n)[ \t]*agentId:[ \t]*([A-Za-z0-9][\w.@-]*)/;
@@ -172,3 +244,142 @@ function taskNotificationOutcome(status) {
172
244
  return 'completed';
173
245
  }
174
246
  }
247
+ const STOPPED_PLURAL = /^\s*(\d+)\s+background agents? (?:were|was) stopped by the user:\s*([\s\S]*?)\s*$/i;
248
+ const STOPPED_SINGULAR = /^\s*Background agent "([\s\S]*)" was stopped by the user\.?\s*$/i;
249
+ /**
250
+ * Parses Claude Code's plain-text notice that the user stopped background
251
+ * agents: `2 background agents were stopped by the user: "A", "B".` (a
252
+ * `queued_command` attachment's prompt) or `Background agent "A" was stopped
253
+ * by the user.` (a queue-operation line). `null` for any other text.
254
+ */
255
+ function parseAgentsStoppedNotice(text) {
256
+ if (typeof text !== 'string')
257
+ return null;
258
+ const singular = STOPPED_SINGULAR.exec(text);
259
+ if (singular)
260
+ return { descriptions: [singular[1].trim()].filter(Boolean) };
261
+ const plural = STOPPED_PLURAL.exec(text);
262
+ if (!plural)
263
+ return null;
264
+ let list = plural[2].trim();
265
+ if (list.endsWith('.'))
266
+ list = list.slice(0, -1).trimEnd();
267
+ if (!list.startsWith('"') || !list.endsWith('"') || list.length < 2)
268
+ return null;
269
+ const descriptions = list
270
+ .slice(1, -1)
271
+ .split(/",\s*"/)
272
+ .map((description) => description.trim())
273
+ .filter(Boolean);
274
+ return descriptions.length > 0 ? { descriptions } : null;
275
+ }
276
+ /**
277
+ * The stop notice a task-notification event carries (a `queued_command`
278
+ * prompt that is plain text, not a `<task-notification>` block). `null`
279
+ * otherwise.
280
+ */
281
+ function agentsStoppedNoticeFromEvent(event) {
282
+ if (!isTaskNotificationSystemEvent(event))
283
+ return null;
284
+ return parseAgentsStoppedNotice(contentText(event.message?.content));
285
+ }
286
+ /** Claude Code's `agents_killed` system line: the user killed every running background agent. */
287
+ exports.AGENTS_KILLED_SUBTYPE = 'agents_killed';
288
+ /**
289
+ * Whether an event, or a raw Claude Code JSONL line, is an `agents_killed`
290
+ * system line (`{ type: 'system', subtype: 'agents_killed' }`).
291
+ */
292
+ function isAgentsKilledSystemEvent(event) {
293
+ if (!event || typeof event !== 'object')
294
+ return false;
295
+ const line = event;
296
+ return line.type === 'system' && line.subtype === exports.AGENTS_KILLED_SUBTYPE;
297
+ }
298
+ /** `origin.kind` of the opt-in `system` event made from a `task_status` attachment. */
299
+ exports.TASK_STATUS_ORIGIN_KIND = 'task-status';
300
+ /** `type` of the content block that carries the status. */
301
+ exports.TASK_STATUS_BLOCK_TYPE = 'task_status';
302
+ const AGENT_TASK_TYPE = 'local_agent';
303
+ function nonEmpty(value) {
304
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
305
+ }
306
+ /**
307
+ * The background agent's status a raw Claude Code `task_status` attachment
308
+ * line reports (`{ type: 'attachment', attachment: { type: 'task_status',
309
+ * taskId, taskType: 'local_agent', status, description } }`). `null` for any
310
+ * other value, including background shell tasks (`local_bash`).
311
+ */
312
+ function taskStatusFromAttachment(raw) {
313
+ if (!raw || typeof raw !== 'object')
314
+ return null;
315
+ const line = raw;
316
+ if (line.type !== 'attachment' || !line.attachment || typeof line.attachment !== 'object') {
317
+ return null;
318
+ }
319
+ const attachment = line.attachment;
320
+ if (attachment.type !== 'task_status' || attachment.taskType !== AGENT_TASK_TYPE)
321
+ return null;
322
+ const taskId = nonEmpty(attachment.taskId);
323
+ const status = nonEmpty(attachment.status);
324
+ if (!taskId || !status)
325
+ return null;
326
+ const description = nonEmpty(attachment.description);
327
+ return { taskId, taskType: AGENT_TASK_TYPE, status, ...(description ? { description } : {}) };
328
+ }
329
+ /** Whether a task status is final (anything but `running` / `pending`). */
330
+ function isFinalTaskStatus(status) {
331
+ if (!status)
332
+ return false;
333
+ const lower = status.toLowerCase();
334
+ return lower !== 'running' && lower !== 'pending';
335
+ }
336
+ /**
337
+ * The synthetic `system` event `extractSessionEvents(raw, {
338
+ * includeTaskNotifications: true })` makes from a background agent's
339
+ * `task_status` attachment. It is bookkeeping, not a message or a prompt.
340
+ */
341
+ function isTaskStatusSystemEvent(event) {
342
+ return event.type === 'system' && event.origin?.kind === exports.TASK_STATUS_ORIGIN_KIND;
343
+ }
344
+ /** The status an opt-in task-status `system` event carries; `null` for any other event. */
345
+ function taskStatusFromEvent(event) {
346
+ if (!isTaskStatusSystemEvent(event))
347
+ return null;
348
+ const content = event.message?.content;
349
+ if (!Array.isArray(content))
350
+ return null;
351
+ for (const block of content) {
352
+ if (!block || typeof block !== 'object')
353
+ continue;
354
+ const raw = block;
355
+ if (raw.type !== exports.TASK_STATUS_BLOCK_TYPE)
356
+ continue;
357
+ const taskId = nonEmpty(raw.taskId);
358
+ const status = nonEmpty(raw.status);
359
+ if (!taskId || !status)
360
+ return null;
361
+ const description = nonEmpty(raw.description);
362
+ return {
363
+ taskId,
364
+ taskType: nonEmpty(raw.taskType) ?? AGENT_TASK_TYPE,
365
+ status,
366
+ ...(description ? { description } : {}),
367
+ };
368
+ }
369
+ return null;
370
+ }
371
+ /**
372
+ * A one-line description of a stop: `Background agents stopped by the user (2)`,
373
+ * or, for an `agents_killed` line (`null`), `Background agents stopped by the user`.
374
+ */
375
+ function describeAgentsStopped(notice) {
376
+ const count = notice?.descriptions.length;
377
+ return count
378
+ ? `Background agents stopped by the user (${count})`
379
+ : 'Background agents stopped by the user';
380
+ }
381
+ /** A one-line description of a task status, e.g. `Background agent "Explore auth" completed`. */
382
+ function describeTaskStatus(status) {
383
+ const name = status.description ? `Background agent "${status.description}"` : 'Background agent';
384
+ return `${name} ${status.status}`;
385
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Redacted rollouts in the shape Codex 0.166 writes for multi-agent sessions:
3
+ * a parent that spawns a child (`spawn_agent` → `SubAgentActivity` started →
4
+ * `{"task_name": …}` output → MESSAGE / FINAL_ANSWER agent messages →
5
+ * `completed`), a failed spawn, `followup_task` / `send_message` /
6
+ * `interrupt_agent` interactions, and a forked child whose rollout embeds the
7
+ * parent's history (and session_meta) before its
8
+ * `subagent_history_start_ordinal`, which itself spawns a grandchild.
9
+ * Payload text is placeholder; ids are synthetic.
10
+ *
11
+ * Test-only helpers.
12
+ *
13
+ * @module testing/codexSubagentFixtures
14
+ */
15
+ export type CodexFixtureRow = Record<string, unknown>;
16
+ export declare const PARENT_ID = "01a18f87-0000-7000-8000-000000000001";
17
+ export declare const CHILD_ID = "01a18f9d-0000-7000-8000-000000000002";
18
+ export declare const GRANDCHILD_ID = "01a18fa0-0000-7000-8000-000000000003";
19
+ export declare const CALL_SPAWN = "call_spawn_builder";
20
+ export declare const CALL_SPAWN_FAILED = "call_spawn_failed";
21
+ export declare const CALL_FOLLOWUP = "call_followup_builder";
22
+ export declare const CALL_SEND = "call_send_builder";
23
+ export declare const CALL_INTERRUPT = "call_interrupt_builder";
24
+ export declare const CALL_SPAWN_GRANDCHILD = "call_spawn_deep_dive";
25
+ export declare const CHILD_TURN_ID = "01a18f9d-0000-7000-8000-0000000000t1";
26
+ export declare const CHILD_PATH = "/root/review_builder";
27
+ export declare const GRANDCHILD_PATH = "/root/review_builder/deep_dive";
28
+ /** `2026-10-31T01:<minute>:<second>.000Z`. */
29
+ export declare function at(minute: number, second?: number): string;
30
+ export interface ParentFixtureOptions {
31
+ cwd: string;
32
+ /** Leave out the `SubAgentActivity` records (as before Codex recorded them). */
33
+ withoutActivity?: boolean;
34
+ /** Leave out the `completed` record (Codex before 0.150). */
35
+ withoutCompleted?: boolean;
36
+ }
37
+ /** The parent session's rollout (ordinals 0…). */
38
+ export declare function parentRollout(options: ParentFixtureOptions): CodexFixtureRow[];
39
+ /** The forked child's rollout: its meta, the parent's history, then its own records. */
40
+ export declare function childRollout(options: {
41
+ cwd: string;
42
+ }): CodexFixtureRow[];
43
+ /** The grandchild's rollout (depth 2): a fork of the child. */
44
+ export declare function grandchildRollout(options: {
45
+ cwd: string;
46
+ }): CodexFixtureRow[];
47
+ /** Rows as JSONL text. */
48
+ export declare function toJsonl(rows: CodexFixtureRow[]): string;