@openacp/cli 2026.403.8 → 2026.404.2

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.
package/dist/cli.js CHANGED
@@ -6611,16 +6611,20 @@ var init_tunnel_service = __esm({
6611
6611
  return entry.publicUrl || `http://localhost:${apiPort}`;
6612
6612
  } catch (err) {
6613
6613
  if (this.config.provider === "openacp") {
6614
- log11.warn({ err: err.message }, "OpenACP tunnel service unreachable, falling back to Cloudflare quick tunnel");
6614
+ const reason = err.message ?? String(err);
6615
+ log11.error({ err: reason }, "[tunnel] OpenACP tunnel failed \u2014 falling back to Cloudflare quick tunnel");
6615
6616
  try {
6616
6617
  const fallbackEntry = await this.registry.add(apiPort, {
6617
6618
  type: "system",
6618
6619
  provider: "cloudflare",
6619
6620
  label: "system"
6620
6621
  });
6621
- this.startError = "OpenACP tunnel unavailable \u2014 using Cloudflare quick tunnel";
6622
- return fallbackEntry.publicUrl || `http://localhost:${apiPort}`;
6622
+ const fallbackUrl = fallbackEntry.publicUrl || `http://localhost:${apiPort}`;
6623
+ log11.warn({ url: fallbackUrl, reason }, "[tunnel] Cloudflare fallback tunnel active");
6624
+ this.startError = `OpenACP tunnel unavailable (${reason}) \u2014 using Cloudflare quick tunnel`;
6625
+ return fallbackUrl;
6623
6626
  } catch (fallbackErr) {
6627
+ log11.error({ err: fallbackErr.message }, "[tunnel] Cloudflare fallback also failed \u2014 no public URL");
6624
6628
  this.startError = fallbackErr.message;
6625
6629
  return `http://localhost:${apiPort}`;
6626
6630
  }
@@ -7814,7 +7818,9 @@ var init_sse_manager = __esm({
7814
7818
  "session:updated",
7815
7819
  "session:deleted",
7816
7820
  "agent:event",
7817
- "permission:request"
7821
+ "permission:request",
7822
+ "message:queued",
7823
+ "message:processing"
7818
7824
  ];
7819
7825
  for (const eventName of events) {
7820
7826
  const handler = (data) => {
@@ -7879,7 +7885,9 @@ data: ${JSON.stringify(data)}
7879
7885
  const sessionEvents = [
7880
7886
  "agent:event",
7881
7887
  "permission:request",
7882
- "session:updated"
7888
+ "session:updated",
7889
+ "message:queued",
7890
+ "message:processing"
7883
7891
  ];
7884
7892
  for (const res of this.sseConnections) {
7885
7893
  const filter = res.sessionFilter;
@@ -8068,7 +8076,10 @@ var init_sessions = __esm({
8068
8076
  });
8069
8077
  PromptBodySchema = z.object({
8070
8078
  // 100 KB limit — prevents memory exhaustion / DoS via enormous payloads
8071
- prompt: z.string().min(1).max(1e5)
8079
+ prompt: z.string().min(1).max(1e5),
8080
+ // Multi-adapter routing fields
8081
+ sourceAdapterId: z.string().optional(),
8082
+ responseAdapterId: z.string().nullable().optional()
8072
8083
  });
8073
8084
  PermissionResponseBodySchema = z.object({
8074
8085
  permissionId: z.string().min(1).max(200),
@@ -8105,22 +8116,23 @@ __export(sessions_exports, {
8105
8116
  });
8106
8117
  async function sessionRoutes(app, deps) {
8107
8118
  app.get("/", { preHandler: requireScopes("sessions:read") }, async () => {
8108
- const sessions = deps.core.sessionManager.listSessions();
8119
+ const summaries = deps.core.sessionManager.listAllSessions();
8109
8120
  return {
8110
- sessions: sessions.map((s) => ({
8121
+ sessions: summaries.map((s) => ({
8111
8122
  id: s.id,
8112
- agent: s.agentName,
8123
+ agent: s.agent,
8113
8124
  status: s.status,
8114
- name: s.name ?? null,
8115
- workspace: s.workingDirectory,
8116
- createdAt: s.createdAt.toISOString(),
8117
- dangerousMode: s.clientOverrides.bypassPermissions ?? false,
8125
+ name: s.name,
8126
+ workspace: s.workspace,
8127
+ channelId: s.channelId,
8128
+ createdAt: s.createdAt,
8129
+ lastActiveAt: s.lastActiveAt,
8130
+ dangerousMode: s.dangerousMode,
8118
8131
  queueDepth: s.queueDepth,
8119
8132
  promptRunning: s.promptRunning,
8120
- lastActiveAt: deps.core.sessionManager.getSessionRecord(s.id)?.lastActiveAt ?? null,
8121
- // ACP state
8122
- configOptions: s.configOptions?.length ? s.configOptions : void 0,
8123
- capabilities: s.agentCapabilities ?? null
8133
+ configOptions: s.configOptions,
8134
+ capabilities: s.capabilities,
8135
+ isLive: s.isLive
8124
8136
  }))
8125
8137
  };
8126
8138
  });
@@ -8232,7 +8244,7 @@ async function sessionRoutes(app, deps) {
8232
8244
  async (request, reply) => {
8233
8245
  const { sessionId: rawId } = SessionIdParamSchema.parse(request.params);
8234
8246
  const sessionId = decodeURIComponent(rawId);
8235
- const session = deps.core.sessionManager.getSession(sessionId);
8247
+ const session = await deps.core.getOrResumeSessionById(sessionId);
8236
8248
  if (!session) {
8237
8249
  throw new NotFoundError(
8238
8250
  "SESSION_NOT_FOUND",
@@ -8243,7 +8255,10 @@ async function sessionRoutes(app, deps) {
8243
8255
  return reply.status(400).send({ error: `Session is ${session.status}` });
8244
8256
  }
8245
8257
  const body = PromptBodySchema.parse(request.body);
8246
- await session.enqueuePrompt(body.prompt);
8258
+ await session.enqueuePrompt(body.prompt, void 0, {
8259
+ sourceAdapterId: body.sourceAdapterId ?? "api",
8260
+ responseAdapterId: body.responseAdapterId
8261
+ });
8247
8262
  return {
8248
8263
  ok: true,
8249
8264
  sessionId,
@@ -8290,6 +8305,9 @@ async function sessionRoutes(app, deps) {
8290
8305
  const body = UpdateSessionBodySchema.parse(request.body);
8291
8306
  const changes = {};
8292
8307
  if (body.agentName !== void 0) {
8308
+ if (session.promptRunning) {
8309
+ await session.abortPrompt();
8310
+ }
8293
8311
  const result = await deps.core.switchSessionAgent(sessionId, body.agentName);
8294
8312
  changes.agentName = body.agentName;
8295
8313
  changes.resumed = result.resumed;
@@ -8425,6 +8443,36 @@ async function sessionRoutes(app, deps) {
8425
8443
  }
8426
8444
  }
8427
8445
  );
8446
+ app.post(
8447
+ "/:sessionId/attach",
8448
+ { preHandler: requireScopes("sessions:write") },
8449
+ async (request, reply) => {
8450
+ const { sessionId } = request.params;
8451
+ const { adapterId } = request.body ?? {};
8452
+ if (!adapterId) return reply.code(400).send({ error: "adapterId is required" });
8453
+ try {
8454
+ const result = await deps.core.attachAdapter(sessionId, adapterId);
8455
+ return { ok: true, threadId: result.threadId };
8456
+ } catch (err) {
8457
+ return reply.code(400).send({ error: err.message });
8458
+ }
8459
+ }
8460
+ );
8461
+ app.post(
8462
+ "/:sessionId/detach",
8463
+ { preHandler: requireScopes("sessions:write") },
8464
+ async (request, reply) => {
8465
+ const { sessionId } = request.params;
8466
+ const { adapterId } = request.body ?? {};
8467
+ if (!adapterId) return reply.code(400).send({ error: "adapterId is required" });
8468
+ try {
8469
+ await deps.core.detachAdapter(sessionId, adapterId);
8470
+ return { ok: true };
8471
+ } catch (err) {
8472
+ return reply.code(400).send({ error: err.message });
8473
+ }
8474
+ }
8475
+ );
8428
8476
  app.get(
8429
8477
  "/:sessionId/history",
8430
8478
  { preHandler: requireScopes("sessions:read") },
@@ -10603,7 +10651,7 @@ async function sseRoutes(app, deps) {
10603
10651
  return reply.status(400).send({ error: `Session is ${session.status}` });
10604
10652
  }
10605
10653
  const body = PromptBodySchema.parse(request.body);
10606
- await session.enqueuePrompt(body.prompt);
10654
+ await session.enqueuePrompt(body.prompt, void 0, { sourceAdapterId: "sse" });
10607
10655
  return { ok: true, sessionId, queueDepth: session.queueDepth };
10608
10656
  }
10609
10657
  );
@@ -18855,6 +18903,7 @@ var init_agent_instance = __esm({
18855
18903
  { sessionId: this.sessionId, exitCode: code, signal },
18856
18904
  "Agent process exited"
18857
18905
  );
18906
+ if (signal === "SIGINT" || signal === "SIGTERM") return;
18858
18907
  if (code !== 0 && code !== null || signal) {
18859
18908
  const stderr = this.stderrCapture.getLastLines();
18860
18909
  this.emit("agent_event", {
@@ -19351,21 +19400,21 @@ var init_prompt_queue = __esm({
19351
19400
  queue = [];
19352
19401
  processing = false;
19353
19402
  abortController = null;
19354
- async enqueue(text6, attachments) {
19403
+ async enqueue(text6, attachments, routing, turnId) {
19355
19404
  if (this.processing) {
19356
19405
  return new Promise((resolve8) => {
19357
- this.queue.push({ text: text6, attachments, resolve: resolve8 });
19406
+ this.queue.push({ text: text6, attachments, routing, turnId, resolve: resolve8 });
19358
19407
  });
19359
19408
  }
19360
- await this.process(text6, attachments);
19409
+ await this.process(text6, attachments, routing, turnId);
19361
19410
  }
19362
- async process(text6, attachments) {
19411
+ async process(text6, attachments, routing, turnId) {
19363
19412
  this.processing = true;
19364
19413
  this.abortController = new AbortController();
19365
19414
  const { signal } = this.abortController;
19366
19415
  try {
19367
19416
  await Promise.race([
19368
- this.processor(text6, attachments),
19417
+ this.processor(text6, attachments, routing, turnId),
19369
19418
  new Promise((_, reject) => {
19370
19419
  signal.addEventListener("abort", () => reject(new Error("Prompt aborted")), { once: true });
19371
19420
  })
@@ -19383,7 +19432,7 @@ var init_prompt_queue = __esm({
19383
19432
  drainNext() {
19384
19433
  const next = this.queue.shift();
19385
19434
  if (next) {
19386
- this.process(next.text, next.attachments).then(next.resolve);
19435
+ this.process(next.text, next.attachments, next.routing, next.turnId).then(next.resolve);
19387
19436
  }
19388
19437
  }
19389
19438
  clear() {
@@ -19478,8 +19527,39 @@ var init_permission_gate = __esm({
19478
19527
  }
19479
19528
  });
19480
19529
 
19481
- // src/core/sessions/session.ts
19530
+ // src/core/sessions/turn-context.ts
19482
19531
  import { nanoid as nanoid3 } from "nanoid";
19532
+ function createTurnContext(sourceAdapterId, responseAdapterId, turnId) {
19533
+ return {
19534
+ turnId: turnId ?? nanoid3(8),
19535
+ sourceAdapterId,
19536
+ responseAdapterId
19537
+ };
19538
+ }
19539
+ function getEffectiveTarget(ctx) {
19540
+ if (ctx.responseAdapterId === null) return null;
19541
+ return ctx.responseAdapterId ?? ctx.sourceAdapterId;
19542
+ }
19543
+ function isSystemEvent(event) {
19544
+ return SYSTEM_EVENT_TYPES.has(event.type);
19545
+ }
19546
+ var SYSTEM_EVENT_TYPES;
19547
+ var init_turn_context = __esm({
19548
+ "src/core/sessions/turn-context.ts"() {
19549
+ "use strict";
19550
+ SYSTEM_EVENT_TYPES = /* @__PURE__ */ new Set([
19551
+ "session_end",
19552
+ "system_message",
19553
+ "session_info_update",
19554
+ "config_option_update",
19555
+ "commands_update",
19556
+ "tts_strip"
19557
+ ]);
19558
+ }
19559
+ });
19560
+
19561
+ // src/core/sessions/session.ts
19562
+ import { nanoid as nanoid4 } from "nanoid";
19483
19563
  import * as fs41 from "fs";
19484
19564
  var moduleLog, TTS_PROMPT_INSTRUCTION, TTS_BLOCK_REGEX, TTS_MAX_LENGTH, TTS_TIMEOUT_MS, VALID_TRANSITIONS, Session;
19485
19565
  var init_session2 = __esm({
@@ -19489,6 +19569,7 @@ var init_session2 = __esm({
19489
19569
  init_prompt_queue();
19490
19570
  init_permission_gate();
19491
19571
  init_log();
19572
+ init_turn_context();
19492
19573
  moduleLog = createChildLogger({ module: "session" });
19493
19574
  TTS_PROMPT_INSTRUCTION = `
19494
19575
 
@@ -19506,7 +19587,15 @@ Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of y
19506
19587
  Session = class extends TypedEmitter {
19507
19588
  id;
19508
19589
  channelId;
19509
- threadId = "";
19590
+ /** @deprecated Use threadIds map directly. Getter returns primary adapter's threadId. */
19591
+ get threadId() {
19592
+ return this.threadIds.get(this.channelId) ?? "";
19593
+ }
19594
+ set threadId(value) {
19595
+ if (value) {
19596
+ this.threadIds.set(this.channelId, value);
19597
+ }
19598
+ }
19510
19599
  agentName;
19511
19600
  workingDirectory;
19512
19601
  agentInstance;
@@ -19527,14 +19616,21 @@ Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of y
19527
19616
  middlewareChain;
19528
19617
  /** Latest commands emitted by the agent — buffered before bridge connects so they're not lost */
19529
19618
  latestCommands = null;
19619
+ /** Adapters currently attached to this session (including primary) */
19620
+ attachedAdapters = [];
19621
+ /** Per-adapter thread IDs: adapterId → threadId */
19622
+ threadIds = /* @__PURE__ */ new Map();
19623
+ /** Active turn context — sealed on prompt dequeue, cleared on turn end */
19624
+ activeTurnContext = null;
19530
19625
  permissionGate = new PermissionGate();
19531
19626
  queue;
19532
19627
  speechService;
19533
19628
  pendingContext = null;
19534
19629
  constructor(opts) {
19535
19630
  super();
19536
- this.id = opts.id || nanoid3(12);
19631
+ this.id = opts.id || nanoid4(12);
19537
19632
  this.channelId = opts.channelId;
19633
+ this.attachedAdapters = [opts.channelId];
19538
19634
  this.agentName = opts.agentName;
19539
19635
  this.firstAgent = opts.agentName;
19540
19636
  this.workingDirectory = opts.workingDirectory;
@@ -19544,7 +19640,7 @@ Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of y
19544
19640
  this.log = createSessionLogger(this.id, moduleLog);
19545
19641
  this.log.info({ agentName: this.agentName }, "Session created");
19546
19642
  this.queue = new PromptQueue(
19547
- (text6, attachments) => this.processPrompt(text6, attachments),
19643
+ (text6, attachments, routing, turnId) => this.processPrompt(text6, attachments, routing, turnId),
19548
19644
  (err) => {
19549
19645
  this.log.error({ err }, "Prompt execution failed");
19550
19646
  const message = err instanceof Error ? err.message : String(err);
@@ -19552,11 +19648,20 @@ Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of y
19552
19648
  this.emit("agent_event", { type: "error", message: `Prompt execution failed: ${message}` });
19553
19649
  }
19554
19650
  );
19555
- this.agentInstance.on("agent_event", (event) => {
19651
+ this.wireCommandsBuffer();
19652
+ }
19653
+ /** Wire a listener on the current agentInstance to buffer commands_update events.
19654
+ * Must be called after every agentInstance replacement (constructor + switchAgent). */
19655
+ commandsBufferCleanup;
19656
+ wireCommandsBuffer() {
19657
+ this.commandsBufferCleanup?.();
19658
+ const handler = (event) => {
19556
19659
  if (event.type === "commands_update") {
19557
19660
  this.latestCommands = event.commands;
19558
19661
  }
19559
- });
19662
+ };
19663
+ this.agentInstance.on("agent_event", handler);
19664
+ this.commandsBufferCleanup = () => this.agentInstance.off("agent_event", handler);
19560
19665
  }
19561
19666
  // --- State Machine ---
19562
19667
  get status() {
@@ -19610,18 +19715,26 @@ Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of y
19610
19715
  this.log.info({ voiceMode: mode }, "TTS mode changed");
19611
19716
  }
19612
19717
  // --- Public API ---
19613
- async enqueuePrompt(text6, attachments) {
19718
+ async enqueuePrompt(text6, attachments, routing, externalTurnId) {
19719
+ const turnId = externalTurnId ?? nanoid4(8);
19614
19720
  if (this.middlewareChain) {
19615
19721
  const payload = { text: text6, attachments, sessionId: this.id };
19616
19722
  const result = await this.middlewareChain.execute("agent:beforePrompt", payload, async (p2) => p2);
19617
- if (!result) return;
19723
+ if (!result) return turnId;
19618
19724
  text6 = result.text;
19619
19725
  attachments = result.attachments;
19620
19726
  }
19621
- await this.queue.enqueue(text6, attachments);
19727
+ await this.queue.enqueue(text6, attachments, routing, turnId);
19728
+ return turnId;
19622
19729
  }
19623
- async processPrompt(text6, attachments) {
19730
+ async processPrompt(text6, attachments, routing, turnId) {
19624
19731
  if (this._status === "finished") return;
19732
+ this.activeTurnContext = createTurnContext(
19733
+ routing?.sourceAdapterId ?? this.channelId,
19734
+ routing?.responseAdapterId,
19735
+ turnId
19736
+ );
19737
+ this.emit("turn_started", this.activeTurnContext);
19625
19738
  this.promptCount++;
19626
19739
  this.emit("prompt_count_changed", this.promptCount);
19627
19740
  if (this._status === "initializing" || this._status === "cancelled" || this._status === "error") {
@@ -19688,6 +19801,7 @@ ${text6}`;
19688
19801
  this.log.warn({ err }, "TTS post-processing failed");
19689
19802
  });
19690
19803
  }
19804
+ this.activeTurnContext = null;
19691
19805
  if (!this.name) {
19692
19806
  await this.autoName();
19693
19807
  }
@@ -19930,6 +20044,7 @@ ${result.text}` : result.text;
19930
20044
  this.configOptions = [];
19931
20045
  this.latestCommands = null;
19932
20046
  this.applySpawnResponse(newAgent.initialSessionResponse, newAgent.agentCapabilities);
20047
+ this.wireCommandsBuffer();
19933
20048
  this.log.info({ from: this.agentSwitchHistory.at(-1).agentName, to: agentName }, "Agent switched");
19934
20049
  }
19935
20050
  async destroy() {
@@ -19994,6 +20109,8 @@ var init_session_manager = __esm({
19994
20109
  }
19995
20110
  getSessionByThread(channelId, threadId) {
19996
20111
  for (const session of this.sessions.values()) {
20112
+ const adapterThread = session.threadIds.get(channelId);
20113
+ if (adapterThread === threadId) return session;
19997
20114
  if (session.channelId === channelId && session.threadId === threadId) {
19998
20115
  return session;
19999
20116
  }
@@ -20061,6 +20178,67 @@ var init_session_manager = __esm({
20061
20178
  if (channelId) return all.filter((s) => s.channelId === channelId);
20062
20179
  return all;
20063
20180
  }
20181
+ listAllSessions(channelId) {
20182
+ if (this.store) {
20183
+ let records = this.store.list();
20184
+ if (channelId) records = records.filter((r) => r.channelId === channelId);
20185
+ return records.map((record) => {
20186
+ const live2 = this.sessions.get(record.sessionId);
20187
+ if (live2) {
20188
+ return {
20189
+ id: live2.id,
20190
+ agent: live2.agentName,
20191
+ status: live2.status,
20192
+ name: live2.name ?? null,
20193
+ workspace: live2.workingDirectory,
20194
+ channelId: live2.channelId,
20195
+ createdAt: live2.createdAt.toISOString(),
20196
+ lastActiveAt: record.lastActiveAt ?? null,
20197
+ dangerousMode: live2.clientOverrides.bypassPermissions ?? false,
20198
+ queueDepth: live2.queueDepth,
20199
+ promptRunning: live2.promptRunning,
20200
+ configOptions: live2.configOptions?.length ? live2.configOptions : void 0,
20201
+ capabilities: live2.agentCapabilities ?? null,
20202
+ isLive: true
20203
+ };
20204
+ }
20205
+ return {
20206
+ id: record.sessionId,
20207
+ agent: record.agentName,
20208
+ status: record.status,
20209
+ name: record.name ?? null,
20210
+ workspace: record.workingDir,
20211
+ channelId: record.channelId,
20212
+ createdAt: record.createdAt,
20213
+ lastActiveAt: record.lastActiveAt ?? null,
20214
+ dangerousMode: record.clientOverrides?.bypassPermissions ?? false,
20215
+ queueDepth: 0,
20216
+ promptRunning: false,
20217
+ configOptions: record.acpState?.configOptions,
20218
+ capabilities: record.acpState?.agentCapabilities ?? null,
20219
+ isLive: false
20220
+ };
20221
+ });
20222
+ }
20223
+ let live = Array.from(this.sessions.values());
20224
+ if (channelId) live = live.filter((s) => s.channelId === channelId);
20225
+ return live.map((s) => ({
20226
+ id: s.id,
20227
+ agent: s.agentName,
20228
+ status: s.status,
20229
+ name: s.name ?? null,
20230
+ workspace: s.workingDirectory,
20231
+ channelId: s.channelId,
20232
+ createdAt: s.createdAt.toISOString(),
20233
+ lastActiveAt: null,
20234
+ dangerousMode: s.clientOverrides.bypassPermissions ?? false,
20235
+ queueDepth: s.queueDepth,
20236
+ promptRunning: s.promptRunning,
20237
+ configOptions: s.configOptions?.length ? s.configOptions : void 0,
20238
+ capabilities: s.agentCapabilities ?? null,
20239
+ isLive: true
20240
+ }));
20241
+ }
20064
20242
  listRecords(filter) {
20065
20243
  if (!this.store) return [];
20066
20244
  let records = this.store.list();
@@ -20083,7 +20261,14 @@ var init_session_manager = __esm({
20083
20261
  for (const session of this.sessions.values()) {
20084
20262
  const record = this.store.get(session.id);
20085
20263
  if (record) {
20086
- await this.store.save({ ...record, status: "finished" });
20264
+ await this.store.save({
20265
+ ...record,
20266
+ status: "finished",
20267
+ acpState: session.toAcpStateSnapshot(),
20268
+ clientOverrides: session.clientOverrides,
20269
+ currentPromptCount: session.promptCount,
20270
+ agentSwitchHistory: session.agentSwitchHistory
20271
+ });
20087
20272
  }
20088
20273
  }
20089
20274
  this.store.flush();
@@ -20093,6 +20278,8 @@ var init_session_manager = __esm({
20093
20278
  /**
20094
20279
  * Forcefully destroy all sessions (kill agent subprocesses).
20095
20280
  * Use only when sessions must be fully torn down (e.g. archive).
20281
+ * Unlike shutdownAll(), this does NOT snapshot live session state (acpState, etc.)
20282
+ * because destroyed sessions are terminal and will not be resumed.
20096
20283
  */
20097
20284
  async destroyAll() {
20098
20285
  if (this.store) {
@@ -20127,15 +20314,18 @@ var init_session_bridge = __esm({
20127
20314
  "use strict";
20128
20315
  init_log();
20129
20316
  init_bypass_detection();
20317
+ init_turn_context();
20130
20318
  log30 = createChildLogger({ module: "session-bridge" });
20131
20319
  SessionBridge = class {
20132
- constructor(session, adapter, deps) {
20320
+ constructor(session, adapter, deps, adapterId) {
20133
20321
  this.session = session;
20134
20322
  this.adapter = adapter;
20135
20323
  this.deps = deps;
20324
+ this.adapterId = adapterId ?? adapter.name;
20136
20325
  }
20137
20326
  connected = false;
20138
20327
  cleanupFns = [];
20328
+ adapterId;
20139
20329
  get tracer() {
20140
20330
  return this.session.agentInstance.debugTracer ?? null;
20141
20331
  }
@@ -20166,6 +20356,15 @@ var init_session_bridge = __esm({
20166
20356
  log30.error({ err, sessionId }, "Error in sendMessage middleware");
20167
20357
  }
20168
20358
  }
20359
+ /** Determine if this bridge should forward the given event based on turn routing. */
20360
+ shouldForward(event) {
20361
+ if (isSystemEvent(event)) return true;
20362
+ const ctx = this.session.activeTurnContext;
20363
+ if (!ctx) return true;
20364
+ const target = getEffectiveTarget(ctx);
20365
+ if (target === null) return false;
20366
+ return this.adapterId === target;
20367
+ }
20169
20368
  connect() {
20170
20369
  if (this.connected) return;
20171
20370
  this.connected = true;
@@ -20173,11 +20372,29 @@ var init_session_bridge = __esm({
20173
20372
  this.session.emit("agent_event", event);
20174
20373
  });
20175
20374
  this.listen(this.session, "agent_event", (event) => {
20176
- this.dispatchAgentEvent(event);
20375
+ if (this.shouldForward(event)) {
20376
+ this.dispatchAgentEvent(event);
20377
+ } else {
20378
+ this.deps.eventBus?.emit("agent:event", { sessionId: this.session.id, event });
20379
+ }
20380
+ });
20381
+ if (!this.session.agentInstance.onPermissionRequest || this.session.agentInstance.onPermissionRequest.__bridgeId === void 0) {
20382
+ const handler = async (request) => {
20383
+ return this.resolvePermission(request);
20384
+ };
20385
+ handler.__bridgeId = this.adapterId;
20386
+ this.session.agentInstance.onPermissionRequest = handler;
20387
+ }
20388
+ this.listen(this.session, "permission_request", async (request) => {
20389
+ const current = this.session.agentInstance.onPermissionRequest;
20390
+ if (current?.__bridgeId === this.adapterId) return;
20391
+ if (!this.session.permissionGate.isPending) return;
20392
+ try {
20393
+ await this.adapter.sendPermissionRequest(this.session.id, request);
20394
+ } catch (err) {
20395
+ log30.error({ err, sessionId: this.session.id, adapterId: this.adapterId }, "Failed to send permission request to adapter");
20396
+ }
20177
20397
  });
20178
- this.session.agentInstance.onPermissionRequest = async (request) => {
20179
- return this.resolvePermission(request);
20180
- };
20181
20398
  this.listen(this.session, "status_change", (from, to) => {
20182
20399
  this.deps.sessionManager.patchRecord(this.session.id, {
20183
20400
  status: to,
@@ -20206,6 +20423,16 @@ var init_session_bridge = __esm({
20206
20423
  this.listen(this.session, "prompt_count_changed", (count) => {
20207
20424
  this.deps.sessionManager.patchRecord(this.session.id, { currentPromptCount: count });
20208
20425
  });
20426
+ this.listen(this.session, "turn_started", (ctx) => {
20427
+ if (ctx.sourceAdapterId !== "sse" && ctx.sourceAdapterId !== "api") {
20428
+ this.deps.eventBus?.emit("message:processing", {
20429
+ sessionId: this.session.id,
20430
+ turnId: ctx.turnId,
20431
+ sourceAdapterId: ctx.sourceAdapterId,
20432
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
20433
+ });
20434
+ }
20435
+ });
20209
20436
  if (this.session.latestCommands !== null) {
20210
20437
  this.session.emit("agent_event", { type: "commands_update", commands: this.session.latestCommands });
20211
20438
  }
@@ -20218,7 +20445,10 @@ var init_session_bridge = __esm({
20218
20445
  this.connected = false;
20219
20446
  this.cleanupFns.forEach((fn) => fn());
20220
20447
  this.cleanupFns = [];
20221
- this.session.agentInstance.onPermissionRequest = async () => "";
20448
+ const current = this.session.agentInstance.onPermissionRequest;
20449
+ if (current?.__bridgeId === this.adapterId) {
20450
+ this.session.agentInstance.onPermissionRequest = async () => "";
20451
+ }
20222
20452
  }
20223
20453
  /** Dispatch an agent event through middleware and to the adapter */
20224
20454
  async dispatchAgentEvent(event) {
@@ -20349,8 +20579,10 @@ var init_session_bridge = __esm({
20349
20579
  this.sendMessage(this.session.id, outgoing);
20350
20580
  break;
20351
20581
  case "config_option_update":
20352
- this.session.updateConfigOptions(event.options);
20353
- this.persistAcpState();
20582
+ this.session.updateConfigOptions(event.options).then(() => {
20583
+ this.persistAcpState();
20584
+ }).catch(() => {
20585
+ });
20354
20586
  outgoing = this.deps.messageTransformer.transform(event);
20355
20587
  this.sendMessage(this.session.id, outgoing);
20356
20588
  break;
@@ -20394,19 +20626,27 @@ var init_session_bridge = __esm({
20394
20626
  return result.autoResolve;
20395
20627
  }
20396
20628
  }
20397
- this.session.emit("permission_request", permReq);
20398
20629
  this.deps.eventBus?.emit("permission:request", {
20399
20630
  sessionId: this.session.id,
20400
20631
  permission: permReq
20401
20632
  });
20402
20633
  const autoDecision = this.checkAutoApprove(permReq);
20403
20634
  if (autoDecision) {
20635
+ this.session.emit("permission_request", permReq);
20404
20636
  this.emitAfterResolve(mw, permReq.id, autoDecision, "system", startTime);
20405
20637
  return autoDecision;
20406
20638
  }
20407
20639
  const promise = this.session.permissionGate.setPending(permReq);
20640
+ this.session.emit("permission_request", permReq);
20408
20641
  await this.adapter.sendPermissionRequest(this.session.id, permReq);
20409
20642
  const optionId = await promise;
20643
+ this.deps.eventBus?.emit("permission:resolved", {
20644
+ sessionId: this.session.id,
20645
+ requestId: permReq.id,
20646
+ decision: optionId,
20647
+ optionId,
20648
+ resolvedBy: this.adapterId
20649
+ });
20410
20650
  this.emitAfterResolve(mw, permReq.id, optionId, "user", startTime);
20411
20651
  return optionId;
20412
20652
  }
@@ -20906,6 +21146,9 @@ var init_session_store = __esm({
20906
21146
  }
20907
21147
  findByPlatform(channelId, predicate) {
20908
21148
  for (const record of this.records.values()) {
21149
+ if (record.platforms?.[channelId]) {
21150
+ if (predicate(record.platforms[channelId])) return record;
21151
+ }
20909
21152
  if (record.channelId === channelId && predicate(record.platform)) {
20910
21153
  return record;
20911
21154
  }
@@ -20972,7 +21215,7 @@ var init_session_store = __esm({
20972
21215
  return;
20973
21216
  }
20974
21217
  for (const [id, record] of Object.entries(raw.sessions)) {
20975
- this.records.set(id, record);
21218
+ this.records.set(id, this.migrateRecord(record));
20976
21219
  }
20977
21220
  log32.debug({ count: this.records.size }, "Loaded session records");
20978
21221
  } catch (err) {
@@ -20983,6 +21226,19 @@ var init_session_store = __esm({
20983
21226
  }
20984
21227
  }
20985
21228
  }
21229
+ /** Migrate old SessionRecord format to new multi-adapter format. */
21230
+ migrateRecord(record) {
21231
+ if (!record.platforms && record.platform && typeof record.platform === "object") {
21232
+ const platformData = record.platform;
21233
+ if (Object.keys(platformData).length > 0) {
21234
+ record.platforms = { [record.channelId]: platformData };
21235
+ }
21236
+ }
21237
+ if (!record.attachedAdapters) {
21238
+ record.attachedAdapters = [record.channelId];
21239
+ }
21240
+ return record;
21241
+ }
20986
21242
  cleanup() {
20987
21243
  const cutoff = Date.now() - this.ttlDays * 24 * 60 * 60 * 1e3;
20988
21244
  let removed = 0;
@@ -21171,6 +21427,65 @@ var init_session_factory = __esm({
21171
21427
  if (session) return session;
21172
21428
  return this.lazyResume(channelId, threadId);
21173
21429
  }
21430
+ async getOrResumeById(sessionId) {
21431
+ const live = this.sessionManager.getSession(sessionId);
21432
+ if (live) return live;
21433
+ if (!this.sessionStore || !this.createFullSession) return null;
21434
+ const record = this.sessionStore.get(sessionId);
21435
+ if (!record) return null;
21436
+ if (record.status === "error" || record.status === "cancelled") return null;
21437
+ const existing = this.resumeLocks.get(sessionId);
21438
+ if (existing) return existing;
21439
+ const resumePromise = (async () => {
21440
+ try {
21441
+ const p2 = record.platform;
21442
+ const existingThreadId = p2?.topicId ? String(p2.topicId) : p2?.threadId;
21443
+ const session = await this.createFullSession({
21444
+ channelId: record.channelId,
21445
+ agentName: record.agentName,
21446
+ workingDirectory: record.workingDir,
21447
+ resumeAgentSessionId: record.agentSessionId,
21448
+ existingSessionId: record.sessionId,
21449
+ initialName: record.name,
21450
+ threadId: existingThreadId
21451
+ });
21452
+ session.activate();
21453
+ if (record.clientOverrides) {
21454
+ session.clientOverrides = record.clientOverrides;
21455
+ } else if (record.dangerousMode) {
21456
+ session.clientOverrides = { bypassPermissions: true };
21457
+ }
21458
+ if (record.firstAgent) session.firstAgent = record.firstAgent;
21459
+ if (record.agentSwitchHistory) session.agentSwitchHistory = record.agentSwitchHistory;
21460
+ if (record.currentPromptCount != null) session.promptCount = record.currentPromptCount;
21461
+ if (record.attachedAdapters) session.attachedAdapters = record.attachedAdapters;
21462
+ if (record.platforms) {
21463
+ for (const [adapterId, platformData] of Object.entries(record.platforms)) {
21464
+ const data = platformData;
21465
+ const tid = adapterId === "telegram" ? String(data.topicId ?? "") : String(data.threadId ?? "");
21466
+ if (tid) session.threadIds.set(adapterId, tid);
21467
+ }
21468
+ }
21469
+ if (record.acpState) {
21470
+ if (record.acpState.configOptions && session.configOptions.length === 0) {
21471
+ session.setInitialConfigOptions(record.acpState.configOptions);
21472
+ }
21473
+ if (record.acpState.agentCapabilities && !session.agentCapabilities) {
21474
+ session.setAgentCapabilities(record.acpState.agentCapabilities);
21475
+ }
21476
+ }
21477
+ log33.info({ sessionId }, "Lazy resume by ID successful");
21478
+ return session;
21479
+ } catch (err) {
21480
+ log33.error({ err, sessionId }, "Lazy resume by ID failed");
21481
+ return null;
21482
+ } finally {
21483
+ this.resumeLocks.delete(sessionId);
21484
+ }
21485
+ })();
21486
+ this.resumeLocks.set(sessionId, resumePromise);
21487
+ return resumePromise;
21488
+ }
21174
21489
  async lazyResume(channelId, threadId) {
21175
21490
  const store = this.sessionStore;
21176
21491
  if (!store || !this.createFullSession) return null;
@@ -21179,7 +21494,7 @@ var init_session_factory = __esm({
21179
21494
  if (existing) return existing;
21180
21495
  const record = store.findByPlatform(
21181
21496
  channelId,
21182
- (p2) => String(p2.topicId) === threadId
21497
+ (p2) => String(p2.topicId) === threadId || String(p2.threadId ?? "") === threadId
21183
21498
  );
21184
21499
  if (!record) {
21185
21500
  log33.debug({ threadId, channelId }, "No session record found for thread");
@@ -21214,11 +21529,21 @@ var init_session_factory = __esm({
21214
21529
  if (record.firstAgent) session.firstAgent = record.firstAgent;
21215
21530
  if (record.agentSwitchHistory) session.agentSwitchHistory = record.agentSwitchHistory;
21216
21531
  if (record.currentPromptCount != null) session.promptCount = record.currentPromptCount;
21532
+ if (record.attachedAdapters) {
21533
+ session.attachedAdapters = record.attachedAdapters;
21534
+ }
21535
+ if (record.platforms) {
21536
+ for (const [adapterId, platformData] of Object.entries(record.platforms)) {
21537
+ const data = platformData;
21538
+ const tid = adapterId === "telegram" ? String(data.topicId ?? "") : String(data.threadId ?? "");
21539
+ if (tid) session.threadIds.set(adapterId, tid);
21540
+ }
21541
+ }
21217
21542
  if (record.acpState) {
21218
- if (record.acpState.configOptions) {
21543
+ if (record.acpState.configOptions && session.configOptions.length === 0) {
21219
21544
  session.setInitialConfigOptions(record.acpState.configOptions);
21220
21545
  }
21221
- if (record.acpState.agentCapabilities) {
21546
+ if (record.acpState.agentCapabilities && !session.agentCapabilities) {
21222
21547
  session.setAgentCapabilities(record.acpState.agentCapabilities);
21223
21548
  }
21224
21549
  }
@@ -21401,8 +21726,15 @@ var init_agent_switch_handler = __esm({
21401
21726
  toAgent,
21402
21727
  status: "starting"
21403
21728
  });
21404
- const bridge = bridges.get(sessionId);
21405
- if (bridge) bridge.disconnect();
21729
+ const sessionBridgeKeys = this.deps.getSessionBridgeKeys(sessionId);
21730
+ const hadBridges = sessionBridgeKeys.length > 0;
21731
+ for (const key of sessionBridgeKeys) {
21732
+ const bridge = bridges.get(key);
21733
+ if (bridge) {
21734
+ bridges.delete(key);
21735
+ bridge.disconnect();
21736
+ }
21737
+ }
21406
21738
  const switchAdapter = adapters.get(session.channelId);
21407
21739
  if (switchAdapter?.sendSkillCommands) {
21408
21740
  await switchAdapter.sendSkillCommands(session.id, []);
@@ -21479,9 +21811,11 @@ var init_agent_switch_handler = __esm({
21479
21811
  session.agentInstance = oldInstance;
21480
21812
  session.agentName = fromAgent;
21481
21813
  session.agentSessionId = oldInstance.sessionId;
21482
- const adapter = adapters.get(session.channelId);
21483
- if (adapter) {
21484
- createBridge(session, adapter).connect();
21814
+ for (const adapterId of session.attachedAdapters) {
21815
+ const adapter = adapters.get(adapterId);
21816
+ if (adapter) {
21817
+ createBridge(session, adapter, adapterId).connect();
21818
+ }
21485
21819
  }
21486
21820
  log34.warn({ sessionId, fromAgent, toAgent, err }, "Agent switch failed, rolled back to previous agent");
21487
21821
  } catch (rollbackErr) {
@@ -21490,10 +21824,14 @@ var init_agent_switch_handler = __esm({
21490
21824
  }
21491
21825
  throw err;
21492
21826
  }
21493
- if (bridge) {
21494
- const adapter = adapters.get(session.channelId);
21495
- if (adapter) {
21496
- createBridge(session, adapter).connect();
21827
+ if (hadBridges) {
21828
+ for (const adapterId of session.attachedAdapters) {
21829
+ const adapter = adapters.get(adapterId);
21830
+ if (adapter) {
21831
+ createBridge(session, adapter, adapterId).connect();
21832
+ } else {
21833
+ log34.warn({ sessionId, adapterId }, "Adapter not available during switch reconnect, skipping bridge");
21834
+ }
21497
21835
  }
21498
21836
  }
21499
21837
  await sessionManager.patchRecord(sessionId, {
@@ -23073,6 +23411,7 @@ var init_core_items = __esm({
23073
23411
  // src/core/core.ts
23074
23412
  import path52 from "path";
23075
23413
  import os23 from "os";
23414
+ import { nanoid as nanoid5 } from "nanoid";
23076
23415
  var log40, OpenACPCore;
23077
23416
  var init_core = __esm({
23078
23417
  "src/core/core.ts"() {
@@ -23105,7 +23444,7 @@ var init_core = __esm({
23105
23444
  sessionManager;
23106
23445
  messageTransformer;
23107
23446
  adapters = /* @__PURE__ */ new Map();
23108
- /** sessionId → SessionBridge — tracks active bridges for disconnect/reconnect during agent switch */
23447
+ /** "adapterId:sessionId" → SessionBridge — tracks active bridges for disconnect/reconnect */
23109
23448
  bridges = /* @__PURE__ */ new Map();
23110
23449
  /** Set by main.ts — triggers graceful shutdown with restart exit code */
23111
23450
  requestRestart = null;
@@ -23201,7 +23540,8 @@ var init_core = __esm({
23201
23540
  eventBus: this.eventBus,
23202
23541
  adapters: this.adapters,
23203
23542
  bridges: this.bridges,
23204
- createBridge: (session, adapter) => this.createBridge(session, adapter),
23543
+ createBridge: (session, adapter, adapterId) => this.createBridge(session, adapter, adapterId),
23544
+ getSessionBridgeKeys: (sessionId) => this.getSessionBridgeKeys(sessionId),
23205
23545
  getMiddlewareChain: () => this.lifecycleManager?.middlewareChain,
23206
23546
  getService: (name) => this.lifecycleManager.serviceRegistry.get(name)
23207
23547
  });
@@ -23381,7 +23721,22 @@ User message:
23381
23721
  ${text6}`;
23382
23722
  }
23383
23723
  }
23384
- await session.enqueuePrompt(text6, message.attachments);
23724
+ const sourceAdapterId = message.routing?.sourceAdapterId ?? message.channelId;
23725
+ if (sourceAdapterId && sourceAdapterId !== "sse" && sourceAdapterId !== "api") {
23726
+ const turnId = nanoid5(8);
23727
+ this.eventBus.emit("message:queued", {
23728
+ sessionId: session.id,
23729
+ turnId,
23730
+ text: text6,
23731
+ sourceAdapterId,
23732
+ attachments: message.attachments,
23733
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
23734
+ queueDepth: session.queueDepth
23735
+ });
23736
+ await session.enqueuePrompt(text6, message.attachments, message.routing, turnId);
23737
+ } else {
23738
+ await session.enqueuePrompt(text6, message.attachments, message.routing);
23739
+ }
23385
23740
  }
23386
23741
  // --- Unified Session Creation Pipeline ---
23387
23742
  async createSession(params) {
@@ -23408,6 +23763,12 @@ ${text6}`;
23408
23763
  platform2.threadId = session.threadId;
23409
23764
  }
23410
23765
  }
23766
+ const platforms = {
23767
+ ...existingRecord?.platforms ?? {}
23768
+ };
23769
+ if (session.threadId) {
23770
+ platforms[params.channelId] = params.channelId === "telegram" ? { topicId: Number(session.threadId) || session.threadId } : { threadId: session.threadId };
23771
+ }
23411
23772
  await this.sessionManager.patchRecord(session.id, {
23412
23773
  sessionId: session.id,
23413
23774
  agentSessionId: session.agentSessionId,
@@ -23419,6 +23780,7 @@ ${text6}`;
23419
23780
  lastActiveAt: (/* @__PURE__ */ new Date()).toISOString(),
23420
23781
  name: session.name,
23421
23782
  platform: platform2,
23783
+ platforms,
23422
23784
  firstAgent: session.firstAgent,
23423
23785
  currentPromptCount: session.promptCount,
23424
23786
  agentSwitchHistory: session.agentSwitchHistory,
@@ -23426,7 +23788,7 @@ ${text6}`;
23426
23788
  acpState: session.toAcpStateSnapshot()
23427
23789
  }, { immediate: true });
23428
23790
  if (adapter) {
23429
- const bridge = this.createBridge(session, adapter);
23791
+ const bridge = this.createBridge(session, adapter, session.channelId);
23430
23792
  bridge.connect();
23431
23793
  adapter.flushPendingSkillCommands?.(session.id).catch((err) => {
23432
23794
  log40.warn({ err, sessionId: session.id }, "Failed to flush pending skill commands");
@@ -23565,9 +23927,14 @@ ${text6}`;
23565
23927
  } else {
23566
23928
  adoptPlatform.threadId = session.threadId;
23567
23929
  }
23930
+ const adoptPlatforms = {};
23931
+ if (session.threadId) {
23932
+ adoptPlatforms[adapterChannelId] = adapterChannelId === "telegram" ? { topicId: Number(session.threadId) || session.threadId } : { threadId: session.threadId };
23933
+ }
23568
23934
  await this.sessionManager.patchRecord(session.id, {
23569
23935
  originalAgentSessionId: agentSessionId,
23570
- platform: adoptPlatform
23936
+ platform: adoptPlatform,
23937
+ platforms: adoptPlatforms
23571
23938
  });
23572
23939
  return {
23573
23940
  ok: true,
@@ -23589,18 +23956,101 @@ ${text6}`;
23589
23956
  async getOrResumeSession(channelId, threadId) {
23590
23957
  return this.sessionFactory.getOrResume(channelId, threadId);
23591
23958
  }
23959
+ async getOrResumeSessionById(sessionId) {
23960
+ return this.sessionFactory.getOrResumeById(sessionId);
23961
+ }
23962
+ async attachAdapter(sessionId, adapterId) {
23963
+ const session = this.sessionManager.getSession(sessionId);
23964
+ if (!session) throw new Error(`Session ${sessionId} not found`);
23965
+ const adapter = this.adapters.get(adapterId);
23966
+ if (!adapter) throw new Error(`Adapter "${adapterId}" not found or not running`);
23967
+ if (session.attachedAdapters.includes(adapterId)) {
23968
+ const existingThread = session.threadIds.get(adapterId) ?? session.id;
23969
+ return { threadId: existingThread };
23970
+ }
23971
+ const threadId = await adapter.createSessionThread(
23972
+ session.id,
23973
+ session.name ?? `Session ${session.id.slice(0, 6)}`
23974
+ );
23975
+ session.threadIds.set(adapterId, threadId);
23976
+ session.attachedAdapters.push(adapterId);
23977
+ const bridge = this.createBridge(session, adapter, adapterId);
23978
+ bridge.connect();
23979
+ await this.sessionManager.patchRecord(session.id, {
23980
+ attachedAdapters: session.attachedAdapters,
23981
+ platforms: this.buildPlatformsFromSession(session)
23982
+ });
23983
+ return { threadId };
23984
+ }
23985
+ async detachAdapter(sessionId, adapterId) {
23986
+ const session = this.sessionManager.getSession(sessionId);
23987
+ if (!session) throw new Error(`Session ${sessionId} not found`);
23988
+ if (adapterId === session.channelId) {
23989
+ throw new Error("Cannot detach primary adapter (channelId)");
23990
+ }
23991
+ if (!session.attachedAdapters.includes(adapterId)) {
23992
+ return;
23993
+ }
23994
+ const adapter = this.adapters.get(adapterId);
23995
+ if (adapter) {
23996
+ try {
23997
+ await adapter.sendMessage(session.id, {
23998
+ type: "system_message",
23999
+ text: "Session detached from this adapter."
24000
+ });
24001
+ } catch {
24002
+ }
24003
+ }
24004
+ const key = this.bridgeKey(adapterId, session.id);
24005
+ const bridge = this.bridges.get(key);
24006
+ if (bridge) {
24007
+ bridge.disconnect();
24008
+ this.bridges.delete(key);
24009
+ }
24010
+ session.attachedAdapters = session.attachedAdapters.filter((a) => a !== adapterId);
24011
+ session.threadIds.delete(adapterId);
24012
+ await this.sessionManager.patchRecord(session.id, {
24013
+ attachedAdapters: session.attachedAdapters,
24014
+ platforms: this.buildPlatformsFromSession(session)
24015
+ });
24016
+ }
24017
+ buildPlatformsFromSession(session) {
24018
+ const platforms = {};
24019
+ for (const [adapterId, threadId] of session.threadIds) {
24020
+ if (adapterId === "telegram") {
24021
+ platforms.telegram = { topicId: Number(threadId) || threadId };
24022
+ } else {
24023
+ platforms[adapterId] = { threadId };
24024
+ }
24025
+ }
24026
+ return platforms;
24027
+ }
23592
24028
  // --- Event Wiring ---
24029
+ /** Composite bridge key: "adapterId:sessionId" */
24030
+ bridgeKey(adapterId, sessionId) {
24031
+ return `${adapterId}:${sessionId}`;
24032
+ }
24033
+ /** Get all bridge keys for a session (regardless of adapter) */
24034
+ getSessionBridgeKeys(sessionId) {
24035
+ const keys = [];
24036
+ for (const key of this.bridges.keys()) {
24037
+ if (key.endsWith(`:${sessionId}`)) keys.push(key);
24038
+ }
24039
+ return keys;
24040
+ }
23593
24041
  /** Connect a session bridge for the given session (used by AssistantManager) */
23594
24042
  connectSessionBridge(session) {
23595
24043
  const adapter = this.adapters.get(session.channelId);
23596
24044
  if (!adapter) return;
23597
- const bridge = this.createBridge(session, adapter);
24045
+ const bridge = this.createBridge(session, adapter, session.channelId);
23598
24046
  bridge.connect();
23599
24047
  }
23600
24048
  /** Create a SessionBridge for the given session and adapter.
23601
- * Disconnects any existing bridge for the same session first. */
23602
- createBridge(session, adapter) {
23603
- const existing = this.bridges.get(session.id);
24049
+ * Disconnects any existing bridge for the same adapter+session first. */
24050
+ createBridge(session, adapter, adapterId) {
24051
+ const id = adapterId ?? adapter.name;
24052
+ const key = this.bridgeKey(id, session.id);
24053
+ const existing = this.bridges.get(key);
23604
24054
  if (existing) {
23605
24055
  existing.disconnect();
23606
24056
  }
@@ -23611,8 +24061,8 @@ ${text6}`;
23611
24061
  eventBus: this.eventBus,
23612
24062
  fileService: this.fileService,
23613
24063
  middlewareChain: this.lifecycleManager?.middlewareChain
23614
- });
23615
- this.bridges.set(session.id, bridge);
24064
+ }, id);
24065
+ this.bridges.set(key, bridge);
23616
24066
  return bridge;
23617
24067
  }
23618
24068
  };
@@ -24257,13 +24707,15 @@ function registerSwitchCommands(registry, _core) {
24257
24707
  return { type: "error", message: "No active session in this topic." };
24258
24708
  }
24259
24709
  if (raw) {
24710
+ const droppedCount = session.queueDepth;
24260
24711
  if (session.promptRunning) {
24261
24712
  await session.abortPrompt();
24262
24713
  }
24263
24714
  try {
24264
24715
  const { resumed } = await core.switchSessionAgent(session.id, raw);
24265
24716
  const status = resumed ? "resumed" : "new session";
24266
- return { type: "text", text: `\u2705 Switched to ${raw} (${status})` };
24717
+ const droppedNote = droppedCount > 0 ? ` (${droppedCount} queued prompt${droppedCount > 1 ? "s" : ""} cleared)` : "";
24718
+ return { type: "text", text: `\u2705 Switched to ${raw} (${status})${droppedNote}` };
24267
24719
  } catch (err) {
24268
24720
  return { type: "error", message: `Failed to switch agent: ${err.message || err}` };
24269
24721
  }
@@ -24356,27 +24808,8 @@ function registerCategoryCommand(registry, core, category, commandName) {
24356
24808
  if (configOption.currentValue === raw) {
24357
24809
  return { type: "text", text: `Already using **${match.name}**.` };
24358
24810
  }
24359
- if (session.middlewareChain) {
24360
- const result = await session.middlewareChain.execute("config:beforeChange", {
24361
- sessionId: session.id,
24362
- configId: configOption.id,
24363
- oldValue: configOption.currentValue,
24364
- newValue: raw
24365
- }, async (p2) => p2);
24366
- if (!result) return { type: "error", message: `This change was blocked by a plugin.` };
24367
- }
24368
24811
  try {
24369
- const response = await session.agentInstance.setConfigOption(
24370
- configOption.id,
24371
- { type: "select", value: raw }
24372
- );
24373
- if (response.configOptions && response.configOptions.length > 0) {
24374
- session.configOptions = response.configOptions;
24375
- } else {
24376
- session.configOptions = session.configOptions.map(
24377
- (o) => o.id === configOption.id && o.type === "select" ? { ...o, currentValue: raw } : o
24378
- );
24379
- }
24812
+ await session.setConfigOption(configOption.id, { type: "select", value: raw });
24380
24813
  core.eventBus.emit("session:configChanged", { sessionId: session.id });
24381
24814
  return { type: "text", text: labels.successMsg(match.name, configOption.name) };
24382
24815
  } catch (err) {
@@ -24438,17 +24871,7 @@ function registerDangerousCommand(registry, core) {
24438
24871
  if (bypassValue && modeConfig) {
24439
24872
  try {
24440
24873
  const targetValue = wantOn ? bypassValue : nonBypassDefault;
24441
- const response = await session.agentInstance.setConfigOption(
24442
- modeConfig.id,
24443
- { type: "select", value: targetValue }
24444
- );
24445
- if (response.configOptions && response.configOptions.length > 0) {
24446
- session.configOptions = response.configOptions;
24447
- } else {
24448
- session.configOptions = session.configOptions.map(
24449
- (o) => o.id === modeConfig.id && o.type === "select" ? { ...o, currentValue: targetValue } : o
24450
- );
24451
- }
24874
+ await session.setConfigOption(modeConfig.id, { type: "select", value: targetValue });
24452
24875
  core.eventBus.emit("session:configChanged", { sessionId: session.id });
24453
24876
  return {
24454
24877
  type: "text",
@@ -26438,7 +26861,10 @@ async function startServer(opts) {
26438
26861
  const tunnelErr = tunnelSvc.getStartError();
26439
26862
  const url = tunnelSvc.getPublicUrl();
26440
26863
  const isPublic = url && !url.startsWith("http://localhost") && !url.startsWith("http://127.0.0.1");
26441
- if (tunnelErr) {
26864
+ if (tunnelErr && isPublic) {
26865
+ warn3(`Primary tunnel failed \u2014 using fallback: ${tunnelErr}`);
26866
+ tunnelUrl = url;
26867
+ } else if (tunnelErr) {
26442
26868
  warn3(`Tunnel failed (${tunnelErr}) \u2014 retrying in background`);
26443
26869
  } else if (isPublic) {
26444
26870
  ok3("Tunnel ready");