@openacp/cli 2026.413.1 → 2026.414.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
@@ -1870,6 +1870,8 @@ var init_events = __esm({
1870
1870
  MESSAGE_QUEUED: "message:queued",
1871
1871
  /** Fired when a queued message starts processing. */
1872
1872
  MESSAGE_PROCESSING: "message:processing",
1873
+ /** Fired when a queued message is rejected (e.g. blocked by middleware). */
1874
+ MESSAGE_FAILED: "message:failed",
1873
1875
  // --- System lifecycle ---
1874
1876
  /** Fired after kernel (core + plugin infrastructure) has booted. */
1875
1877
  KERNEL_BOOTED: "kernel:booted",
@@ -2031,7 +2033,16 @@ function createSecurityPlugin() {
2031
2033
  handler: async (payload, next) => {
2032
2034
  const access2 = await guard.checkAccess(payload);
2033
2035
  if (!access2.allowed) {
2034
- ctx.log.info(`Access denied: ${access2.reason}`);
2036
+ ctx.log.info(`Access denied for user=${payload.userId} channel=${payload.channelId}: ${access2.reason}`);
2037
+ const adapter = core.adapters?.get?.(payload.channelId);
2038
+ if (adapter?.sendMessage && payload.threadId) {
2039
+ adapter.sendMessage(payload.threadId, {
2040
+ type: "error",
2041
+ message: `Access denied: ${access2.reason ?? "You are not allowed to use this service."}`
2042
+ }).catch((err) => {
2043
+ ctx.log.warn(`Failed to send access-denied message to adapter: ${err}`);
2044
+ });
2045
+ }
2035
2046
  return null;
2036
2047
  }
2037
2048
  return next();
@@ -2700,23 +2711,30 @@ function createIdentityPlugin() {
2700
2711
  });
2701
2712
  ctx.registerCommand({
2702
2713
  name: "whoami",
2703
- description: "Set your display name and username",
2704
- usage: "[name]",
2714
+ description: "Set your username and display name",
2715
+ usage: "@username [Display Name]",
2705
2716
  category: "plugin",
2706
2717
  async handler(args2) {
2707
- const name = args2.raw.trim();
2708
- if (!name) {
2709
- return { type: "text", text: "Usage: /whoami <name>" };
2710
- }
2718
+ const raw = args2.raw.trim();
2719
+ if (!raw) return { type: "error", message: "Usage: /whoami @username [Display Name]" };
2720
+ const tokens = raw.split(/\s+/);
2721
+ const first = tokens[0];
2722
+ const usernameRaw = first.startsWith("@") ? first.slice(1) : first;
2723
+ if (!/^[a-zA-Z0-9_.-]+$/.test(usernameRaw)) {
2724
+ return { type: "error", message: "Invalid username. Only letters, numbers, _ . - allowed." };
2725
+ }
2726
+ const username = usernameRaw;
2727
+ const displayName = tokens.slice(1).join(" ") || void 0;
2711
2728
  const identityId = formatIdentityId(args2.channelId, args2.userId);
2712
2729
  const user = await service.getUserByIdentity(identityId);
2713
2730
  if (!user) {
2714
- return { type: "error", message: "User not found \u2014 send a message first." };
2731
+ return { type: "error", message: "Identity not found. Send a message first." };
2715
2732
  }
2716
2733
  try {
2717
- const username = name.toLowerCase().replace(/[^a-z0-9_]/g, "");
2718
- await service.updateUser(user.userId, { displayName: name, username });
2719
- return { type: "text", text: `Display name set to "${name}", username: @${username}` };
2734
+ await service.updateUser(user.userId, { username, ...displayName && { displayName } });
2735
+ const parts = [`@${username}`];
2736
+ if (displayName) parts.push(`"${displayName}"`);
2737
+ return { type: "text", text: `\u2705 Profile updated: ${parts.join(" ")}` };
2720
2738
  } catch (err) {
2721
2739
  const message = err instanceof Error ? err.message : String(err);
2722
2740
  return { type: "error", message };
@@ -8987,7 +9005,8 @@ var init_sse_manager = __esm({
8987
9005
  BusEvent.PERMISSION_REQUEST,
8988
9006
  BusEvent.PERMISSION_RESOLVED,
8989
9007
  BusEvent.MESSAGE_QUEUED,
8990
- BusEvent.MESSAGE_PROCESSING
9008
+ BusEvent.MESSAGE_PROCESSING,
9009
+ BusEvent.MESSAGE_FAILED
8991
9010
  ];
8992
9011
  for (const eventName of events) {
8993
9012
  const handler = (data) => {
@@ -9069,7 +9088,8 @@ data: ${JSON.stringify(data)}
9069
9088
  BusEvent.PERMISSION_RESOLVED,
9070
9089
  BusEvent.SESSION_UPDATED,
9071
9090
  BusEvent.MESSAGE_QUEUED,
9072
- BusEvent.MESSAGE_PROCESSING
9091
+ BusEvent.MESSAGE_PROCESSING,
9092
+ BusEvent.MESSAGE_FAILED
9073
9093
  ];
9074
9094
  for (const res of this.sseConnections) {
9075
9095
  const filter = res.sessionFilter;
@@ -9339,7 +9359,6 @@ var sessions_exports = {};
9339
9359
  __export(sessions_exports, {
9340
9360
  sessionRoutes: () => sessionRoutes
9341
9361
  });
9342
- import { nanoid as nanoid3 } from "nanoid";
9343
9362
  async function sessionRoutes(app, deps) {
9344
9363
  app.get("/", { preHandler: requireScopes("sessions:read") }, async () => {
9345
9364
  const summaries = deps.core.sessionManager.listAllSessions();
@@ -9515,34 +9534,36 @@ async function sessionRoutes(app, deps) {
9515
9534
  attachments = await resolveAttachments(fileService, sessionId, body.attachments);
9516
9535
  }
9517
9536
  const sourceAdapterId = body.sourceAdapterId ?? "sse";
9518
- const turnId = body.turnId ?? nanoid3(8);
9519
9537
  const userId = request.auth?.tokenId ?? "api";
9520
- const meta = { turnId, channelUser: { channelId: "sse", userId } };
9521
- if (deps.lifecycleManager?.middlewareChain) {
9522
- await deps.lifecycleManager.middlewareChain.execute(
9523
- Hook.MESSAGE_INCOMING,
9524
- { channelId: sourceAdapterId, threadId: session.id, userId, text: body.prompt, attachments, meta },
9525
- async (p2) => p2
9538
+ const result = await deps.core.handleMessageInSession(
9539
+ session,
9540
+ { channelId: sourceAdapterId, userId, text: body.prompt, attachments },
9541
+ { channelUser: { channelId: "sse", userId } },
9542
+ { externalTurnId: body.turnId, responseAdapterId: body.responseAdapterId }
9543
+ );
9544
+ if (!result) {
9545
+ throw new AuthError("MESSAGE_BLOCKED", "Message was blocked by a middleware plugin.", 403);
9546
+ }
9547
+ return { ok: true, sessionId, queueDepth: result.queueDepth, turnId: result.turnId };
9548
+ }
9549
+ );
9550
+ app.get(
9551
+ "/:sessionId/queue",
9552
+ { preHandler: requireScopes("sessions:read") },
9553
+ async (request) => {
9554
+ const { sessionId: rawId } = SessionIdParamSchema.parse(request.params);
9555
+ const sessionId = decodeURIComponent(rawId);
9556
+ const session = await deps.core.getOrResumeSessionById(sessionId);
9557
+ if (!session) {
9558
+ throw new NotFoundError(
9559
+ "SESSION_NOT_FOUND",
9560
+ `Session "${sessionId}" not found`
9526
9561
  );
9527
9562
  }
9528
- deps.core.eventBus.emit(BusEvent.MESSAGE_QUEUED, {
9529
- sessionId,
9530
- turnId,
9531
- text: body.prompt,
9532
- sourceAdapterId,
9533
- attachments,
9534
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
9535
- queueDepth: session.queueDepth
9536
- });
9537
- await session.enqueuePrompt(body.prompt, attachments, {
9538
- sourceAdapterId,
9539
- responseAdapterId: body.responseAdapterId
9540
- }, turnId, meta);
9541
9563
  return {
9542
- ok: true,
9543
- sessionId,
9544
- queueDepth: session.queueDepth,
9545
- turnId
9564
+ pending: session.queueItems,
9565
+ processing: session.promptRunning,
9566
+ queueDepth: session.queueDepth
9546
9567
  };
9547
9568
  }
9548
9569
  );
@@ -9810,7 +9831,6 @@ var init_sessions2 = __esm({
9810
9831
  init_error_handler();
9811
9832
  init_auth();
9812
9833
  init_attachment_utils();
9813
- init_events();
9814
9834
  init_sessions();
9815
9835
  }
9816
9836
  });
@@ -12139,14 +12159,13 @@ async function sseRoutes(app, deps) {
12139
12159
  }
12140
12160
  attachments = await resolveAttachments(fileService, sessionId, body.attachments);
12141
12161
  }
12142
- const queueDepth = session.queueDepth + 1;
12143
12162
  const userId = request.auth?.tokenId ?? "api";
12144
- await deps.core.handleMessageInSession(
12163
+ const { turnId, queueDepth } = await deps.core.handleMessageInSession(
12145
12164
  session,
12146
12165
  { channelId: "sse", userId, text: body.prompt, attachments },
12147
12166
  { channelUser: { channelId: "sse", userId } }
12148
12167
  );
12149
- return { ok: true, sessionId, queueDepth };
12168
+ return { ok: true, sessionId, queueDepth, turnId };
12150
12169
  }
12151
12170
  );
12152
12171
  app.post(
@@ -16363,7 +16382,7 @@ var init_commands3 = __esm({
16363
16382
 
16364
16383
  // src/plugins/telegram/permissions.ts
16365
16384
  import { InlineKeyboard as InlineKeyboard11 } from "grammy";
16366
- import { nanoid as nanoid4 } from "nanoid";
16385
+ import { nanoid as nanoid3 } from "nanoid";
16367
16386
  var log26, PermissionHandler;
16368
16387
  var init_permissions = __esm({
16369
16388
  "src/plugins/telegram/permissions.ts"() {
@@ -16390,7 +16409,7 @@ var init_permissions = __esm({
16390
16409
  */
16391
16410
  async sendPermissionRequest(session, request) {
16392
16411
  const threadId = Number(session.threadId);
16393
- const callbackKey = nanoid4(8);
16412
+ const callbackKey = nanoid3(8);
16394
16413
  this.pending.set(callbackKey, {
16395
16414
  sessionId: session.id,
16396
16415
  requestId: request.id,
@@ -18401,7 +18420,11 @@ var init_adapter2 = __esm({
18401
18420
  }
18402
18421
  return prev(method, payload, signal);
18403
18422
  });
18404
- this.registerCommandsWithRetry();
18423
+ const onCommandsReady = ({ commands }) => {
18424
+ this.core.eventBus.off(BusEvent.SYSTEM_COMMANDS_READY, onCommandsReady);
18425
+ this.syncCommandsWithRetry(commands);
18426
+ };
18427
+ this.core.eventBus.on(BusEvent.SYSTEM_COMMANDS_READY, onCommandsReady);
18405
18428
  this.bot.use((ctx, next) => {
18406
18429
  const chatId = ctx.chat?.id ?? ctx.callbackQuery?.message?.chat?.id;
18407
18430
  if (chatId !== this.telegramConfig.chatId) return;
@@ -18627,12 +18650,16 @@ ${p2}` : p2;
18627
18650
  throw new Error("unreachable");
18628
18651
  }
18629
18652
  /**
18630
- * Register Telegram commands in the background with retries.
18631
- * Non-critical bot works fine without autocomplete commands.
18653
+ * Sync Telegram autocomplete commands after all plugins are ready.
18654
+ * Merges STATIC_COMMANDS (hardcoded system commands) with plugin commands
18655
+ * from the registry, deduplicating by command name. Non-critical.
18632
18656
  */
18633
- registerCommandsWithRetry() {
18657
+ syncCommandsWithRetry(registryCommands) {
18658
+ const staticNames = new Set(STATIC_COMMANDS.map((c3) => c3.command));
18659
+ const pluginCommands = registryCommands.filter((c3) => c3.category === "plugin" && !staticNames.has(c3.name) && /^[a-z0-9_]+$/.test(c3.name)).map((c3) => ({ command: c3.name, description: c3.description.slice(0, 256) }));
18660
+ const allCommands = [...STATIC_COMMANDS, ...pluginCommands].slice(0, 100);
18634
18661
  this.retryWithBackoff(
18635
- () => this.bot.api.setMyCommands(STATIC_COMMANDS, {
18662
+ () => this.bot.api.setMyCommands(allCommands, {
18636
18663
  scope: { type: "chat", chat_id: this.telegramConfig.chatId }
18637
18664
  }),
18638
18665
  "setMyCommands"
@@ -22161,16 +22188,16 @@ var init_prompt_queue = __esm({
22161
22188
  * immediately. Otherwise, it's buffered and the returned promise resolves
22162
22189
  * only after the prompt finishes processing.
22163
22190
  */
22164
- async enqueue(text5, attachments, routing, turnId, meta) {
22191
+ async enqueue(text5, userPrompt, attachments, routing, turnId, meta) {
22165
22192
  if (this.processing) {
22166
22193
  return new Promise((resolve9) => {
22167
- this.queue.push({ text: text5, attachments, routing, turnId, meta, resolve: resolve9 });
22194
+ this.queue.push({ text: text5, userPrompt, attachments, routing, turnId, meta, resolve: resolve9 });
22168
22195
  });
22169
22196
  }
22170
- await this.process(text5, attachments, routing, turnId, meta);
22197
+ await this.process(text5, userPrompt, attachments, routing, turnId, meta);
22171
22198
  }
22172
22199
  /** Run a single prompt through the processor, then drain the next queued item. */
22173
- async process(text5, attachments, routing, turnId, meta) {
22200
+ async process(text5, userPrompt, attachments, routing, turnId, meta) {
22174
22201
  this.processing = true;
22175
22202
  this.abortController = new AbortController();
22176
22203
  const { signal } = this.abortController;
@@ -22180,7 +22207,7 @@ var init_prompt_queue = __esm({
22180
22207
  });
22181
22208
  try {
22182
22209
  await Promise.race([
22183
- this.processor(text5, attachments, routing, turnId, meta),
22210
+ this.processor(text5, userPrompt, attachments, routing, turnId, meta),
22184
22211
  new Promise((_, reject) => {
22185
22212
  signal.addEventListener("abort", () => reject(new Error("Prompt aborted")), { once: true });
22186
22213
  })
@@ -22201,7 +22228,7 @@ var init_prompt_queue = __esm({
22201
22228
  drainNext() {
22202
22229
  const next = this.queue.shift();
22203
22230
  if (next) {
22204
- this.process(next.text, next.attachments, next.routing, next.turnId, next.meta).then(next.resolve);
22231
+ this.process(next.text, next.userPrompt, next.attachments, next.routing, next.turnId, next.meta).then(next.resolve);
22205
22232
  }
22206
22233
  }
22207
22234
  /**
@@ -22223,6 +22250,13 @@ var init_prompt_queue = __esm({
22223
22250
  get isProcessing() {
22224
22251
  return this.processing;
22225
22252
  }
22253
+ /** Snapshot of queued (not yet processing) items — used for queue inspection by callers. */
22254
+ get pendingItems() {
22255
+ return this.queue.map((item) => ({
22256
+ userPrompt: item.userPrompt,
22257
+ turnId: item.turnId
22258
+ }));
22259
+ }
22226
22260
  };
22227
22261
  }
22228
22262
  });
@@ -22307,18 +22341,32 @@ var init_permission_gate = __esm({
22307
22341
  });
22308
22342
 
22309
22343
  // src/core/sessions/turn-context.ts
22310
- import { nanoid as nanoid5 } from "nanoid";
22311
- function createTurnContext(sourceAdapterId, responseAdapterId, turnId) {
22344
+ import { nanoid as nanoid4 } from "nanoid";
22345
+ function extractSender(meta) {
22346
+ const identity = meta?.identity;
22347
+ if (!identity || !identity.userId || !identity.identityId) return null;
22312
22348
  return {
22313
- turnId: turnId ?? nanoid5(8),
22314
- sourceAdapterId,
22315
- responseAdapterId
22349
+ userId: identity.userId,
22350
+ identityId: identity.identityId,
22351
+ displayName: identity.displayName,
22352
+ username: identity.username
22316
22353
  };
22317
22354
  }
22318
22355
  function getEffectiveTarget(ctx) {
22319
22356
  if (ctx.responseAdapterId === null) return null;
22320
22357
  return ctx.responseAdapterId ?? ctx.sourceAdapterId;
22321
22358
  }
22359
+ function createTurnContext(sourceAdapterId, responseAdapterId, turnId, userPrompt, finalPrompt, attachments, meta) {
22360
+ return {
22361
+ turnId: turnId ?? nanoid4(8),
22362
+ sourceAdapterId,
22363
+ responseAdapterId,
22364
+ userPrompt,
22365
+ finalPrompt,
22366
+ attachments,
22367
+ meta
22368
+ };
22369
+ }
22322
22370
  function isSystemEvent(event) {
22323
22371
  return SYSTEM_EVENT_TYPES.has(event.type);
22324
22372
  }
@@ -22338,7 +22386,7 @@ var init_turn_context = __esm({
22338
22386
  });
22339
22387
 
22340
22388
  // src/core/sessions/session.ts
22341
- import { nanoid as nanoid6 } from "nanoid";
22389
+ import { nanoid as nanoid5 } from "nanoid";
22342
22390
  import * as fs41 from "fs";
22343
22391
  var moduleLog, TTS_PROMPT_INSTRUCTION, TTS_BLOCK_REGEX, TTS_MAX_LENGTH, TTS_TIMEOUT_MS, VALID_TRANSITIONS, Session;
22344
22392
  var init_session2 = __esm({
@@ -22418,7 +22466,7 @@ Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of y
22418
22466
  pendingContext = null;
22419
22467
  constructor(opts) {
22420
22468
  super();
22421
- this.id = opts.id || nanoid6(12);
22469
+ this.id = opts.id || nanoid5(12);
22422
22470
  this.channelId = opts.channelId;
22423
22471
  this.attachedAdapters = [opts.channelId];
22424
22472
  this.agentName = opts.agentName;
@@ -22430,7 +22478,7 @@ Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of y
22430
22478
  this.log = createSessionLogger(this.id, moduleLog);
22431
22479
  this.log.info({ agentName: this.agentName }, "Session created");
22432
22480
  this.queue = new PromptQueue(
22433
- (text5, attachments, routing, turnId, meta) => this.processPrompt(text5, attachments, routing, turnId, meta),
22481
+ (text5, userPrompt, attachments, routing, turnId, meta) => this.processPrompt(text5, userPrompt, attachments, routing, turnId, meta),
22434
22482
  (err) => {
22435
22483
  this.log.error({ err }, "Prompt execution failed");
22436
22484
  const message = err instanceof Error ? err.message : String(err);
@@ -22510,6 +22558,10 @@ Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of y
22510
22558
  get promptRunning() {
22511
22559
  return this.queue.isProcessing;
22512
22560
  }
22561
+ /** Snapshot of queued (not yet processing) items — for inspection by API consumers. */
22562
+ get queueItems() {
22563
+ return this.queue.pendingItems;
22564
+ }
22513
22565
  // --- Context Injection ---
22514
22566
  /** Store context markdown to be prepended to the next prompt (used for session resume with history). */
22515
22567
  setContext(markdown) {
@@ -22530,24 +22582,30 @@ Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of y
22530
22582
  * queued/processing events before the prompt actually runs.
22531
22583
  */
22532
22584
  async enqueuePrompt(text5, attachments, routing, externalTurnId, meta) {
22533
- const turnId = externalTurnId ?? nanoid6(8);
22585
+ const turnId = externalTurnId ?? nanoid5(8);
22534
22586
  const turnMeta = meta ?? { turnId };
22587
+ const userPrompt = text5;
22535
22588
  if (this.middlewareChain) {
22536
22589
  const payload = { text: text5, attachments, sessionId: this.id, sourceAdapterId: routing?.sourceAdapterId, meta: turnMeta };
22537
22590
  const result = await this.middlewareChain.execute(Hook.AGENT_BEFORE_PROMPT, payload, async (p2) => p2);
22538
- if (!result) return turnId;
22591
+ if (!result) throw new Error("PROMPT_BLOCKED");
22539
22592
  text5 = result.text;
22540
22593
  attachments = result.attachments;
22541
22594
  }
22542
- await this.queue.enqueue(text5, attachments, routing, turnId, turnMeta);
22595
+ await this.queue.enqueue(text5, userPrompt, attachments, routing, turnId, turnMeta);
22543
22596
  return turnId;
22544
22597
  }
22545
- async processPrompt(text5, attachments, routing, turnId, meta) {
22598
+ async processPrompt(text5, userPrompt, attachments, routing, turnId, meta) {
22546
22599
  if (this._status === "finished") return;
22547
22600
  this.activeTurnContext = createTurnContext(
22548
22601
  routing?.sourceAdapterId ?? this.channelId,
22549
22602
  routing?.responseAdapterId,
22550
- turnId
22603
+ turnId,
22604
+ userPrompt,
22605
+ text5,
22606
+ // finalPrompt (after middleware transformations)
22607
+ attachments,
22608
+ meta
22551
22609
  );
22552
22610
  this.emit(SessionEv.TURN_STARTED, this.activeTurnContext);
22553
22611
  this.promptCount++;
@@ -22598,7 +22656,16 @@ ${text5}`;
22598
22656
  this.agentInstance.on(SessionEv.AGENT_EVENT, afterEventListener);
22599
22657
  }
22600
22658
  if (this.middlewareChain) {
22601
- this.middlewareChain.execute(Hook.TURN_START, { sessionId: this.id, promptText: processed.text, promptNumber: this.promptCount, turnId: this.activeTurnContext?.turnId ?? turnId ?? "", meta }, async (p2) => p2).catch(() => {
22659
+ this.middlewareChain.execute(Hook.TURN_START, {
22660
+ sessionId: this.id,
22661
+ promptText: processed.text,
22662
+ promptNumber: this.promptCount,
22663
+ turnId: this.activeTurnContext?.turnId ?? turnId ?? "",
22664
+ meta,
22665
+ userPrompt: this.activeTurnContext?.userPrompt,
22666
+ sourceAdapterId: this.activeTurnContext?.sourceAdapterId,
22667
+ responseAdapterId: this.activeTurnContext?.responseAdapterId
22668
+ }, async (p2) => p2).catch(() => {
22602
22669
  });
22603
22670
  }
22604
22671
  let stopReason = "end_turn";
@@ -23269,7 +23336,7 @@ var init_session_bridge = __esm({
23269
23336
  if (this.shouldForward(event)) {
23270
23337
  this.dispatchAgentEvent(event);
23271
23338
  } else {
23272
- this.deps.eventBus?.emit(BusEvent.AGENT_EVENT, { sessionId: this.session.id, event });
23339
+ this.deps.eventBus?.emit(BusEvent.AGENT_EVENT, { sessionId: this.session.id, turnId: "", event });
23273
23340
  }
23274
23341
  });
23275
23342
  if (!this.session.agentInstance.onPermissionRequest || this.session.agentInstance.onPermissionRequest.__bridgeId === void 0) {
@@ -23322,14 +23389,16 @@ var init_session_bridge = __esm({
23322
23389
  this.deps.sessionManager.patchRecord(this.session.id, { currentPromptCount: count });
23323
23390
  });
23324
23391
  this.listen(this.session, SessionEv.TURN_STARTED, (ctx) => {
23325
- if (ctx.sourceAdapterId !== "sse") {
23326
- this.deps.eventBus?.emit(BusEvent.MESSAGE_PROCESSING, {
23327
- sessionId: this.session.id,
23328
- turnId: ctx.turnId,
23329
- sourceAdapterId: ctx.sourceAdapterId,
23330
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
23331
- });
23332
- }
23392
+ this.deps.eventBus?.emit(BusEvent.MESSAGE_PROCESSING, {
23393
+ sessionId: this.session.id,
23394
+ turnId: ctx.turnId,
23395
+ sourceAdapterId: ctx.sourceAdapterId,
23396
+ userPrompt: ctx.userPrompt,
23397
+ finalPrompt: ctx.finalPrompt,
23398
+ attachments: ctx.attachments,
23399
+ sender: extractSender(ctx.meta),
23400
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
23401
+ });
23333
23402
  });
23334
23403
  if (this.session.latestCommands !== null) {
23335
23404
  this.session.emit(SessionEv.AGENT_EVENT, { type: "commands_update", commands: this.session.latestCommands });
@@ -23495,6 +23564,7 @@ var init_session_bridge = __esm({
23495
23564
  }
23496
23565
  this.deps.eventBus?.emit(BusEvent.AGENT_EVENT, {
23497
23566
  sessionId: this.session.id,
23567
+ turnId: this.session.activeTurnContext?.turnId ?? "",
23498
23568
  event
23499
23569
  });
23500
23570
  return outgoing;
@@ -24408,6 +24478,7 @@ var init_session_factory = __esm({
24408
24478
  const failedSessionId = createParams.existingSessionId ?? `failed-${Date.now()}`;
24409
24479
  this.eventBus.emit(BusEvent.AGENT_EVENT, {
24410
24480
  sessionId: failedSessionId,
24481
+ turnId: "",
24411
24482
  event: guidance
24412
24483
  });
24413
24484
  throw err;
@@ -24778,7 +24849,7 @@ var init_agent_switch_handler = __esm({
24778
24849
  message: `Switching from ${fromAgent} to ${toAgent}...`
24779
24850
  };
24780
24851
  session.emit(SessionEv.AGENT_EVENT, startEvent);
24781
- eventBus.emit(BusEvent.AGENT_EVENT, { sessionId, event: startEvent });
24852
+ eventBus.emit(BusEvent.AGENT_EVENT, { sessionId, turnId: "", event: startEvent });
24782
24853
  eventBus.emit(BusEvent.SESSION_AGENT_SWITCH, {
24783
24854
  sessionId,
24784
24855
  fromAgent,
@@ -24841,7 +24912,7 @@ var init_agent_switch_handler = __esm({
24841
24912
  message: resumed ? `Switched to ${toAgent} (resumed previous session).` : `Switched to ${toAgent} (new session).`
24842
24913
  };
24843
24914
  session.emit(SessionEv.AGENT_EVENT, successEvent);
24844
- eventBus.emit(BusEvent.AGENT_EVENT, { sessionId, event: successEvent });
24915
+ eventBus.emit(BusEvent.AGENT_EVENT, { sessionId, turnId: "", event: successEvent });
24845
24916
  eventBus.emit(BusEvent.SESSION_AGENT_SWITCH, {
24846
24917
  sessionId,
24847
24918
  fromAgent,
@@ -24856,7 +24927,7 @@ var init_agent_switch_handler = __esm({
24856
24927
  message: `Failed to switch to ${toAgent}: ${errorMessage}`
24857
24928
  };
24858
24929
  session.emit(SessionEv.AGENT_EVENT, failedEvent);
24859
- eventBus.emit(BusEvent.AGENT_EVENT, { sessionId, event: failedEvent });
24930
+ eventBus.emit(BusEvent.AGENT_EVENT, { sessionId, turnId: "", event: failedEvent });
24860
24931
  eventBus.emit(BusEvent.SESSION_AGENT_SWITCH, {
24861
24932
  sessionId,
24862
24933
  fromAgent,
@@ -26769,7 +26840,7 @@ var init_core_items = __esm({
26769
26840
 
26770
26841
  // src/core/core.ts
26771
26842
  import path51 from "path";
26772
- import { nanoid as nanoid7 } from "nanoid";
26843
+ import { nanoid as nanoid6 } from "nanoid";
26773
26844
  var log44, OpenACPCore;
26774
26845
  var init_core = __esm({
26775
26846
  "src/core/core.ts"() {
@@ -26795,6 +26866,7 @@ var init_core = __esm({
26795
26866
  init_error_tracker();
26796
26867
  init_log();
26797
26868
  init_events();
26869
+ init_turn_context();
26798
26870
  log44 = createChildLogger({ module: "core" });
26799
26871
  OpenACPCore = class {
26800
26872
  configManager;
@@ -27080,7 +27152,7 @@ var init_core = __esm({
27080
27152
  },
27081
27153
  "Incoming message"
27082
27154
  );
27083
- const turnId = nanoid7(8);
27155
+ const turnId = nanoid6(8);
27084
27156
  const meta = { turnId, ...initialMeta };
27085
27157
  if (this.lifecycleManager?.middlewareChain) {
27086
27158
  const result = await this.lifecycleManager.middlewareChain.execute(
@@ -27120,9 +27192,6 @@ var init_core = __esm({
27120
27192
  }
27121
27193
  return;
27122
27194
  }
27123
- this.sessionManager.patchRecord(session.id, {
27124
- lastActiveAt: (/* @__PURE__ */ new Date()).toISOString()
27125
- });
27126
27195
  let text5 = message.text;
27127
27196
  if (this.assistantManager?.isAssistant(session.id)) {
27128
27197
  const pending = this.assistantManager.consumePendingSystemPrompt(message.channelId);
@@ -27135,38 +27204,56 @@ User message:
27135
27204
  ${text5}`;
27136
27205
  }
27137
27206
  }
27138
- const sourceAdapterId = message.routing?.sourceAdapterId ?? message.channelId;
27139
- const routing = sourceAdapterId !== message.routing?.sourceAdapterId ? { ...message.routing, sourceAdapterId } : message.routing;
27140
27207
  const enrichedMeta = message.meta ?? meta;
27141
- if (sourceAdapterId && sourceAdapterId !== "sse" && sourceAdapterId !== "api") {
27142
- this.eventBus.emit(BusEvent.MESSAGE_QUEUED, {
27208
+ await this._dispatchToSession(session, text5, message.attachments, {
27209
+ sourceAdapterId: message.routing?.sourceAdapterId ?? message.channelId,
27210
+ responseAdapterId: message.routing?.responseAdapterId
27211
+ }, turnId, enrichedMeta);
27212
+ }
27213
+ /**
27214
+ * Shared dispatch path for sending a prompt to a session.
27215
+ * Called by both handleMessage (Telegram) and handleMessageInSession (SSE/API)
27216
+ * after their respective middleware/enrichment steps.
27217
+ */
27218
+ async _dispatchToSession(session, text5, attachments, routing, turnId, meta) {
27219
+ this.sessionManager.patchRecord(session.id, {
27220
+ lastActiveAt: (/* @__PURE__ */ new Date()).toISOString()
27221
+ });
27222
+ this.eventBus.emit(BusEvent.MESSAGE_QUEUED, {
27223
+ sessionId: session.id,
27224
+ turnId,
27225
+ text: text5,
27226
+ sourceAdapterId: routing.sourceAdapterId,
27227
+ attachments,
27228
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
27229
+ queueDepth: session.queueDepth + 1,
27230
+ sender: extractSender(meta)
27231
+ });
27232
+ session.enqueuePrompt(text5, attachments, routing, turnId, meta).catch((err) => {
27233
+ const reason = err instanceof Error ? err.message : String(err);
27234
+ log44.warn({ err, sessionId: session.id, turnId, reason }, "enqueuePrompt failed \u2014 emitting message:failed");
27235
+ this.eventBus.emit(BusEvent.MESSAGE_FAILED, {
27143
27236
  sessionId: session.id,
27144
27237
  turnId,
27145
- text: text5,
27146
- sourceAdapterId,
27147
- attachments: message.attachments,
27148
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
27149
- queueDepth: session.queueDepth
27238
+ reason
27150
27239
  });
27151
- await session.enqueuePrompt(text5, message.attachments, routing, turnId, enrichedMeta);
27152
- } else {
27153
- await session.enqueuePrompt(text5, message.attachments, routing, turnId, enrichedMeta);
27154
- }
27240
+ });
27155
27241
  }
27156
27242
  /**
27157
27243
  * Send a message to a known session, running the full message:incoming → agent:beforePrompt
27158
27244
  * middleware chain (same as handleMessage) but without the threadId-based session lookup.
27159
27245
  *
27160
- * Used by channels that already hold a direct session reference (e.g. SSE adapter), where
27161
- * looking up by channelId+threadId is unreliable (API sessions may have no threadId).
27246
+ * Used by channels that already hold a direct session reference (e.g. SSE adapter, api-server),
27247
+ * where looking up by channelId+threadId is unreliable (API sessions may have no threadId).
27162
27248
  *
27163
27249
  * @param session The target session — caller is responsible for validating its status.
27164
27250
  * @param message Sender context and message content.
27165
27251
  * @param initialMeta Optional adapter-specific context to seed the TurnMeta bag
27166
27252
  * (e.g. channelUser with display name/username).
27253
+ * @param options Optional turnId override and response routing.
27167
27254
  */
27168
- async handleMessageInSession(session, message, initialMeta) {
27169
- const turnId = nanoid7(8);
27255
+ async handleMessageInSession(session, message, initialMeta, options) {
27256
+ const turnId = options?.externalTurnId ?? nanoid6(8);
27170
27257
  const meta = { turnId, ...initialMeta };
27171
27258
  let text5 = message.text;
27172
27259
  let { attachments } = message;
@@ -27185,13 +27272,17 @@ ${text5}`;
27185
27272
  payload,
27186
27273
  async (p2) => p2
27187
27274
  );
27188
- if (!result) return;
27275
+ if (!result) return { turnId, queueDepth: session.queueDepth };
27189
27276
  text5 = result.text;
27190
27277
  attachments = result.attachments;
27191
27278
  enrichedMeta = result.meta ?? meta;
27192
27279
  }
27193
- const routing = { sourceAdapterId: message.channelId };
27194
- await session.enqueuePrompt(text5, attachments, routing, turnId, enrichedMeta);
27280
+ const routing = {
27281
+ sourceAdapterId: message.channelId,
27282
+ responseAdapterId: options?.responseAdapterId
27283
+ };
27284
+ await this._dispatchToSession(session, text5, attachments, routing, turnId, enrichedMeta);
27285
+ return { turnId, queueDepth: session.queueDepth };
27195
27286
  }
27196
27287
  // --- Unified Session Creation Pipeline ---
27197
27288
  /**
@@ -27303,7 +27394,7 @@ ${text5}`;
27303
27394
  } else if (processedEvent.type === "error") {
27304
27395
  session.fail(processedEvent.message);
27305
27396
  }
27306
- this.eventBus.emit(BusEvent.AGENT_EVENT, { sessionId: session.id, event: processedEvent });
27397
+ this.eventBus.emit(BusEvent.AGENT_EVENT, { sessionId: session.id, turnId: session.activeTurnContext?.turnId ?? "", event: processedEvent });
27307
27398
  });
27308
27399
  session.on(SessionEv.STATUS_CHANGE, (_from, to) => {
27309
27400
  this.sessionManager.patchRecord(session.id, {
@@ -27315,6 +27406,18 @@ ${text5}`;
27315
27406
  session.on(SessionEv.PROMPT_COUNT_CHANGED, (count) => {
27316
27407
  this.sessionManager.patchRecord(session.id, { currentPromptCount: count });
27317
27408
  });
27409
+ session.on(SessionEv.TURN_STARTED, (ctx) => {
27410
+ this.eventBus.emit(BusEvent.MESSAGE_PROCESSING, {
27411
+ sessionId: session.id,
27412
+ turnId: ctx.turnId,
27413
+ sourceAdapterId: ctx.sourceAdapterId,
27414
+ userPrompt: ctx.userPrompt,
27415
+ finalPrompt: ctx.finalPrompt,
27416
+ attachments: ctx.attachments,
27417
+ sender: extractSender(ctx.meta),
27418
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
27419
+ });
27420
+ });
27318
27421
  }
27319
27422
  this.sessionFactory.wireSideEffects(session, {
27320
27423
  eventBus: this.eventBus,