@openacp/cli 2026.403.7 → 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
  );
@@ -11411,7 +11455,7 @@ var BYPASS_KEYWORDS;
11411
11455
  var init_bypass_detection = __esm({
11412
11456
  "src/core/utils/bypass-detection.ts"() {
11413
11457
  "use strict";
11414
- BYPASS_KEYWORDS = ["bypass", "dangerous", "auto_accept"];
11458
+ BYPASS_KEYWORDS = ["bypass", "dangerous", "auto_accept", "yolo"];
11415
11459
  }
11416
11460
  });
11417
11461
 
@@ -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", {
@@ -18884,12 +18929,18 @@ ${stderr}`
18884
18929
  cwd: workingDirectory,
18885
18930
  mcpServers: resolvedMcp
18886
18931
  });
18932
+ log29.info(response, "newSession response");
18887
18933
  instance.sessionId = response.sessionId;
18888
18934
  instance.initialSessionResponse = response;
18889
18935
  instance.debugTracer = createDebugTracer(response.sessionId, workingDirectory);
18890
18936
  instance.setupCrashDetection();
18891
18937
  log29.info(
18892
- { sessionId: response.sessionId, durationMs: Date.now() - spawnStart },
18938
+ {
18939
+ sessionId: response.sessionId,
18940
+ durationMs: Date.now() - spawnStart,
18941
+ configOptions: response.configOptions ?? [],
18942
+ agentCapabilities: instance.agentCapabilities ?? null
18943
+ },
18893
18944
  "Agent spawn complete"
18894
18945
  );
18895
18946
  return instance;
@@ -18901,24 +18952,47 @@ ${stderr}`
18901
18952
  agentDef,
18902
18953
  workingDirectory
18903
18954
  );
18955
+ const resolvedMcp = _AgentInstance.mcpManager.resolve(mcpServers);
18904
18956
  try {
18905
- const response = await instance.connection.unstable_resumeSession({
18906
- sessionId: agentSessionId,
18907
- cwd: workingDirectory
18908
- });
18909
- instance.sessionId = response.sessionId;
18910
- instance.initialSessionResponse = response;
18911
- instance.debugTracer = createDebugTracer(response.sessionId, workingDirectory);
18912
- log29.info(
18913
- { sessionId: response.sessionId, durationMs: Date.now() - spawnStart },
18914
- "Agent resume complete"
18915
- );
18957
+ if (instance.agentCapabilities?.loadSession) {
18958
+ const response = await instance.connection.loadSession({
18959
+ sessionId: agentSessionId,
18960
+ cwd: workingDirectory,
18961
+ mcpServers: resolvedMcp
18962
+ });
18963
+ instance.sessionId = agentSessionId;
18964
+ instance.initialSessionResponse = response;
18965
+ instance.debugTracer = createDebugTracer(agentSessionId, workingDirectory);
18966
+ log29.info(
18967
+ {
18968
+ sessionId: agentSessionId,
18969
+ durationMs: Date.now() - spawnStart,
18970
+ agentCapabilities: instance.agentCapabilities ?? null
18971
+ },
18972
+ "Agent load complete"
18973
+ );
18974
+ } else {
18975
+ const response = await instance.connection.unstable_resumeSession({
18976
+ sessionId: agentSessionId,
18977
+ cwd: workingDirectory
18978
+ });
18979
+ instance.sessionId = response.sessionId;
18980
+ instance.initialSessionResponse = response;
18981
+ instance.debugTracer = createDebugTracer(response.sessionId, workingDirectory);
18982
+ log29.info(
18983
+ {
18984
+ sessionId: response.sessionId,
18985
+ durationMs: Date.now() - spawnStart,
18986
+ agentCapabilities: instance.agentCapabilities ?? null
18987
+ },
18988
+ "Agent resume complete"
18989
+ );
18990
+ }
18916
18991
  } catch (err) {
18917
18992
  log29.warn(
18918
18993
  { err, agentSessionId },
18919
18994
  "Resume failed, falling back to new session"
18920
18995
  );
18921
- const resolvedMcp = _AgentInstance.mcpManager.resolve(mcpServers);
18922
18996
  const response = await instance.connection.newSession({
18923
18997
  cwd: workingDirectory,
18924
18998
  mcpServers: resolvedMcp
@@ -19149,11 +19223,31 @@ ${stderr}`
19149
19223
  }
19150
19224
  // ── New ACP methods ──────────────────────────────────────────────────
19151
19225
  async setConfigOption(configId, value) {
19152
- return await this.connection.setSessionConfigOption({
19153
- sessionId: this.sessionId,
19154
- configId,
19155
- ...value
19156
- });
19226
+ try {
19227
+ return await this.connection.setSessionConfigOption({
19228
+ sessionId: this.sessionId,
19229
+ configId,
19230
+ ...value
19231
+ });
19232
+ } catch (err) {
19233
+ if (typeof err === "object" && err !== null && err.code === -32601) {
19234
+ if (configId === "mode" && value.type === "select") {
19235
+ await this.connection.setSessionMode({
19236
+ sessionId: this.sessionId,
19237
+ modeId: value.value
19238
+ });
19239
+ return { configOptions: [] };
19240
+ }
19241
+ if (configId === "model" && value.type === "select") {
19242
+ await this.connection.unstable_setSessionModel({
19243
+ sessionId: this.sessionId,
19244
+ modelId: value.value
19245
+ });
19246
+ return { configOptions: [] };
19247
+ }
19248
+ }
19249
+ throw err;
19250
+ }
19157
19251
  }
19158
19252
  async listSessions(cwd, cursor) {
19159
19253
  return await this.connection.listSessions({
@@ -19302,21 +19396,21 @@ var init_prompt_queue = __esm({
19302
19396
  queue = [];
19303
19397
  processing = false;
19304
19398
  abortController = null;
19305
- async enqueue(text6, attachments) {
19399
+ async enqueue(text6, attachments, routing) {
19306
19400
  if (this.processing) {
19307
19401
  return new Promise((resolve8) => {
19308
- this.queue.push({ text: text6, attachments, resolve: resolve8 });
19402
+ this.queue.push({ text: text6, attachments, routing, resolve: resolve8 });
19309
19403
  });
19310
19404
  }
19311
- await this.process(text6, attachments);
19405
+ await this.process(text6, attachments, routing);
19312
19406
  }
19313
- async process(text6, attachments) {
19407
+ async process(text6, attachments, routing) {
19314
19408
  this.processing = true;
19315
19409
  this.abortController = new AbortController();
19316
19410
  const { signal } = this.abortController;
19317
19411
  try {
19318
19412
  await Promise.race([
19319
- this.processor(text6, attachments),
19413
+ this.processor(text6, attachments, routing),
19320
19414
  new Promise((_, reject) => {
19321
19415
  signal.addEventListener("abort", () => reject(new Error("Prompt aborted")), { once: true });
19322
19416
  })
@@ -19334,7 +19428,7 @@ var init_prompt_queue = __esm({
19334
19428
  drainNext() {
19335
19429
  const next = this.queue.shift();
19336
19430
  if (next) {
19337
- this.process(next.text, next.attachments).then(next.resolve);
19431
+ this.process(next.text, next.attachments, next.routing).then(next.resolve);
19338
19432
  }
19339
19433
  }
19340
19434
  clear() {
@@ -19429,8 +19523,39 @@ var init_permission_gate = __esm({
19429
19523
  }
19430
19524
  });
19431
19525
 
19432
- // src/core/sessions/session.ts
19526
+ // src/core/sessions/turn-context.ts
19433
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";
19434
19559
  import * as fs41 from "fs";
19435
19560
  var moduleLog, TTS_PROMPT_INSTRUCTION, TTS_BLOCK_REGEX, TTS_MAX_LENGTH, TTS_TIMEOUT_MS, VALID_TRANSITIONS, Session;
19436
19561
  var init_session2 = __esm({
@@ -19440,6 +19565,7 @@ var init_session2 = __esm({
19440
19565
  init_prompt_queue();
19441
19566
  init_permission_gate();
19442
19567
  init_log();
19568
+ init_turn_context();
19443
19569
  moduleLog = createChildLogger({ module: "session" });
19444
19570
  TTS_PROMPT_INSTRUCTION = `
19445
19571
 
@@ -19457,7 +19583,15 @@ Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of y
19457
19583
  Session = class extends TypedEmitter {
19458
19584
  id;
19459
19585
  channelId;
19460
- 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
+ }
19461
19595
  agentName;
19462
19596
  workingDirectory;
19463
19597
  agentInstance;
@@ -19478,14 +19612,21 @@ Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of y
19478
19612
  middlewareChain;
19479
19613
  /** Latest commands emitted by the agent — buffered before bridge connects so they're not lost */
19480
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;
19481
19621
  permissionGate = new PermissionGate();
19482
19622
  queue;
19483
19623
  speechService;
19484
19624
  pendingContext = null;
19485
19625
  constructor(opts) {
19486
19626
  super();
19487
- this.id = opts.id || nanoid3(12);
19627
+ this.id = opts.id || nanoid4(12);
19488
19628
  this.channelId = opts.channelId;
19629
+ this.attachedAdapters = [opts.channelId];
19489
19630
  this.agentName = opts.agentName;
19490
19631
  this.firstAgent = opts.agentName;
19491
19632
  this.workingDirectory = opts.workingDirectory;
@@ -19495,18 +19636,28 @@ Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of y
19495
19636
  this.log = createSessionLogger(this.id, moduleLog);
19496
19637
  this.log.info({ agentName: this.agentName }, "Session created");
19497
19638
  this.queue = new PromptQueue(
19498
- (text6, attachments) => this.processPrompt(text6, attachments),
19639
+ (text6, attachments, routing) => this.processPrompt(text6, attachments, routing),
19499
19640
  (err) => {
19500
19641
  this.log.error({ err }, "Prompt execution failed");
19501
19642
  const message = err instanceof Error ? err.message : String(err);
19643
+ this.fail(message);
19502
19644
  this.emit("agent_event", { type: "error", message: `Prompt execution failed: ${message}` });
19503
19645
  }
19504
19646
  );
19505
- 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) => {
19506
19655
  if (event.type === "commands_update") {
19507
19656
  this.latestCommands = event.commands;
19508
19657
  }
19509
- });
19658
+ };
19659
+ this.agentInstance.on("agent_event", handler);
19660
+ this.commandsBufferCleanup = () => this.agentInstance.off("agent_event", handler);
19510
19661
  }
19511
19662
  // --- State Machine ---
19512
19663
  get status() {
@@ -19560,7 +19711,7 @@ Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of y
19560
19711
  this.log.info({ voiceMode: mode }, "TTS mode changed");
19561
19712
  }
19562
19713
  // --- Public API ---
19563
- async enqueuePrompt(text6, attachments) {
19714
+ async enqueuePrompt(text6, attachments, routing) {
19564
19715
  if (this.middlewareChain) {
19565
19716
  const payload = { text: text6, attachments, sessionId: this.id };
19566
19717
  const result = await this.middlewareChain.execute("agent:beforePrompt", payload, async (p2) => p2);
@@ -19568,10 +19719,14 @@ Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of y
19568
19719
  text6 = result.text;
19569
19720
  attachments = result.attachments;
19570
19721
  }
19571
- await this.queue.enqueue(text6, attachments);
19722
+ await this.queue.enqueue(text6, attachments, routing);
19572
19723
  }
19573
- async processPrompt(text6, attachments) {
19724
+ async processPrompt(text6, attachments, routing) {
19574
19725
  if (this._status === "finished") return;
19726
+ this.activeTurnContext = createTurnContext(
19727
+ routing?.sourceAdapterId ?? this.channelId,
19728
+ routing?.responseAdapterId
19729
+ );
19575
19730
  this.promptCount++;
19576
19731
  this.emit("prompt_count_changed", this.promptCount);
19577
19732
  if (this._status === "initializing" || this._status === "cancelled" || this._status === "error") {
@@ -19638,6 +19793,7 @@ ${text6}`;
19638
19793
  this.log.warn({ err }, "TTS post-processing failed");
19639
19794
  });
19640
19795
  }
19796
+ this.activeTurnContext = null;
19641
19797
  if (!this.name) {
19642
19798
  await this.autoName();
19643
19799
  }
@@ -19753,6 +19909,42 @@ ${result.text}` : result.text;
19753
19909
  setAgentCapabilities(caps) {
19754
19910
  this.agentCapabilities = caps;
19755
19911
  }
19912
+ /**
19913
+ * Hydrate configOptions and agentCapabilities from a spawn response.
19914
+ * Handles both the native configOptions format and legacy modes/models fields.
19915
+ */
19916
+ applySpawnResponse(resp, caps) {
19917
+ if (caps) this.agentCapabilities = caps;
19918
+ if (!resp) return;
19919
+ if (resp.configOptions) {
19920
+ this.configOptions = resp.configOptions;
19921
+ return;
19922
+ }
19923
+ const legacyOptions = [];
19924
+ if (resp.modes) {
19925
+ const m = resp.modes;
19926
+ legacyOptions.push({
19927
+ id: "mode",
19928
+ name: "Mode",
19929
+ category: "mode",
19930
+ type: "select",
19931
+ currentValue: m.currentModeId,
19932
+ options: m.availableModes.map((x) => ({ value: x.id, name: x.name, description: x.description }))
19933
+ });
19934
+ }
19935
+ if (resp.models) {
19936
+ const m = resp.models;
19937
+ legacyOptions.push({
19938
+ id: "model",
19939
+ name: "Model",
19940
+ category: "model",
19941
+ type: "select",
19942
+ currentValue: m.currentModelId,
19943
+ options: m.availableModels.map((x) => ({ value: x.modelId ?? x.id, name: x.name, description: x.description }))
19944
+ });
19945
+ }
19946
+ if (legacyOptions.length > 0) this.configOptions = legacyOptions;
19947
+ }
19756
19948
  getConfigOption(id) {
19757
19949
  return this.configOptions.find((o) => o.id === id);
19758
19950
  }
@@ -19772,8 +19964,13 @@ ${result.text}` : result.text;
19772
19964
  /** Send a config option change to the agent and update local state from the response. */
19773
19965
  async setConfigOption(configId, value) {
19774
19966
  const response = await this.agentInstance.setConfigOption(configId, value);
19775
- if (response.configOptions) {
19967
+ if (response.configOptions && response.configOptions.length > 0) {
19776
19968
  await this.updateConfigOptions(response.configOptions);
19969
+ } else if (value.type === "select") {
19970
+ const updated = this.configOptions.map(
19971
+ (o) => o.id === configId && o.type === "select" ? { ...o, currentValue: value.value } : o
19972
+ );
19973
+ await this.updateConfigOptions(updated);
19777
19974
  }
19778
19975
  }
19779
19976
  async updateConfigOptions(options) {
@@ -19837,6 +20034,9 @@ ${result.text}` : result.text;
19837
20034
  this.promptCount = 0;
19838
20035
  this.agentCapabilities = void 0;
19839
20036
  this.configOptions = [];
20037
+ this.latestCommands = null;
20038
+ this.applySpawnResponse(newAgent.initialSessionResponse, newAgent.agentCapabilities);
20039
+ this.wireCommandsBuffer();
19840
20040
  this.log.info({ from: this.agentSwitchHistory.at(-1).agentName, to: agentName }, "Agent switched");
19841
20041
  }
19842
20042
  async destroy() {
@@ -19901,6 +20101,8 @@ var init_session_manager = __esm({
19901
20101
  }
19902
20102
  getSessionByThread(channelId, threadId) {
19903
20103
  for (const session of this.sessions.values()) {
20104
+ const adapterThread = session.threadIds.get(channelId);
20105
+ if (adapterThread === threadId) return session;
19904
20106
  if (session.channelId === channelId && session.threadId === threadId) {
19905
20107
  return session;
19906
20108
  }
@@ -19968,6 +20170,67 @@ var init_session_manager = __esm({
19968
20170
  if (channelId) return all.filter((s) => s.channelId === channelId);
19969
20171
  return all;
19970
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
+ }
19971
20234
  listRecords(filter) {
19972
20235
  if (!this.store) return [];
19973
20236
  let records = this.store.list();
@@ -19990,7 +20253,14 @@ var init_session_manager = __esm({
19990
20253
  for (const session of this.sessions.values()) {
19991
20254
  const record = this.store.get(session.id);
19992
20255
  if (record) {
19993
- 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
+ });
19994
20264
  }
19995
20265
  }
19996
20266
  this.store.flush();
@@ -20000,6 +20270,8 @@ var init_session_manager = __esm({
20000
20270
  /**
20001
20271
  * Forcefully destroy all sessions (kill agent subprocesses).
20002
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.
20003
20275
  */
20004
20276
  async destroyAll() {
20005
20277
  if (this.store) {
@@ -20034,15 +20306,18 @@ var init_session_bridge = __esm({
20034
20306
  "use strict";
20035
20307
  init_log();
20036
20308
  init_bypass_detection();
20309
+ init_turn_context();
20037
20310
  log30 = createChildLogger({ module: "session-bridge" });
20038
20311
  SessionBridge = class {
20039
- constructor(session, adapter, deps) {
20312
+ constructor(session, adapter, deps, adapterId) {
20040
20313
  this.session = session;
20041
20314
  this.adapter = adapter;
20042
20315
  this.deps = deps;
20316
+ this.adapterId = adapterId ?? adapter.name;
20043
20317
  }
20044
20318
  connected = false;
20045
20319
  cleanupFns = [];
20320
+ adapterId;
20046
20321
  get tracer() {
20047
20322
  return this.session.agentInstance.debugTracer ?? null;
20048
20323
  }
@@ -20073,6 +20348,15 @@ var init_session_bridge = __esm({
20073
20348
  log30.error({ err, sessionId }, "Error in sendMessage middleware");
20074
20349
  }
20075
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
+ }
20076
20360
  connect() {
20077
20361
  if (this.connected) return;
20078
20362
  this.connected = true;
@@ -20080,11 +20364,29 @@ var init_session_bridge = __esm({
20080
20364
  this.session.emit("agent_event", event);
20081
20365
  });
20082
20366
  this.listen(this.session, "agent_event", (event) => {
20083
- 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
+ }
20084
20389
  });
20085
- this.session.agentInstance.onPermissionRequest = async (request) => {
20086
- return this.resolvePermission(request);
20087
- };
20088
20390
  this.listen(this.session, "status_change", (from, to) => {
20089
20391
  this.deps.sessionManager.patchRecord(this.session.id, {
20090
20392
  status: to,
@@ -20116,13 +20418,19 @@ var init_session_bridge = __esm({
20116
20418
  if (this.session.latestCommands !== null) {
20117
20419
  this.session.emit("agent_event", { type: "commands_update", commands: this.session.latestCommands });
20118
20420
  }
20421
+ if (this.session.configOptions.length > 0) {
20422
+ this.session.emit("agent_event", { type: "config_option_update", options: this.session.configOptions });
20423
+ }
20119
20424
  }
20120
20425
  disconnect() {
20121
20426
  if (!this.connected) return;
20122
20427
  this.connected = false;
20123
20428
  this.cleanupFns.forEach((fn) => fn());
20124
20429
  this.cleanupFns = [];
20125
- 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
+ }
20126
20434
  }
20127
20435
  /** Dispatch an agent event through middleware and to the adapter */
20128
20436
  async dispatchAgentEvent(event) {
@@ -20253,8 +20561,10 @@ var init_session_bridge = __esm({
20253
20561
  this.sendMessage(this.session.id, outgoing);
20254
20562
  break;
20255
20563
  case "config_option_update":
20256
- this.session.updateConfigOptions(event.options);
20257
- this.persistAcpState();
20564
+ this.session.updateConfigOptions(event.options).then(() => {
20565
+ this.persistAcpState();
20566
+ }).catch(() => {
20567
+ });
20258
20568
  outgoing = this.deps.messageTransformer.transform(event);
20259
20569
  this.sendMessage(this.session.id, outgoing);
20260
20570
  break;
@@ -20298,19 +20608,27 @@ var init_session_bridge = __esm({
20298
20608
  return result.autoResolve;
20299
20609
  }
20300
20610
  }
20301
- this.session.emit("permission_request", permReq);
20302
20611
  this.deps.eventBus?.emit("permission:request", {
20303
20612
  sessionId: this.session.id,
20304
20613
  permission: permReq
20305
20614
  });
20306
20615
  const autoDecision = this.checkAutoApprove(permReq);
20307
20616
  if (autoDecision) {
20617
+ this.session.emit("permission_request", permReq);
20308
20618
  this.emitAfterResolve(mw, permReq.id, autoDecision, "system", startTime);
20309
20619
  return autoDecision;
20310
20620
  }
20311
20621
  const promise = this.session.permissionGate.setPending(permReq);
20622
+ this.session.emit("permission_request", permReq);
20312
20623
  await this.adapter.sendPermissionRequest(this.session.id, permReq);
20313
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
+ });
20314
20632
  this.emitAfterResolve(mw, permReq.id, optionId, "user", startTime);
20315
20633
  return optionId;
20316
20634
  }
@@ -20810,6 +21128,9 @@ var init_session_store = __esm({
20810
21128
  }
20811
21129
  findByPlatform(channelId, predicate) {
20812
21130
  for (const record of this.records.values()) {
21131
+ if (record.platforms?.[channelId]) {
21132
+ if (predicate(record.platforms[channelId])) return record;
21133
+ }
20813
21134
  if (record.channelId === channelId && predicate(record.platform)) {
20814
21135
  return record;
20815
21136
  }
@@ -20876,7 +21197,7 @@ var init_session_store = __esm({
20876
21197
  return;
20877
21198
  }
20878
21199
  for (const [id, record] of Object.entries(raw.sessions)) {
20879
- this.records.set(id, record);
21200
+ this.records.set(id, this.migrateRecord(record));
20880
21201
  }
20881
21202
  log32.debug({ count: this.records.size }, "Loaded session records");
20882
21203
  } catch (err) {
@@ -20887,6 +21208,19 @@ var init_session_store = __esm({
20887
21208
  }
20888
21209
  }
20889
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
+ }
20890
21224
  cleanup() {
20891
21225
  const cutoff = Date.now() - this.ttlDays * 24 * 60 * 60 * 1e3;
20892
21226
  let removed = 0;
@@ -21057,17 +21391,7 @@ var init_session_factory = __esm({
21057
21391
  if (createParams.initialName) {
21058
21392
  session.name = createParams.initialName;
21059
21393
  }
21060
- const resp = agentInstance.initialSessionResponse;
21061
- if (resp) {
21062
- if (resp.configOptions) {
21063
- session.setInitialConfigOptions(resp.configOptions);
21064
- }
21065
- if (agentInstance.agentCapabilities) {
21066
- session.setAgentCapabilities(agentInstance.agentCapabilities);
21067
- }
21068
- } else if (agentInstance.agentCapabilities) {
21069
- session.setAgentCapabilities(agentInstance.agentCapabilities);
21070
- }
21394
+ session.applySpawnResponse(agentInstance.initialSessionResponse, agentInstance.agentCapabilities);
21071
21395
  this.sessionManager.registerSession(session);
21072
21396
  this.eventBus.emit("session:created", {
21073
21397
  sessionId: session.id,
@@ -21085,6 +21409,65 @@ var init_session_factory = __esm({
21085
21409
  if (session) return session;
21086
21410
  return this.lazyResume(channelId, threadId);
21087
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
+ }
21088
21471
  async lazyResume(channelId, threadId) {
21089
21472
  const store = this.sessionStore;
21090
21473
  if (!store || !this.createFullSession) return null;
@@ -21093,7 +21476,7 @@ var init_session_factory = __esm({
21093
21476
  if (existing) return existing;
21094
21477
  const record = store.findByPlatform(
21095
21478
  channelId,
21096
- (p2) => String(p2.topicId) === threadId
21479
+ (p2) => String(p2.topicId) === threadId || String(p2.threadId ?? "") === threadId
21097
21480
  );
21098
21481
  if (!record) {
21099
21482
  log33.debug({ threadId, channelId }, "No session record found for thread");
@@ -21128,11 +21511,21 @@ var init_session_factory = __esm({
21128
21511
  if (record.firstAgent) session.firstAgent = record.firstAgent;
21129
21512
  if (record.agentSwitchHistory) session.agentSwitchHistory = record.agentSwitchHistory;
21130
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
+ }
21131
21524
  if (record.acpState) {
21132
- if (record.acpState.configOptions) {
21525
+ if (record.acpState.configOptions && session.configOptions.length === 0) {
21133
21526
  session.setInitialConfigOptions(record.acpState.configOptions);
21134
21527
  }
21135
- if (record.acpState.agentCapabilities) {
21528
+ if (record.acpState.agentCapabilities && !session.agentCapabilities) {
21136
21529
  session.setAgentCapabilities(record.acpState.agentCapabilities);
21137
21530
  }
21138
21531
  }
@@ -21315,8 +21708,15 @@ var init_agent_switch_handler = __esm({
21315
21708
  toAgent,
21316
21709
  status: "starting"
21317
21710
  });
21318
- const bridge = bridges.get(sessionId);
21319
- 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
+ }
21320
21720
  const switchAdapter = adapters.get(session.channelId);
21321
21721
  if (switchAdapter?.sendSkillCommands) {
21322
21722
  await switchAdapter.sendSkillCommands(session.id, []);
@@ -21393,9 +21793,11 @@ var init_agent_switch_handler = __esm({
21393
21793
  session.agentInstance = oldInstance;
21394
21794
  session.agentName = fromAgent;
21395
21795
  session.agentSessionId = oldInstance.sessionId;
21396
- const adapter = adapters.get(session.channelId);
21397
- if (adapter) {
21398
- 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
+ }
21399
21801
  }
21400
21802
  log34.warn({ sessionId, fromAgent, toAgent, err }, "Agent switch failed, rolled back to previous agent");
21401
21803
  } catch (rollbackErr) {
@@ -21404,10 +21806,14 @@ var init_agent_switch_handler = __esm({
21404
21806
  }
21405
21807
  throw err;
21406
21808
  }
21407
- if (bridge) {
21408
- const adapter = adapters.get(session.channelId);
21409
- if (adapter) {
21410
- 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
+ }
21411
21817
  }
21412
21818
  }
21413
21819
  await sessionManager.patchRecord(sessionId, {
@@ -22155,7 +22561,7 @@ function createPluginContext(opts) {
22155
22561
  }
22156
22562
  };
22157
22563
  const baseLog = opts.log ?? noopLog;
22158
- const log44 = typeof baseLog.child === "function" ? baseLog.child({ plugin: pluginName }) : baseLog;
22564
+ const log45 = typeof baseLog.child === "function" ? baseLog.child({ plugin: pluginName }) : baseLog;
22159
22565
  const storageImpl = new PluginStorageImpl(storagePath);
22160
22566
  const storage = {
22161
22567
  async get(key) {
@@ -22182,7 +22588,7 @@ function createPluginContext(opts) {
22182
22588
  const ctx = {
22183
22589
  pluginName,
22184
22590
  pluginConfig,
22185
- log: log44,
22591
+ log: log45,
22186
22592
  storage,
22187
22593
  on(event, handler) {
22188
22594
  requirePermission(permissions, "events:read", "on()");
@@ -22217,7 +22623,7 @@ function createPluginContext(opts) {
22217
22623
  const registry = serviceRegistry.get("command-registry");
22218
22624
  if (registry && typeof registry.register === "function") {
22219
22625
  registry.register(def, pluginName);
22220
- log44.debug(`Command '/${def.name}' registered`);
22626
+ log45.debug(`Command '/${def.name}' registered`);
22221
22627
  }
22222
22628
  },
22223
22629
  async sendMessage(_sessionId, _content) {
@@ -23019,7 +23425,7 @@ var init_core = __esm({
23019
23425
  sessionManager;
23020
23426
  messageTransformer;
23021
23427
  adapters = /* @__PURE__ */ new Map();
23022
- /** sessionId → SessionBridge — tracks active bridges for disconnect/reconnect during agent switch */
23428
+ /** "adapterId:sessionId" → SessionBridge — tracks active bridges for disconnect/reconnect */
23023
23429
  bridges = /* @__PURE__ */ new Map();
23024
23430
  /** Set by main.ts — triggers graceful shutdown with restart exit code */
23025
23431
  requestRestart = null;
@@ -23115,7 +23521,8 @@ var init_core = __esm({
23115
23521
  eventBus: this.eventBus,
23116
23522
  adapters: this.adapters,
23117
23523
  bridges: this.bridges,
23118
- createBridge: (session, adapter) => this.createBridge(session, adapter),
23524
+ createBridge: (session, adapter, adapterId) => this.createBridge(session, adapter, adapterId),
23525
+ getSessionBridgeKeys: (sessionId) => this.getSessionBridgeKeys(sessionId),
23119
23526
  getMiddlewareChain: () => this.lifecycleManager?.middlewareChain,
23120
23527
  getService: (name) => this.lifecycleManager.serviceRegistry.get(name)
23121
23528
  });
@@ -23295,7 +23702,7 @@ User message:
23295
23702
  ${text6}`;
23296
23703
  }
23297
23704
  }
23298
- await session.enqueuePrompt(text6, message.attachments);
23705
+ await session.enqueuePrompt(text6, message.attachments, message.routing);
23299
23706
  }
23300
23707
  // --- Unified Session Creation Pipeline ---
23301
23708
  async createSession(params) {
@@ -23322,6 +23729,12 @@ ${text6}`;
23322
23729
  platform2.threadId = session.threadId;
23323
23730
  }
23324
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
+ }
23325
23738
  await this.sessionManager.patchRecord(session.id, {
23326
23739
  sessionId: session.id,
23327
23740
  agentSessionId: session.agentSessionId,
@@ -23333,6 +23746,7 @@ ${text6}`;
23333
23746
  lastActiveAt: (/* @__PURE__ */ new Date()).toISOString(),
23334
23747
  name: session.name,
23335
23748
  platform: platform2,
23749
+ platforms,
23336
23750
  firstAgent: session.firstAgent,
23337
23751
  currentPromptCount: session.promptCount,
23338
23752
  agentSwitchHistory: session.agentSwitchHistory,
@@ -23340,7 +23754,7 @@ ${text6}`;
23340
23754
  acpState: session.toAcpStateSnapshot()
23341
23755
  }, { immediate: true });
23342
23756
  if (adapter) {
23343
- const bridge = this.createBridge(session, adapter);
23757
+ const bridge = this.createBridge(session, adapter, session.channelId);
23344
23758
  bridge.connect();
23345
23759
  adapter.flushPendingSkillCommands?.(session.id).catch((err) => {
23346
23760
  log40.warn({ err, sessionId: session.id }, "Failed to flush pending skill commands");
@@ -23479,9 +23893,14 @@ ${text6}`;
23479
23893
  } else {
23480
23894
  adoptPlatform.threadId = session.threadId;
23481
23895
  }
23896
+ const adoptPlatforms = {};
23897
+ if (session.threadId) {
23898
+ adoptPlatforms[adapterChannelId] = adapterChannelId === "telegram" ? { topicId: Number(session.threadId) || session.threadId } : { threadId: session.threadId };
23899
+ }
23482
23900
  await this.sessionManager.patchRecord(session.id, {
23483
23901
  originalAgentSessionId: agentSessionId,
23484
- platform: adoptPlatform
23902
+ platform: adoptPlatform,
23903
+ platforms: adoptPlatforms
23485
23904
  });
23486
23905
  return {
23487
23906
  ok: true,
@@ -23503,18 +23922,101 @@ ${text6}`;
23503
23922
  async getOrResumeSession(channelId, threadId) {
23504
23923
  return this.sessionFactory.getOrResume(channelId, threadId);
23505
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
+ }
23506
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
+ }
23507
24007
  /** Connect a session bridge for the given session (used by AssistantManager) */
23508
24008
  connectSessionBridge(session) {
23509
24009
  const adapter = this.adapters.get(session.channelId);
23510
24010
  if (!adapter) return;
23511
- const bridge = this.createBridge(session, adapter);
24011
+ const bridge = this.createBridge(session, adapter, session.channelId);
23512
24012
  bridge.connect();
23513
24013
  }
23514
24014
  /** Create a SessionBridge for the given session and adapter.
23515
- * Disconnects any existing bridge for the same session first. */
23516
- createBridge(session, adapter) {
23517
- 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);
23518
24020
  if (existing) {
23519
24021
  existing.disconnect();
23520
24022
  }
@@ -23525,8 +24027,8 @@ ${text6}`;
23525
24027
  eventBus: this.eventBus,
23526
24028
  fileService: this.fileService,
23527
24029
  middlewareChain: this.lifecycleManager?.middlewareChain
23528
- });
23529
- this.bridges.set(session.id, bridge);
24030
+ }, id);
24031
+ this.bridges.set(key, bridge);
23530
24032
  return bridge;
23531
24033
  }
23532
24034
  };
@@ -24171,13 +24673,15 @@ function registerSwitchCommands(registry, _core) {
24171
24673
  return { type: "error", message: "No active session in this topic." };
24172
24674
  }
24173
24675
  if (raw) {
24676
+ const droppedCount = session.queueDepth;
24174
24677
  if (session.promptRunning) {
24175
24678
  await session.abortPrompt();
24176
24679
  }
24177
24680
  try {
24178
24681
  const { resumed } = await core.switchSessionAgent(session.id, raw);
24179
24682
  const status = resumed ? "resumed" : "new session";
24180
- 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}` };
24181
24685
  } catch (err) {
24182
24686
  return { type: "error", message: `Failed to switch agent: ${err.message || err}` };
24183
24687
  }
@@ -24270,27 +24774,13 @@ function registerCategoryCommand(registry, core, category, commandName) {
24270
24774
  if (configOption.currentValue === raw) {
24271
24775
  return { type: "text", text: `Already using **${match.name}**.` };
24272
24776
  }
24273
- if (session.middlewareChain) {
24274
- const result = await session.middlewareChain.execute("config:beforeChange", {
24275
- sessionId: session.id,
24276
- configId: configOption.id,
24277
- oldValue: configOption.currentValue,
24278
- newValue: raw
24279
- }, async (p2) => p2);
24280
- if (!result) return { type: "error", message: `This change was blocked by a plugin.` };
24281
- }
24282
24777
  try {
24283
- const response = await session.agentInstance.setConfigOption(
24284
- configOption.id,
24285
- { type: "select", value: raw }
24286
- );
24287
- if (response.configOptions) {
24288
- session.configOptions = response.configOptions;
24289
- }
24778
+ await session.setConfigOption(configOption.id, { type: "select", value: raw });
24290
24779
  core.eventBus.emit("session:configChanged", { sessionId: session.id });
24291
24780
  return { type: "text", text: labels.successMsg(match.name, configOption.name) };
24292
24781
  } catch (err) {
24293
- const msg = err instanceof Error ? err.message : String(err);
24782
+ log41.error({ err, commandName, configId: configOption.id }, "setConfigOption failed");
24783
+ const msg = err instanceof Error ? err.message : typeof err === "object" && err !== null && typeof err.message === "string" ? err.message : String(err);
24294
24784
  return { type: "error", message: `Could not change ${commandName}: ${msg}` };
24295
24785
  }
24296
24786
  }
@@ -24347,20 +24837,15 @@ function registerDangerousCommand(registry, core) {
24347
24837
  if (bypassValue && modeConfig) {
24348
24838
  try {
24349
24839
  const targetValue = wantOn ? bypassValue : nonBypassDefault;
24350
- const response = await session.agentInstance.setConfigOption(
24351
- modeConfig.id,
24352
- { type: "select", value: targetValue }
24353
- );
24354
- if (response.configOptions) {
24355
- session.configOptions = response.configOptions;
24356
- }
24840
+ await session.setConfigOption(modeConfig.id, { type: "select", value: targetValue });
24357
24841
  core.eventBus.emit("session:configChanged", { sessionId: session.id });
24358
24842
  return {
24359
24843
  type: "text",
24360
24844
  text: wantOn ? "\u2620\uFE0F **Bypass Permissions enabled** \u2014 all permission requests will be auto-approved. The agent can run any action without asking." : "\u{1F510} **Bypass Permissions disabled** \u2014 you will be asked to approve risky actions."
24361
24845
  };
24362
24846
  } catch (err) {
24363
- const msg = err instanceof Error ? err.message : String(err);
24847
+ log41.error({ err }, "setConfigOption failed (bypass toggle)");
24848
+ const msg = err instanceof Error ? err.message : typeof err === "object" && err !== null && typeof err.message === "string" ? err.message : String(err);
24364
24849
  return { type: "error", message: `Could not toggle bypass: ${msg}` };
24365
24850
  }
24366
24851
  }
@@ -24383,12 +24868,14 @@ function registerConfigCommands(registry, _core) {
24383
24868
  registerCategoryCommand(registry, core, "thought_level", "thought");
24384
24869
  registerDangerousCommand(registry, core);
24385
24870
  }
24386
- var CATEGORY_LABELS;
24871
+ var log41, CATEGORY_LABELS;
24387
24872
  var init_config6 = __esm({
24388
24873
  "src/core/commands/config.ts"() {
24389
24874
  "use strict";
24875
+ init_log();
24390
24876
  init_bypass_detection();
24391
24877
  init_bypass_detection();
24878
+ log41 = createChildLogger({ module: "commands/config" });
24392
24879
  CATEGORY_LABELS = {
24393
24880
  mode: {
24394
24881
  menuTitle: (cur) => `Choose session mode (current: ${cur})`,
@@ -24781,7 +25268,7 @@ function installAutoStart(logDir2) {
24781
25268
  fs46.mkdirSync(dir, { recursive: true });
24782
25269
  fs46.writeFileSync(LAUNCHD_PLIST_PATH, plist);
24783
25270
  execFileSync7("launchctl", ["load", LAUNCHD_PLIST_PATH], { stdio: "pipe" });
24784
- log41.info("LaunchAgent installed");
25271
+ log42.info("LaunchAgent installed");
24785
25272
  return { success: true };
24786
25273
  }
24787
25274
  if (process.platform === "linux") {
@@ -24791,13 +25278,13 @@ function installAutoStart(logDir2) {
24791
25278
  fs46.writeFileSync(SYSTEMD_SERVICE_PATH, unit);
24792
25279
  execFileSync7("systemctl", ["--user", "daemon-reload"], { stdio: "pipe" });
24793
25280
  execFileSync7("systemctl", ["--user", "enable", "openacp"], { stdio: "pipe" });
24794
- log41.info("systemd user service installed");
25281
+ log42.info("systemd user service installed");
24795
25282
  return { success: true };
24796
25283
  }
24797
25284
  return { success: false, error: "Unsupported platform" };
24798
25285
  } catch (e) {
24799
25286
  const msg = e.message;
24800
- log41.error({ err: msg }, "Failed to install auto-start");
25287
+ log42.error({ err: msg }, "Failed to install auto-start");
24801
25288
  return { success: false, error: msg };
24802
25289
  }
24803
25290
  }
@@ -24813,7 +25300,7 @@ function uninstallAutoStart() {
24813
25300
  } catch {
24814
25301
  }
24815
25302
  fs46.unlinkSync(LAUNCHD_PLIST_PATH);
24816
- log41.info("LaunchAgent removed");
25303
+ log42.info("LaunchAgent removed");
24817
25304
  }
24818
25305
  return { success: true };
24819
25306
  }
@@ -24825,14 +25312,14 @@ function uninstallAutoStart() {
24825
25312
  }
24826
25313
  fs46.unlinkSync(SYSTEMD_SERVICE_PATH);
24827
25314
  execFileSync7("systemctl", ["--user", "daemon-reload"], { stdio: "pipe" });
24828
- log41.info("systemd user service removed");
25315
+ log42.info("systemd user service removed");
24829
25316
  }
24830
25317
  return { success: true };
24831
25318
  }
24832
25319
  return { success: false, error: "Unsupported platform" };
24833
25320
  } catch (e) {
24834
25321
  const msg = e.message;
24835
- log41.error({ err: msg }, "Failed to uninstall auto-start");
25322
+ log42.error({ err: msg }, "Failed to uninstall auto-start");
24836
25323
  return { success: false, error: msg };
24837
25324
  }
24838
25325
  }
@@ -24845,12 +25332,12 @@ function isAutoStartInstalled() {
24845
25332
  }
24846
25333
  return false;
24847
25334
  }
24848
- var log41, LAUNCHD_LABEL, LAUNCHD_PLIST_PATH, SYSTEMD_SERVICE_PATH;
25335
+ var log42, LAUNCHD_LABEL, LAUNCHD_PLIST_PATH, SYSTEMD_SERVICE_PATH;
24849
25336
  var init_autostart = __esm({
24850
25337
  "src/cli/autostart.ts"() {
24851
25338
  "use strict";
24852
25339
  init_log();
24853
- log41 = createChildLogger({ module: "autostart" });
25340
+ log42 = createChildLogger({ module: "autostart" });
24854
25341
  LAUNCHD_LABEL = "com.openacp.daemon";
24855
25342
  LAUNCHD_PLIST_PATH = path53.join(os24.homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
24856
25343
  SYSTEMD_SERVICE_PATH = path53.join(os24.homedir(), ".config", "systemd", "user", "openacp.service");
@@ -24953,7 +25440,7 @@ async function setupIntegrations() {
24953
25440
  if (integration) {
24954
25441
  for (const item of integration.items) {
24955
25442
  const result = await item.install();
24956
- for (const log44 of result.logs) console.log(` ${log44}`);
25443
+ for (const log45 of result.logs) console.log(` ${log45}`);
24957
25444
  }
24958
25445
  }
24959
25446
  console.log("Claude CLI integration installed.\n");
@@ -25911,14 +26398,14 @@ async function runPostUpgradeChecks(config) {
25911
26398
  const { ensureCloudflared: ensureCloudflared2 } = await Promise.resolve().then(() => (init_install_cloudflared(), install_cloudflared_exports));
25912
26399
  await ensureCloudflared2();
25913
26400
  } catch (err) {
25914
- log43.warn(
26401
+ log44.warn(
25915
26402
  { err: err.message },
25916
26403
  "Could not install cloudflared. Tunnel may not work."
25917
26404
  );
25918
26405
  }
25919
26406
  } else {
25920
26407
  if (!commandExists(config.tunnel.provider)) {
25921
- log43.warn(
26408
+ log44.warn(
25922
26409
  `Tunnel provider "${config.tunnel.provider}" is not installed. Install it or switch to cloudflare (free, auto-installed).`
25923
26410
  );
25924
26411
  }
@@ -25930,7 +26417,7 @@ async function runPostUpgradeChecks(config) {
25930
26417
  if (integration) {
25931
26418
  const allInstalled = integration.items.every((item) => item.isInstalled());
25932
26419
  if (!allInstalled) {
25933
- log43.info(
26420
+ log44.info(
25934
26421
  'Claude CLI integration not installed. Run "openacp integrate claude" for session transfer + tunnel skill.'
25935
26422
  );
25936
26423
  }
@@ -25940,7 +26427,7 @@ async function runPostUpgradeChecks(config) {
25940
26427
  const { ensureJq: ensureJq2 } = await Promise.resolve().then(() => (init_install_jq(), install_jq_exports));
25941
26428
  await ensureJq2();
25942
26429
  } catch (err) {
25943
- log43.warn(
26430
+ log44.warn(
25944
26431
  { err: err.message },
25945
26432
  "Could not install jq. Handoff hooks may not work."
25946
26433
  );
@@ -25950,7 +26437,7 @@ async function runPostUpgradeChecks(config) {
25950
26437
  } catch {
25951
26438
  }
25952
26439
  if (!commandExists("unzip")) {
25953
- log43.warn(
26440
+ log44.warn(
25954
26441
  "unzip is not installed. Some agent installations (binary distribution) may fail. Install: brew install unzip (macOS) or apt install unzip (Linux)"
25955
26442
  );
25956
26443
  }
@@ -25963,20 +26450,20 @@ async function runPostUpgradeChecks(config) {
25963
26450
  (a) => a.distribution === "uvx"
25964
26451
  );
25965
26452
  if (hasUvxAgent && !commandExists("uvx")) {
25966
- log43.warn(
26453
+ log44.warn(
25967
26454
  "uvx is not installed but you have Python-based agents. Install: pip install uv"
25968
26455
  );
25969
26456
  }
25970
26457
  } catch {
25971
26458
  }
25972
26459
  }
25973
- var log43;
26460
+ var log44;
25974
26461
  var init_post_upgrade = __esm({
25975
26462
  "src/cli/post-upgrade.ts"() {
25976
26463
  "use strict";
25977
26464
  init_log();
25978
26465
  init_agent_dependencies();
25979
- log43 = createChildLogger({ module: "post-upgrade" });
26466
+ log44 = createChildLogger({ module: "post-upgrade" });
25980
26467
  }
25981
26468
  });
25982
26469
 
@@ -26340,7 +26827,10 @@ async function startServer(opts) {
26340
26827
  const tunnelErr = tunnelSvc.getStartError();
26341
26828
  const url = tunnelSvc.getPublicUrl();
26342
26829
  const isPublic = url && !url.startsWith("http://localhost") && !url.startsWith("http://127.0.0.1");
26343
- if (tunnelErr) {
26830
+ if (tunnelErr && isPublic) {
26831
+ warn3(`Primary tunnel failed \u2014 using fallback: ${tunnelErr}`);
26832
+ tunnelUrl = url;
26833
+ } else if (tunnelErr) {
26344
26834
  warn3(`Tunnel failed (${tunnelErr}) \u2014 retrying in background`);
26345
26835
  } else if (isPublic) {
26346
26836
  ok3("Tunnel ready");
@@ -29711,7 +30201,7 @@ a "Handoff" slash command to Claude Code.
29711
30201
  if (uninstall) {
29712
30202
  console.log(`Removing ${agent}/${item.id}...`);
29713
30203
  const result = await item.uninstall();
29714
- for (const log44 of result.logs) console.log(` ${log44}`);
30204
+ for (const log45 of result.logs) console.log(` ${log45}`);
29715
30205
  if (result.success) {
29716
30206
  console.log(` ${item.name} removed.`);
29717
30207
  } else {
@@ -29721,7 +30211,7 @@ a "Handoff" slash command to Claude Code.
29721
30211
  } else {
29722
30212
  console.log(`Installing ${agent}/${item.id}...`);
29723
30213
  const result = await item.install();
29724
- for (const log44 of result.logs) console.log(` ${log44}`);
30214
+ for (const log45 of result.logs) console.log(` ${log45}`);
29725
30215
  if (result.success) {
29726
30216
  console.log(` ${item.name} installed.`);
29727
30217
  } else {