wave-code 0.19.3 → 0.19.4

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.
@@ -2,9 +2,15 @@
2
2
  * AgentBridge — wraps the SDK Agent and translates between the JSON-RPC-like
3
3
  * stdio protocol and Agent method calls / callbacks.
4
4
  *
5
+ * Multi-tenant: maintains a Map<sessionId, {agent, storedConfig}> so a single
6
+ * `wave --stdio` process can host multiple sessions. Session-scoped requests
7
+ * carry `sessionId` on the JSON-RPC envelope for routing; global requests
8
+ * (listSessions/searchFiles/auth/plugins) don't require it but may use it
9
+ * for workdir fallback.
10
+ *
5
11
  * Responsibilities:
6
- * - Route incoming requests to the appropriate Agent method
7
- * - Translate AgentCallbacks into outgoing notifications
12
+ * - Route incoming requests to the appropriate Agent method (by sessionId)
13
+ * - Translate AgentCallbacks into outgoing notifications (with sessionId)
8
14
  * - Implement the canUseTool permission flow over the stdio protocol
9
15
  * - Handle config updates by destroying and recreating the Agent
10
16
  */
@@ -35,8 +41,27 @@ import {
35
41
  INTERNAL_ERROR as PROTOCOL_INTERNAL_ERROR,
36
42
  METHOD_NOT_FOUND as PROTOCOL_METHOD_NOT_FOUND,
37
43
  } from "./protocol.js";
44
+ import { readFileSync } from "fs";
45
+ import path from "path";
46
+ import { fileURLToPath } from "url";
47
+
48
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
49
+ const CLI_VERSION: string = (() => {
50
+ try {
51
+ const pkg = JSON.parse(
52
+ readFileSync(path.resolve(__dirname, "../../package.json"), "utf-8"),
53
+ );
54
+ return pkg.version ?? "";
55
+ } catch {
56
+ return "";
57
+ }
58
+ })();
38
59
 
39
- export type NotificationEmitter = (method: string, params: unknown) => void;
60
+ export type NotificationEmitter = (
61
+ method: string,
62
+ params: unknown,
63
+ sessionId?: string,
64
+ ) => void;
40
65
 
41
66
  export interface AgentBridgeOptions {
42
67
  emit: NotificationEmitter;
@@ -58,6 +83,7 @@ interface InitializeParams {
58
83
  disallowedTools?: string[];
59
84
  pluginDirs?: string[];
60
85
  mcpServers?: Record<string, McpServerConfig>;
86
+ clientVersion?: string;
61
87
  }
62
88
 
63
89
  interface UpdateConfigParams {
@@ -76,14 +102,30 @@ interface SearchFilesParams {
76
102
  workdir?: string;
77
103
  }
78
104
 
105
+ interface SessionEntry {
106
+ agent: Agent;
107
+ storedConfig: Partial<InitializeParams>;
108
+ }
109
+
110
+ /**
111
+ * Mutable holder so callbacks/canUseTool (created before Agent.create resolves)
112
+ * can reference the agent and its registered sessionId after creation.
113
+ * `registeredSessionId` is the sessionId the client knows about; it's used as
114
+ * the envelope sessionId for outgoing notifications so the client's router can
115
+ * demultiplex correctly. It's updated atomically when onSessionIdChange fires.
116
+ */
117
+ interface SessionContext {
118
+ agent?: Agent;
119
+ registeredSessionId?: string;
120
+ }
121
+
79
122
  export class AgentBridge {
80
- private agent: Agent | undefined;
123
+ private sessions = new Map<string, SessionEntry>();
81
124
  private pendingPermissions = new Map<
82
125
  string,
83
126
  (decision: PermissionDecision) => void
84
127
  >();
85
128
  private permissionCounter = 0;
86
- private storedConfig: Partial<InitializeParams> = {};
87
129
  private emit: NotificationEmitter;
88
130
  private pluginCore: PluginCore | undefined;
89
131
  private pluginCoreWorkdir: string | undefined;
@@ -94,22 +136,26 @@ export class AgentBridge {
94
136
 
95
137
  // ── Public API ────────────────────────────────────────────────
96
138
 
97
- async handleRequest(method: string, params: unknown): Promise<unknown> {
139
+ async handleRequest(
140
+ method: string,
141
+ params: unknown,
142
+ sessionId?: string,
143
+ ): Promise<unknown> {
98
144
  const p = (params ?? {}) as Record<string, unknown>;
99
145
  switch (method) {
100
146
  // ── Lifecycle ──
101
147
  case "initialize":
102
148
  return this.initialize(p as unknown as InitializeParams);
103
149
  case "destroy":
104
- return this.destroy();
150
+ return this.destroy(sessionId);
105
151
  case "restoreSession":
106
- return this.restoreSession(p.sessionId as string);
152
+ return this.restoreSession(p.sessionId as string, sessionId);
107
153
  case "listSessions":
108
- return this.listSessions(p.workdir as string | undefined);
154
+ return this.listSessions(p.workdir as string | undefined, sessionId);
109
155
  case "getSessionInfo":
110
- return this.getSessionInfo();
156
+ return this.getSessionInfo(sessionId);
111
157
  case "updateConfig":
112
- return this.updateConfig(p as unknown as UpdateConfigParams);
158
+ return this.updateConfig(p as unknown as UpdateConfigParams, sessionId);
113
159
 
114
160
  // ── Messages ──
115
161
  case "sendMessage":
@@ -119,52 +165,57 @@ export class AgentBridge {
119
165
  images?: Array<{ path: string; mimeType: string }>;
120
166
  force?: boolean;
121
167
  },
168
+ sessionId,
122
169
  );
123
170
  case "bang":
124
- return this.bang(p.command as string);
171
+ return this.bang(p.command as string, sessionId);
125
172
  case "abortMessage":
126
- return this.abortMessage();
173
+ return this.abortMessage(sessionId);
127
174
  case "clearMessages":
128
- return this.clearMessages();
175
+ return this.clearMessages(sessionId);
129
176
  case "rewindToMessage":
130
- return this.rewindToMessage(p.messageId as string);
177
+ return this.rewindToMessage(p.messageId as string, sessionId);
131
178
  case "deleteQueuedMessage":
132
- return this.deleteQueuedMessage(p.index as number);
179
+ return this.deleteQueuedMessage(p.index as number, sessionId);
133
180
  case "getMessages":
134
- return this.getMessages();
181
+ return this.getMessages(sessionId);
135
182
  case "getFullMessageThread":
136
- return this.getFullMessageThread();
183
+ return this.getFullMessageThread(sessionId);
137
184
 
138
185
  // ── Permissions ──
139
186
  case "setPermissionMode":
140
- return this.setPermissionMode(p.mode as PermissionMode);
187
+ return this.setPermissionMode(p.mode as PermissionMode, sessionId);
141
188
  case "getPermissionMode":
142
- return this.getPermissionMode();
189
+ return this.getPermissionMode(sessionId);
143
190
 
144
191
  // ── MCP ──
145
192
  case "getMcpServers":
146
- return this.getMcpServers();
193
+ return this.getMcpServers(sessionId);
147
194
  case "connectMcpServer":
148
- return this.connectMcpServer(p.serverName as string);
195
+ return this.connectMcpServer(p.serverName as string, sessionId);
149
196
  case "disconnectMcpServer":
150
- return this.disconnectMcpServer(p.serverName as string);
197
+ return this.disconnectMcpServer(p.serverName as string, sessionId);
151
198
 
152
199
  // ── Commands ──
153
200
  case "getSlashCommands":
154
- return this.getSlashCommands();
201
+ return this.getSlashCommands(sessionId);
155
202
 
156
- // ── File / History ──
203
+ // ── File / History (global — no session required) ──
157
204
  case "searchFiles":
158
- return this.searchFiles(p as unknown as SearchFilesParams);
205
+ return this.searchFiles(p as unknown as SearchFilesParams, sessionId);
159
206
  case "getPromptHistory":
160
- return this.getPromptHistory(p.workdir as string | undefined);
207
+ return this.getPromptHistory(
208
+ p.workdir as string | undefined,
209
+ sessionId,
210
+ );
161
211
  case "searchPromptHistory":
162
212
  return this.searchPromptHistory(
163
213
  p.query as string,
164
214
  p.workdir as string | undefined,
215
+ sessionId,
165
216
  );
166
217
 
167
- // ── Auth ──
218
+ // ── Auth (global — no session required) ──
168
219
  case "getAuthStatus":
169
220
  return this.getAuthStatus();
170
221
  case "login":
@@ -172,55 +223,66 @@ export class AgentBridge {
172
223
  case "logout":
173
224
  return this.logout();
174
225
 
175
- // ── Plugins ──
226
+ // ── Plugins (global — no session required) ──
176
227
  case "listPlugins":
177
- return this.listPlugins(p.workdir as string | undefined);
228
+ return this.listPlugins(p.workdir as string | undefined, sessionId);
178
229
  case "installPlugin":
179
230
  return this.installPlugin(
180
231
  p.pluginId as string,
181
232
  p.scope as Scope | undefined,
182
233
  p.workdir as string | undefined,
234
+ sessionId,
183
235
  );
184
236
  case "uninstallPlugin":
185
237
  return this.uninstallPlugin(
186
238
  p.pluginId as string,
187
239
  p.workdir as string | undefined,
240
+ sessionId,
188
241
  );
189
242
  case "enablePlugin":
190
243
  return this.enablePlugin(
191
244
  p.pluginId as string,
192
245
  p.scope as Scope | undefined,
193
246
  p.workdir as string | undefined,
247
+ sessionId,
194
248
  );
195
249
  case "disablePlugin":
196
250
  return this.disablePlugin(
197
251
  p.pluginId as string,
198
252
  p.scope as Scope | undefined,
199
253
  p.workdir as string | undefined,
254
+ sessionId,
200
255
  );
201
256
  case "updatePlugin":
202
257
  return this.updatePlugin(
203
258
  p.pluginId as string,
204
259
  p.workdir as string | undefined,
260
+ sessionId,
205
261
  );
206
262
  case "listMarketplaces":
207
- return this.listMarketplaces(p.workdir as string | undefined);
263
+ return this.listMarketplaces(
264
+ p.workdir as string | undefined,
265
+ sessionId,
266
+ );
208
267
  case "addMarketplace":
209
268
  return this.addMarketplace(
210
269
  p.input as string,
211
270
  p.scope as Scope | undefined,
212
271
  p.workdir as string | undefined,
272
+ sessionId,
213
273
  );
214
274
  case "removeMarketplace":
215
275
  return this.removeMarketplace(
216
276
  p.name as string,
217
277
  p.scope as Scope | undefined,
218
278
  p.workdir as string | undefined,
279
+ sessionId,
219
280
  );
220
281
  case "updateMarketplace":
221
282
  return this.updateMarketplace(
222
283
  p.name as string | undefined,
223
284
  p.workdir as string | undefined,
285
+ sessionId,
224
286
  );
225
287
 
226
288
  default:
@@ -237,6 +299,7 @@ export class AgentBridge {
237
299
  requestId: string;
238
300
  decision: PermissionDecision;
239
301
  };
302
+ // requestId is process-level unique; lookup doesn't need sessionId
240
303
  const resolve = this.pendingPermissions.get(p.requestId);
241
304
  if (resolve) {
242
305
  this.pendingPermissions.delete(p.requestId);
@@ -252,11 +315,11 @@ export class AgentBridge {
252
315
  workingDirectory: string;
253
316
  permissionMode: PermissionMode;
254
317
  latestTotalTokens: number;
318
+ serverVersion: string;
255
319
  }> {
256
- // Merge with stored config (CLI defaults can be overridden by client)
257
- this.storedConfig = { ...this.storedConfig, ...params };
320
+ const ctx: SessionContext = {};
321
+ const callbacks = this.createCallbacks(ctx);
258
322
 
259
- const callbacks = this.createCallbacks();
260
323
  const options: AgentOptions = {
261
324
  callbacks,
262
325
  workdir: params.workdir,
@@ -273,125 +336,184 @@ export class AgentBridge {
273
336
  disallowedTools: params.disallowedTools,
274
337
  plugins: params.pluginDirs?.map((path) => ({ type: "local", path })),
275
338
  mcpServers: params.mcpServers,
276
- canUseTool: (context: ToolPermissionContext) => this.canUseTool(context),
339
+ canUseTool: (context: ToolPermissionContext) =>
340
+ this.canUseTool(context, ctx),
277
341
  };
278
342
 
279
- this.agent = await Agent.create(options);
343
+ const agent = await Agent.create(options);
344
+ ctx.agent = agent;
345
+ ctx.registeredSessionId = agent.sessionId;
346
+
347
+ this.sessions.set(agent.sessionId, {
348
+ agent,
349
+ storedConfig: { ...params },
350
+ });
351
+
352
+ if (params.clientVersion) {
353
+ console.debug(
354
+ `[agentBridge] clientVersion=${params.clientVersion} serverVersion=${CLI_VERSION}`,
355
+ );
356
+ }
280
357
 
281
358
  return {
282
- sessionId: this.agent.sessionId,
283
- workingDirectory: this.agent.workingDirectory,
284
- permissionMode: this.agent.getPermissionMode(),
285
- latestTotalTokens: this.agent.latestTotalTokens,
359
+ sessionId: agent.sessionId,
360
+ workingDirectory: agent.workingDirectory,
361
+ permissionMode: agent.getPermissionMode(),
362
+ latestTotalTokens: agent.latestTotalTokens,
363
+ serverVersion: CLI_VERSION,
286
364
  };
287
365
  }
288
366
 
289
- private async destroy(): Promise<null> {
290
- if (this.agent) {
291
- await this.agent.destroy();
292
- this.agent = undefined;
367
+ private async destroy(sessionId?: string): Promise<null> {
368
+ if (sessionId) {
369
+ const entry = this.sessions.get(sessionId);
370
+ if (entry) {
371
+ await entry.agent.destroy();
372
+ this.sessions.delete(sessionId);
373
+ }
293
374
  }
294
375
  return null;
295
376
  }
296
377
 
297
- private async restoreSession(sessionId: string): Promise<null> {
298
- this.requireAgent();
299
- await this.agent!.restoreSession(sessionId);
378
+ private async restoreSession(
379
+ restoreId: string,
380
+ sessionId?: string,
381
+ ): Promise<null> {
382
+ const entry = this.requireSession(sessionId);
383
+ await entry.agent.restoreSession(restoreId);
300
384
  return null;
301
385
  }
302
386
 
303
387
  private async listSessions(
304
388
  workdir?: string,
389
+ sessionId?: string,
305
390
  ): Promise<{ sessions: SessionMetadata[] }> {
306
391
  const sessions = await listSessions(
307
- workdir || this.agent?.workingDirectory || process.cwd(),
392
+ workdir || this.getSessionWorkdir(sessionId) || process.cwd(),
308
393
  );
309
394
  return { sessions };
310
395
  }
311
396
 
312
- private getSessionInfo(): {
397
+ private getSessionInfo(sessionId?: string): {
313
398
  sessionId: string;
314
399
  workingDirectory: string;
315
400
  latestTotalTokens: number;
316
401
  permissionMode: PermissionMode;
317
402
  availableTools: string[];
318
403
  } {
319
- this.requireAgent();
404
+ const entry = this.requireSession(sessionId);
320
405
  return {
321
- sessionId: this.agent!.sessionId,
322
- workingDirectory: this.agent!.workingDirectory,
323
- latestTotalTokens: this.agent!.latestTotalTokens,
324
- permissionMode: this.agent!.getPermissionMode(),
325
- availableTools: this.agent!.getAvailableToolNames(),
406
+ sessionId: entry.agent.sessionId,
407
+ workingDirectory: entry.agent.workingDirectory,
408
+ latestTotalTokens: entry.agent.latestTotalTokens,
409
+ permissionMode: entry.agent.getPermissionMode(),
410
+ availableTools: entry.agent.getAvailableToolNames(),
326
411
  };
327
412
  }
328
413
 
329
414
  private async updateConfig(
330
415
  params: UpdateConfigParams,
416
+ sessionId?: string,
331
417
  ): Promise<{ sessionId: string }> {
332
- this.requireAgent();
333
- const currentSessionId = this.agent!.sessionId;
418
+ const entry = this.requireSession(sessionId);
419
+ const currentSessionId = entry.agent.sessionId;
334
420
  // Merge new config into stored config
335
- this.storedConfig = { ...this.storedConfig, ...params };
336
- // Destroy and recreate
337
- await this.agent!.destroy();
338
- this.agent = undefined;
339
- await this.initialize({
340
- ...this.storedConfig,
421
+ entry.storedConfig = { ...entry.storedConfig, ...params };
422
+ // Destroy and recreate within the same session slot
423
+ await entry.agent.destroy();
424
+ this.sessions.delete(currentSessionId);
425
+
426
+ const ctx: SessionContext = {};
427
+ const callbacks = this.createCallbacks(ctx);
428
+ const options: AgentOptions = {
429
+ callbacks,
430
+ workdir: entry.storedConfig.workdir,
341
431
  restoreSessionId: currentSessionId,
432
+ apiKey: entry.storedConfig.apiKey,
433
+ baseURL: entry.storedConfig.baseURL,
434
+ defaultHeaders: entry.storedConfig.defaultHeaders,
435
+ model: entry.storedConfig.model,
436
+ fastModel: entry.storedConfig.fastModel,
437
+ language: entry.storedConfig.language,
438
+ permissionMode: entry.storedConfig.permissionMode,
439
+ tools: entry.storedConfig.tools,
440
+ allowedTools: entry.storedConfig.allowedTools,
441
+ disallowedTools: entry.storedConfig.disallowedTools,
442
+ plugins: entry.storedConfig.pluginDirs?.map((p) => ({
443
+ type: "local",
444
+ path: p,
445
+ })),
446
+ mcpServers: entry.storedConfig.mcpServers,
447
+ canUseTool: (context: ToolPermissionContext) =>
448
+ this.canUseTool(context, ctx),
449
+ };
450
+
451
+ const agent = await Agent.create(options);
452
+ ctx.agent = agent;
453
+ ctx.registeredSessionId = agent.sessionId;
454
+ this.sessions.set(agent.sessionId, {
455
+ agent,
456
+ storedConfig: { ...entry.storedConfig },
342
457
  });
343
- return { sessionId: this.agent!.sessionId };
458
+
459
+ return { sessionId: agent.sessionId };
344
460
  }
345
461
 
346
462
  // ── Messages ──────────────────────────────────────────────────
347
463
 
348
- private async sendMessage(params: {
349
- text: string;
350
- images?: Array<{ path: string; mimeType: string }>;
351
- force?: boolean;
352
- }): Promise<null> {
353
- this.requireAgent();
464
+ private async sendMessage(
465
+ params: {
466
+ text: string;
467
+ images?: Array<{ path: string; mimeType: string }>;
468
+ force?: boolean;
469
+ },
470
+ sessionId?: string,
471
+ ): Promise<null> {
472
+ const entry = this.requireSession(sessionId);
354
473
  if (params.force) {
355
- this.agent!.abortMessage();
474
+ entry.agent.abortMessage();
356
475
  }
357
476
  // Save prompt to history (mirrors VSCE chatSession.ts:236-242)
358
477
  try {
359
478
  await PromptHistoryManager.addEntry(
360
479
  params.text,
361
- this.agent!.sessionId,
480
+ entry.agent.sessionId,
362
481
  {},
363
- this.agent!.workingDirectory,
482
+ entry.agent.workingDirectory,
364
483
  );
365
484
  } catch {
366
485
  // Best-effort; don't block message sending on history save failure
367
486
  }
368
- await this.agent!.sendMessage(params.text, params.images);
487
+ await entry.agent.sendMessage(params.text, params.images);
369
488
  return null;
370
489
  }
371
490
 
372
- private async bang(command: string): Promise<null> {
373
- this.requireAgent();
374
- await this.agent!.bang(command);
491
+ private async bang(command: string, sessionId?: string): Promise<null> {
492
+ const entry = this.requireSession(sessionId);
493
+ await entry.agent.bang(command);
375
494
  return null;
376
495
  }
377
496
 
378
- private async abortMessage(): Promise<null> {
379
- this.requireAgent();
380
- this.agent!.abortMessage();
497
+ private async abortMessage(sessionId?: string): Promise<null> {
498
+ const entry = this.requireSession(sessionId);
499
+ entry.agent.abortMessage();
381
500
  return null;
382
501
  }
383
502
 
384
- private async clearMessages(): Promise<null> {
385
- this.requireAgent();
386
- this.agent!.clearMessages();
503
+ private async clearMessages(sessionId?: string): Promise<null> {
504
+ const entry = this.requireSession(sessionId);
505
+ entry.agent.clearMessages();
387
506
  return null;
388
507
  }
389
508
 
390
- private async rewindToMessage(messageId: string): Promise<{
509
+ private async rewindToMessage(
510
+ messageId: string,
511
+ sessionId?: string,
512
+ ): Promise<{
391
513
  inputContent: string;
392
514
  }> {
393
- this.requireAgent();
394
- const { messages } = await this.agent!.getFullMessageThread();
515
+ const entry = this.requireSession(sessionId);
516
+ const { messages } = await entry.agent.getFullMessageThread();
395
517
  const index = messages.findIndex((m) => m.id === messageId);
396
518
  if (index === -1) {
397
519
  throw new RpcError(
@@ -403,90 +525,99 @@ export class AgentBridge {
403
525
  const textBlock = message.blocks.find((b) => b.type === "text") as
404
526
  | { content?: string }
405
527
  | undefined;
406
- await this.agent!.truncateHistory(index);
528
+ await entry.agent.truncateHistory(index);
407
529
  return { inputContent: textBlock?.content || "" };
408
530
  }
409
531
 
410
- private deleteQueuedMessage(index: number): null {
411
- this.requireAgent();
412
- this.agent!.removeQueuedMessage(index);
532
+ private deleteQueuedMessage(index: number, sessionId?: string): null {
533
+ const entry = this.requireSession(sessionId);
534
+ entry.agent.removeQueuedMessage(index);
413
535
  return null;
414
536
  }
415
537
 
416
- private getMessages(): { messages: Message[] } {
417
- this.requireAgent();
418
- return { messages: this.agent!.messages };
538
+ private getMessages(sessionId?: string): { messages: Message[] } {
539
+ const entry = this.requireSession(sessionId);
540
+ return { messages: entry.agent.messages };
419
541
  }
420
542
 
421
- private async getFullMessageThread(): Promise<{
543
+ private async getFullMessageThread(sessionId?: string): Promise<{
422
544
  messages: Message[];
423
545
  sessionIds: string[];
424
546
  }> {
425
- this.requireAgent();
426
- return this.agent!.getFullMessageThread();
547
+ const entry = this.requireSession(sessionId);
548
+ return entry.agent.getFullMessageThread();
427
549
  }
428
550
 
429
551
  // ── Permissions ───────────────────────────────────────────────
430
552
 
431
- private async setPermissionMode(mode: PermissionMode): Promise<null> {
432
- this.requireAgent();
433
- await this.agent!.setPermissionMode(mode);
553
+ private async setPermissionMode(
554
+ mode: PermissionMode,
555
+ sessionId?: string,
556
+ ): Promise<null> {
557
+ const entry = this.requireSession(sessionId);
558
+ await entry.agent.setPermissionMode(mode);
434
559
  return null;
435
560
  }
436
561
 
437
- private getPermissionMode(): { mode: PermissionMode } {
438
- this.requireAgent();
439
- return { mode: this.agent!.getPermissionMode() };
562
+ private getPermissionMode(sessionId?: string): { mode: PermissionMode } {
563
+ const entry = this.requireSession(sessionId);
564
+ return { mode: entry.agent.getPermissionMode() };
440
565
  }
441
566
 
442
567
  // ── MCP ───────────────────────────────────────────────────────
443
568
 
444
- private getMcpServers(): { servers: McpServerStatus[] } {
445
- this.requireAgent();
446
- return { servers: this.agent!.getMcpServers() };
569
+ private getMcpServers(sessionId?: string): { servers: McpServerStatus[] } {
570
+ const entry = this.requireSession(sessionId);
571
+ return { servers: entry.agent.getMcpServers() };
447
572
  }
448
573
 
449
574
  private async connectMcpServer(
450
575
  serverName: string,
576
+ sessionId?: string,
451
577
  ): Promise<{ success: boolean }> {
452
- this.requireAgent();
453
- const success = await this.agent!.connectMcpServer(serverName);
578
+ const entry = this.requireSession(sessionId);
579
+ const success = await entry.agent.connectMcpServer(serverName);
454
580
  return { success };
455
581
  }
456
582
 
457
583
  private async disconnectMcpServer(
458
584
  serverName: string,
585
+ sessionId?: string,
459
586
  ): Promise<{ success: boolean }> {
460
- this.requireAgent();
461
- const success = await this.agent!.disconnectMcpServer(serverName);
587
+ const entry = this.requireSession(sessionId);
588
+ const success = await entry.agent.disconnectMcpServer(serverName);
462
589
  return { success };
463
590
  }
464
591
 
465
592
  // ── Commands ──────────────────────────────────────────────────
466
593
 
467
- private getSlashCommands(): { commands: SlashCommand[] } {
468
- this.requireAgent();
469
- return { commands: this.agent!.getSlashCommands() };
594
+ private getSlashCommands(sessionId?: string): { commands: SlashCommand[] } {
595
+ const entry = this.requireSession(sessionId);
596
+ return { commands: entry.agent.getSlashCommands() };
470
597
  }
471
598
 
472
- // ── File / History ────────────────────────────────────────────
599
+ // ── File / History (global) ───────────────────────────────────
473
600
 
474
601
  private async searchFiles(
475
602
  params: SearchFilesParams,
603
+ sessionId?: string,
476
604
  ): Promise<{ files: Awaited<ReturnType<typeof searchFiles>> }> {
477
605
  const files = await searchFiles(params.query, {
478
606
  maxResults: params.maxResults,
479
607
  workingDirectory:
480
- params.workdir || this.agent?.workingDirectory || process.cwd(),
608
+ params.workdir || this.getSessionWorkdir(sessionId) || process.cwd(),
481
609
  });
482
610
  return { files };
483
611
  }
484
612
 
485
- private async getPromptHistory(workdir?: string): Promise<{
613
+ private async getPromptHistory(
614
+ workdir?: string,
615
+ sessionId?: string,
616
+ ): Promise<{
486
617
  history: Awaited<ReturnType<typeof PromptHistoryManager.getHistory>>;
487
618
  }> {
488
619
  const history = await PromptHistoryManager.getHistory({
489
- workdir: workdir || this.agent?.workingDirectory,
620
+ workdir: workdir || this.getSessionWorkdir(sessionId),
490
621
  });
491
622
  return { history };
492
623
  }
@@ -494,11 +625,12 @@ export class AgentBridge {
494
625
  private async searchPromptHistory(
495
626
  query: string,
496
627
  workdir?: string,
628
+ sessionId?: string,
497
629
  ): Promise<{
498
630
  history: Awaited<ReturnType<typeof PromptHistoryManager.searchHistory>>;
499
631
  }> {
500
632
  const history = await PromptHistoryManager.searchHistory(query, {
501
- workdir: workdir || this.agent?.workingDirectory,
633
+ workdir: workdir || this.getSessionWorkdir(sessionId),
502
634
  });
503
635
  return { history };
504
636
  }
@@ -507,11 +639,16 @@ export class AgentBridge {
507
639
 
508
640
  private canUseTool(
509
641
  context: ToolPermissionContext,
642
+ ctx: SessionContext,
510
643
  ): Promise<PermissionDecision> {
511
644
  const requestId = `perm_${++this.permissionCounter}`;
512
645
  return new Promise<PermissionDecision>((resolve) => {
513
646
  this.pendingPermissions.set(requestId, resolve);
514
- this.emit("permissionRequest", { requestId, context });
647
+ this.emit(
648
+ "permissionRequest",
649
+ { requestId, context },
650
+ ctx.registeredSessionId,
651
+ );
515
652
 
516
653
  // 5-minute timeout → auto-deny
517
654
  setTimeout(
@@ -529,7 +666,7 @@ export class AgentBridge {
529
666
  });
530
667
  }
531
668
 
532
- // ── Auth ─────────────────────────────────────────────────────
669
+ // ── Auth (global) ────────────────────────────────────────────
533
670
 
534
671
  private async getAuthStatus(): Promise<{
535
672
  isAuthenticated: boolean;
@@ -561,11 +698,11 @@ export class AgentBridge {
561
698
  return null;
562
699
  }
563
700
 
564
- // ── Plugins ──────────────────────────────────────────────────
701
+ // ── Plugins (global) ─────────────────────────────────────────
565
702
 
566
- private getPluginCore(workdir?: string): PluginCore {
703
+ private getPluginCore(workdir?: string, sessionId?: string): PluginCore {
567
704
  const resolvedWorkdir =
568
- workdir || this.agent?.workingDirectory || process.cwd();
705
+ workdir || this.getSessionWorkdir(sessionId) || process.cwd();
569
706
  if (!this.pluginCore || this.pluginCoreWorkdir !== resolvedWorkdir) {
570
707
  this.pluginCore = new PluginCore(resolvedWorkdir);
571
708
  this.pluginCoreWorkdir = resolvedWorkdir;
@@ -573,8 +710,8 @@ export class AgentBridge {
573
710
  return this.pluginCore;
574
711
  }
575
712
 
576
- private async listPlugins(workdir?: string) {
577
- const core = this.getPluginCore(workdir);
713
+ private async listPlugins(workdir?: string, sessionId?: string) {
714
+ const core = this.getPluginCore(workdir, sessionId);
578
715
  const { plugins, mergedEnabled } = await core.listPlugins();
579
716
  return {
580
717
  plugins: plugins.map((p) => {
@@ -597,12 +734,20 @@ export class AgentBridge {
597
734
  pluginId: string,
598
735
  scope?: Scope,
599
736
  workdir?: string,
737
+ sessionId?: string,
600
738
  ) {
601
- return this.getPluginCore(workdir).installPlugin(pluginId, scope);
739
+ return this.getPluginCore(workdir, sessionId).installPlugin(
740
+ pluginId,
741
+ scope,
742
+ );
602
743
  }
603
744
 
604
- private async uninstallPlugin(pluginId: string, workdir?: string) {
605
- await this.getPluginCore(workdir).uninstallPlugin(pluginId);
745
+ private async uninstallPlugin(
746
+ pluginId: string,
747
+ workdir?: string,
748
+ sessionId?: string,
749
+ ) {
750
+ await this.getPluginCore(workdir, sessionId).uninstallPlugin(pluginId);
606
751
  return null;
607
752
  }
608
753
 
@@ -610,106 +755,154 @@ export class AgentBridge {
610
755
  pluginId: string,
611
756
  scope?: Scope,
612
757
  workdir?: string,
758
+ sessionId?: string,
613
759
  ) {
614
- return this.getPluginCore(workdir).enablePlugin(pluginId, scope);
760
+ return this.getPluginCore(workdir, sessionId).enablePlugin(pluginId, scope);
615
761
  }
616
762
 
617
763
  private async disablePlugin(
618
764
  pluginId: string,
619
765
  scope?: Scope,
620
766
  workdir?: string,
767
+ sessionId?: string,
621
768
  ) {
622
- return this.getPluginCore(workdir).disablePlugin(pluginId, scope);
769
+ return this.getPluginCore(workdir, sessionId).disablePlugin(
770
+ pluginId,
771
+ scope,
772
+ );
623
773
  }
624
774
 
625
- private async updatePlugin(pluginId: string, workdir?: string) {
626
- return this.getPluginCore(workdir).updatePlugin(pluginId);
775
+ private async updatePlugin(
776
+ pluginId: string,
777
+ workdir?: string,
778
+ sessionId?: string,
779
+ ) {
780
+ return this.getPluginCore(workdir, sessionId).updatePlugin(pluginId);
627
781
  }
628
782
 
629
- private async listMarketplaces(workdir?: string) {
630
- return this.getPluginCore(workdir).listMarketplaces();
783
+ private async listMarketplaces(workdir?: string, sessionId?: string) {
784
+ return this.getPluginCore(workdir, sessionId).listMarketplaces();
631
785
  }
632
786
 
633
- private async addMarketplace(input: string, scope?: Scope, workdir?: string) {
634
- return this.getPluginCore(workdir).addMarketplace(input, scope);
787
+ private async addMarketplace(
788
+ input: string,
789
+ scope?: Scope,
790
+ workdir?: string,
791
+ sessionId?: string,
792
+ ) {
793
+ return this.getPluginCore(workdir, sessionId).addMarketplace(input, scope);
635
794
  }
636
795
 
637
796
  private async removeMarketplace(
638
797
  name: string,
639
798
  scope?: Scope,
640
799
  workdir?: string,
800
+ sessionId?: string,
641
801
  ) {
642
- await this.getPluginCore(workdir).removeMarketplace(name, scope);
802
+ await this.getPluginCore(workdir, sessionId).removeMarketplace(name, scope);
643
803
  return null;
644
804
  }
645
805
 
646
- private async updateMarketplace(name?: string, workdir?: string) {
647
- await this.getPluginCore(workdir).updateMarketplace(name);
806
+ private async updateMarketplace(
807
+ name?: string,
808
+ workdir?: string,
809
+ sessionId?: string,
810
+ ) {
811
+ await this.getPluginCore(workdir, sessionId).updateMarketplace(name);
648
812
  return null;
649
813
  }
650
814
 
651
815
  // ── Callbacks → Notifications ─────────────────────────────────
652
816
 
653
- private createCallbacks(): AgentCallbacks {
817
+ private createCallbacks(ctx: SessionContext): AgentCallbacks {
654
818
  return {
655
819
  onMessagesChange: (messages: Message[]) => {
656
- this.emit("messagesChange", { messages });
820
+ this.emit("messagesChange", { messages }, ctx.registeredSessionId);
657
821
  },
658
822
  onUserMessageAdded: () => {
659
- const msg = this.findLastUserMessage();
660
- if (msg) this.emit("userMessageAdded", { message: msg });
823
+ const msg = this.findLastUserMessage(ctx.agent);
824
+ if (msg)
825
+ this.emit(
826
+ "userMessageAdded",
827
+ { message: msg },
828
+ ctx.registeredSessionId,
829
+ );
661
830
  },
662
831
  onAssistantMessageAdded: (messageId: string) => {
663
- const msg = this.agent?.messages.find((m) => m.id === messageId);
664
- if (msg) this.emit("assistantMessageAdded", { message: msg });
832
+ const msg = ctx.agent?.messages.find((m) => m.id === messageId);
833
+ if (msg)
834
+ this.emit(
835
+ "assistantMessageAdded",
836
+ { message: msg },
837
+ ctx.registeredSessionId,
838
+ );
665
839
  },
666
840
  onAssistantContentUpdated: (params) => {
667
- this.emit("assistantContentUpdated", params);
841
+ this.emit("assistantContentUpdated", params, ctx.registeredSessionId);
668
842
  },
669
843
  onAssistantReasoningUpdated: (params) => {
670
- this.emit("assistantReasoningUpdated", params);
844
+ this.emit("assistantReasoningUpdated", params, ctx.registeredSessionId);
671
845
  },
672
846
  onToolBlockUpdated: (params) => {
673
- this.emit("toolBlockUpdated", params);
847
+ this.emit("toolBlockUpdated", params, ctx.registeredSessionId);
674
848
  },
675
849
  onErrorBlockAdded: (error: string) => {
676
- this.emit("errorBlockAdded", { error });
850
+ this.emit("errorBlockAdded", { error }, ctx.registeredSessionId);
677
851
  },
678
852
  onLoadingChange: (loading: boolean) => {
679
- this.emit("loadingChange", {
680
- loading,
681
- latestTotalTokens: this.agent?.latestTotalTokens,
682
- });
853
+ this.emit(
854
+ "loadingChange",
855
+ {
856
+ loading,
857
+ latestTotalTokens: ctx.agent?.latestTotalTokens,
858
+ },
859
+ ctx.registeredSessionId,
860
+ );
683
861
  },
684
862
  onCommandRunningChange: (running: boolean) => {
685
- this.emit("commandRunningChange", { running });
863
+ this.emit("commandRunningChange", { running }, ctx.registeredSessionId);
686
864
  },
687
865
  onQueuedMessagesChange: (messages: QueuedMessage[]) => {
688
- this.emit("queuedMessagesChange", { messages });
866
+ this.emit(
867
+ "queuedMessagesChange",
868
+ { messages },
869
+ ctx.registeredSessionId,
870
+ );
689
871
  },
690
872
  onTasksChange: (tasks: Task[]) => {
691
- this.emit("tasksChange", { tasks });
873
+ this.emit("tasksChange", { tasks }, ctx.registeredSessionId);
692
874
  },
693
- onSessionIdChange: (sessionId: string) => {
694
- this.emit("sessionIdChange", { sessionId });
875
+ onSessionIdChange: (newSessionId: string) => {
876
+ const oldSessionId = ctx.registeredSessionId;
877
+ // Emit with the OLD sessionId so the client's router can deliver it
878
+ this.emit("sessionIdChange", { sessionId: newSessionId }, oldSessionId);
879
+ // Update the sessions Map key atomically (single-threaded, no await)
880
+ if (oldSessionId && oldSessionId !== newSessionId) {
881
+ const entry = this.sessions.get(oldSessionId);
882
+ if (entry) {
883
+ this.sessions.delete(oldSessionId);
884
+ this.sessions.set(newSessionId, entry);
885
+ }
886
+ }
887
+ ctx.registeredSessionId = newSessionId;
695
888
  },
696
889
  onPermissionModeChange: (mode: PermissionMode) => {
697
- this.emit("permissionModeChange", { mode });
890
+ this.emit("permissionModeChange", { mode }, ctx.registeredSessionId);
698
891
  },
699
892
  onMcpServersChange: (servers: McpServerStatus[]) => {
700
- this.emit("mcpServersChange", { servers });
893
+ this.emit("mcpServersChange", { servers }, ctx.registeredSessionId);
701
894
  },
702
895
  onAddBangMessage: () => {
703
- this.emit("bangMessageAdded", {});
896
+ this.emit("bangMessageAdded", {}, ctx.registeredSessionId);
704
897
  },
705
898
  onUpdateBangMessage: () => {
706
- this.emit("bangMessageUpdated", {});
899
+ this.emit("bangMessageUpdated", {}, ctx.registeredSessionId);
707
900
  },
708
901
  onCompleteBangMessage: () => {
709
- this.emit("bangMessageCompleted", {});
902
+ this.emit("bangMessageCompleted", {}, ctx.registeredSessionId);
710
903
  },
711
904
  onNotificationMessageAdded: (params) => {
712
- const msg = this.agent?.messages.find(
905
+ const msg = ctx.agent?.messages.find(
713
906
  (m) =>
714
907
  m.role === "user" &&
715
908
  m.blocks.some(
@@ -718,26 +911,42 @@ export class AgentBridge {
718
911
  (b as { taskId: string }).taskId === params.taskId,
719
912
  ),
720
913
  );
721
- this.emit("notificationMessageAdded", {
722
- ...params,
723
- message: msg,
724
- });
914
+ this.emit(
915
+ "notificationMessageAdded",
916
+ { ...params, message: msg },
917
+ ctx.registeredSessionId,
918
+ );
725
919
  },
726
920
  };
727
921
  }
728
922
 
729
- private findLastUserMessage(): Message | undefined {
730
- const userMessages =
731
- this.agent?.messages.filter((m) => m.role === "user") ?? [];
923
+ private findLastUserMessage(agent?: Agent): Message | undefined {
924
+ const userMessages = agent?.messages.filter((m) => m.role === "user") ?? [];
732
925
  return userMessages[userMessages.length - 1];
733
926
  }
734
927
 
735
928
  // ── Utils ─────────────────────────────────────────────────────
736
929
 
737
- private requireAgent(): void {
738
- if (!this.agent) {
739
- throw new RpcError(PROTOCOL_INTERNAL_ERROR, "Agent not initialized");
930
+ private requireSession(sessionId?: string): SessionEntry {
931
+ if (!sessionId) {
932
+ throw new RpcError(
933
+ PROTOCOL_INTERNAL_ERROR,
934
+ "sessionId is required for this request",
935
+ );
740
936
  }
937
+ const entry = this.sessions.get(sessionId);
938
+ if (!entry) {
939
+ throw new RpcError(
940
+ PROTOCOL_INTERNAL_ERROR,
941
+ `Session not found: ${sessionId}`,
942
+ );
943
+ }
944
+ return entry;
945
+ }
946
+
947
+ private getSessionWorkdir(sessionId?: string): string | undefined {
948
+ if (!sessionId) return undefined;
949
+ return this.sessions.get(sessionId)?.agent.workingDirectory;
741
950
  }
742
951
  }
743
952