@openacp/cli 2026.410.3 → 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/index.js CHANGED
@@ -793,6 +793,8 @@ var init_events = __esm({
793
793
  TURN_START: "turn:start",
794
794
  /** Turn ended (always fires, even on error) — read-only, fire-and-forget. */
795
795
  TURN_END: "turn:end",
796
+ /** After a turn completes — full assembled agent text, read-only, fire-and-forget. */
797
+ AGENT_AFTER_TURN: "agent:afterTurn",
796
798
  // --- Session lifecycle ---
797
799
  /** Before a new session is created — modifiable, can block. */
798
800
  SESSION_BEFORE_CREATE: "session:beforeCreate",
@@ -850,6 +852,8 @@ var init_events = __esm({
850
852
  MESSAGE_QUEUED: "message:queued",
851
853
  /** Fired when a queued message starts processing. */
852
854
  MESSAGE_PROCESSING: "message:processing",
855
+ /** Fired when a queued message is rejected (e.g. blocked by middleware). */
856
+ MESSAGE_FAILED: "message:failed",
853
857
  // --- System lifecycle ---
854
858
  /** Fired after kernel (core + plugin infrastructure) has booted. */
855
859
  KERNEL_BOOTED: "kernel:booted",
@@ -870,7 +874,22 @@ var init_events = __esm({
870
874
  PLUGIN_UNLOADED: "plugin:unloaded",
871
875
  // --- Usage ---
872
876
  /** Fired when a token usage record is captured (consumed by usage plugin). */
873
- USAGE_RECORDED: "usage:recorded"
877
+ USAGE_RECORDED: "usage:recorded",
878
+ // --- Identity lifecycle ---
879
+ /** Fired when a new user+identity record is created. */
880
+ IDENTITY_CREATED: "identity:created",
881
+ /** Fired when user profile fields change. */
882
+ IDENTITY_UPDATED: "identity:updated",
883
+ /** Fired when two identities are linked (same person). */
884
+ IDENTITY_LINKED: "identity:linked",
885
+ /** Fired when an identity is unlinked into a new user. */
886
+ IDENTITY_UNLINKED: "identity:unlinked",
887
+ /** Fired when two user records are merged during a link operation. */
888
+ IDENTITY_USER_MERGED: "identity:userMerged",
889
+ /** Fired when a user's role changes. */
890
+ IDENTITY_ROLE_CHANGED: "identity:roleChanged",
891
+ /** Fired when a user is seen (throttled). */
892
+ IDENTITY_SEEN: "identity:seen"
874
893
  };
875
894
  SessionEv = {
876
895
  /** Agent produced an event (text, tool_call, etc.) during a turn. */
@@ -2829,14 +2848,20 @@ var init_api_client = __esm({
2829
2848
  });
2830
2849
 
2831
2850
  // src/plugins/notifications/notification.ts
2832
- var NotificationManager;
2851
+ var NotificationService;
2833
2852
  var init_notification = __esm({
2834
2853
  "src/plugins/notifications/notification.ts"() {
2835
2854
  "use strict";
2836
- NotificationManager = class {
2855
+ NotificationService = class {
2837
2856
  constructor(adapters) {
2838
2857
  this.adapters = adapters;
2839
2858
  }
2859
+ identityResolver;
2860
+ /** Inject identity resolver for user-targeted notifications. */
2861
+ setIdentityResolver(resolver) {
2862
+ this.identityResolver = resolver;
2863
+ }
2864
+ // --- Legacy API (backward compat with NotificationManager) ---
2840
2865
  /**
2841
2866
  * Send a notification to a specific channel adapter.
2842
2867
  *
@@ -2865,6 +2890,62 @@ var init_notification = __esm({
2865
2890
  }
2866
2891
  }
2867
2892
  }
2893
+ // --- New user-targeted API ---
2894
+ /**
2895
+ * Send a notification to a user across all their linked platforms.
2896
+ * Fire-and-forget — never throws, swallows all errors.
2897
+ */
2898
+ async notifyUser(target, message, options) {
2899
+ try {
2900
+ await this._resolveAndDeliver(target, message, options);
2901
+ } catch {
2902
+ }
2903
+ }
2904
+ async _resolveAndDeliver(target, message, options) {
2905
+ if ("channelId" in target && "platformId" in target) {
2906
+ const adapter = this.adapters.get(target.channelId);
2907
+ if (!adapter?.sendUserNotification) return;
2908
+ await adapter.sendUserNotification(target.platformId, message, {
2909
+ via: options?.via,
2910
+ topicId: options?.topicId,
2911
+ sessionId: options?.sessionId
2912
+ });
2913
+ return;
2914
+ }
2915
+ if (!this.identityResolver) return;
2916
+ let identities = [];
2917
+ if ("identityId" in target) {
2918
+ const identity = await this.identityResolver.getIdentity(target.identityId);
2919
+ if (!identity) return;
2920
+ const user = await this.identityResolver.getUser(identity.userId);
2921
+ if (!user) return;
2922
+ identities = await this.identityResolver.getIdentitiesFor(user.userId);
2923
+ } else if ("userId" in target) {
2924
+ identities = await this.identityResolver.getIdentitiesFor(target.userId);
2925
+ }
2926
+ if (options?.onlyPlatforms) {
2927
+ identities = identities.filter((i) => options.onlyPlatforms.includes(i.source));
2928
+ }
2929
+ if (options?.excludePlatforms) {
2930
+ identities = identities.filter((i) => !options.excludePlatforms.includes(i.source));
2931
+ }
2932
+ for (const identity of identities) {
2933
+ const adapter = this.adapters.get(identity.source);
2934
+ if (!adapter?.sendUserNotification) continue;
2935
+ try {
2936
+ await adapter.sendUserNotification(identity.platformId, message, {
2937
+ via: options?.via,
2938
+ topicId: options?.topicId,
2939
+ sessionId: options?.sessionId,
2940
+ platformMention: {
2941
+ platformUsername: identity.platformUsername,
2942
+ platformId: identity.platformId
2943
+ }
2944
+ });
2945
+ } catch {
2946
+ }
2947
+ }
2948
+ }
2868
2949
  };
2869
2950
  }
2870
2951
  });
@@ -3356,9 +3437,16 @@ async function createApiServer(options) {
3356
3437
  });
3357
3438
  const authPreHandler = createAuthPreHandler(options.getSecret, options.getJwtSecret, options.tokenStore);
3358
3439
  app.decorateRequest("auth", null, []);
3440
+ let booted = false;
3441
+ app.addHook("onReady", async () => {
3442
+ booted = true;
3443
+ });
3359
3444
  return {
3360
3445
  app,
3361
3446
  registerPlugin(prefix, plugin, opts) {
3447
+ if (booted) {
3448
+ return;
3449
+ }
3362
3450
  const wrappedPlugin = async (pluginApp, pluginOpts) => {
3363
3451
  if (opts?.auth !== false) {
3364
3452
  pluginApp.addHook("onRequest", authPreHandler);
@@ -3452,7 +3540,8 @@ var init_sse_manager = __esm({
3452
3540
  BusEvent.PERMISSION_REQUEST,
3453
3541
  BusEvent.PERMISSION_RESOLVED,
3454
3542
  BusEvent.MESSAGE_QUEUED,
3455
- BusEvent.MESSAGE_PROCESSING
3543
+ BusEvent.MESSAGE_PROCESSING,
3544
+ BusEvent.MESSAGE_FAILED
3456
3545
  ];
3457
3546
  for (const eventName of events) {
3458
3547
  const handler = (data) => {
@@ -3534,7 +3623,8 @@ data: ${JSON.stringify(data)}
3534
3623
  BusEvent.PERMISSION_RESOLVED,
3535
3624
  BusEvent.SESSION_UPDATED,
3536
3625
  BusEvent.MESSAGE_QUEUED,
3537
- BusEvent.MESSAGE_PROCESSING
3626
+ BusEvent.MESSAGE_PROCESSING,
3627
+ BusEvent.MESSAGE_FAILED
3538
3628
  ];
3539
3629
  for (const res of this.sseConnections) {
3540
3630
  const filter = res.sessionFilter;
@@ -10024,7 +10114,11 @@ var init_adapter = __esm({
10024
10114
  }
10025
10115
  return prev(method, payload, signal);
10026
10116
  });
10027
- this.registerCommandsWithRetry();
10117
+ const onCommandsReady = ({ commands }) => {
10118
+ this.core.eventBus.off(BusEvent.SYSTEM_COMMANDS_READY, onCommandsReady);
10119
+ this.syncCommandsWithRetry(commands);
10120
+ };
10121
+ this.core.eventBus.on(BusEvent.SYSTEM_COMMANDS_READY, onCommandsReady);
10028
10122
  this.bot.use((ctx, next) => {
10029
10123
  const chatId = ctx.chat?.id ?? ctx.callbackQuery?.message?.chat?.id;
10030
10124
  if (chatId !== this.telegramConfig.chatId) return;
@@ -10148,10 +10242,19 @@ var init_adapter = __esm({
10148
10242
  });
10149
10243
  } catch {
10150
10244
  }
10151
- } else if (response.type === "text" || response.type === "error") {
10152
- const text3 = response.type === "text" ? response.text : `\u274C ${response.message}`;
10245
+ } else if (response.type === "text" || response.type === "error" || response.type === "adaptive") {
10246
+ let text3;
10247
+ let parseMode;
10248
+ if (response.type === "adaptive") {
10249
+ const variant = response.variants?.["telegram"];
10250
+ text3 = variant?.text ?? response.fallback;
10251
+ parseMode = variant?.parse_mode;
10252
+ } else {
10253
+ text3 = response.type === "text" ? response.text : `\u274C ${response.message}`;
10254
+ parseMode = "Markdown";
10255
+ }
10153
10256
  try {
10154
- await ctx.editMessageText(text3, { parse_mode: "Markdown" });
10257
+ await ctx.editMessageText(text3, { ...parseMode && { parse_mode: parseMode } });
10155
10258
  } catch {
10156
10259
  }
10157
10260
  }
@@ -10241,12 +10344,16 @@ ${p}` : p;
10241
10344
  throw new Error("unreachable");
10242
10345
  }
10243
10346
  /**
10244
- * Register Telegram commands in the background with retries.
10245
- * Non-critical bot works fine without autocomplete commands.
10347
+ * Sync Telegram autocomplete commands after all plugins are ready.
10348
+ * Merges STATIC_COMMANDS (hardcoded system commands) with plugin commands
10349
+ * from the registry, deduplicating by command name. Non-critical.
10246
10350
  */
10247
- registerCommandsWithRetry() {
10351
+ syncCommandsWithRetry(registryCommands) {
10352
+ const staticNames = new Set(STATIC_COMMANDS.map((c2) => c2.command));
10353
+ const pluginCommands = registryCommands.filter((c2) => c2.category === "plugin" && !staticNames.has(c2.name) && /^[a-z0-9_]+$/.test(c2.name)).map((c2) => ({ command: c2.name, description: c2.description.slice(0, 256) }));
10354
+ const allCommands = [...STATIC_COMMANDS, ...pluginCommands].slice(0, 100);
10248
10355
  this.retryWithBackoff(
10249
- () => this.bot.api.setMyCommands(STATIC_COMMANDS, {
10356
+ () => this.bot.api.setMyCommands(allCommands, {
10250
10357
  scope: { type: "chat", chat_id: this.telegramConfig.chatId }
10251
10358
  }),
10252
10359
  "setMyCommands"
@@ -10433,6 +10540,15 @@ OpenACP will automatically retry until this is resolved.`;
10433
10540
  message_thread_id: topicId
10434
10541
  });
10435
10542
  break;
10543
+ case "adaptive": {
10544
+ const variant = response.variants?.["telegram"];
10545
+ const text3 = variant?.text ?? response.fallback;
10546
+ await this.bot.api.sendMessage(chatId, text3, {
10547
+ message_thread_id: topicId,
10548
+ ...variant?.parse_mode && { parse_mode: variant.parse_mode }
10549
+ });
10550
+ break;
10551
+ }
10436
10552
  case "error":
10437
10553
  await this.bot.api.sendMessage(
10438
10554
  chatId,
@@ -10541,12 +10657,25 @@ ${lines.join("\n")}`;
10541
10657
  }
10542
10658
  ctx.replyWithChatAction("typing").catch(() => {
10543
10659
  });
10544
- this.core.handleMessage({
10545
- channelId: "telegram",
10546
- threadId: String(threadId),
10547
- userId: String(ctx.from.id),
10548
- text: forwardText
10549
- }).catch((err) => log36.error({ err }, "handleMessage error"));
10660
+ const fromName = [ctx.from.first_name, ctx.from.last_name].filter(Boolean).join(" ") || void 0;
10661
+ this.core.handleMessage(
10662
+ {
10663
+ channelId: "telegram",
10664
+ threadId: String(threadId),
10665
+ userId: String(ctx.from.id),
10666
+ text: forwardText
10667
+ },
10668
+ // Inject structured channel user info into TurnMeta so plugins can identify
10669
+ // the sender by name without adapter-specific fields on IncomingMessage.
10670
+ {
10671
+ channelUser: {
10672
+ channelId: "telegram",
10673
+ userId: String(ctx.from.id),
10674
+ displayName: fromName,
10675
+ username: ctx.from.username
10676
+ }
10677
+ }
10678
+ ).catch((err) => log36.error({ err }, "handleMessage error"));
10550
10679
  });
10551
10680
  this.bot.on("message:photo", async (ctx) => {
10552
10681
  const threadId = ctx.message.message_thread_id;
@@ -11259,6 +11388,8 @@ var ChannelAdapter = class {
11259
11388
  }
11260
11389
  async archiveSessionTopic(_sessionId) {
11261
11390
  }
11391
+ async sendUserNotification(_platformId, _message, _options) {
11392
+ }
11262
11393
  };
11263
11394
 
11264
11395
  // src/core/utils/streams.ts
@@ -12714,16 +12845,16 @@ var PromptQueue = class {
12714
12845
  * immediately. Otherwise, it's buffered and the returned promise resolves
12715
12846
  * only after the prompt finishes processing.
12716
12847
  */
12717
- async enqueue(text3, attachments, routing, turnId) {
12848
+ async enqueue(text3, userPrompt, attachments, routing, turnId, meta) {
12718
12849
  if (this.processing) {
12719
12850
  return new Promise((resolve7) => {
12720
- this.queue.push({ text: text3, attachments, routing, turnId, resolve: resolve7 });
12851
+ this.queue.push({ text: text3, userPrompt, attachments, routing, turnId, meta, resolve: resolve7 });
12721
12852
  });
12722
12853
  }
12723
- await this.process(text3, attachments, routing, turnId);
12854
+ await this.process(text3, userPrompt, attachments, routing, turnId, meta);
12724
12855
  }
12725
12856
  /** Run a single prompt through the processor, then drain the next queued item. */
12726
- async process(text3, attachments, routing, turnId) {
12857
+ async process(text3, userPrompt, attachments, routing, turnId, meta) {
12727
12858
  this.processing = true;
12728
12859
  this.abortController = new AbortController();
12729
12860
  const { signal } = this.abortController;
@@ -12733,7 +12864,7 @@ var PromptQueue = class {
12733
12864
  });
12734
12865
  try {
12735
12866
  await Promise.race([
12736
- this.processor(text3, attachments, routing, turnId),
12867
+ this.processor(text3, userPrompt, attachments, routing, turnId, meta),
12737
12868
  new Promise((_, reject) => {
12738
12869
  signal.addEventListener("abort", () => reject(new Error("Prompt aborted")), { once: true });
12739
12870
  })
@@ -12754,7 +12885,7 @@ var PromptQueue = class {
12754
12885
  drainNext() {
12755
12886
  const next = this.queue.shift();
12756
12887
  if (next) {
12757
- this.process(next.text, next.attachments, next.routing, next.turnId).then(next.resolve);
12888
+ this.process(next.text, next.userPrompt, next.attachments, next.routing, next.turnId, next.meta).then(next.resolve);
12758
12889
  }
12759
12890
  }
12760
12891
  /**
@@ -12776,6 +12907,13 @@ var PromptQueue = class {
12776
12907
  get isProcessing() {
12777
12908
  return this.processing;
12778
12909
  }
12910
+ /** Snapshot of queued (not yet processing) items — used for queue inspection by callers. */
12911
+ get pendingItems() {
12912
+ return this.queue.map((item) => ({
12913
+ userPrompt: item.userPrompt,
12914
+ turnId: item.turnId
12915
+ }));
12916
+ }
12779
12917
  };
12780
12918
 
12781
12919
  // src/core/sessions/permission-gate.ts
@@ -12857,17 +12995,31 @@ import * as fs8 from "fs";
12857
12995
 
12858
12996
  // src/core/sessions/turn-context.ts
12859
12997
  import { nanoid } from "nanoid";
12860
- function createTurnContext(sourceAdapterId, responseAdapterId, turnId) {
12998
+ function extractSender(meta) {
12999
+ const identity = meta?.identity;
13000
+ if (!identity || !identity.userId || !identity.identityId) return null;
12861
13001
  return {
12862
- turnId: turnId ?? nanoid(8),
12863
- sourceAdapterId,
12864
- responseAdapterId
13002
+ userId: identity.userId,
13003
+ identityId: identity.identityId,
13004
+ displayName: identity.displayName,
13005
+ username: identity.username
12865
13006
  };
12866
13007
  }
12867
13008
  function getEffectiveTarget(ctx) {
12868
13009
  if (ctx.responseAdapterId === null) return null;
12869
13010
  return ctx.responseAdapterId ?? ctx.sourceAdapterId;
12870
13011
  }
13012
+ function createTurnContext(sourceAdapterId, responseAdapterId, turnId, userPrompt, finalPrompt, attachments, meta) {
13013
+ return {
13014
+ turnId: turnId ?? nanoid(8),
13015
+ sourceAdapterId,
13016
+ responseAdapterId,
13017
+ userPrompt,
13018
+ finalPrompt,
13019
+ attachments,
13020
+ meta
13021
+ };
13022
+ }
12871
13023
  var SYSTEM_EVENT_TYPES = /* @__PURE__ */ new Set([
12872
13024
  "session_end",
12873
13025
  "system_message",
@@ -12962,7 +13114,7 @@ var Session = class extends TypedEmitter {
12962
13114
  this.log = createSessionLogger(this.id, moduleLog);
12963
13115
  this.log.info({ agentName: this.agentName }, "Session created");
12964
13116
  this.queue = new PromptQueue(
12965
- (text3, attachments, routing, turnId) => this.processPrompt(text3, attachments, routing, turnId),
13117
+ (text3, userPrompt, attachments, routing, turnId, meta) => this.processPrompt(text3, userPrompt, attachments, routing, turnId, meta),
12966
13118
  (err) => {
12967
13119
  this.log.error({ err }, "Prompt execution failed");
12968
13120
  const message = err instanceof Error ? err.message : String(err);
@@ -13042,6 +13194,10 @@ var Session = class extends TypedEmitter {
13042
13194
  get promptRunning() {
13043
13195
  return this.queue.isProcessing;
13044
13196
  }
13197
+ /** Snapshot of queued (not yet processing) items — for inspection by API consumers. */
13198
+ get queueItems() {
13199
+ return this.queue.pendingItems;
13200
+ }
13045
13201
  // --- Context Injection ---
13046
13202
  /** Store context markdown to be prepended to the next prompt (used for session resume with history). */
13047
13203
  setContext(markdown) {
@@ -13061,24 +13217,31 @@ var Session = class extends TypedEmitter {
13061
13217
  * then adds it to the PromptQueue. Returns a turnId that callers can use to correlate
13062
13218
  * queued/processing events before the prompt actually runs.
13063
13219
  */
13064
- async enqueuePrompt(text3, attachments, routing, externalTurnId) {
13220
+ async enqueuePrompt(text3, attachments, routing, externalTurnId, meta) {
13065
13221
  const turnId = externalTurnId ?? nanoid2(8);
13222
+ const turnMeta = meta ?? { turnId };
13223
+ const userPrompt = text3;
13066
13224
  if (this.middlewareChain) {
13067
- const payload = { text: text3, attachments, sessionId: this.id, sourceAdapterId: routing?.sourceAdapterId };
13225
+ const payload = { text: text3, attachments, sessionId: this.id, sourceAdapterId: routing?.sourceAdapterId, meta: turnMeta };
13068
13226
  const result = await this.middlewareChain.execute(Hook.AGENT_BEFORE_PROMPT, payload, async (p) => p);
13069
- if (!result) return turnId;
13227
+ if (!result) throw new Error("PROMPT_BLOCKED");
13070
13228
  text3 = result.text;
13071
13229
  attachments = result.attachments;
13072
13230
  }
13073
- await this.queue.enqueue(text3, attachments, routing, turnId);
13231
+ await this.queue.enqueue(text3, userPrompt, attachments, routing, turnId, turnMeta);
13074
13232
  return turnId;
13075
13233
  }
13076
- async processPrompt(text3, attachments, routing, turnId) {
13234
+ async processPrompt(text3, userPrompt, attachments, routing, turnId, meta) {
13077
13235
  if (this._status === "finished") return;
13078
13236
  this.activeTurnContext = createTurnContext(
13079
13237
  routing?.sourceAdapterId ?? this.channelId,
13080
13238
  routing?.responseAdapterId,
13081
- turnId
13239
+ turnId,
13240
+ userPrompt,
13241
+ text3,
13242
+ // finalPrompt (after middleware transformations)
13243
+ attachments,
13244
+ meta
13082
13245
  );
13083
13246
  this.emit(SessionEv.TURN_STARTED, this.activeTurnContext);
13084
13247
  this.promptCount++;
@@ -13113,6 +13276,13 @@ ${text3}`;
13113
13276
  if (accumulatorListener) {
13114
13277
  this.on(SessionEv.AGENT_EVENT, accumulatorListener);
13115
13278
  }
13279
+ const turnTextBuffer = [];
13280
+ const turnTextListener = (event) => {
13281
+ if (event.type === "text" && typeof event.content === "string") {
13282
+ turnTextBuffer.push(event.content);
13283
+ }
13284
+ };
13285
+ this.on(SessionEv.AGENT_EVENT, turnTextListener);
13116
13286
  const mw = this.middlewareChain;
13117
13287
  const afterEventListener = mw ? (event) => {
13118
13288
  mw.execute(Hook.AGENT_AFTER_EVENT, { sessionId: this.id, event, outgoingMessage: { type: "text", text: "" } }, async (e) => e).catch(() => {
@@ -13122,7 +13292,16 @@ ${text3}`;
13122
13292
  this.agentInstance.on(SessionEv.AGENT_EVENT, afterEventListener);
13123
13293
  }
13124
13294
  if (this.middlewareChain) {
13125
- this.middlewareChain.execute(Hook.TURN_START, { sessionId: this.id, promptText: processed.text, promptNumber: this.promptCount }, async (p) => p).catch(() => {
13295
+ this.middlewareChain.execute(Hook.TURN_START, {
13296
+ sessionId: this.id,
13297
+ promptText: processed.text,
13298
+ promptNumber: this.promptCount,
13299
+ turnId: this.activeTurnContext?.turnId ?? turnId ?? "",
13300
+ meta,
13301
+ userPrompt: this.activeTurnContext?.userPrompt,
13302
+ sourceAdapterId: this.activeTurnContext?.sourceAdapterId,
13303
+ responseAdapterId: this.activeTurnContext?.responseAdapterId
13304
+ }, async (p) => p).catch(() => {
13126
13305
  });
13127
13306
  }
13128
13307
  let stopReason = "end_turn";
@@ -13148,8 +13327,20 @@ ${text3}`;
13148
13327
  if (afterEventListener) {
13149
13328
  this.agentInstance.off(SessionEv.AGENT_EVENT, afterEventListener);
13150
13329
  }
13330
+ this.off(SessionEv.AGENT_EVENT, turnTextListener);
13331
+ const finalTurnId = this.activeTurnContext?.turnId ?? turnId ?? "";
13151
13332
  if (this.middlewareChain) {
13152
- this.middlewareChain.execute(Hook.TURN_END, { sessionId: this.id, stopReason, durationMs: Date.now() - promptStart }, async (p) => p).catch(() => {
13333
+ this.middlewareChain.execute(Hook.TURN_END, { sessionId: this.id, stopReason, durationMs: Date.now() - promptStart, turnId: finalTurnId, meta }, async (p) => p).catch(() => {
13334
+ });
13335
+ }
13336
+ if (this.middlewareChain) {
13337
+ this.middlewareChain.execute(Hook.AGENT_AFTER_TURN, {
13338
+ sessionId: this.id,
13339
+ turnId: finalTurnId,
13340
+ fullText: turnTextBuffer.join(""),
13341
+ stopReason,
13342
+ meta
13343
+ }, async (p) => p).catch(() => {
13153
13344
  });
13154
13345
  }
13155
13346
  this.activeTurnContext = null;
@@ -14279,7 +14470,7 @@ var SessionBridge = class {
14279
14470
  if (this.shouldForward(event)) {
14280
14471
  this.dispatchAgentEvent(event);
14281
14472
  } else {
14282
- this.deps.eventBus?.emit(BusEvent.AGENT_EVENT, { sessionId: this.session.id, event });
14473
+ this.deps.eventBus?.emit(BusEvent.AGENT_EVENT, { sessionId: this.session.id, turnId: "", event });
14283
14474
  }
14284
14475
  });
14285
14476
  if (!this.session.agentInstance.onPermissionRequest || this.session.agentInstance.onPermissionRequest.__bridgeId === void 0) {
@@ -14332,14 +14523,16 @@ var SessionBridge = class {
14332
14523
  this.deps.sessionManager.patchRecord(this.session.id, { currentPromptCount: count });
14333
14524
  });
14334
14525
  this.listen(this.session, SessionEv.TURN_STARTED, (ctx) => {
14335
- if (ctx.sourceAdapterId !== "sse") {
14336
- this.deps.eventBus?.emit(BusEvent.MESSAGE_PROCESSING, {
14337
- sessionId: this.session.id,
14338
- turnId: ctx.turnId,
14339
- sourceAdapterId: ctx.sourceAdapterId,
14340
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
14341
- });
14342
- }
14526
+ this.deps.eventBus?.emit(BusEvent.MESSAGE_PROCESSING, {
14527
+ sessionId: this.session.id,
14528
+ turnId: ctx.turnId,
14529
+ sourceAdapterId: ctx.sourceAdapterId,
14530
+ userPrompt: ctx.userPrompt,
14531
+ finalPrompt: ctx.finalPrompt,
14532
+ attachments: ctx.attachments,
14533
+ sender: extractSender(ctx.meta),
14534
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
14535
+ });
14343
14536
  });
14344
14537
  if (this.session.latestCommands !== null) {
14345
14538
  this.session.emit(SessionEv.AGENT_EVENT, { type: "commands_update", commands: this.session.latestCommands });
@@ -14505,6 +14698,7 @@ var SessionBridge = class {
14505
14698
  }
14506
14699
  this.deps.eventBus?.emit(BusEvent.AGENT_EVENT, {
14507
14700
  sessionId: this.session.id,
14701
+ turnId: this.session.activeTurnContext?.turnId ?? "",
14508
14702
  event
14509
14703
  });
14510
14704
  return outgoing;
@@ -14631,8 +14825,7 @@ var SessionFactory = class {
14631
14825
  const payload = {
14632
14826
  agentName: params.agentName,
14633
14827
  workingDir: params.workingDirectory,
14634
- userId: "",
14635
- // userId is not part of SessionCreateParams — resolved upstream
14828
+ userId: params.userId ?? "",
14636
14829
  channelId: params.channelId,
14637
14830
  threadId: ""
14638
14831
  // threadId is assigned after session creation
@@ -14706,6 +14899,7 @@ var SessionFactory = class {
14706
14899
  const failedSessionId = createParams.existingSessionId ?? `failed-${Date.now()}`;
14707
14900
  this.eventBus.emit(BusEvent.AGENT_EVENT, {
14708
14901
  sessionId: failedSessionId,
14902
+ turnId: "",
14709
14903
  event: guidance
14710
14904
  });
14711
14905
  throw err;
@@ -14734,7 +14928,8 @@ var SessionFactory = class {
14734
14928
  this.eventBus.emit(BusEvent.SESSION_CREATED, {
14735
14929
  sessionId: session.id,
14736
14930
  agent: session.agentName,
14737
- status: session.status
14931
+ status: session.status,
14932
+ userId: createParams.userId
14738
14933
  });
14739
14934
  }
14740
14935
  return session;
@@ -15254,7 +15449,7 @@ var AgentSwitchHandler = class {
15254
15449
  message: `Switching from ${fromAgent} to ${toAgent}...`
15255
15450
  };
15256
15451
  session.emit(SessionEv.AGENT_EVENT, startEvent);
15257
- eventBus.emit(BusEvent.AGENT_EVENT, { sessionId, event: startEvent });
15452
+ eventBus.emit(BusEvent.AGENT_EVENT, { sessionId, turnId: "", event: startEvent });
15258
15453
  eventBus.emit(BusEvent.SESSION_AGENT_SWITCH, {
15259
15454
  sessionId,
15260
15455
  fromAgent,
@@ -15317,7 +15512,7 @@ var AgentSwitchHandler = class {
15317
15512
  message: resumed ? `Switched to ${toAgent} (resumed previous session).` : `Switched to ${toAgent} (new session).`
15318
15513
  };
15319
15514
  session.emit(SessionEv.AGENT_EVENT, successEvent);
15320
- eventBus.emit(BusEvent.AGENT_EVENT, { sessionId, event: successEvent });
15515
+ eventBus.emit(BusEvent.AGENT_EVENT, { sessionId, turnId: "", event: successEvent });
15321
15516
  eventBus.emit(BusEvent.SESSION_AGENT_SWITCH, {
15322
15517
  sessionId,
15323
15518
  fromAgent,
@@ -15332,7 +15527,7 @@ var AgentSwitchHandler = class {
15332
15527
  message: `Failed to switch to ${toAgent}: ${errorMessage}`
15333
15528
  };
15334
15529
  session.emit(SessionEv.AGENT_EVENT, failedEvent);
15335
- eventBus.emit(BusEvent.AGENT_EVENT, { sessionId, event: failedEvent });
15530
+ eventBus.emit(BusEvent.AGENT_EVENT, { sessionId, turnId: "", event: failedEvent });
15336
15531
  eventBus.emit(BusEvent.SESSION_AGENT_SWITCH, {
15337
15532
  sessionId,
15338
15533
  fromAgent,
@@ -16102,11 +16297,55 @@ var PluginStorageImpl = class {
16102
16297
  async list() {
16103
16298
  return Object.keys(this.readKv());
16104
16299
  }
16300
+ async keys(prefix) {
16301
+ const all = Object.keys(this.readKv());
16302
+ return prefix ? all.filter((k) => k.startsWith(prefix)) : all;
16303
+ }
16304
+ async clear() {
16305
+ this.writeChain = this.writeChain.then(() => {
16306
+ this.writeKv({});
16307
+ });
16308
+ return this.writeChain;
16309
+ }
16105
16310
  /** Returns the plugin's data directory, creating it lazily on first access. */
16106
16311
  getDataDir() {
16107
16312
  fs14.mkdirSync(this.dataDir, { recursive: true });
16108
16313
  return this.dataDir;
16109
16314
  }
16315
+ /**
16316
+ * Creates a namespaced storage instance scoped to a session.
16317
+ * Keys are prefixed with `session:{sessionId}:` to isolate session data
16318
+ * from global plugin storage in the same backing file.
16319
+ */
16320
+ forSession(sessionId) {
16321
+ const prefix = `session:${sessionId}:`;
16322
+ return {
16323
+ get: (key) => this.get(`${prefix}${key}`),
16324
+ set: (key, value) => this.set(`${prefix}${key}`, value),
16325
+ delete: (key) => this.delete(`${prefix}${key}`),
16326
+ list: async () => {
16327
+ const all = await this.keys(prefix);
16328
+ return all.map((k) => k.slice(prefix.length));
16329
+ },
16330
+ keys: async (p) => {
16331
+ const full = p ? `${prefix}${p}` : prefix;
16332
+ const all = await this.keys(full);
16333
+ return all.map((k) => k.slice(prefix.length));
16334
+ },
16335
+ clear: async () => {
16336
+ this.writeChain = this.writeChain.then(() => {
16337
+ const data = this.readKv();
16338
+ for (const key of Object.keys(data)) {
16339
+ if (key.startsWith(prefix)) delete data[key];
16340
+ }
16341
+ this.writeKv(data);
16342
+ });
16343
+ return this.writeChain;
16344
+ },
16345
+ getDataDir: () => this.getDataDir(),
16346
+ forSession: (nestedId) => this.forSession(`${sessionId}:${nestedId}`)
16347
+ };
16348
+ }
16110
16349
  };
16111
16350
 
16112
16351
  // src/core/plugin/plugin-context.ts
@@ -16170,9 +16409,52 @@ function createPluginContext(opts) {
16170
16409
  requirePermission(permissions, "storage:read", "storage.list");
16171
16410
  return storageImpl.list();
16172
16411
  },
16412
+ async keys(prefix) {
16413
+ requirePermission(permissions, "storage:read", "storage.keys");
16414
+ return storageImpl.keys(prefix);
16415
+ },
16416
+ async clear() {
16417
+ requirePermission(permissions, "storage:write", "storage.clear");
16418
+ return storageImpl.clear();
16419
+ },
16173
16420
  getDataDir() {
16174
16421
  requirePermission(permissions, "storage:read", "storage.getDataDir");
16175
16422
  return storageImpl.getDataDir();
16423
+ },
16424
+ forSession(sessionId) {
16425
+ requirePermission(permissions, "storage:read", "storage.forSession");
16426
+ const scoped = storageImpl.forSession(sessionId);
16427
+ return {
16428
+ get: (key) => {
16429
+ requirePermission(permissions, "storage:read", "storage.get");
16430
+ return scoped.get(key);
16431
+ },
16432
+ set: (key, value) => {
16433
+ requirePermission(permissions, "storage:write", "storage.set");
16434
+ return scoped.set(key, value);
16435
+ },
16436
+ delete: (key) => {
16437
+ requirePermission(permissions, "storage:write", "storage.delete");
16438
+ return scoped.delete(key);
16439
+ },
16440
+ list: () => {
16441
+ requirePermission(permissions, "storage:read", "storage.list");
16442
+ return scoped.list();
16443
+ },
16444
+ keys: (prefix) => {
16445
+ requirePermission(permissions, "storage:read", "storage.keys");
16446
+ return scoped.keys(prefix);
16447
+ },
16448
+ clear: () => {
16449
+ requirePermission(permissions, "storage:write", "storage.clear");
16450
+ return scoped.clear();
16451
+ },
16452
+ getDataDir: () => {
16453
+ requirePermission(permissions, "storage:read", "storage.getDataDir");
16454
+ return scoped.getDataDir();
16455
+ },
16456
+ forSession: (nestedId) => storage.forSession(`${sessionId}:${nestedId}`)
16457
+ };
16176
16458
  }
16177
16459
  };
16178
16460
  const ctx = {
@@ -16223,6 +16505,26 @@ function createPluginContext(opts) {
16223
16505
  await router.send(_sessionId, _content);
16224
16506
  }
16225
16507
  },
16508
+ notify(target, message, options) {
16509
+ requirePermission(permissions, "notifications:send", "notify()");
16510
+ const svc = serviceRegistry.get("notifications");
16511
+ if (svc?.notifyUser) {
16512
+ svc.notifyUser(target, message, options).catch(() => {
16513
+ });
16514
+ }
16515
+ },
16516
+ defineHook(_name) {
16517
+ },
16518
+ async emitHook(name, payload) {
16519
+ const qualifiedName = `plugin:${pluginName}:${name}`;
16520
+ return middlewareChain.execute(qualifiedName, payload, (p) => p);
16521
+ },
16522
+ async getSessionInfo(sessionId) {
16523
+ requirePermission(permissions, "sessions:read", "getSessionInfo()");
16524
+ const sessionMgr = serviceRegistry.get("session-info");
16525
+ if (!sessionMgr) return void 0;
16526
+ return sessionMgr.getSessionInfo(sessionId);
16527
+ },
16226
16528
  registerMenuItem(item) {
16227
16529
  requirePermission(permissions, "commands:register", "registerMenuItem()");
16228
16530
  const menuRegistry = serviceRegistry.get("menu-registry");
@@ -17272,7 +17574,7 @@ var OpenACPCore = class {
17272
17574
  *
17273
17575
  * If no session is found, the user is told to start one with /new.
17274
17576
  */
17275
- async handleMessage(message) {
17577
+ async handleMessage(message, initialMeta) {
17276
17578
  log16.debug(
17277
17579
  {
17278
17580
  channelId: message.channelId,
@@ -17281,10 +17583,12 @@ var OpenACPCore = class {
17281
17583
  },
17282
17584
  "Incoming message"
17283
17585
  );
17586
+ const turnId = nanoid3(8);
17587
+ const meta = { turnId, ...initialMeta };
17284
17588
  if (this.lifecycleManager?.middlewareChain) {
17285
17589
  const result = await this.lifecycleManager.middlewareChain.execute(
17286
17590
  Hook.MESSAGE_INCOMING,
17287
- message,
17591
+ { ...message, meta },
17288
17592
  async (msg) => msg
17289
17593
  );
17290
17594
  if (!result) return;
@@ -17319,9 +17623,6 @@ var OpenACPCore = class {
17319
17623
  }
17320
17624
  return;
17321
17625
  }
17322
- this.sessionManager.patchRecord(session.id, {
17323
- lastActiveAt: (/* @__PURE__ */ new Date()).toISOString()
17324
- });
17325
17626
  let text3 = message.text;
17326
17627
  if (this.assistantManager?.isAssistant(session.id)) {
17327
17628
  const pending = this.assistantManager.consumePendingSystemPrompt(message.channelId);
@@ -17334,23 +17635,85 @@ User message:
17334
17635
  ${text3}`;
17335
17636
  }
17336
17637
  }
17337
- const sourceAdapterId = message.routing?.sourceAdapterId ?? message.channelId;
17338
- const routing = sourceAdapterId !== message.routing?.sourceAdapterId ? { ...message.routing, sourceAdapterId } : message.routing;
17339
- if (sourceAdapterId && sourceAdapterId !== "sse" && sourceAdapterId !== "api") {
17340
- const turnId = nanoid3(8);
17341
- this.eventBus.emit(BusEvent.MESSAGE_QUEUED, {
17638
+ const enrichedMeta = message.meta ?? meta;
17639
+ await this._dispatchToSession(session, text3, message.attachments, {
17640
+ sourceAdapterId: message.routing?.sourceAdapterId ?? message.channelId,
17641
+ responseAdapterId: message.routing?.responseAdapterId
17642
+ }, turnId, enrichedMeta);
17643
+ }
17644
+ /**
17645
+ * Shared dispatch path for sending a prompt to a session.
17646
+ * Called by both handleMessage (Telegram) and handleMessageInSession (SSE/API)
17647
+ * after their respective middleware/enrichment steps.
17648
+ */
17649
+ async _dispatchToSession(session, text3, attachments, routing, turnId, meta) {
17650
+ this.sessionManager.patchRecord(session.id, {
17651
+ lastActiveAt: (/* @__PURE__ */ new Date()).toISOString()
17652
+ });
17653
+ this.eventBus.emit(BusEvent.MESSAGE_QUEUED, {
17654
+ sessionId: session.id,
17655
+ turnId,
17656
+ text: text3,
17657
+ sourceAdapterId: routing.sourceAdapterId,
17658
+ attachments,
17659
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
17660
+ queueDepth: session.queueDepth + 1,
17661
+ sender: extractSender(meta)
17662
+ });
17663
+ session.enqueuePrompt(text3, attachments, routing, turnId, meta).catch((err) => {
17664
+ const reason = err instanceof Error ? err.message : String(err);
17665
+ log16.warn({ err, sessionId: session.id, turnId, reason }, "enqueuePrompt failed \u2014 emitting message:failed");
17666
+ this.eventBus.emit(BusEvent.MESSAGE_FAILED, {
17342
17667
  sessionId: session.id,
17343
17668
  turnId,
17344
- text: text3,
17345
- sourceAdapterId,
17346
- attachments: message.attachments,
17347
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
17348
- queueDepth: session.queueDepth
17669
+ reason
17349
17670
  });
17350
- await session.enqueuePrompt(text3, message.attachments, routing, turnId);
17351
- } else {
17352
- await session.enqueuePrompt(text3, message.attachments, routing);
17671
+ });
17672
+ }
17673
+ /**
17674
+ * Send a message to a known session, running the full message:incoming → agent:beforePrompt
17675
+ * middleware chain (same as handleMessage) but without the threadId-based session lookup.
17676
+ *
17677
+ * Used by channels that already hold a direct session reference (e.g. SSE adapter, api-server),
17678
+ * where looking up by channelId+threadId is unreliable (API sessions may have no threadId).
17679
+ *
17680
+ * @param session The target session — caller is responsible for validating its status.
17681
+ * @param message Sender context and message content.
17682
+ * @param initialMeta Optional adapter-specific context to seed the TurnMeta bag
17683
+ * (e.g. channelUser with display name/username).
17684
+ * @param options Optional turnId override and response routing.
17685
+ */
17686
+ async handleMessageInSession(session, message, initialMeta, options) {
17687
+ const turnId = options?.externalTurnId ?? nanoid3(8);
17688
+ const meta = { turnId, ...initialMeta };
17689
+ let text3 = message.text;
17690
+ let { attachments } = message;
17691
+ let enrichedMeta = meta;
17692
+ if (this.lifecycleManager?.middlewareChain) {
17693
+ const payload = {
17694
+ channelId: message.channelId,
17695
+ threadId: session.id,
17696
+ userId: message.userId,
17697
+ text: text3,
17698
+ attachments,
17699
+ meta
17700
+ };
17701
+ const result = await this.lifecycleManager.middlewareChain.execute(
17702
+ Hook.MESSAGE_INCOMING,
17703
+ payload,
17704
+ async (p) => p
17705
+ );
17706
+ if (!result) return { turnId, queueDepth: session.queueDepth };
17707
+ text3 = result.text;
17708
+ attachments = result.attachments;
17709
+ enrichedMeta = result.meta ?? meta;
17353
17710
  }
17711
+ const routing = {
17712
+ sourceAdapterId: message.channelId,
17713
+ responseAdapterId: options?.responseAdapterId
17714
+ };
17715
+ await this._dispatchToSession(session, text3, attachments, routing, turnId, enrichedMeta);
17716
+ return { turnId, queueDepth: session.queueDepth };
17354
17717
  }
17355
17718
  // --- Unified Session Creation Pipeline ---
17356
17719
  /**
@@ -17462,7 +17825,7 @@ ${text3}`;
17462
17825
  } else if (processedEvent.type === "error") {
17463
17826
  session.fail(processedEvent.message);
17464
17827
  }
17465
- this.eventBus.emit(BusEvent.AGENT_EVENT, { sessionId: session.id, event: processedEvent });
17828
+ this.eventBus.emit(BusEvent.AGENT_EVENT, { sessionId: session.id, turnId: session.activeTurnContext?.turnId ?? "", event: processedEvent });
17466
17829
  });
17467
17830
  session.on(SessionEv.STATUS_CHANGE, (_from, to) => {
17468
17831
  this.sessionManager.patchRecord(session.id, {
@@ -17474,6 +17837,18 @@ ${text3}`;
17474
17837
  session.on(SessionEv.PROMPT_COUNT_CHANGED, (count) => {
17475
17838
  this.sessionManager.patchRecord(session.id, { currentPromptCount: count });
17476
17839
  });
17840
+ session.on(SessionEv.TURN_STARTED, (ctx) => {
17841
+ this.eventBus.emit(BusEvent.MESSAGE_PROCESSING, {
17842
+ sessionId: session.id,
17843
+ turnId: ctx.turnId,
17844
+ sourceAdapterId: ctx.sourceAdapterId,
17845
+ userPrompt: ctx.userPrompt,
17846
+ finalPrompt: ctx.finalPrompt,
17847
+ attachments: ctx.attachments,
17848
+ sender: extractSender(ctx.meta),
17849
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
17850
+ });
17851
+ });
17477
17852
  }
17478
17853
  this.sessionFactory.wireSideEffects(session, {
17479
17854
  eventBus: this.eventBus,
@@ -19792,7 +20167,7 @@ export {
19792
20167
  MenuRegistry,
19793
20168
  MessageTransformer,
19794
20169
  MessagingAdapter,
19795
- NotificationManager,
20170
+ NotificationService as NotificationManager,
19796
20171
  OpenACPCore,
19797
20172
  OutputModeResolver,
19798
20173
  PRODUCT_GUIDE,