@openacp/cli 2026.413.1 → 2026.414.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/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();
@@ -2622,6 +2633,22 @@ function registerSetupRoutes(app, deps) {
2622
2633
  if (user2) return user2;
2623
2634
  }
2624
2635
  const body = request.body;
2636
+ if (body?.identitySecret) {
2637
+ const oldToken = tokenStore?.getByIdentitySecret(body.identitySecret);
2638
+ if (!oldToken) {
2639
+ return reply.status(401).send({ error: "Invalid identity secret" });
2640
+ }
2641
+ const userId = tokenStore?.getUserId(oldToken.id);
2642
+ if (!userId) {
2643
+ return reply.status(401).send({ error: "No identity linked to this secret" });
2644
+ }
2645
+ await service.createIdentity(userId, {
2646
+ source: "api",
2647
+ platformId: auth.tokenId
2648
+ });
2649
+ tokenStore?.setUserId?.(auth.tokenId, userId);
2650
+ return service.getUser(userId);
2651
+ }
2625
2652
  if (body?.linkCode) {
2626
2653
  const entry = linkCodes.get(body.linkCode);
2627
2654
  if (!entry || entry.expiresAt < Date.now()) {
@@ -2700,23 +2727,30 @@ function createIdentityPlugin() {
2700
2727
  });
2701
2728
  ctx.registerCommand({
2702
2729
  name: "whoami",
2703
- description: "Set your display name and username",
2704
- usage: "[name]",
2730
+ description: "Set your username and display name",
2731
+ usage: "@username [Display Name]",
2705
2732
  category: "plugin",
2706
2733
  async handler(args2) {
2707
- const name = args2.raw.trim();
2708
- if (!name) {
2709
- return { type: "text", text: "Usage: /whoami <name>" };
2710
- }
2734
+ const raw = args2.raw.trim();
2735
+ if (!raw) return { type: "error", message: "Usage: /whoami @username [Display Name]" };
2736
+ const tokens = raw.split(/\s+/);
2737
+ const first = tokens[0];
2738
+ const usernameRaw = first.startsWith("@") ? first.slice(1) : first;
2739
+ if (!/^[a-zA-Z0-9_.-]+$/.test(usernameRaw)) {
2740
+ return { type: "error", message: "Invalid username. Only letters, numbers, _ . - allowed." };
2741
+ }
2742
+ const username = usernameRaw;
2743
+ const displayName = tokens.slice(1).join(" ") || void 0;
2711
2744
  const identityId = formatIdentityId(args2.channelId, args2.userId);
2712
2745
  const user = await service.getUserByIdentity(identityId);
2713
2746
  if (!user) {
2714
- return { type: "error", message: "User not found \u2014 send a message first." };
2747
+ return { type: "error", message: "Identity not found. Send a message first." };
2715
2748
  }
2716
2749
  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}` };
2750
+ await service.updateUser(user.userId, { username, ...displayName && { displayName } });
2751
+ const parts = [`@${username}`];
2752
+ if (displayName) parts.push(`"${displayName}"`);
2753
+ return { type: "text", text: `\u2705 Profile updated: ${parts.join(" ")}` };
2720
2754
  } catch (err) {
2721
2755
  const message = err instanceof Error ? err.message : String(err);
2722
2756
  return { type: "error", message };
@@ -8255,9 +8289,15 @@ var init_token_store = __esm({
8255
8289
  return;
8256
8290
  }
8257
8291
  this.tokens.clear();
8292
+ let needsMigration = false;
8258
8293
  for (const token of parsed.tokens) {
8294
+ if (!token.identitySecret) {
8295
+ token.identitySecret = randomBytes2(16).toString("hex");
8296
+ needsMigration = true;
8297
+ }
8259
8298
  this.tokens.set(token.id, token);
8260
8299
  }
8300
+ if (needsMigration) this.scheduleSave();
8261
8301
  this.codes.clear();
8262
8302
  for (const code of parsed.codes ?? []) {
8263
8303
  this.codes.set(code.code, code);
@@ -8303,7 +8343,8 @@ var init_token_store = __esm({
8303
8343
  scopes: opts.scopes,
8304
8344
  createdAt: now.toISOString(),
8305
8345
  refreshDeadline: new Date(now.getTime() + REFRESH_DEADLINE_MS).toISOString(),
8306
- revoked: false
8346
+ revoked: false,
8347
+ identitySecret: randomBytes2(16).toString("hex")
8307
8348
  };
8308
8349
  this.tokens.set(token.id, token);
8309
8350
  this.scheduleSave();
@@ -8372,6 +8413,19 @@ var init_token_store = __esm({
8372
8413
  getUserId(tokenId) {
8373
8414
  return this.tokens.get(tokenId)?.userId;
8374
8415
  }
8416
+ /**
8417
+ * Looks up a non-revoked token by its identity secret.
8418
+ *
8419
+ * Used by the identity re-link flow: the App sends the old token's identitySecret
8420
+ * to prove continuity of identity when reconnecting with a new JWT.
8421
+ * Returns undefined if no match, or if the matching token is revoked.
8422
+ */
8423
+ getByIdentitySecret(secret) {
8424
+ for (const token of this.tokens.values()) {
8425
+ if (!token.revoked && token.identitySecret === secret) return token;
8426
+ }
8427
+ return void 0;
8428
+ }
8375
8429
  /**
8376
8430
  * Generates a one-time authorization code that can be exchanged for a JWT.
8377
8431
  *
@@ -8987,7 +9041,8 @@ var init_sse_manager = __esm({
8987
9041
  BusEvent.PERMISSION_REQUEST,
8988
9042
  BusEvent.PERMISSION_RESOLVED,
8989
9043
  BusEvent.MESSAGE_QUEUED,
8990
- BusEvent.MESSAGE_PROCESSING
9044
+ BusEvent.MESSAGE_PROCESSING,
9045
+ BusEvent.MESSAGE_FAILED
8991
9046
  ];
8992
9047
  for (const eventName of events) {
8993
9048
  const handler = (data) => {
@@ -9069,7 +9124,8 @@ data: ${JSON.stringify(data)}
9069
9124
  BusEvent.PERMISSION_RESOLVED,
9070
9125
  BusEvent.SESSION_UPDATED,
9071
9126
  BusEvent.MESSAGE_QUEUED,
9072
- BusEvent.MESSAGE_PROCESSING
9127
+ BusEvent.MESSAGE_PROCESSING,
9128
+ BusEvent.MESSAGE_FAILED
9073
9129
  ];
9074
9130
  for (const res of this.sseConnections) {
9075
9131
  const filter = res.sessionFilter;
@@ -9339,7 +9395,6 @@ var sessions_exports = {};
9339
9395
  __export(sessions_exports, {
9340
9396
  sessionRoutes: () => sessionRoutes
9341
9397
  });
9342
- import { nanoid as nanoid3 } from "nanoid";
9343
9398
  async function sessionRoutes(app, deps) {
9344
9399
  app.get("/", { preHandler: requireScopes("sessions:read") }, async () => {
9345
9400
  const summaries = deps.core.sessionManager.listAllSessions();
@@ -9515,34 +9570,36 @@ async function sessionRoutes(app, deps) {
9515
9570
  attachments = await resolveAttachments(fileService, sessionId, body.attachments);
9516
9571
  }
9517
9572
  const sourceAdapterId = body.sourceAdapterId ?? "sse";
9518
- const turnId = body.turnId ?? nanoid3(8);
9519
9573
  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
9574
+ const result = await deps.core.handleMessageInSession(
9575
+ session,
9576
+ { channelId: sourceAdapterId, userId, text: body.prompt, attachments },
9577
+ { channelUser: { channelId: "sse", userId } },
9578
+ { externalTurnId: body.turnId, responseAdapterId: body.responseAdapterId }
9579
+ );
9580
+ if (!result) {
9581
+ throw new AuthError("MESSAGE_BLOCKED", "Message was blocked by a middleware plugin.", 403);
9582
+ }
9583
+ return { ok: true, sessionId, queueDepth: result.queueDepth, turnId: result.turnId };
9584
+ }
9585
+ );
9586
+ app.get(
9587
+ "/:sessionId/queue",
9588
+ { preHandler: requireScopes("sessions:read") },
9589
+ async (request) => {
9590
+ const { sessionId: rawId } = SessionIdParamSchema.parse(request.params);
9591
+ const sessionId = decodeURIComponent(rawId);
9592
+ const session = await deps.core.getOrResumeSessionById(sessionId);
9593
+ if (!session) {
9594
+ throw new NotFoundError(
9595
+ "SESSION_NOT_FOUND",
9596
+ `Session "${sessionId}" not found`
9526
9597
  );
9527
9598
  }
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
9599
  return {
9542
- ok: true,
9543
- sessionId,
9544
- queueDepth: session.queueDepth,
9545
- turnId
9600
+ pending: session.queueItems,
9601
+ processing: session.promptRunning,
9602
+ queueDepth: session.queueDepth
9546
9603
  };
9547
9604
  }
9548
9605
  );
@@ -9810,7 +9867,6 @@ var init_sessions2 = __esm({
9810
9867
  init_error_handler();
9811
9868
  init_auth();
9812
9869
  init_attachment_utils();
9813
- init_events();
9814
9870
  init_sessions();
9815
9871
  }
9816
9872
  });
@@ -10792,7 +10848,8 @@ async function authRoutes(app, deps) {
10792
10848
  tokenId: stored.id,
10793
10849
  accessToken,
10794
10850
  expiresAt,
10795
- refreshDeadline: stored.refreshDeadline
10851
+ refreshDeadline: stored.refreshDeadline,
10852
+ identitySecret: stored.identitySecret
10796
10853
  };
10797
10854
  });
10798
10855
  app.get("/tokens", {
@@ -11552,7 +11609,8 @@ function createApiServerPlugin() {
11552
11609
  accessToken,
11553
11610
  tokenId: token.id,
11554
11611
  expiresAt: new Date(Date.now() + parseDuration2(code.expire)).toISOString(),
11555
- refreshDeadline: token.refreshDeadline
11612
+ refreshDeadline: token.refreshDeadline,
11613
+ identitySecret: token.identitySecret
11556
11614
  });
11557
11615
  });
11558
11616
  }, { auth: false });
@@ -12139,14 +12197,13 @@ async function sseRoutes(app, deps) {
12139
12197
  }
12140
12198
  attachments = await resolveAttachments(fileService, sessionId, body.attachments);
12141
12199
  }
12142
- const queueDepth = session.queueDepth + 1;
12143
12200
  const userId = request.auth?.tokenId ?? "api";
12144
- await deps.core.handleMessageInSession(
12201
+ const { turnId, queueDepth } = await deps.core.handleMessageInSession(
12145
12202
  session,
12146
12203
  { channelId: "sse", userId, text: body.prompt, attachments },
12147
12204
  { channelUser: { channelId: "sse", userId } }
12148
12205
  );
12149
- return { ok: true, sessionId, queueDepth };
12206
+ return { ok: true, sessionId, queueDepth, turnId };
12150
12207
  }
12151
12208
  );
12152
12209
  app.post(
@@ -16363,7 +16420,7 @@ var init_commands3 = __esm({
16363
16420
 
16364
16421
  // src/plugins/telegram/permissions.ts
16365
16422
  import { InlineKeyboard as InlineKeyboard11 } from "grammy";
16366
- import { nanoid as nanoid4 } from "nanoid";
16423
+ import { nanoid as nanoid3 } from "nanoid";
16367
16424
  var log26, PermissionHandler;
16368
16425
  var init_permissions = __esm({
16369
16426
  "src/plugins/telegram/permissions.ts"() {
@@ -16390,7 +16447,7 @@ var init_permissions = __esm({
16390
16447
  */
16391
16448
  async sendPermissionRequest(session, request) {
16392
16449
  const threadId = Number(session.threadId);
16393
- const callbackKey = nanoid4(8);
16450
+ const callbackKey = nanoid3(8);
16394
16451
  this.pending.set(callbackKey, {
16395
16452
  sessionId: session.id,
16396
16453
  requestId: request.id,
@@ -18401,7 +18458,11 @@ var init_adapter2 = __esm({
18401
18458
  }
18402
18459
  return prev(method, payload, signal);
18403
18460
  });
18404
- this.registerCommandsWithRetry();
18461
+ const onCommandsReady = ({ commands }) => {
18462
+ this.core.eventBus.off(BusEvent.SYSTEM_COMMANDS_READY, onCommandsReady);
18463
+ this.syncCommandsWithRetry(commands);
18464
+ };
18465
+ this.core.eventBus.on(BusEvent.SYSTEM_COMMANDS_READY, onCommandsReady);
18405
18466
  this.bot.use((ctx, next) => {
18406
18467
  const chatId = ctx.chat?.id ?? ctx.callbackQuery?.message?.chat?.id;
18407
18468
  if (chatId !== this.telegramConfig.chatId) return;
@@ -18627,12 +18688,16 @@ ${p2}` : p2;
18627
18688
  throw new Error("unreachable");
18628
18689
  }
18629
18690
  /**
18630
- * Register Telegram commands in the background with retries.
18631
- * Non-critical bot works fine without autocomplete commands.
18691
+ * Sync Telegram autocomplete commands after all plugins are ready.
18692
+ * Merges STATIC_COMMANDS (hardcoded system commands) with plugin commands
18693
+ * from the registry, deduplicating by command name. Non-critical.
18632
18694
  */
18633
- registerCommandsWithRetry() {
18695
+ syncCommandsWithRetry(registryCommands) {
18696
+ const staticNames = new Set(STATIC_COMMANDS.map((c3) => c3.command));
18697
+ 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) }));
18698
+ const allCommands = [...STATIC_COMMANDS, ...pluginCommands].slice(0, 100);
18634
18699
  this.retryWithBackoff(
18635
- () => this.bot.api.setMyCommands(STATIC_COMMANDS, {
18700
+ () => this.bot.api.setMyCommands(allCommands, {
18636
18701
  scope: { type: "chat", chat_id: this.telegramConfig.chatId }
18637
18702
  }),
18638
18703
  "setMyCommands"
@@ -22161,16 +22226,16 @@ var init_prompt_queue = __esm({
22161
22226
  * immediately. Otherwise, it's buffered and the returned promise resolves
22162
22227
  * only after the prompt finishes processing.
22163
22228
  */
22164
- async enqueue(text5, attachments, routing, turnId, meta) {
22229
+ async enqueue(text5, userPrompt, attachments, routing, turnId, meta) {
22165
22230
  if (this.processing) {
22166
22231
  return new Promise((resolve9) => {
22167
- this.queue.push({ text: text5, attachments, routing, turnId, meta, resolve: resolve9 });
22232
+ this.queue.push({ text: text5, userPrompt, attachments, routing, turnId, meta, resolve: resolve9 });
22168
22233
  });
22169
22234
  }
22170
- await this.process(text5, attachments, routing, turnId, meta);
22235
+ await this.process(text5, userPrompt, attachments, routing, turnId, meta);
22171
22236
  }
22172
22237
  /** Run a single prompt through the processor, then drain the next queued item. */
22173
- async process(text5, attachments, routing, turnId, meta) {
22238
+ async process(text5, userPrompt, attachments, routing, turnId, meta) {
22174
22239
  this.processing = true;
22175
22240
  this.abortController = new AbortController();
22176
22241
  const { signal } = this.abortController;
@@ -22180,7 +22245,7 @@ var init_prompt_queue = __esm({
22180
22245
  });
22181
22246
  try {
22182
22247
  await Promise.race([
22183
- this.processor(text5, attachments, routing, turnId, meta),
22248
+ this.processor(text5, userPrompt, attachments, routing, turnId, meta),
22184
22249
  new Promise((_, reject) => {
22185
22250
  signal.addEventListener("abort", () => reject(new Error("Prompt aborted")), { once: true });
22186
22251
  })
@@ -22201,7 +22266,7 @@ var init_prompt_queue = __esm({
22201
22266
  drainNext() {
22202
22267
  const next = this.queue.shift();
22203
22268
  if (next) {
22204
- this.process(next.text, next.attachments, next.routing, next.turnId, next.meta).then(next.resolve);
22269
+ this.process(next.text, next.userPrompt, next.attachments, next.routing, next.turnId, next.meta).then(next.resolve);
22205
22270
  }
22206
22271
  }
22207
22272
  /**
@@ -22223,6 +22288,13 @@ var init_prompt_queue = __esm({
22223
22288
  get isProcessing() {
22224
22289
  return this.processing;
22225
22290
  }
22291
+ /** Snapshot of queued (not yet processing) items — used for queue inspection by callers. */
22292
+ get pendingItems() {
22293
+ return this.queue.map((item) => ({
22294
+ userPrompt: item.userPrompt,
22295
+ turnId: item.turnId
22296
+ }));
22297
+ }
22226
22298
  };
22227
22299
  }
22228
22300
  });
@@ -22307,18 +22379,32 @@ var init_permission_gate = __esm({
22307
22379
  });
22308
22380
 
22309
22381
  // src/core/sessions/turn-context.ts
22310
- import { nanoid as nanoid5 } from "nanoid";
22311
- function createTurnContext(sourceAdapterId, responseAdapterId, turnId) {
22382
+ import { nanoid as nanoid4 } from "nanoid";
22383
+ function extractSender(meta) {
22384
+ const identity = meta?.identity;
22385
+ if (!identity || !identity.userId || !identity.identityId) return null;
22312
22386
  return {
22313
- turnId: turnId ?? nanoid5(8),
22314
- sourceAdapterId,
22315
- responseAdapterId
22387
+ userId: identity.userId,
22388
+ identityId: identity.identityId,
22389
+ displayName: identity.displayName,
22390
+ username: identity.username
22316
22391
  };
22317
22392
  }
22318
22393
  function getEffectiveTarget(ctx) {
22319
22394
  if (ctx.responseAdapterId === null) return null;
22320
22395
  return ctx.responseAdapterId ?? ctx.sourceAdapterId;
22321
22396
  }
22397
+ function createTurnContext(sourceAdapterId, responseAdapterId, turnId, userPrompt, finalPrompt, attachments, meta) {
22398
+ return {
22399
+ turnId: turnId ?? nanoid4(8),
22400
+ sourceAdapterId,
22401
+ responseAdapterId,
22402
+ userPrompt,
22403
+ finalPrompt,
22404
+ attachments,
22405
+ meta
22406
+ };
22407
+ }
22322
22408
  function isSystemEvent(event) {
22323
22409
  return SYSTEM_EVENT_TYPES.has(event.type);
22324
22410
  }
@@ -22338,7 +22424,7 @@ var init_turn_context = __esm({
22338
22424
  });
22339
22425
 
22340
22426
  // src/core/sessions/session.ts
22341
- import { nanoid as nanoid6 } from "nanoid";
22427
+ import { nanoid as nanoid5 } from "nanoid";
22342
22428
  import * as fs41 from "fs";
22343
22429
  var moduleLog, TTS_PROMPT_INSTRUCTION, TTS_BLOCK_REGEX, TTS_MAX_LENGTH, TTS_TIMEOUT_MS, VALID_TRANSITIONS, Session;
22344
22430
  var init_session2 = __esm({
@@ -22418,7 +22504,7 @@ Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of y
22418
22504
  pendingContext = null;
22419
22505
  constructor(opts) {
22420
22506
  super();
22421
- this.id = opts.id || nanoid6(12);
22507
+ this.id = opts.id || nanoid5(12);
22422
22508
  this.channelId = opts.channelId;
22423
22509
  this.attachedAdapters = [opts.channelId];
22424
22510
  this.agentName = opts.agentName;
@@ -22430,7 +22516,7 @@ Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of y
22430
22516
  this.log = createSessionLogger(this.id, moduleLog);
22431
22517
  this.log.info({ agentName: this.agentName }, "Session created");
22432
22518
  this.queue = new PromptQueue(
22433
- (text5, attachments, routing, turnId, meta) => this.processPrompt(text5, attachments, routing, turnId, meta),
22519
+ (text5, userPrompt, attachments, routing, turnId, meta) => this.processPrompt(text5, userPrompt, attachments, routing, turnId, meta),
22434
22520
  (err) => {
22435
22521
  this.log.error({ err }, "Prompt execution failed");
22436
22522
  const message = err instanceof Error ? err.message : String(err);
@@ -22510,6 +22596,10 @@ Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of y
22510
22596
  get promptRunning() {
22511
22597
  return this.queue.isProcessing;
22512
22598
  }
22599
+ /** Snapshot of queued (not yet processing) items — for inspection by API consumers. */
22600
+ get queueItems() {
22601
+ return this.queue.pendingItems;
22602
+ }
22513
22603
  // --- Context Injection ---
22514
22604
  /** Store context markdown to be prepended to the next prompt (used for session resume with history). */
22515
22605
  setContext(markdown) {
@@ -22530,24 +22620,30 @@ Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of y
22530
22620
  * queued/processing events before the prompt actually runs.
22531
22621
  */
22532
22622
  async enqueuePrompt(text5, attachments, routing, externalTurnId, meta) {
22533
- const turnId = externalTurnId ?? nanoid6(8);
22623
+ const turnId = externalTurnId ?? nanoid5(8);
22534
22624
  const turnMeta = meta ?? { turnId };
22625
+ const userPrompt = text5;
22535
22626
  if (this.middlewareChain) {
22536
22627
  const payload = { text: text5, attachments, sessionId: this.id, sourceAdapterId: routing?.sourceAdapterId, meta: turnMeta };
22537
22628
  const result = await this.middlewareChain.execute(Hook.AGENT_BEFORE_PROMPT, payload, async (p2) => p2);
22538
- if (!result) return turnId;
22629
+ if (!result) throw new Error("PROMPT_BLOCKED");
22539
22630
  text5 = result.text;
22540
22631
  attachments = result.attachments;
22541
22632
  }
22542
- await this.queue.enqueue(text5, attachments, routing, turnId, turnMeta);
22633
+ await this.queue.enqueue(text5, userPrompt, attachments, routing, turnId, turnMeta);
22543
22634
  return turnId;
22544
22635
  }
22545
- async processPrompt(text5, attachments, routing, turnId, meta) {
22636
+ async processPrompt(text5, userPrompt, attachments, routing, turnId, meta) {
22546
22637
  if (this._status === "finished") return;
22547
22638
  this.activeTurnContext = createTurnContext(
22548
22639
  routing?.sourceAdapterId ?? this.channelId,
22549
22640
  routing?.responseAdapterId,
22550
- turnId
22641
+ turnId,
22642
+ userPrompt,
22643
+ text5,
22644
+ // finalPrompt (after middleware transformations)
22645
+ attachments,
22646
+ meta
22551
22647
  );
22552
22648
  this.emit(SessionEv.TURN_STARTED, this.activeTurnContext);
22553
22649
  this.promptCount++;
@@ -22598,7 +22694,16 @@ ${text5}`;
22598
22694
  this.agentInstance.on(SessionEv.AGENT_EVENT, afterEventListener);
22599
22695
  }
22600
22696
  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(() => {
22697
+ this.middlewareChain.execute(Hook.TURN_START, {
22698
+ sessionId: this.id,
22699
+ promptText: processed.text,
22700
+ promptNumber: this.promptCount,
22701
+ turnId: this.activeTurnContext?.turnId ?? turnId ?? "",
22702
+ meta,
22703
+ userPrompt: this.activeTurnContext?.userPrompt,
22704
+ sourceAdapterId: this.activeTurnContext?.sourceAdapterId,
22705
+ responseAdapterId: this.activeTurnContext?.responseAdapterId
22706
+ }, async (p2) => p2).catch(() => {
22602
22707
  });
22603
22708
  }
22604
22709
  let stopReason = "end_turn";
@@ -23269,7 +23374,7 @@ var init_session_bridge = __esm({
23269
23374
  if (this.shouldForward(event)) {
23270
23375
  this.dispatchAgentEvent(event);
23271
23376
  } else {
23272
- this.deps.eventBus?.emit(BusEvent.AGENT_EVENT, { sessionId: this.session.id, event });
23377
+ this.deps.eventBus?.emit(BusEvent.AGENT_EVENT, { sessionId: this.session.id, turnId: "", event });
23273
23378
  }
23274
23379
  });
23275
23380
  if (!this.session.agentInstance.onPermissionRequest || this.session.agentInstance.onPermissionRequest.__bridgeId === void 0) {
@@ -23322,14 +23427,16 @@ var init_session_bridge = __esm({
23322
23427
  this.deps.sessionManager.patchRecord(this.session.id, { currentPromptCount: count });
23323
23428
  });
23324
23429
  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
- }
23430
+ this.deps.eventBus?.emit(BusEvent.MESSAGE_PROCESSING, {
23431
+ sessionId: this.session.id,
23432
+ turnId: ctx.turnId,
23433
+ sourceAdapterId: ctx.sourceAdapterId,
23434
+ userPrompt: ctx.userPrompt,
23435
+ finalPrompt: ctx.finalPrompt,
23436
+ attachments: ctx.attachments,
23437
+ sender: extractSender(ctx.meta),
23438
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
23439
+ });
23333
23440
  });
23334
23441
  if (this.session.latestCommands !== null) {
23335
23442
  this.session.emit(SessionEv.AGENT_EVENT, { type: "commands_update", commands: this.session.latestCommands });
@@ -23495,6 +23602,7 @@ var init_session_bridge = __esm({
23495
23602
  }
23496
23603
  this.deps.eventBus?.emit(BusEvent.AGENT_EVENT, {
23497
23604
  sessionId: this.session.id,
23605
+ turnId: this.session.activeTurnContext?.turnId ?? "",
23498
23606
  event
23499
23607
  });
23500
23608
  return outgoing;
@@ -24408,6 +24516,7 @@ var init_session_factory = __esm({
24408
24516
  const failedSessionId = createParams.existingSessionId ?? `failed-${Date.now()}`;
24409
24517
  this.eventBus.emit(BusEvent.AGENT_EVENT, {
24410
24518
  sessionId: failedSessionId,
24519
+ turnId: "",
24411
24520
  event: guidance
24412
24521
  });
24413
24522
  throw err;
@@ -24778,7 +24887,7 @@ var init_agent_switch_handler = __esm({
24778
24887
  message: `Switching from ${fromAgent} to ${toAgent}...`
24779
24888
  };
24780
24889
  session.emit(SessionEv.AGENT_EVENT, startEvent);
24781
- eventBus.emit(BusEvent.AGENT_EVENT, { sessionId, event: startEvent });
24890
+ eventBus.emit(BusEvent.AGENT_EVENT, { sessionId, turnId: "", event: startEvent });
24782
24891
  eventBus.emit(BusEvent.SESSION_AGENT_SWITCH, {
24783
24892
  sessionId,
24784
24893
  fromAgent,
@@ -24841,7 +24950,7 @@ var init_agent_switch_handler = __esm({
24841
24950
  message: resumed ? `Switched to ${toAgent} (resumed previous session).` : `Switched to ${toAgent} (new session).`
24842
24951
  };
24843
24952
  session.emit(SessionEv.AGENT_EVENT, successEvent);
24844
- eventBus.emit(BusEvent.AGENT_EVENT, { sessionId, event: successEvent });
24953
+ eventBus.emit(BusEvent.AGENT_EVENT, { sessionId, turnId: "", event: successEvent });
24845
24954
  eventBus.emit(BusEvent.SESSION_AGENT_SWITCH, {
24846
24955
  sessionId,
24847
24956
  fromAgent,
@@ -24856,7 +24965,7 @@ var init_agent_switch_handler = __esm({
24856
24965
  message: `Failed to switch to ${toAgent}: ${errorMessage}`
24857
24966
  };
24858
24967
  session.emit(SessionEv.AGENT_EVENT, failedEvent);
24859
- eventBus.emit(BusEvent.AGENT_EVENT, { sessionId, event: failedEvent });
24968
+ eventBus.emit(BusEvent.AGENT_EVENT, { sessionId, turnId: "", event: failedEvent });
24860
24969
  eventBus.emit(BusEvent.SESSION_AGENT_SWITCH, {
24861
24970
  sessionId,
24862
24971
  fromAgent,
@@ -26769,7 +26878,7 @@ var init_core_items = __esm({
26769
26878
 
26770
26879
  // src/core/core.ts
26771
26880
  import path51 from "path";
26772
- import { nanoid as nanoid7 } from "nanoid";
26881
+ import { nanoid as nanoid6 } from "nanoid";
26773
26882
  var log44, OpenACPCore;
26774
26883
  var init_core = __esm({
26775
26884
  "src/core/core.ts"() {
@@ -26795,6 +26904,7 @@ var init_core = __esm({
26795
26904
  init_error_tracker();
26796
26905
  init_log();
26797
26906
  init_events();
26907
+ init_turn_context();
26798
26908
  log44 = createChildLogger({ module: "core" });
26799
26909
  OpenACPCore = class {
26800
26910
  configManager;
@@ -27080,7 +27190,7 @@ var init_core = __esm({
27080
27190
  },
27081
27191
  "Incoming message"
27082
27192
  );
27083
- const turnId = nanoid7(8);
27193
+ const turnId = nanoid6(8);
27084
27194
  const meta = { turnId, ...initialMeta };
27085
27195
  if (this.lifecycleManager?.middlewareChain) {
27086
27196
  const result = await this.lifecycleManager.middlewareChain.execute(
@@ -27120,9 +27230,6 @@ var init_core = __esm({
27120
27230
  }
27121
27231
  return;
27122
27232
  }
27123
- this.sessionManager.patchRecord(session.id, {
27124
- lastActiveAt: (/* @__PURE__ */ new Date()).toISOString()
27125
- });
27126
27233
  let text5 = message.text;
27127
27234
  if (this.assistantManager?.isAssistant(session.id)) {
27128
27235
  const pending = this.assistantManager.consumePendingSystemPrompt(message.channelId);
@@ -27135,38 +27242,56 @@ User message:
27135
27242
  ${text5}`;
27136
27243
  }
27137
27244
  }
27138
- const sourceAdapterId = message.routing?.sourceAdapterId ?? message.channelId;
27139
- const routing = sourceAdapterId !== message.routing?.sourceAdapterId ? { ...message.routing, sourceAdapterId } : message.routing;
27140
27245
  const enrichedMeta = message.meta ?? meta;
27141
- if (sourceAdapterId && sourceAdapterId !== "sse" && sourceAdapterId !== "api") {
27142
- this.eventBus.emit(BusEvent.MESSAGE_QUEUED, {
27246
+ await this._dispatchToSession(session, text5, message.attachments, {
27247
+ sourceAdapterId: message.routing?.sourceAdapterId ?? message.channelId,
27248
+ responseAdapterId: message.routing?.responseAdapterId
27249
+ }, turnId, enrichedMeta);
27250
+ }
27251
+ /**
27252
+ * Shared dispatch path for sending a prompt to a session.
27253
+ * Called by both handleMessage (Telegram) and handleMessageInSession (SSE/API)
27254
+ * after their respective middleware/enrichment steps.
27255
+ */
27256
+ async _dispatchToSession(session, text5, attachments, routing, turnId, meta) {
27257
+ this.sessionManager.patchRecord(session.id, {
27258
+ lastActiveAt: (/* @__PURE__ */ new Date()).toISOString()
27259
+ });
27260
+ this.eventBus.emit(BusEvent.MESSAGE_QUEUED, {
27261
+ sessionId: session.id,
27262
+ turnId,
27263
+ text: text5,
27264
+ sourceAdapterId: routing.sourceAdapterId,
27265
+ attachments,
27266
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
27267
+ queueDepth: session.queueDepth + 1,
27268
+ sender: extractSender(meta)
27269
+ });
27270
+ session.enqueuePrompt(text5, attachments, routing, turnId, meta).catch((err) => {
27271
+ const reason = err instanceof Error ? err.message : String(err);
27272
+ log44.warn({ err, sessionId: session.id, turnId, reason }, "enqueuePrompt failed \u2014 emitting message:failed");
27273
+ this.eventBus.emit(BusEvent.MESSAGE_FAILED, {
27143
27274
  sessionId: session.id,
27144
27275
  turnId,
27145
- text: text5,
27146
- sourceAdapterId,
27147
- attachments: message.attachments,
27148
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
27149
- queueDepth: session.queueDepth
27276
+ reason
27150
27277
  });
27151
- await session.enqueuePrompt(text5, message.attachments, routing, turnId, enrichedMeta);
27152
- } else {
27153
- await session.enqueuePrompt(text5, message.attachments, routing, turnId, enrichedMeta);
27154
- }
27278
+ });
27155
27279
  }
27156
27280
  /**
27157
27281
  * Send a message to a known session, running the full message:incoming → agent:beforePrompt
27158
27282
  * middleware chain (same as handleMessage) but without the threadId-based session lookup.
27159
27283
  *
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).
27284
+ * Used by channels that already hold a direct session reference (e.g. SSE adapter, api-server),
27285
+ * where looking up by channelId+threadId is unreliable (API sessions may have no threadId).
27162
27286
  *
27163
27287
  * @param session The target session — caller is responsible for validating its status.
27164
27288
  * @param message Sender context and message content.
27165
27289
  * @param initialMeta Optional adapter-specific context to seed the TurnMeta bag
27166
27290
  * (e.g. channelUser with display name/username).
27291
+ * @param options Optional turnId override and response routing.
27167
27292
  */
27168
- async handleMessageInSession(session, message, initialMeta) {
27169
- const turnId = nanoid7(8);
27293
+ async handleMessageInSession(session, message, initialMeta, options) {
27294
+ const turnId = options?.externalTurnId ?? nanoid6(8);
27170
27295
  const meta = { turnId, ...initialMeta };
27171
27296
  let text5 = message.text;
27172
27297
  let { attachments } = message;
@@ -27185,13 +27310,17 @@ ${text5}`;
27185
27310
  payload,
27186
27311
  async (p2) => p2
27187
27312
  );
27188
- if (!result) return;
27313
+ if (!result) return { turnId, queueDepth: session.queueDepth };
27189
27314
  text5 = result.text;
27190
27315
  attachments = result.attachments;
27191
27316
  enrichedMeta = result.meta ?? meta;
27192
27317
  }
27193
- const routing = { sourceAdapterId: message.channelId };
27194
- await session.enqueuePrompt(text5, attachments, routing, turnId, enrichedMeta);
27318
+ const routing = {
27319
+ sourceAdapterId: message.channelId,
27320
+ responseAdapterId: options?.responseAdapterId
27321
+ };
27322
+ await this._dispatchToSession(session, text5, attachments, routing, turnId, enrichedMeta);
27323
+ return { turnId, queueDepth: session.queueDepth };
27195
27324
  }
27196
27325
  // --- Unified Session Creation Pipeline ---
27197
27326
  /**
@@ -27303,7 +27432,7 @@ ${text5}`;
27303
27432
  } else if (processedEvent.type === "error") {
27304
27433
  session.fail(processedEvent.message);
27305
27434
  }
27306
- this.eventBus.emit(BusEvent.AGENT_EVENT, { sessionId: session.id, event: processedEvent });
27435
+ this.eventBus.emit(BusEvent.AGENT_EVENT, { sessionId: session.id, turnId: session.activeTurnContext?.turnId ?? "", event: processedEvent });
27307
27436
  });
27308
27437
  session.on(SessionEv.STATUS_CHANGE, (_from, to) => {
27309
27438
  this.sessionManager.patchRecord(session.id, {
@@ -27315,6 +27444,18 @@ ${text5}`;
27315
27444
  session.on(SessionEv.PROMPT_COUNT_CHANGED, (count) => {
27316
27445
  this.sessionManager.patchRecord(session.id, { currentPromptCount: count });
27317
27446
  });
27447
+ session.on(SessionEv.TURN_STARTED, (ctx) => {
27448
+ this.eventBus.emit(BusEvent.MESSAGE_PROCESSING, {
27449
+ sessionId: session.id,
27450
+ turnId: ctx.turnId,
27451
+ sourceAdapterId: ctx.sourceAdapterId,
27452
+ userPrompt: ctx.userPrompt,
27453
+ finalPrompt: ctx.finalPrompt,
27454
+ attachments: ctx.attachments,
27455
+ sender: extractSender(ctx.meta),
27456
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
27457
+ });
27458
+ });
27318
27459
  }
27319
27460
  this.sessionFactory.wireSideEffects(session, {
27320
27461
  eventBus: this.eventBus,