@openacp/cli 2026.403.8 → 2026.404.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3474,7 +3474,9 @@ var init_sse_manager = __esm({
3474
3474
  "session:updated",
3475
3475
  "session:deleted",
3476
3476
  "agent:event",
3477
- "permission:request"
3477
+ "permission:request",
3478
+ "message:queued",
3479
+ "message:processing"
3478
3480
  ];
3479
3481
  for (const eventName of events) {
3480
3482
  const handler = (data) => {
@@ -3539,7 +3541,9 @@ data: ${JSON.stringify(data)}
3539
3541
  const sessionEvents = [
3540
3542
  "agent:event",
3541
3543
  "permission:request",
3542
- "session:updated"
3544
+ "session:updated",
3545
+ "message:queued",
3546
+ "message:processing"
3543
3547
  ];
3544
3548
  for (const res of this.sseConnections) {
3545
3549
  const filter = res.sessionFilter;
@@ -8362,7 +8366,7 @@ var init_commands = __esm({
8362
8366
 
8363
8367
  // src/plugins/telegram/permissions.ts
8364
8368
  import { InlineKeyboard as InlineKeyboard11 } from "grammy";
8365
- import { nanoid as nanoid2 } from "nanoid";
8369
+ import { nanoid as nanoid4 } from "nanoid";
8366
8370
  var log30, PermissionHandler;
8367
8371
  var init_permissions = __esm({
8368
8372
  "src/plugins/telegram/permissions.ts"() {
@@ -8381,7 +8385,7 @@ var init_permissions = __esm({
8381
8385
  pending = /* @__PURE__ */ new Map();
8382
8386
  async sendPermissionRequest(session, request) {
8383
8387
  const threadId = Number(session.threadId);
8384
- const callbackKey = nanoid2(8);
8388
+ const callbackKey = nanoid4(8);
8385
8389
  this.pending.set(callbackKey, {
8386
8390
  sessionId: session.id,
8387
8391
  requestId: request.id,
@@ -11182,6 +11186,7 @@ var AgentInstance = class _AgentInstance extends TypedEmitter {
11182
11186
  { sessionId: this.sessionId, exitCode: code, signal },
11183
11187
  "Agent process exited"
11184
11188
  );
11189
+ if (signal === "SIGINT" || signal === "SIGTERM") return;
11185
11190
  if (code !== 0 && code !== null || signal) {
11186
11191
  const stderr = this.stderrCapture.getLastLines();
11187
11192
  this.emit("agent_event", {
@@ -11657,7 +11662,7 @@ var AgentManager = class {
11657
11662
  };
11658
11663
 
11659
11664
  // src/core/sessions/session.ts
11660
- import { nanoid } from "nanoid";
11665
+ import { nanoid as nanoid2 } from "nanoid";
11661
11666
 
11662
11667
  // src/core/sessions/prompt-queue.ts
11663
11668
  var PromptQueue = class {
@@ -11668,21 +11673,21 @@ var PromptQueue = class {
11668
11673
  queue = [];
11669
11674
  processing = false;
11670
11675
  abortController = null;
11671
- async enqueue(text3, attachments) {
11676
+ async enqueue(text3, attachments, routing, turnId) {
11672
11677
  if (this.processing) {
11673
11678
  return new Promise((resolve6) => {
11674
- this.queue.push({ text: text3, attachments, resolve: resolve6 });
11679
+ this.queue.push({ text: text3, attachments, routing, turnId, resolve: resolve6 });
11675
11680
  });
11676
11681
  }
11677
- await this.process(text3, attachments);
11682
+ await this.process(text3, attachments, routing, turnId);
11678
11683
  }
11679
- async process(text3, attachments) {
11684
+ async process(text3, attachments, routing, turnId) {
11680
11685
  this.processing = true;
11681
11686
  this.abortController = new AbortController();
11682
11687
  const { signal } = this.abortController;
11683
11688
  try {
11684
11689
  await Promise.race([
11685
- this.processor(text3, attachments),
11690
+ this.processor(text3, attachments, routing, turnId),
11686
11691
  new Promise((_, reject) => {
11687
11692
  signal.addEventListener("abort", () => reject(new Error("Prompt aborted")), { once: true });
11688
11693
  })
@@ -11700,7 +11705,7 @@ var PromptQueue = class {
11700
11705
  drainNext() {
11701
11706
  const next = this.queue.shift();
11702
11707
  if (next) {
11703
- this.process(next.text, next.attachments).then(next.resolve);
11708
+ this.process(next.text, next.attachments, next.routing, next.turnId).then(next.resolve);
11704
11709
  }
11705
11710
  }
11706
11711
  clear() {
@@ -11790,6 +11795,33 @@ var PermissionGate = class {
11790
11795
  // src/core/sessions/session.ts
11791
11796
  init_log();
11792
11797
  import * as fs10 from "fs";
11798
+
11799
+ // src/core/sessions/turn-context.ts
11800
+ import { nanoid } from "nanoid";
11801
+ function createTurnContext(sourceAdapterId, responseAdapterId, turnId) {
11802
+ return {
11803
+ turnId: turnId ?? nanoid(8),
11804
+ sourceAdapterId,
11805
+ responseAdapterId
11806
+ };
11807
+ }
11808
+ function getEffectiveTarget(ctx) {
11809
+ if (ctx.responseAdapterId === null) return null;
11810
+ return ctx.responseAdapterId ?? ctx.sourceAdapterId;
11811
+ }
11812
+ var SYSTEM_EVENT_TYPES = /* @__PURE__ */ new Set([
11813
+ "session_end",
11814
+ "system_message",
11815
+ "session_info_update",
11816
+ "config_option_update",
11817
+ "commands_update",
11818
+ "tts_strip"
11819
+ ]);
11820
+ function isSystemEvent(event) {
11821
+ return SYSTEM_EVENT_TYPES.has(event.type);
11822
+ }
11823
+
11824
+ // src/core/sessions/session.ts
11793
11825
  var moduleLog = createChildLogger({ module: "session" });
11794
11826
  var TTS_PROMPT_INSTRUCTION = `
11795
11827
 
@@ -11807,7 +11839,15 @@ var VALID_TRANSITIONS = {
11807
11839
  var Session = class extends TypedEmitter {
11808
11840
  id;
11809
11841
  channelId;
11810
- threadId = "";
11842
+ /** @deprecated Use threadIds map directly. Getter returns primary adapter's threadId. */
11843
+ get threadId() {
11844
+ return this.threadIds.get(this.channelId) ?? "";
11845
+ }
11846
+ set threadId(value) {
11847
+ if (value) {
11848
+ this.threadIds.set(this.channelId, value);
11849
+ }
11850
+ }
11811
11851
  agentName;
11812
11852
  workingDirectory;
11813
11853
  agentInstance;
@@ -11828,14 +11868,21 @@ var Session = class extends TypedEmitter {
11828
11868
  middlewareChain;
11829
11869
  /** Latest commands emitted by the agent — buffered before bridge connects so they're not lost */
11830
11870
  latestCommands = null;
11871
+ /** Adapters currently attached to this session (including primary) */
11872
+ attachedAdapters = [];
11873
+ /** Per-adapter thread IDs: adapterId → threadId */
11874
+ threadIds = /* @__PURE__ */ new Map();
11875
+ /** Active turn context — sealed on prompt dequeue, cleared on turn end */
11876
+ activeTurnContext = null;
11831
11877
  permissionGate = new PermissionGate();
11832
11878
  queue;
11833
11879
  speechService;
11834
11880
  pendingContext = null;
11835
11881
  constructor(opts) {
11836
11882
  super();
11837
- this.id = opts.id || nanoid(12);
11883
+ this.id = opts.id || nanoid2(12);
11838
11884
  this.channelId = opts.channelId;
11885
+ this.attachedAdapters = [opts.channelId];
11839
11886
  this.agentName = opts.agentName;
11840
11887
  this.firstAgent = opts.agentName;
11841
11888
  this.workingDirectory = opts.workingDirectory;
@@ -11845,7 +11892,7 @@ var Session = class extends TypedEmitter {
11845
11892
  this.log = createSessionLogger(this.id, moduleLog);
11846
11893
  this.log.info({ agentName: this.agentName }, "Session created");
11847
11894
  this.queue = new PromptQueue(
11848
- (text3, attachments) => this.processPrompt(text3, attachments),
11895
+ (text3, attachments, routing, turnId) => this.processPrompt(text3, attachments, routing, turnId),
11849
11896
  (err) => {
11850
11897
  this.log.error({ err }, "Prompt execution failed");
11851
11898
  const message = err instanceof Error ? err.message : String(err);
@@ -11853,11 +11900,20 @@ var Session = class extends TypedEmitter {
11853
11900
  this.emit("agent_event", { type: "error", message: `Prompt execution failed: ${message}` });
11854
11901
  }
11855
11902
  );
11856
- this.agentInstance.on("agent_event", (event) => {
11903
+ this.wireCommandsBuffer();
11904
+ }
11905
+ /** Wire a listener on the current agentInstance to buffer commands_update events.
11906
+ * Must be called after every agentInstance replacement (constructor + switchAgent). */
11907
+ commandsBufferCleanup;
11908
+ wireCommandsBuffer() {
11909
+ this.commandsBufferCleanup?.();
11910
+ const handler = (event) => {
11857
11911
  if (event.type === "commands_update") {
11858
11912
  this.latestCommands = event.commands;
11859
11913
  }
11860
- });
11914
+ };
11915
+ this.agentInstance.on("agent_event", handler);
11916
+ this.commandsBufferCleanup = () => this.agentInstance.off("agent_event", handler);
11861
11917
  }
11862
11918
  // --- State Machine ---
11863
11919
  get status() {
@@ -11911,18 +11967,26 @@ var Session = class extends TypedEmitter {
11911
11967
  this.log.info({ voiceMode: mode }, "TTS mode changed");
11912
11968
  }
11913
11969
  // --- Public API ---
11914
- async enqueuePrompt(text3, attachments) {
11970
+ async enqueuePrompt(text3, attachments, routing, externalTurnId) {
11971
+ const turnId = externalTurnId ?? nanoid2(8);
11915
11972
  if (this.middlewareChain) {
11916
11973
  const payload = { text: text3, attachments, sessionId: this.id };
11917
11974
  const result = await this.middlewareChain.execute("agent:beforePrompt", payload, async (p) => p);
11918
- if (!result) return;
11975
+ if (!result) return turnId;
11919
11976
  text3 = result.text;
11920
11977
  attachments = result.attachments;
11921
11978
  }
11922
- await this.queue.enqueue(text3, attachments);
11979
+ await this.queue.enqueue(text3, attachments, routing, turnId);
11980
+ return turnId;
11923
11981
  }
11924
- async processPrompt(text3, attachments) {
11982
+ async processPrompt(text3, attachments, routing, turnId) {
11925
11983
  if (this._status === "finished") return;
11984
+ this.activeTurnContext = createTurnContext(
11985
+ routing?.sourceAdapterId ?? this.channelId,
11986
+ routing?.responseAdapterId,
11987
+ turnId
11988
+ );
11989
+ this.emit("turn_started", this.activeTurnContext);
11926
11990
  this.promptCount++;
11927
11991
  this.emit("prompt_count_changed", this.promptCount);
11928
11992
  if (this._status === "initializing" || this._status === "cancelled" || this._status === "error") {
@@ -11989,6 +12053,7 @@ ${text3}`;
11989
12053
  this.log.warn({ err }, "TTS post-processing failed");
11990
12054
  });
11991
12055
  }
12056
+ this.activeTurnContext = null;
11992
12057
  if (!this.name) {
11993
12058
  await this.autoName();
11994
12059
  }
@@ -12231,6 +12296,7 @@ ${result.text}` : result.text;
12231
12296
  this.configOptions = [];
12232
12297
  this.latestCommands = null;
12233
12298
  this.applySpawnResponse(newAgent.initialSessionResponse, newAgent.agentCapabilities);
12299
+ this.wireCommandsBuffer();
12234
12300
  this.log.info({ from: this.agentSwitchHistory.at(-1).agentName, to: agentName }, "Agent switched");
12235
12301
  }
12236
12302
  async destroy() {
@@ -12698,6 +12764,8 @@ var SessionManager = class {
12698
12764
  }
12699
12765
  getSessionByThread(channelId, threadId) {
12700
12766
  for (const session of this.sessions.values()) {
12767
+ const adapterThread = session.threadIds.get(channelId);
12768
+ if (adapterThread === threadId) return session;
12701
12769
  if (session.channelId === channelId && session.threadId === threadId) {
12702
12770
  return session;
12703
12771
  }
@@ -12765,6 +12833,67 @@ var SessionManager = class {
12765
12833
  if (channelId) return all.filter((s) => s.channelId === channelId);
12766
12834
  return all;
12767
12835
  }
12836
+ listAllSessions(channelId) {
12837
+ if (this.store) {
12838
+ let records = this.store.list();
12839
+ if (channelId) records = records.filter((r) => r.channelId === channelId);
12840
+ return records.map((record) => {
12841
+ const live2 = this.sessions.get(record.sessionId);
12842
+ if (live2) {
12843
+ return {
12844
+ id: live2.id,
12845
+ agent: live2.agentName,
12846
+ status: live2.status,
12847
+ name: live2.name ?? null,
12848
+ workspace: live2.workingDirectory,
12849
+ channelId: live2.channelId,
12850
+ createdAt: live2.createdAt.toISOString(),
12851
+ lastActiveAt: record.lastActiveAt ?? null,
12852
+ dangerousMode: live2.clientOverrides.bypassPermissions ?? false,
12853
+ queueDepth: live2.queueDepth,
12854
+ promptRunning: live2.promptRunning,
12855
+ configOptions: live2.configOptions?.length ? live2.configOptions : void 0,
12856
+ capabilities: live2.agentCapabilities ?? null,
12857
+ isLive: true
12858
+ };
12859
+ }
12860
+ return {
12861
+ id: record.sessionId,
12862
+ agent: record.agentName,
12863
+ status: record.status,
12864
+ name: record.name ?? null,
12865
+ workspace: record.workingDir,
12866
+ channelId: record.channelId,
12867
+ createdAt: record.createdAt,
12868
+ lastActiveAt: record.lastActiveAt ?? null,
12869
+ dangerousMode: record.clientOverrides?.bypassPermissions ?? false,
12870
+ queueDepth: 0,
12871
+ promptRunning: false,
12872
+ configOptions: record.acpState?.configOptions,
12873
+ capabilities: record.acpState?.agentCapabilities ?? null,
12874
+ isLive: false
12875
+ };
12876
+ });
12877
+ }
12878
+ let live = Array.from(this.sessions.values());
12879
+ if (channelId) live = live.filter((s) => s.channelId === channelId);
12880
+ return live.map((s) => ({
12881
+ id: s.id,
12882
+ agent: s.agentName,
12883
+ status: s.status,
12884
+ name: s.name ?? null,
12885
+ workspace: s.workingDirectory,
12886
+ channelId: s.channelId,
12887
+ createdAt: s.createdAt.toISOString(),
12888
+ lastActiveAt: null,
12889
+ dangerousMode: s.clientOverrides.bypassPermissions ?? false,
12890
+ queueDepth: s.queueDepth,
12891
+ promptRunning: s.promptRunning,
12892
+ configOptions: s.configOptions?.length ? s.configOptions : void 0,
12893
+ capabilities: s.agentCapabilities ?? null,
12894
+ isLive: true
12895
+ }));
12896
+ }
12768
12897
  listRecords(filter) {
12769
12898
  if (!this.store) return [];
12770
12899
  let records = this.store.list();
@@ -12787,7 +12916,14 @@ var SessionManager = class {
12787
12916
  for (const session of this.sessions.values()) {
12788
12917
  const record = this.store.get(session.id);
12789
12918
  if (record) {
12790
- await this.store.save({ ...record, status: "finished" });
12919
+ await this.store.save({
12920
+ ...record,
12921
+ status: "finished",
12922
+ acpState: session.toAcpStateSnapshot(),
12923
+ clientOverrides: session.clientOverrides,
12924
+ currentPromptCount: session.promptCount,
12925
+ agentSwitchHistory: session.agentSwitchHistory
12926
+ });
12791
12927
  }
12792
12928
  }
12793
12929
  this.store.flush();
@@ -12797,6 +12933,8 @@ var SessionManager = class {
12797
12933
  /**
12798
12934
  * Forcefully destroy all sessions (kill agent subprocesses).
12799
12935
  * Use only when sessions must be fully torn down (e.g. archive).
12936
+ * Unlike shutdownAll(), this does NOT snapshot live session state (acpState, etc.)
12937
+ * because destroyed sessions are terminal and will not be resumed.
12800
12938
  */
12801
12939
  async destroyAll() {
12802
12940
  if (this.store) {
@@ -12827,13 +12965,15 @@ init_log();
12827
12965
  init_bypass_detection();
12828
12966
  var log6 = createChildLogger({ module: "session-bridge" });
12829
12967
  var SessionBridge = class {
12830
- constructor(session, adapter, deps) {
12968
+ constructor(session, adapter, deps, adapterId) {
12831
12969
  this.session = session;
12832
12970
  this.adapter = adapter;
12833
12971
  this.deps = deps;
12972
+ this.adapterId = adapterId ?? adapter.name;
12834
12973
  }
12835
12974
  connected = false;
12836
12975
  cleanupFns = [];
12976
+ adapterId;
12837
12977
  get tracer() {
12838
12978
  return this.session.agentInstance.debugTracer ?? null;
12839
12979
  }
@@ -12864,6 +13004,15 @@ var SessionBridge = class {
12864
13004
  log6.error({ err, sessionId }, "Error in sendMessage middleware");
12865
13005
  }
12866
13006
  }
13007
+ /** Determine if this bridge should forward the given event based on turn routing. */
13008
+ shouldForward(event) {
13009
+ if (isSystemEvent(event)) return true;
13010
+ const ctx = this.session.activeTurnContext;
13011
+ if (!ctx) return true;
13012
+ const target = getEffectiveTarget(ctx);
13013
+ if (target === null) return false;
13014
+ return this.adapterId === target;
13015
+ }
12867
13016
  connect() {
12868
13017
  if (this.connected) return;
12869
13018
  this.connected = true;
@@ -12871,11 +13020,29 @@ var SessionBridge = class {
12871
13020
  this.session.emit("agent_event", event);
12872
13021
  });
12873
13022
  this.listen(this.session, "agent_event", (event) => {
12874
- this.dispatchAgentEvent(event);
13023
+ if (this.shouldForward(event)) {
13024
+ this.dispatchAgentEvent(event);
13025
+ } else {
13026
+ this.deps.eventBus?.emit("agent:event", { sessionId: this.session.id, event });
13027
+ }
13028
+ });
13029
+ if (!this.session.agentInstance.onPermissionRequest || this.session.agentInstance.onPermissionRequest.__bridgeId === void 0) {
13030
+ const handler = async (request) => {
13031
+ return this.resolvePermission(request);
13032
+ };
13033
+ handler.__bridgeId = this.adapterId;
13034
+ this.session.agentInstance.onPermissionRequest = handler;
13035
+ }
13036
+ this.listen(this.session, "permission_request", async (request) => {
13037
+ const current = this.session.agentInstance.onPermissionRequest;
13038
+ if (current?.__bridgeId === this.adapterId) return;
13039
+ if (!this.session.permissionGate.isPending) return;
13040
+ try {
13041
+ await this.adapter.sendPermissionRequest(this.session.id, request);
13042
+ } catch (err) {
13043
+ log6.error({ err, sessionId: this.session.id, adapterId: this.adapterId }, "Failed to send permission request to adapter");
13044
+ }
12875
13045
  });
12876
- this.session.agentInstance.onPermissionRequest = async (request) => {
12877
- return this.resolvePermission(request);
12878
- };
12879
13046
  this.listen(this.session, "status_change", (from, to) => {
12880
13047
  this.deps.sessionManager.patchRecord(this.session.id, {
12881
13048
  status: to,
@@ -12904,6 +13071,16 @@ var SessionBridge = class {
12904
13071
  this.listen(this.session, "prompt_count_changed", (count) => {
12905
13072
  this.deps.sessionManager.patchRecord(this.session.id, { currentPromptCount: count });
12906
13073
  });
13074
+ this.listen(this.session, "turn_started", (ctx) => {
13075
+ if (ctx.sourceAdapterId !== "sse" && ctx.sourceAdapterId !== "api") {
13076
+ this.deps.eventBus?.emit("message:processing", {
13077
+ sessionId: this.session.id,
13078
+ turnId: ctx.turnId,
13079
+ sourceAdapterId: ctx.sourceAdapterId,
13080
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
13081
+ });
13082
+ }
13083
+ });
12907
13084
  if (this.session.latestCommands !== null) {
12908
13085
  this.session.emit("agent_event", { type: "commands_update", commands: this.session.latestCommands });
12909
13086
  }
@@ -12916,7 +13093,10 @@ var SessionBridge = class {
12916
13093
  this.connected = false;
12917
13094
  this.cleanupFns.forEach((fn) => fn());
12918
13095
  this.cleanupFns = [];
12919
- this.session.agentInstance.onPermissionRequest = async () => "";
13096
+ const current = this.session.agentInstance.onPermissionRequest;
13097
+ if (current?.__bridgeId === this.adapterId) {
13098
+ this.session.agentInstance.onPermissionRequest = async () => "";
13099
+ }
12920
13100
  }
12921
13101
  /** Dispatch an agent event through middleware and to the adapter */
12922
13102
  async dispatchAgentEvent(event) {
@@ -13047,8 +13227,10 @@ var SessionBridge = class {
13047
13227
  this.sendMessage(this.session.id, outgoing);
13048
13228
  break;
13049
13229
  case "config_option_update":
13050
- this.session.updateConfigOptions(event.options);
13051
- this.persistAcpState();
13230
+ this.session.updateConfigOptions(event.options).then(() => {
13231
+ this.persistAcpState();
13232
+ }).catch(() => {
13233
+ });
13052
13234
  outgoing = this.deps.messageTransformer.transform(event);
13053
13235
  this.sendMessage(this.session.id, outgoing);
13054
13236
  break;
@@ -13092,19 +13274,27 @@ var SessionBridge = class {
13092
13274
  return result.autoResolve;
13093
13275
  }
13094
13276
  }
13095
- this.session.emit("permission_request", permReq);
13096
13277
  this.deps.eventBus?.emit("permission:request", {
13097
13278
  sessionId: this.session.id,
13098
13279
  permission: permReq
13099
13280
  });
13100
13281
  const autoDecision = this.checkAutoApprove(permReq);
13101
13282
  if (autoDecision) {
13283
+ this.session.emit("permission_request", permReq);
13102
13284
  this.emitAfterResolve(mw, permReq.id, autoDecision, "system", startTime);
13103
13285
  return autoDecision;
13104
13286
  }
13105
13287
  const promise = this.session.permissionGate.setPending(permReq);
13288
+ this.session.emit("permission_request", permReq);
13106
13289
  await this.adapter.sendPermissionRequest(this.session.id, permReq);
13107
13290
  const optionId = await promise;
13291
+ this.deps.eventBus?.emit("permission:resolved", {
13292
+ sessionId: this.session.id,
13293
+ requestId: permReq.id,
13294
+ decision: optionId,
13295
+ optionId,
13296
+ resolvedBy: this.adapterId
13297
+ });
13108
13298
  this.emitAfterResolve(mw, permReq.id, optionId, "user", startTime);
13109
13299
  return optionId;
13110
13300
  }
@@ -13298,6 +13488,65 @@ var SessionFactory = class {
13298
13488
  if (session) return session;
13299
13489
  return this.lazyResume(channelId, threadId);
13300
13490
  }
13491
+ async getOrResumeById(sessionId) {
13492
+ const live = this.sessionManager.getSession(sessionId);
13493
+ if (live) return live;
13494
+ if (!this.sessionStore || !this.createFullSession) return null;
13495
+ const record = this.sessionStore.get(sessionId);
13496
+ if (!record) return null;
13497
+ if (record.status === "error" || record.status === "cancelled") return null;
13498
+ const existing = this.resumeLocks.get(sessionId);
13499
+ if (existing) return existing;
13500
+ const resumePromise = (async () => {
13501
+ try {
13502
+ const p = record.platform;
13503
+ const existingThreadId = p?.topicId ? String(p.topicId) : p?.threadId;
13504
+ const session = await this.createFullSession({
13505
+ channelId: record.channelId,
13506
+ agentName: record.agentName,
13507
+ workingDirectory: record.workingDir,
13508
+ resumeAgentSessionId: record.agentSessionId,
13509
+ existingSessionId: record.sessionId,
13510
+ initialName: record.name,
13511
+ threadId: existingThreadId
13512
+ });
13513
+ session.activate();
13514
+ if (record.clientOverrides) {
13515
+ session.clientOverrides = record.clientOverrides;
13516
+ } else if (record.dangerousMode) {
13517
+ session.clientOverrides = { bypassPermissions: true };
13518
+ }
13519
+ if (record.firstAgent) session.firstAgent = record.firstAgent;
13520
+ if (record.agentSwitchHistory) session.agentSwitchHistory = record.agentSwitchHistory;
13521
+ if (record.currentPromptCount != null) session.promptCount = record.currentPromptCount;
13522
+ if (record.attachedAdapters) session.attachedAdapters = record.attachedAdapters;
13523
+ if (record.platforms) {
13524
+ for (const [adapterId, platformData] of Object.entries(record.platforms)) {
13525
+ const data = platformData;
13526
+ const tid = adapterId === "telegram" ? String(data.topicId ?? "") : String(data.threadId ?? "");
13527
+ if (tid) session.threadIds.set(adapterId, tid);
13528
+ }
13529
+ }
13530
+ if (record.acpState) {
13531
+ if (record.acpState.configOptions && session.configOptions.length === 0) {
13532
+ session.setInitialConfigOptions(record.acpState.configOptions);
13533
+ }
13534
+ if (record.acpState.agentCapabilities && !session.agentCapabilities) {
13535
+ session.setAgentCapabilities(record.acpState.agentCapabilities);
13536
+ }
13537
+ }
13538
+ log7.info({ sessionId }, "Lazy resume by ID successful");
13539
+ return session;
13540
+ } catch (err) {
13541
+ log7.error({ err, sessionId }, "Lazy resume by ID failed");
13542
+ return null;
13543
+ } finally {
13544
+ this.resumeLocks.delete(sessionId);
13545
+ }
13546
+ })();
13547
+ this.resumeLocks.set(sessionId, resumePromise);
13548
+ return resumePromise;
13549
+ }
13301
13550
  async lazyResume(channelId, threadId) {
13302
13551
  const store = this.sessionStore;
13303
13552
  if (!store || !this.createFullSession) return null;
@@ -13306,7 +13555,7 @@ var SessionFactory = class {
13306
13555
  if (existing) return existing;
13307
13556
  const record = store.findByPlatform(
13308
13557
  channelId,
13309
- (p) => String(p.topicId) === threadId
13558
+ (p) => String(p.topicId) === threadId || String(p.threadId ?? "") === threadId
13310
13559
  );
13311
13560
  if (!record) {
13312
13561
  log7.debug({ threadId, channelId }, "No session record found for thread");
@@ -13341,11 +13590,21 @@ var SessionFactory = class {
13341
13590
  if (record.firstAgent) session.firstAgent = record.firstAgent;
13342
13591
  if (record.agentSwitchHistory) session.agentSwitchHistory = record.agentSwitchHistory;
13343
13592
  if (record.currentPromptCount != null) session.promptCount = record.currentPromptCount;
13593
+ if (record.attachedAdapters) {
13594
+ session.attachedAdapters = record.attachedAdapters;
13595
+ }
13596
+ if (record.platforms) {
13597
+ for (const [adapterId, platformData] of Object.entries(record.platforms)) {
13598
+ const data = platformData;
13599
+ const tid = adapterId === "telegram" ? String(data.topicId ?? "") : String(data.threadId ?? "");
13600
+ if (tid) session.threadIds.set(adapterId, tid);
13601
+ }
13602
+ }
13344
13603
  if (record.acpState) {
13345
- if (record.acpState.configOptions) {
13604
+ if (record.acpState.configOptions && session.configOptions.length === 0) {
13346
13605
  session.setInitialConfigOptions(record.acpState.configOptions);
13347
13606
  }
13348
- if (record.acpState.agentCapabilities) {
13607
+ if (record.acpState.agentCapabilities && !session.agentCapabilities) {
13349
13608
  session.setAgentCapabilities(record.acpState.agentCapabilities);
13350
13609
  }
13351
13610
  }
@@ -13475,6 +13734,7 @@ var SessionFactory = class {
13475
13734
  // src/core/core.ts
13476
13735
  import path17 from "path";
13477
13736
  import os8 from "os";
13737
+ import { nanoid as nanoid3 } from "nanoid";
13478
13738
 
13479
13739
  // src/core/sessions/session-store.ts
13480
13740
  init_log();
@@ -13512,6 +13772,9 @@ var JsonFileSessionStore = class {
13512
13772
  }
13513
13773
  findByPlatform(channelId, predicate) {
13514
13774
  for (const record of this.records.values()) {
13775
+ if (record.platforms?.[channelId]) {
13776
+ if (predicate(record.platforms[channelId])) return record;
13777
+ }
13515
13778
  if (record.channelId === channelId && predicate(record.platform)) {
13516
13779
  return record;
13517
13780
  }
@@ -13578,7 +13841,7 @@ var JsonFileSessionStore = class {
13578
13841
  return;
13579
13842
  }
13580
13843
  for (const [id, record] of Object.entries(raw.sessions)) {
13581
- this.records.set(id, record);
13844
+ this.records.set(id, this.migrateRecord(record));
13582
13845
  }
13583
13846
  log8.debug({ count: this.records.size }, "Loaded session records");
13584
13847
  } catch (err) {
@@ -13589,6 +13852,19 @@ var JsonFileSessionStore = class {
13589
13852
  }
13590
13853
  }
13591
13854
  }
13855
+ /** Migrate old SessionRecord format to new multi-adapter format. */
13856
+ migrateRecord(record) {
13857
+ if (!record.platforms && record.platform && typeof record.platform === "object") {
13858
+ const platformData = record.platform;
13859
+ if (Object.keys(platformData).length > 0) {
13860
+ record.platforms = { [record.channelId]: platformData };
13861
+ }
13862
+ }
13863
+ if (!record.attachedAdapters) {
13864
+ record.attachedAdapters = [record.channelId];
13865
+ }
13866
+ return record;
13867
+ }
13592
13868
  cleanup() {
13593
13869
  const cutoff = Date.now() - this.ttlDays * 24 * 60 * 60 * 1e3;
13594
13870
  let removed = 0;
@@ -13667,8 +13943,15 @@ var AgentSwitchHandler = class {
13667
13943
  toAgent,
13668
13944
  status: "starting"
13669
13945
  });
13670
- const bridge = bridges.get(sessionId);
13671
- if (bridge) bridge.disconnect();
13946
+ const sessionBridgeKeys = this.deps.getSessionBridgeKeys(sessionId);
13947
+ const hadBridges = sessionBridgeKeys.length > 0;
13948
+ for (const key of sessionBridgeKeys) {
13949
+ const bridge = bridges.get(key);
13950
+ if (bridge) {
13951
+ bridges.delete(key);
13952
+ bridge.disconnect();
13953
+ }
13954
+ }
13672
13955
  const switchAdapter = adapters.get(session.channelId);
13673
13956
  if (switchAdapter?.sendSkillCommands) {
13674
13957
  await switchAdapter.sendSkillCommands(session.id, []);
@@ -13745,9 +14028,11 @@ var AgentSwitchHandler = class {
13745
14028
  session.agentInstance = oldInstance;
13746
14029
  session.agentName = fromAgent;
13747
14030
  session.agentSessionId = oldInstance.sessionId;
13748
- const adapter = adapters.get(session.channelId);
13749
- if (adapter) {
13750
- createBridge(session, adapter).connect();
14031
+ for (const adapterId of session.attachedAdapters) {
14032
+ const adapter = adapters.get(adapterId);
14033
+ if (adapter) {
14034
+ createBridge(session, adapter, adapterId).connect();
14035
+ }
13751
14036
  }
13752
14037
  log9.warn({ sessionId, fromAgent, toAgent, err }, "Agent switch failed, rolled back to previous agent");
13753
14038
  } catch (rollbackErr) {
@@ -13756,10 +14041,14 @@ var AgentSwitchHandler = class {
13756
14041
  }
13757
14042
  throw err;
13758
14043
  }
13759
- if (bridge) {
13760
- const adapter = adapters.get(session.channelId);
13761
- if (adapter) {
13762
- createBridge(session, adapter).connect();
14044
+ if (hadBridges) {
14045
+ for (const adapterId of session.attachedAdapters) {
14046
+ const adapter = adapters.get(adapterId);
14047
+ if (adapter) {
14048
+ createBridge(session, adapter, adapterId).connect();
14049
+ } else {
14050
+ log9.warn({ sessionId, adapterId }, "Adapter not available during switch reconnect, skipping bridge");
14051
+ }
13763
14052
  }
13764
14053
  }
13765
14054
  await sessionManager.patchRecord(sessionId, {
@@ -15212,7 +15501,7 @@ var OpenACPCore = class {
15212
15501
  sessionManager;
15213
15502
  messageTransformer;
15214
15503
  adapters = /* @__PURE__ */ new Map();
15215
- /** sessionId → SessionBridge — tracks active bridges for disconnect/reconnect during agent switch */
15504
+ /** "adapterId:sessionId" → SessionBridge — tracks active bridges for disconnect/reconnect */
15216
15505
  bridges = /* @__PURE__ */ new Map();
15217
15506
  /** Set by main.ts — triggers graceful shutdown with restart exit code */
15218
15507
  requestRestart = null;
@@ -15308,7 +15597,8 @@ var OpenACPCore = class {
15308
15597
  eventBus: this.eventBus,
15309
15598
  adapters: this.adapters,
15310
15599
  bridges: this.bridges,
15311
- createBridge: (session, adapter) => this.createBridge(session, adapter),
15600
+ createBridge: (session, adapter, adapterId) => this.createBridge(session, adapter, adapterId),
15601
+ getSessionBridgeKeys: (sessionId) => this.getSessionBridgeKeys(sessionId),
15312
15602
  getMiddlewareChain: () => this.lifecycleManager?.middlewareChain,
15313
15603
  getService: (name) => this.lifecycleManager.serviceRegistry.get(name)
15314
15604
  });
@@ -15488,7 +15778,22 @@ User message:
15488
15778
  ${text3}`;
15489
15779
  }
15490
15780
  }
15491
- await session.enqueuePrompt(text3, message.attachments);
15781
+ const sourceAdapterId = message.routing?.sourceAdapterId ?? message.channelId;
15782
+ if (sourceAdapterId && sourceAdapterId !== "sse" && sourceAdapterId !== "api") {
15783
+ const turnId = nanoid3(8);
15784
+ this.eventBus.emit("message:queued", {
15785
+ sessionId: session.id,
15786
+ turnId,
15787
+ text: text3,
15788
+ sourceAdapterId,
15789
+ attachments: message.attachments,
15790
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
15791
+ queueDepth: session.queueDepth
15792
+ });
15793
+ await session.enqueuePrompt(text3, message.attachments, message.routing, turnId);
15794
+ } else {
15795
+ await session.enqueuePrompt(text3, message.attachments, message.routing);
15796
+ }
15492
15797
  }
15493
15798
  // --- Unified Session Creation Pipeline ---
15494
15799
  async createSession(params) {
@@ -15515,6 +15820,12 @@ ${text3}`;
15515
15820
  platform2.threadId = session.threadId;
15516
15821
  }
15517
15822
  }
15823
+ const platforms = {
15824
+ ...existingRecord?.platforms ?? {}
15825
+ };
15826
+ if (session.threadId) {
15827
+ platforms[params.channelId] = params.channelId === "telegram" ? { topicId: Number(session.threadId) || session.threadId } : { threadId: session.threadId };
15828
+ }
15518
15829
  await this.sessionManager.patchRecord(session.id, {
15519
15830
  sessionId: session.id,
15520
15831
  agentSessionId: session.agentSessionId,
@@ -15526,6 +15837,7 @@ ${text3}`;
15526
15837
  lastActiveAt: (/* @__PURE__ */ new Date()).toISOString(),
15527
15838
  name: session.name,
15528
15839
  platform: platform2,
15840
+ platforms,
15529
15841
  firstAgent: session.firstAgent,
15530
15842
  currentPromptCount: session.promptCount,
15531
15843
  agentSwitchHistory: session.agentSwitchHistory,
@@ -15533,7 +15845,7 @@ ${text3}`;
15533
15845
  acpState: session.toAcpStateSnapshot()
15534
15846
  }, { immediate: true });
15535
15847
  if (adapter) {
15536
- const bridge = this.createBridge(session, adapter);
15848
+ const bridge = this.createBridge(session, adapter, session.channelId);
15537
15849
  bridge.connect();
15538
15850
  adapter.flushPendingSkillCommands?.(session.id).catch((err) => {
15539
15851
  log16.warn({ err, sessionId: session.id }, "Failed to flush pending skill commands");
@@ -15672,9 +15984,14 @@ ${text3}`;
15672
15984
  } else {
15673
15985
  adoptPlatform.threadId = session.threadId;
15674
15986
  }
15987
+ const adoptPlatforms = {};
15988
+ if (session.threadId) {
15989
+ adoptPlatforms[adapterChannelId] = adapterChannelId === "telegram" ? { topicId: Number(session.threadId) || session.threadId } : { threadId: session.threadId };
15990
+ }
15675
15991
  await this.sessionManager.patchRecord(session.id, {
15676
15992
  originalAgentSessionId: agentSessionId,
15677
- platform: adoptPlatform
15993
+ platform: adoptPlatform,
15994
+ platforms: adoptPlatforms
15678
15995
  });
15679
15996
  return {
15680
15997
  ok: true,
@@ -15696,18 +16013,101 @@ ${text3}`;
15696
16013
  async getOrResumeSession(channelId, threadId) {
15697
16014
  return this.sessionFactory.getOrResume(channelId, threadId);
15698
16015
  }
16016
+ async getOrResumeSessionById(sessionId) {
16017
+ return this.sessionFactory.getOrResumeById(sessionId);
16018
+ }
16019
+ async attachAdapter(sessionId, adapterId) {
16020
+ const session = this.sessionManager.getSession(sessionId);
16021
+ if (!session) throw new Error(`Session ${sessionId} not found`);
16022
+ const adapter = this.adapters.get(adapterId);
16023
+ if (!adapter) throw new Error(`Adapter "${adapterId}" not found or not running`);
16024
+ if (session.attachedAdapters.includes(adapterId)) {
16025
+ const existingThread = session.threadIds.get(adapterId) ?? session.id;
16026
+ return { threadId: existingThread };
16027
+ }
16028
+ const threadId = await adapter.createSessionThread(
16029
+ session.id,
16030
+ session.name ?? `Session ${session.id.slice(0, 6)}`
16031
+ );
16032
+ session.threadIds.set(adapterId, threadId);
16033
+ session.attachedAdapters.push(adapterId);
16034
+ const bridge = this.createBridge(session, adapter, adapterId);
16035
+ bridge.connect();
16036
+ await this.sessionManager.patchRecord(session.id, {
16037
+ attachedAdapters: session.attachedAdapters,
16038
+ platforms: this.buildPlatformsFromSession(session)
16039
+ });
16040
+ return { threadId };
16041
+ }
16042
+ async detachAdapter(sessionId, adapterId) {
16043
+ const session = this.sessionManager.getSession(sessionId);
16044
+ if (!session) throw new Error(`Session ${sessionId} not found`);
16045
+ if (adapterId === session.channelId) {
16046
+ throw new Error("Cannot detach primary adapter (channelId)");
16047
+ }
16048
+ if (!session.attachedAdapters.includes(adapterId)) {
16049
+ return;
16050
+ }
16051
+ const adapter = this.adapters.get(adapterId);
16052
+ if (adapter) {
16053
+ try {
16054
+ await adapter.sendMessage(session.id, {
16055
+ type: "system_message",
16056
+ text: "Session detached from this adapter."
16057
+ });
16058
+ } catch {
16059
+ }
16060
+ }
16061
+ const key = this.bridgeKey(adapterId, session.id);
16062
+ const bridge = this.bridges.get(key);
16063
+ if (bridge) {
16064
+ bridge.disconnect();
16065
+ this.bridges.delete(key);
16066
+ }
16067
+ session.attachedAdapters = session.attachedAdapters.filter((a) => a !== adapterId);
16068
+ session.threadIds.delete(adapterId);
16069
+ await this.sessionManager.patchRecord(session.id, {
16070
+ attachedAdapters: session.attachedAdapters,
16071
+ platforms: this.buildPlatformsFromSession(session)
16072
+ });
16073
+ }
16074
+ buildPlatformsFromSession(session) {
16075
+ const platforms = {};
16076
+ for (const [adapterId, threadId] of session.threadIds) {
16077
+ if (adapterId === "telegram") {
16078
+ platforms.telegram = { topicId: Number(threadId) || threadId };
16079
+ } else {
16080
+ platforms[adapterId] = { threadId };
16081
+ }
16082
+ }
16083
+ return platforms;
16084
+ }
15699
16085
  // --- Event Wiring ---
16086
+ /** Composite bridge key: "adapterId:sessionId" */
16087
+ bridgeKey(adapterId, sessionId) {
16088
+ return `${adapterId}:${sessionId}`;
16089
+ }
16090
+ /** Get all bridge keys for a session (regardless of adapter) */
16091
+ getSessionBridgeKeys(sessionId) {
16092
+ const keys = [];
16093
+ for (const key of this.bridges.keys()) {
16094
+ if (key.endsWith(`:${sessionId}`)) keys.push(key);
16095
+ }
16096
+ return keys;
16097
+ }
15700
16098
  /** Connect a session bridge for the given session (used by AssistantManager) */
15701
16099
  connectSessionBridge(session) {
15702
16100
  const adapter = this.adapters.get(session.channelId);
15703
16101
  if (!adapter) return;
15704
- const bridge = this.createBridge(session, adapter);
16102
+ const bridge = this.createBridge(session, adapter, session.channelId);
15705
16103
  bridge.connect();
15706
16104
  }
15707
16105
  /** 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);
16106
+ * Disconnects any existing bridge for the same adapter+session first. */
16107
+ createBridge(session, adapter, adapterId) {
16108
+ const id = adapterId ?? adapter.name;
16109
+ const key = this.bridgeKey(id, session.id);
16110
+ const existing = this.bridges.get(key);
15711
16111
  if (existing) {
15712
16112
  existing.disconnect();
15713
16113
  }
@@ -15718,8 +16118,8 @@ ${text3}`;
15718
16118
  eventBus: this.eventBus,
15719
16119
  fileService: this.fileService,
15720
16120
  middlewareChain: this.lifecycleManager?.middlewareChain
15721
- });
15722
- this.bridges.set(session.id, bridge);
16121
+ }, id);
16122
+ this.bridges.set(key, bridge);
15723
16123
  return bridge;
15724
16124
  }
15725
16125
  };
@@ -17754,12 +18154,14 @@ export {
17754
18154
  createApiServerService,
17755
18155
  createChildLogger,
17756
18156
  createSessionLogger,
18157
+ createTurnContext,
17757
18158
  expandHome3 as expandHome,
17758
18159
  extractContentText,
17759
18160
  formatTokens,
17760
18161
  formatToolSummary,
17761
18162
  formatToolTitle,
17762
18163
  getConfigValue,
18164
+ getEffectiveTarget,
17763
18165
  getFieldDef,
17764
18166
  getPidPath,
17765
18167
  getSafeFields,
@@ -17769,6 +18171,7 @@ export {
17769
18171
  isAutoStartInstalled,
17770
18172
  isAutoStartSupported,
17771
18173
  isHotReloadable,
18174
+ isSystemEvent,
17772
18175
  log,
17773
18176
  nodeToWebReadable,
17774
18177
  nodeToWebWritable,