@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/index.js CHANGED
@@ -8362,7 +8362,7 @@ var init_commands = __esm({
8362
8362
 
8363
8363
  // src/plugins/telegram/permissions.ts
8364
8364
  import { InlineKeyboard as InlineKeyboard11 } from "grammy";
8365
- import { nanoid as nanoid2 } from "nanoid";
8365
+ import { nanoid as nanoid3 } from "nanoid";
8366
8366
  var log30, PermissionHandler;
8367
8367
  var init_permissions = __esm({
8368
8368
  "src/plugins/telegram/permissions.ts"() {
@@ -8381,7 +8381,7 @@ var init_permissions = __esm({
8381
8381
  pending = /* @__PURE__ */ new Map();
8382
8382
  async sendPermissionRequest(session, request) {
8383
8383
  const threadId = Number(session.threadId);
8384
- const callbackKey = nanoid2(8);
8384
+ const callbackKey = nanoid3(8);
8385
8385
  this.pending.set(callbackKey, {
8386
8386
  sessionId: session.id,
8387
8387
  requestId: request.id,
@@ -11182,6 +11182,7 @@ var AgentInstance = class _AgentInstance extends TypedEmitter {
11182
11182
  { sessionId: this.sessionId, exitCode: code, signal },
11183
11183
  "Agent process exited"
11184
11184
  );
11185
+ if (signal === "SIGINT" || signal === "SIGTERM") return;
11185
11186
  if (code !== 0 && code !== null || signal) {
11186
11187
  const stderr = this.stderrCapture.getLastLines();
11187
11188
  this.emit("agent_event", {
@@ -11657,7 +11658,7 @@ var AgentManager = class {
11657
11658
  };
11658
11659
 
11659
11660
  // src/core/sessions/session.ts
11660
- import { nanoid } from "nanoid";
11661
+ import { nanoid as nanoid2 } from "nanoid";
11661
11662
 
11662
11663
  // src/core/sessions/prompt-queue.ts
11663
11664
  var PromptQueue = class {
@@ -11668,21 +11669,21 @@ var PromptQueue = class {
11668
11669
  queue = [];
11669
11670
  processing = false;
11670
11671
  abortController = null;
11671
- async enqueue(text3, attachments) {
11672
+ async enqueue(text3, attachments, routing) {
11672
11673
  if (this.processing) {
11673
11674
  return new Promise((resolve6) => {
11674
- this.queue.push({ text: text3, attachments, resolve: resolve6 });
11675
+ this.queue.push({ text: text3, attachments, routing, resolve: resolve6 });
11675
11676
  });
11676
11677
  }
11677
- await this.process(text3, attachments);
11678
+ await this.process(text3, attachments, routing);
11678
11679
  }
11679
- async process(text3, attachments) {
11680
+ async process(text3, attachments, routing) {
11680
11681
  this.processing = true;
11681
11682
  this.abortController = new AbortController();
11682
11683
  const { signal } = this.abortController;
11683
11684
  try {
11684
11685
  await Promise.race([
11685
- this.processor(text3, attachments),
11686
+ this.processor(text3, attachments, routing),
11686
11687
  new Promise((_, reject) => {
11687
11688
  signal.addEventListener("abort", () => reject(new Error("Prompt aborted")), { once: true });
11688
11689
  })
@@ -11700,7 +11701,7 @@ var PromptQueue = class {
11700
11701
  drainNext() {
11701
11702
  const next = this.queue.shift();
11702
11703
  if (next) {
11703
- this.process(next.text, next.attachments).then(next.resolve);
11704
+ this.process(next.text, next.attachments, next.routing).then(next.resolve);
11704
11705
  }
11705
11706
  }
11706
11707
  clear() {
@@ -11790,6 +11791,33 @@ var PermissionGate = class {
11790
11791
  // src/core/sessions/session.ts
11791
11792
  init_log();
11792
11793
  import * as fs10 from "fs";
11794
+
11795
+ // src/core/sessions/turn-context.ts
11796
+ import { nanoid } from "nanoid";
11797
+ function createTurnContext(sourceAdapterId, responseAdapterId) {
11798
+ return {
11799
+ turnId: nanoid(8),
11800
+ sourceAdapterId,
11801
+ responseAdapterId
11802
+ };
11803
+ }
11804
+ function getEffectiveTarget(ctx) {
11805
+ if (ctx.responseAdapterId === null) return null;
11806
+ return ctx.responseAdapterId ?? ctx.sourceAdapterId;
11807
+ }
11808
+ var SYSTEM_EVENT_TYPES = /* @__PURE__ */ new Set([
11809
+ "session_end",
11810
+ "system_message",
11811
+ "session_info_update",
11812
+ "config_option_update",
11813
+ "commands_update",
11814
+ "tts_strip"
11815
+ ]);
11816
+ function isSystemEvent(event) {
11817
+ return SYSTEM_EVENT_TYPES.has(event.type);
11818
+ }
11819
+
11820
+ // src/core/sessions/session.ts
11793
11821
  var moduleLog = createChildLogger({ module: "session" });
11794
11822
  var TTS_PROMPT_INSTRUCTION = `
11795
11823
 
@@ -11807,7 +11835,15 @@ var VALID_TRANSITIONS = {
11807
11835
  var Session = class extends TypedEmitter {
11808
11836
  id;
11809
11837
  channelId;
11810
- threadId = "";
11838
+ /** @deprecated Use threadIds map directly. Getter returns primary adapter's threadId. */
11839
+ get threadId() {
11840
+ return this.threadIds.get(this.channelId) ?? "";
11841
+ }
11842
+ set threadId(value) {
11843
+ if (value) {
11844
+ this.threadIds.set(this.channelId, value);
11845
+ }
11846
+ }
11811
11847
  agentName;
11812
11848
  workingDirectory;
11813
11849
  agentInstance;
@@ -11828,14 +11864,21 @@ var Session = class extends TypedEmitter {
11828
11864
  middlewareChain;
11829
11865
  /** Latest commands emitted by the agent — buffered before bridge connects so they're not lost */
11830
11866
  latestCommands = null;
11867
+ /** Adapters currently attached to this session (including primary) */
11868
+ attachedAdapters = [];
11869
+ /** Per-adapter thread IDs: adapterId → threadId */
11870
+ threadIds = /* @__PURE__ */ new Map();
11871
+ /** Active turn context — sealed on prompt dequeue, cleared on turn end */
11872
+ activeTurnContext = null;
11831
11873
  permissionGate = new PermissionGate();
11832
11874
  queue;
11833
11875
  speechService;
11834
11876
  pendingContext = null;
11835
11877
  constructor(opts) {
11836
11878
  super();
11837
- this.id = opts.id || nanoid(12);
11879
+ this.id = opts.id || nanoid2(12);
11838
11880
  this.channelId = opts.channelId;
11881
+ this.attachedAdapters = [opts.channelId];
11839
11882
  this.agentName = opts.agentName;
11840
11883
  this.firstAgent = opts.agentName;
11841
11884
  this.workingDirectory = opts.workingDirectory;
@@ -11845,7 +11888,7 @@ var Session = class extends TypedEmitter {
11845
11888
  this.log = createSessionLogger(this.id, moduleLog);
11846
11889
  this.log.info({ agentName: this.agentName }, "Session created");
11847
11890
  this.queue = new PromptQueue(
11848
- (text3, attachments) => this.processPrompt(text3, attachments),
11891
+ (text3, attachments, routing) => this.processPrompt(text3, attachments, routing),
11849
11892
  (err) => {
11850
11893
  this.log.error({ err }, "Prompt execution failed");
11851
11894
  const message = err instanceof Error ? err.message : String(err);
@@ -11853,11 +11896,20 @@ var Session = class extends TypedEmitter {
11853
11896
  this.emit("agent_event", { type: "error", message: `Prompt execution failed: ${message}` });
11854
11897
  }
11855
11898
  );
11856
- this.agentInstance.on("agent_event", (event) => {
11899
+ this.wireCommandsBuffer();
11900
+ }
11901
+ /** Wire a listener on the current agentInstance to buffer commands_update events.
11902
+ * Must be called after every agentInstance replacement (constructor + switchAgent). */
11903
+ commandsBufferCleanup;
11904
+ wireCommandsBuffer() {
11905
+ this.commandsBufferCleanup?.();
11906
+ const handler = (event) => {
11857
11907
  if (event.type === "commands_update") {
11858
11908
  this.latestCommands = event.commands;
11859
11909
  }
11860
- });
11910
+ };
11911
+ this.agentInstance.on("agent_event", handler);
11912
+ this.commandsBufferCleanup = () => this.agentInstance.off("agent_event", handler);
11861
11913
  }
11862
11914
  // --- State Machine ---
11863
11915
  get status() {
@@ -11911,7 +11963,7 @@ var Session = class extends TypedEmitter {
11911
11963
  this.log.info({ voiceMode: mode }, "TTS mode changed");
11912
11964
  }
11913
11965
  // --- Public API ---
11914
- async enqueuePrompt(text3, attachments) {
11966
+ async enqueuePrompt(text3, attachments, routing) {
11915
11967
  if (this.middlewareChain) {
11916
11968
  const payload = { text: text3, attachments, sessionId: this.id };
11917
11969
  const result = await this.middlewareChain.execute("agent:beforePrompt", payload, async (p) => p);
@@ -11919,10 +11971,14 @@ var Session = class extends TypedEmitter {
11919
11971
  text3 = result.text;
11920
11972
  attachments = result.attachments;
11921
11973
  }
11922
- await this.queue.enqueue(text3, attachments);
11974
+ await this.queue.enqueue(text3, attachments, routing);
11923
11975
  }
11924
- async processPrompt(text3, attachments) {
11976
+ async processPrompt(text3, attachments, routing) {
11925
11977
  if (this._status === "finished") return;
11978
+ this.activeTurnContext = createTurnContext(
11979
+ routing?.sourceAdapterId ?? this.channelId,
11980
+ routing?.responseAdapterId
11981
+ );
11926
11982
  this.promptCount++;
11927
11983
  this.emit("prompt_count_changed", this.promptCount);
11928
11984
  if (this._status === "initializing" || this._status === "cancelled" || this._status === "error") {
@@ -11989,6 +12045,7 @@ ${text3}`;
11989
12045
  this.log.warn({ err }, "TTS post-processing failed");
11990
12046
  });
11991
12047
  }
12048
+ this.activeTurnContext = null;
11992
12049
  if (!this.name) {
11993
12050
  await this.autoName();
11994
12051
  }
@@ -12231,6 +12288,7 @@ ${result.text}` : result.text;
12231
12288
  this.configOptions = [];
12232
12289
  this.latestCommands = null;
12233
12290
  this.applySpawnResponse(newAgent.initialSessionResponse, newAgent.agentCapabilities);
12291
+ this.wireCommandsBuffer();
12234
12292
  this.log.info({ from: this.agentSwitchHistory.at(-1).agentName, to: agentName }, "Agent switched");
12235
12293
  }
12236
12294
  async destroy() {
@@ -12698,6 +12756,8 @@ var SessionManager = class {
12698
12756
  }
12699
12757
  getSessionByThread(channelId, threadId) {
12700
12758
  for (const session of this.sessions.values()) {
12759
+ const adapterThread = session.threadIds.get(channelId);
12760
+ if (adapterThread === threadId) return session;
12701
12761
  if (session.channelId === channelId && session.threadId === threadId) {
12702
12762
  return session;
12703
12763
  }
@@ -12765,6 +12825,67 @@ var SessionManager = class {
12765
12825
  if (channelId) return all.filter((s) => s.channelId === channelId);
12766
12826
  return all;
12767
12827
  }
12828
+ listAllSessions(channelId) {
12829
+ if (this.store) {
12830
+ let records = this.store.list();
12831
+ if (channelId) records = records.filter((r) => r.channelId === channelId);
12832
+ return records.map((record) => {
12833
+ const live2 = this.sessions.get(record.sessionId);
12834
+ if (live2) {
12835
+ return {
12836
+ id: live2.id,
12837
+ agent: live2.agentName,
12838
+ status: live2.status,
12839
+ name: live2.name ?? null,
12840
+ workspace: live2.workingDirectory,
12841
+ channelId: live2.channelId,
12842
+ createdAt: live2.createdAt.toISOString(),
12843
+ lastActiveAt: record.lastActiveAt ?? null,
12844
+ dangerousMode: live2.clientOverrides.bypassPermissions ?? false,
12845
+ queueDepth: live2.queueDepth,
12846
+ promptRunning: live2.promptRunning,
12847
+ configOptions: live2.configOptions?.length ? live2.configOptions : void 0,
12848
+ capabilities: live2.agentCapabilities ?? null,
12849
+ isLive: true
12850
+ };
12851
+ }
12852
+ return {
12853
+ id: record.sessionId,
12854
+ agent: record.agentName,
12855
+ status: record.status,
12856
+ name: record.name ?? null,
12857
+ workspace: record.workingDir,
12858
+ channelId: record.channelId,
12859
+ createdAt: record.createdAt,
12860
+ lastActiveAt: record.lastActiveAt ?? null,
12861
+ dangerousMode: record.clientOverrides?.bypassPermissions ?? false,
12862
+ queueDepth: 0,
12863
+ promptRunning: false,
12864
+ configOptions: record.acpState?.configOptions,
12865
+ capabilities: record.acpState?.agentCapabilities ?? null,
12866
+ isLive: false
12867
+ };
12868
+ });
12869
+ }
12870
+ let live = Array.from(this.sessions.values());
12871
+ if (channelId) live = live.filter((s) => s.channelId === channelId);
12872
+ return live.map((s) => ({
12873
+ id: s.id,
12874
+ agent: s.agentName,
12875
+ status: s.status,
12876
+ name: s.name ?? null,
12877
+ workspace: s.workingDirectory,
12878
+ channelId: s.channelId,
12879
+ createdAt: s.createdAt.toISOString(),
12880
+ lastActiveAt: null,
12881
+ dangerousMode: s.clientOverrides.bypassPermissions ?? false,
12882
+ queueDepth: s.queueDepth,
12883
+ promptRunning: s.promptRunning,
12884
+ configOptions: s.configOptions?.length ? s.configOptions : void 0,
12885
+ capabilities: s.agentCapabilities ?? null,
12886
+ isLive: true
12887
+ }));
12888
+ }
12768
12889
  listRecords(filter) {
12769
12890
  if (!this.store) return [];
12770
12891
  let records = this.store.list();
@@ -12787,7 +12908,14 @@ var SessionManager = class {
12787
12908
  for (const session of this.sessions.values()) {
12788
12909
  const record = this.store.get(session.id);
12789
12910
  if (record) {
12790
- await this.store.save({ ...record, status: "finished" });
12911
+ await this.store.save({
12912
+ ...record,
12913
+ status: "finished",
12914
+ acpState: session.toAcpStateSnapshot(),
12915
+ clientOverrides: session.clientOverrides,
12916
+ currentPromptCount: session.promptCount,
12917
+ agentSwitchHistory: session.agentSwitchHistory
12918
+ });
12791
12919
  }
12792
12920
  }
12793
12921
  this.store.flush();
@@ -12797,6 +12925,8 @@ var SessionManager = class {
12797
12925
  /**
12798
12926
  * Forcefully destroy all sessions (kill agent subprocesses).
12799
12927
  * Use only when sessions must be fully torn down (e.g. archive).
12928
+ * Unlike shutdownAll(), this does NOT snapshot live session state (acpState, etc.)
12929
+ * because destroyed sessions are terminal and will not be resumed.
12800
12930
  */
12801
12931
  async destroyAll() {
12802
12932
  if (this.store) {
@@ -12827,13 +12957,15 @@ init_log();
12827
12957
  init_bypass_detection();
12828
12958
  var log6 = createChildLogger({ module: "session-bridge" });
12829
12959
  var SessionBridge = class {
12830
- constructor(session, adapter, deps) {
12960
+ constructor(session, adapter, deps, adapterId) {
12831
12961
  this.session = session;
12832
12962
  this.adapter = adapter;
12833
12963
  this.deps = deps;
12964
+ this.adapterId = adapterId ?? adapter.name;
12834
12965
  }
12835
12966
  connected = false;
12836
12967
  cleanupFns = [];
12968
+ adapterId;
12837
12969
  get tracer() {
12838
12970
  return this.session.agentInstance.debugTracer ?? null;
12839
12971
  }
@@ -12864,6 +12996,15 @@ var SessionBridge = class {
12864
12996
  log6.error({ err, sessionId }, "Error in sendMessage middleware");
12865
12997
  }
12866
12998
  }
12999
+ /** Determine if this bridge should forward the given event based on turn routing. */
13000
+ shouldForward(event) {
13001
+ if (isSystemEvent(event)) return true;
13002
+ const ctx = this.session.activeTurnContext;
13003
+ if (!ctx) return true;
13004
+ const target = getEffectiveTarget(ctx);
13005
+ if (target === null) return false;
13006
+ return this.adapterId === target;
13007
+ }
12867
13008
  connect() {
12868
13009
  if (this.connected) return;
12869
13010
  this.connected = true;
@@ -12871,11 +13012,29 @@ var SessionBridge = class {
12871
13012
  this.session.emit("agent_event", event);
12872
13013
  });
12873
13014
  this.listen(this.session, "agent_event", (event) => {
12874
- this.dispatchAgentEvent(event);
13015
+ if (this.shouldForward(event)) {
13016
+ this.dispatchAgentEvent(event);
13017
+ } else {
13018
+ this.deps.eventBus?.emit("agent:event", { sessionId: this.session.id, event });
13019
+ }
13020
+ });
13021
+ if (!this.session.agentInstance.onPermissionRequest || this.session.agentInstance.onPermissionRequest.__bridgeId === void 0) {
13022
+ const handler = async (request) => {
13023
+ return this.resolvePermission(request);
13024
+ };
13025
+ handler.__bridgeId = this.adapterId;
13026
+ this.session.agentInstance.onPermissionRequest = handler;
13027
+ }
13028
+ this.listen(this.session, "permission_request", async (request) => {
13029
+ const current = this.session.agentInstance.onPermissionRequest;
13030
+ if (current?.__bridgeId === this.adapterId) return;
13031
+ if (!this.session.permissionGate.isPending) return;
13032
+ try {
13033
+ await this.adapter.sendPermissionRequest(this.session.id, request);
13034
+ } catch (err) {
13035
+ log6.error({ err, sessionId: this.session.id, adapterId: this.adapterId }, "Failed to send permission request to adapter");
13036
+ }
12875
13037
  });
12876
- this.session.agentInstance.onPermissionRequest = async (request) => {
12877
- return this.resolvePermission(request);
12878
- };
12879
13038
  this.listen(this.session, "status_change", (from, to) => {
12880
13039
  this.deps.sessionManager.patchRecord(this.session.id, {
12881
13040
  status: to,
@@ -12916,7 +13075,10 @@ var SessionBridge = class {
12916
13075
  this.connected = false;
12917
13076
  this.cleanupFns.forEach((fn) => fn());
12918
13077
  this.cleanupFns = [];
12919
- this.session.agentInstance.onPermissionRequest = async () => "";
13078
+ const current = this.session.agentInstance.onPermissionRequest;
13079
+ if (current?.__bridgeId === this.adapterId) {
13080
+ this.session.agentInstance.onPermissionRequest = async () => "";
13081
+ }
12920
13082
  }
12921
13083
  /** Dispatch an agent event through middleware and to the adapter */
12922
13084
  async dispatchAgentEvent(event) {
@@ -13047,8 +13209,10 @@ var SessionBridge = class {
13047
13209
  this.sendMessage(this.session.id, outgoing);
13048
13210
  break;
13049
13211
  case "config_option_update":
13050
- this.session.updateConfigOptions(event.options);
13051
- this.persistAcpState();
13212
+ this.session.updateConfigOptions(event.options).then(() => {
13213
+ this.persistAcpState();
13214
+ }).catch(() => {
13215
+ });
13052
13216
  outgoing = this.deps.messageTransformer.transform(event);
13053
13217
  this.sendMessage(this.session.id, outgoing);
13054
13218
  break;
@@ -13092,19 +13256,27 @@ var SessionBridge = class {
13092
13256
  return result.autoResolve;
13093
13257
  }
13094
13258
  }
13095
- this.session.emit("permission_request", permReq);
13096
13259
  this.deps.eventBus?.emit("permission:request", {
13097
13260
  sessionId: this.session.id,
13098
13261
  permission: permReq
13099
13262
  });
13100
13263
  const autoDecision = this.checkAutoApprove(permReq);
13101
13264
  if (autoDecision) {
13265
+ this.session.emit("permission_request", permReq);
13102
13266
  this.emitAfterResolve(mw, permReq.id, autoDecision, "system", startTime);
13103
13267
  return autoDecision;
13104
13268
  }
13105
13269
  const promise = this.session.permissionGate.setPending(permReq);
13270
+ this.session.emit("permission_request", permReq);
13106
13271
  await this.adapter.sendPermissionRequest(this.session.id, permReq);
13107
13272
  const optionId = await promise;
13273
+ this.deps.eventBus?.emit("permission:resolved", {
13274
+ sessionId: this.session.id,
13275
+ requestId: permReq.id,
13276
+ decision: optionId,
13277
+ optionId,
13278
+ resolvedBy: this.adapterId
13279
+ });
13108
13280
  this.emitAfterResolve(mw, permReq.id, optionId, "user", startTime);
13109
13281
  return optionId;
13110
13282
  }
@@ -13298,6 +13470,65 @@ var SessionFactory = class {
13298
13470
  if (session) return session;
13299
13471
  return this.lazyResume(channelId, threadId);
13300
13472
  }
13473
+ async getOrResumeById(sessionId) {
13474
+ const live = this.sessionManager.getSession(sessionId);
13475
+ if (live) return live;
13476
+ if (!this.sessionStore || !this.createFullSession) return null;
13477
+ const record = this.sessionStore.get(sessionId);
13478
+ if (!record) return null;
13479
+ if (record.status === "error" || record.status === "cancelled") return null;
13480
+ const existing = this.resumeLocks.get(sessionId);
13481
+ if (existing) return existing;
13482
+ const resumePromise = (async () => {
13483
+ try {
13484
+ const p = record.platform;
13485
+ const existingThreadId = p?.topicId ? String(p.topicId) : p?.threadId;
13486
+ const session = await this.createFullSession({
13487
+ channelId: record.channelId,
13488
+ agentName: record.agentName,
13489
+ workingDirectory: record.workingDir,
13490
+ resumeAgentSessionId: record.agentSessionId,
13491
+ existingSessionId: record.sessionId,
13492
+ initialName: record.name,
13493
+ threadId: existingThreadId
13494
+ });
13495
+ session.activate();
13496
+ if (record.clientOverrides) {
13497
+ session.clientOverrides = record.clientOverrides;
13498
+ } else if (record.dangerousMode) {
13499
+ session.clientOverrides = { bypassPermissions: true };
13500
+ }
13501
+ if (record.firstAgent) session.firstAgent = record.firstAgent;
13502
+ if (record.agentSwitchHistory) session.agentSwitchHistory = record.agentSwitchHistory;
13503
+ if (record.currentPromptCount != null) session.promptCount = record.currentPromptCount;
13504
+ if (record.attachedAdapters) session.attachedAdapters = record.attachedAdapters;
13505
+ if (record.platforms) {
13506
+ for (const [adapterId, platformData] of Object.entries(record.platforms)) {
13507
+ const data = platformData;
13508
+ const tid = adapterId === "telegram" ? String(data.topicId ?? "") : String(data.threadId ?? "");
13509
+ if (tid) session.threadIds.set(adapterId, tid);
13510
+ }
13511
+ }
13512
+ if (record.acpState) {
13513
+ if (record.acpState.configOptions && session.configOptions.length === 0) {
13514
+ session.setInitialConfigOptions(record.acpState.configOptions);
13515
+ }
13516
+ if (record.acpState.agentCapabilities && !session.agentCapabilities) {
13517
+ session.setAgentCapabilities(record.acpState.agentCapabilities);
13518
+ }
13519
+ }
13520
+ log7.info({ sessionId }, "Lazy resume by ID successful");
13521
+ return session;
13522
+ } catch (err) {
13523
+ log7.error({ err, sessionId }, "Lazy resume by ID failed");
13524
+ return null;
13525
+ } finally {
13526
+ this.resumeLocks.delete(sessionId);
13527
+ }
13528
+ })();
13529
+ this.resumeLocks.set(sessionId, resumePromise);
13530
+ return resumePromise;
13531
+ }
13301
13532
  async lazyResume(channelId, threadId) {
13302
13533
  const store = this.sessionStore;
13303
13534
  if (!store || !this.createFullSession) return null;
@@ -13306,7 +13537,7 @@ var SessionFactory = class {
13306
13537
  if (existing) return existing;
13307
13538
  const record = store.findByPlatform(
13308
13539
  channelId,
13309
- (p) => String(p.topicId) === threadId
13540
+ (p) => String(p.topicId) === threadId || String(p.threadId ?? "") === threadId
13310
13541
  );
13311
13542
  if (!record) {
13312
13543
  log7.debug({ threadId, channelId }, "No session record found for thread");
@@ -13341,11 +13572,21 @@ var SessionFactory = class {
13341
13572
  if (record.firstAgent) session.firstAgent = record.firstAgent;
13342
13573
  if (record.agentSwitchHistory) session.agentSwitchHistory = record.agentSwitchHistory;
13343
13574
  if (record.currentPromptCount != null) session.promptCount = record.currentPromptCount;
13575
+ if (record.attachedAdapters) {
13576
+ session.attachedAdapters = record.attachedAdapters;
13577
+ }
13578
+ if (record.platforms) {
13579
+ for (const [adapterId, platformData] of Object.entries(record.platforms)) {
13580
+ const data = platformData;
13581
+ const tid = adapterId === "telegram" ? String(data.topicId ?? "") : String(data.threadId ?? "");
13582
+ if (tid) session.threadIds.set(adapterId, tid);
13583
+ }
13584
+ }
13344
13585
  if (record.acpState) {
13345
- if (record.acpState.configOptions) {
13586
+ if (record.acpState.configOptions && session.configOptions.length === 0) {
13346
13587
  session.setInitialConfigOptions(record.acpState.configOptions);
13347
13588
  }
13348
- if (record.acpState.agentCapabilities) {
13589
+ if (record.acpState.agentCapabilities && !session.agentCapabilities) {
13349
13590
  session.setAgentCapabilities(record.acpState.agentCapabilities);
13350
13591
  }
13351
13592
  }
@@ -13512,6 +13753,9 @@ var JsonFileSessionStore = class {
13512
13753
  }
13513
13754
  findByPlatform(channelId, predicate) {
13514
13755
  for (const record of this.records.values()) {
13756
+ if (record.platforms?.[channelId]) {
13757
+ if (predicate(record.platforms[channelId])) return record;
13758
+ }
13515
13759
  if (record.channelId === channelId && predicate(record.platform)) {
13516
13760
  return record;
13517
13761
  }
@@ -13578,7 +13822,7 @@ var JsonFileSessionStore = class {
13578
13822
  return;
13579
13823
  }
13580
13824
  for (const [id, record] of Object.entries(raw.sessions)) {
13581
- this.records.set(id, record);
13825
+ this.records.set(id, this.migrateRecord(record));
13582
13826
  }
13583
13827
  log8.debug({ count: this.records.size }, "Loaded session records");
13584
13828
  } catch (err) {
@@ -13589,6 +13833,19 @@ var JsonFileSessionStore = class {
13589
13833
  }
13590
13834
  }
13591
13835
  }
13836
+ /** Migrate old SessionRecord format to new multi-adapter format. */
13837
+ migrateRecord(record) {
13838
+ if (!record.platforms && record.platform && typeof record.platform === "object") {
13839
+ const platformData = record.platform;
13840
+ if (Object.keys(platformData).length > 0) {
13841
+ record.platforms = { [record.channelId]: platformData };
13842
+ }
13843
+ }
13844
+ if (!record.attachedAdapters) {
13845
+ record.attachedAdapters = [record.channelId];
13846
+ }
13847
+ return record;
13848
+ }
13592
13849
  cleanup() {
13593
13850
  const cutoff = Date.now() - this.ttlDays * 24 * 60 * 60 * 1e3;
13594
13851
  let removed = 0;
@@ -13667,8 +13924,15 @@ var AgentSwitchHandler = class {
13667
13924
  toAgent,
13668
13925
  status: "starting"
13669
13926
  });
13670
- const bridge = bridges.get(sessionId);
13671
- if (bridge) bridge.disconnect();
13927
+ const sessionBridgeKeys = this.deps.getSessionBridgeKeys(sessionId);
13928
+ const hadBridges = sessionBridgeKeys.length > 0;
13929
+ for (const key of sessionBridgeKeys) {
13930
+ const bridge = bridges.get(key);
13931
+ if (bridge) {
13932
+ bridges.delete(key);
13933
+ bridge.disconnect();
13934
+ }
13935
+ }
13672
13936
  const switchAdapter = adapters.get(session.channelId);
13673
13937
  if (switchAdapter?.sendSkillCommands) {
13674
13938
  await switchAdapter.sendSkillCommands(session.id, []);
@@ -13745,9 +14009,11 @@ var AgentSwitchHandler = class {
13745
14009
  session.agentInstance = oldInstance;
13746
14010
  session.agentName = fromAgent;
13747
14011
  session.agentSessionId = oldInstance.sessionId;
13748
- const adapter = adapters.get(session.channelId);
13749
- if (adapter) {
13750
- createBridge(session, adapter).connect();
14012
+ for (const adapterId of session.attachedAdapters) {
14013
+ const adapter = adapters.get(adapterId);
14014
+ if (adapter) {
14015
+ createBridge(session, adapter, adapterId).connect();
14016
+ }
13751
14017
  }
13752
14018
  log9.warn({ sessionId, fromAgent, toAgent, err }, "Agent switch failed, rolled back to previous agent");
13753
14019
  } catch (rollbackErr) {
@@ -13756,10 +14022,14 @@ var AgentSwitchHandler = class {
13756
14022
  }
13757
14023
  throw err;
13758
14024
  }
13759
- if (bridge) {
13760
- const adapter = adapters.get(session.channelId);
13761
- if (adapter) {
13762
- createBridge(session, adapter).connect();
14025
+ if (hadBridges) {
14026
+ for (const adapterId of session.attachedAdapters) {
14027
+ const adapter = adapters.get(adapterId);
14028
+ if (adapter) {
14029
+ createBridge(session, adapter, adapterId).connect();
14030
+ } else {
14031
+ log9.warn({ sessionId, adapterId }, "Adapter not available during switch reconnect, skipping bridge");
14032
+ }
13763
14033
  }
13764
14034
  }
13765
14035
  await sessionManager.patchRecord(sessionId, {
@@ -15212,7 +15482,7 @@ var OpenACPCore = class {
15212
15482
  sessionManager;
15213
15483
  messageTransformer;
15214
15484
  adapters = /* @__PURE__ */ new Map();
15215
- /** sessionId → SessionBridge — tracks active bridges for disconnect/reconnect during agent switch */
15485
+ /** "adapterId:sessionId" → SessionBridge — tracks active bridges for disconnect/reconnect */
15216
15486
  bridges = /* @__PURE__ */ new Map();
15217
15487
  /** Set by main.ts — triggers graceful shutdown with restart exit code */
15218
15488
  requestRestart = null;
@@ -15308,7 +15578,8 @@ var OpenACPCore = class {
15308
15578
  eventBus: this.eventBus,
15309
15579
  adapters: this.adapters,
15310
15580
  bridges: this.bridges,
15311
- createBridge: (session, adapter) => this.createBridge(session, adapter),
15581
+ createBridge: (session, adapter, adapterId) => this.createBridge(session, adapter, adapterId),
15582
+ getSessionBridgeKeys: (sessionId) => this.getSessionBridgeKeys(sessionId),
15312
15583
  getMiddlewareChain: () => this.lifecycleManager?.middlewareChain,
15313
15584
  getService: (name) => this.lifecycleManager.serviceRegistry.get(name)
15314
15585
  });
@@ -15488,7 +15759,7 @@ User message:
15488
15759
  ${text3}`;
15489
15760
  }
15490
15761
  }
15491
- await session.enqueuePrompt(text3, message.attachments);
15762
+ await session.enqueuePrompt(text3, message.attachments, message.routing);
15492
15763
  }
15493
15764
  // --- Unified Session Creation Pipeline ---
15494
15765
  async createSession(params) {
@@ -15515,6 +15786,12 @@ ${text3}`;
15515
15786
  platform2.threadId = session.threadId;
15516
15787
  }
15517
15788
  }
15789
+ const platforms = {
15790
+ ...existingRecord?.platforms ?? {}
15791
+ };
15792
+ if (session.threadId) {
15793
+ platforms[params.channelId] = params.channelId === "telegram" ? { topicId: Number(session.threadId) || session.threadId } : { threadId: session.threadId };
15794
+ }
15518
15795
  await this.sessionManager.patchRecord(session.id, {
15519
15796
  sessionId: session.id,
15520
15797
  agentSessionId: session.agentSessionId,
@@ -15526,6 +15803,7 @@ ${text3}`;
15526
15803
  lastActiveAt: (/* @__PURE__ */ new Date()).toISOString(),
15527
15804
  name: session.name,
15528
15805
  platform: platform2,
15806
+ platforms,
15529
15807
  firstAgent: session.firstAgent,
15530
15808
  currentPromptCount: session.promptCount,
15531
15809
  agentSwitchHistory: session.agentSwitchHistory,
@@ -15533,7 +15811,7 @@ ${text3}`;
15533
15811
  acpState: session.toAcpStateSnapshot()
15534
15812
  }, { immediate: true });
15535
15813
  if (adapter) {
15536
- const bridge = this.createBridge(session, adapter);
15814
+ const bridge = this.createBridge(session, adapter, session.channelId);
15537
15815
  bridge.connect();
15538
15816
  adapter.flushPendingSkillCommands?.(session.id).catch((err) => {
15539
15817
  log16.warn({ err, sessionId: session.id }, "Failed to flush pending skill commands");
@@ -15672,9 +15950,14 @@ ${text3}`;
15672
15950
  } else {
15673
15951
  adoptPlatform.threadId = session.threadId;
15674
15952
  }
15953
+ const adoptPlatforms = {};
15954
+ if (session.threadId) {
15955
+ adoptPlatforms[adapterChannelId] = adapterChannelId === "telegram" ? { topicId: Number(session.threadId) || session.threadId } : { threadId: session.threadId };
15956
+ }
15675
15957
  await this.sessionManager.patchRecord(session.id, {
15676
15958
  originalAgentSessionId: agentSessionId,
15677
- platform: adoptPlatform
15959
+ platform: adoptPlatform,
15960
+ platforms: adoptPlatforms
15678
15961
  });
15679
15962
  return {
15680
15963
  ok: true,
@@ -15696,18 +15979,101 @@ ${text3}`;
15696
15979
  async getOrResumeSession(channelId, threadId) {
15697
15980
  return this.sessionFactory.getOrResume(channelId, threadId);
15698
15981
  }
15982
+ async getOrResumeSessionById(sessionId) {
15983
+ return this.sessionFactory.getOrResumeById(sessionId);
15984
+ }
15985
+ async attachAdapter(sessionId, adapterId) {
15986
+ const session = this.sessionManager.getSession(sessionId);
15987
+ if (!session) throw new Error(`Session ${sessionId} not found`);
15988
+ const adapter = this.adapters.get(adapterId);
15989
+ if (!adapter) throw new Error(`Adapter "${adapterId}" not found or not running`);
15990
+ if (session.attachedAdapters.includes(adapterId)) {
15991
+ const existingThread = session.threadIds.get(adapterId) ?? session.id;
15992
+ return { threadId: existingThread };
15993
+ }
15994
+ const threadId = await adapter.createSessionThread(
15995
+ session.id,
15996
+ session.name ?? `Session ${session.id.slice(0, 6)}`
15997
+ );
15998
+ session.threadIds.set(adapterId, threadId);
15999
+ session.attachedAdapters.push(adapterId);
16000
+ const bridge = this.createBridge(session, adapter, adapterId);
16001
+ bridge.connect();
16002
+ await this.sessionManager.patchRecord(session.id, {
16003
+ attachedAdapters: session.attachedAdapters,
16004
+ platforms: this.buildPlatformsFromSession(session)
16005
+ });
16006
+ return { threadId };
16007
+ }
16008
+ async detachAdapter(sessionId, adapterId) {
16009
+ const session = this.sessionManager.getSession(sessionId);
16010
+ if (!session) throw new Error(`Session ${sessionId} not found`);
16011
+ if (adapterId === session.channelId) {
16012
+ throw new Error("Cannot detach primary adapter (channelId)");
16013
+ }
16014
+ if (!session.attachedAdapters.includes(adapterId)) {
16015
+ return;
16016
+ }
16017
+ const adapter = this.adapters.get(adapterId);
16018
+ if (adapter) {
16019
+ try {
16020
+ await adapter.sendMessage(session.id, {
16021
+ type: "system_message",
16022
+ text: "Session detached from this adapter."
16023
+ });
16024
+ } catch {
16025
+ }
16026
+ }
16027
+ const key = this.bridgeKey(adapterId, session.id);
16028
+ const bridge = this.bridges.get(key);
16029
+ if (bridge) {
16030
+ bridge.disconnect();
16031
+ this.bridges.delete(key);
16032
+ }
16033
+ session.attachedAdapters = session.attachedAdapters.filter((a) => a !== adapterId);
16034
+ session.threadIds.delete(adapterId);
16035
+ await this.sessionManager.patchRecord(session.id, {
16036
+ attachedAdapters: session.attachedAdapters,
16037
+ platforms: this.buildPlatformsFromSession(session)
16038
+ });
16039
+ }
16040
+ buildPlatformsFromSession(session) {
16041
+ const platforms = {};
16042
+ for (const [adapterId, threadId] of session.threadIds) {
16043
+ if (adapterId === "telegram") {
16044
+ platforms.telegram = { topicId: Number(threadId) || threadId };
16045
+ } else {
16046
+ platforms[adapterId] = { threadId };
16047
+ }
16048
+ }
16049
+ return platforms;
16050
+ }
15699
16051
  // --- Event Wiring ---
16052
+ /** Composite bridge key: "adapterId:sessionId" */
16053
+ bridgeKey(adapterId, sessionId) {
16054
+ return `${adapterId}:${sessionId}`;
16055
+ }
16056
+ /** Get all bridge keys for a session (regardless of adapter) */
16057
+ getSessionBridgeKeys(sessionId) {
16058
+ const keys = [];
16059
+ for (const key of this.bridges.keys()) {
16060
+ if (key.endsWith(`:${sessionId}`)) keys.push(key);
16061
+ }
16062
+ return keys;
16063
+ }
15700
16064
  /** Connect a session bridge for the given session (used by AssistantManager) */
15701
16065
  connectSessionBridge(session) {
15702
16066
  const adapter = this.adapters.get(session.channelId);
15703
16067
  if (!adapter) return;
15704
- const bridge = this.createBridge(session, adapter);
16068
+ const bridge = this.createBridge(session, adapter, session.channelId);
15705
16069
  bridge.connect();
15706
16070
  }
15707
16071
  /** Create a SessionBridge for the given session and adapter.
15708
- * Disconnects any existing bridge for the same session first. */
15709
- createBridge(session, adapter) {
15710
- const existing = this.bridges.get(session.id);
16072
+ * Disconnects any existing bridge for the same adapter+session first. */
16073
+ createBridge(session, adapter, adapterId) {
16074
+ const id = adapterId ?? adapter.name;
16075
+ const key = this.bridgeKey(id, session.id);
16076
+ const existing = this.bridges.get(key);
15711
16077
  if (existing) {
15712
16078
  existing.disconnect();
15713
16079
  }
@@ -15718,8 +16084,8 @@ ${text3}`;
15718
16084
  eventBus: this.eventBus,
15719
16085
  fileService: this.fileService,
15720
16086
  middlewareChain: this.lifecycleManager?.middlewareChain
15721
- });
15722
- this.bridges.set(session.id, bridge);
16087
+ }, id);
16088
+ this.bridges.set(key, bridge);
15723
16089
  return bridge;
15724
16090
  }
15725
16091
  };
@@ -17754,12 +18120,14 @@ export {
17754
18120
  createApiServerService,
17755
18121
  createChildLogger,
17756
18122
  createSessionLogger,
18123
+ createTurnContext,
17757
18124
  expandHome3 as expandHome,
17758
18125
  extractContentText,
17759
18126
  formatTokens,
17760
18127
  formatToolSummary,
17761
18128
  formatToolTitle,
17762
18129
  getConfigValue,
18130
+ getEffectiveTarget,
17763
18131
  getFieldDef,
17764
18132
  getPidPath,
17765
18133
  getSafeFields,
@@ -17769,6 +18137,7 @@ export {
17769
18137
  isAutoStartInstalled,
17770
18138
  isAutoStartSupported,
17771
18139
  isHotReloadable,
18140
+ isSystemEvent,
17772
18141
  log,
17773
18142
  nodeToWebReadable,
17774
18143
  nodeToWebWritable,