@agent-link/agent 0.1.202 → 0.1.206

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.
@@ -0,0 +1,379 @@
1
+ /**
2
+ * Backend abstraction for AgentLink.
3
+ *
4
+ * Stage 1: defines the interface only. ClaudeBackend implementation lives in
5
+ * agent/src/claude.ts (refactor to backends/claude/* is a follow-up PR).
6
+ */
7
+ export type BackendType = 'claude' | 'codex';
8
+ export declare const KNOWN_BACKEND_TYPES: readonly BackendType[];
9
+ /**
10
+ * Backend-neutral file attachment for `UserTurnInput.files`.
11
+ * Defined here (not in claude.ts) so the boundary stays clean.
12
+ * (PR3 will re-export this as `ChatFile` from claude.ts for back-compat;
13
+ * not yet wired in Stage 1.)
14
+ */
15
+ export interface BackendFileAttachment {
16
+ name: string;
17
+ mimeType: string;
18
+ /** base64-encoded content */
19
+ data: string;
20
+ }
21
+ /**
22
+ * Backend-neutral history message — fully opaque from the backend interface's
23
+ * point of view. Concrete backends (Claude's `{ role, content, ... }`,
24
+ * Codex's event records) all flow through unchanged; the wire/UI layer
25
+ * narrows as needed. PR3 will define the normalized shape if/when needed.
26
+ */
27
+ export type BackendHistoryMessage = Record<string, unknown>;
28
+ /**
29
+ * Backend-neutral session listing entry. Carries identity required for
30
+ * cross-backend routing — `backendType` + `backendSessionId` together
31
+ * uniquely identify a session.
32
+ */
33
+ export interface BackendSessionInfo {
34
+ /** Composite identity for routing / resume. */
35
+ session: BackendSessionRef;
36
+ /** Display title (custom title > derived). */
37
+ title: string;
38
+ /** Optional user-set custom title (separate from derived). */
39
+ customTitle?: string;
40
+ /** Short preview of last message (UI hint). */
41
+ preview?: string;
42
+ /** Epoch ms of last activity. */
43
+ lastModified: number;
44
+ }
45
+ /** Backend-neutral session identifier. Avoids leaking `claudeSessionId` into shared code. */
46
+ export interface BackendSessionRef {
47
+ backendType: BackendType;
48
+ /** Stable id used for routing, listing, resume. For Claude: claudeSessionId. For Codex: Thread.id. */
49
+ backendSessionId: string;
50
+ /** `backendThreadId === backendSessionId` for Claude (single-thread sessions). Codex sets it to `Thread.id`, distinct from session id. */
51
+ backendThreadId?: string | null;
52
+ /** Provider's raw id, kept for debugging / cross-backend migration. */
53
+ providerSessionId?: string | null;
54
+ }
55
+ /** Backend-neutral sandbox / permission mode. Each backend maps internally. */
56
+ export type SandboxMode = 'safe' | 'workspace' | 'unrestricted';
57
+ /**
58
+ * Named union for per-conversation permission modes (PR4-C, Codex finding C).
59
+ * Mirrors claude.ts:806 — backends declare which values they support via
60
+ * `capabilities.controls.permissionMode`.
61
+ */
62
+ export type PermissionMode = 'normal' | 'acceptEdits' | 'auto' | 'default' | 'plan';
63
+ /**
64
+ * Result of a `restartSession` call (PR4-C). `wasTurnActive` drives
65
+ * `execution_cancelled` emission in connection.ts.
66
+ */
67
+ export interface BackendRestartResult {
68
+ /** New backend session id after restart, or null if conv was empty. */
69
+ claudeSessionId: string | null;
70
+ /** Whether a turn was in flight when restart fired. */
71
+ wasTurnActive: boolean;
72
+ /** Backend-internal history snapshot (opaque). */
73
+ history?: unknown;
74
+ }
75
+ /** Options for `restartSession` / `createPlaceholderSession` (PR4-C). */
76
+ export interface RestartOpts {
77
+ planMode?: boolean;
78
+ brainMode?: boolean;
79
+ permissionMode?: PermissionMode;
80
+ }
81
+ /** Capability descriptor with optional per-feature details. */
82
+ export interface Capability<TDetails = unknown> {
83
+ supported: boolean;
84
+ details?: TDetails;
85
+ }
86
+ export interface BackendCapabilities {
87
+ lifecycle: {
88
+ persistentProcess: boolean;
89
+ resumableSessions: boolean;
90
+ concurrentTurns: boolean;
91
+ backgroundTurns: boolean;
92
+ fork: Capability;
93
+ compaction: Capability;
94
+ };
95
+ approvals: {
96
+ command: Capability;
97
+ fileChange: Capability;
98
+ permissions: Capability;
99
+ genericTool: Capability;
100
+ userInput: Capability;
101
+ };
102
+ streaming: {
103
+ text: boolean;
104
+ reasoning: Capability;
105
+ plan: Capability;
106
+ usage: Capability<{
107
+ incremental: boolean;
108
+ }>;
109
+ processOutput: Capability;
110
+ fsWatch: Capability;
111
+ remoteControl: Capability<{
112
+ statusEvents: boolean;
113
+ }>;
114
+ };
115
+ history: {
116
+ list: boolean;
117
+ read: boolean;
118
+ rename: boolean;
119
+ delete: boolean;
120
+ search: boolean;
121
+ };
122
+ models: {
123
+ list: Capability;
124
+ switchPerSession: boolean;
125
+ switchPerTurn: boolean;
126
+ };
127
+ tools: {
128
+ /** Item kinds this backend emits (e.g. 'Read','Write','Bash' for Claude; 'command_execution','file_change' for Codex). */
129
+ itemTypes: string[];
130
+ fallbackRenderer: boolean;
131
+ specializedRenderers: string[];
132
+ };
133
+ integrations: {
134
+ accountInfo: Capability;
135
+ codeReview: Capability;
136
+ mcpManagement: Capability;
137
+ hooks: Capability;
138
+ personalitySwitch: Capability;
139
+ skills: Capability;
140
+ };
141
+ sandboxModes: SandboxMode[];
142
+ /** PR4-C: per-conversation user-action controls. */
143
+ controls: {
144
+ setModel: Capability;
145
+ restartSession: Capability;
146
+ brainMode: Capability;
147
+ planMode: Capability;
148
+ permissionMode: Capability<{
149
+ modes: PermissionMode[];
150
+ }>;
151
+ isCompacting: Capability;
152
+ };
153
+ }
154
+ /** Approval / confirmation requests from the backend to the user. */
155
+ export type ApprovalRequest = {
156
+ kind: 'command';
157
+ command: string;
158
+ cwd?: string;
159
+ reason?: string;
160
+ } | {
161
+ kind: 'file_change';
162
+ summary: string;
163
+ changes?: unknown;
164
+ } | {
165
+ kind: 'permissions';
166
+ permissions: unknown;
167
+ reason?: string;
168
+ } | {
169
+ kind: 'tool';
170
+ toolName: string;
171
+ input: unknown;
172
+ reason?: string;
173
+ };
174
+ export type UserInputSource = 'ask_user_question' | 'mcp_elicitation' | 'tool_request_user_input';
175
+ export interface UserInputQuestion {
176
+ question: string;
177
+ header?: string;
178
+ options: Array<{
179
+ label: string;
180
+ description?: string;
181
+ }>;
182
+ multiSelect?: boolean;
183
+ }
184
+ export interface UserInputRequest {
185
+ questions: UserInputQuestion[];
186
+ source: UserInputSource;
187
+ }
188
+ export interface UsageInfo {
189
+ inputTokens?: number;
190
+ outputTokens?: number;
191
+ cacheReadInputTokens?: number;
192
+ cacheCreationInputTokens?: number;
193
+ totalCostUsd?: number;
194
+ }
195
+ /** Normalized tool event (start / update / result). */
196
+ export interface NormalizedToolEvent {
197
+ id: string;
198
+ name: string;
199
+ /** Backend item kind (e.g. 'Read','Bash','command_execution','file_change'). */
200
+ itemKind?: string;
201
+ input?: unknown;
202
+ output?: unknown;
203
+ isError?: boolean;
204
+ status?: 'in_progress' | 'completed' | 'error';
205
+ }
206
+ export type NormalizedEvent = {
207
+ type: 'session_started';
208
+ session: BackendSessionRef;
209
+ } | {
210
+ type: 'turn_started';
211
+ session: BackendSessionRef;
212
+ turnId?: string;
213
+ } | {
214
+ type: 'text_delta';
215
+ session: BackendSessionRef;
216
+ text: string;
217
+ } | {
218
+ type: 'tool_started';
219
+ session: BackendSessionRef;
220
+ tool: NormalizedToolEvent;
221
+ } | {
222
+ type: 'tool_updated';
223
+ session: BackendSessionRef;
224
+ tool: NormalizedToolEvent;
225
+ } | {
226
+ type: 'tool_completed';
227
+ session: BackendSessionRef;
228
+ tool: NormalizedToolEvent;
229
+ } | {
230
+ type: 'plan_delta';
231
+ session: BackendSessionRef;
232
+ plan: unknown;
233
+ } | {
234
+ type: 'reasoning_delta';
235
+ session: BackendSessionRef;
236
+ text: string;
237
+ } | {
238
+ type: 'compact_started';
239
+ session: BackendSessionRef;
240
+ } | {
241
+ type: 'compact_completed';
242
+ session: BackendSessionRef;
243
+ } | {
244
+ type: 'usage_update';
245
+ session: BackendSessionRef;
246
+ usage: UsageInfo;
247
+ } | {
248
+ type: 'fs_changed';
249
+ session: BackendSessionRef;
250
+ paths: string[];
251
+ } | {
252
+ type: 'process_output';
253
+ session: BackendSessionRef;
254
+ processId: string;
255
+ chunk: string;
256
+ } | {
257
+ type: 'process_exited';
258
+ session: BackendSessionRef;
259
+ processId: string;
260
+ exitCode: number;
261
+ } | {
262
+ type: 'approval_requested';
263
+ session: BackendSessionRef;
264
+ requestId: string;
265
+ request: ApprovalRequest;
266
+ } | {
267
+ type: 'user_input_requested';
268
+ session: BackendSessionRef;
269
+ requestId: string;
270
+ request: UserInputRequest;
271
+ } | {
272
+ type: 'turn_completed';
273
+ session: BackendSessionRef;
274
+ usage?: UsageInfo;
275
+ } | {
276
+ type: 'turn_cancelled';
277
+ session: BackendSessionRef;
278
+ } | {
279
+ type: 'error';
280
+ session?: BackendSessionRef;
281
+ message: string;
282
+ };
283
+ export interface StartOpts {
284
+ workDir: string;
285
+ /** Backend-neutral permission mode; backend maps to its own sandbox/permission flags. */
286
+ permissions?: {
287
+ mode: SandboxMode;
288
+ additionalWritableDirs?: string[];
289
+ };
290
+ }
291
+ export interface EnsureSessionOpts {
292
+ conversationId: string;
293
+ workDir: string;
294
+ resumeSessionId?: string;
295
+ /** Optional metadata that backends with rich session metadata (Claude's `recapId`, `briefingDate` etc.) can store. */
296
+ metadata?: Record<string, unknown>;
297
+ }
298
+ export interface UserTurnInput {
299
+ text: string;
300
+ files?: BackendFileAttachment[];
301
+ /**
302
+ * Backend-specific per-turn metadata. ClaudeBackend forwards these as
303
+ * `HandleChatOptions` (brainMode, recapId, briefingDate, devops*, projectName,
304
+ * icmId, etc.). Codex/other backends may ignore unknown keys. The runtime
305
+ * passes this through opaquely; only the backend interprets it.
306
+ */
307
+ metadata?: Record<string, unknown>;
308
+ }
309
+ export interface ApprovalAnswer {
310
+ requestId: string;
311
+ decision: 'allow' | 'deny';
312
+ reason?: string;
313
+ }
314
+ export interface UserInputAnswer {
315
+ requestId: string;
316
+ /** questionText -> selected option label */
317
+ answers: Record<string, string>;
318
+ }
319
+ export type EventListener = (event: NormalizedEvent) => void;
320
+ /**
321
+ * Backend interface. Methods marked optional are capability-gated: callers must
322
+ * check `capabilities` before invoking. Required methods all backends must
323
+ * implement (even if as no-ops or throws).
324
+ */
325
+ export interface AgentBackend {
326
+ readonly type: BackendType;
327
+ readonly capabilities: BackendCapabilities;
328
+ start(opts: StartOpts): Promise<void>;
329
+ shutdown(): Promise<void>;
330
+ ensureSession(opts: EnsureSessionOpts): Promise<BackendSessionRef>;
331
+ startTurn(session: BackendSessionRef, input: UserTurnInput): Promise<void>;
332
+ interruptTurn(session: BackendSessionRef): Promise<void>;
333
+ shutdownSession?(session: BackendSessionRef): Promise<void>;
334
+ answerUserInput(answer: UserInputAnswer): void;
335
+ answerApproval(answer: ApprovalAnswer): void;
336
+ listSessions(workDir: string): Promise<BackendSessionInfo[]>;
337
+ readSession(workDir: string, session: BackendSessionRef): Promise<BackendHistoryMessage[]>;
338
+ renameSession?(workDir: string, session: BackendSessionRef, title: string): Promise<boolean>;
339
+ deleteSession?(workDir: string, session: BackendSessionRef): Promise<boolean>;
340
+ compact?(session: BackendSessionRef): Promise<void>;
341
+ fork?(session: BackendSessionRef, fromMessageId?: string): Promise<BackendSessionRef>;
342
+ getAccountInfo?(): Promise<{
343
+ usage?: UsageInfo;
344
+ limits?: unknown;
345
+ }>;
346
+ listModels?(): Promise<Array<{
347
+ id: string;
348
+ label?: string;
349
+ }>>;
350
+ /**
351
+ * PR4-C (Codex finding B): keyed by conversationId, NOT BackendSessionRef.
352
+ * Passing null targets the global default model. claude.ts uses
353
+ * `pendingModels.set(convId, model)` for not-yet-started conversations
354
+ * — the convId must be passed straight through.
355
+ */
356
+ setModel?(conversationId: string | null, model: string | null): void;
357
+ getEffectiveModel?(conversationId?: string): string | null;
358
+ isCompacting?(conversationId?: string): boolean;
359
+ /** Returns null if no conversation exists for convId (caller should fall
360
+ * back to createPlaceholderSession). */
361
+ restartSession?(conversationId: string, opts?: RestartOpts): BackendRestartResult | null;
362
+ /** Create a placeholder + workDir entry so the next ensureSession picks
363
+ * up sticky preferences. Used when no live conversation exists. */
364
+ createPlaceholderSession?(conversationId: string, workDir: string, opts?: RestartOpts): void;
365
+ /** True if the backend currently has a live conversation for convId. */
366
+ hasConversation?(conversationId: string): boolean;
367
+ on(listener: EventListener): () => void;
368
+ }
369
+ /** Helper: minimal capabilities object for a stub/unknown backend. */
370
+ export declare function emptyCapabilities(): BackendCapabilities;
371
+ /**
372
+ * Capabilities declared by the ClaudeBackend adapter (current implementation
373
+ * as of 2026-05-09). These reflect what the adapter exposes through the
374
+ * AgentBackend interface — not the full surface of the underlying claude CLI.
375
+ * For example, claude.ts has restartConversation and an interactive /compact
376
+ * flow, but neither is wired through AgentBackend.fork / AgentBackend.compact
377
+ * yet, so both are reported as unsupported.
378
+ */
379
+ export declare function claudeCapabilities(): BackendCapabilities;
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Backend abstraction for AgentLink.
3
+ *
4
+ * Stage 1: defines the interface only. ClaudeBackend implementation lives in
5
+ * agent/src/claude.ts (refactor to backends/claude/* is a follow-up PR).
6
+ */
7
+ export const KNOWN_BACKEND_TYPES = ['claude', 'codex'];
8
+ /** Helper: minimal capabilities object for a stub/unknown backend. */
9
+ export function emptyCapabilities() {
10
+ const cap = () => ({ supported: false });
11
+ return {
12
+ lifecycle: {
13
+ persistentProcess: false,
14
+ resumableSessions: false,
15
+ concurrentTurns: false,
16
+ backgroundTurns: false,
17
+ fork: cap(),
18
+ compaction: cap(),
19
+ },
20
+ approvals: {
21
+ command: cap(),
22
+ fileChange: cap(),
23
+ permissions: cap(),
24
+ genericTool: cap(),
25
+ userInput: cap(),
26
+ },
27
+ streaming: {
28
+ text: false,
29
+ reasoning: cap(),
30
+ plan: cap(),
31
+ usage: { supported: false },
32
+ processOutput: cap(),
33
+ fsWatch: cap(),
34
+ remoteControl: { supported: false },
35
+ },
36
+ history: { list: false, read: false, rename: false, delete: false, search: false },
37
+ models: { list: cap(), switchPerSession: false, switchPerTurn: false },
38
+ tools: { itemTypes: [], fallbackRenderer: false, specializedRenderers: [] },
39
+ integrations: {
40
+ accountInfo: cap(),
41
+ codeReview: cap(),
42
+ mcpManagement: cap(),
43
+ hooks: cap(),
44
+ personalitySwitch: cap(),
45
+ skills: cap(),
46
+ },
47
+ sandboxModes: [],
48
+ controls: {
49
+ setModel: cap(),
50
+ restartSession: cap(),
51
+ brainMode: cap(),
52
+ planMode: cap(),
53
+ permissionMode: { supported: false },
54
+ isCompacting: cap(),
55
+ },
56
+ };
57
+ }
58
+ /**
59
+ * Capabilities declared by the ClaudeBackend adapter (current implementation
60
+ * as of 2026-05-09). These reflect what the adapter exposes through the
61
+ * AgentBackend interface — not the full surface of the underlying claude CLI.
62
+ * For example, claude.ts has restartConversation and an interactive /compact
63
+ * flow, but neither is wired through AgentBackend.fork / AgentBackend.compact
64
+ * yet, so both are reported as unsupported.
65
+ */
66
+ export function claudeCapabilities() {
67
+ const yes = (details) => ({ supported: true, details });
68
+ const no = () => ({ supported: false });
69
+ return {
70
+ lifecycle: {
71
+ persistentProcess: true,
72
+ resumableSessions: true,
73
+ concurrentTurns: true, // multiple conversations = multiple processes
74
+ backgroundTurns: false,
75
+ fork: no(), // restartConversation exists but its signature doesn't match AgentBackend.fork
76
+ compaction: no(), // claude.ts has no programmatic compact API
77
+ },
78
+ approvals: {
79
+ command: no(), // Claude uses bypassPermissions; no per-command approval
80
+ fileChange: no(),
81
+ permissions: no(),
82
+ genericTool: yes(), // tool permission via permission-prompt-tool stdio
83
+ userInput: yes(), // AskUserQuestion via control_request
84
+ },
85
+ streaming: {
86
+ text: true,
87
+ reasoning: yes(), // thinking blocks
88
+ plan: yes(), // EnterPlanMode tool
89
+ usage: { supported: true, details: { incremental: false } },
90
+ processOutput: no(), // Bash background mode is approximated, not first-class
91
+ fsWatch: no(),
92
+ remoteControl: { supported: false },
93
+ },
94
+ history: { list: true, read: true, rename: true, delete: true, search: true },
95
+ models: { list: no(), switchPerSession: true, switchPerTurn: false },
96
+ tools: {
97
+ itemTypes: ['Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep', 'Task', 'Agent', 'WebFetch', 'WebSearch', 'TodoWrite'],
98
+ fallbackRenderer: true,
99
+ specializedRenderers: ['Edit', 'EnterPlanMode'],
100
+ },
101
+ integrations: {
102
+ accountInfo: no(),
103
+ codeReview: no(),
104
+ mcpManagement: no(),
105
+ hooks: no(),
106
+ personalitySwitch: no(),
107
+ skills: no(),
108
+ },
109
+ sandboxModes: ['unrestricted'], // Claude runs with bypassPermissions
110
+ controls: {
111
+ setModel: yes(),
112
+ restartSession: yes(),
113
+ brainMode: yes(),
114
+ planMode: yes(),
115
+ permissionMode: { supported: true, details: { modes: ['normal', 'acceptEdits', 'auto', 'default', 'plan'] } },
116
+ isCompacting: yes(),
117
+ },
118
+ };
119
+ }
120
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/backends/types.ts"],"names":[],"mappings":"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"}
package/dist/claude.d.ts CHANGED
@@ -74,9 +74,29 @@ export declare function removeCloseObserver(fn: CloseObserverFn): void;
74
74
  export declare function setCloseObserver(fn: CloseObserverFn): void;
75
75
  /** @deprecated Use removeCloseObserver() instead. Clears ALL close observers. */
76
76
  export declare function clearCloseObserver(): void;
77
- export declare function setSendFn(fn: SendFn): void;
77
+ /**
78
+ * Subscribe a SendFn additively. Returns an unsubscribe function. Use this
79
+ * when multiple subscribers should each receive every outbound frame (e.g.
80
+ * the legacy connection.ts path and the ClaudeBackend adapter coexisting).
81
+ */
82
+ export declare function addSendFn(fn: SendFn): () => void;
83
+ /**
84
+ * Legacy replace-one semantics: clear all existing subscribers then install
85
+ * just this one. Returns an unsubscribe function. Existing call sites should
86
+ * migrate to {@link addSendFn} when they need additive behavior.
87
+ */
88
+ export declare function setSendFn(fn: SendFn): () => void;
78
89
  type SessionStartedFn = (conversationId: string, claudeSessionId: string) => void;
79
- export declare function setOnSessionStarted(fn: SessionStartedFn): void;
90
+ /**
91
+ * Subscribe a callback for every Claude session-started event additively.
92
+ * Returns an unsubscribe function.
93
+ */
94
+ export declare function addOnSessionStarted(fn: SessionStartedFn): () => void;
95
+ /**
96
+ * Legacy replace-one semantics for session-started subscriber. Existing call
97
+ * sites should migrate to {@link addOnSessionStarted}.
98
+ */
99
+ export declare function setOnSessionStarted(fn: SessionStartedFn): () => void;
80
100
  /**
81
101
  * Set the model override for a conversation.
82
102
  * If the conversation has a claudeSessionId, persists to disk.
package/dist/claude.js CHANGED
@@ -53,7 +53,21 @@ const conversations = new Map();
53
53
  const lastSessionIdsByConv = new Map();
54
54
  /** Pending model override per conversation (set before first message, before sessionId is known). */
55
55
  const pendingModels = new Map();
56
- let sendFn = () => { };
56
+ // Multi-subscriber send fanout. Historically a single closure; switched to a
57
+ // Set so multiple callers (e.g. ClaudeBackend adapter + connection.ts) can
58
+ // each subscribe and receive every outbound frame. Each subscriber is invoked
59
+ // in a try/catch so one throwing handler doesn't block the others.
60
+ const sendFns = new Set();
61
+ const sendFn = (msg) => {
62
+ for (const fn of sendFns) {
63
+ try {
64
+ fn(msg);
65
+ }
66
+ catch (e) {
67
+ console.error('[claude] sendFn subscriber threw', e);
68
+ }
69
+ }
70
+ };
57
71
  const pendingControlRequests = new Map();
58
72
  const outputObservers = [];
59
73
  export function addOutputObserver(fn) {
@@ -89,12 +103,40 @@ export function setCloseObserver(fn) {
89
103
  export function clearCloseObserver() {
90
104
  closeObservers.length = 0;
91
105
  }
106
+ /**
107
+ * Subscribe a SendFn additively. Returns an unsubscribe function. Use this
108
+ * when multiple subscribers should each receive every outbound frame (e.g.
109
+ * the legacy connection.ts path and the ClaudeBackend adapter coexisting).
110
+ */
111
+ export function addSendFn(fn) {
112
+ sendFns.add(fn);
113
+ return () => { sendFns.delete(fn); };
114
+ }
115
+ /**
116
+ * Legacy replace-one semantics: clear all existing subscribers then install
117
+ * just this one. Returns an unsubscribe function. Existing call sites should
118
+ * migrate to {@link addSendFn} when they need additive behavior.
119
+ */
92
120
  export function setSendFn(fn) {
93
- sendFn = fn;
121
+ sendFns.clear();
122
+ return addSendFn(fn);
94
123
  }
95
- let onSessionStarted = null;
124
+ const sessionStartedFns = new Set();
125
+ /**
126
+ * Subscribe a callback for every Claude session-started event additively.
127
+ * Returns an unsubscribe function.
128
+ */
129
+ export function addOnSessionStarted(fn) {
130
+ sessionStartedFns.add(fn);
131
+ return () => { sessionStartedFns.delete(fn); };
132
+ }
133
+ /**
134
+ * Legacy replace-one semantics for session-started subscriber. Existing call
135
+ * sites should migrate to {@link addOnSessionStarted}.
136
+ */
96
137
  export function setOnSessionStarted(fn) {
97
- onSessionStarted = fn;
138
+ sessionStartedFns.clear();
139
+ return addOnSessionStarted(fn);
98
140
  }
99
141
  /**
100
142
  * Set the model override for a conversation.
@@ -897,8 +939,14 @@ async function processOutput(child, state, stderr) {
897
939
  // Notify web client so sidebar can refresh session list
898
940
  sendWithConvId({ type: 'session_started', claudeSessionId: state.claudeSessionId });
899
941
  // Check if this chat is linked to an action item → call CLI start
900
- if (onSessionStarted)
901
- onSessionStarted(state.conversationId, state.claudeSessionId);
942
+ for (const fn of sessionStartedFns) {
943
+ try {
944
+ fn(state.conversationId, state.claudeSessionId);
945
+ }
946
+ catch (e) {
947
+ console.error('[claude] onSessionStarted subscriber threw', e);
948
+ }
949
+ }
902
950
  // Persist pending model to conversation-models.json now that we have a sessionId
903
951
  const pendingModel = pendingModels.get(state.conversationId);
904
952
  if (pendingModel) {