@openacp/cli 2026.406.1 → 2026.406.3

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";
@@ -8221,7 +8309,6 @@ var init_commands = __esm({
8221
8309
  { command: "menu", description: "Show menu" },
8222
8310
  { command: "integrate", description: "Manage agent integrations" },
8223
8311
  { command: "handoff", description: "Continue this session in your terminal" },
8224
- { command: "clear", description: "Clear assistant history" },
8225
8312
  { command: "restart", description: "Restart OpenACP" },
8226
8313
  { command: "update", description: "Update to latest version and restart" },
8227
8314
  { command: "doctor", description: "Run system diagnostics" },
@@ -9312,6 +9399,7 @@ var log35, TelegramAdapter;
9312
9399
  var init_adapter = __esm({
9313
9400
  "src/plugins/telegram/adapter.ts"() {
9314
9401
  "use strict";
9402
+ init_events();
9315
9403
  init_log();
9316
9404
  init_topics();
9317
9405
  init_commands();
@@ -9781,7 +9869,7 @@ ${p}` : p;
9781
9869
  log35.warn({ err, sessionId }, "Failed to send initial messages for new session");
9782
9870
  });
9783
9871
  };
9784
- this.core.eventBus.on("session:threadReady", this._threadReadyHandler);
9872
+ this.core.eventBus.on(BusEvent.SESSION_THREAD_READY, this._threadReadyHandler);
9785
9873
  this._configChangedHandler = ({ sessionId }) => {
9786
9874
  this.updateControlMessage(sessionId).catch(() => {
9787
9875
  });
@@ -9814,7 +9902,7 @@ ${p}` : p;
9814
9902
  log35.warn({ err }, "Failed to send welcome message");
9815
9903
  }
9816
9904
  try {
9817
- await this.core.assistantManager.spawn("telegram", String(this.assistantTopicId));
9905
+ await this.core.assistantManager.getOrSpawn("telegram", String(this.assistantTopicId));
9818
9906
  } catch (err) {
9819
9907
  log35.error({ err }, "Failed to spawn assistant");
9820
9908
  }
@@ -9873,7 +9961,7 @@ OpenACP will automatically retry until this is resolved.`;
9873
9961
  }
9874
9962
  this.sessionTrackers.clear();
9875
9963
  if (this._threadReadyHandler) {
9876
- this.core.eventBus.off("session:threadReady", this._threadReadyHandler);
9964
+ this.core.eventBus.off(BusEvent.SESSION_THREAD_READY, this._threadReadyHandler);
9877
9965
  this._threadReadyHandler = void 0;
9878
9966
  }
9879
9967
  if (this._configChangedHandler) {
@@ -10943,11 +11031,49 @@ var TypedEmitter = class _TypedEmitter {
10943
11031
 
10944
11032
  // src/core/agents/agent-instance.ts
10945
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
10946
11071
  import { PROTOCOL_VERSION } from "@agentclientprotocol/sdk";
10947
11072
 
10948
11073
  // src/core/sessions/terminal-manager.ts
10949
11074
  import { spawn } from "child_process";
10950
11075
  import { randomUUID } from "crypto";
11076
+ init_events();
10951
11077
  var TerminalManager = class {
10952
11078
  terminals = /* @__PURE__ */ new Map();
10953
11079
  maxOutputBytes;
@@ -10964,7 +11090,7 @@ var TerminalManager = class {
10964
11090
  for (const ev of termEnvArr) {
10965
11091
  envRecord[ev.name] = ev.value;
10966
11092
  }
10967
- const result = await middlewareChain.execute("terminal:beforeCreate", {
11093
+ const result = await middlewareChain.execute(Hook.TERMINAL_BEFORE_CREATE, {
10968
11094
  sessionId,
10969
11095
  command: termCommand,
10970
11096
  args: termArgs,
@@ -11018,7 +11144,7 @@ var TerminalManager = class {
11018
11144
  childProcess.on("exit", (code, signal) => {
11019
11145
  state.exitStatus = { exitCode: code, signal };
11020
11146
  if (middlewareChain) {
11021
- middlewareChain.execute("terminal:afterExit", {
11147
+ middlewareChain.execute(Hook.TERMINAL_AFTER_EXIT, {
11022
11148
  sessionId,
11023
11149
  terminalId,
11024
11150
  command: state.command,
@@ -11148,6 +11274,7 @@ function createDebugTracer(sessionId, workingDirectory) {
11148
11274
 
11149
11275
  // src/core/agents/agent-instance.ts
11150
11276
  init_log();
11277
+ init_events();
11151
11278
  var log4 = createChildLogger({ module: "agent-instance" });
11152
11279
  function findPackageRoot(startDir) {
11153
11280
  let dir = startDir;
@@ -11230,7 +11357,7 @@ var AgentInstance = class _AgentInstance extends TypedEmitter {
11230
11357
  super();
11231
11358
  this.agentName = agentName;
11232
11359
  }
11233
- static async spawnSubprocess(agentDef, workingDirectory) {
11360
+ static async spawnSubprocess(agentDef, workingDirectory, allowedPaths = []) {
11234
11361
  const instance = new _AgentInstance(agentDef.name);
11235
11362
  const resolved = resolveAgentCommand(agentDef.command);
11236
11363
  log4.debug(
@@ -11244,10 +11371,7 @@ var AgentInstance = class _AgentInstance extends TypedEmitter {
11244
11371
  const ignorePatterns = PathGuard.loadIgnoreFile(workingDirectory);
11245
11372
  instance.pathGuard = new PathGuard({
11246
11373
  cwd: workingDirectory,
11247
- // allowedPaths is wired from workspace.security.allowedPaths config;
11248
- // spawnSubprocess would need to receive a SecurityConfig param to use it.
11249
- // Tracked as follow-up: pass workspace security config through spawn/resume call chain.
11250
- allowedPaths: [],
11374
+ allowedPaths,
11251
11375
  ignorePatterns
11252
11376
  });
11253
11377
  instance.child = spawn2(
@@ -11341,7 +11465,7 @@ var AgentInstance = class _AgentInstance extends TypedEmitter {
11341
11465
  if (signal === "SIGINT" || signal === "SIGTERM") return;
11342
11466
  if (code !== 0 && code !== null || signal) {
11343
11467
  const stderr = this.stderrCapture.getLastLines();
11344
- this.emit("agent_event", {
11468
+ this.emit(SessionEv.AGENT_EVENT, {
11345
11469
  type: "error",
11346
11470
  message: signal ? `Agent killed by signal ${signal}
11347
11471
  ${stderr}` : `Agent crashed (exit code ${code})
@@ -11353,7 +11477,7 @@ ${stderr}`
11353
11477
  log4.debug({ sessionId: this.sessionId }, "ACP connection closed");
11354
11478
  });
11355
11479
  }
11356
- static async spawn(agentDef, workingDirectory, mcpServers) {
11480
+ static async spawn(agentDef, workingDirectory, mcpServers, allowedPaths) {
11357
11481
  log4.debug(
11358
11482
  { agentName: agentDef.name, command: agentDef.command },
11359
11483
  "Spawning agent"
@@ -11361,7 +11485,8 @@ ${stderr}`
11361
11485
  const spawnStart = Date.now();
11362
11486
  const instance = await _AgentInstance.spawnSubprocess(
11363
11487
  agentDef,
11364
- workingDirectory
11488
+ workingDirectory,
11489
+ allowedPaths
11365
11490
  );
11366
11491
  const resolvedMcp = _AgentInstance.mcpManager.resolve(mcpServers);
11367
11492
  const response = await instance.connection.newSession({
@@ -11384,12 +11509,13 @@ ${stderr}`
11384
11509
  );
11385
11510
  return instance;
11386
11511
  }
11387
- static async resume(agentDef, workingDirectory, agentSessionId, mcpServers) {
11512
+ static async resume(agentDef, workingDirectory, agentSessionId, mcpServers, allowedPaths) {
11388
11513
  log4.debug({ agentName: agentDef.name, agentSessionId }, "Resuming agent");
11389
11514
  const spawnStart = Date.now();
11390
11515
  const instance = await _AgentInstance.spawnSubprocess(
11391
11516
  agentDef,
11392
- workingDirectory
11517
+ workingDirectory,
11518
+ allowedPaths
11393
11519
  );
11394
11520
  const resolvedMcp = _AgentInstance.mcpManager.resolve(mcpServers);
11395
11521
  try {
@@ -11577,7 +11703,7 @@ ${stderr}`
11577
11703
  return;
11578
11704
  }
11579
11705
  if (event !== null) {
11580
- self.emit("agent_event", event);
11706
+ self.emit(SessionEv.AGENT_EVENT, event);
11581
11707
  }
11582
11708
  },
11583
11709
  // ── Permission requests ──────────────────────────────────────────────
@@ -11604,7 +11730,7 @@ ${stderr}`
11604
11730
  throw new Error(`[Access denied] ${pathCheck.reason}`);
11605
11731
  }
11606
11732
  if (self.middlewareChain) {
11607
- 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);
11608
11734
  if (!result) return { content: "" };
11609
11735
  p.path = result.path;
11610
11736
  }
@@ -11622,7 +11748,7 @@ ${stderr}`
11622
11748
  throw new Error(`[Access denied] ${pathCheck.reason}`);
11623
11749
  }
11624
11750
  if (self.middlewareChain) {
11625
- 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);
11626
11752
  if (!result) return {};
11627
11753
  writePath = result.path;
11628
11754
  writeContent = result.content;
@@ -11719,10 +11845,16 @@ ${stderr}`
11719
11845
  // ── Prompt & lifecycle ──────────────────────────────────────────────
11720
11846
  async prompt(text3, attachments) {
11721
11847
  const contentBlocks = [{ type: "text", text: text3 }];
11722
- const SUPPORTED_IMAGE_MIMES = /* @__PURE__ */ new Set(["image/jpeg", "image/png", "image/gif", "image/webp"]);
11848
+ const capabilities = this.promptCapabilities ?? {};
11723
11849
  for (const att of attachments ?? []) {
11724
- const tooLarge = att.size > 10 * 1024 * 1024;
11725
- 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)) {
11726
11858
  const attCheck = this.pathGuard.validatePath(att.filePath, "read");
11727
11859
  if (!attCheck.allowed) {
11728
11860
  contentBlocks[0].text += `
@@ -11732,7 +11864,7 @@ ${stderr}`
11732
11864
  }
11733
11865
  const data = await fs8.promises.readFile(att.filePath);
11734
11866
  contentBlocks.push({ type: "image", data: data.toString("base64"), mimeType: att.mimeType });
11735
- } else if (att.type === "audio" && this.promptCapabilities?.audio && !tooLarge) {
11867
+ } else if (att.type === "audio" && capabilities.audio) {
11736
11868
  const attCheck = this.pathGuard.validatePath(att.filePath, "read");
11737
11869
  if (!attCheck.allowed) {
11738
11870
  contentBlocks[0].text += `
@@ -11743,9 +11875,9 @@ ${stderr}`
11743
11875
  const data = await fs8.promises.readFile(att.filePath);
11744
11876
  contentBlocks.push({ type: "audio", data: data.toString("base64"), mimeType: att.mimeType });
11745
11877
  } else {
11746
- if ((att.type === "image" || att.type === "audio") && !tooLarge) {
11878
+ if (att.type === "image" || att.type === "audio") {
11747
11879
  log4.debug(
11748
- { type: att.type, capabilities: this.promptCapabilities ?? {} },
11880
+ { type: att.type, capabilities },
11749
11881
  "Agent does not support %s content, falling back to file path",
11750
11882
  att.type
11751
11883
  );
@@ -11801,15 +11933,15 @@ var AgentManager = class {
11801
11933
  getAgent(name) {
11802
11934
  return this.catalog.resolve(name);
11803
11935
  }
11804
- async spawn(agentName, workingDirectory) {
11936
+ async spawn(agentName, workingDirectory, allowedPaths) {
11805
11937
  const agentDef = this.getAgent(agentName);
11806
11938
  if (!agentDef) throw new Error(`Agent "${agentName}" is not installed. Run "openacp agents install ${agentName}" to add it.`);
11807
- return AgentInstance.spawn(agentDef, workingDirectory);
11939
+ return AgentInstance.spawn(agentDef, workingDirectory, void 0, allowedPaths);
11808
11940
  }
11809
- async resume(agentName, workingDirectory, agentSessionId) {
11941
+ async resume(agentName, workingDirectory, agentSessionId, allowedPaths) {
11810
11942
  const agentDef = this.getAgent(agentName);
11811
11943
  if (!agentDef) throw new Error(`Agent "${agentName}" is not installed. Run "openacp agents install ${agentName}" to add it.`);
11812
- return AgentInstance.resume(agentDef, workingDirectory, agentSessionId);
11944
+ return AgentInstance.resume(agentDef, workingDirectory, agentSessionId, void 0, allowedPaths);
11813
11945
  }
11814
11946
  };
11815
11947
 
@@ -11982,6 +12114,7 @@ function isSystemEvent(event) {
11982
12114
  }
11983
12115
 
11984
12116
  // src/core/sessions/session.ts
12117
+ init_events();
11985
12118
  var moduleLog = createChildLogger({ module: "session" });
11986
12119
  var TTS_PROMPT_INSTRUCTION = `
11987
12120
 
@@ -12010,7 +12143,15 @@ var Session = class extends TypedEmitter {
12010
12143
  }
12011
12144
  agentName;
12012
12145
  workingDirectory;
12013
- 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
+ }
12014
12155
  agentSessionId = "";
12015
12156
  _status = "initializing";
12016
12157
  name;
@@ -12034,9 +12175,6 @@ var Session = class extends TypedEmitter {
12034
12175
  threadIds = /* @__PURE__ */ new Map();
12035
12176
  /** Active turn context — sealed on prompt dequeue, cleared on turn end */
12036
12177
  activeTurnContext = null;
12037
- /** The agentInstance for which the agent→session event relay is wired (prevents duplicate relays from multiple bridges).
12038
- * When the agent is swapped, the relay must be re-wired to the new instance. */
12039
- agentRelaySource = null;
12040
12178
  permissionGate = new PermissionGate();
12041
12179
  queue;
12042
12180
  speechService;
@@ -12060,23 +12198,37 @@ var Session = class extends TypedEmitter {
12060
12198
  this.log.error({ err }, "Prompt execution failed");
12061
12199
  const message = err instanceof Error ? err.message : String(err);
12062
12200
  this.fail(message);
12063
- this.emit("agent_event", { type: "error", message: `Prompt execution failed: ${message}` });
12201
+ this.emit(SessionEv.AGENT_EVENT, { type: "error", message: `Prompt execution failed: ${message}` });
12064
12202
  }
12065
12203
  );
12066
- 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);
12067
12218
  }
12068
12219
  /** Wire a listener on the current agentInstance to buffer commands_update events.
12069
12220
  * Must be called after every agentInstance replacement (constructor + switchAgent). */
12070
12221
  commandsBufferCleanup;
12071
12222
  wireCommandsBuffer() {
12072
12223
  this.commandsBufferCleanup?.();
12224
+ const instance = this._agentInstance;
12073
12225
  const handler = (event) => {
12074
12226
  if (event.type === "commands_update") {
12075
12227
  this.latestCommands = event.commands;
12076
12228
  }
12077
12229
  };
12078
- this.agentInstance.on("agent_event", handler);
12079
- this.commandsBufferCleanup = () => this.agentInstance.off("agent_event", handler);
12230
+ instance.on(SessionEv.AGENT_EVENT, handler);
12231
+ this.commandsBufferCleanup = () => instance.off(SessionEv.AGENT_EVENT, handler);
12080
12232
  }
12081
12233
  // --- State Machine ---
12082
12234
  get status() {
@@ -12090,12 +12242,12 @@ var Session = class extends TypedEmitter {
12090
12242
  fail(reason) {
12091
12243
  if (this._status === "error") return;
12092
12244
  this.transition("error");
12093
- this.emit("error", new Error(reason));
12245
+ this.emit(SessionEv.ERROR, new Error(reason));
12094
12246
  }
12095
12247
  /** Transition to finished — from active only. Emits session_end for backward compat. */
12096
12248
  finish(reason) {
12097
12249
  this.transition("finished");
12098
- this.emit("session_end", reason ?? "completed");
12250
+ this.emit(SessionEv.SESSION_END, reason ?? "completed");
12099
12251
  }
12100
12252
  /** Transition to cancelled — from active only (terminal session cancel) */
12101
12253
  markCancelled() {
@@ -12111,7 +12263,7 @@ var Session = class extends TypedEmitter {
12111
12263
  }
12112
12264
  this._status = to;
12113
12265
  this.log.debug({ from, to }, "Session status transition");
12114
- this.emit("status_change", from, to);
12266
+ this.emit(SessionEv.STATUS_CHANGE, from, to);
12115
12267
  }
12116
12268
  /** Number of prompts waiting in queue */
12117
12269
  get queueDepth() {
@@ -12133,8 +12285,8 @@ var Session = class extends TypedEmitter {
12133
12285
  async enqueuePrompt(text3, attachments, routing, externalTurnId) {
12134
12286
  const turnId = externalTurnId ?? nanoid2(8);
12135
12287
  if (this.middlewareChain) {
12136
- const payload = { text: text3, attachments, sessionId: this.id };
12137
- 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);
12138
12290
  if (!result) return turnId;
12139
12291
  text3 = result.text;
12140
12292
  attachments = result.attachments;
@@ -12149,9 +12301,9 @@ var Session = class extends TypedEmitter {
12149
12301
  routing?.responseAdapterId,
12150
12302
  turnId
12151
12303
  );
12152
- this.emit("turn_started", this.activeTurnContext);
12304
+ this.emit(SessionEv.TURN_STARTED, this.activeTurnContext);
12153
12305
  this.promptCount++;
12154
- this.emit("prompt_count_changed", this.promptCount);
12306
+ this.emit(SessionEv.PROMPT_COUNT_CHANGED, this.promptCount);
12155
12307
  if (this._status === "initializing" || this._status === "cancelled" || this._status === "error") {
12156
12308
  this.activate();
12157
12309
  }
@@ -12180,10 +12332,18 @@ ${text3}`;
12180
12332
  }
12181
12333
  } : null;
12182
12334
  if (accumulatorListener) {
12183
- 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);
12184
12344
  }
12185
12345
  if (this.middlewareChain) {
12186
- 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(() => {
12187
12347
  });
12188
12348
  }
12189
12349
  let stopReason = "end_turn";
@@ -12204,10 +12364,13 @@ ${text3}`;
12204
12364
  promptError = err;
12205
12365
  } finally {
12206
12366
  if (accumulatorListener) {
12207
- this.off("agent_event", accumulatorListener);
12367
+ this.off(SessionEv.AGENT_EVENT, accumulatorListener);
12368
+ }
12369
+ if (afterEventListener) {
12370
+ this.agentInstance.off(SessionEv.AGENT_EVENT, afterEventListener);
12208
12371
  }
12209
12372
  if (this.middlewareChain) {
12210
- 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(() => {
12211
12374
  });
12212
12375
  }
12213
12376
  this.activeTurnContext = null;
@@ -12252,7 +12415,7 @@ ${text3}`;
12252
12415
  const audioBuffer = await fs9.promises.readFile(audioPath);
12253
12416
  const result = await this.speechService.transcribe(audioBuffer, audioMime);
12254
12417
  this.log.info({ provider: "stt", duration: result.duration }, "Voice transcribed");
12255
- this.emit("agent_event", {
12418
+ this.emit(SessionEv.AGENT_EVENT, {
12256
12419
  type: "system_message",
12257
12420
  message: `\u{1F3A4} You said: ${result.text}`
12258
12421
  });
@@ -12261,7 +12424,7 @@ ${text3}`;
12261
12424
  ${result.text}` : result.text;
12262
12425
  } catch (err) {
12263
12426
  this.log.warn({ err }, "STT transcription failed, keeping audio attachment");
12264
- this.emit("agent_event", {
12427
+ this.emit(SessionEv.AGENT_EVENT, {
12265
12428
  type: "error",
12266
12429
  message: `Voice transcription failed: ${err.message}`
12267
12430
  });
@@ -12295,12 +12458,12 @@ ${result.text}` : result.text;
12295
12458
  timeoutPromise
12296
12459
  ]);
12297
12460
  const base64 = result.audioBuffer.toString("base64");
12298
- this.emit("agent_event", {
12461
+ this.emit(SessionEv.AGENT_EVENT, {
12299
12462
  type: "audio_content",
12300
12463
  data: base64,
12301
12464
  mimeType: result.mimeType
12302
12465
  });
12303
- this.emit("agent_event", { type: "tts_strip" });
12466
+ this.emit(SessionEv.AGENT_EVENT, { type: "tts_strip" });
12304
12467
  this.log.info("TTS synthesis completed");
12305
12468
  } finally {
12306
12469
  clearTimeout(ttsTimer);
@@ -12315,19 +12478,19 @@ ${result.text}` : result.text;
12315
12478
  const captureHandler = (event) => {
12316
12479
  if (event.type === "text") title += event.content;
12317
12480
  };
12318
- this.pause((event) => event !== "agent_event");
12319
- this.agentInstance.on("agent_event", captureHandler);
12481
+ this.pause((event) => event !== SessionEv.AGENT_EVENT);
12482
+ this.agentInstance.on(SessionEv.AGENT_EVENT, captureHandler);
12320
12483
  try {
12321
12484
  await this.agentInstance.prompt(
12322
12485
  "Summarize this conversation in max 5 words for a topic title. Reply ONLY with the title, nothing else."
12323
12486
  );
12324
12487
  this.name = title.trim().slice(0, 50) || `Session ${this.id.slice(0, 6)}`;
12325
12488
  this.log.info({ name: this.name }, "Session auto-named");
12326
- this.emit("named", this.name);
12489
+ this.emit(SessionEv.NAMED, this.name);
12327
12490
  } catch {
12328
12491
  this.name = `Session ${this.id.slice(0, 6)}`;
12329
12492
  } finally {
12330
- this.agentInstance.off("agent_event", captureHandler);
12493
+ this.agentInstance.off(SessionEv.AGENT_EVENT, captureHandler);
12331
12494
  this.clearBuffer();
12332
12495
  this.resume();
12333
12496
  }
@@ -12389,7 +12552,7 @@ ${result.text}` : result.text;
12389
12552
  /** Set session name explicitly and emit 'named' event */
12390
12553
  setName(name) {
12391
12554
  this.name = name;
12392
- this.emit("named", name);
12555
+ this.emit(SessionEv.NAMED, name);
12393
12556
  }
12394
12557
  /** Send a config option change to the agent and update local state from the response. */
12395
12558
  async setConfigOption(configId, value) {
@@ -12405,7 +12568,7 @@ ${result.text}` : result.text;
12405
12568
  }
12406
12569
  async updateConfigOptions(options) {
12407
12570
  if (this.middlewareChain) {
12408
- 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);
12409
12572
  if (!result) return;
12410
12573
  }
12411
12574
  this.configOptions = options;
@@ -12425,7 +12588,7 @@ ${result.text}` : result.text;
12425
12588
  /** Cancel the current prompt and clear the queue. Stays in active state. */
12426
12589
  async abortPrompt() {
12427
12590
  if (this.middlewareChain) {
12428
- 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);
12429
12592
  if (!result) return;
12430
12593
  }
12431
12594
  this.queue.clear();
@@ -12466,7 +12629,6 @@ ${result.text}` : result.text;
12466
12629
  this.configOptions = [];
12467
12630
  this.latestCommands = null;
12468
12631
  this.applySpawnResponse(newAgent.initialSessionResponse, newAgent.agentCapabilities);
12469
- this.wireCommandsBuffer();
12470
12632
  this.log.info({ from: this.agentSwitchHistory.at(-1).agentName, to: agentName }, "Agent switched");
12471
12633
  }
12472
12634
  async destroy() {
@@ -12905,6 +13067,7 @@ var MessageTransformer = class {
12905
13067
  };
12906
13068
 
12907
13069
  // src/core/sessions/session-manager.ts
13070
+ init_events();
12908
13071
  var SessionManager = class {
12909
13072
  sessions = /* @__PURE__ */ new Map();
12910
13073
  store;
@@ -13009,18 +13172,18 @@ var SessionManager = class {
13009
13172
  }
13010
13173
  }
13011
13174
  if (this.middlewareChain) {
13012
- this.middlewareChain.execute("session:afterDestroy", { sessionId }, async (p) => p).catch(() => {
13175
+ this.middlewareChain.execute(Hook.SESSION_AFTER_DESTROY, { sessionId }, async (p) => p).catch(() => {
13013
13176
  });
13014
13177
  }
13015
13178
  }
13016
13179
  listSessions(channelId) {
13017
- const all = Array.from(this.sessions.values());
13180
+ const all = Array.from(this.sessions.values()).filter((s) => !s.isAssistant);
13018
13181
  if (channelId) return all.filter((s) => s.channelId === channelId);
13019
13182
  return all;
13020
13183
  }
13021
13184
  listAllSessions(channelId) {
13022
13185
  if (this.store) {
13023
- let records = this.store.list();
13186
+ let records = this.store.list().filter((r) => !r.isAssistant);
13024
13187
  if (channelId) records = records.filter((r) => r.channelId === channelId);
13025
13188
  return records.map((record) => {
13026
13189
  const live2 = this.sessions.get(record.sessionId);
@@ -13060,7 +13223,7 @@ var SessionManager = class {
13060
13223
  };
13061
13224
  });
13062
13225
  }
13063
- let live = Array.from(this.sessions.values());
13226
+ let live = Array.from(this.sessions.values()).filter((s) => !s.isAssistant);
13064
13227
  if (channelId) live = live.filter((s) => s.channelId === channelId);
13065
13228
  return live.map((s) => ({
13066
13229
  id: s.id,
@@ -13081,7 +13244,7 @@ var SessionManager = class {
13081
13244
  }
13082
13245
  listRecords(filter) {
13083
13246
  if (!this.store) return [];
13084
- let records = this.store.list();
13247
+ let records = this.store.list().filter((r) => !r.isAssistant);
13085
13248
  if (filter?.statuses?.length) {
13086
13249
  records = records.filter((r) => filter.statuses.includes(r.status));
13087
13250
  }
@@ -13090,7 +13253,7 @@ var SessionManager = class {
13090
13253
  async removeRecord(sessionId) {
13091
13254
  if (!this.store) return;
13092
13255
  await this.store.remove(sessionId);
13093
- this.eventBus?.emit("session:deleted", { sessionId });
13256
+ this.eventBus?.emit(BusEvent.SESSION_DELETED, { sessionId });
13094
13257
  }
13095
13258
  /**
13096
13259
  * Graceful shutdown: persist session state without killing agent subprocesses.
@@ -13138,7 +13301,7 @@ var SessionManager = class {
13138
13301
  this.sessions.clear();
13139
13302
  if (this.middlewareChain) {
13140
13303
  for (const sessionId of sessionIds) {
13141
- this.middlewareChain.execute("session:afterDestroy", { sessionId }, async (p) => p).catch(() => {
13304
+ this.middlewareChain.execute(Hook.SESSION_AFTER_DESTROY, { sessionId }, async (p) => p).catch(() => {
13142
13305
  });
13143
13306
  }
13144
13307
  }
@@ -13148,6 +13311,7 @@ var SessionManager = class {
13148
13311
  // src/core/sessions/session-bridge.ts
13149
13312
  init_log();
13150
13313
  init_bypass_detection();
13314
+ init_events();
13151
13315
  var log6 = createChildLogger({ module: "session-bridge" });
13152
13316
  var SessionBridge = class {
13153
13317
  constructor(session, adapter, deps, adapterId) {
@@ -13172,7 +13336,7 @@ var SessionBridge = class {
13172
13336
  try {
13173
13337
  const mw = this.deps.middlewareChain;
13174
13338
  if (mw) {
13175
- 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);
13176
13340
  this.tracer?.log("core", { step: "middleware:outgoing", sessionId, hook: "message:outgoing", blocked: !result });
13177
13341
  if (!result) return;
13178
13342
  this.tracer?.log("core", { step: "dispatch", sessionId, message: result.message });
@@ -13201,17 +13365,11 @@ var SessionBridge = class {
13201
13365
  connect() {
13202
13366
  if (this.connected) return;
13203
13367
  this.connected = true;
13204
- if (this.session.agentRelaySource !== this.session.agentInstance) {
13205
- this.listen(this.session.agentInstance, "agent_event", (event) => {
13206
- this.session.emit("agent_event", event);
13207
- });
13208
- this.session.agentRelaySource = this.session.agentInstance;
13209
- }
13210
- this.listen(this.session, "agent_event", (event) => {
13368
+ this.listen(this.session, SessionEv.AGENT_EVENT, (event) => {
13211
13369
  if (this.shouldForward(event)) {
13212
13370
  this.dispatchAgentEvent(event);
13213
13371
  } else {
13214
- this.deps.eventBus?.emit("agent:event", { sessionId: this.session.id, event });
13372
+ this.deps.eventBus?.emit(BusEvent.AGENT_EVENT, { sessionId: this.session.id, event });
13215
13373
  }
13216
13374
  });
13217
13375
  if (!this.session.agentInstance.onPermissionRequest || this.session.agentInstance.onPermissionRequest.__bridgeId === void 0) {
@@ -13221,7 +13379,7 @@ var SessionBridge = class {
13221
13379
  handler.__bridgeId = this.adapterId;
13222
13380
  this.session.agentInstance.onPermissionRequest = handler;
13223
13381
  }
13224
- this.listen(this.session, "permission_request", async (request) => {
13382
+ this.listen(this.session, SessionEv.PERMISSION_REQUEST, async (request) => {
13225
13383
  const current = this.session.agentInstance.onPermissionRequest;
13226
13384
  if (current?.__bridgeId === this.adapterId) return;
13227
13385
  if (!this.session.permissionGate.isPending) return;
@@ -13231,37 +13389,41 @@ var SessionBridge = class {
13231
13389
  log6.error({ err, sessionId: this.session.id, adapterId: this.adapterId }, "Failed to send permission request to adapter");
13232
13390
  }
13233
13391
  });
13234
- this.listen(this.session, "status_change", (from, to) => {
13392
+ this.listen(this.session, SessionEv.STATUS_CHANGE, (from, to) => {
13235
13393
  this.deps.sessionManager.patchRecord(this.session.id, {
13236
13394
  status: to,
13237
13395
  lastActiveAt: (/* @__PURE__ */ new Date()).toISOString()
13238
13396
  });
13239
- this.deps.eventBus?.emit("session:updated", {
13240
- sessionId: this.session.id,
13241
- status: to
13242
- });
13397
+ if (!this.session.isAssistant) {
13398
+ this.deps.eventBus?.emit(BusEvent.SESSION_UPDATED, {
13399
+ sessionId: this.session.id,
13400
+ status: to
13401
+ });
13402
+ }
13243
13403
  if (to === "finished") {
13244
13404
  queueMicrotask(() => this.disconnect());
13245
13405
  }
13246
13406
  });
13247
- this.listen(this.session, "named", async (name) => {
13407
+ this.listen(this.session, SessionEv.NAMED, async (name) => {
13248
13408
  const record = this.deps.sessionManager.getSessionRecord(this.session.id);
13249
13409
  const alreadyNamed = !!record?.name;
13250
13410
  await this.deps.sessionManager.patchRecord(this.session.id, { name });
13251
- this.deps.eventBus?.emit("session:updated", {
13252
- sessionId: this.session.id,
13253
- name
13254
- });
13411
+ if (!this.session.isAssistant) {
13412
+ this.deps.eventBus?.emit(BusEvent.SESSION_UPDATED, {
13413
+ sessionId: this.session.id,
13414
+ name
13415
+ });
13416
+ }
13255
13417
  if (!alreadyNamed) {
13256
13418
  await this.adapter.renameSessionThread(this.session.id, name);
13257
13419
  }
13258
13420
  });
13259
- this.listen(this.session, "prompt_count_changed", (count) => {
13421
+ this.listen(this.session, SessionEv.PROMPT_COUNT_CHANGED, (count) => {
13260
13422
  this.deps.sessionManager.patchRecord(this.session.id, { currentPromptCount: count });
13261
13423
  });
13262
- this.listen(this.session, "turn_started", (ctx) => {
13424
+ this.listen(this.session, SessionEv.TURN_STARTED, (ctx) => {
13263
13425
  if (ctx.sourceAdapterId !== "sse" && ctx.sourceAdapterId !== "api") {
13264
- this.deps.eventBus?.emit("message:processing", {
13426
+ this.deps.eventBus?.emit(BusEvent.MESSAGE_PROCESSING, {
13265
13427
  sessionId: this.session.id,
13266
13428
  turnId: ctx.turnId,
13267
13429
  sourceAdapterId: ctx.sourceAdapterId,
@@ -13270,10 +13432,10 @@ var SessionBridge = class {
13270
13432
  }
13271
13433
  });
13272
13434
  if (this.session.latestCommands !== null) {
13273
- 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 });
13274
13436
  }
13275
13437
  if (this.session.configOptions.length > 0) {
13276
- 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 });
13277
13439
  }
13278
13440
  }
13279
13441
  disconnect() {
@@ -13293,17 +13455,11 @@ var SessionBridge = class {
13293
13455
  const mw = this.deps.middlewareChain;
13294
13456
  if (mw) {
13295
13457
  try {
13296
- 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);
13297
13459
  this.tracer?.log("core", { step: "middleware:before", sessionId: this.session.id, hook: "agent:beforeEvent", blocked: !result });
13298
13460
  if (!result) return;
13299
13461
  const transformedEvent = result.event;
13300
- const outgoing = this.handleAgentEvent(transformedEvent);
13301
- mw.execute("agent:afterEvent", {
13302
- sessionId: this.session.id,
13303
- event: transformedEvent,
13304
- outgoingMessage: outgoing ?? { type: "text", text: "" }
13305
- }, async (e) => e).catch(() => {
13306
- });
13462
+ this.handleAgentEvent(transformedEvent);
13307
13463
  } catch {
13308
13464
  try {
13309
13465
  this.handleAgentEvent(event);
@@ -13436,7 +13592,7 @@ var SessionBridge = class {
13436
13592
  this.adapter.stripTTSBlock?.(this.session.id);
13437
13593
  break;
13438
13594
  }
13439
- this.deps.eventBus?.emit("agent:event", {
13595
+ this.deps.eventBus?.emit(BusEvent.AGENT_EVENT, {
13440
13596
  sessionId: this.session.id,
13441
13597
  event
13442
13598
  });
@@ -13455,7 +13611,7 @@ var SessionBridge = class {
13455
13611
  let permReq = request;
13456
13612
  if (mw) {
13457
13613
  const payload = { sessionId: this.session.id, request, autoResolve: void 0 };
13458
- 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);
13459
13615
  if (!result) return "";
13460
13616
  permReq = result.request;
13461
13617
  if (result.autoResolve) {
@@ -13463,21 +13619,21 @@ var SessionBridge = class {
13463
13619
  return result.autoResolve;
13464
13620
  }
13465
13621
  }
13466
- this.deps.eventBus?.emit("permission:request", {
13622
+ this.deps.eventBus?.emit(BusEvent.PERMISSION_REQUEST, {
13467
13623
  sessionId: this.session.id,
13468
13624
  permission: permReq
13469
13625
  });
13470
13626
  const autoDecision = this.checkAutoApprove(permReq);
13471
13627
  if (autoDecision) {
13472
- this.session.emit("permission_request", permReq);
13628
+ this.session.emit(SessionEv.PERMISSION_REQUEST, permReq);
13473
13629
  this.emitAfterResolve(mw, permReq.id, autoDecision, "system", startTime);
13474
13630
  return autoDecision;
13475
13631
  }
13476
13632
  const promise = this.session.permissionGate.setPending(permReq);
13477
- this.session.emit("permission_request", permReq);
13633
+ this.session.emit(SessionEv.PERMISSION_REQUEST, permReq);
13478
13634
  await this.adapter.sendPermissionRequest(this.session.id, permReq);
13479
13635
  const optionId = await promise;
13480
- this.deps.eventBus?.emit("permission:resolved", {
13636
+ this.deps.eventBus?.emit(BusEvent.PERMISSION_RESOLVED, {
13481
13637
  sessionId: this.session.id,
13482
13638
  requestId: permReq.id,
13483
13639
  decision: optionId,
@@ -13509,7 +13665,7 @@ var SessionBridge = class {
13509
13665
  /** Emit permission:afterResolve middleware hook (fire-and-forget) */
13510
13666
  emitAfterResolve(mw, requestId, decision, userId, startTime) {
13511
13667
  if (mw) {
13512
- mw.execute("permission:afterResolve", {
13668
+ mw.execute(Hook.PERMISSION_AFTER_RESOLVE, {
13513
13669
  sessionId: this.session.id,
13514
13670
  requestId,
13515
13671
  decision,
@@ -13523,6 +13679,7 @@ var SessionBridge = class {
13523
13679
 
13524
13680
  // src/core/sessions/session-factory.ts
13525
13681
  init_log();
13682
+ init_events();
13526
13683
  var log7 = createChildLogger({ module: "session-factory" });
13527
13684
  var SessionFactory = class {
13528
13685
  constructor(agentManager, sessionManager, speechServiceAccessor, eventBus, instanceRoot) {
@@ -13565,7 +13722,7 @@ var SessionFactory = class {
13565
13722
  threadId: ""
13566
13723
  // threadId is assigned after session creation
13567
13724
  };
13568
- 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);
13569
13726
  if (!result) throw new Error("Session creation blocked by middleware");
13570
13727
  createParams = {
13571
13728
  ...params,
@@ -13574,6 +13731,7 @@ var SessionFactory = class {
13574
13731
  channelId: result.channelId
13575
13732
  };
13576
13733
  }
13734
+ const configAllowedPaths = this.configManager?.get().workspace?.security?.allowedPaths ?? [];
13577
13735
  let agentInstance;
13578
13736
  try {
13579
13737
  if (createParams.resumeAgentSessionId) {
@@ -13581,7 +13739,8 @@ var SessionFactory = class {
13581
13739
  agentInstance = await this.agentManager.resume(
13582
13740
  createParams.agentName,
13583
13741
  createParams.workingDirectory,
13584
- createParams.resumeAgentSessionId
13742
+ createParams.resumeAgentSessionId,
13743
+ configAllowedPaths
13585
13744
  );
13586
13745
  } catch (resumeErr) {
13587
13746
  log7.warn(
@@ -13590,13 +13749,15 @@ var SessionFactory = class {
13590
13749
  );
13591
13750
  agentInstance = await this.agentManager.spawn(
13592
13751
  createParams.agentName,
13593
- createParams.workingDirectory
13752
+ createParams.workingDirectory,
13753
+ configAllowedPaths
13594
13754
  );
13595
13755
  }
13596
13756
  } else {
13597
13757
  agentInstance = await this.agentManager.spawn(
13598
13758
  createParams.agentName,
13599
- createParams.workingDirectory
13759
+ createParams.workingDirectory,
13760
+ configAllowedPaths
13600
13761
  );
13601
13762
  }
13602
13763
  } catch (err) {
@@ -13628,7 +13789,7 @@ var SessionFactory = class {
13628
13789
  message: guidanceLines.join("\n")
13629
13790
  };
13630
13791
  const failedSessionId = createParams.existingSessionId ?? `failed-${Date.now()}`;
13631
- this.eventBus.emit("agent:event", {
13792
+ this.eventBus.emit(BusEvent.AGENT_EVENT, {
13632
13793
  sessionId: failedSessionId,
13633
13794
  event: guidance
13634
13795
  });
@@ -13654,11 +13815,13 @@ var SessionFactory = class {
13654
13815
  }
13655
13816
  session.applySpawnResponse(agentInstance.initialSessionResponse, agentInstance.agentCapabilities);
13656
13817
  this.sessionManager.registerSession(session);
13657
- this.eventBus.emit("session:created", {
13658
- sessionId: session.id,
13659
- agent: session.agentName,
13660
- status: session.status
13661
- });
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
+ }
13662
13825
  return session;
13663
13826
  }
13664
13827
  /**
@@ -13676,6 +13839,7 @@ var SessionFactory = class {
13676
13839
  if (!this.sessionStore || !this.createFullSession) return null;
13677
13840
  const record = this.sessionStore.get(sessionId);
13678
13841
  if (!record) return null;
13842
+ if (record.isAssistant) return null;
13679
13843
  if (record.status === "error" || record.status === "cancelled") return null;
13680
13844
  const existing = this.resumeLocks.get(sessionId);
13681
13845
  if (existing) return existing;
@@ -13743,6 +13907,7 @@ var SessionFactory = class {
13743
13907
  log7.debug({ threadId, channelId }, "No session record found for thread");
13744
13908
  return null;
13745
13909
  }
13910
+ if (record.isAssistant) return null;
13746
13911
  if (record.status === "error" || record.status === "cancelled") {
13747
13912
  log7.warn(
13748
13913
  { threadId, sessionId: record.sessionId, status: record.status },
@@ -13902,9 +14067,9 @@ var SessionFactory = class {
13902
14067
  return { session, contextResult };
13903
14068
  }
13904
14069
  wireSideEffects(session, deps) {
13905
- session.on("agent_event", (event) => {
14070
+ session.on(SessionEv.AGENT_EVENT, (event) => {
13906
14071
  if (event.type !== "usage") return;
13907
- deps.eventBus.emit("usage:recorded", {
14072
+ deps.eventBus.emit(BusEvent.USAGE_RECORDED, {
13908
14073
  sessionId: session.id,
13909
14074
  agentName: session.agentName,
13910
14075
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -13913,7 +14078,7 @@ var SessionFactory = class {
13913
14078
  cost: event.cost
13914
14079
  });
13915
14080
  });
13916
- session.on("status_change", (_from, to) => {
14081
+ session.on(SessionEv.STATUS_CHANGE, (_from, to) => {
13917
14082
  if ((to === "finished" || to === "cancelled") && deps.tunnelService) {
13918
14083
  deps.tunnelService.stopBySession(session.id).then((stopped) => {
13919
14084
  for (const entry of stopped) {
@@ -13993,6 +14158,14 @@ var JsonFileSessionStore = class {
13993
14158
  }
13994
14159
  return void 0;
13995
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
+ }
13996
14169
  list(channelId) {
13997
14170
  const all = [...this.records.values()];
13998
14171
  if (channelId) return all.filter((r) => r.channelId === channelId);
@@ -14072,6 +14245,8 @@ var JsonFileSessionStore = class {
14072
14245
  for (const [id, record] of this.records) {
14073
14246
  if (record.status === "active" || record.status === "initializing")
14074
14247
  continue;
14248
+ if (record.isAssistant === true)
14249
+ continue;
14075
14250
  const raw = record.lastActiveAt;
14076
14251
  if (!raw) continue;
14077
14252
  const lastActive = new Date(raw).getTime();
@@ -14100,6 +14275,7 @@ init_agent_registry();
14100
14275
  // src/core/agent-switch-handler.ts
14101
14276
  init_agent_registry();
14102
14277
  init_log();
14278
+ init_events();
14103
14279
  var log9 = createChildLogger({ module: "agent-switch" });
14104
14280
  var AgentSwitchHandler = class {
14105
14281
  constructor(deps) {
@@ -14125,7 +14301,7 @@ var AgentSwitchHandler = class {
14125
14301
  if (!agentDef) throw new Error(`Agent "${toAgent}" is not installed`);
14126
14302
  const fromAgent = session.agentName;
14127
14303
  const middlewareChain = this.deps.getMiddlewareChain();
14128
- const result = await middlewareChain?.execute("agent:beforeSwitch", {
14304
+ const result = await middlewareChain?.execute(Hook.AGENT_BEFORE_SWITCH, {
14129
14305
  sessionId,
14130
14306
  fromAgent,
14131
14307
  toAgent
@@ -14139,9 +14315,9 @@ var AgentSwitchHandler = class {
14139
14315
  type: "system_message",
14140
14316
  message: `Switching from ${fromAgent} to ${toAgent}...`
14141
14317
  };
14142
- session.emit("agent_event", startEvent);
14143
- eventBus.emit("agent:event", { sessionId, event: startEvent });
14144
- 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, {
14145
14321
  sessionId,
14146
14322
  fromAgent,
14147
14323
  toAgent,
@@ -14165,11 +14341,12 @@ var AgentSwitchHandler = class {
14165
14341
  }
14166
14342
  const fromAgentSessionId = session.agentSessionId;
14167
14343
  const fileService = this.deps.getService("file-service");
14344
+ const configAllowedPaths = configManager.get().workspace?.security?.allowedPaths ?? [];
14168
14345
  try {
14169
14346
  await session.switchAgent(toAgent, async () => {
14170
14347
  if (canResume) {
14171
14348
  try {
14172
- const instance2 = await agentManager.resume(toAgent, session.workingDirectory, lastEntry.agentSessionId);
14349
+ const instance2 = await agentManager.resume(toAgent, session.workingDirectory, lastEntry.agentSessionId, configAllowedPaths);
14173
14350
  if (fileService) instance2.addAllowedPath(fileService.baseDir);
14174
14351
  resumed = true;
14175
14352
  return instance2;
@@ -14177,7 +14354,7 @@ var AgentSwitchHandler = class {
14177
14354
  log9.warn({ sessionId, toAgent }, "Resume failed, falling back to new agent with context injection");
14178
14355
  }
14179
14356
  }
14180
- const instance = await agentManager.spawn(toAgent, session.workingDirectory);
14357
+ const instance = await agentManager.spawn(toAgent, session.workingDirectory, configAllowedPaths);
14181
14358
  if (fileService) instance.addAllowedPath(fileService.baseDir);
14182
14359
  try {
14183
14360
  const contextService = this.deps.getService("context");
@@ -14201,9 +14378,9 @@ var AgentSwitchHandler = class {
14201
14378
  type: "system_message",
14202
14379
  message: resumed ? `Switched to ${toAgent} (resumed previous session).` : `Switched to ${toAgent} (new session).`
14203
14380
  };
14204
- session.emit("agent_event", successEvent);
14205
- eventBus.emit("agent:event", { sessionId, event: successEvent });
14206
- 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, {
14207
14384
  sessionId,
14208
14385
  fromAgent,
14209
14386
  toAgent,
@@ -14216,9 +14393,9 @@ var AgentSwitchHandler = class {
14216
14393
  type: "system_message",
14217
14394
  message: `Failed to switch to ${toAgent}: ${errorMessage}`
14218
14395
  };
14219
- session.emit("agent_event", failedEvent);
14220
- eventBus.emit("agent:event", { sessionId, event: failedEvent });
14221
- 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, {
14222
14399
  sessionId,
14223
14400
  fromAgent,
14224
14401
  toAgent,
@@ -14267,7 +14444,7 @@ var AgentSwitchHandler = class {
14267
14444
  currentPromptCount: 0,
14268
14445
  agentSwitchHistory: session.agentSwitchHistory
14269
14446
  });
14270
- middlewareChain?.execute("agent:afterSwitch", {
14447
+ middlewareChain?.execute(Hook.AGENT_AFTER_SWITCH, {
14271
14448
  sessionId,
14272
14449
  fromAgent,
14273
14450
  toAgent,
@@ -15109,6 +15286,7 @@ function createPluginContext(opts) {
15109
15286
  }
15110
15287
 
15111
15288
  // src/core/plugin/lifecycle-manager.ts
15289
+ init_events();
15112
15290
  var SETUP_TIMEOUT_MS = 3e4;
15113
15291
  var TEARDOWN_TIMEOUT_MS = 1e4;
15114
15292
  function withTimeout(promise, ms, label) {
@@ -15254,7 +15432,7 @@ var LifecycleManager = class {
15254
15432
  }
15255
15433
  const registryEntry = this.pluginRegistry?.get(plugin.name);
15256
15434
  if (registryEntry && registryEntry.enabled === false) {
15257
- this.eventBus?.emit("plugin:disabled", { name: plugin.name });
15435
+ this.eventBus?.emit(BusEvent.PLUGIN_DISABLED, { name: plugin.name });
15258
15436
  continue;
15259
15437
  }
15260
15438
  if (registryEntry && plugin.migrate && registryEntry.version !== plugin.version && this.settingsManager) {
@@ -15297,7 +15475,7 @@ var LifecycleManager = class {
15297
15475
  if (!validation.valid) {
15298
15476
  this._failed.add(plugin.name);
15299
15477
  this.getPluginLogger(plugin.name).error(`Settings validation failed: ${validation.errors?.join("; ")}`);
15300
- 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("; ")}` });
15301
15479
  continue;
15302
15480
  }
15303
15481
  }
@@ -15320,13 +15498,13 @@ var LifecycleManager = class {
15320
15498
  await withTimeout(plugin.setup(ctx), SETUP_TIMEOUT_MS, `${plugin.name}.setup()`);
15321
15499
  this.contexts.set(plugin.name, ctx);
15322
15500
  this._loaded.add(plugin.name);
15323
- this.eventBus?.emit("plugin:loaded", { name: plugin.name, version: plugin.version });
15501
+ this.eventBus?.emit(BusEvent.PLUGIN_LOADED, { name: plugin.name, version: plugin.version });
15324
15502
  } catch (err) {
15325
15503
  this._failed.add(plugin.name);
15326
15504
  ctx.cleanup();
15327
15505
  console.error(`[lifecycle] Plugin ${plugin.name} setup() FAILED:`, err);
15328
15506
  this.getPluginLogger(plugin.name).error(`setup() failed: ${err}`);
15329
- this.eventBus?.emit("plugin:failed", { name: plugin.name, error: String(err) });
15507
+ this.eventBus?.emit(BusEvent.PLUGIN_FAILED, { name: plugin.name, error: String(err) });
15330
15508
  }
15331
15509
  }
15332
15510
  }
@@ -15347,7 +15525,7 @@ var LifecycleManager = class {
15347
15525
  this._loaded.delete(name);
15348
15526
  this._failed.delete(name);
15349
15527
  this.loadOrder = this.loadOrder.filter((p) => p.name !== name);
15350
- this.eventBus?.emit("plugin:unloaded", { name });
15528
+ this.eventBus?.emit(BusEvent.PLUGIN_UNLOADED, { name });
15351
15529
  }
15352
15530
  async shutdown() {
15353
15531
  const reversed = [...this.loadOrder].reverse();
@@ -15364,7 +15542,7 @@ var LifecycleManager = class {
15364
15542
  ctx.cleanup();
15365
15543
  this.contexts.delete(plugin.name);
15366
15544
  }
15367
- this.eventBus?.emit("plugin:unloaded", { name: plugin.name });
15545
+ this.eventBus?.emit(BusEvent.PLUGIN_UNLOADED, { name: plugin.name });
15368
15546
  }
15369
15547
  this._loaded.clear();
15370
15548
  this.loadOrder = [];
@@ -15512,21 +15690,25 @@ var AssistantManager = class {
15512
15690
  this.registry = registry;
15513
15691
  }
15514
15692
  sessions = /* @__PURE__ */ new Map();
15515
- respawning = /* @__PURE__ */ new Set();
15516
15693
  pendingSystemPrompts = /* @__PURE__ */ new Map();
15517
- async spawn(channelId, threadId) {
15694
+ async getOrSpawn(channelId, threadId) {
15695
+ const existing = this.core.sessionStore?.findAssistant(channelId);
15518
15696
  const session = await this.core.createSession({
15519
15697
  channelId,
15520
15698
  agentName: this.core.configManager.get().defaultAgent,
15521
15699
  workingDirectory: this.core.configManager.resolveWorkspace(),
15522
15700
  initialName: "Assistant",
15523
15701
  isAssistant: true,
15524
- threadId
15702
+ threadId,
15703
+ existingSessionId: existing?.sessionId
15525
15704
  });
15526
15705
  this.sessions.set(channelId, session);
15527
15706
  const systemPrompt = this.registry.buildSystemPrompt(channelId);
15528
15707
  this.pendingSystemPrompts.set(channelId, systemPrompt);
15529
- 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
+ );
15530
15712
  return session;
15531
15713
  }
15532
15714
  get(channelId) {
@@ -15547,19 +15729,6 @@ var AssistantManager = class {
15547
15729
  }
15548
15730
  return false;
15549
15731
  }
15550
- async respawn(channelId, threadId) {
15551
- if (this.respawning.has(channelId)) {
15552
- return this.sessions.get(channelId);
15553
- }
15554
- this.respawning.add(channelId);
15555
- try {
15556
- const old = this.sessions.get(channelId);
15557
- if (old) await old.destroy();
15558
- return await this.spawn(channelId, threadId);
15559
- } finally {
15560
- this.respawning.delete(channelId);
15561
- }
15562
- }
15563
15732
  };
15564
15733
 
15565
15734
  // src/core/assistant/sections/sessions.ts
@@ -15653,6 +15822,34 @@ function createSystemSection() {
15653
15822
  };
15654
15823
  }
15655
15824
 
15825
+ // src/core/assistant/sections/remote.ts
15826
+ function createRemoteSection() {
15827
+ return {
15828
+ id: "core:remote",
15829
+ title: "Remote Access",
15830
+ priority: 35,
15831
+ buildContext: () => {
15832
+ return `Generate a one-time remote access link so the user can connect to this OpenACP instance from the app or a browser.
15833
+
15834
+ The link contains a short-lived code (expires in 30 minutes, single-use) that exchanges for a long-lived token.
15835
+
15836
+ Roles:
15837
+ admin \u2014 full control (default)
15838
+ viewer \u2014 read-only access
15839
+
15840
+ The command automatically includes the tunnel URL if a tunnel is active. Without a tunnel, the local link only works on the same machine.
15841
+
15842
+ Always show both the link and QR code when available so the user can choose how to open it.`;
15843
+ },
15844
+ commands: [
15845
+ { command: "openacp remote", description: "Generate remote access link (admin role, 24h expiry)" },
15846
+ { command: "openacp remote --role viewer", description: "Generate read-only access link" },
15847
+ { command: "openacp remote --expire 48h", description: "Generate link with custom expiry" },
15848
+ { command: "openacp remote --no-qr", description: "Skip QR code output" }
15849
+ ]
15850
+ };
15851
+ }
15852
+
15656
15853
  // src/core/menu/core-items.ts
15657
15854
  function registerCoreMenuItems(registry) {
15658
15855
  registry.register({
@@ -15729,6 +15926,7 @@ function registerCoreMenuItems(registry) {
15729
15926
 
15730
15927
  // src/core/core.ts
15731
15928
  init_log();
15929
+ init_events();
15732
15930
  var log16 = createChildLogger({ module: "core" });
15733
15931
  var OpenACPCore = class {
15734
15932
  configManager;
@@ -15882,6 +16080,7 @@ var OpenACPCore = class {
15882
16080
  this.assistantRegistry.register(createAgentsSection(this));
15883
16081
  this.assistantRegistry.register(createConfigSection(this));
15884
16082
  this.assistantRegistry.register(createSystemSection());
16083
+ this.assistantRegistry.register(createRemoteSection());
15885
16084
  this.assistantManager = new AssistantManager(this, this.assistantRegistry);
15886
16085
  this.lifecycleManager.serviceRegistry.register("menu-registry", this.menuRegistry, "core");
15887
16086
  this.lifecycleManager.serviceRegistry.register("assistant-registry", this.assistantRegistry, "core");
@@ -15963,7 +16162,7 @@ var OpenACPCore = class {
15963
16162
  );
15964
16163
  if (this.lifecycleManager?.middlewareChain) {
15965
16164
  const result = await this.lifecycleManager.middlewareChain.execute(
15966
- "message:incoming",
16165
+ Hook.MESSAGE_INCOMING,
15967
16166
  message,
15968
16167
  async (msg) => msg
15969
16168
  );
@@ -16015,9 +16214,10 @@ ${text3}`;
16015
16214
  }
16016
16215
  }
16017
16216
  const sourceAdapterId = message.routing?.sourceAdapterId ?? message.channelId;
16217
+ const routing = sourceAdapterId !== message.routing?.sourceAdapterId ? { ...message.routing, sourceAdapterId } : message.routing;
16018
16218
  if (sourceAdapterId && sourceAdapterId !== "sse" && sourceAdapterId !== "api") {
16019
16219
  const turnId = nanoid3(8);
16020
- this.eventBus.emit("message:queued", {
16220
+ this.eventBus.emit(BusEvent.MESSAGE_QUEUED, {
16021
16221
  sessionId: session.id,
16022
16222
  turnId,
16023
16223
  text: text3,
@@ -16026,9 +16226,9 @@ ${text3}`;
16026
16226
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
16027
16227
  queueDepth: session.queueDepth
16028
16228
  });
16029
- await session.enqueuePrompt(text3, message.attachments, message.routing, turnId);
16229
+ await session.enqueuePrompt(text3, message.attachments, routing, turnId);
16030
16230
  } else {
16031
- await session.enqueuePrompt(text3, message.attachments, message.routing);
16231
+ await session.enqueuePrompt(text3, message.attachments, routing);
16032
16232
  }
16033
16233
  }
16034
16234
  // --- Unified Session Creation Pipeline ---
@@ -16072,6 +16272,7 @@ ${text3}`;
16072
16272
  createdAt: session.createdAt.toISOString(),
16073
16273
  lastActiveAt: (/* @__PURE__ */ new Date()).toISOString(),
16074
16274
  name: session.name,
16275
+ isAssistant: params.isAssistant,
16075
16276
  platform: platform2,
16076
16277
  platforms,
16077
16278
  firstAgent: session.firstAgent,
@@ -16087,7 +16288,7 @@ ${text3}`;
16087
16288
  log16.warn({ err, sessionId: session.id }, "Failed to flush pending skill commands");
16088
16289
  });
16089
16290
  if (params.createThread && session.threadId) {
16090
- this.eventBus.emit("session:threadReady", {
16291
+ this.eventBus.emit(BusEvent.SESSION_THREAD_READY, {
16091
16292
  sessionId: session.id,
16092
16293
  channelId: params.channelId,
16093
16294
  threadId: session.threadId
@@ -16111,6 +16312,36 @@ ${text3}`;
16111
16312
  );
16112
16313
  return allowOption.id;
16113
16314
  };
16315
+ session.on(SessionEv.NAMED, async (name) => {
16316
+ await this.sessionManager.patchRecord(session.id, { name });
16317
+ this.eventBus.emit(BusEvent.SESSION_UPDATED, { sessionId: session.id, name });
16318
+ });
16319
+ const mw = () => this.lifecycleManager?.middlewareChain;
16320
+ session.on(SessionEv.AGENT_EVENT, async (event) => {
16321
+ let processedEvent = event;
16322
+ const chain = mw();
16323
+ if (chain) {
16324
+ const result = await chain.execute(Hook.AGENT_BEFORE_EVENT, { sessionId: session.id, event }, async (e) => e);
16325
+ if (!result) return;
16326
+ processedEvent = result.event;
16327
+ }
16328
+ if (processedEvent.type === "session_end") {
16329
+ session.finish(processedEvent.reason);
16330
+ } else if (processedEvent.type === "error") {
16331
+ session.fail(processedEvent.message);
16332
+ }
16333
+ this.eventBus.emit(BusEvent.AGENT_EVENT, { sessionId: session.id, event: processedEvent });
16334
+ });
16335
+ session.on(SessionEv.STATUS_CHANGE, (_from, to) => {
16336
+ this.sessionManager.patchRecord(session.id, {
16337
+ status: to,
16338
+ lastActiveAt: (/* @__PURE__ */ new Date()).toISOString()
16339
+ });
16340
+ this.eventBus.emit(BusEvent.SESSION_UPDATED, { sessionId: session.id, status: to });
16341
+ });
16342
+ session.on(SessionEv.PROMPT_COUNT_CHANGED, (count) => {
16343
+ this.sessionManager.patchRecord(session.id, { currentPromptCount: count });
16344
+ });
16114
16345
  }
16115
16346
  this.sessionFactory.wireSideEffects(session, {
16116
16347
  eventBus: this.eventBus,