@openacp/cli 2026.403.8 → 2026.404.1

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
  }
@@ -8068,7 +8072,10 @@ var init_sessions = __esm({
8068
8072
  });
8069
8073
  PromptBodySchema = z.object({
8070
8074
  // 100 KB limit — prevents memory exhaustion / DoS via enormous payloads
8071
- prompt: z.string().min(1).max(1e5)
8075
+ prompt: z.string().min(1).max(1e5),
8076
+ // Multi-adapter routing fields
8077
+ sourceAdapterId: z.string().optional(),
8078
+ responseAdapterId: z.string().nullable().optional()
8072
8079
  });
8073
8080
  PermissionResponseBodySchema = z.object({
8074
8081
  permissionId: z.string().min(1).max(200),
@@ -8105,22 +8112,23 @@ __export(sessions_exports, {
8105
8112
  });
8106
8113
  async function sessionRoutes(app, deps) {
8107
8114
  app.get("/", { preHandler: requireScopes("sessions:read") }, async () => {
8108
- const sessions = deps.core.sessionManager.listSessions();
8115
+ const summaries = deps.core.sessionManager.listAllSessions();
8109
8116
  return {
8110
- sessions: sessions.map((s) => ({
8117
+ sessions: summaries.map((s) => ({
8111
8118
  id: s.id,
8112
- agent: s.agentName,
8119
+ agent: s.agent,
8113
8120
  status: s.status,
8114
- name: s.name ?? null,
8115
- workspace: s.workingDirectory,
8116
- createdAt: s.createdAt.toISOString(),
8117
- dangerousMode: s.clientOverrides.bypassPermissions ?? false,
8121
+ name: s.name,
8122
+ workspace: s.workspace,
8123
+ channelId: s.channelId,
8124
+ createdAt: s.createdAt,
8125
+ lastActiveAt: s.lastActiveAt,
8126
+ dangerousMode: s.dangerousMode,
8118
8127
  queueDepth: s.queueDepth,
8119
8128
  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
8129
+ configOptions: s.configOptions,
8130
+ capabilities: s.capabilities,
8131
+ isLive: s.isLive
8124
8132
  }))
8125
8133
  };
8126
8134
  });
@@ -8232,7 +8240,7 @@ async function sessionRoutes(app, deps) {
8232
8240
  async (request, reply) => {
8233
8241
  const { sessionId: rawId } = SessionIdParamSchema.parse(request.params);
8234
8242
  const sessionId = decodeURIComponent(rawId);
8235
- const session = deps.core.sessionManager.getSession(sessionId);
8243
+ const session = await deps.core.getOrResumeSessionById(sessionId);
8236
8244
  if (!session) {
8237
8245
  throw new NotFoundError(
8238
8246
  "SESSION_NOT_FOUND",
@@ -8243,7 +8251,10 @@ async function sessionRoutes(app, deps) {
8243
8251
  return reply.status(400).send({ error: `Session is ${session.status}` });
8244
8252
  }
8245
8253
  const body = PromptBodySchema.parse(request.body);
8246
- await session.enqueuePrompt(body.prompt);
8254
+ await session.enqueuePrompt(body.prompt, void 0, {
8255
+ sourceAdapterId: body.sourceAdapterId ?? "api",
8256
+ responseAdapterId: body.responseAdapterId
8257
+ });
8247
8258
  return {
8248
8259
  ok: true,
8249
8260
  sessionId,
@@ -8290,6 +8301,9 @@ async function sessionRoutes(app, deps) {
8290
8301
  const body = UpdateSessionBodySchema.parse(request.body);
8291
8302
  const changes = {};
8292
8303
  if (body.agentName !== void 0) {
8304
+ if (session.promptRunning) {
8305
+ await session.abortPrompt();
8306
+ }
8293
8307
  const result = await deps.core.switchSessionAgent(sessionId, body.agentName);
8294
8308
  changes.agentName = body.agentName;
8295
8309
  changes.resumed = result.resumed;
@@ -8425,6 +8439,36 @@ async function sessionRoutes(app, deps) {
8425
8439
  }
8426
8440
  }
8427
8441
  );
8442
+ app.post(
8443
+ "/:sessionId/attach",
8444
+ { preHandler: requireScopes("sessions:write") },
8445
+ async (request, reply) => {
8446
+ const { sessionId } = request.params;
8447
+ const { adapterId } = request.body ?? {};
8448
+ if (!adapterId) return reply.code(400).send({ error: "adapterId is required" });
8449
+ try {
8450
+ const result = await deps.core.attachAdapter(sessionId, adapterId);
8451
+ return { ok: true, threadId: result.threadId };
8452
+ } catch (err) {
8453
+ return reply.code(400).send({ error: err.message });
8454
+ }
8455
+ }
8456
+ );
8457
+ app.post(
8458
+ "/:sessionId/detach",
8459
+ { preHandler: requireScopes("sessions:write") },
8460
+ async (request, reply) => {
8461
+ const { sessionId } = request.params;
8462
+ const { adapterId } = request.body ?? {};
8463
+ if (!adapterId) return reply.code(400).send({ error: "adapterId is required" });
8464
+ try {
8465
+ await deps.core.detachAdapter(sessionId, adapterId);
8466
+ return { ok: true };
8467
+ } catch (err) {
8468
+ return reply.code(400).send({ error: err.message });
8469
+ }
8470
+ }
8471
+ );
8428
8472
  app.get(
8429
8473
  "/:sessionId/history",
8430
8474
  { preHandler: requireScopes("sessions:read") },
@@ -10603,7 +10647,7 @@ async function sseRoutes(app, deps) {
10603
10647
  return reply.status(400).send({ error: `Session is ${session.status}` });
10604
10648
  }
10605
10649
  const body = PromptBodySchema.parse(request.body);
10606
- await session.enqueuePrompt(body.prompt);
10650
+ await session.enqueuePrompt(body.prompt, void 0, { sourceAdapterId: "sse" });
10607
10651
  return { ok: true, sessionId, queueDepth: session.queueDepth };
10608
10652
  }
10609
10653
  );
@@ -18855,6 +18899,7 @@ var init_agent_instance = __esm({
18855
18899
  { sessionId: this.sessionId, exitCode: code, signal },
18856
18900
  "Agent process exited"
18857
18901
  );
18902
+ if (signal === "SIGINT" || signal === "SIGTERM") return;
18858
18903
  if (code !== 0 && code !== null || signal) {
18859
18904
  const stderr = this.stderrCapture.getLastLines();
18860
18905
  this.emit("agent_event", {
@@ -19351,21 +19396,21 @@ var init_prompt_queue = __esm({
19351
19396
  queue = [];
19352
19397
  processing = false;
19353
19398
  abortController = null;
19354
- async enqueue(text6, attachments) {
19399
+ async enqueue(text6, attachments, routing) {
19355
19400
  if (this.processing) {
19356
19401
  return new Promise((resolve8) => {
19357
- this.queue.push({ text: text6, attachments, resolve: resolve8 });
19402
+ this.queue.push({ text: text6, attachments, routing, resolve: resolve8 });
19358
19403
  });
19359
19404
  }
19360
- await this.process(text6, attachments);
19405
+ await this.process(text6, attachments, routing);
19361
19406
  }
19362
- async process(text6, attachments) {
19407
+ async process(text6, attachments, routing) {
19363
19408
  this.processing = true;
19364
19409
  this.abortController = new AbortController();
19365
19410
  const { signal } = this.abortController;
19366
19411
  try {
19367
19412
  await Promise.race([
19368
- this.processor(text6, attachments),
19413
+ this.processor(text6, attachments, routing),
19369
19414
  new Promise((_, reject) => {
19370
19415
  signal.addEventListener("abort", () => reject(new Error("Prompt aborted")), { once: true });
19371
19416
  })
@@ -19383,7 +19428,7 @@ var init_prompt_queue = __esm({
19383
19428
  drainNext() {
19384
19429
  const next = this.queue.shift();
19385
19430
  if (next) {
19386
- this.process(next.text, next.attachments).then(next.resolve);
19431
+ this.process(next.text, next.attachments, next.routing).then(next.resolve);
19387
19432
  }
19388
19433
  }
19389
19434
  clear() {
@@ -19478,8 +19523,39 @@ var init_permission_gate = __esm({
19478
19523
  }
19479
19524
  });
19480
19525
 
19481
- // src/core/sessions/session.ts
19526
+ // src/core/sessions/turn-context.ts
19482
19527
  import { nanoid as nanoid3 } from "nanoid";
19528
+ function createTurnContext(sourceAdapterId, responseAdapterId) {
19529
+ return {
19530
+ turnId: nanoid3(8),
19531
+ sourceAdapterId,
19532
+ responseAdapterId
19533
+ };
19534
+ }
19535
+ function getEffectiveTarget(ctx) {
19536
+ if (ctx.responseAdapterId === null) return null;
19537
+ return ctx.responseAdapterId ?? ctx.sourceAdapterId;
19538
+ }
19539
+ function isSystemEvent(event) {
19540
+ return SYSTEM_EVENT_TYPES.has(event.type);
19541
+ }
19542
+ var SYSTEM_EVENT_TYPES;
19543
+ var init_turn_context = __esm({
19544
+ "src/core/sessions/turn-context.ts"() {
19545
+ "use strict";
19546
+ SYSTEM_EVENT_TYPES = /* @__PURE__ */ new Set([
19547
+ "session_end",
19548
+ "system_message",
19549
+ "session_info_update",
19550
+ "config_option_update",
19551
+ "commands_update",
19552
+ "tts_strip"
19553
+ ]);
19554
+ }
19555
+ });
19556
+
19557
+ // src/core/sessions/session.ts
19558
+ import { nanoid as nanoid4 } from "nanoid";
19483
19559
  import * as fs41 from "fs";
19484
19560
  var moduleLog, TTS_PROMPT_INSTRUCTION, TTS_BLOCK_REGEX, TTS_MAX_LENGTH, TTS_TIMEOUT_MS, VALID_TRANSITIONS, Session;
19485
19561
  var init_session2 = __esm({
@@ -19489,6 +19565,7 @@ var init_session2 = __esm({
19489
19565
  init_prompt_queue();
19490
19566
  init_permission_gate();
19491
19567
  init_log();
19568
+ init_turn_context();
19492
19569
  moduleLog = createChildLogger({ module: "session" });
19493
19570
  TTS_PROMPT_INSTRUCTION = `
19494
19571
 
@@ -19506,7 +19583,15 @@ Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of y
19506
19583
  Session = class extends TypedEmitter {
19507
19584
  id;
19508
19585
  channelId;
19509
- threadId = "";
19586
+ /** @deprecated Use threadIds map directly. Getter returns primary adapter's threadId. */
19587
+ get threadId() {
19588
+ return this.threadIds.get(this.channelId) ?? "";
19589
+ }
19590
+ set threadId(value) {
19591
+ if (value) {
19592
+ this.threadIds.set(this.channelId, value);
19593
+ }
19594
+ }
19510
19595
  agentName;
19511
19596
  workingDirectory;
19512
19597
  agentInstance;
@@ -19527,14 +19612,21 @@ Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of y
19527
19612
  middlewareChain;
19528
19613
  /** Latest commands emitted by the agent — buffered before bridge connects so they're not lost */
19529
19614
  latestCommands = null;
19615
+ /** Adapters currently attached to this session (including primary) */
19616
+ attachedAdapters = [];
19617
+ /** Per-adapter thread IDs: adapterId → threadId */
19618
+ threadIds = /* @__PURE__ */ new Map();
19619
+ /** Active turn context — sealed on prompt dequeue, cleared on turn end */
19620
+ activeTurnContext = null;
19530
19621
  permissionGate = new PermissionGate();
19531
19622
  queue;
19532
19623
  speechService;
19533
19624
  pendingContext = null;
19534
19625
  constructor(opts) {
19535
19626
  super();
19536
- this.id = opts.id || nanoid3(12);
19627
+ this.id = opts.id || nanoid4(12);
19537
19628
  this.channelId = opts.channelId;
19629
+ this.attachedAdapters = [opts.channelId];
19538
19630
  this.agentName = opts.agentName;
19539
19631
  this.firstAgent = opts.agentName;
19540
19632
  this.workingDirectory = opts.workingDirectory;
@@ -19544,7 +19636,7 @@ Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of y
19544
19636
  this.log = createSessionLogger(this.id, moduleLog);
19545
19637
  this.log.info({ agentName: this.agentName }, "Session created");
19546
19638
  this.queue = new PromptQueue(
19547
- (text6, attachments) => this.processPrompt(text6, attachments),
19639
+ (text6, attachments, routing) => this.processPrompt(text6, attachments, routing),
19548
19640
  (err) => {
19549
19641
  this.log.error({ err }, "Prompt execution failed");
19550
19642
  const message = err instanceof Error ? err.message : String(err);
@@ -19552,11 +19644,20 @@ Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of y
19552
19644
  this.emit("agent_event", { type: "error", message: `Prompt execution failed: ${message}` });
19553
19645
  }
19554
19646
  );
19555
- this.agentInstance.on("agent_event", (event) => {
19647
+ this.wireCommandsBuffer();
19648
+ }
19649
+ /** Wire a listener on the current agentInstance to buffer commands_update events.
19650
+ * Must be called after every agentInstance replacement (constructor + switchAgent). */
19651
+ commandsBufferCleanup;
19652
+ wireCommandsBuffer() {
19653
+ this.commandsBufferCleanup?.();
19654
+ const handler = (event) => {
19556
19655
  if (event.type === "commands_update") {
19557
19656
  this.latestCommands = event.commands;
19558
19657
  }
19559
- });
19658
+ };
19659
+ this.agentInstance.on("agent_event", handler);
19660
+ this.commandsBufferCleanup = () => this.agentInstance.off("agent_event", handler);
19560
19661
  }
19561
19662
  // --- State Machine ---
19562
19663
  get status() {
@@ -19610,7 +19711,7 @@ Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of y
19610
19711
  this.log.info({ voiceMode: mode }, "TTS mode changed");
19611
19712
  }
19612
19713
  // --- Public API ---
19613
- async enqueuePrompt(text6, attachments) {
19714
+ async enqueuePrompt(text6, attachments, routing) {
19614
19715
  if (this.middlewareChain) {
19615
19716
  const payload = { text: text6, attachments, sessionId: this.id };
19616
19717
  const result = await this.middlewareChain.execute("agent:beforePrompt", payload, async (p2) => p2);
@@ -19618,10 +19719,14 @@ Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of y
19618
19719
  text6 = result.text;
19619
19720
  attachments = result.attachments;
19620
19721
  }
19621
- await this.queue.enqueue(text6, attachments);
19722
+ await this.queue.enqueue(text6, attachments, routing);
19622
19723
  }
19623
- async processPrompt(text6, attachments) {
19724
+ async processPrompt(text6, attachments, routing) {
19624
19725
  if (this._status === "finished") return;
19726
+ this.activeTurnContext = createTurnContext(
19727
+ routing?.sourceAdapterId ?? this.channelId,
19728
+ routing?.responseAdapterId
19729
+ );
19625
19730
  this.promptCount++;
19626
19731
  this.emit("prompt_count_changed", this.promptCount);
19627
19732
  if (this._status === "initializing" || this._status === "cancelled" || this._status === "error") {
@@ -19688,6 +19793,7 @@ ${text6}`;
19688
19793
  this.log.warn({ err }, "TTS post-processing failed");
19689
19794
  });
19690
19795
  }
19796
+ this.activeTurnContext = null;
19691
19797
  if (!this.name) {
19692
19798
  await this.autoName();
19693
19799
  }
@@ -19930,6 +20036,7 @@ ${result.text}` : result.text;
19930
20036
  this.configOptions = [];
19931
20037
  this.latestCommands = null;
19932
20038
  this.applySpawnResponse(newAgent.initialSessionResponse, newAgent.agentCapabilities);
20039
+ this.wireCommandsBuffer();
19933
20040
  this.log.info({ from: this.agentSwitchHistory.at(-1).agentName, to: agentName }, "Agent switched");
19934
20041
  }
19935
20042
  async destroy() {
@@ -19994,6 +20101,8 @@ var init_session_manager = __esm({
19994
20101
  }
19995
20102
  getSessionByThread(channelId, threadId) {
19996
20103
  for (const session of this.sessions.values()) {
20104
+ const adapterThread = session.threadIds.get(channelId);
20105
+ if (adapterThread === threadId) return session;
19997
20106
  if (session.channelId === channelId && session.threadId === threadId) {
19998
20107
  return session;
19999
20108
  }
@@ -20061,6 +20170,67 @@ var init_session_manager = __esm({
20061
20170
  if (channelId) return all.filter((s) => s.channelId === channelId);
20062
20171
  return all;
20063
20172
  }
20173
+ listAllSessions(channelId) {
20174
+ if (this.store) {
20175
+ let records = this.store.list();
20176
+ if (channelId) records = records.filter((r) => r.channelId === channelId);
20177
+ return records.map((record) => {
20178
+ const live2 = this.sessions.get(record.sessionId);
20179
+ if (live2) {
20180
+ return {
20181
+ id: live2.id,
20182
+ agent: live2.agentName,
20183
+ status: live2.status,
20184
+ name: live2.name ?? null,
20185
+ workspace: live2.workingDirectory,
20186
+ channelId: live2.channelId,
20187
+ createdAt: live2.createdAt.toISOString(),
20188
+ lastActiveAt: record.lastActiveAt ?? null,
20189
+ dangerousMode: live2.clientOverrides.bypassPermissions ?? false,
20190
+ queueDepth: live2.queueDepth,
20191
+ promptRunning: live2.promptRunning,
20192
+ configOptions: live2.configOptions?.length ? live2.configOptions : void 0,
20193
+ capabilities: live2.agentCapabilities ?? null,
20194
+ isLive: true
20195
+ };
20196
+ }
20197
+ return {
20198
+ id: record.sessionId,
20199
+ agent: record.agentName,
20200
+ status: record.status,
20201
+ name: record.name ?? null,
20202
+ workspace: record.workingDir,
20203
+ channelId: record.channelId,
20204
+ createdAt: record.createdAt,
20205
+ lastActiveAt: record.lastActiveAt ?? null,
20206
+ dangerousMode: record.clientOverrides?.bypassPermissions ?? false,
20207
+ queueDepth: 0,
20208
+ promptRunning: false,
20209
+ configOptions: record.acpState?.configOptions,
20210
+ capabilities: record.acpState?.agentCapabilities ?? null,
20211
+ isLive: false
20212
+ };
20213
+ });
20214
+ }
20215
+ let live = Array.from(this.sessions.values());
20216
+ if (channelId) live = live.filter((s) => s.channelId === channelId);
20217
+ return live.map((s) => ({
20218
+ id: s.id,
20219
+ agent: s.agentName,
20220
+ status: s.status,
20221
+ name: s.name ?? null,
20222
+ workspace: s.workingDirectory,
20223
+ channelId: s.channelId,
20224
+ createdAt: s.createdAt.toISOString(),
20225
+ lastActiveAt: null,
20226
+ dangerousMode: s.clientOverrides.bypassPermissions ?? false,
20227
+ queueDepth: s.queueDepth,
20228
+ promptRunning: s.promptRunning,
20229
+ configOptions: s.configOptions?.length ? s.configOptions : void 0,
20230
+ capabilities: s.agentCapabilities ?? null,
20231
+ isLive: true
20232
+ }));
20233
+ }
20064
20234
  listRecords(filter) {
20065
20235
  if (!this.store) return [];
20066
20236
  let records = this.store.list();
@@ -20083,7 +20253,14 @@ var init_session_manager = __esm({
20083
20253
  for (const session of this.sessions.values()) {
20084
20254
  const record = this.store.get(session.id);
20085
20255
  if (record) {
20086
- await this.store.save({ ...record, status: "finished" });
20256
+ await this.store.save({
20257
+ ...record,
20258
+ status: "finished",
20259
+ acpState: session.toAcpStateSnapshot(),
20260
+ clientOverrides: session.clientOverrides,
20261
+ currentPromptCount: session.promptCount,
20262
+ agentSwitchHistory: session.agentSwitchHistory
20263
+ });
20087
20264
  }
20088
20265
  }
20089
20266
  this.store.flush();
@@ -20093,6 +20270,8 @@ var init_session_manager = __esm({
20093
20270
  /**
20094
20271
  * Forcefully destroy all sessions (kill agent subprocesses).
20095
20272
  * Use only when sessions must be fully torn down (e.g. archive).
20273
+ * Unlike shutdownAll(), this does NOT snapshot live session state (acpState, etc.)
20274
+ * because destroyed sessions are terminal and will not be resumed.
20096
20275
  */
20097
20276
  async destroyAll() {
20098
20277
  if (this.store) {
@@ -20127,15 +20306,18 @@ var init_session_bridge = __esm({
20127
20306
  "use strict";
20128
20307
  init_log();
20129
20308
  init_bypass_detection();
20309
+ init_turn_context();
20130
20310
  log30 = createChildLogger({ module: "session-bridge" });
20131
20311
  SessionBridge = class {
20132
- constructor(session, adapter, deps) {
20312
+ constructor(session, adapter, deps, adapterId) {
20133
20313
  this.session = session;
20134
20314
  this.adapter = adapter;
20135
20315
  this.deps = deps;
20316
+ this.adapterId = adapterId ?? adapter.name;
20136
20317
  }
20137
20318
  connected = false;
20138
20319
  cleanupFns = [];
20320
+ adapterId;
20139
20321
  get tracer() {
20140
20322
  return this.session.agentInstance.debugTracer ?? null;
20141
20323
  }
@@ -20166,6 +20348,15 @@ var init_session_bridge = __esm({
20166
20348
  log30.error({ err, sessionId }, "Error in sendMessage middleware");
20167
20349
  }
20168
20350
  }
20351
+ /** Determine if this bridge should forward the given event based on turn routing. */
20352
+ shouldForward(event) {
20353
+ if (isSystemEvent(event)) return true;
20354
+ const ctx = this.session.activeTurnContext;
20355
+ if (!ctx) return true;
20356
+ const target = getEffectiveTarget(ctx);
20357
+ if (target === null) return false;
20358
+ return this.adapterId === target;
20359
+ }
20169
20360
  connect() {
20170
20361
  if (this.connected) return;
20171
20362
  this.connected = true;
@@ -20173,11 +20364,29 @@ var init_session_bridge = __esm({
20173
20364
  this.session.emit("agent_event", event);
20174
20365
  });
20175
20366
  this.listen(this.session, "agent_event", (event) => {
20176
- this.dispatchAgentEvent(event);
20367
+ if (this.shouldForward(event)) {
20368
+ this.dispatchAgentEvent(event);
20369
+ } else {
20370
+ this.deps.eventBus?.emit("agent:event", { sessionId: this.session.id, event });
20371
+ }
20372
+ });
20373
+ if (!this.session.agentInstance.onPermissionRequest || this.session.agentInstance.onPermissionRequest.__bridgeId === void 0) {
20374
+ const handler = async (request) => {
20375
+ return this.resolvePermission(request);
20376
+ };
20377
+ handler.__bridgeId = this.adapterId;
20378
+ this.session.agentInstance.onPermissionRequest = handler;
20379
+ }
20380
+ this.listen(this.session, "permission_request", async (request) => {
20381
+ const current = this.session.agentInstance.onPermissionRequest;
20382
+ if (current?.__bridgeId === this.adapterId) return;
20383
+ if (!this.session.permissionGate.isPending) return;
20384
+ try {
20385
+ await this.adapter.sendPermissionRequest(this.session.id, request);
20386
+ } catch (err) {
20387
+ log30.error({ err, sessionId: this.session.id, adapterId: this.adapterId }, "Failed to send permission request to adapter");
20388
+ }
20177
20389
  });
20178
- this.session.agentInstance.onPermissionRequest = async (request) => {
20179
- return this.resolvePermission(request);
20180
- };
20181
20390
  this.listen(this.session, "status_change", (from, to) => {
20182
20391
  this.deps.sessionManager.patchRecord(this.session.id, {
20183
20392
  status: to,
@@ -20218,7 +20427,10 @@ var init_session_bridge = __esm({
20218
20427
  this.connected = false;
20219
20428
  this.cleanupFns.forEach((fn) => fn());
20220
20429
  this.cleanupFns = [];
20221
- this.session.agentInstance.onPermissionRequest = async () => "";
20430
+ const current = this.session.agentInstance.onPermissionRequest;
20431
+ if (current?.__bridgeId === this.adapterId) {
20432
+ this.session.agentInstance.onPermissionRequest = async () => "";
20433
+ }
20222
20434
  }
20223
20435
  /** Dispatch an agent event through middleware and to the adapter */
20224
20436
  async dispatchAgentEvent(event) {
@@ -20349,8 +20561,10 @@ var init_session_bridge = __esm({
20349
20561
  this.sendMessage(this.session.id, outgoing);
20350
20562
  break;
20351
20563
  case "config_option_update":
20352
- this.session.updateConfigOptions(event.options);
20353
- this.persistAcpState();
20564
+ this.session.updateConfigOptions(event.options).then(() => {
20565
+ this.persistAcpState();
20566
+ }).catch(() => {
20567
+ });
20354
20568
  outgoing = this.deps.messageTransformer.transform(event);
20355
20569
  this.sendMessage(this.session.id, outgoing);
20356
20570
  break;
@@ -20394,19 +20608,27 @@ var init_session_bridge = __esm({
20394
20608
  return result.autoResolve;
20395
20609
  }
20396
20610
  }
20397
- this.session.emit("permission_request", permReq);
20398
20611
  this.deps.eventBus?.emit("permission:request", {
20399
20612
  sessionId: this.session.id,
20400
20613
  permission: permReq
20401
20614
  });
20402
20615
  const autoDecision = this.checkAutoApprove(permReq);
20403
20616
  if (autoDecision) {
20617
+ this.session.emit("permission_request", permReq);
20404
20618
  this.emitAfterResolve(mw, permReq.id, autoDecision, "system", startTime);
20405
20619
  return autoDecision;
20406
20620
  }
20407
20621
  const promise = this.session.permissionGate.setPending(permReq);
20622
+ this.session.emit("permission_request", permReq);
20408
20623
  await this.adapter.sendPermissionRequest(this.session.id, permReq);
20409
20624
  const optionId = await promise;
20625
+ this.deps.eventBus?.emit("permission:resolved", {
20626
+ sessionId: this.session.id,
20627
+ requestId: permReq.id,
20628
+ decision: optionId,
20629
+ optionId,
20630
+ resolvedBy: this.adapterId
20631
+ });
20410
20632
  this.emitAfterResolve(mw, permReq.id, optionId, "user", startTime);
20411
20633
  return optionId;
20412
20634
  }
@@ -20906,6 +21128,9 @@ var init_session_store = __esm({
20906
21128
  }
20907
21129
  findByPlatform(channelId, predicate) {
20908
21130
  for (const record of this.records.values()) {
21131
+ if (record.platforms?.[channelId]) {
21132
+ if (predicate(record.platforms[channelId])) return record;
21133
+ }
20909
21134
  if (record.channelId === channelId && predicate(record.platform)) {
20910
21135
  return record;
20911
21136
  }
@@ -20972,7 +21197,7 @@ var init_session_store = __esm({
20972
21197
  return;
20973
21198
  }
20974
21199
  for (const [id, record] of Object.entries(raw.sessions)) {
20975
- this.records.set(id, record);
21200
+ this.records.set(id, this.migrateRecord(record));
20976
21201
  }
20977
21202
  log32.debug({ count: this.records.size }, "Loaded session records");
20978
21203
  } catch (err) {
@@ -20983,6 +21208,19 @@ var init_session_store = __esm({
20983
21208
  }
20984
21209
  }
20985
21210
  }
21211
+ /** Migrate old SessionRecord format to new multi-adapter format. */
21212
+ migrateRecord(record) {
21213
+ if (!record.platforms && record.platform && typeof record.platform === "object") {
21214
+ const platformData = record.platform;
21215
+ if (Object.keys(platformData).length > 0) {
21216
+ record.platforms = { [record.channelId]: platformData };
21217
+ }
21218
+ }
21219
+ if (!record.attachedAdapters) {
21220
+ record.attachedAdapters = [record.channelId];
21221
+ }
21222
+ return record;
21223
+ }
20986
21224
  cleanup() {
20987
21225
  const cutoff = Date.now() - this.ttlDays * 24 * 60 * 60 * 1e3;
20988
21226
  let removed = 0;
@@ -21171,6 +21409,65 @@ var init_session_factory = __esm({
21171
21409
  if (session) return session;
21172
21410
  return this.lazyResume(channelId, threadId);
21173
21411
  }
21412
+ async getOrResumeById(sessionId) {
21413
+ const live = this.sessionManager.getSession(sessionId);
21414
+ if (live) return live;
21415
+ if (!this.sessionStore || !this.createFullSession) return null;
21416
+ const record = this.sessionStore.get(sessionId);
21417
+ if (!record) return null;
21418
+ if (record.status === "error" || record.status === "cancelled") return null;
21419
+ const existing = this.resumeLocks.get(sessionId);
21420
+ if (existing) return existing;
21421
+ const resumePromise = (async () => {
21422
+ try {
21423
+ const p2 = record.platform;
21424
+ const existingThreadId = p2?.topicId ? String(p2.topicId) : p2?.threadId;
21425
+ const session = await this.createFullSession({
21426
+ channelId: record.channelId,
21427
+ agentName: record.agentName,
21428
+ workingDirectory: record.workingDir,
21429
+ resumeAgentSessionId: record.agentSessionId,
21430
+ existingSessionId: record.sessionId,
21431
+ initialName: record.name,
21432
+ threadId: existingThreadId
21433
+ });
21434
+ session.activate();
21435
+ if (record.clientOverrides) {
21436
+ session.clientOverrides = record.clientOverrides;
21437
+ } else if (record.dangerousMode) {
21438
+ session.clientOverrides = { bypassPermissions: true };
21439
+ }
21440
+ if (record.firstAgent) session.firstAgent = record.firstAgent;
21441
+ if (record.agentSwitchHistory) session.agentSwitchHistory = record.agentSwitchHistory;
21442
+ if (record.currentPromptCount != null) session.promptCount = record.currentPromptCount;
21443
+ if (record.attachedAdapters) session.attachedAdapters = record.attachedAdapters;
21444
+ if (record.platforms) {
21445
+ for (const [adapterId, platformData] of Object.entries(record.platforms)) {
21446
+ const data = platformData;
21447
+ const tid = adapterId === "telegram" ? String(data.topicId ?? "") : String(data.threadId ?? "");
21448
+ if (tid) session.threadIds.set(adapterId, tid);
21449
+ }
21450
+ }
21451
+ if (record.acpState) {
21452
+ if (record.acpState.configOptions && session.configOptions.length === 0) {
21453
+ session.setInitialConfigOptions(record.acpState.configOptions);
21454
+ }
21455
+ if (record.acpState.agentCapabilities && !session.agentCapabilities) {
21456
+ session.setAgentCapabilities(record.acpState.agentCapabilities);
21457
+ }
21458
+ }
21459
+ log33.info({ sessionId }, "Lazy resume by ID successful");
21460
+ return session;
21461
+ } catch (err) {
21462
+ log33.error({ err, sessionId }, "Lazy resume by ID failed");
21463
+ return null;
21464
+ } finally {
21465
+ this.resumeLocks.delete(sessionId);
21466
+ }
21467
+ })();
21468
+ this.resumeLocks.set(sessionId, resumePromise);
21469
+ return resumePromise;
21470
+ }
21174
21471
  async lazyResume(channelId, threadId) {
21175
21472
  const store = this.sessionStore;
21176
21473
  if (!store || !this.createFullSession) return null;
@@ -21179,7 +21476,7 @@ var init_session_factory = __esm({
21179
21476
  if (existing) return existing;
21180
21477
  const record = store.findByPlatform(
21181
21478
  channelId,
21182
- (p2) => String(p2.topicId) === threadId
21479
+ (p2) => String(p2.topicId) === threadId || String(p2.threadId ?? "") === threadId
21183
21480
  );
21184
21481
  if (!record) {
21185
21482
  log33.debug({ threadId, channelId }, "No session record found for thread");
@@ -21214,11 +21511,21 @@ var init_session_factory = __esm({
21214
21511
  if (record.firstAgent) session.firstAgent = record.firstAgent;
21215
21512
  if (record.agentSwitchHistory) session.agentSwitchHistory = record.agentSwitchHistory;
21216
21513
  if (record.currentPromptCount != null) session.promptCount = record.currentPromptCount;
21514
+ if (record.attachedAdapters) {
21515
+ session.attachedAdapters = record.attachedAdapters;
21516
+ }
21517
+ if (record.platforms) {
21518
+ for (const [adapterId, platformData] of Object.entries(record.platforms)) {
21519
+ const data = platformData;
21520
+ const tid = adapterId === "telegram" ? String(data.topicId ?? "") : String(data.threadId ?? "");
21521
+ if (tid) session.threadIds.set(adapterId, tid);
21522
+ }
21523
+ }
21217
21524
  if (record.acpState) {
21218
- if (record.acpState.configOptions) {
21525
+ if (record.acpState.configOptions && session.configOptions.length === 0) {
21219
21526
  session.setInitialConfigOptions(record.acpState.configOptions);
21220
21527
  }
21221
- if (record.acpState.agentCapabilities) {
21528
+ if (record.acpState.agentCapabilities && !session.agentCapabilities) {
21222
21529
  session.setAgentCapabilities(record.acpState.agentCapabilities);
21223
21530
  }
21224
21531
  }
@@ -21401,8 +21708,15 @@ var init_agent_switch_handler = __esm({
21401
21708
  toAgent,
21402
21709
  status: "starting"
21403
21710
  });
21404
- const bridge = bridges.get(sessionId);
21405
- if (bridge) bridge.disconnect();
21711
+ const sessionBridgeKeys = this.deps.getSessionBridgeKeys(sessionId);
21712
+ const hadBridges = sessionBridgeKeys.length > 0;
21713
+ for (const key of sessionBridgeKeys) {
21714
+ const bridge = bridges.get(key);
21715
+ if (bridge) {
21716
+ bridges.delete(key);
21717
+ bridge.disconnect();
21718
+ }
21719
+ }
21406
21720
  const switchAdapter = adapters.get(session.channelId);
21407
21721
  if (switchAdapter?.sendSkillCommands) {
21408
21722
  await switchAdapter.sendSkillCommands(session.id, []);
@@ -21479,9 +21793,11 @@ var init_agent_switch_handler = __esm({
21479
21793
  session.agentInstance = oldInstance;
21480
21794
  session.agentName = fromAgent;
21481
21795
  session.agentSessionId = oldInstance.sessionId;
21482
- const adapter = adapters.get(session.channelId);
21483
- if (adapter) {
21484
- createBridge(session, adapter).connect();
21796
+ for (const adapterId of session.attachedAdapters) {
21797
+ const adapter = adapters.get(adapterId);
21798
+ if (adapter) {
21799
+ createBridge(session, adapter, adapterId).connect();
21800
+ }
21485
21801
  }
21486
21802
  log34.warn({ sessionId, fromAgent, toAgent, err }, "Agent switch failed, rolled back to previous agent");
21487
21803
  } catch (rollbackErr) {
@@ -21490,10 +21806,14 @@ var init_agent_switch_handler = __esm({
21490
21806
  }
21491
21807
  throw err;
21492
21808
  }
21493
- if (bridge) {
21494
- const adapter = adapters.get(session.channelId);
21495
- if (adapter) {
21496
- createBridge(session, adapter).connect();
21809
+ if (hadBridges) {
21810
+ for (const adapterId of session.attachedAdapters) {
21811
+ const adapter = adapters.get(adapterId);
21812
+ if (adapter) {
21813
+ createBridge(session, adapter, adapterId).connect();
21814
+ } else {
21815
+ log34.warn({ sessionId, adapterId }, "Adapter not available during switch reconnect, skipping bridge");
21816
+ }
21497
21817
  }
21498
21818
  }
21499
21819
  await sessionManager.patchRecord(sessionId, {
@@ -23105,7 +23425,7 @@ var init_core = __esm({
23105
23425
  sessionManager;
23106
23426
  messageTransformer;
23107
23427
  adapters = /* @__PURE__ */ new Map();
23108
- /** sessionId → SessionBridge — tracks active bridges for disconnect/reconnect during agent switch */
23428
+ /** "adapterId:sessionId" → SessionBridge — tracks active bridges for disconnect/reconnect */
23109
23429
  bridges = /* @__PURE__ */ new Map();
23110
23430
  /** Set by main.ts — triggers graceful shutdown with restart exit code */
23111
23431
  requestRestart = null;
@@ -23201,7 +23521,8 @@ var init_core = __esm({
23201
23521
  eventBus: this.eventBus,
23202
23522
  adapters: this.adapters,
23203
23523
  bridges: this.bridges,
23204
- createBridge: (session, adapter) => this.createBridge(session, adapter),
23524
+ createBridge: (session, adapter, adapterId) => this.createBridge(session, adapter, adapterId),
23525
+ getSessionBridgeKeys: (sessionId) => this.getSessionBridgeKeys(sessionId),
23205
23526
  getMiddlewareChain: () => this.lifecycleManager?.middlewareChain,
23206
23527
  getService: (name) => this.lifecycleManager.serviceRegistry.get(name)
23207
23528
  });
@@ -23381,7 +23702,7 @@ User message:
23381
23702
  ${text6}`;
23382
23703
  }
23383
23704
  }
23384
- await session.enqueuePrompt(text6, message.attachments);
23705
+ await session.enqueuePrompt(text6, message.attachments, message.routing);
23385
23706
  }
23386
23707
  // --- Unified Session Creation Pipeline ---
23387
23708
  async createSession(params) {
@@ -23408,6 +23729,12 @@ ${text6}`;
23408
23729
  platform2.threadId = session.threadId;
23409
23730
  }
23410
23731
  }
23732
+ const platforms = {
23733
+ ...existingRecord?.platforms ?? {}
23734
+ };
23735
+ if (session.threadId) {
23736
+ platforms[params.channelId] = params.channelId === "telegram" ? { topicId: Number(session.threadId) || session.threadId } : { threadId: session.threadId };
23737
+ }
23411
23738
  await this.sessionManager.patchRecord(session.id, {
23412
23739
  sessionId: session.id,
23413
23740
  agentSessionId: session.agentSessionId,
@@ -23419,6 +23746,7 @@ ${text6}`;
23419
23746
  lastActiveAt: (/* @__PURE__ */ new Date()).toISOString(),
23420
23747
  name: session.name,
23421
23748
  platform: platform2,
23749
+ platforms,
23422
23750
  firstAgent: session.firstAgent,
23423
23751
  currentPromptCount: session.promptCount,
23424
23752
  agentSwitchHistory: session.agentSwitchHistory,
@@ -23426,7 +23754,7 @@ ${text6}`;
23426
23754
  acpState: session.toAcpStateSnapshot()
23427
23755
  }, { immediate: true });
23428
23756
  if (adapter) {
23429
- const bridge = this.createBridge(session, adapter);
23757
+ const bridge = this.createBridge(session, adapter, session.channelId);
23430
23758
  bridge.connect();
23431
23759
  adapter.flushPendingSkillCommands?.(session.id).catch((err) => {
23432
23760
  log40.warn({ err, sessionId: session.id }, "Failed to flush pending skill commands");
@@ -23565,9 +23893,14 @@ ${text6}`;
23565
23893
  } else {
23566
23894
  adoptPlatform.threadId = session.threadId;
23567
23895
  }
23896
+ const adoptPlatforms = {};
23897
+ if (session.threadId) {
23898
+ adoptPlatforms[adapterChannelId] = adapterChannelId === "telegram" ? { topicId: Number(session.threadId) || session.threadId } : { threadId: session.threadId };
23899
+ }
23568
23900
  await this.sessionManager.patchRecord(session.id, {
23569
23901
  originalAgentSessionId: agentSessionId,
23570
- platform: adoptPlatform
23902
+ platform: adoptPlatform,
23903
+ platforms: adoptPlatforms
23571
23904
  });
23572
23905
  return {
23573
23906
  ok: true,
@@ -23589,18 +23922,101 @@ ${text6}`;
23589
23922
  async getOrResumeSession(channelId, threadId) {
23590
23923
  return this.sessionFactory.getOrResume(channelId, threadId);
23591
23924
  }
23925
+ async getOrResumeSessionById(sessionId) {
23926
+ return this.sessionFactory.getOrResumeById(sessionId);
23927
+ }
23928
+ async attachAdapter(sessionId, adapterId) {
23929
+ const session = this.sessionManager.getSession(sessionId);
23930
+ if (!session) throw new Error(`Session ${sessionId} not found`);
23931
+ const adapter = this.adapters.get(adapterId);
23932
+ if (!adapter) throw new Error(`Adapter "${adapterId}" not found or not running`);
23933
+ if (session.attachedAdapters.includes(adapterId)) {
23934
+ const existingThread = session.threadIds.get(adapterId) ?? session.id;
23935
+ return { threadId: existingThread };
23936
+ }
23937
+ const threadId = await adapter.createSessionThread(
23938
+ session.id,
23939
+ session.name ?? `Session ${session.id.slice(0, 6)}`
23940
+ );
23941
+ session.threadIds.set(adapterId, threadId);
23942
+ session.attachedAdapters.push(adapterId);
23943
+ const bridge = this.createBridge(session, adapter, adapterId);
23944
+ bridge.connect();
23945
+ await this.sessionManager.patchRecord(session.id, {
23946
+ attachedAdapters: session.attachedAdapters,
23947
+ platforms: this.buildPlatformsFromSession(session)
23948
+ });
23949
+ return { threadId };
23950
+ }
23951
+ async detachAdapter(sessionId, adapterId) {
23952
+ const session = this.sessionManager.getSession(sessionId);
23953
+ if (!session) throw new Error(`Session ${sessionId} not found`);
23954
+ if (adapterId === session.channelId) {
23955
+ throw new Error("Cannot detach primary adapter (channelId)");
23956
+ }
23957
+ if (!session.attachedAdapters.includes(adapterId)) {
23958
+ return;
23959
+ }
23960
+ const adapter = this.adapters.get(adapterId);
23961
+ if (adapter) {
23962
+ try {
23963
+ await adapter.sendMessage(session.id, {
23964
+ type: "system_message",
23965
+ text: "Session detached from this adapter."
23966
+ });
23967
+ } catch {
23968
+ }
23969
+ }
23970
+ const key = this.bridgeKey(adapterId, session.id);
23971
+ const bridge = this.bridges.get(key);
23972
+ if (bridge) {
23973
+ bridge.disconnect();
23974
+ this.bridges.delete(key);
23975
+ }
23976
+ session.attachedAdapters = session.attachedAdapters.filter((a) => a !== adapterId);
23977
+ session.threadIds.delete(adapterId);
23978
+ await this.sessionManager.patchRecord(session.id, {
23979
+ attachedAdapters: session.attachedAdapters,
23980
+ platforms: this.buildPlatformsFromSession(session)
23981
+ });
23982
+ }
23983
+ buildPlatformsFromSession(session) {
23984
+ const platforms = {};
23985
+ for (const [adapterId, threadId] of session.threadIds) {
23986
+ if (adapterId === "telegram") {
23987
+ platforms.telegram = { topicId: Number(threadId) || threadId };
23988
+ } else {
23989
+ platforms[adapterId] = { threadId };
23990
+ }
23991
+ }
23992
+ return platforms;
23993
+ }
23592
23994
  // --- Event Wiring ---
23995
+ /** Composite bridge key: "adapterId:sessionId" */
23996
+ bridgeKey(adapterId, sessionId) {
23997
+ return `${adapterId}:${sessionId}`;
23998
+ }
23999
+ /** Get all bridge keys for a session (regardless of adapter) */
24000
+ getSessionBridgeKeys(sessionId) {
24001
+ const keys = [];
24002
+ for (const key of this.bridges.keys()) {
24003
+ if (key.endsWith(`:${sessionId}`)) keys.push(key);
24004
+ }
24005
+ return keys;
24006
+ }
23593
24007
  /** Connect a session bridge for the given session (used by AssistantManager) */
23594
24008
  connectSessionBridge(session) {
23595
24009
  const adapter = this.adapters.get(session.channelId);
23596
24010
  if (!adapter) return;
23597
- const bridge = this.createBridge(session, adapter);
24011
+ const bridge = this.createBridge(session, adapter, session.channelId);
23598
24012
  bridge.connect();
23599
24013
  }
23600
24014
  /** 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);
24015
+ * Disconnects any existing bridge for the same adapter+session first. */
24016
+ createBridge(session, adapter, adapterId) {
24017
+ const id = adapterId ?? adapter.name;
24018
+ const key = this.bridgeKey(id, session.id);
24019
+ const existing = this.bridges.get(key);
23604
24020
  if (existing) {
23605
24021
  existing.disconnect();
23606
24022
  }
@@ -23611,8 +24027,8 @@ ${text6}`;
23611
24027
  eventBus: this.eventBus,
23612
24028
  fileService: this.fileService,
23613
24029
  middlewareChain: this.lifecycleManager?.middlewareChain
23614
- });
23615
- this.bridges.set(session.id, bridge);
24030
+ }, id);
24031
+ this.bridges.set(key, bridge);
23616
24032
  return bridge;
23617
24033
  }
23618
24034
  };
@@ -24257,13 +24673,15 @@ function registerSwitchCommands(registry, _core) {
24257
24673
  return { type: "error", message: "No active session in this topic." };
24258
24674
  }
24259
24675
  if (raw) {
24676
+ const droppedCount = session.queueDepth;
24260
24677
  if (session.promptRunning) {
24261
24678
  await session.abortPrompt();
24262
24679
  }
24263
24680
  try {
24264
24681
  const { resumed } = await core.switchSessionAgent(session.id, raw);
24265
24682
  const status = resumed ? "resumed" : "new session";
24266
- return { type: "text", text: `\u2705 Switched to ${raw} (${status})` };
24683
+ const droppedNote = droppedCount > 0 ? ` (${droppedCount} queued prompt${droppedCount > 1 ? "s" : ""} cleared)` : "";
24684
+ return { type: "text", text: `\u2705 Switched to ${raw} (${status})${droppedNote}` };
24267
24685
  } catch (err) {
24268
24686
  return { type: "error", message: `Failed to switch agent: ${err.message || err}` };
24269
24687
  }
@@ -24356,27 +24774,8 @@ function registerCategoryCommand(registry, core, category, commandName) {
24356
24774
  if (configOption.currentValue === raw) {
24357
24775
  return { type: "text", text: `Already using **${match.name}**.` };
24358
24776
  }
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
24777
  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
- }
24778
+ await session.setConfigOption(configOption.id, { type: "select", value: raw });
24380
24779
  core.eventBus.emit("session:configChanged", { sessionId: session.id });
24381
24780
  return { type: "text", text: labels.successMsg(match.name, configOption.name) };
24382
24781
  } catch (err) {
@@ -24438,17 +24837,7 @@ function registerDangerousCommand(registry, core) {
24438
24837
  if (bypassValue && modeConfig) {
24439
24838
  try {
24440
24839
  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
- }
24840
+ await session.setConfigOption(modeConfig.id, { type: "select", value: targetValue });
24452
24841
  core.eventBus.emit("session:configChanged", { sessionId: session.id });
24453
24842
  return {
24454
24843
  type: "text",
@@ -26438,7 +26827,10 @@ async function startServer(opts) {
26438
26827
  const tunnelErr = tunnelSvc.getStartError();
26439
26828
  const url = tunnelSvc.getPublicUrl();
26440
26829
  const isPublic = url && !url.startsWith("http://localhost") && !url.startsWith("http://127.0.0.1");
26441
- if (tunnelErr) {
26830
+ if (tunnelErr && isPublic) {
26831
+ warn3(`Primary tunnel failed \u2014 using fallback: ${tunnelErr}`);
26832
+ tunnelUrl = url;
26833
+ } else if (tunnelErr) {
26442
26834
  warn3(`Tunnel failed (${tunnelErr}) \u2014 retrying in background`);
26443
26835
  } else if (isPublic) {
26444
26836
  ok3("Tunnel ready");