@openacp/cli 2026.405.2 → 2026.406.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -517,6 +517,7 @@ var init_config = __esm({
517
517
  defaultAgent: z.string(),
518
518
  workspace: z.object({
519
519
  baseDir: z.string().default("~/openacp-workspace"),
520
+ allowExternalWorkspaces: z.boolean().default(true),
520
521
  security: z.object({
521
522
  allowedPaths: z.array(z.string()).default([]),
522
523
  envWhitelist: z.array(z.string()).default([])
@@ -637,13 +638,22 @@ var init_config = __esm({
637
638
  if (input2.startsWith("/") || input2.startsWith("~")) {
638
639
  const resolved2 = expandHome3(input2);
639
640
  const base = expandHome3(this.config.workspace.baseDir);
640
- if (resolved2 === base || resolved2.startsWith(base + path4.sep)) {
641
- fs4.mkdirSync(resolved2, { recursive: true });
641
+ const isInternal = resolved2 === base || resolved2.startsWith(base + path4.sep);
642
+ if (!isInternal) {
643
+ if (!this.config.workspace.allowExternalWorkspaces) {
644
+ throw new Error(
645
+ `Workspace path "${input2}" is outside base directory "${this.config.workspace.baseDir}". Set allowExternalWorkspaces: true to allow this.`
646
+ );
647
+ }
648
+ if (!fs4.existsSync(resolved2)) {
649
+ throw new Error(
650
+ `Workspace path "${input2}" does not exist.`
651
+ );
652
+ }
642
653
  return resolved2;
643
654
  }
644
- throw new Error(
645
- `Workspace path "${input2}" is outside base directory "${this.config.workspace.baseDir}".`
646
- );
655
+ fs4.mkdirSync(resolved2, { recursive: true });
656
+ return resolved2;
647
657
  }
648
658
  const name = input2.replace(/[^a-zA-Z0-9_-]/g, "");
649
659
  if (name !== input2) {
@@ -765,6 +775,104 @@ var init_read_text_file = __esm({
765
775
  }
766
776
  });
767
777
 
778
+ // src/core/events.ts
779
+ var Hook, BusEvent, SessionEv;
780
+ var init_events = __esm({
781
+ "src/core/events.ts"() {
782
+ "use strict";
783
+ Hook = {
784
+ // --- Message flow ---
785
+ /** Incoming message from any adapter — modifiable, can block. */
786
+ MESSAGE_INCOMING: "message:incoming",
787
+ /** Outgoing message before it reaches the adapter — modifiable, can block. */
788
+ MESSAGE_OUTGOING: "message:outgoing",
789
+ // --- Agent / turn lifecycle ---
790
+ /** Before a user prompt is sent to the agent — modifiable, can block. */
791
+ AGENT_BEFORE_PROMPT: "agent:beforePrompt",
792
+ /** Before an agent event is dispatched — modifiable, can block. */
793
+ AGENT_BEFORE_EVENT: "agent:beforeEvent",
794
+ /** After an agent event is dispatched — read-only, fire-and-forget. */
795
+ AGENT_AFTER_EVENT: "agent:afterEvent",
796
+ /** Before the current prompt is cancelled — modifiable, can block. */
797
+ AGENT_BEFORE_CANCEL: "agent:beforeCancel",
798
+ /** Before the agent is switched — modifiable, can block. */
799
+ AGENT_BEFORE_SWITCH: "agent:beforeSwitch",
800
+ /** After the agent has been switched — read-only, fire-and-forget. */
801
+ AGENT_AFTER_SWITCH: "agent:afterSwitch",
802
+ // --- Turn boundaries ---
803
+ /** Turn started — read-only, fire-and-forget. */
804
+ TURN_START: "turn:start",
805
+ /** Turn ended (always fires, even on error) — read-only, fire-and-forget. */
806
+ TURN_END: "turn:end",
807
+ // --- Session lifecycle ---
808
+ /** Before a new session is created — modifiable, can block. */
809
+ SESSION_BEFORE_CREATE: "session:beforeCreate",
810
+ /** After a session is destroyed — read-only, fire-and-forget. */
811
+ SESSION_AFTER_DESTROY: "session:afterDestroy",
812
+ // --- Permissions ---
813
+ /** Before a permission request is shown to the user — modifiable, can block. */
814
+ PERMISSION_BEFORE_REQUEST: "permission:beforeRequest",
815
+ /** After a permission request is resolved — read-only, fire-and-forget. */
816
+ PERMISSION_AFTER_RESOLVE: "permission:afterResolve",
817
+ // --- Config ---
818
+ /** Before config options change — modifiable, can block. */
819
+ CONFIG_BEFORE_CHANGE: "config:beforeChange",
820
+ // --- Filesystem (agent-level) ---
821
+ /** Before a file read operation — modifiable. */
822
+ FS_BEFORE_READ: "fs:beforeRead",
823
+ /** Before a file write operation — modifiable. */
824
+ FS_BEFORE_WRITE: "fs:beforeWrite",
825
+ // --- Terminal ---
826
+ /** Before a terminal session is created — modifiable, can block. */
827
+ TERMINAL_BEFORE_CREATE: "terminal:beforeCreate",
828
+ /** After a terminal session exits — read-only, fire-and-forget. */
829
+ TERMINAL_AFTER_EXIT: "terminal:afterExit"
830
+ };
831
+ BusEvent = {
832
+ // --- Session lifecycle ---
833
+ SESSION_CREATED: "session:created",
834
+ SESSION_UPDATED: "session:updated",
835
+ SESSION_DELETED: "session:deleted",
836
+ SESSION_ENDED: "session:ended",
837
+ SESSION_NAMED: "session:named",
838
+ SESSION_THREAD_READY: "session:threadReady",
839
+ SESSION_CONFIG_CHANGED: "session:configChanged",
840
+ SESSION_AGENT_SWITCH: "session:agentSwitch",
841
+ // --- Agent ---
842
+ AGENT_EVENT: "agent:event",
843
+ AGENT_PROMPT: "agent:prompt",
844
+ // --- Permissions ---
845
+ PERMISSION_REQUEST: "permission:request",
846
+ PERMISSION_RESOLVED: "permission:resolved",
847
+ // --- Message visibility ---
848
+ MESSAGE_QUEUED: "message:queued",
849
+ MESSAGE_PROCESSING: "message:processing",
850
+ // --- System lifecycle ---
851
+ KERNEL_BOOTED: "kernel:booted",
852
+ SYSTEM_READY: "system:ready",
853
+ SYSTEM_SHUTDOWN: "system:shutdown",
854
+ SYSTEM_COMMANDS_READY: "system:commands-ready",
855
+ // --- Plugin lifecycle ---
856
+ PLUGIN_LOADED: "plugin:loaded",
857
+ PLUGIN_FAILED: "plugin:failed",
858
+ PLUGIN_DISABLED: "plugin:disabled",
859
+ PLUGIN_UNLOADED: "plugin:unloaded",
860
+ // --- Usage ---
861
+ USAGE_RECORDED: "usage:recorded"
862
+ };
863
+ SessionEv = {
864
+ AGENT_EVENT: "agent_event",
865
+ PERMISSION_REQUEST: "permission_request",
866
+ SESSION_END: "session_end",
867
+ STATUS_CHANGE: "status_change",
868
+ NAMED: "named",
869
+ ERROR: "error",
870
+ PROMPT_COUNT_CHANGED: "prompt_count_changed",
871
+ TURN_STARTED: "turn_started"
872
+ };
873
+ }
874
+ });
875
+
768
876
  // src/core/utils/bypass-detection.ts
769
877
  function isPermissionBypass(value) {
770
878
  const lower = value.toLowerCase();
@@ -3108,6 +3216,7 @@ var MAX_SSE_CONNECTIONS, SSEManager;
3108
3216
  var init_sse_manager = __esm({
3109
3217
  "src/plugins/api-server/sse-manager.ts"() {
3110
3218
  "use strict";
3219
+ init_events();
3111
3220
  MAX_SSE_CONNECTIONS = 50;
3112
3221
  SSEManager = class {
3113
3222
  constructor(eventBus, getSessionStats, startedAt) {
@@ -3122,14 +3231,14 @@ var init_sse_manager = __esm({
3122
3231
  setup() {
3123
3232
  if (!this.eventBus) return;
3124
3233
  const events = [
3125
- "session:created",
3126
- "session:updated",
3127
- "session:deleted",
3128
- "agent:event",
3129
- "permission:request",
3130
- "permission:resolved",
3131
- "message:queued",
3132
- "message:processing"
3234
+ BusEvent.SESSION_CREATED,
3235
+ BusEvent.SESSION_UPDATED,
3236
+ BusEvent.SESSION_DELETED,
3237
+ BusEvent.AGENT_EVENT,
3238
+ BusEvent.PERMISSION_REQUEST,
3239
+ BusEvent.PERMISSION_RESOLVED,
3240
+ BusEvent.MESSAGE_QUEUED,
3241
+ BusEvent.MESSAGE_PROCESSING
3133
3242
  ];
3134
3243
  for (const eventName of events) {
3135
3244
  const handler = (data) => {
@@ -3192,12 +3301,12 @@ data: ${JSON.stringify(data)}
3192
3301
 
3193
3302
  `;
3194
3303
  const sessionEvents = [
3195
- "agent:event",
3196
- "permission:request",
3197
- "permission:resolved",
3198
- "session:updated",
3199
- "message:queued",
3200
- "message:processing"
3304
+ BusEvent.AGENT_EVENT,
3305
+ BusEvent.PERMISSION_REQUEST,
3306
+ BusEvent.PERMISSION_RESOLVED,
3307
+ BusEvent.SESSION_UPDATED,
3308
+ BusEvent.MESSAGE_QUEUED,
3309
+ BusEvent.MESSAGE_PROCESSING
3201
3310
  ];
3202
3311
  for (const res of this.sseConnections) {
3203
3312
  const filter = res.sessionFilter;
@@ -7318,7 +7427,6 @@ var menu_exports = {};
7318
7427
  __export(menu_exports, {
7319
7428
  buildMenuKeyboard: () => buildMenuKeyboard,
7320
7429
  buildSkillMessages: () => buildSkillMessages,
7321
- handleClear: () => handleClear,
7322
7430
  handleHelp: () => handleHelp,
7323
7431
  handleMenu: () => handleMenu
7324
7432
  });
@@ -7379,31 +7487,11 @@ Each session gets its own topic \u2014 chat there to work with the agent.
7379
7487
  /bypass_permissions \u2014 Toggle bypass permissions
7380
7488
  /handoff \u2014 Continue session in terminal
7381
7489
  /archive \u2014 Archive session topic
7382
- /clear \u2014 Clear assistant history
7383
7490
 
7384
7491
  \u{1F4AC} Need help? Just ask me in this topic!`,
7385
7492
  { parse_mode: "HTML" }
7386
7493
  );
7387
7494
  }
7388
- async function handleClear(ctx, assistant) {
7389
- if (!assistant) {
7390
- await ctx.reply("\u26A0\uFE0F Assistant is not available.", { parse_mode: "HTML" });
7391
- return;
7392
- }
7393
- const threadId = ctx.message?.message_thread_id;
7394
- if (threadId !== assistant.topicId) {
7395
- await ctx.reply("\u2139\uFE0F /clear only works in the Assistant topic.", { parse_mode: "HTML" });
7396
- return;
7397
- }
7398
- await ctx.reply("\u{1F504} Clearing assistant history...", { parse_mode: "HTML" });
7399
- try {
7400
- await assistant.respawn();
7401
- await ctx.reply("\u2705 Assistant history cleared.", { parse_mode: "HTML" });
7402
- } catch (err) {
7403
- const message = err instanceof Error ? err.message : String(err);
7404
- await ctx.reply(`\u274C Failed to clear: <code>${message}</code>`, { parse_mode: "HTML" });
7405
- }
7406
- }
7407
7495
  function buildSkillMessages(commands) {
7408
7496
  const sorted = [...commands].sort((a, b) => a.name.localeCompare(b.name));
7409
7497
  const header2 = "\u{1F6E0} <b>Available Skills</b>\n";
@@ -8096,7 +8184,10 @@ function setupAllCallbacks(bot, core, chatId, systemTopicIds, getAssistantSessio
8096
8184
  const menuRegistry = core.lifecycleManager?.serviceRegistry?.get("menu-registry");
8097
8185
  if (!menuRegistry) return;
8098
8186
  const item = menuRegistry.getItem(itemId);
8099
- if (!item) return;
8187
+ if (!item) {
8188
+ log30.warn({ itemId }, "Menu item not found in registry");
8189
+ return;
8190
+ }
8100
8191
  const topicId = ctx.callbackQuery.message?.message_thread_id;
8101
8192
  const registry = core.lifecycleManager?.serviceRegistry?.get("command-registry");
8102
8193
  switch (item.action.type) {
@@ -8180,7 +8271,7 @@ ${lines}`, { parse_mode: "HTML" }).catch(() => {
8180
8271
  }
8181
8272
  });
8182
8273
  }
8183
- var STATIC_COMMANDS;
8274
+ var log30, STATIC_COMMANDS;
8184
8275
  var init_commands = __esm({
8185
8276
  "src/plugins/telegram/commands/index.ts"() {
8186
8277
  "use strict";
@@ -8193,6 +8284,7 @@ var init_commands = __esm({
8193
8284
  init_tunnel2();
8194
8285
  init_switch();
8195
8286
  init_telegram_overrides();
8287
+ init_log();
8196
8288
  init_menu();
8197
8289
  init_menu();
8198
8290
  init_telegram_overrides();
@@ -8204,6 +8296,7 @@ var init_commands = __esm({
8204
8296
  init_settings();
8205
8297
  init_doctor2();
8206
8298
  init_resume();
8299
+ log30 = createChildLogger({ module: "telegram-menu-callbacks" });
8207
8300
  STATIC_COMMANDS = [
8208
8301
  { command: "new", description: "Create new session" },
8209
8302
  { command: "newchat", description: "New chat, same agent & workspace" },
@@ -8216,7 +8309,6 @@ var init_commands = __esm({
8216
8309
  { command: "menu", description: "Show menu" },
8217
8310
  { command: "integrate", description: "Manage agent integrations" },
8218
8311
  { command: "handoff", description: "Continue this session in your terminal" },
8219
- { command: "clear", description: "Clear assistant history" },
8220
8312
  { command: "restart", description: "Restart OpenACP" },
8221
8313
  { command: "update", description: "Update to latest version and restart" },
8222
8314
  { command: "doctor", description: "Run system diagnostics" },
@@ -8239,14 +8331,14 @@ var init_commands = __esm({
8239
8331
  // src/plugins/telegram/permissions.ts
8240
8332
  import { InlineKeyboard as InlineKeyboard11 } from "grammy";
8241
8333
  import { nanoid as nanoid4 } from "nanoid";
8242
- var log30, PermissionHandler;
8334
+ var log31, PermissionHandler;
8243
8335
  var init_permissions = __esm({
8244
8336
  "src/plugins/telegram/permissions.ts"() {
8245
8337
  "use strict";
8246
8338
  init_formatting();
8247
8339
  init_topics();
8248
8340
  init_log();
8249
- log30 = createChildLogger({ module: "telegram-permissions" });
8341
+ log31 = createChildLogger({ module: "telegram-permissions" });
8250
8342
  PermissionHandler = class {
8251
8343
  constructor(bot, chatId, getSession, sendNotification) {
8252
8344
  this.bot = bot;
@@ -8290,11 +8382,11 @@ ${escapeHtml(request.description)}`,
8290
8382
  });
8291
8383
  }
8292
8384
  setupCallbackHandler() {
8293
- this.bot.on("callback_query:data", async (ctx) => {
8385
+ this.bot.on("callback_query:data", async (ctx, next) => {
8294
8386
  const data = ctx.callbackQuery.data;
8295
- if (!data.startsWith("p:")) return;
8387
+ if (!data.startsWith("p:")) return next();
8296
8388
  const parts = data.split(":");
8297
- if (parts.length < 3) return;
8389
+ if (parts.length < 3) return next();
8298
8390
  const [, callbackKey, optionId] = parts;
8299
8391
  const pending = this.pending.get(callbackKey);
8300
8392
  if (!pending) {
@@ -8306,7 +8398,7 @@ ${escapeHtml(request.description)}`,
8306
8398
  }
8307
8399
  const session = this.getSession(pending.sessionId);
8308
8400
  const isAllow = pending.options.find((o) => o.id === optionId)?.isAllow ?? false;
8309
- log30.info({ requestId: pending.requestId, optionId, isAllow }, "Permission responded");
8401
+ log31.info({ requestId: pending.requestId, optionId, isAllow }, "Permission responded");
8310
8402
  if (session?.permissionGate.requestId === pending.requestId) {
8311
8403
  session.permissionGate.resolve(optionId);
8312
8404
  }
@@ -8326,7 +8418,7 @@ ${escapeHtml(request.description)}`,
8326
8418
  });
8327
8419
 
8328
8420
  // src/plugins/telegram/activity.ts
8329
- var log31, THINKING_REFRESH_MS, THINKING_MAX_MS, ThinkingIndicator, ToolCard, ActivityTracker2;
8421
+ var log32, THINKING_REFRESH_MS, THINKING_MAX_MS, ThinkingIndicator, ToolCard, ActivityTracker2;
8330
8422
  var init_activity = __esm({
8331
8423
  "src/plugins/telegram/activity.ts"() {
8332
8424
  "use strict";
@@ -8336,7 +8428,7 @@ var init_activity = __esm({
8336
8428
  init_stream_accumulator();
8337
8429
  init_stream_accumulator();
8338
8430
  init_display_spec_builder();
8339
- log31 = createChildLogger({ module: "telegram:activity" });
8431
+ log32 = createChildLogger({ module: "telegram:activity" });
8340
8432
  THINKING_REFRESH_MS = 15e3;
8341
8433
  THINKING_MAX_MS = 3 * 60 * 1e3;
8342
8434
  ThinkingIndicator = class {
@@ -8377,7 +8469,7 @@ var init_activity = __esm({
8377
8469
  }
8378
8470
  }
8379
8471
  } catch (err) {
8380
- log31.warn({ err }, "ThinkingIndicator.show() failed");
8472
+ log32.warn({ err }, "ThinkingIndicator.show() failed");
8381
8473
  } finally {
8382
8474
  this.sending = false;
8383
8475
  }
@@ -8545,7 +8637,7 @@ var init_activity = __esm({
8545
8637
  this.tracer?.log("telegram", { action: "telegram:delete:overflow", sessionId: this.sessionId, msgId: staleId });
8546
8638
  }
8547
8639
  } catch (err) {
8548
- log31.warn({ err }, "[ToolCard] send/edit failed");
8640
+ log32.warn({ err }, "[ToolCard] send/edit failed");
8549
8641
  }
8550
8642
  }
8551
8643
  };
@@ -8649,7 +8741,7 @@ var init_activity = __esm({
8649
8741
  const entry = this.toolStateMap.merge(id, status, rawInput, content, viewerLinks, diffStats);
8650
8742
  if (!existed || !entry) return;
8651
8743
  if (viewerLinks || entry.viewerLinks) {
8652
- log31.debug({ toolId: id, status, hasIncomingLinks: !!viewerLinks, hasEntryLinks: !!entry.viewerLinks, entryLinks: entry.viewerLinks }, "toolUpdate: viewer links trace");
8744
+ log32.debug({ toolId: id, status, hasIncomingLinks: !!viewerLinks, hasEntryLinks: !!entry.viewerLinks, entryLinks: entry.viewerLinks }, "toolUpdate: viewer links trace");
8653
8745
  }
8654
8746
  const spec = this.specBuilder.buildToolSpec(entry, this._outputMode, this.sessionContext);
8655
8747
  this.toolCard.updateFromSpec(spec);
@@ -8968,13 +9060,13 @@ var init_draft_manager = __esm({
8968
9060
  });
8969
9061
 
8970
9062
  // src/plugins/telegram/skill-command-manager.ts
8971
- var log32, SkillCommandManager;
9063
+ var log33, SkillCommandManager;
8972
9064
  var init_skill_command_manager = __esm({
8973
9065
  "src/plugins/telegram/skill-command-manager.ts"() {
8974
9066
  "use strict";
8975
9067
  init_commands();
8976
9068
  init_log();
8977
- log32 = createChildLogger({ module: "skill-commands" });
9069
+ log33 = createChildLogger({ module: "skill-commands" });
8978
9070
  SkillCommandManager = class {
8979
9071
  // sessionId → pinned msgId
8980
9072
  constructor(bot, chatId, sendQueue, sessionManager) {
@@ -9040,7 +9132,7 @@ var init_skill_command_manager = __esm({
9040
9132
  disable_notification: true
9041
9133
  });
9042
9134
  } catch (err) {
9043
- log32.error({ err, sessionId }, "Failed to send skill commands");
9135
+ log33.error({ err, sessionId }, "Failed to send skill commands");
9044
9136
  }
9045
9137
  }
9046
9138
  async cleanup(sessionId) {
@@ -9219,7 +9311,7 @@ async function validateBotAdmin(token, chatId) {
9219
9311
  };
9220
9312
  }
9221
9313
  const { status } = data.result;
9222
- log33.info(
9314
+ log34.info(
9223
9315
  { status, can_manage_topics: data.result.can_manage_topics, raw_result: data.result },
9224
9316
  "validateBotAdmin: getChatMember raw result"
9225
9317
  );
@@ -9239,7 +9331,7 @@ async function validateBotAdmin(token, chatId) {
9239
9331
  }
9240
9332
  async function checkTopicsPrerequisites(token, chatId) {
9241
9333
  const issues = [];
9242
- log33.info({ chatId }, "checkTopicsPrerequisites: starting checks");
9334
+ log34.info({ chatId }, "checkTopicsPrerequisites: starting checks");
9243
9335
  try {
9244
9336
  const res = await fetch(`https://api.telegram.org/bot${token}/getChat`, {
9245
9337
  method: "POST",
@@ -9247,7 +9339,7 @@ async function checkTopicsPrerequisites(token, chatId) {
9247
9339
  body: JSON.stringify({ chat_id: chatId })
9248
9340
  });
9249
9341
  const data = await res.json();
9250
- log33.info(
9342
+ log34.info(
9251
9343
  { chatId, apiOk: data.ok, is_forum: data.result?.is_forum, type: data.result?.type, title: data.result?.title },
9252
9344
  "checkTopicsPrerequisites: getChat result"
9253
9345
  );
@@ -9257,11 +9349,11 @@ async function checkTopicsPrerequisites(token, chatId) {
9257
9349
  );
9258
9350
  }
9259
9351
  } catch (err) {
9260
- log33.warn({ err, chatId }, "checkTopicsPrerequisites: getChat failed (network error)");
9352
+ log34.warn({ err, chatId }, "checkTopicsPrerequisites: getChat failed (network error)");
9261
9353
  issues.push("\u274C Could not check if Topics are enabled (network error).");
9262
9354
  }
9263
9355
  const adminResult = await validateBotAdmin(token, chatId);
9264
- log33.info(
9356
+ log34.info(
9265
9357
  { chatId, adminOk: adminResult.ok, canManageTopics: adminResult.ok ? adminResult.canManageTopics : void 0, error: !adminResult.ok ? adminResult.error : void 0 },
9266
9358
  "checkTopicsPrerequisites: validateBotAdmin result"
9267
9359
  );
@@ -9275,16 +9367,16 @@ async function checkTopicsPrerequisites(token, chatId) {
9275
9367
  '\u274C Bot cannot manage topics.\n\u2192 In Admin settings, enable the "Manage Topics" permission'
9276
9368
  );
9277
9369
  }
9278
- log33.info({ chatId, issueCount: issues.length, ok: issues.length === 0 }, "checkTopicsPrerequisites: result");
9370
+ log34.info({ chatId, issueCount: issues.length, ok: issues.length === 0 }, "checkTopicsPrerequisites: result");
9279
9371
  if (issues.length > 0) return { ok: false, issues };
9280
9372
  return { ok: true };
9281
9373
  }
9282
- var log33;
9374
+ var log34;
9283
9375
  var init_validators = __esm({
9284
9376
  "src/plugins/telegram/validators.ts"() {
9285
9377
  "use strict";
9286
9378
  init_log();
9287
- log33 = createChildLogger({ module: "telegram-validators" });
9379
+ log34 = createChildLogger({ module: "telegram-validators" });
9288
9380
  }
9289
9381
  });
9290
9382
 
@@ -9303,10 +9395,11 @@ function patchedFetch(input2, init) {
9303
9395
  }
9304
9396
  return fetch(input2, init);
9305
9397
  }
9306
- var log34, TelegramAdapter;
9398
+ var log35, TelegramAdapter;
9307
9399
  var init_adapter = __esm({
9308
9400
  "src/plugins/telegram/adapter.ts"() {
9309
9401
  "use strict";
9402
+ init_events();
9310
9403
  init_log();
9311
9404
  init_topics();
9312
9405
  init_commands();
@@ -9322,7 +9415,7 @@ var init_adapter = __esm({
9322
9415
  init_messaging_adapter();
9323
9416
  init_renderer2();
9324
9417
  init_output_mode_resolver();
9325
- log34 = createChildLogger({ module: "telegram" });
9418
+ log35 = createChildLogger({ module: "telegram" });
9326
9419
  TelegramAdapter = class extends MessagingAdapter {
9327
9420
  name = "telegram";
9328
9421
  renderer = new TelegramRenderer();
@@ -9449,7 +9542,7 @@ var init_adapter = __esm({
9449
9542
  );
9450
9543
  this.bot.catch((err) => {
9451
9544
  const rootCause = err.error instanceof Error ? err.error : err;
9452
- log34.error({ err: rootCause }, "Telegram bot error");
9545
+ log35.error({ err: rootCause }, "Telegram bot error");
9453
9546
  });
9454
9547
  this.bot.api.config.use(async (prev, method, payload, signal) => {
9455
9548
  const maxRetries = 3;
@@ -9467,7 +9560,7 @@ var init_adapter = __esm({
9467
9560
  if (rateLimitedMethods.includes(method)) {
9468
9561
  this.sendQueue.onRateLimited();
9469
9562
  }
9470
- log34.warn(
9563
+ log35.warn(
9471
9564
  { method, retryAfter, attempt: attempt + 1 },
9472
9565
  "Rate limited by Telegram, retrying"
9473
9566
  );
@@ -9655,9 +9748,9 @@ ${p}` : p;
9655
9748
  this.setupRoutes();
9656
9749
  this.bot.start({
9657
9750
  allowed_updates: ["message", "callback_query"],
9658
- onStart: () => log34.info({ chatId: this.telegramConfig.chatId }, "Telegram bot started")
9751
+ onStart: () => log35.info({ chatId: this.telegramConfig.chatId }, "Telegram bot started")
9659
9752
  });
9660
- log34.info(
9753
+ log35.info(
9661
9754
  {
9662
9755
  chatId: this.telegramConfig.chatId,
9663
9756
  notificationTopicId: this.telegramConfig.notificationTopicId,
@@ -9671,12 +9764,12 @@ ${p}` : p;
9671
9764
  this.telegramConfig.chatId
9672
9765
  );
9673
9766
  if (prereqResult.ok) {
9674
- log34.info("Telegram adapter: prerequisites OK, initializing topic-dependent features");
9767
+ log35.info("Telegram adapter: prerequisites OK, initializing topic-dependent features");
9675
9768
  await this.initTopicDependentFeatures();
9676
9769
  } else {
9677
- log34.warn({ issues: prereqResult.issues }, "Telegram adapter: prerequisites NOT met, starting watcher");
9770
+ log35.warn({ issues: prereqResult.issues }, "Telegram adapter: prerequisites NOT met, starting watcher");
9678
9771
  for (const issue of prereqResult.issues) {
9679
- log34.warn({ issue }, "Telegram prerequisite not met");
9772
+ log35.warn({ issue }, "Telegram prerequisite not met");
9680
9773
  }
9681
9774
  this.startPrerequisiteWatcher(prereqResult.issues);
9682
9775
  }
@@ -9692,7 +9785,7 @@ ${p}` : p;
9692
9785
  } catch (err) {
9693
9786
  if (attempt === maxRetries) throw err;
9694
9787
  const delay = baseDelayMs * Math.pow(2, attempt - 1);
9695
- log34.warn(
9788
+ log35.warn(
9696
9789
  { err, attempt, maxRetries, delayMs: delay, operation: label },
9697
9790
  `${label} failed, retrying in ${delay}ms`
9698
9791
  );
@@ -9712,12 +9805,12 @@ ${p}` : p;
9712
9805
  }),
9713
9806
  "setMyCommands"
9714
9807
  ).catch((err) => {
9715
- log34.warn({ err }, "Failed to register Telegram commands after retries (non-critical)");
9808
+ log35.warn({ err }, "Failed to register Telegram commands after retries (non-critical)");
9716
9809
  });
9717
9810
  }
9718
9811
  async initTopicDependentFeatures() {
9719
9812
  if (this._topicsInitialized) return;
9720
- log34.info(
9813
+ log35.info(
9721
9814
  { notificationTopicId: this.telegramConfig.notificationTopicId, assistantTopicId: this.telegramConfig.assistantTopicId },
9722
9815
  "initTopicDependentFeatures: starting (existing IDs in config)"
9723
9816
  );
@@ -9742,7 +9835,7 @@ ${p}` : p;
9742
9835
  this.assistantTopicId = topics.assistantTopicId;
9743
9836
  this._systemTopicIds.notificationTopicId = topics.notificationTopicId;
9744
9837
  this._systemTopicIds.assistantTopicId = topics.assistantTopicId;
9745
- log34.info(
9838
+ log35.info(
9746
9839
  { notificationTopicId: this.notificationTopicId, assistantTopicId: this.assistantTopicId },
9747
9840
  "initTopicDependentFeatures: topics ready"
9748
9841
  );
@@ -9773,16 +9866,16 @@ ${p}` : p;
9773
9866
  ).then((msg) => {
9774
9867
  if (msg) this.storeControlMsgId(sessionId, msg.message_id);
9775
9868
  }).catch((err) => {
9776
- log34.warn({ err, sessionId }, "Failed to send initial messages for new session");
9869
+ log35.warn({ err, sessionId }, "Failed to send initial messages for new session");
9777
9870
  });
9778
9871
  };
9779
- this.core.eventBus.on("session:threadReady", this._threadReadyHandler);
9872
+ this.core.eventBus.on(BusEvent.SESSION_THREAD_READY, this._threadReadyHandler);
9780
9873
  this._configChangedHandler = ({ sessionId }) => {
9781
9874
  this.updateControlMessage(sessionId).catch(() => {
9782
9875
  });
9783
9876
  };
9784
9877
  this.core.eventBus.on("session:configChanged", this._configChangedHandler);
9785
- log34.info({ assistantTopicId: this.assistantTopicId }, "initTopicDependentFeatures: sending welcome message");
9878
+ log35.info({ assistantTopicId: this.assistantTopicId }, "initTopicDependentFeatures: sending welcome message");
9786
9879
  try {
9787
9880
  const config = this.core.configManager.get();
9788
9881
  const agents = this.core.agentManager.getAvailableAgents();
@@ -9804,17 +9897,17 @@ ${p}` : p;
9804
9897
  this.core.lifecycleManager?.serviceRegistry?.get("menu-registry")
9805
9898
  )
9806
9899
  });
9807
- log34.info("initTopicDependentFeatures: welcome message sent");
9900
+ log35.info("initTopicDependentFeatures: welcome message sent");
9808
9901
  } catch (err) {
9809
- log34.warn({ err }, "Failed to send welcome message");
9902
+ log35.warn({ err }, "Failed to send welcome message");
9810
9903
  }
9811
9904
  try {
9812
- await this.core.assistantManager.spawn("telegram", String(this.assistantTopicId));
9905
+ await this.core.assistantManager.getOrSpawn("telegram", String(this.assistantTopicId));
9813
9906
  } catch (err) {
9814
- log34.error({ err }, "Failed to spawn assistant");
9907
+ log35.error({ err }, "Failed to spawn assistant");
9815
9908
  }
9816
9909
  this._topicsInitialized = true;
9817
- log34.info("Telegram adapter fully initialized");
9910
+ log35.info("Telegram adapter fully initialized");
9818
9911
  }
9819
9912
  startPrerequisiteWatcher(issues) {
9820
9913
  const setupMessage = `\u26A0\uFE0F <b>OpenACP needs setup before it can start.</b>
@@ -9825,7 +9918,7 @@ OpenACP will automatically retry until this is resolved.`;
9825
9918
  this.bot.api.sendMessage(this.telegramConfig.chatId, setupMessage, {
9826
9919
  parse_mode: "HTML"
9827
9920
  }).catch((err) => {
9828
- log34.warn({ err }, "Failed to send setup guidance to General topic");
9921
+ log35.warn({ err }, "Failed to send setup guidance to General topic");
9829
9922
  });
9830
9923
  const schedule = [5e3, 1e4, 3e4];
9831
9924
  let attempt = 1;
@@ -9838,7 +9931,7 @@ OpenACP will automatically retry until this is resolved.`;
9838
9931
  );
9839
9932
  if (result.ok) {
9840
9933
  this._prerequisiteWatcher = null;
9841
- log34.info("Prerequisites met \u2014 completing Telegram adapter initialization");
9934
+ log35.info("Prerequisites met \u2014 completing Telegram adapter initialization");
9842
9935
  try {
9843
9936
  await this.initTopicDependentFeatures();
9844
9937
  await this.bot.api.sendMessage(
@@ -9847,11 +9940,11 @@ OpenACP will automatically retry until this is resolved.`;
9847
9940
  { parse_mode: "HTML" }
9848
9941
  );
9849
9942
  } catch (err) {
9850
- log34.error({ err }, "Failed to complete initialization after prerequisites met");
9943
+ log35.error({ err }, "Failed to complete initialization after prerequisites met");
9851
9944
  }
9852
9945
  return;
9853
9946
  }
9854
- log34.debug({ issues: result.issues }, "Prerequisites not yet met, retrying");
9947
+ log35.debug({ issues: result.issues }, "Prerequisites not yet met, retrying");
9855
9948
  const delay = schedule[Math.min(attempt, schedule.length - 1)];
9856
9949
  attempt++;
9857
9950
  this._prerequisiteWatcher = setTimeout(retry, delay);
@@ -9868,7 +9961,7 @@ OpenACP will automatically retry until this is resolved.`;
9868
9961
  }
9869
9962
  this.sessionTrackers.clear();
9870
9963
  if (this._threadReadyHandler) {
9871
- this.core.eventBus.off("session:threadReady", this._threadReadyHandler);
9964
+ this.core.eventBus.off(BusEvent.SESSION_THREAD_READY, this._threadReadyHandler);
9872
9965
  this._threadReadyHandler = void 0;
9873
9966
  }
9874
9967
  if (this._configChangedHandler) {
@@ -9877,7 +9970,7 @@ OpenACP will automatically retry until this is resolved.`;
9877
9970
  }
9878
9971
  this.sendQueue.clear();
9879
9972
  await this.bot.stop();
9880
- log34.info("Telegram bot stopped");
9973
+ log35.info("Telegram bot stopped");
9881
9974
  }
9882
9975
  // --- CommandRegistry response rendering ---
9883
9976
  async renderCommandResponse(response, chatId, topicId) {
@@ -9962,6 +10055,13 @@ ${lines.join("\n")}`;
9962
10055
  }
9963
10056
  setupRoutes() {
9964
10057
  this.bot.on("message:text", async (ctx) => {
10058
+ if (!this._topicsInitialized) {
10059
+ await ctx.reply(
10060
+ "\u23F3 OpenACP is still setting up. Check the General topic for instructions."
10061
+ ).catch(() => {
10062
+ });
10063
+ return;
10064
+ }
9965
10065
  const threadId = ctx.message.message_thread_id;
9966
10066
  const text3 = ctx.message.text;
9967
10067
  if (!threadId) {
@@ -9994,7 +10094,7 @@ ${lines.join("\n")}`;
9994
10094
  threadId: String(threadId),
9995
10095
  userId: String(ctx.from.id),
9996
10096
  text: forwardText
9997
- }).catch((err) => log34.error({ err }, "handleMessage error"));
10097
+ }).catch((err) => log35.error({ err }, "handleMessage error"));
9998
10098
  });
9999
10099
  this.bot.on("message:photo", async (ctx) => {
10000
10100
  const threadId = ctx.message.message_thread_id;
@@ -10083,7 +10183,7 @@ ${lines.join("\n")}`;
10083
10183
  if (session.archiving) return;
10084
10184
  const threadId = Number(session.threadId);
10085
10185
  if (!threadId || isNaN(threadId)) {
10086
- log34.warn(
10186
+ log35.warn(
10087
10187
  { sessionId, threadId: session.threadId },
10088
10188
  "Session has no valid threadId, skipping message"
10089
10189
  );
@@ -10099,7 +10199,7 @@ ${lines.join("\n")}`;
10099
10199
  this._sessionThreadIds.delete(sessionId);
10100
10200
  }
10101
10201
  }).catch((err) => {
10102
- log34.warn({ err, sessionId }, "Dispatch queue error");
10202
+ log35.warn({ err, sessionId }, "Dispatch queue error");
10103
10203
  });
10104
10204
  this._dispatchQueues.set(sessionId, next);
10105
10205
  await next;
@@ -10221,7 +10321,7 @@ ${lines.join("\n")}`;
10221
10321
  );
10222
10322
  usageMsgId = result?.message_id;
10223
10323
  } catch (err) {
10224
- log34.warn({ err, sessionId }, "Failed to send usage message");
10324
+ log35.warn({ err, sessionId }, "Failed to send usage message");
10225
10325
  }
10226
10326
  if (this.notificationTopicId && sessionId !== this.core.assistantManager?.get("telegram")?.id) {
10227
10327
  const sess = this.core.sessionManager.getSession(sessionId);
@@ -10249,7 +10349,7 @@ Task completed.
10249
10349
  if (!content.attachment) return;
10250
10350
  const { attachment } = content;
10251
10351
  if (attachment.size > 50 * 1024 * 1024) {
10252
- log34.warn(
10352
+ log35.warn(
10253
10353
  {
10254
10354
  sessionId,
10255
10355
  fileName: attachment.fileName,
@@ -10293,7 +10393,7 @@ Task completed.
10293
10393
  );
10294
10394
  }
10295
10395
  } catch (err) {
10296
- log34.error(
10396
+ log35.error(
10297
10397
  { err, sessionId, fileName: attachment.fileName },
10298
10398
  "Failed to send attachment"
10299
10399
  );
@@ -10392,7 +10492,7 @@ Task completed.
10392
10492
  }
10393
10493
  async sendPermissionRequest(sessionId, request) {
10394
10494
  this.getTracer(sessionId)?.log("telegram", { action: "permission:send", sessionId, requestId: request.id, description: request.description });
10395
- log34.info({ sessionId, requestId: request.id }, "Permission request sent");
10495
+ log35.info({ sessionId, requestId: request.id }, "Permission request sent");
10396
10496
  const session = this.core.sessionManager.getSession(sessionId);
10397
10497
  if (!session) return;
10398
10498
  await this.sendQueue.enqueue(
@@ -10402,7 +10502,7 @@ Task completed.
10402
10502
  async sendNotification(notification) {
10403
10503
  this.getTracer(notification.sessionId)?.log("telegram", { action: "notification:send", sessionId: notification.sessionId, type: notification.type });
10404
10504
  if (notification.sessionId === this.core.assistantManager?.get("telegram")?.id) return;
10405
- log34.info(
10505
+ log35.info(
10406
10506
  { sessionId: notification.sessionId, type: notification.type },
10407
10507
  "Notification sent"
10408
10508
  );
@@ -10441,7 +10541,7 @@ Task completed.
10441
10541
  }
10442
10542
  async createSessionThread(sessionId, name) {
10443
10543
  this.getTracer(sessionId)?.log("telegram", { action: "thread:create", sessionId, name });
10444
- log34.info({ sessionId, name }, "Session topic created");
10544
+ log35.info({ sessionId, name }, "Session topic created");
10445
10545
  return String(
10446
10546
  await createSessionTopic(this.bot, this.telegramConfig.chatId, name)
10447
10547
  );
@@ -10452,7 +10552,7 @@ Task completed.
10452
10552
  if (!session) return;
10453
10553
  const threadId = Number(session.threadId);
10454
10554
  if (!threadId) {
10455
- log34.debug({ sessionId, newName }, "Cannot rename thread \u2014 threadId not set yet");
10555
+ log35.debug({ sessionId, newName }, "Cannot rename thread \u2014 threadId not set yet");
10456
10556
  return;
10457
10557
  }
10458
10558
  await renameSessionTopic(
@@ -10471,7 +10571,7 @@ Task completed.
10471
10571
  try {
10472
10572
  await this.bot.api.deleteForumTopic(this.telegramConfig.chatId, topicId);
10473
10573
  } catch (err) {
10474
- log34.warn(
10574
+ log35.warn(
10475
10575
  { err, sessionId, topicId },
10476
10576
  "Failed to delete forum topic (may already be deleted)"
10477
10577
  );
@@ -10515,7 +10615,7 @@ Task completed.
10515
10615
  const buffer = Buffer.from(await response.arrayBuffer());
10516
10616
  return { buffer, filePath: file.file_path };
10517
10617
  } catch (err) {
10518
- log34.error({ err }, "Failed to download file from Telegram");
10618
+ log35.error({ err }, "Failed to download file from Telegram");
10519
10619
  return null;
10520
10620
  }
10521
10621
  }
@@ -10536,7 +10636,7 @@ Task completed.
10536
10636
  try {
10537
10637
  buffer = await this.fileService.convertOggToWav(buffer);
10538
10638
  } catch (err) {
10539
- log34.warn({ err }, "OGG\u2192WAV conversion failed, saving original OGG");
10639
+ log35.warn({ err }, "OGG\u2192WAV conversion failed, saving original OGG");
10540
10640
  fileName = "voice.ogg";
10541
10641
  mimeType = "audio/ogg";
10542
10642
  originalFilePath = void 0;
@@ -10568,7 +10668,7 @@ Task completed.
10568
10668
  userId: String(userId),
10569
10669
  text: text3,
10570
10670
  attachments: [att]
10571
- }).catch((err) => log34.error({ err }, "handleMessage error"));
10671
+ }).catch((err) => log35.error({ err }, "handleMessage error"));
10572
10672
  }
10573
10673
  async cleanupSkillCommands(sessionId) {
10574
10674
  this._pendingSkillCommands.delete(sessionId);
@@ -10931,11 +11031,49 @@ var TypedEmitter = class _TypedEmitter {
10931
11031
 
10932
11032
  // src/core/agents/agent-instance.ts
10933
11033
  init_read_text_file();
11034
+
11035
+ // src/core/agents/attachment-blocks.ts
11036
+ var SUPPORTED_IMAGE_MIMES = /* @__PURE__ */ new Set([
11037
+ "image/jpeg",
11038
+ "image/png",
11039
+ "image/gif",
11040
+ "image/webp",
11041
+ "image/avif",
11042
+ "image/heic",
11043
+ "image/heif"
11044
+ ]);
11045
+ var MAX_ATTACHMENT_SIZE = 10 * 1024 * 1024;
11046
+ function buildAttachmentNote(att, capabilities) {
11047
+ const tooLarge = att.size > MAX_ATTACHMENT_SIZE;
11048
+ if (tooLarge) {
11049
+ const sizeMB = Math.round(att.size / 1024 / 1024);
11050
+ return `[Attachment skipped: "${att.fileName}" is too large (${sizeMB}MB > 10MB limit)]`;
11051
+ }
11052
+ if (att.type === "image") {
11053
+ if (!capabilities.image) {
11054
+ return null;
11055
+ }
11056
+ if (!SUPPORTED_IMAGE_MIMES.has(att.mimeType)) {
11057
+ return `[Attachment skipped: image format not supported (${att.mimeType})]`;
11058
+ }
11059
+ return null;
11060
+ }
11061
+ if (att.type === "audio") {
11062
+ if (!capabilities.audio) {
11063
+ return null;
11064
+ }
11065
+ return null;
11066
+ }
11067
+ return null;
11068
+ }
11069
+
11070
+ // src/core/agents/agent-instance.ts
10934
11071
  import { PROTOCOL_VERSION } from "@agentclientprotocol/sdk";
10935
11072
 
10936
11073
  // src/core/sessions/terminal-manager.ts
10937
11074
  import { spawn } from "child_process";
10938
11075
  import { randomUUID } from "crypto";
11076
+ init_events();
10939
11077
  var TerminalManager = class {
10940
11078
  terminals = /* @__PURE__ */ new Map();
10941
11079
  maxOutputBytes;
@@ -10952,7 +11090,7 @@ var TerminalManager = class {
10952
11090
  for (const ev of termEnvArr) {
10953
11091
  envRecord[ev.name] = ev.value;
10954
11092
  }
10955
- const result = await middlewareChain.execute("terminal:beforeCreate", {
11093
+ const result = await middlewareChain.execute(Hook.TERMINAL_BEFORE_CREATE, {
10956
11094
  sessionId,
10957
11095
  command: termCommand,
10958
11096
  args: termArgs,
@@ -11006,7 +11144,7 @@ var TerminalManager = class {
11006
11144
  childProcess.on("exit", (code, signal) => {
11007
11145
  state.exitStatus = { exitCode: code, signal };
11008
11146
  if (middlewareChain) {
11009
- middlewareChain.execute("terminal:afterExit", {
11147
+ middlewareChain.execute(Hook.TERMINAL_AFTER_EXIT, {
11010
11148
  sessionId,
11011
11149
  terminalId,
11012
11150
  command: state.command,
@@ -11136,6 +11274,7 @@ function createDebugTracer(sessionId, workingDirectory) {
11136
11274
 
11137
11275
  // src/core/agents/agent-instance.ts
11138
11276
  init_log();
11277
+ init_events();
11139
11278
  var log4 = createChildLogger({ module: "agent-instance" });
11140
11279
  function findPackageRoot(startDir) {
11141
11280
  let dir = startDir;
@@ -11218,7 +11357,7 @@ var AgentInstance = class _AgentInstance extends TypedEmitter {
11218
11357
  super();
11219
11358
  this.agentName = agentName;
11220
11359
  }
11221
- static async spawnSubprocess(agentDef, workingDirectory) {
11360
+ static async spawnSubprocess(agentDef, workingDirectory, allowedPaths = []) {
11222
11361
  const instance = new _AgentInstance(agentDef.name);
11223
11362
  const resolved = resolveAgentCommand(agentDef.command);
11224
11363
  log4.debug(
@@ -11232,10 +11371,7 @@ var AgentInstance = class _AgentInstance extends TypedEmitter {
11232
11371
  const ignorePatterns = PathGuard.loadIgnoreFile(workingDirectory);
11233
11372
  instance.pathGuard = new PathGuard({
11234
11373
  cwd: workingDirectory,
11235
- // allowedPaths is wired from workspace.security.allowedPaths config;
11236
- // spawnSubprocess would need to receive a SecurityConfig param to use it.
11237
- // Tracked as follow-up: pass workspace security config through spawn/resume call chain.
11238
- allowedPaths: [],
11374
+ allowedPaths,
11239
11375
  ignorePatterns
11240
11376
  });
11241
11377
  instance.child = spawn2(
@@ -11329,7 +11465,7 @@ var AgentInstance = class _AgentInstance extends TypedEmitter {
11329
11465
  if (signal === "SIGINT" || signal === "SIGTERM") return;
11330
11466
  if (code !== 0 && code !== null || signal) {
11331
11467
  const stderr = this.stderrCapture.getLastLines();
11332
- this.emit("agent_event", {
11468
+ this.emit(SessionEv.AGENT_EVENT, {
11333
11469
  type: "error",
11334
11470
  message: signal ? `Agent killed by signal ${signal}
11335
11471
  ${stderr}` : `Agent crashed (exit code ${code})
@@ -11341,7 +11477,7 @@ ${stderr}`
11341
11477
  log4.debug({ sessionId: this.sessionId }, "ACP connection closed");
11342
11478
  });
11343
11479
  }
11344
- static async spawn(agentDef, workingDirectory, mcpServers) {
11480
+ static async spawn(agentDef, workingDirectory, mcpServers, allowedPaths) {
11345
11481
  log4.debug(
11346
11482
  { agentName: agentDef.name, command: agentDef.command },
11347
11483
  "Spawning agent"
@@ -11349,7 +11485,8 @@ ${stderr}`
11349
11485
  const spawnStart = Date.now();
11350
11486
  const instance = await _AgentInstance.spawnSubprocess(
11351
11487
  agentDef,
11352
- workingDirectory
11488
+ workingDirectory,
11489
+ allowedPaths
11353
11490
  );
11354
11491
  const resolvedMcp = _AgentInstance.mcpManager.resolve(mcpServers);
11355
11492
  const response = await instance.connection.newSession({
@@ -11372,12 +11509,13 @@ ${stderr}`
11372
11509
  );
11373
11510
  return instance;
11374
11511
  }
11375
- static async resume(agentDef, workingDirectory, agentSessionId, mcpServers) {
11512
+ static async resume(agentDef, workingDirectory, agentSessionId, mcpServers, allowedPaths) {
11376
11513
  log4.debug({ agentName: agentDef.name, agentSessionId }, "Resuming agent");
11377
11514
  const spawnStart = Date.now();
11378
11515
  const instance = await _AgentInstance.spawnSubprocess(
11379
11516
  agentDef,
11380
- workingDirectory
11517
+ workingDirectory,
11518
+ allowedPaths
11381
11519
  );
11382
11520
  const resolvedMcp = _AgentInstance.mcpManager.resolve(mcpServers);
11383
11521
  try {
@@ -11565,7 +11703,7 @@ ${stderr}`
11565
11703
  return;
11566
11704
  }
11567
11705
  if (event !== null) {
11568
- self.emit("agent_event", event);
11706
+ self.emit(SessionEv.AGENT_EVENT, event);
11569
11707
  }
11570
11708
  },
11571
11709
  // ── Permission requests ──────────────────────────────────────────────
@@ -11592,7 +11730,7 @@ ${stderr}`
11592
11730
  throw new Error(`[Access denied] ${pathCheck.reason}`);
11593
11731
  }
11594
11732
  if (self.middlewareChain) {
11595
- const result = await self.middlewareChain.execute("fs:beforeRead", { sessionId: self.sessionId, path: p.path, line: p.line, limit: p.limit }, async (r) => r);
11733
+ const result = await self.middlewareChain.execute(Hook.FS_BEFORE_READ, { sessionId: self.sessionId, path: p.path, line: p.line, limit: p.limit }, async (r) => r);
11596
11734
  if (!result) return { content: "" };
11597
11735
  p.path = result.path;
11598
11736
  }
@@ -11610,7 +11748,7 @@ ${stderr}`
11610
11748
  throw new Error(`[Access denied] ${pathCheck.reason}`);
11611
11749
  }
11612
11750
  if (self.middlewareChain) {
11613
- const result = await self.middlewareChain.execute("fs:beforeWrite", { sessionId: self.sessionId, path: writePath, content: writeContent }, async (r) => r);
11751
+ const result = await self.middlewareChain.execute(Hook.FS_BEFORE_WRITE, { sessionId: self.sessionId, path: writePath, content: writeContent }, async (r) => r);
11614
11752
  if (!result) return {};
11615
11753
  writePath = result.path;
11616
11754
  writeContent = result.content;
@@ -11707,10 +11845,16 @@ ${stderr}`
11707
11845
  // ── Prompt & lifecycle ──────────────────────────────────────────────
11708
11846
  async prompt(text3, attachments) {
11709
11847
  const contentBlocks = [{ type: "text", text: text3 }];
11710
- const SUPPORTED_IMAGE_MIMES = /* @__PURE__ */ new Set(["image/jpeg", "image/png", "image/gif", "image/webp"]);
11848
+ const capabilities = this.promptCapabilities ?? {};
11711
11849
  for (const att of attachments ?? []) {
11712
- const tooLarge = att.size > 10 * 1024 * 1024;
11713
- if (att.type === "image" && this.promptCapabilities?.image && !tooLarge && SUPPORTED_IMAGE_MIMES.has(att.mimeType)) {
11850
+ const skipNote = buildAttachmentNote(att, capabilities);
11851
+ if (skipNote !== null) {
11852
+ contentBlocks[0].text += `
11853
+
11854
+ ${skipNote}`;
11855
+ continue;
11856
+ }
11857
+ if (att.type === "image" && capabilities.image && SUPPORTED_IMAGE_MIMES.has(att.mimeType)) {
11714
11858
  const attCheck = this.pathGuard.validatePath(att.filePath, "read");
11715
11859
  if (!attCheck.allowed) {
11716
11860
  contentBlocks[0].text += `
@@ -11720,7 +11864,7 @@ ${stderr}`
11720
11864
  }
11721
11865
  const data = await fs8.promises.readFile(att.filePath);
11722
11866
  contentBlocks.push({ type: "image", data: data.toString("base64"), mimeType: att.mimeType });
11723
- } else if (att.type === "audio" && this.promptCapabilities?.audio && !tooLarge) {
11867
+ } else if (att.type === "audio" && capabilities.audio) {
11724
11868
  const attCheck = this.pathGuard.validatePath(att.filePath, "read");
11725
11869
  if (!attCheck.allowed) {
11726
11870
  contentBlocks[0].text += `
@@ -11731,9 +11875,9 @@ ${stderr}`
11731
11875
  const data = await fs8.promises.readFile(att.filePath);
11732
11876
  contentBlocks.push({ type: "audio", data: data.toString("base64"), mimeType: att.mimeType });
11733
11877
  } else {
11734
- if ((att.type === "image" || att.type === "audio") && !tooLarge) {
11878
+ if (att.type === "image" || att.type === "audio") {
11735
11879
  log4.debug(
11736
- { type: att.type, capabilities: this.promptCapabilities ?? {} },
11880
+ { type: att.type, capabilities },
11737
11881
  "Agent does not support %s content, falling back to file path",
11738
11882
  att.type
11739
11883
  );
@@ -11789,15 +11933,15 @@ var AgentManager = class {
11789
11933
  getAgent(name) {
11790
11934
  return this.catalog.resolve(name);
11791
11935
  }
11792
- async spawn(agentName, workingDirectory) {
11936
+ async spawn(agentName, workingDirectory, allowedPaths) {
11793
11937
  const agentDef = this.getAgent(agentName);
11794
11938
  if (!agentDef) throw new Error(`Agent "${agentName}" is not installed. Run "openacp agents install ${agentName}" to add it.`);
11795
- return AgentInstance.spawn(agentDef, workingDirectory);
11939
+ return AgentInstance.spawn(agentDef, workingDirectory, void 0, allowedPaths);
11796
11940
  }
11797
- async resume(agentName, workingDirectory, agentSessionId) {
11941
+ async resume(agentName, workingDirectory, agentSessionId, allowedPaths) {
11798
11942
  const agentDef = this.getAgent(agentName);
11799
11943
  if (!agentDef) throw new Error(`Agent "${agentName}" is not installed. Run "openacp agents install ${agentName}" to add it.`);
11800
- return AgentInstance.resume(agentDef, workingDirectory, agentSessionId);
11944
+ return AgentInstance.resume(agentDef, workingDirectory, agentSessionId, void 0, allowedPaths);
11801
11945
  }
11802
11946
  };
11803
11947
 
@@ -11970,6 +12114,7 @@ function isSystemEvent(event) {
11970
12114
  }
11971
12115
 
11972
12116
  // src/core/sessions/session.ts
12117
+ init_events();
11973
12118
  var moduleLog = createChildLogger({ module: "session" });
11974
12119
  var TTS_PROMPT_INSTRUCTION = `
11975
12120
 
@@ -11998,7 +12143,15 @@ var Session = class extends TypedEmitter {
11998
12143
  }
11999
12144
  agentName;
12000
12145
  workingDirectory;
12001
- agentInstance;
12146
+ _agentInstance;
12147
+ get agentInstance() {
12148
+ return this._agentInstance;
12149
+ }
12150
+ set agentInstance(agent) {
12151
+ this._agentInstance = agent;
12152
+ this.wireAgentRelay();
12153
+ this.wireCommandsBuffer();
12154
+ }
12002
12155
  agentSessionId = "";
12003
12156
  _status = "initializing";
12004
12157
  name;
@@ -12022,9 +12175,6 @@ var Session = class extends TypedEmitter {
12022
12175
  threadIds = /* @__PURE__ */ new Map();
12023
12176
  /** Active turn context — sealed on prompt dequeue, cleared on turn end */
12024
12177
  activeTurnContext = null;
12025
- /** The agentInstance for which the agent→session event relay is wired (prevents duplicate relays from multiple bridges).
12026
- * When the agent is swapped, the relay must be re-wired to the new instance. */
12027
- agentRelaySource = null;
12028
12178
  permissionGate = new PermissionGate();
12029
12179
  queue;
12030
12180
  speechService;
@@ -12048,23 +12198,37 @@ var Session = class extends TypedEmitter {
12048
12198
  this.log.error({ err }, "Prompt execution failed");
12049
12199
  const message = err instanceof Error ? err.message : String(err);
12050
12200
  this.fail(message);
12051
- this.emit("agent_event", { type: "error", message: `Prompt execution failed: ${message}` });
12201
+ this.emit(SessionEv.AGENT_EVENT, { type: "error", message: `Prompt execution failed: ${message}` });
12052
12202
  }
12053
12203
  );
12054
- this.wireCommandsBuffer();
12204
+ }
12205
+ /** Wire the agent→session event relay on the current agentInstance.
12206
+ * Removes any previous relay first to avoid duplicates on agent switch.
12207
+ * This relay ensures session.emit("agent_event") fires for ALL sessions,
12208
+ * including headless API sessions that have no SessionBridge attached. */
12209
+ agentRelayCleanup;
12210
+ wireAgentRelay() {
12211
+ this.agentRelayCleanup?.();
12212
+ const instance = this._agentInstance;
12213
+ const handler = (event) => {
12214
+ this.emit(SessionEv.AGENT_EVENT, event);
12215
+ };
12216
+ instance.on(SessionEv.AGENT_EVENT, handler);
12217
+ this.agentRelayCleanup = () => instance.off(SessionEv.AGENT_EVENT, handler);
12055
12218
  }
12056
12219
  /** Wire a listener on the current agentInstance to buffer commands_update events.
12057
12220
  * Must be called after every agentInstance replacement (constructor + switchAgent). */
12058
12221
  commandsBufferCleanup;
12059
12222
  wireCommandsBuffer() {
12060
12223
  this.commandsBufferCleanup?.();
12224
+ const instance = this._agentInstance;
12061
12225
  const handler = (event) => {
12062
12226
  if (event.type === "commands_update") {
12063
12227
  this.latestCommands = event.commands;
12064
12228
  }
12065
12229
  };
12066
- this.agentInstance.on("agent_event", handler);
12067
- this.commandsBufferCleanup = () => this.agentInstance.off("agent_event", handler);
12230
+ instance.on(SessionEv.AGENT_EVENT, handler);
12231
+ this.commandsBufferCleanup = () => instance.off(SessionEv.AGENT_EVENT, handler);
12068
12232
  }
12069
12233
  // --- State Machine ---
12070
12234
  get status() {
@@ -12078,12 +12242,12 @@ var Session = class extends TypedEmitter {
12078
12242
  fail(reason) {
12079
12243
  if (this._status === "error") return;
12080
12244
  this.transition("error");
12081
- this.emit("error", new Error(reason));
12245
+ this.emit(SessionEv.ERROR, new Error(reason));
12082
12246
  }
12083
12247
  /** Transition to finished — from active only. Emits session_end for backward compat. */
12084
12248
  finish(reason) {
12085
12249
  this.transition("finished");
12086
- this.emit("session_end", reason ?? "completed");
12250
+ this.emit(SessionEv.SESSION_END, reason ?? "completed");
12087
12251
  }
12088
12252
  /** Transition to cancelled — from active only (terminal session cancel) */
12089
12253
  markCancelled() {
@@ -12099,7 +12263,7 @@ var Session = class extends TypedEmitter {
12099
12263
  }
12100
12264
  this._status = to;
12101
12265
  this.log.debug({ from, to }, "Session status transition");
12102
- this.emit("status_change", from, to);
12266
+ this.emit(SessionEv.STATUS_CHANGE, from, to);
12103
12267
  }
12104
12268
  /** Number of prompts waiting in queue */
12105
12269
  get queueDepth() {
@@ -12121,8 +12285,8 @@ var Session = class extends TypedEmitter {
12121
12285
  async enqueuePrompt(text3, attachments, routing, externalTurnId) {
12122
12286
  const turnId = externalTurnId ?? nanoid2(8);
12123
12287
  if (this.middlewareChain) {
12124
- const payload = { text: text3, attachments, sessionId: this.id };
12125
- const result = await this.middlewareChain.execute("agent:beforePrompt", payload, async (p) => p);
12288
+ const payload = { text: text3, attachments, sessionId: this.id, sourceAdapterId: routing?.sourceAdapterId };
12289
+ const result = await this.middlewareChain.execute(Hook.AGENT_BEFORE_PROMPT, payload, async (p) => p);
12126
12290
  if (!result) return turnId;
12127
12291
  text3 = result.text;
12128
12292
  attachments = result.attachments;
@@ -12137,9 +12301,9 @@ var Session = class extends TypedEmitter {
12137
12301
  routing?.responseAdapterId,
12138
12302
  turnId
12139
12303
  );
12140
- this.emit("turn_started", this.activeTurnContext);
12304
+ this.emit(SessionEv.TURN_STARTED, this.activeTurnContext);
12141
12305
  this.promptCount++;
12142
- this.emit("prompt_count_changed", this.promptCount);
12306
+ this.emit(SessionEv.PROMPT_COUNT_CHANGED, this.promptCount);
12143
12307
  if (this._status === "initializing" || this._status === "cancelled" || this._status === "error") {
12144
12308
  this.activate();
12145
12309
  }
@@ -12168,10 +12332,18 @@ ${text3}`;
12168
12332
  }
12169
12333
  } : null;
12170
12334
  if (accumulatorListener) {
12171
- this.on("agent_event", accumulatorListener);
12335
+ this.on(SessionEv.AGENT_EVENT, accumulatorListener);
12336
+ }
12337
+ const mw = this.middlewareChain;
12338
+ const afterEventListener = mw ? (event) => {
12339
+ mw.execute(Hook.AGENT_AFTER_EVENT, { sessionId: this.id, event, outgoingMessage: { type: "text", text: "" } }, async (e) => e).catch(() => {
12340
+ });
12341
+ } : null;
12342
+ if (afterEventListener) {
12343
+ this.agentInstance.on(SessionEv.AGENT_EVENT, afterEventListener);
12172
12344
  }
12173
12345
  if (this.middlewareChain) {
12174
- this.middlewareChain.execute("turn:start", { sessionId: this.id, promptText: processed.text, promptNumber: this.promptCount }, async (p) => p).catch(() => {
12346
+ this.middlewareChain.execute(Hook.TURN_START, { sessionId: this.id, promptText: processed.text, promptNumber: this.promptCount }, async (p) => p).catch(() => {
12175
12347
  });
12176
12348
  }
12177
12349
  let stopReason = "end_turn";
@@ -12192,10 +12364,13 @@ ${text3}`;
12192
12364
  promptError = err;
12193
12365
  } finally {
12194
12366
  if (accumulatorListener) {
12195
- this.off("agent_event", accumulatorListener);
12367
+ this.off(SessionEv.AGENT_EVENT, accumulatorListener);
12368
+ }
12369
+ if (afterEventListener) {
12370
+ this.agentInstance.off(SessionEv.AGENT_EVENT, afterEventListener);
12196
12371
  }
12197
12372
  if (this.middlewareChain) {
12198
- this.middlewareChain.execute("turn:end", { sessionId: this.id, stopReason, durationMs: Date.now() - promptStart }, async (p) => p).catch(() => {
12373
+ this.middlewareChain.execute(Hook.TURN_END, { sessionId: this.id, stopReason, durationMs: Date.now() - promptStart }, async (p) => p).catch(() => {
12199
12374
  });
12200
12375
  }
12201
12376
  this.activeTurnContext = null;
@@ -12240,7 +12415,7 @@ ${text3}`;
12240
12415
  const audioBuffer = await fs9.promises.readFile(audioPath);
12241
12416
  const result = await this.speechService.transcribe(audioBuffer, audioMime);
12242
12417
  this.log.info({ provider: "stt", duration: result.duration }, "Voice transcribed");
12243
- this.emit("agent_event", {
12418
+ this.emit(SessionEv.AGENT_EVENT, {
12244
12419
  type: "system_message",
12245
12420
  message: `\u{1F3A4} You said: ${result.text}`
12246
12421
  });
@@ -12249,7 +12424,7 @@ ${text3}`;
12249
12424
  ${result.text}` : result.text;
12250
12425
  } catch (err) {
12251
12426
  this.log.warn({ err }, "STT transcription failed, keeping audio attachment");
12252
- this.emit("agent_event", {
12427
+ this.emit(SessionEv.AGENT_EVENT, {
12253
12428
  type: "error",
12254
12429
  message: `Voice transcription failed: ${err.message}`
12255
12430
  });
@@ -12283,12 +12458,12 @@ ${result.text}` : result.text;
12283
12458
  timeoutPromise
12284
12459
  ]);
12285
12460
  const base64 = result.audioBuffer.toString("base64");
12286
- this.emit("agent_event", {
12461
+ this.emit(SessionEv.AGENT_EVENT, {
12287
12462
  type: "audio_content",
12288
12463
  data: base64,
12289
12464
  mimeType: result.mimeType
12290
12465
  });
12291
- this.emit("agent_event", { type: "tts_strip" });
12466
+ this.emit(SessionEv.AGENT_EVENT, { type: "tts_strip" });
12292
12467
  this.log.info("TTS synthesis completed");
12293
12468
  } finally {
12294
12469
  clearTimeout(ttsTimer);
@@ -12303,19 +12478,19 @@ ${result.text}` : result.text;
12303
12478
  const captureHandler = (event) => {
12304
12479
  if (event.type === "text") title += event.content;
12305
12480
  };
12306
- this.pause((event) => event !== "agent_event");
12307
- this.agentInstance.on("agent_event", captureHandler);
12481
+ this.pause((event) => event !== SessionEv.AGENT_EVENT);
12482
+ this.agentInstance.on(SessionEv.AGENT_EVENT, captureHandler);
12308
12483
  try {
12309
12484
  await this.agentInstance.prompt(
12310
12485
  "Summarize this conversation in max 5 words for a topic title. Reply ONLY with the title, nothing else."
12311
12486
  );
12312
12487
  this.name = title.trim().slice(0, 50) || `Session ${this.id.slice(0, 6)}`;
12313
12488
  this.log.info({ name: this.name }, "Session auto-named");
12314
- this.emit("named", this.name);
12489
+ this.emit(SessionEv.NAMED, this.name);
12315
12490
  } catch {
12316
12491
  this.name = `Session ${this.id.slice(0, 6)}`;
12317
12492
  } finally {
12318
- this.agentInstance.off("agent_event", captureHandler);
12493
+ this.agentInstance.off(SessionEv.AGENT_EVENT, captureHandler);
12319
12494
  this.clearBuffer();
12320
12495
  this.resume();
12321
12496
  }
@@ -12377,7 +12552,7 @@ ${result.text}` : result.text;
12377
12552
  /** Set session name explicitly and emit 'named' event */
12378
12553
  setName(name) {
12379
12554
  this.name = name;
12380
- this.emit("named", name);
12555
+ this.emit(SessionEv.NAMED, name);
12381
12556
  }
12382
12557
  /** Send a config option change to the agent and update local state from the response. */
12383
12558
  async setConfigOption(configId, value) {
@@ -12393,7 +12568,7 @@ ${result.text}` : result.text;
12393
12568
  }
12394
12569
  async updateConfigOptions(options) {
12395
12570
  if (this.middlewareChain) {
12396
- const result = await this.middlewareChain.execute("config:beforeChange", { sessionId: this.id, configId: "options", oldValue: this.configOptions, newValue: options }, async (p) => p);
12571
+ const result = await this.middlewareChain.execute(Hook.CONFIG_BEFORE_CHANGE, { sessionId: this.id, configId: "options", oldValue: this.configOptions, newValue: options }, async (p) => p);
12397
12572
  if (!result) return;
12398
12573
  }
12399
12574
  this.configOptions = options;
@@ -12413,7 +12588,7 @@ ${result.text}` : result.text;
12413
12588
  /** Cancel the current prompt and clear the queue. Stays in active state. */
12414
12589
  async abortPrompt() {
12415
12590
  if (this.middlewareChain) {
12416
- const result = await this.middlewareChain.execute("agent:beforeCancel", { sessionId: this.id }, async (p) => p);
12591
+ const result = await this.middlewareChain.execute(Hook.AGENT_BEFORE_CANCEL, { sessionId: this.id }, async (p) => p);
12417
12592
  if (!result) return;
12418
12593
  }
12419
12594
  this.queue.clear();
@@ -12454,7 +12629,6 @@ ${result.text}` : result.text;
12454
12629
  this.configOptions = [];
12455
12630
  this.latestCommands = null;
12456
12631
  this.applySpawnResponse(newAgent.initialSessionResponse, newAgent.agentCapabilities);
12457
- this.wireCommandsBuffer();
12458
12632
  this.log.info({ from: this.agentSwitchHistory.at(-1).agentName, to: agentName }, "Agent switched");
12459
12633
  }
12460
12634
  async destroy() {
@@ -12893,6 +13067,7 @@ var MessageTransformer = class {
12893
13067
  };
12894
13068
 
12895
13069
  // src/core/sessions/session-manager.ts
13070
+ init_events();
12896
13071
  var SessionManager = class {
12897
13072
  sessions = /* @__PURE__ */ new Map();
12898
13073
  store;
@@ -12997,18 +13172,18 @@ var SessionManager = class {
12997
13172
  }
12998
13173
  }
12999
13174
  if (this.middlewareChain) {
13000
- this.middlewareChain.execute("session:afterDestroy", { sessionId }, async (p) => p).catch(() => {
13175
+ this.middlewareChain.execute(Hook.SESSION_AFTER_DESTROY, { sessionId }, async (p) => p).catch(() => {
13001
13176
  });
13002
13177
  }
13003
13178
  }
13004
13179
  listSessions(channelId) {
13005
- const all = Array.from(this.sessions.values());
13180
+ const all = Array.from(this.sessions.values()).filter((s) => !s.isAssistant);
13006
13181
  if (channelId) return all.filter((s) => s.channelId === channelId);
13007
13182
  return all;
13008
13183
  }
13009
13184
  listAllSessions(channelId) {
13010
13185
  if (this.store) {
13011
- let records = this.store.list();
13186
+ let records = this.store.list().filter((r) => !r.isAssistant);
13012
13187
  if (channelId) records = records.filter((r) => r.channelId === channelId);
13013
13188
  return records.map((record) => {
13014
13189
  const live2 = this.sessions.get(record.sessionId);
@@ -13048,7 +13223,7 @@ var SessionManager = class {
13048
13223
  };
13049
13224
  });
13050
13225
  }
13051
- let live = Array.from(this.sessions.values());
13226
+ let live = Array.from(this.sessions.values()).filter((s) => !s.isAssistant);
13052
13227
  if (channelId) live = live.filter((s) => s.channelId === channelId);
13053
13228
  return live.map((s) => ({
13054
13229
  id: s.id,
@@ -13069,7 +13244,7 @@ var SessionManager = class {
13069
13244
  }
13070
13245
  listRecords(filter) {
13071
13246
  if (!this.store) return [];
13072
- let records = this.store.list();
13247
+ let records = this.store.list().filter((r) => !r.isAssistant);
13073
13248
  if (filter?.statuses?.length) {
13074
13249
  records = records.filter((r) => filter.statuses.includes(r.status));
13075
13250
  }
@@ -13078,7 +13253,7 @@ var SessionManager = class {
13078
13253
  async removeRecord(sessionId) {
13079
13254
  if (!this.store) return;
13080
13255
  await this.store.remove(sessionId);
13081
- this.eventBus?.emit("session:deleted", { sessionId });
13256
+ this.eventBus?.emit(BusEvent.SESSION_DELETED, { sessionId });
13082
13257
  }
13083
13258
  /**
13084
13259
  * Graceful shutdown: persist session state without killing agent subprocesses.
@@ -13126,7 +13301,7 @@ var SessionManager = class {
13126
13301
  this.sessions.clear();
13127
13302
  if (this.middlewareChain) {
13128
13303
  for (const sessionId of sessionIds) {
13129
- this.middlewareChain.execute("session:afterDestroy", { sessionId }, async (p) => p).catch(() => {
13304
+ this.middlewareChain.execute(Hook.SESSION_AFTER_DESTROY, { sessionId }, async (p) => p).catch(() => {
13130
13305
  });
13131
13306
  }
13132
13307
  }
@@ -13136,6 +13311,7 @@ var SessionManager = class {
13136
13311
  // src/core/sessions/session-bridge.ts
13137
13312
  init_log();
13138
13313
  init_bypass_detection();
13314
+ init_events();
13139
13315
  var log6 = createChildLogger({ module: "session-bridge" });
13140
13316
  var SessionBridge = class {
13141
13317
  constructor(session, adapter, deps, adapterId) {
@@ -13160,7 +13336,7 @@ var SessionBridge = class {
13160
13336
  try {
13161
13337
  const mw = this.deps.middlewareChain;
13162
13338
  if (mw) {
13163
- const result = await mw.execute("message:outgoing", { sessionId, message }, async (m) => m);
13339
+ const result = await mw.execute(Hook.MESSAGE_OUTGOING, { sessionId, message }, async (m) => m);
13164
13340
  this.tracer?.log("core", { step: "middleware:outgoing", sessionId, hook: "message:outgoing", blocked: !result });
13165
13341
  if (!result) return;
13166
13342
  this.tracer?.log("core", { step: "dispatch", sessionId, message: result.message });
@@ -13189,17 +13365,11 @@ var SessionBridge = class {
13189
13365
  connect() {
13190
13366
  if (this.connected) return;
13191
13367
  this.connected = true;
13192
- if (this.session.agentRelaySource !== this.session.agentInstance) {
13193
- this.listen(this.session.agentInstance, "agent_event", (event) => {
13194
- this.session.emit("agent_event", event);
13195
- });
13196
- this.session.agentRelaySource = this.session.agentInstance;
13197
- }
13198
- this.listen(this.session, "agent_event", (event) => {
13368
+ this.listen(this.session, SessionEv.AGENT_EVENT, (event) => {
13199
13369
  if (this.shouldForward(event)) {
13200
13370
  this.dispatchAgentEvent(event);
13201
13371
  } else {
13202
- this.deps.eventBus?.emit("agent:event", { sessionId: this.session.id, event });
13372
+ this.deps.eventBus?.emit(BusEvent.AGENT_EVENT, { sessionId: this.session.id, event });
13203
13373
  }
13204
13374
  });
13205
13375
  if (!this.session.agentInstance.onPermissionRequest || this.session.agentInstance.onPermissionRequest.__bridgeId === void 0) {
@@ -13209,7 +13379,7 @@ var SessionBridge = class {
13209
13379
  handler.__bridgeId = this.adapterId;
13210
13380
  this.session.agentInstance.onPermissionRequest = handler;
13211
13381
  }
13212
- this.listen(this.session, "permission_request", async (request) => {
13382
+ this.listen(this.session, SessionEv.PERMISSION_REQUEST, async (request) => {
13213
13383
  const current = this.session.agentInstance.onPermissionRequest;
13214
13384
  if (current?.__bridgeId === this.adapterId) return;
13215
13385
  if (!this.session.permissionGate.isPending) return;
@@ -13219,37 +13389,41 @@ var SessionBridge = class {
13219
13389
  log6.error({ err, sessionId: this.session.id, adapterId: this.adapterId }, "Failed to send permission request to adapter");
13220
13390
  }
13221
13391
  });
13222
- this.listen(this.session, "status_change", (from, to) => {
13392
+ this.listen(this.session, SessionEv.STATUS_CHANGE, (from, to) => {
13223
13393
  this.deps.sessionManager.patchRecord(this.session.id, {
13224
13394
  status: to,
13225
13395
  lastActiveAt: (/* @__PURE__ */ new Date()).toISOString()
13226
13396
  });
13227
- this.deps.eventBus?.emit("session:updated", {
13228
- sessionId: this.session.id,
13229
- status: to
13230
- });
13397
+ if (!this.session.isAssistant) {
13398
+ this.deps.eventBus?.emit(BusEvent.SESSION_UPDATED, {
13399
+ sessionId: this.session.id,
13400
+ status: to
13401
+ });
13402
+ }
13231
13403
  if (to === "finished") {
13232
13404
  queueMicrotask(() => this.disconnect());
13233
13405
  }
13234
13406
  });
13235
- this.listen(this.session, "named", async (name) => {
13407
+ this.listen(this.session, SessionEv.NAMED, async (name) => {
13236
13408
  const record = this.deps.sessionManager.getSessionRecord(this.session.id);
13237
13409
  const alreadyNamed = !!record?.name;
13238
13410
  await this.deps.sessionManager.patchRecord(this.session.id, { name });
13239
- this.deps.eventBus?.emit("session:updated", {
13240
- sessionId: this.session.id,
13241
- name
13242
- });
13411
+ if (!this.session.isAssistant) {
13412
+ this.deps.eventBus?.emit(BusEvent.SESSION_UPDATED, {
13413
+ sessionId: this.session.id,
13414
+ name
13415
+ });
13416
+ }
13243
13417
  if (!alreadyNamed) {
13244
13418
  await this.adapter.renameSessionThread(this.session.id, name);
13245
13419
  }
13246
13420
  });
13247
- this.listen(this.session, "prompt_count_changed", (count) => {
13421
+ this.listen(this.session, SessionEv.PROMPT_COUNT_CHANGED, (count) => {
13248
13422
  this.deps.sessionManager.patchRecord(this.session.id, { currentPromptCount: count });
13249
13423
  });
13250
- this.listen(this.session, "turn_started", (ctx) => {
13424
+ this.listen(this.session, SessionEv.TURN_STARTED, (ctx) => {
13251
13425
  if (ctx.sourceAdapterId !== "sse" && ctx.sourceAdapterId !== "api") {
13252
- this.deps.eventBus?.emit("message:processing", {
13426
+ this.deps.eventBus?.emit(BusEvent.MESSAGE_PROCESSING, {
13253
13427
  sessionId: this.session.id,
13254
13428
  turnId: ctx.turnId,
13255
13429
  sourceAdapterId: ctx.sourceAdapterId,
@@ -13258,10 +13432,10 @@ var SessionBridge = class {
13258
13432
  }
13259
13433
  });
13260
13434
  if (this.session.latestCommands !== null) {
13261
- this.session.emit("agent_event", { type: "commands_update", commands: this.session.latestCommands });
13435
+ this.session.emit(SessionEv.AGENT_EVENT, { type: "commands_update", commands: this.session.latestCommands });
13262
13436
  }
13263
13437
  if (this.session.configOptions.length > 0) {
13264
- this.session.emit("agent_event", { type: "config_option_update", options: this.session.configOptions });
13438
+ this.session.emit(SessionEv.AGENT_EVENT, { type: "config_option_update", options: this.session.configOptions });
13265
13439
  }
13266
13440
  }
13267
13441
  disconnect() {
@@ -13281,17 +13455,11 @@ var SessionBridge = class {
13281
13455
  const mw = this.deps.middlewareChain;
13282
13456
  if (mw) {
13283
13457
  try {
13284
- const result = await mw.execute("agent:beforeEvent", { sessionId: this.session.id, event }, async (e) => e);
13458
+ const result = await mw.execute(Hook.AGENT_BEFORE_EVENT, { sessionId: this.session.id, event }, async (e) => e);
13285
13459
  this.tracer?.log("core", { step: "middleware:before", sessionId: this.session.id, hook: "agent:beforeEvent", blocked: !result });
13286
13460
  if (!result) return;
13287
13461
  const transformedEvent = result.event;
13288
- const outgoing = this.handleAgentEvent(transformedEvent);
13289
- mw.execute("agent:afterEvent", {
13290
- sessionId: this.session.id,
13291
- event: transformedEvent,
13292
- outgoingMessage: outgoing ?? { type: "text", text: "" }
13293
- }, async (e) => e).catch(() => {
13294
- });
13462
+ this.handleAgentEvent(transformedEvent);
13295
13463
  } catch {
13296
13464
  try {
13297
13465
  this.handleAgentEvent(event);
@@ -13424,7 +13592,7 @@ var SessionBridge = class {
13424
13592
  this.adapter.stripTTSBlock?.(this.session.id);
13425
13593
  break;
13426
13594
  }
13427
- this.deps.eventBus?.emit("agent:event", {
13595
+ this.deps.eventBus?.emit(BusEvent.AGENT_EVENT, {
13428
13596
  sessionId: this.session.id,
13429
13597
  event
13430
13598
  });
@@ -13443,7 +13611,7 @@ var SessionBridge = class {
13443
13611
  let permReq = request;
13444
13612
  if (mw) {
13445
13613
  const payload = { sessionId: this.session.id, request, autoResolve: void 0 };
13446
- const result = await mw.execute("permission:beforeRequest", payload, async (r) => r);
13614
+ const result = await mw.execute(Hook.PERMISSION_BEFORE_REQUEST, payload, async (r) => r);
13447
13615
  if (!result) return "";
13448
13616
  permReq = result.request;
13449
13617
  if (result.autoResolve) {
@@ -13451,21 +13619,21 @@ var SessionBridge = class {
13451
13619
  return result.autoResolve;
13452
13620
  }
13453
13621
  }
13454
- this.deps.eventBus?.emit("permission:request", {
13622
+ this.deps.eventBus?.emit(BusEvent.PERMISSION_REQUEST, {
13455
13623
  sessionId: this.session.id,
13456
13624
  permission: permReq
13457
13625
  });
13458
13626
  const autoDecision = this.checkAutoApprove(permReq);
13459
13627
  if (autoDecision) {
13460
- this.session.emit("permission_request", permReq);
13628
+ this.session.emit(SessionEv.PERMISSION_REQUEST, permReq);
13461
13629
  this.emitAfterResolve(mw, permReq.id, autoDecision, "system", startTime);
13462
13630
  return autoDecision;
13463
13631
  }
13464
13632
  const promise = this.session.permissionGate.setPending(permReq);
13465
- this.session.emit("permission_request", permReq);
13633
+ this.session.emit(SessionEv.PERMISSION_REQUEST, permReq);
13466
13634
  await this.adapter.sendPermissionRequest(this.session.id, permReq);
13467
13635
  const optionId = await promise;
13468
- this.deps.eventBus?.emit("permission:resolved", {
13636
+ this.deps.eventBus?.emit(BusEvent.PERMISSION_RESOLVED, {
13469
13637
  sessionId: this.session.id,
13470
13638
  requestId: permReq.id,
13471
13639
  decision: optionId,
@@ -13497,7 +13665,7 @@ var SessionBridge = class {
13497
13665
  /** Emit permission:afterResolve middleware hook (fire-and-forget) */
13498
13666
  emitAfterResolve(mw, requestId, decision, userId, startTime) {
13499
13667
  if (mw) {
13500
- mw.execute("permission:afterResolve", {
13668
+ mw.execute(Hook.PERMISSION_AFTER_RESOLVE, {
13501
13669
  sessionId: this.session.id,
13502
13670
  requestId,
13503
13671
  decision,
@@ -13511,6 +13679,7 @@ var SessionBridge = class {
13511
13679
 
13512
13680
  // src/core/sessions/session-factory.ts
13513
13681
  init_log();
13682
+ init_events();
13514
13683
  var log7 = createChildLogger({ module: "session-factory" });
13515
13684
  var SessionFactory = class {
13516
13685
  constructor(agentManager, sessionManager, speechServiceAccessor, eventBus, instanceRoot) {
@@ -13553,7 +13722,7 @@ var SessionFactory = class {
13553
13722
  threadId: ""
13554
13723
  // threadId is assigned after session creation
13555
13724
  };
13556
- const result = await this.middlewareChain.execute("session:beforeCreate", payload, async (p) => p);
13725
+ const result = await this.middlewareChain.execute(Hook.SESSION_BEFORE_CREATE, payload, async (p) => p);
13557
13726
  if (!result) throw new Error("Session creation blocked by middleware");
13558
13727
  createParams = {
13559
13728
  ...params,
@@ -13562,6 +13731,7 @@ var SessionFactory = class {
13562
13731
  channelId: result.channelId
13563
13732
  };
13564
13733
  }
13734
+ const configAllowedPaths = this.configManager?.get().workspace?.security?.allowedPaths ?? [];
13565
13735
  let agentInstance;
13566
13736
  try {
13567
13737
  if (createParams.resumeAgentSessionId) {
@@ -13569,7 +13739,8 @@ var SessionFactory = class {
13569
13739
  agentInstance = await this.agentManager.resume(
13570
13740
  createParams.agentName,
13571
13741
  createParams.workingDirectory,
13572
- createParams.resumeAgentSessionId
13742
+ createParams.resumeAgentSessionId,
13743
+ configAllowedPaths
13573
13744
  );
13574
13745
  } catch (resumeErr) {
13575
13746
  log7.warn(
@@ -13578,13 +13749,15 @@ var SessionFactory = class {
13578
13749
  );
13579
13750
  agentInstance = await this.agentManager.spawn(
13580
13751
  createParams.agentName,
13581
- createParams.workingDirectory
13752
+ createParams.workingDirectory,
13753
+ configAllowedPaths
13582
13754
  );
13583
13755
  }
13584
13756
  } else {
13585
13757
  agentInstance = await this.agentManager.spawn(
13586
13758
  createParams.agentName,
13587
- createParams.workingDirectory
13759
+ createParams.workingDirectory,
13760
+ configAllowedPaths
13588
13761
  );
13589
13762
  }
13590
13763
  } catch (err) {
@@ -13616,7 +13789,7 @@ var SessionFactory = class {
13616
13789
  message: guidanceLines.join("\n")
13617
13790
  };
13618
13791
  const failedSessionId = createParams.existingSessionId ?? `failed-${Date.now()}`;
13619
- this.eventBus.emit("agent:event", {
13792
+ this.eventBus.emit(BusEvent.AGENT_EVENT, {
13620
13793
  sessionId: failedSessionId,
13621
13794
  event: guidance
13622
13795
  });
@@ -13642,11 +13815,13 @@ var SessionFactory = class {
13642
13815
  }
13643
13816
  session.applySpawnResponse(agentInstance.initialSessionResponse, agentInstance.agentCapabilities);
13644
13817
  this.sessionManager.registerSession(session);
13645
- this.eventBus.emit("session:created", {
13646
- sessionId: session.id,
13647
- agent: session.agentName,
13648
- status: session.status
13649
- });
13818
+ if (!session.isAssistant) {
13819
+ this.eventBus.emit(BusEvent.SESSION_CREATED, {
13820
+ sessionId: session.id,
13821
+ agent: session.agentName,
13822
+ status: session.status
13823
+ });
13824
+ }
13650
13825
  return session;
13651
13826
  }
13652
13827
  /**
@@ -13664,6 +13839,7 @@ var SessionFactory = class {
13664
13839
  if (!this.sessionStore || !this.createFullSession) return null;
13665
13840
  const record = this.sessionStore.get(sessionId);
13666
13841
  if (!record) return null;
13842
+ if (record.isAssistant) return null;
13667
13843
  if (record.status === "error" || record.status === "cancelled") return null;
13668
13844
  const existing = this.resumeLocks.get(sessionId);
13669
13845
  if (existing) return existing;
@@ -13731,6 +13907,7 @@ var SessionFactory = class {
13731
13907
  log7.debug({ threadId, channelId }, "No session record found for thread");
13732
13908
  return null;
13733
13909
  }
13910
+ if (record.isAssistant) return null;
13734
13911
  if (record.status === "error" || record.status === "cancelled") {
13735
13912
  log7.warn(
13736
13913
  { threadId, sessionId: record.sessionId, status: record.status },
@@ -13890,9 +14067,9 @@ var SessionFactory = class {
13890
14067
  return { session, contextResult };
13891
14068
  }
13892
14069
  wireSideEffects(session, deps) {
13893
- session.on("agent_event", (event) => {
14070
+ session.on(SessionEv.AGENT_EVENT, (event) => {
13894
14071
  if (event.type !== "usage") return;
13895
- deps.eventBus.emit("usage:recorded", {
14072
+ deps.eventBus.emit(BusEvent.USAGE_RECORDED, {
13896
14073
  sessionId: session.id,
13897
14074
  agentName: session.agentName,
13898
14075
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -13901,7 +14078,7 @@ var SessionFactory = class {
13901
14078
  cost: event.cost
13902
14079
  });
13903
14080
  });
13904
- session.on("status_change", (_from, to) => {
14081
+ session.on(SessionEv.STATUS_CHANGE, (_from, to) => {
13905
14082
  if ((to === "finished" || to === "cancelled") && deps.tunnelService) {
13906
14083
  deps.tunnelService.stopBySession(session.id).then((stopped) => {
13907
14084
  for (const entry of stopped) {
@@ -13981,6 +14158,14 @@ var JsonFileSessionStore = class {
13981
14158
  }
13982
14159
  return void 0;
13983
14160
  }
14161
+ findAssistant(channelId) {
14162
+ for (const record of this.records.values()) {
14163
+ if (record.isAssistant === true && record.channelId === channelId) {
14164
+ return record;
14165
+ }
14166
+ }
14167
+ return void 0;
14168
+ }
13984
14169
  list(channelId) {
13985
14170
  const all = [...this.records.values()];
13986
14171
  if (channelId) return all.filter((r) => r.channelId === channelId);
@@ -14060,6 +14245,8 @@ var JsonFileSessionStore = class {
14060
14245
  for (const [id, record] of this.records) {
14061
14246
  if (record.status === "active" || record.status === "initializing")
14062
14247
  continue;
14248
+ if (record.isAssistant === true)
14249
+ continue;
14063
14250
  const raw = record.lastActiveAt;
14064
14251
  if (!raw) continue;
14065
14252
  const lastActive = new Date(raw).getTime();
@@ -14088,6 +14275,7 @@ init_agent_registry();
14088
14275
  // src/core/agent-switch-handler.ts
14089
14276
  init_agent_registry();
14090
14277
  init_log();
14278
+ init_events();
14091
14279
  var log9 = createChildLogger({ module: "agent-switch" });
14092
14280
  var AgentSwitchHandler = class {
14093
14281
  constructor(deps) {
@@ -14113,7 +14301,7 @@ var AgentSwitchHandler = class {
14113
14301
  if (!agentDef) throw new Error(`Agent "${toAgent}" is not installed`);
14114
14302
  const fromAgent = session.agentName;
14115
14303
  const middlewareChain = this.deps.getMiddlewareChain();
14116
- const result = await middlewareChain?.execute("agent:beforeSwitch", {
14304
+ const result = await middlewareChain?.execute(Hook.AGENT_BEFORE_SWITCH, {
14117
14305
  sessionId,
14118
14306
  fromAgent,
14119
14307
  toAgent
@@ -14127,9 +14315,9 @@ var AgentSwitchHandler = class {
14127
14315
  type: "system_message",
14128
14316
  message: `Switching from ${fromAgent} to ${toAgent}...`
14129
14317
  };
14130
- session.emit("agent_event", startEvent);
14131
- eventBus.emit("agent:event", { sessionId, event: startEvent });
14132
- eventBus.emit("session:agentSwitch", {
14318
+ session.emit(SessionEv.AGENT_EVENT, startEvent);
14319
+ eventBus.emit(BusEvent.AGENT_EVENT, { sessionId, event: startEvent });
14320
+ eventBus.emit(BusEvent.SESSION_AGENT_SWITCH, {
14133
14321
  sessionId,
14134
14322
  fromAgent,
14135
14323
  toAgent,
@@ -14153,11 +14341,12 @@ var AgentSwitchHandler = class {
14153
14341
  }
14154
14342
  const fromAgentSessionId = session.agentSessionId;
14155
14343
  const fileService = this.deps.getService("file-service");
14344
+ const configAllowedPaths = configManager.get().workspace?.security?.allowedPaths ?? [];
14156
14345
  try {
14157
14346
  await session.switchAgent(toAgent, async () => {
14158
14347
  if (canResume) {
14159
14348
  try {
14160
- const instance2 = await agentManager.resume(toAgent, session.workingDirectory, lastEntry.agentSessionId);
14349
+ const instance2 = await agentManager.resume(toAgent, session.workingDirectory, lastEntry.agentSessionId, configAllowedPaths);
14161
14350
  if (fileService) instance2.addAllowedPath(fileService.baseDir);
14162
14351
  resumed = true;
14163
14352
  return instance2;
@@ -14165,7 +14354,7 @@ var AgentSwitchHandler = class {
14165
14354
  log9.warn({ sessionId, toAgent }, "Resume failed, falling back to new agent with context injection");
14166
14355
  }
14167
14356
  }
14168
- const instance = await agentManager.spawn(toAgent, session.workingDirectory);
14357
+ const instance = await agentManager.spawn(toAgent, session.workingDirectory, configAllowedPaths);
14169
14358
  if (fileService) instance.addAllowedPath(fileService.baseDir);
14170
14359
  try {
14171
14360
  const contextService = this.deps.getService("context");
@@ -14189,9 +14378,9 @@ var AgentSwitchHandler = class {
14189
14378
  type: "system_message",
14190
14379
  message: resumed ? `Switched to ${toAgent} (resumed previous session).` : `Switched to ${toAgent} (new session).`
14191
14380
  };
14192
- session.emit("agent_event", successEvent);
14193
- eventBus.emit("agent:event", { sessionId, event: successEvent });
14194
- eventBus.emit("session:agentSwitch", {
14381
+ session.emit(SessionEv.AGENT_EVENT, successEvent);
14382
+ eventBus.emit(BusEvent.AGENT_EVENT, { sessionId, event: successEvent });
14383
+ eventBus.emit(BusEvent.SESSION_AGENT_SWITCH, {
14195
14384
  sessionId,
14196
14385
  fromAgent,
14197
14386
  toAgent,
@@ -14204,9 +14393,9 @@ var AgentSwitchHandler = class {
14204
14393
  type: "system_message",
14205
14394
  message: `Failed to switch to ${toAgent}: ${errorMessage}`
14206
14395
  };
14207
- session.emit("agent_event", failedEvent);
14208
- eventBus.emit("agent:event", { sessionId, event: failedEvent });
14209
- eventBus.emit("session:agentSwitch", {
14396
+ session.emit(SessionEv.AGENT_EVENT, failedEvent);
14397
+ eventBus.emit(BusEvent.AGENT_EVENT, { sessionId, event: failedEvent });
14398
+ eventBus.emit(BusEvent.SESSION_AGENT_SWITCH, {
14210
14399
  sessionId,
14211
14400
  fromAgent,
14212
14401
  toAgent,
@@ -14255,7 +14444,7 @@ var AgentSwitchHandler = class {
14255
14444
  currentPromptCount: 0,
14256
14445
  agentSwitchHistory: session.agentSwitchHistory
14257
14446
  });
14258
- middlewareChain?.execute("agent:afterSwitch", {
14447
+ middlewareChain?.execute(Hook.AGENT_AFTER_SWITCH, {
14259
14448
  sessionId,
14260
14449
  fromAgent,
14261
14450
  toAgent,
@@ -14940,7 +15129,7 @@ function createPluginContext(opts) {
14940
15129
  }
14941
15130
  };
14942
15131
  const baseLog = opts.log ?? noopLog;
14943
- const log35 = typeof baseLog.child === "function" ? baseLog.child({ plugin: pluginName }) : baseLog;
15132
+ const log36 = typeof baseLog.child === "function" ? baseLog.child({ plugin: pluginName }) : baseLog;
14944
15133
  const storageImpl = new PluginStorageImpl(storagePath);
14945
15134
  const storage = {
14946
15135
  async get(key) {
@@ -14967,7 +15156,7 @@ function createPluginContext(opts) {
14967
15156
  const ctx = {
14968
15157
  pluginName,
14969
15158
  pluginConfig,
14970
- log: log35,
15159
+ log: log36,
14971
15160
  storage,
14972
15161
  on(event, handler) {
14973
15162
  requirePermission(permissions, "events:read", "on()");
@@ -15002,7 +15191,7 @@ function createPluginContext(opts) {
15002
15191
  const registry = serviceRegistry.get("command-registry");
15003
15192
  if (registry && typeof registry.register === "function") {
15004
15193
  registry.register(def, pluginName);
15005
- log35.debug(`Command '/${def.name}' registered`);
15194
+ log36.debug(`Command '/${def.name}' registered`);
15006
15195
  }
15007
15196
  },
15008
15197
  async sendMessage(_sessionId, _content) {
@@ -15045,7 +15234,7 @@ function createPluginContext(opts) {
15045
15234
  const registry = serviceRegistry.get("field-registry");
15046
15235
  if (registry && typeof registry.register === "function") {
15047
15236
  registry.register(pluginName, fields);
15048
- log35.debug(`Registered ${fields.length} editable field(s) for ${pluginName}`);
15237
+ log36.debug(`Registered ${fields.length} editable field(s) for ${pluginName}`);
15049
15238
  }
15050
15239
  },
15051
15240
  get sessions() {
@@ -15097,6 +15286,7 @@ function createPluginContext(opts) {
15097
15286
  }
15098
15287
 
15099
15288
  // src/core/plugin/lifecycle-manager.ts
15289
+ init_events();
15100
15290
  var SETUP_TIMEOUT_MS = 3e4;
15101
15291
  var TEARDOWN_TIMEOUT_MS = 1e4;
15102
15292
  function withTimeout(promise, ms, label) {
@@ -15242,7 +15432,7 @@ var LifecycleManager = class {
15242
15432
  }
15243
15433
  const registryEntry = this.pluginRegistry?.get(plugin.name);
15244
15434
  if (registryEntry && registryEntry.enabled === false) {
15245
- this.eventBus?.emit("plugin:disabled", { name: plugin.name });
15435
+ this.eventBus?.emit(BusEvent.PLUGIN_DISABLED, { name: plugin.name });
15246
15436
  continue;
15247
15437
  }
15248
15438
  if (registryEntry && plugin.migrate && registryEntry.version !== plugin.version && this.settingsManager) {
@@ -15285,7 +15475,7 @@ var LifecycleManager = class {
15285
15475
  if (!validation.valid) {
15286
15476
  this._failed.add(plugin.name);
15287
15477
  this.getPluginLogger(plugin.name).error(`Settings validation failed: ${validation.errors?.join("; ")}`);
15288
- this.eventBus?.emit("plugin:failed", { name: plugin.name, error: `Settings validation failed: ${validation.errors?.join("; ")}` });
15478
+ this.eventBus?.emit(BusEvent.PLUGIN_FAILED, { name: plugin.name, error: `Settings validation failed: ${validation.errors?.join("; ")}` });
15289
15479
  continue;
15290
15480
  }
15291
15481
  }
@@ -15308,13 +15498,13 @@ var LifecycleManager = class {
15308
15498
  await withTimeout(plugin.setup(ctx), SETUP_TIMEOUT_MS, `${plugin.name}.setup()`);
15309
15499
  this.contexts.set(plugin.name, ctx);
15310
15500
  this._loaded.add(plugin.name);
15311
- this.eventBus?.emit("plugin:loaded", { name: plugin.name, version: plugin.version });
15501
+ this.eventBus?.emit(BusEvent.PLUGIN_LOADED, { name: plugin.name, version: plugin.version });
15312
15502
  } catch (err) {
15313
15503
  this._failed.add(plugin.name);
15314
15504
  ctx.cleanup();
15315
15505
  console.error(`[lifecycle] Plugin ${plugin.name} setup() FAILED:`, err);
15316
15506
  this.getPluginLogger(plugin.name).error(`setup() failed: ${err}`);
15317
- this.eventBus?.emit("plugin:failed", { name: plugin.name, error: String(err) });
15507
+ this.eventBus?.emit(BusEvent.PLUGIN_FAILED, { name: plugin.name, error: String(err) });
15318
15508
  }
15319
15509
  }
15320
15510
  }
@@ -15335,7 +15525,7 @@ var LifecycleManager = class {
15335
15525
  this._loaded.delete(name);
15336
15526
  this._failed.delete(name);
15337
15527
  this.loadOrder = this.loadOrder.filter((p) => p.name !== name);
15338
- this.eventBus?.emit("plugin:unloaded", { name });
15528
+ this.eventBus?.emit(BusEvent.PLUGIN_UNLOADED, { name });
15339
15529
  }
15340
15530
  async shutdown() {
15341
15531
  const reversed = [...this.loadOrder].reverse();
@@ -15352,7 +15542,7 @@ var LifecycleManager = class {
15352
15542
  ctx.cleanup();
15353
15543
  this.contexts.delete(plugin.name);
15354
15544
  }
15355
- this.eventBus?.emit("plugin:unloaded", { name: plugin.name });
15545
+ this.eventBus?.emit(BusEvent.PLUGIN_UNLOADED, { name: plugin.name });
15356
15546
  }
15357
15547
  this._loaded.clear();
15358
15548
  this.loadOrder = [];
@@ -15500,21 +15690,25 @@ var AssistantManager = class {
15500
15690
  this.registry = registry;
15501
15691
  }
15502
15692
  sessions = /* @__PURE__ */ new Map();
15503
- respawning = /* @__PURE__ */ new Set();
15504
15693
  pendingSystemPrompts = /* @__PURE__ */ new Map();
15505
- async spawn(channelId, threadId) {
15694
+ async getOrSpawn(channelId, threadId) {
15695
+ const existing = this.core.sessionStore?.findAssistant(channelId);
15506
15696
  const session = await this.core.createSession({
15507
15697
  channelId,
15508
15698
  agentName: this.core.configManager.get().defaultAgent,
15509
15699
  workingDirectory: this.core.configManager.resolveWorkspace(),
15510
15700
  initialName: "Assistant",
15511
15701
  isAssistant: true,
15512
- threadId
15702
+ threadId,
15703
+ existingSessionId: existing?.sessionId
15513
15704
  });
15514
15705
  this.sessions.set(channelId, session);
15515
15706
  const systemPrompt = this.registry.buildSystemPrompt(channelId);
15516
15707
  this.pendingSystemPrompts.set(channelId, systemPrompt);
15517
- log15.info({ sessionId: session.id, channelId }, "Assistant spawned (system prompt deferred)");
15708
+ log15.info(
15709
+ { sessionId: session.id, channelId, reused: !!existing },
15710
+ existing ? "Assistant session reused (system prompt deferred)" : "Assistant spawned (system prompt deferred)"
15711
+ );
15518
15712
  return session;
15519
15713
  }
15520
15714
  get(channelId) {
@@ -15535,19 +15729,6 @@ var AssistantManager = class {
15535
15729
  }
15536
15730
  return false;
15537
15731
  }
15538
- async respawn(channelId, threadId) {
15539
- if (this.respawning.has(channelId)) {
15540
- return this.sessions.get(channelId);
15541
- }
15542
- this.respawning.add(channelId);
15543
- try {
15544
- const old = this.sessions.get(channelId);
15545
- if (old) await old.destroy();
15546
- return await this.spawn(channelId, threadId);
15547
- } finally {
15548
- this.respawning.delete(channelId);
15549
- }
15550
- }
15551
15732
  };
15552
15733
 
15553
15734
  // src/core/assistant/sections/sessions.ts
@@ -15717,6 +15898,7 @@ function registerCoreMenuItems(registry) {
15717
15898
 
15718
15899
  // src/core/core.ts
15719
15900
  init_log();
15901
+ init_events();
15720
15902
  var log16 = createChildLogger({ module: "core" });
15721
15903
  var OpenACPCore = class {
15722
15904
  configManager;
@@ -15951,7 +16133,7 @@ var OpenACPCore = class {
15951
16133
  );
15952
16134
  if (this.lifecycleManager?.middlewareChain) {
15953
16135
  const result = await this.lifecycleManager.middlewareChain.execute(
15954
- "message:incoming",
16136
+ Hook.MESSAGE_INCOMING,
15955
16137
  message,
15956
16138
  async (msg) => msg
15957
16139
  );
@@ -16003,9 +16185,10 @@ ${text3}`;
16003
16185
  }
16004
16186
  }
16005
16187
  const sourceAdapterId = message.routing?.sourceAdapterId ?? message.channelId;
16188
+ const routing = sourceAdapterId !== message.routing?.sourceAdapterId ? { ...message.routing, sourceAdapterId } : message.routing;
16006
16189
  if (sourceAdapterId && sourceAdapterId !== "sse" && sourceAdapterId !== "api") {
16007
16190
  const turnId = nanoid3(8);
16008
- this.eventBus.emit("message:queued", {
16191
+ this.eventBus.emit(BusEvent.MESSAGE_QUEUED, {
16009
16192
  sessionId: session.id,
16010
16193
  turnId,
16011
16194
  text: text3,
@@ -16014,9 +16197,9 @@ ${text3}`;
16014
16197
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
16015
16198
  queueDepth: session.queueDepth
16016
16199
  });
16017
- await session.enqueuePrompt(text3, message.attachments, message.routing, turnId);
16200
+ await session.enqueuePrompt(text3, message.attachments, routing, turnId);
16018
16201
  } else {
16019
- await session.enqueuePrompt(text3, message.attachments, message.routing);
16202
+ await session.enqueuePrompt(text3, message.attachments, routing);
16020
16203
  }
16021
16204
  }
16022
16205
  // --- Unified Session Creation Pipeline ---
@@ -16060,6 +16243,7 @@ ${text3}`;
16060
16243
  createdAt: session.createdAt.toISOString(),
16061
16244
  lastActiveAt: (/* @__PURE__ */ new Date()).toISOString(),
16062
16245
  name: session.name,
16246
+ isAssistant: params.isAssistant,
16063
16247
  platform: platform2,
16064
16248
  platforms,
16065
16249
  firstAgent: session.firstAgent,
@@ -16075,7 +16259,7 @@ ${text3}`;
16075
16259
  log16.warn({ err, sessionId: session.id }, "Failed to flush pending skill commands");
16076
16260
  });
16077
16261
  if (params.createThread && session.threadId) {
16078
- this.eventBus.emit("session:threadReady", {
16262
+ this.eventBus.emit(BusEvent.SESSION_THREAD_READY, {
16079
16263
  sessionId: session.id,
16080
16264
  channelId: params.channelId,
16081
16265
  threadId: session.threadId
@@ -16099,6 +16283,36 @@ ${text3}`;
16099
16283
  );
16100
16284
  return allowOption.id;
16101
16285
  };
16286
+ session.on(SessionEv.NAMED, async (name) => {
16287
+ await this.sessionManager.patchRecord(session.id, { name });
16288
+ this.eventBus.emit(BusEvent.SESSION_UPDATED, { sessionId: session.id, name });
16289
+ });
16290
+ const mw = () => this.lifecycleManager?.middlewareChain;
16291
+ session.on(SessionEv.AGENT_EVENT, async (event) => {
16292
+ let processedEvent = event;
16293
+ const chain = mw();
16294
+ if (chain) {
16295
+ const result = await chain.execute(Hook.AGENT_BEFORE_EVENT, { sessionId: session.id, event }, async (e) => e);
16296
+ if (!result) return;
16297
+ processedEvent = result.event;
16298
+ }
16299
+ if (processedEvent.type === "session_end") {
16300
+ session.finish(processedEvent.reason);
16301
+ } else if (processedEvent.type === "error") {
16302
+ session.fail(processedEvent.message);
16303
+ }
16304
+ this.eventBus.emit(BusEvent.AGENT_EVENT, { sessionId: session.id, event: processedEvent });
16305
+ });
16306
+ session.on(SessionEv.STATUS_CHANGE, (_from, to) => {
16307
+ this.sessionManager.patchRecord(session.id, {
16308
+ status: to,
16309
+ lastActiveAt: (/* @__PURE__ */ new Date()).toISOString()
16310
+ });
16311
+ this.eventBus.emit(BusEvent.SESSION_UPDATED, { sessionId: session.id, status: to });
16312
+ });
16313
+ session.on(SessionEv.PROMPT_COUNT_CHANGED, (count) => {
16314
+ this.sessionManager.patchRecord(session.id, { currentPromptCount: count });
16315
+ });
16102
16316
  }
16103
16317
  this.sessionFactory.wireSideEffects(session, {
16104
16318
  eventBus: this.eventBus,