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,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,7 +368,7 @@ 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 {
@@ -333,17 +391,17 @@ export class AgentBridge {
333
391
  await authService.clearAuth();
334
392
  return null;
335
393
  }
336
- // ── Plugins ──────────────────────────────────────────────────
337
- getPluginCore(workdir) {
338
- const resolvedWorkdir = workdir || this.agent?.workingDirectory || process.cwd();
394
+ // ── Plugins (global) ─────────────────────────────────────────
395
+ getPluginCore(workdir, sessionId) {
396
+ const resolvedWorkdir = workdir || this.getSessionWorkdir(sessionId) || process.cwd();
339
397
  if (!this.pluginCore || this.pluginCoreWorkdir !== resolvedWorkdir) {
340
398
  this.pluginCore = new PluginCore(resolvedWorkdir);
341
399
  this.pluginCoreWorkdir = resolvedWorkdir;
342
400
  }
343
401
  return this.pluginCore;
344
402
  }
345
- async listPlugins(workdir) {
346
- const core = this.getPluginCore(workdir);
403
+ async listPlugins(workdir, sessionId) {
404
+ const core = this.getPluginCore(workdir, sessionId);
347
405
  const { plugins, mergedEnabled } = await core.listPlugins();
348
406
  return {
349
407
  plugins: plugins.map((p) => {
@@ -361,117 +419,135 @@ export class AgentBridge {
361
419
  }),
362
420
  };
363
421
  }
364
- async installPlugin(pluginId, scope, workdir) {
365
- return this.getPluginCore(workdir).installPlugin(pluginId, scope);
422
+ async installPlugin(pluginId, scope, workdir, sessionId) {
423
+ return this.getPluginCore(workdir, sessionId).installPlugin(pluginId, scope);
366
424
  }
367
- async uninstallPlugin(pluginId, workdir) {
368
- await this.getPluginCore(workdir).uninstallPlugin(pluginId);
425
+ async uninstallPlugin(pluginId, workdir, sessionId) {
426
+ await this.getPluginCore(workdir, sessionId).uninstallPlugin(pluginId);
369
427
  return null;
370
428
  }
371
- async enablePlugin(pluginId, scope, workdir) {
372
- return this.getPluginCore(workdir).enablePlugin(pluginId, scope);
429
+ async enablePlugin(pluginId, scope, workdir, sessionId) {
430
+ return this.getPluginCore(workdir, sessionId).enablePlugin(pluginId, scope);
373
431
  }
374
- async disablePlugin(pluginId, scope, workdir) {
375
- return this.getPluginCore(workdir).disablePlugin(pluginId, scope);
432
+ async disablePlugin(pluginId, scope, workdir, sessionId) {
433
+ return this.getPluginCore(workdir, sessionId).disablePlugin(pluginId, scope);
376
434
  }
377
- async updatePlugin(pluginId, workdir) {
378
- return this.getPluginCore(workdir).updatePlugin(pluginId);
435
+ async updatePlugin(pluginId, workdir, sessionId) {
436
+ return this.getPluginCore(workdir, sessionId).updatePlugin(pluginId);
379
437
  }
380
- async listMarketplaces(workdir) {
381
- return this.getPluginCore(workdir).listMarketplaces();
438
+ async listMarketplaces(workdir, sessionId) {
439
+ return this.getPluginCore(workdir, sessionId).listMarketplaces();
382
440
  }
383
- async addMarketplace(input, scope, workdir) {
384
- return this.getPluginCore(workdir).addMarketplace(input, scope);
441
+ async addMarketplace(input, scope, workdir, sessionId) {
442
+ return this.getPluginCore(workdir, sessionId).addMarketplace(input, scope);
385
443
  }
386
- async removeMarketplace(name, scope, workdir) {
387
- await this.getPluginCore(workdir).removeMarketplace(name, scope);
444
+ async removeMarketplace(name, scope, workdir, sessionId) {
445
+ await this.getPluginCore(workdir, sessionId).removeMarketplace(name, scope);
388
446
  return null;
389
447
  }
390
- async updateMarketplace(name, workdir) {
391
- await this.getPluginCore(workdir).updateMarketplace(name);
448
+ async updateMarketplace(name, workdir, sessionId) {
449
+ await this.getPluginCore(workdir, sessionId).updateMarketplace(name);
392
450
  return null;
393
451
  }
394
452
  // ── Callbacks → Notifications ─────────────────────────────────
395
- createCallbacks() {
453
+ createCallbacks(ctx) {
396
454
  return {
397
455
  onMessagesChange: (messages) => {
398
- this.emit("messagesChange", { messages });
456
+ this.emit("messagesChange", { messages }, ctx.registeredSessionId);
399
457
  },
400
458
  onUserMessageAdded: () => {
401
- const msg = this.findLastUserMessage();
459
+ const msg = this.findLastUserMessage(ctx.agent);
402
460
  if (msg)
403
- this.emit("userMessageAdded", { message: msg });
461
+ this.emit("userMessageAdded", { message: msg }, ctx.registeredSessionId);
404
462
  },
405
463
  onAssistantMessageAdded: (messageId) => {
406
- const msg = this.agent?.messages.find((m) => m.id === messageId);
464
+ const msg = ctx.agent?.messages.find((m) => m.id === messageId);
407
465
  if (msg)
408
- this.emit("assistantMessageAdded", { message: msg });
466
+ this.emit("assistantMessageAdded", { message: msg }, ctx.registeredSessionId);
409
467
  },
410
468
  onAssistantContentUpdated: (params) => {
411
- this.emit("assistantContentUpdated", params);
469
+ this.emit("assistantContentUpdated", params, ctx.registeredSessionId);
412
470
  },
413
471
  onAssistantReasoningUpdated: (params) => {
414
- this.emit("assistantReasoningUpdated", params);
472
+ this.emit("assistantReasoningUpdated", params, ctx.registeredSessionId);
415
473
  },
416
474
  onToolBlockUpdated: (params) => {
417
- this.emit("toolBlockUpdated", params);
475
+ this.emit("toolBlockUpdated", params, ctx.registeredSessionId);
418
476
  },
419
477
  onErrorBlockAdded: (error) => {
420
- this.emit("errorBlockAdded", { error });
478
+ this.emit("errorBlockAdded", { error }, ctx.registeredSessionId);
421
479
  },
422
480
  onLoadingChange: (loading) => {
423
481
  this.emit("loadingChange", {
424
482
  loading,
425
- latestTotalTokens: this.agent?.latestTotalTokens,
426
- });
483
+ latestTotalTokens: ctx.agent?.latestTotalTokens,
484
+ }, ctx.registeredSessionId);
427
485
  },
428
486
  onCommandRunningChange: (running) => {
429
- this.emit("commandRunningChange", { running });
487
+ this.emit("commandRunningChange", { running }, ctx.registeredSessionId);
430
488
  },
431
489
  onQueuedMessagesChange: (messages) => {
432
- this.emit("queuedMessagesChange", { messages });
490
+ this.emit("queuedMessagesChange", { messages }, ctx.registeredSessionId);
433
491
  },
434
492
  onTasksChange: (tasks) => {
435
- this.emit("tasksChange", { tasks });
493
+ this.emit("tasksChange", { tasks }, ctx.registeredSessionId);
436
494
  },
437
- onSessionIdChange: (sessionId) => {
438
- this.emit("sessionIdChange", { sessionId });
495
+ onSessionIdChange: (newSessionId) => {
496
+ const oldSessionId = ctx.registeredSessionId;
497
+ // Emit with the OLD sessionId so the client's router can deliver it
498
+ this.emit("sessionIdChange", { sessionId: newSessionId }, oldSessionId);
499
+ // Update the sessions Map key atomically (single-threaded, no await)
500
+ if (oldSessionId && oldSessionId !== newSessionId) {
501
+ const entry = this.sessions.get(oldSessionId);
502
+ if (entry) {
503
+ this.sessions.delete(oldSessionId);
504
+ this.sessions.set(newSessionId, entry);
505
+ }
506
+ }
507
+ ctx.registeredSessionId = newSessionId;
439
508
  },
440
509
  onPermissionModeChange: (mode) => {
441
- this.emit("permissionModeChange", { mode });
510
+ this.emit("permissionModeChange", { mode }, ctx.registeredSessionId);
442
511
  },
443
512
  onMcpServersChange: (servers) => {
444
- this.emit("mcpServersChange", { servers });
513
+ this.emit("mcpServersChange", { servers }, ctx.registeredSessionId);
445
514
  },
446
515
  onAddBangMessage: () => {
447
- this.emit("bangMessageAdded", {});
516
+ this.emit("bangMessageAdded", {}, ctx.registeredSessionId);
448
517
  },
449
518
  onUpdateBangMessage: () => {
450
- this.emit("bangMessageUpdated", {});
519
+ this.emit("bangMessageUpdated", {}, ctx.registeredSessionId);
451
520
  },
452
521
  onCompleteBangMessage: () => {
453
- this.emit("bangMessageCompleted", {});
522
+ this.emit("bangMessageCompleted", {}, ctx.registeredSessionId);
454
523
  },
455
524
  onNotificationMessageAdded: (params) => {
456
- const msg = this.agent?.messages.find((m) => m.role === "user" &&
525
+ const msg = ctx.agent?.messages.find((m) => m.role === "user" &&
457
526
  m.blocks.some((b) => b.type === "task_notification" &&
458
527
  b.taskId === params.taskId));
459
- this.emit("notificationMessageAdded", {
460
- ...params,
461
- message: msg,
462
- });
528
+ this.emit("notificationMessageAdded", { ...params, message: msg }, ctx.registeredSessionId);
463
529
  },
464
530
  };
465
531
  }
466
- findLastUserMessage() {
467
- const userMessages = this.agent?.messages.filter((m) => m.role === "user") ?? [];
532
+ findLastUserMessage(agent) {
533
+ const userMessages = agent?.messages.filter((m) => m.role === "user") ?? [];
468
534
  return userMessages[userMessages.length - 1];
469
535
  }
470
536
  // ── Utils ─────────────────────────────────────────────────────
471
- requireAgent() {
472
- if (!this.agent) {
473
- throw new RpcError(PROTOCOL_INTERNAL_ERROR, "Agent not initialized");
537
+ requireSession(sessionId) {
538
+ if (!sessionId) {
539
+ throw new RpcError(PROTOCOL_INTERNAL_ERROR, "sessionId is required for this request");
474
540
  }
541
+ const entry = this.sessions.get(sessionId);
542
+ if (!entry) {
543
+ throw new RpcError(PROTOCOL_INTERNAL_ERROR, `Session not found: ${sessionId}`);
544
+ }
545
+ return entry;
546
+ }
547
+ getSessionWorkdir(sessionId) {
548
+ if (!sessionId)
549
+ return undefined;
550
+ return this.sessions.get(sessionId)?.agent.workingDirectory;
475
551
  }
476
552
  }
477
553
  // ── Error class for protocol errors ─────────────────────────────