wave-code 0.19.3 → 0.19.5

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,105 +2,124 @@
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
  */
11
17
  import { Agent, listSessions, searchFiles, PromptHistoryManager, AuthService, PluginCore, } from "wave-agent-sdk";
12
18
  import { INTERNAL_ERROR as PROTOCOL_INTERNAL_ERROR, METHOD_NOT_FOUND as PROTOCOL_METHOD_NOT_FOUND, } from "./protocol.js";
19
+ import { readFileSync } from "fs";
20
+ import path from "path";
21
+ import { fileURLToPath } from "url";
22
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
+ const CLI_VERSION = (() => {
24
+ try {
25
+ const pkg = JSON.parse(readFileSync(path.resolve(__dirname, "../../package.json"), "utf-8"));
26
+ return pkg.version ?? "";
27
+ }
28
+ catch {
29
+ return "";
30
+ }
31
+ })();
13
32
  export class AgentBridge {
14
33
  constructor(options) {
34
+ this.sessions = new Map();
15
35
  this.pendingPermissions = new Map();
16
36
  this.permissionCounter = 0;
17
- this.storedConfig = {};
18
37
  this.emit = options.emit;
19
38
  }
20
39
  // ── Public API ────────────────────────────────────────────────
21
- async handleRequest(method, params) {
40
+ async handleRequest(method, params, sessionId) {
22
41
  const p = (params ?? {});
23
42
  switch (method) {
24
43
  // ── Lifecycle ──
25
44
  case "initialize":
26
45
  return this.initialize(p);
27
46
  case "destroy":
28
- return this.destroy();
47
+ return this.destroy(sessionId);
29
48
  case "restoreSession":
30
- return this.restoreSession(p.sessionId);
49
+ return this.restoreSession(p.sessionId, sessionId);
31
50
  case "listSessions":
32
- return this.listSessions(p.workdir);
51
+ return this.listSessions(p.workdir, sessionId);
33
52
  case "getSessionInfo":
34
- return this.getSessionInfo();
53
+ return this.getSessionInfo(sessionId);
35
54
  case "updateConfig":
36
- return this.updateConfig(p);
55
+ return this.updateConfig(p, sessionId);
37
56
  // ── Messages ──
38
57
  case "sendMessage":
39
- return this.sendMessage(p);
58
+ return this.sendMessage(p, sessionId);
40
59
  case "bang":
41
- return this.bang(p.command);
60
+ return this.bang(p.command, sessionId);
42
61
  case "abortMessage":
43
- return this.abortMessage();
62
+ return this.abortMessage(sessionId);
44
63
  case "clearMessages":
45
- return this.clearMessages();
64
+ return this.clearMessages(sessionId);
46
65
  case "rewindToMessage":
47
- return this.rewindToMessage(p.messageId);
66
+ return this.rewindToMessage(p.messageId, sessionId);
48
67
  case "deleteQueuedMessage":
49
- return this.deleteQueuedMessage(p.index);
68
+ return this.deleteQueuedMessage(p.index, sessionId);
50
69
  case "getMessages":
51
- return this.getMessages();
70
+ return this.getMessages(sessionId);
52
71
  case "getFullMessageThread":
53
- return this.getFullMessageThread();
72
+ return this.getFullMessageThread(sessionId);
54
73
  // ── Permissions ──
55
74
  case "setPermissionMode":
56
- return this.setPermissionMode(p.mode);
75
+ return this.setPermissionMode(p.mode, sessionId);
57
76
  case "getPermissionMode":
58
- return this.getPermissionMode();
77
+ return this.getPermissionMode(sessionId);
59
78
  // ── MCP ──
60
79
  case "getMcpServers":
61
- return this.getMcpServers();
80
+ return this.getMcpServers(sessionId);
62
81
  case "connectMcpServer":
63
- return this.connectMcpServer(p.serverName);
82
+ return this.connectMcpServer(p.serverName, sessionId);
64
83
  case "disconnectMcpServer":
65
- return this.disconnectMcpServer(p.serverName);
84
+ return this.disconnectMcpServer(p.serverName, sessionId);
66
85
  // ── Commands ──
67
86
  case "getSlashCommands":
68
- return this.getSlashCommands();
69
- // ── File / History ──
87
+ return this.getSlashCommands(sessionId);
88
+ // ── File / History (global — no session required) ──
70
89
  case "searchFiles":
71
- return this.searchFiles(p);
90
+ return this.searchFiles(p, sessionId);
72
91
  case "getPromptHistory":
73
- return this.getPromptHistory(p.workdir);
92
+ return this.getPromptHistory(p.workdir, sessionId);
74
93
  case "searchPromptHistory":
75
- return this.searchPromptHistory(p.query, p.workdir);
76
- // ── Auth ──
94
+ return this.searchPromptHistory(p.query, p.workdir, sessionId);
95
+ // ── Auth (global — no session required) ──
77
96
  case "getAuthStatus":
78
97
  return this.getAuthStatus();
79
98
  case "login":
80
99
  return this.login(p.serverUrl);
81
100
  case "logout":
82
101
  return this.logout();
83
- // ── Plugins ──
102
+ // ── Plugins (global — no session required) ──
84
103
  case "listPlugins":
85
- return this.listPlugins(p.workdir);
104
+ return this.listPlugins(p.workdir, sessionId);
86
105
  case "installPlugin":
87
- return this.installPlugin(p.pluginId, p.scope, p.workdir);
106
+ return this.installPlugin(p.pluginId, p.scope, p.workdir, sessionId);
88
107
  case "uninstallPlugin":
89
- return this.uninstallPlugin(p.pluginId, p.workdir);
108
+ return this.uninstallPlugin(p.pluginId, p.workdir, sessionId);
90
109
  case "enablePlugin":
91
- return this.enablePlugin(p.pluginId, p.scope, p.workdir);
110
+ return this.enablePlugin(p.pluginId, p.scope, p.workdir, sessionId);
92
111
  case "disablePlugin":
93
- return this.disablePlugin(p.pluginId, p.scope, p.workdir);
112
+ return this.disablePlugin(p.pluginId, p.scope, p.workdir, sessionId);
94
113
  case "updatePlugin":
95
- return this.updatePlugin(p.pluginId, p.workdir);
114
+ return this.updatePlugin(p.pluginId, p.workdir, sessionId);
96
115
  case "listMarketplaces":
97
- return this.listMarketplaces(p.workdir);
116
+ return this.listMarketplaces(p.workdir, sessionId);
98
117
  case "addMarketplace":
99
- return this.addMarketplace(p.input, p.scope, p.workdir);
118
+ return this.addMarketplace(p.input, p.scope, p.workdir, sessionId);
100
119
  case "removeMarketplace":
101
- return this.removeMarketplace(p.name, p.scope, p.workdir);
120
+ return this.removeMarketplace(p.name, p.scope, p.workdir, sessionId);
102
121
  case "updateMarketplace":
103
- return this.updateMarketplace(p.name, p.workdir);
122
+ return this.updateMarketplace(p.name, p.workdir, sessionId);
104
123
  default:
105
124
  throw new RpcError(PROTOCOL_METHOD_NOT_FOUND, `Method not found: ${method}`);
106
125
  }
@@ -108,6 +127,7 @@ export class AgentBridge {
108
127
  handleNotification(method, params) {
109
128
  if (method === "permissionResponse") {
110
129
  const p = params;
130
+ // requestId is process-level unique; lookup doesn't need sessionId
111
131
  const resolve = this.pendingPermissions.get(p.requestId);
112
132
  if (resolve) {
113
133
  this.pendingPermissions.delete(p.requestId);
@@ -117,9 +137,8 @@ export class AgentBridge {
117
137
  }
118
138
  // ── Lifecycle ─────────────────────────────────────────────────
119
139
  async initialize(params) {
120
- // Merge with stored config (CLI defaults can be overridden by client)
121
- this.storedConfig = { ...this.storedConfig, ...params };
122
- const callbacks = this.createCallbacks();
140
+ const ctx = {};
141
+ const callbacks = this.createCallbacks(ctx);
123
142
  const options = {
124
143
  callbacks,
125
144
  workdir: params.workdir,
@@ -136,168 +155,207 @@ export class AgentBridge {
136
155
  disallowedTools: params.disallowedTools,
137
156
  plugins: params.pluginDirs?.map((path) => ({ type: "local", path })),
138
157
  mcpServers: params.mcpServers,
139
- canUseTool: (context) => this.canUseTool(context),
158
+ canUseTool: (context) => this.canUseTool(context, ctx),
140
159
  };
141
- this.agent = await Agent.create(options);
160
+ const agent = await Agent.create(options);
161
+ ctx.agent = agent;
162
+ ctx.registeredSessionId = agent.sessionId;
163
+ this.sessions.set(agent.sessionId, {
164
+ agent,
165
+ storedConfig: { ...params },
166
+ });
167
+ if (params.clientVersion) {
168
+ console.debug(`[agentBridge] clientVersion=${params.clientVersion} serverVersion=${CLI_VERSION}`);
169
+ }
142
170
  return {
143
- sessionId: this.agent.sessionId,
144
- workingDirectory: this.agent.workingDirectory,
145
- permissionMode: this.agent.getPermissionMode(),
146
- latestTotalTokens: this.agent.latestTotalTokens,
171
+ sessionId: agent.sessionId,
172
+ workingDirectory: agent.workingDirectory,
173
+ permissionMode: agent.getPermissionMode(),
174
+ latestTotalTokens: agent.latestTotalTokens,
175
+ serverVersion: CLI_VERSION,
147
176
  };
148
177
  }
149
- async destroy() {
150
- if (this.agent) {
151
- await this.agent.destroy();
152
- this.agent = undefined;
178
+ async destroy(sessionId) {
179
+ if (sessionId) {
180
+ const entry = this.sessions.get(sessionId);
181
+ if (entry) {
182
+ await entry.agent.destroy();
183
+ this.sessions.delete(sessionId);
184
+ }
153
185
  }
154
186
  return null;
155
187
  }
156
- async restoreSession(sessionId) {
157
- this.requireAgent();
158
- await this.agent.restoreSession(sessionId);
188
+ async restoreSession(restoreId, sessionId) {
189
+ const entry = this.requireSession(sessionId);
190
+ await entry.agent.restoreSession(restoreId);
159
191
  return null;
160
192
  }
161
- async listSessions(workdir) {
162
- const sessions = await listSessions(workdir || this.agent?.workingDirectory || process.cwd());
193
+ async listSessions(workdir, sessionId) {
194
+ const sessions = await listSessions(workdir || this.getSessionWorkdir(sessionId) || process.cwd());
163
195
  return { sessions };
164
196
  }
165
- getSessionInfo() {
166
- this.requireAgent();
197
+ getSessionInfo(sessionId) {
198
+ const entry = this.requireSession(sessionId);
167
199
  return {
168
- sessionId: this.agent.sessionId,
169
- workingDirectory: this.agent.workingDirectory,
170
- latestTotalTokens: this.agent.latestTotalTokens,
171
- permissionMode: this.agent.getPermissionMode(),
172
- availableTools: this.agent.getAvailableToolNames(),
200
+ sessionId: entry.agent.sessionId,
201
+ workingDirectory: entry.agent.workingDirectory,
202
+ latestTotalTokens: entry.agent.latestTotalTokens,
203
+ permissionMode: entry.agent.getPermissionMode(),
204
+ availableTools: entry.agent.getAvailableToolNames(),
173
205
  };
174
206
  }
175
- async updateConfig(params) {
176
- this.requireAgent();
177
- const currentSessionId = this.agent.sessionId;
207
+ async updateConfig(params, sessionId) {
208
+ const entry = this.requireSession(sessionId);
209
+ const currentSessionId = entry.agent.sessionId;
178
210
  // Merge new config into stored config
179
- this.storedConfig = { ...this.storedConfig, ...params };
180
- // Destroy and recreate
181
- await this.agent.destroy();
182
- this.agent = undefined;
183
- await this.initialize({
184
- ...this.storedConfig,
211
+ entry.storedConfig = { ...entry.storedConfig, ...params };
212
+ // Destroy and recreate within the same session slot
213
+ await entry.agent.destroy();
214
+ this.sessions.delete(currentSessionId);
215
+ const ctx = {};
216
+ const callbacks = this.createCallbacks(ctx);
217
+ const options = {
218
+ callbacks,
219
+ workdir: entry.storedConfig.workdir,
185
220
  restoreSessionId: currentSessionId,
221
+ apiKey: entry.storedConfig.apiKey,
222
+ baseURL: entry.storedConfig.baseURL,
223
+ defaultHeaders: entry.storedConfig.defaultHeaders,
224
+ model: entry.storedConfig.model,
225
+ fastModel: entry.storedConfig.fastModel,
226
+ language: entry.storedConfig.language,
227
+ permissionMode: entry.storedConfig.permissionMode,
228
+ tools: entry.storedConfig.tools,
229
+ allowedTools: entry.storedConfig.allowedTools,
230
+ disallowedTools: entry.storedConfig.disallowedTools,
231
+ plugins: entry.storedConfig.pluginDirs?.map((p) => ({
232
+ type: "local",
233
+ path: p,
234
+ })),
235
+ mcpServers: entry.storedConfig.mcpServers,
236
+ canUseTool: (context) => this.canUseTool(context, ctx),
237
+ };
238
+ const agent = await Agent.create(options);
239
+ ctx.agent = agent;
240
+ ctx.registeredSessionId = agent.sessionId;
241
+ this.sessions.set(agent.sessionId, {
242
+ agent,
243
+ storedConfig: { ...entry.storedConfig },
186
244
  });
187
- return { sessionId: this.agent.sessionId };
245
+ return { sessionId: agent.sessionId };
188
246
  }
189
247
  // ── Messages ──────────────────────────────────────────────────
190
- async sendMessage(params) {
191
- this.requireAgent();
248
+ async sendMessage(params, sessionId) {
249
+ const entry = this.requireSession(sessionId);
192
250
  if (params.force) {
193
- this.agent.abortMessage();
251
+ entry.agent.abortMessage();
194
252
  }
195
253
  // Save prompt to history (mirrors VSCE chatSession.ts:236-242)
196
254
  try {
197
- await PromptHistoryManager.addEntry(params.text, this.agent.sessionId, {}, this.agent.workingDirectory);
255
+ await PromptHistoryManager.addEntry(params.text, entry.agent.sessionId, {}, entry.agent.workingDirectory);
198
256
  }
199
257
  catch {
200
258
  // Best-effort; don't block message sending on history save failure
201
259
  }
202
- await this.agent.sendMessage(params.text, params.images);
260
+ await entry.agent.sendMessage(params.text, params.images);
203
261
  return null;
204
262
  }
205
- async bang(command) {
206
- this.requireAgent();
207
- await this.agent.bang(command);
263
+ async bang(command, sessionId) {
264
+ const entry = this.requireSession(sessionId);
265
+ await entry.agent.bang(command);
208
266
  return null;
209
267
  }
210
- async abortMessage() {
211
- this.requireAgent();
212
- this.agent.abortMessage();
268
+ async abortMessage(sessionId) {
269
+ const entry = this.requireSession(sessionId);
270
+ entry.agent.abortMessage();
213
271
  return null;
214
272
  }
215
- async clearMessages() {
216
- this.requireAgent();
217
- this.agent.clearMessages();
273
+ async clearMessages(sessionId) {
274
+ const entry = this.requireSession(sessionId);
275
+ entry.agent.clearMessages();
218
276
  return null;
219
277
  }
220
- async rewindToMessage(messageId) {
221
- this.requireAgent();
222
- const { messages } = await this.agent.getFullMessageThread();
278
+ async rewindToMessage(messageId, sessionId) {
279
+ const entry = this.requireSession(sessionId);
280
+ const { messages } = await entry.agent.getFullMessageThread();
223
281
  const index = messages.findIndex((m) => m.id === messageId);
224
282
  if (index === -1) {
225
283
  throw new RpcError(PROTOCOL_INTERNAL_ERROR, `Message not found: ${messageId}`);
226
284
  }
227
285
  const message = messages[index];
228
286
  const textBlock = message.blocks.find((b) => b.type === "text");
229
- await this.agent.truncateHistory(index);
287
+ await entry.agent.truncateHistory(index);
230
288
  return { inputContent: textBlock?.content || "" };
231
289
  }
232
- deleteQueuedMessage(index) {
233
- this.requireAgent();
234
- this.agent.removeQueuedMessage(index);
290
+ deleteQueuedMessage(index, sessionId) {
291
+ const entry = this.requireSession(sessionId);
292
+ entry.agent.removeQueuedMessage(index);
235
293
  return null;
236
294
  }
237
- getMessages() {
238
- this.requireAgent();
239
- return { messages: this.agent.messages };
295
+ getMessages(sessionId) {
296
+ const entry = this.requireSession(sessionId);
297
+ return { messages: entry.agent.messages };
240
298
  }
241
- async getFullMessageThread() {
242
- this.requireAgent();
243
- return this.agent.getFullMessageThread();
299
+ async getFullMessageThread(sessionId) {
300
+ const entry = this.requireSession(sessionId);
301
+ return entry.agent.getFullMessageThread();
244
302
  }
245
303
  // ── Permissions ───────────────────────────────────────────────
246
- async setPermissionMode(mode) {
247
- this.requireAgent();
248
- await this.agent.setPermissionMode(mode);
304
+ async setPermissionMode(mode, sessionId) {
305
+ const entry = this.requireSession(sessionId);
306
+ await entry.agent.setPermissionMode(mode);
249
307
  return null;
250
308
  }
251
- getPermissionMode() {
252
- this.requireAgent();
253
- return { mode: this.agent.getPermissionMode() };
309
+ getPermissionMode(sessionId) {
310
+ const entry = this.requireSession(sessionId);
311
+ return { mode: entry.agent.getPermissionMode() };
254
312
  }
255
313
  // ── MCP ───────────────────────────────────────────────────────
256
- getMcpServers() {
257
- this.requireAgent();
258
- return { servers: this.agent.getMcpServers() };
314
+ getMcpServers(sessionId) {
315
+ const entry = this.requireSession(sessionId);
316
+ return { servers: entry.agent.getMcpServers() };
259
317
  }
260
- async connectMcpServer(serverName) {
261
- this.requireAgent();
262
- const success = await this.agent.connectMcpServer(serverName);
318
+ async connectMcpServer(serverName, sessionId) {
319
+ const entry = this.requireSession(sessionId);
320
+ const success = await entry.agent.connectMcpServer(serverName);
263
321
  return { success };
264
322
  }
265
- async disconnectMcpServer(serverName) {
266
- this.requireAgent();
267
- const success = await this.agent.disconnectMcpServer(serverName);
323
+ async disconnectMcpServer(serverName, sessionId) {
324
+ const entry = this.requireSession(sessionId);
325
+ const success = await entry.agent.disconnectMcpServer(serverName);
268
326
  return { success };
269
327
  }
270
328
  // ── Commands ──────────────────────────────────────────────────
271
- getSlashCommands() {
272
- this.requireAgent();
273
- return { commands: this.agent.getSlashCommands() };
329
+ getSlashCommands(sessionId) {
330
+ const entry = this.requireSession(sessionId);
331
+ return { commands: entry.agent.getSlashCommands() };
274
332
  }
275
- // ── File / History ────────────────────────────────────────────
276
- async searchFiles(params) {
333
+ // ── File / History (global) ───────────────────────────────────
334
+ async searchFiles(params, sessionId) {
277
335
  const files = await searchFiles(params.query, {
278
336
  maxResults: params.maxResults,
279
- workingDirectory: params.workdir || this.agent?.workingDirectory || process.cwd(),
337
+ workingDirectory: params.workdir || this.getSessionWorkdir(sessionId) || process.cwd(),
280
338
  });
281
339
  return { files };
282
340
  }
283
- async getPromptHistory(workdir) {
341
+ async getPromptHistory(workdir, sessionId) {
284
342
  const history = await PromptHistoryManager.getHistory({
285
- workdir: workdir || this.agent?.workingDirectory,
343
+ workdir: workdir || this.getSessionWorkdir(sessionId),
286
344
  });
287
345
  return { history };
288
346
  }
289
- async searchPromptHistory(query, workdir) {
347
+ async searchPromptHistory(query, workdir, sessionId) {
290
348
  const history = await PromptHistoryManager.searchHistory(query, {
291
- workdir: workdir || this.agent?.workingDirectory,
349
+ workdir: workdir || this.getSessionWorkdir(sessionId),
292
350
  });
293
351
  return { history };
294
352
  }
295
353
  // ── canUseTool flow ───────────────────────────────────────────
296
- canUseTool(context) {
354
+ canUseTool(context, ctx) {
297
355
  const requestId = `perm_${++this.permissionCounter}`;
298
356
  return new Promise((resolve) => {
299
357
  this.pendingPermissions.set(requestId, resolve);
300
- this.emit("permissionRequest", { requestId, context });
358
+ this.emit("permissionRequest", { requestId, context }, ctx.registeredSessionId);
301
359
  // 5-minute timeout → auto-deny
302
360
  setTimeout(() => {
303
361
  if (this.pendingPermissions.has(requestId)) {
@@ -310,12 +368,13 @@ export class AgentBridge {
310
368
  }, 5 * 60 * 1000);
311
369
  });
312
370
  }
313
- // ── Auth ─────────────────────────────────────────────────────
371
+ // ── Auth (global) ────────────────────────────────────────────
314
372
  async getAuthStatus() {
315
373
  const authService = AuthService.getInstance();
316
374
  return {
317
375
  isAuthenticated: authService.isSSOAuthenticated(),
318
376
  user: authService.getAuthUser(),
377
+ serverUrl: authService.getServerUrl(),
319
378
  };
320
379
  }
321
380
  async login(serverUrl) {
@@ -333,17 +392,17 @@ export class AgentBridge {
333
392
  await authService.clearAuth();
334
393
  return null;
335
394
  }
336
- // ── Plugins ──────────────────────────────────────────────────
337
- getPluginCore(workdir) {
338
- const resolvedWorkdir = workdir || this.agent?.workingDirectory || process.cwd();
395
+ // ── Plugins (global) ─────────────────────────────────────────
396
+ getPluginCore(workdir, sessionId) {
397
+ const resolvedWorkdir = workdir || this.getSessionWorkdir(sessionId) || process.cwd();
339
398
  if (!this.pluginCore || this.pluginCoreWorkdir !== resolvedWorkdir) {
340
399
  this.pluginCore = new PluginCore(resolvedWorkdir);
341
400
  this.pluginCoreWorkdir = resolvedWorkdir;
342
401
  }
343
402
  return this.pluginCore;
344
403
  }
345
- async listPlugins(workdir) {
346
- const core = this.getPluginCore(workdir);
404
+ async listPlugins(workdir, sessionId) {
405
+ const core = this.getPluginCore(workdir, sessionId);
347
406
  const { plugins, mergedEnabled } = await core.listPlugins();
348
407
  return {
349
408
  plugins: plugins.map((p) => {
@@ -361,117 +420,135 @@ export class AgentBridge {
361
420
  }),
362
421
  };
363
422
  }
364
- async installPlugin(pluginId, scope, workdir) {
365
- return this.getPluginCore(workdir).installPlugin(pluginId, scope);
423
+ async installPlugin(pluginId, scope, workdir, sessionId) {
424
+ return this.getPluginCore(workdir, sessionId).installPlugin(pluginId, scope);
366
425
  }
367
- async uninstallPlugin(pluginId, workdir) {
368
- await this.getPluginCore(workdir).uninstallPlugin(pluginId);
426
+ async uninstallPlugin(pluginId, workdir, sessionId) {
427
+ await this.getPluginCore(workdir, sessionId).uninstallPlugin(pluginId);
369
428
  return null;
370
429
  }
371
- async enablePlugin(pluginId, scope, workdir) {
372
- return this.getPluginCore(workdir).enablePlugin(pluginId, scope);
430
+ async enablePlugin(pluginId, scope, workdir, sessionId) {
431
+ return this.getPluginCore(workdir, sessionId).enablePlugin(pluginId, scope);
373
432
  }
374
- async disablePlugin(pluginId, scope, workdir) {
375
- return this.getPluginCore(workdir).disablePlugin(pluginId, scope);
433
+ async disablePlugin(pluginId, scope, workdir, sessionId) {
434
+ return this.getPluginCore(workdir, sessionId).disablePlugin(pluginId, scope);
376
435
  }
377
- async updatePlugin(pluginId, workdir) {
378
- return this.getPluginCore(workdir).updatePlugin(pluginId);
436
+ async updatePlugin(pluginId, workdir, sessionId) {
437
+ return this.getPluginCore(workdir, sessionId).updatePlugin(pluginId);
379
438
  }
380
- async listMarketplaces(workdir) {
381
- return this.getPluginCore(workdir).listMarketplaces();
439
+ async listMarketplaces(workdir, sessionId) {
440
+ return this.getPluginCore(workdir, sessionId).listMarketplaces();
382
441
  }
383
- async addMarketplace(input, scope, workdir) {
384
- return this.getPluginCore(workdir).addMarketplace(input, scope);
442
+ async addMarketplace(input, scope, workdir, sessionId) {
443
+ return this.getPluginCore(workdir, sessionId).addMarketplace(input, scope);
385
444
  }
386
- async removeMarketplace(name, scope, workdir) {
387
- await this.getPluginCore(workdir).removeMarketplace(name, scope);
445
+ async removeMarketplace(name, scope, workdir, sessionId) {
446
+ await this.getPluginCore(workdir, sessionId).removeMarketplace(name, scope);
388
447
  return null;
389
448
  }
390
- async updateMarketplace(name, workdir) {
391
- await this.getPluginCore(workdir).updateMarketplace(name);
449
+ async updateMarketplace(name, workdir, sessionId) {
450
+ await this.getPluginCore(workdir, sessionId).updateMarketplace(name);
392
451
  return null;
393
452
  }
394
453
  // ── Callbacks → Notifications ─────────────────────────────────
395
- createCallbacks() {
454
+ createCallbacks(ctx) {
396
455
  return {
397
456
  onMessagesChange: (messages) => {
398
- this.emit("messagesChange", { messages });
457
+ this.emit("messagesChange", { messages }, ctx.registeredSessionId);
399
458
  },
400
459
  onUserMessageAdded: () => {
401
- const msg = this.findLastUserMessage();
460
+ const msg = this.findLastUserMessage(ctx.agent);
402
461
  if (msg)
403
- this.emit("userMessageAdded", { message: msg });
462
+ this.emit("userMessageAdded", { message: msg }, ctx.registeredSessionId);
404
463
  },
405
464
  onAssistantMessageAdded: (messageId) => {
406
- const msg = this.agent?.messages.find((m) => m.id === messageId);
465
+ const msg = ctx.agent?.messages.find((m) => m.id === messageId);
407
466
  if (msg)
408
- this.emit("assistantMessageAdded", { message: msg });
467
+ this.emit("assistantMessageAdded", { message: msg }, ctx.registeredSessionId);
409
468
  },
410
469
  onAssistantContentUpdated: (params) => {
411
- this.emit("assistantContentUpdated", params);
470
+ this.emit("assistantContentUpdated", params, ctx.registeredSessionId);
412
471
  },
413
472
  onAssistantReasoningUpdated: (params) => {
414
- this.emit("assistantReasoningUpdated", params);
473
+ this.emit("assistantReasoningUpdated", params, ctx.registeredSessionId);
415
474
  },
416
475
  onToolBlockUpdated: (params) => {
417
- this.emit("toolBlockUpdated", params);
476
+ this.emit("toolBlockUpdated", params, ctx.registeredSessionId);
418
477
  },
419
478
  onErrorBlockAdded: (error) => {
420
- this.emit("errorBlockAdded", { error });
479
+ this.emit("errorBlockAdded", { error }, ctx.registeredSessionId);
421
480
  },
422
481
  onLoadingChange: (loading) => {
423
482
  this.emit("loadingChange", {
424
483
  loading,
425
- latestTotalTokens: this.agent?.latestTotalTokens,
426
- });
484
+ latestTotalTokens: ctx.agent?.latestTotalTokens,
485
+ }, ctx.registeredSessionId);
427
486
  },
428
487
  onCommandRunningChange: (running) => {
429
- this.emit("commandRunningChange", { running });
488
+ this.emit("commandRunningChange", { running }, ctx.registeredSessionId);
430
489
  },
431
490
  onQueuedMessagesChange: (messages) => {
432
- this.emit("queuedMessagesChange", { messages });
491
+ this.emit("queuedMessagesChange", { messages }, ctx.registeredSessionId);
433
492
  },
434
493
  onTasksChange: (tasks) => {
435
- this.emit("tasksChange", { tasks });
494
+ this.emit("tasksChange", { tasks }, ctx.registeredSessionId);
436
495
  },
437
- onSessionIdChange: (sessionId) => {
438
- this.emit("sessionIdChange", { sessionId });
496
+ onSessionIdChange: (newSessionId) => {
497
+ const oldSessionId = ctx.registeredSessionId;
498
+ // Emit with the OLD sessionId so the client's router can deliver it
499
+ this.emit("sessionIdChange", { sessionId: newSessionId }, oldSessionId);
500
+ // Update the sessions Map key atomically (single-threaded, no await)
501
+ if (oldSessionId && oldSessionId !== newSessionId) {
502
+ const entry = this.sessions.get(oldSessionId);
503
+ if (entry) {
504
+ this.sessions.delete(oldSessionId);
505
+ this.sessions.set(newSessionId, entry);
506
+ }
507
+ }
508
+ ctx.registeredSessionId = newSessionId;
439
509
  },
440
510
  onPermissionModeChange: (mode) => {
441
- this.emit("permissionModeChange", { mode });
511
+ this.emit("permissionModeChange", { mode }, ctx.registeredSessionId);
442
512
  },
443
513
  onMcpServersChange: (servers) => {
444
- this.emit("mcpServersChange", { servers });
514
+ this.emit("mcpServersChange", { servers }, ctx.registeredSessionId);
445
515
  },
446
516
  onAddBangMessage: () => {
447
- this.emit("bangMessageAdded", {});
517
+ this.emit("bangMessageAdded", {}, ctx.registeredSessionId);
448
518
  },
449
519
  onUpdateBangMessage: () => {
450
- this.emit("bangMessageUpdated", {});
520
+ this.emit("bangMessageUpdated", {}, ctx.registeredSessionId);
451
521
  },
452
522
  onCompleteBangMessage: () => {
453
- this.emit("bangMessageCompleted", {});
523
+ this.emit("bangMessageCompleted", {}, ctx.registeredSessionId);
454
524
  },
455
525
  onNotificationMessageAdded: (params) => {
456
- const msg = this.agent?.messages.find((m) => m.role === "user" &&
526
+ const msg = ctx.agent?.messages.find((m) => m.role === "user" &&
457
527
  m.blocks.some((b) => b.type === "task_notification" &&
458
528
  b.taskId === params.taskId));
459
- this.emit("notificationMessageAdded", {
460
- ...params,
461
- message: msg,
462
- });
529
+ this.emit("notificationMessageAdded", { ...params, message: msg }, ctx.registeredSessionId);
463
530
  },
464
531
  };
465
532
  }
466
- findLastUserMessage() {
467
- const userMessages = this.agent?.messages.filter((m) => m.role === "user") ?? [];
533
+ findLastUserMessage(agent) {
534
+ const userMessages = agent?.messages.filter((m) => m.role === "user") ?? [];
468
535
  return userMessages[userMessages.length - 1];
469
536
  }
470
537
  // ── Utils ─────────────────────────────────────────────────────
471
- requireAgent() {
472
- if (!this.agent) {
473
- throw new RpcError(PROTOCOL_INTERNAL_ERROR, "Agent not initialized");
538
+ requireSession(sessionId) {
539
+ if (!sessionId) {
540
+ throw new RpcError(PROTOCOL_INTERNAL_ERROR, "sessionId is required for this request");
474
541
  }
542
+ const entry = this.sessions.get(sessionId);
543
+ if (!entry) {
544
+ throw new RpcError(PROTOCOL_INTERNAL_ERROR, `Session not found: ${sessionId}`);
545
+ }
546
+ return entry;
547
+ }
548
+ getSessionWorkdir(sessionId) {
549
+ if (!sessionId)
550
+ return undefined;
551
+ return this.sessions.get(sessionId)?.agent.workingDirectory;
475
552
  }
476
553
  }
477
554
  // ── Error class for protocol errors ─────────────────────────────