@openacp/cli 2026.410.1 → 2026.410.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
@@ -265,6 +265,8 @@ var init_config_migrations = __esm({
265
265
  log2 = createChildLogger({ module: "config-migrations" });
266
266
  migrations = [
267
267
  {
268
+ // v2025.x: instanceName was added to support multi-instance setups.
269
+ // Old configs lack this field — default to "Main" so the UI has a display name.
268
270
  name: "add-instance-name",
269
271
  apply(raw) {
270
272
  if (raw.instanceName) return false;
@@ -274,6 +276,8 @@ var init_config_migrations = __esm({
274
276
  }
275
277
  },
276
278
  {
279
+ // displayVerbosity was replaced by outputMode — remove the legacy key
280
+ // so it doesn't confuse Zod strict parsing or the config editor.
277
281
  name: "delete-display-verbosity",
278
282
  apply(raw) {
279
283
  if (!("displayVerbosity" in raw)) return false;
@@ -283,6 +287,9 @@ var init_config_migrations = __esm({
283
287
  }
284
288
  },
285
289
  {
290
+ // Instance IDs were originally only in instances.json (the global registry).
291
+ // This migration copies the ID into config.json so each instance is self-identifying
292
+ // without needing to cross-reference the registry.
286
293
  name: "add-instance-id",
287
294
  apply(raw, ctx) {
288
295
  if (raw.id) return false;
@@ -457,10 +464,11 @@ var init_config = __esm({
457
464
  sessionLogRetentionDays: z.number().default(30)
458
465
  }).default({});
459
466
  ConfigSchema = z.object({
467
+ /** Instance UUID, written once at creation time. */
460
468
  id: z.string().optional(),
461
- // instance UUID, written once at creation time
462
469
  instanceName: z.string().optional(),
463
470
  defaultAgent: z.string(),
471
+ // --- Workspace security & path resolution ---
464
472
  workspace: z.object({
465
473
  allowExternalWorkspaces: z.boolean().default(true),
466
474
  security: z.object({
@@ -468,12 +476,16 @@ var init_config = __esm({
468
476
  envWhitelist: z.array(z.string()).default([])
469
477
  }).default({})
470
478
  }).default({}),
479
+ // --- Logging ---
471
480
  logging: LoggingSchema,
481
+ // --- Process lifecycle ---
472
482
  runMode: z.enum(["foreground", "daemon"]).default("foreground"),
473
483
  autoStart: z.boolean().default(false),
484
+ // --- Session persistence ---
474
485
  sessionStore: z.object({
475
486
  ttlDays: z.number().default(30)
476
487
  }).default({}),
488
+ // --- Installed integration tracking (e.g. plugins installed via CLI) ---
477
489
  integrations: z.record(
478
490
  z.string(),
479
491
  z.object({
@@ -481,7 +493,9 @@ var init_config = __esm({
481
493
  installedAt: z.string().optional()
482
494
  })
483
495
  ).default({}),
496
+ // --- Agent output verbosity control ---
484
497
  outputMode: z.enum(["low", "medium", "high"]).default("medium").optional(),
498
+ // --- Multi-agent switching behavior ---
485
499
  agentSwitch: z.object({
486
500
  labelHistory: z.boolean().default(true)
487
501
  }).default({})
@@ -497,6 +511,13 @@ var init_config = __esm({
497
511
  super();
498
512
  this.configPath = process.env.OPENACP_CONFIG_PATH || configPath || expandHome2("~/.openacp/config.json");
499
513
  }
514
+ /**
515
+ * Loads config from disk through the full validation pipeline:
516
+ * 1. Create default config if missing (first run)
517
+ * 2. Apply migrations for older config formats
518
+ * 3. Apply environment variable overrides
519
+ * 4. Validate against Zod schema — exits on failure
520
+ */
500
521
  async load() {
501
522
  const dir = path3.dirname(this.configPath);
502
523
  fs3.mkdirSync(dir, { recursive: true });
@@ -530,9 +551,17 @@ var init_config = __esm({
530
551
  }
531
552
  this.config = result.data;
532
553
  }
554
+ /** Returns a deep clone of the current config to prevent external mutation. */
533
555
  get() {
534
556
  return structuredClone(this.config);
535
557
  }
558
+ /**
559
+ * Merges partial updates into the config file using atomic write (write tmp + rename).
560
+ *
561
+ * Validates the merged result before writing. If `changePath` is provided,
562
+ * emits a `config:changed` event with old and new values for that path,
563
+ * enabling hot-reload without restart.
564
+ */
536
565
  async save(updates, changePath) {
537
566
  const oldConfig = this.config ? structuredClone(this.config) : void 0;
538
567
  const raw = JSON.parse(fs3.readFileSync(this.configPath, "utf-8"));
@@ -554,9 +583,12 @@ var init_config = __esm({
554
583
  }
555
584
  }
556
585
  /**
557
- * Set a single config value by dot-path (e.g. "logging.level").
558
- * Builds the nested update object, validates, and saves.
559
- * Throws if the path contains blocked keys or the value fails Zod validation.
586
+ * Convenience wrapper for updating a single deeply-nested config field
587
+ * without constructing the full update object manually.
588
+ *
589
+ * Accepts a dot-path (e.g. "logging.level") and builds the nested
590
+ * update object internally before delegating to `save()`.
591
+ * Throws if the path contains prototype-pollution keys.
560
592
  */
561
593
  async setPath(dotPath, value) {
562
594
  const BLOCKED_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
@@ -573,6 +605,13 @@ var init_config = __esm({
573
605
  target[parts[parts.length - 1]] = value;
574
606
  await this.save(updates, dotPath);
575
607
  }
608
+ /**
609
+ * Resolves a workspace path from user input.
610
+ *
611
+ * Supports three forms: no input (returns base dir), absolute/tilde paths
612
+ * (validated against allowExternalWorkspaces), and named workspaces
613
+ * (alphanumeric subdirectories under the base).
614
+ */
576
615
  resolveWorkspace(input2) {
577
616
  const workspaceBase = path3.dirname(path3.dirname(this.configPath));
578
617
  if (!input2) {
@@ -607,17 +646,26 @@ var init_config = __esm({
607
646
  fs3.mkdirSync(namedPath, { recursive: true });
608
647
  return namedPath;
609
648
  }
649
+ /** Checks whether the config file exists on disk. Wraps synchronous `fs.existsSync` behind an async interface for consistency with the rest of the ConfigManager API. */
610
650
  async exists() {
611
651
  return fs3.existsSync(this.configPath);
612
652
  }
653
+ /** Returns the resolved path to the config JSON file. */
613
654
  getConfigPath() {
614
655
  return this.configPath;
615
656
  }
657
+ /** Writes a complete config object to disk, creating the directory if needed. Used during initial setup. */
616
658
  async writeNew(config) {
617
659
  const dir = path3.dirname(this.configPath);
618
660
  fs3.mkdirSync(dir, { recursive: true });
619
661
  fs3.writeFileSync(this.configPath, JSON.stringify(config, null, 2));
620
662
  }
663
+ /**
664
+ * Applies `OPENACP_*` environment variables as overrides to per-plugin settings.
665
+ *
666
+ * This lets users configure plugin values (bot tokens, ports, etc.) via env vars
667
+ * without editing settings files — useful for Docker, CI, and headless setups.
668
+ */
621
669
  async applyEnvToPluginSettings(settingsManager) {
622
670
  const pluginOverrides = [
623
671
  { envVar: "OPENACP_TUNNEL_ENABLED", pluginName: "@openacp/tunnel", key: "enabled", transform: (v) => v === "true" },
@@ -644,6 +692,7 @@ var init_config = __esm({
644
692
  }
645
693
  }
646
694
  }
695
+ /** Applies env var overrides to the raw config object before Zod validation. */
647
696
  applyEnvOverrides(raw) {
648
697
  const overrides = [
649
698
  ["OPENACP_DEFAULT_AGENT", ["defaultAgent"]],
@@ -674,6 +723,7 @@ var init_config = __esm({
674
723
  raw.logging.level = "debug";
675
724
  }
676
725
  }
726
+ /** Recursively merges source into target, skipping prototype-pollution keys. */
677
727
  deepMerge(target, source) {
678
728
  const DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
679
729
  for (const key of Object.keys(source)) {
@@ -769,44 +819,75 @@ var init_events = __esm({
769
819
  };
770
820
  BusEvent = {
771
821
  // --- Session lifecycle ---
822
+ /** Fired when a new session is created and ready. */
772
823
  SESSION_CREATED: "session:created",
824
+ /** Fired when session metadata changes (status, name, overrides). */
773
825
  SESSION_UPDATED: "session:updated",
826
+ /** Fired when a session record is deleted from the store. */
774
827
  SESSION_DELETED: "session:deleted",
828
+ /** Fired when a session ends (agent finished or error). */
775
829
  SESSION_ENDED: "session:ended",
830
+ /** Fired when a session receives its auto-generated name. */
776
831
  SESSION_NAMED: "session:named",
832
+ /** Fired after a new session thread is created and bridge connected. */
777
833
  SESSION_THREAD_READY: "session:threadReady",
834
+ /** Fired when an agent's config options change (adapters update control UIs). */
778
835
  SESSION_CONFIG_CHANGED: "session:configChanged",
836
+ /** Fired during agent switch lifecycle (starting/succeeded/failed). */
779
837
  SESSION_AGENT_SWITCH: "session:agentSwitch",
780
838
  // --- Agent ---
839
+ /** Fired for every agent event (text, tool_call, usage, etc.). */
781
840
  AGENT_EVENT: "agent:event",
841
+ /** Fired when a prompt is sent to the agent. */
782
842
  AGENT_PROMPT: "agent:prompt",
783
843
  // --- Permissions ---
844
+ /** Fired when the agent requests user permission (blocks until resolved). */
784
845
  PERMISSION_REQUEST: "permission:request",
846
+ /** Fired after a permission request is resolved (approved or denied). */
785
847
  PERMISSION_RESOLVED: "permission:resolved",
786
848
  // --- Message visibility ---
849
+ /** Fired when a user message is queued (for cross-adapter input visibility). */
787
850
  MESSAGE_QUEUED: "message:queued",
851
+ /** Fired when a queued message starts processing. */
788
852
  MESSAGE_PROCESSING: "message:processing",
789
853
  // --- System lifecycle ---
854
+ /** Fired after kernel (core + plugin infrastructure) has booted. */
790
855
  KERNEL_BOOTED: "kernel:booted",
856
+ /** Fired when the system is fully ready (all adapters connected). */
791
857
  SYSTEM_READY: "system:ready",
858
+ /** Fired during graceful shutdown. */
792
859
  SYSTEM_SHUTDOWN: "system:shutdown",
860
+ /** Fired when all system commands are registered and available. */
793
861
  SYSTEM_COMMANDS_READY: "system:commands-ready",
794
862
  // --- Plugin lifecycle ---
863
+ /** Fired when a plugin loads successfully. */
795
864
  PLUGIN_LOADED: "plugin:loaded",
865
+ /** Fired when a plugin fails to load. */
796
866
  PLUGIN_FAILED: "plugin:failed",
867
+ /** Fired when a plugin is disabled (e.g., missing config). */
797
868
  PLUGIN_DISABLED: "plugin:disabled",
869
+ /** Fired when a plugin is unloaded during shutdown. */
798
870
  PLUGIN_UNLOADED: "plugin:unloaded",
799
871
  // --- Usage ---
872
+ /** Fired when a token usage record is captured (consumed by usage plugin). */
800
873
  USAGE_RECORDED: "usage:recorded"
801
874
  };
802
875
  SessionEv = {
876
+ /** Agent produced an event (text, tool_call, etc.) during a turn. */
803
877
  AGENT_EVENT: "agent_event",
878
+ /** Agent is requesting user permission — blocks until resolved. */
804
879
  PERMISSION_REQUEST: "permission_request",
880
+ /** Session ended (agent finished, cancelled, or errored). */
805
881
  SESSION_END: "session_end",
882
+ /** Session status changed (e.g., initializing → active). */
806
883
  STATUS_CHANGE: "status_change",
884
+ /** Session received an auto-generated name from the first response. */
807
885
  NAMED: "named",
886
+ /** An unrecoverable error occurred in the session. */
808
887
  ERROR: "error",
888
+ /** The session's prompt count changed (used for UI counters). */
809
889
  PROMPT_COUNT_CHANGED: "prompt_count_changed",
890
+ /** A new prompt turn started (provides TurnContext for middleware). */
810
891
  TURN_STARTED: "turn_started"
811
892
  };
812
893
  }
@@ -1563,13 +1644,16 @@ var init_settings_manager = __esm({
1563
1644
  constructor(basePath) {
1564
1645
  this.basePath = basePath;
1565
1646
  }
1647
+ /** Returns the base path for all plugin settings directories. */
1566
1648
  getBasePath() {
1567
1649
  return this.basePath;
1568
1650
  }
1651
+ /** Create a SettingsAPI instance scoped to a specific plugin. */
1569
1652
  createAPI(pluginName) {
1570
1653
  const settingsPath = this.getSettingsPath(pluginName);
1571
1654
  return new SettingsAPIImpl(settingsPath);
1572
1655
  }
1656
+ /** Load a plugin's settings from disk. Returns empty object if file doesn't exist. */
1573
1657
  async loadSettings(pluginName) {
1574
1658
  const settingsPath = this.getSettingsPath(pluginName);
1575
1659
  try {
@@ -1579,6 +1663,7 @@ var init_settings_manager = __esm({
1579
1663
  return {};
1580
1664
  }
1581
1665
  }
1666
+ /** Validate settings against a Zod schema. Returns valid if no schema is provided. */
1582
1667
  validateSettings(_pluginName, settings, schema) {
1583
1668
  if (!schema) return { valid: true };
1584
1669
  const result = schema.safeParse(settings);
@@ -1590,12 +1675,14 @@ var init_settings_manager = __esm({
1590
1675
  )
1591
1676
  };
1592
1677
  }
1678
+ /** Resolve the absolute path to a plugin's settings.json file. */
1593
1679
  getSettingsPath(pluginName) {
1594
1680
  return path16.join(this.basePath, pluginName, "settings.json");
1595
1681
  }
1596
1682
  async getPluginSettings(pluginName) {
1597
1683
  return this.loadSettings(pluginName);
1598
1684
  }
1685
+ /** Merge updates into existing settings (shallow merge). */
1599
1686
  async updatePluginSettings(pluginName, updates) {
1600
1687
  const api = this.createAPI(pluginName);
1601
1688
  const current = await api.getAll();
@@ -2321,6 +2408,12 @@ var init_doctor = __esm({
2321
2408
  this.dryRun = options?.dryRun ?? false;
2322
2409
  this.dataDir = options.dataDir;
2323
2410
  }
2411
+ /**
2412
+ * Executes all checks and returns an aggregated report.
2413
+ *
2414
+ * Safe fixes are applied inline (mutating CheckResult.message to show "Fixed").
2415
+ * Risky fixes are deferred to `report.pendingFixes` for user confirmation.
2416
+ */
2324
2417
  async runAll() {
2325
2418
  const ctx = await this.buildContext();
2326
2419
  const checks = [...ALL_CHECKS].sort((a, b) => a.order - b.order);
@@ -2368,6 +2461,7 @@ var init_doctor = __esm({
2368
2461
  }
2369
2462
  return { categories, summary, pendingFixes };
2370
2463
  }
2464
+ /** Constructs the shared context used by all checks — loads config if available. */
2371
2465
  async buildContext() {
2372
2466
  const dataDir = this.dataDir;
2373
2467
  const configPath = process.env.OPENACP_CONFIG_PATH || path22.join(dataDir, "config.json");
@@ -2432,6 +2526,7 @@ var init_instance_registry = __esm({
2432
2526
  this.registryPath = registryPath;
2433
2527
  }
2434
2528
  data = { version: 1, instances: {} };
2529
+ /** Load the registry from disk. If the file is missing or corrupt, starts fresh. */
2435
2530
  load() {
2436
2531
  try {
2437
2532
  const raw = fs27.readFileSync(this.registryPath, "utf-8");
@@ -2459,26 +2554,33 @@ var init_instance_registry = __esm({
2459
2554
  this.save();
2460
2555
  }
2461
2556
  }
2557
+ /** Persist the registry to disk, creating parent directories if needed. */
2462
2558
  save() {
2463
2559
  const dir = path25.dirname(this.registryPath);
2464
2560
  fs27.mkdirSync(dir, { recursive: true });
2465
2561
  fs27.writeFileSync(this.registryPath, JSON.stringify(this.data, null, 2));
2466
2562
  }
2563
+ /** Add or update an instance entry in the registry. Does not persist — call save() after. */
2467
2564
  register(id, root) {
2468
2565
  this.data.instances[id] = { id, root };
2469
2566
  }
2567
+ /** Remove an instance entry. Does not persist — call save() after. */
2470
2568
  remove(id) {
2471
2569
  delete this.data.instances[id];
2472
2570
  }
2571
+ /** Look up an instance by its ID. */
2473
2572
  get(id) {
2474
2573
  return this.data.instances[id];
2475
2574
  }
2575
+ /** Look up an instance by its root directory path. */
2476
2576
  getByRoot(root) {
2477
2577
  return Object.values(this.data.instances).find((e) => e.root === root);
2478
2578
  }
2579
+ /** Returns all registered instances. */
2479
2580
  list() {
2480
2581
  return Object.values(this.data.instances);
2481
2582
  }
2583
+ /** Returns `baseId` if available, otherwise appends `-2`, `-3`, etc. until unique. */
2482
2584
  uniqueId(baseId) {
2483
2585
  if (!this.data.instances[baseId]) return baseId;
2484
2586
  let n = 2;
@@ -2735,6 +2837,12 @@ var init_notification = __esm({
2735
2837
  constructor(adapters) {
2736
2838
  this.adapters = adapters;
2737
2839
  }
2840
+ /**
2841
+ * Send a notification to a specific channel adapter.
2842
+ *
2843
+ * Failures are swallowed — notifications are best-effort and must not crash
2844
+ * the session or caller (e.g. on session completion).
2845
+ */
2738
2846
  async notify(channelId, notification) {
2739
2847
  const adapter = this.adapters.get(channelId);
2740
2848
  if (!adapter) return;
@@ -2743,6 +2851,12 @@ var init_notification = __esm({
2743
2851
  } catch {
2744
2852
  }
2745
2853
  }
2854
+ /**
2855
+ * Broadcast a notification to every registered adapter.
2856
+ *
2857
+ * Used for system-wide alerts (e.g. global budget exhausted). Each adapter
2858
+ * failure is isolated so one broken adapter cannot block the rest.
2859
+ */
2746
2860
  async notifyAll(notification) {
2747
2861
  for (const adapter of this.adapters.values()) {
2748
2862
  try {
@@ -2830,6 +2944,14 @@ var init_file_service = __esm({
2830
2944
  }
2831
2945
  return removed;
2832
2946
  }
2947
+ /**
2948
+ * Persist a file to the session's directory and return an `Attachment` descriptor.
2949
+ *
2950
+ * The file name is sanitized (non-safe characters replaced with `_`) and prefixed
2951
+ * with a millisecond timestamp to prevent name collisions across multiple saves in
2952
+ * the same session. The original `fileName` is preserved in the returned descriptor
2953
+ * so the user-facing name is not lost.
2954
+ */
2833
2955
  async saveFile(sessionId, fileName, data, mimeType) {
2834
2956
  const sessionDir = path32.join(this.baseDir, sessionId);
2835
2957
  await fs32.promises.mkdir(sessionDir, { recursive: true });
@@ -2844,6 +2966,10 @@ var init_file_service = __esm({
2844
2966
  size: data.length
2845
2967
  };
2846
2968
  }
2969
+ /**
2970
+ * Build an `Attachment` descriptor for a file that already exists on disk.
2971
+ * Returns `null` if the path does not exist or is not a regular file.
2972
+ */
2847
2973
  async resolveFile(filePath) {
2848
2974
  try {
2849
2975
  const stat = await fs32.promises.stat(filePath);
@@ -2888,6 +3014,7 @@ var init_file_service = __esm({
2888
3014
  extensionFromMime(mimeType) {
2889
3015
  return _FileService.extensionFromMime(mimeType);
2890
3016
  }
3017
+ /** Returns the canonical file extension for a given MIME type (e.g. `"image/png"` → `".png"`). */
2891
3018
  static extensionFromMime(mimeType) {
2892
3019
  return MIME_TO_EXT[mimeType] || ".bin";
2893
3020
  }
@@ -2905,6 +3032,16 @@ var init_security_guard = __esm({
2905
3032
  this.getSecurityConfig = getSecurityConfig;
2906
3033
  this.sessionManager = sessionManager;
2907
3034
  }
3035
+ /**
3036
+ * Returns `{ allowed: true }` when the message may proceed, or
3037
+ * `{ allowed: false, reason }` when it should be blocked.
3038
+ *
3039
+ * Two checks run in order:
3040
+ * 1. **Allowlist** — if `allowedUserIds` is non-empty, the user's ID (coerced to string)
3041
+ * must appear in the list. Telegram/Slack IDs are numbers, so coercion is required.
3042
+ * 2. **Session cap** — counts sessions in `active` or `initializing` state. `initializing`
3043
+ * is included because a session holds resources before it reaches `active`.
3044
+ */
2908
3045
  async checkAccess(message) {
2909
3046
  const config = await this.getSecurityConfig();
2910
3047
  const allowedIds = config.allowedUserIds ?? [];
@@ -3298,6 +3435,13 @@ var init_sse_manager = __esm({
3298
3435
  sseCleanupHandlers = /* @__PURE__ */ new Map();
3299
3436
  healthInterval;
3300
3437
  boundHandlers = [];
3438
+ /**
3439
+ * Subscribes to EventBus events and starts the health heartbeat interval.
3440
+ *
3441
+ * Must be called after the HTTP server is listening, not during plugin setup —
3442
+ * before `setup()` runs, no EventBus listeners are registered, so any events
3443
+ * emitted in the interim are silently missed.
3444
+ */
3301
3445
  setup() {
3302
3446
  if (!this.eventBus) return;
3303
3447
  const events = [
@@ -3331,6 +3475,13 @@ var init_sse_manager = __esm({
3331
3475
  });
3332
3476
  }, 15e3);
3333
3477
  }
3478
+ /**
3479
+ * Handles an incoming SSE request by upgrading the HTTP response to an event stream.
3480
+ *
3481
+ * The response is kept open indefinitely; cleanup runs when the client disconnects.
3482
+ * An initial `: connected` comment is written immediately so proxies and browsers
3483
+ * flush the response headers before the first real event arrives.
3484
+ */
3334
3485
  handleRequest(req, res) {
3335
3486
  if (this.sseConnections.size >= MAX_SSE_CONNECTIONS) {
3336
3487
  res.writeHead(503, { "Content-Type": "application/json" });
@@ -3365,6 +3516,13 @@ var init_sse_manager = __esm({
3365
3516
  this.sseCleanupHandlers.set(res, cleanup);
3366
3517
  req.on("close", cleanup);
3367
3518
  }
3519
+ /**
3520
+ * Broadcasts an event to all connected SSE clients.
3521
+ *
3522
+ * Session-scoped events (agent_event, permission_request, etc.) are filtered per-connection:
3523
+ * a client that subscribed with `?sessionId=X` only receives events for session X.
3524
+ * Global events (session_created, health) are delivered to every client.
3525
+ */
3368
3526
  broadcast(event, data) {
3369
3527
  const payload = `event: ${event}
3370
3528
  data: ${JSON.stringify(data)}
@@ -3400,6 +3558,12 @@ data: ${JSON.stringify(data)}
3400
3558
  this.handleRequest(request.raw, reply.raw);
3401
3559
  };
3402
3560
  }
3561
+ /**
3562
+ * Stops the heartbeat, removes all EventBus listeners, and closes all open SSE connections.
3563
+ *
3564
+ * Only listeners registered by this instance are removed — other consumers on the same
3565
+ * EventBus are unaffected.
3566
+ */
3403
3567
  stop() {
3404
3568
  if (this.healthInterval) clearInterval(this.healthInterval);
3405
3569
  if (this.eventBus) {
@@ -3459,9 +3623,15 @@ var init_static_server = __esm({
3459
3623
  }
3460
3624
  }
3461
3625
  }
3626
+ /** Returns true if a UI build was found and static serving is active. */
3462
3627
  isAvailable() {
3463
3628
  return this.uiDir !== void 0;
3464
3629
  }
3630
+ /**
3631
+ * Attempts to serve a static file or SPA fallback for the given request.
3632
+ *
3633
+ * @returns true if the response was handled, false if the caller should return a 404.
3634
+ */
3465
3635
  serve(req, res) {
3466
3636
  if (!this.uiDir) return false;
3467
3637
  const urlPath = (req.url || "/").split("?")[0];
@@ -3523,23 +3693,38 @@ var init_speech_service = __esm({
3523
3693
  setProviderFactory(factory) {
3524
3694
  this.providerFactory = factory;
3525
3695
  }
3696
+ /** Register an STT provider by name. Overwrites any existing provider with the same name. */
3526
3697
  registerSTTProvider(name, provider) {
3527
3698
  this.sttProviders.set(name, provider);
3528
3699
  }
3700
+ /** Register a TTS provider by name. Called by external TTS plugins (e.g. msedge-tts-plugin). */
3529
3701
  registerTTSProvider(name, provider) {
3530
3702
  this.ttsProviders.set(name, provider);
3531
3703
  }
3704
+ /** Remove a TTS provider — called by external plugins on teardown. */
3532
3705
  unregisterTTSProvider(name) {
3533
3706
  this.ttsProviders.delete(name);
3534
3707
  }
3708
+ /** Returns true if an STT provider is configured and has credentials. */
3535
3709
  isSTTAvailable() {
3536
3710
  const { provider, providers } = this.config.stt;
3537
3711
  return provider !== null && providers[provider]?.apiKey !== void 0;
3538
3712
  }
3713
+ /**
3714
+ * Returns true if a TTS provider is configured and an implementation is registered.
3715
+ *
3716
+ * Config alone is not enough — the TTS provider plugin must have registered
3717
+ * its implementation via `registerTTSProvider` before this returns true.
3718
+ */
3539
3719
  isTTSAvailable() {
3540
3720
  const provider = this.config.tts.provider;
3541
3721
  return provider !== null && this.ttsProviders.has(provider);
3542
3722
  }
3723
+ /**
3724
+ * Transcribes audio using the configured STT provider.
3725
+ *
3726
+ * @throws if no STT provider is configured or if the named provider is not registered.
3727
+ */
3543
3728
  async transcribe(audioBuffer, mimeType, options) {
3544
3729
  const providerName = this.config.stt.provider;
3545
3730
  if (!providerName || !this.config.stt.providers[providerName]?.apiKey) {
@@ -3551,6 +3736,11 @@ var init_speech_service = __esm({
3551
3736
  }
3552
3737
  return provider.transcribe(audioBuffer, mimeType, options);
3553
3738
  }
3739
+ /**
3740
+ * Synthesizes speech using the configured TTS provider.
3741
+ *
3742
+ * @throws if no TTS provider is configured or if the named provider is not registered.
3743
+ */
3554
3744
  async synthesize(text3, options) {
3555
3745
  const providerName = this.config.tts.provider;
3556
3746
  if (!providerName) {
@@ -3562,10 +3752,17 @@ var init_speech_service = __esm({
3562
3752
  }
3563
3753
  return provider.synthesize(text3, options);
3564
3754
  }
3755
+ /** Replace the active config without rebuilding providers. Use `refreshProviders` to also rebuild. */
3565
3756
  updateConfig(config) {
3566
3757
  this.config = config;
3567
3758
  }
3568
- /** Re-create factory-managed providers from config. Preserves externally-registered providers (e.g. from plugins). */
3759
+ /**
3760
+ * Reloads TTS and STT providers from a new config snapshot.
3761
+ *
3762
+ * Called after config changes or plugin hot-reload. Factory-managed providers are
3763
+ * rebuilt via the registered `ProviderFactory`; externally-registered providers
3764
+ * (e.g. from `@openacp/msedge-tts-plugin`) are preserved rather than discarded.
3765
+ */
3569
3766
  refreshProviders(newConfig) {
3570
3767
  this.config = newConfig;
3571
3768
  if (this.providerFactory) {
@@ -3605,6 +3802,12 @@ var init_groq = __esm({
3605
3802
  this.defaultModel = defaultModel;
3606
3803
  }
3607
3804
  name = "groq";
3805
+ /**
3806
+ * Transcribes audio using the Groq Whisper API.
3807
+ *
3808
+ * `verbose_json` response format is requested so the API returns language
3809
+ * detection and duration metadata alongside the transcript text.
3810
+ */
3608
3811
  async transcribe(audioBuffer, mimeType, options) {
3609
3812
  const ext = mimeToExt(mimeType);
3610
3813
  const form = new FormData();
@@ -3707,6 +3910,12 @@ var init_context_manager = __esm({
3707
3910
  constructor(cachePath) {
3708
3911
  this.cache = new ContextCache(cachePath);
3709
3912
  }
3913
+ /**
3914
+ * Wire in the history store after construction.
3915
+ *
3916
+ * Injected separately because the history store (backed by the context plugin's
3917
+ * recorder) may not be ready when ContextManager is first instantiated.
3918
+ */
3710
3919
  setHistoryStore(store) {
3711
3920
  this.historyStore = store;
3712
3921
  }
@@ -3722,19 +3931,43 @@ var init_context_manager = __esm({
3722
3931
  async flushSession(sessionId) {
3723
3932
  if (this.sessionFlusher) await this.sessionFlusher(sessionId);
3724
3933
  }
3934
+ /**
3935
+ * Read the raw history for a session directly from the history store.
3936
+ *
3937
+ * Returns null if no historyStore has been configured via `setHistoryStore()`,
3938
+ * or if the session has no recorded history.
3939
+ */
3725
3940
  async getHistory(sessionId) {
3726
3941
  if (!this.historyStore) return null;
3727
3942
  return this.historyStore.read(sessionId);
3728
3943
  }
3944
+ /**
3945
+ * Register a provider. Providers are queried in insertion order.
3946
+ * Register higher-priority sources (e.g. local history) before lower-priority ones (e.g. entire).
3947
+ */
3729
3948
  register(provider) {
3730
3949
  this.providers.push(provider);
3731
3950
  }
3951
+ /**
3952
+ * Return the first provider that reports itself available for the given repo.
3953
+ *
3954
+ * This is a availability check — it returns the highest-priority available provider
3955
+ * (i.e. the first registered one that passes `isAvailable`), not necessarily the
3956
+ * one that would yield the richest context for a specific query.
3957
+ */
3732
3958
  async getProvider(repoPath) {
3733
3959
  for (const provider of this.providers) {
3734
3960
  if (await provider.isAvailable(repoPath)) return provider;
3735
3961
  }
3736
3962
  return null;
3737
3963
  }
3964
+ /**
3965
+ * List sessions using the same provider-waterfall logic as `buildContext`.
3966
+ *
3967
+ * Tries each registered provider in order, returning the first non-empty result.
3968
+ * Unlike `buildContext`, results are not cached — callers should avoid calling
3969
+ * this in hot paths.
3970
+ */
3738
3971
  async listSessions(query) {
3739
3972
  for (const provider of this.providers) {
3740
3973
  if (!await provider.isAvailable(query.repoPath)) continue;
@@ -3743,6 +3976,13 @@ var init_context_manager = __esm({
3743
3976
  }
3744
3977
  return null;
3745
3978
  }
3979
+ /**
3980
+ * Build a context block for injection into an agent prompt.
3981
+ *
3982
+ * Tries each registered provider in order. Results are cached by (repoPath + queryKey)
3983
+ * to avoid redundant disk reads. Pass `options.noCache = true` when the caller knows
3984
+ * the history just changed (e.g. immediately after an agent switch + flush).
3985
+ */
3746
3986
  async buildContext(query, options) {
3747
3987
  const queryKey = `${query.type}:${query.value}:${options?.limit ?? ""}:${options?.maxTokens ?? ""}:${options?.labelAgent ?? ""}`;
3748
3988
  if (!options?.noCache) {
@@ -3905,8 +4145,9 @@ var init_checkpoint_reader = __esm({
3905
4145
  sessionIndex: String(idx),
3906
4146
  transcriptPath,
3907
4147
  createdAt,
4148
+ // endedAt isn't stored in the checkpoint metadata; EntireProvider fills
4149
+ // it from the last turn timestamp when parsing the JSONL transcript.
3908
4150
  endedAt: createdAt,
3909
- // will be filled from JSONL by conversation builder
3910
4151
  branch: smeta.branch ?? cpMeta.branch ?? "",
3911
4152
  agent: smeta.agent ?? "",
3912
4153
  turnCount: smeta.session_metrics?.turn_count ?? 0,
@@ -4735,11 +4976,21 @@ var init_messaging_adapter = __esm({
4735
4976
  this.adapterConfig = adapterConfig;
4736
4977
  }
4737
4978
  // === Message dispatch flow ===
4979
+ /**
4980
+ * Entry point for all outbound messages from sessions to the platform.
4981
+ * Resolves the current verbosity, filters messages that should be hidden,
4982
+ * then dispatches to the appropriate type-specific handler.
4983
+ */
4738
4984
  async sendMessage(sessionId, content) {
4739
4985
  const verbosity = this.getVerbosity();
4740
4986
  if (!this.shouldDisplay(content, verbosity)) return;
4741
4987
  await this.dispatchMessage(sessionId, content, verbosity);
4742
4988
  }
4989
+ /**
4990
+ * Routes a message to its type-specific handler.
4991
+ * Subclasses can override this for custom dispatch logic, but typically
4992
+ * override individual handle* methods instead.
4993
+ */
4743
4994
  async dispatchMessage(sessionId, content, verbosity) {
4744
4995
  switch (content.type) {
4745
4996
  case "text":
@@ -4777,6 +5028,8 @@ var init_messaging_adapter = __esm({
4777
5028
  }
4778
5029
  }
4779
5030
  // === Default handlers — all protected, all overridable ===
5031
+ // Each handler is a no-op by default. Subclasses override only the message
5032
+ // types they support (e.g., Telegram overrides handleText, handleToolCall, etc.).
4780
5033
  async handleText(_sessionId, _content) {
4781
5034
  }
4782
5035
  async handleThought(_sessionId, _content, _verbosity) {
@@ -4810,6 +5063,10 @@ var init_messaging_adapter = __esm({
4810
5063
  async handleResourceLink(_sessionId, _content) {
4811
5064
  }
4812
5065
  // === Helpers ===
5066
+ /**
5067
+ * Resolves the current output verbosity by checking (in priority order):
5068
+ * per-channel config, global config, then adapter default. Falls back to "medium".
5069
+ */
4813
5070
  getVerbosity() {
4814
5071
  const config = this.context.configManager.get();
4815
5072
  const channelConfig = config.channels;
@@ -4818,6 +5075,13 @@ var init_messaging_adapter = __esm({
4818
5075
  if (v === "low" || v === "high") return v;
4819
5076
  return "medium";
4820
5077
  }
5078
+ /**
5079
+ * Determines whether a message should be displayed at the given verbosity.
5080
+ *
5081
+ * Noise filtering: tool calls matching noise rules (e.g., `ls`, `glob`, `grep`)
5082
+ * are hidden at medium/low verbosity to reduce clutter. Thoughts and usage
5083
+ * stats are hidden entirely at "low".
5084
+ */
4821
5085
  shouldDisplay(content, verbosity) {
4822
5086
  if (verbosity === "low" && HIDDEN_ON_LOW.has(content.type)) return false;
4823
5087
  if (content.type === "tool_call") {
@@ -5044,6 +5308,13 @@ var init_send_queue = __esm({
5044
5308
  get pending() {
5045
5309
  return this.items.length;
5046
5310
  }
5311
+ /**
5312
+ * Queues an async operation for rate-limited execution.
5313
+ *
5314
+ * For text-type items with a key, replaces any existing queued item with
5315
+ * the same key (deduplication). This is used for streaming draft updates
5316
+ * where only the latest content matters.
5317
+ */
5047
5318
  enqueue(fn, opts) {
5048
5319
  const type = opts?.type ?? "other";
5049
5320
  const key = opts?.key;
@@ -5071,6 +5342,11 @@ var init_send_queue = __esm({
5071
5342
  this.scheduleProcess();
5072
5343
  return promise;
5073
5344
  }
5345
+ /**
5346
+ * Called when a platform rate limit is hit. Drops all pending text items
5347
+ * (draft updates) to reduce backlog, keeping only non-text items that
5348
+ * represent important operations (e.g., permission requests).
5349
+ */
5074
5350
  onRateLimited() {
5075
5351
  this.config.onRateLimited?.();
5076
5352
  const remaining = [];
@@ -5089,6 +5365,10 @@ var init_send_queue = __esm({
5089
5365
  }
5090
5366
  this.items = [];
5091
5367
  }
5368
+ /**
5369
+ * Schedules the next item for processing after the rate-limit delay.
5370
+ * Uses per-category timing when available, falling back to the global minInterval.
5371
+ */
5092
5372
  scheduleProcess() {
5093
5373
  if (this.processing) return;
5094
5374
  if (this.items.length === 0) return;
@@ -5147,6 +5427,9 @@ var init_tool_card_state = __esm({
5147
5427
  specs = [];
5148
5428
  planEntries;
5149
5429
  usage;
5430
+ // Lifecycle: active (first flush pending) → active (subsequent updates debounced) → finalized.
5431
+ // Once finalized, all updateFromSpec/updatePlan/appendUsage/finalize() calls are no-ops —
5432
+ // guards against events arriving after the session has ended or the tool has already completed.
5150
5433
  finalized = false;
5151
5434
  isFirstFlush = true;
5152
5435
  debounceTimer;
@@ -5154,6 +5437,7 @@ var init_tool_card_state = __esm({
5154
5437
  constructor(config) {
5155
5438
  this.onFlush = config.onFlush;
5156
5439
  }
5440
+ /** Adds or updates a tool spec. First call flushes immediately; subsequent calls are debounced. */
5157
5441
  updateFromSpec(spec) {
5158
5442
  if (this.finalized) return;
5159
5443
  const existingIdx = this.specs.findIndex((s) => s.id === spec.id);
@@ -5169,6 +5453,7 @@ var init_tool_card_state = __esm({
5169
5453
  this.scheduleFlush();
5170
5454
  }
5171
5455
  }
5456
+ /** Updates the plan entries displayed alongside tool cards. */
5172
5457
  updatePlan(entries) {
5173
5458
  if (this.finalized) return;
5174
5459
  this.planEntries = entries;
@@ -5179,17 +5464,20 @@ var init_tool_card_state = __esm({
5179
5464
  this.scheduleFlush();
5180
5465
  }
5181
5466
  }
5467
+ /** Appends token usage data to the tool card (typically at end of turn). */
5182
5468
  appendUsage(usage) {
5183
5469
  if (this.finalized) return;
5184
5470
  this.usage = usage;
5185
5471
  this.scheduleFlush();
5186
5472
  }
5473
+ /** Marks the turn as complete and flushes the final snapshot immediately. */
5187
5474
  finalize() {
5188
5475
  if (this.finalized) return;
5189
5476
  this.finalized = true;
5190
5477
  this.clearDebounce();
5191
5478
  this.flush();
5192
5479
  }
5480
+ /** Stops all pending flushes without emitting a final snapshot. */
5193
5481
  destroy() {
5194
5482
  this.finalized = true;
5195
5483
  this.clearDebounce();
@@ -5295,9 +5583,11 @@ var init_stream_accumulator = __esm({
5295
5583
  entry.diffStats = update.diffStats;
5296
5584
  }
5297
5585
  }
5586
+ /** Retrieves a tool entry by ID, or undefined if not yet tracked. */
5298
5587
  get(id) {
5299
5588
  return this.entries.get(id);
5300
5589
  }
5590
+ /** Resets all state between turns. */
5301
5591
  clear() {
5302
5592
  this.entries.clear();
5303
5593
  this.pendingUpdates.clear();
@@ -5306,20 +5596,24 @@ var init_stream_accumulator = __esm({
5306
5596
  ThoughtBuffer = class {
5307
5597
  chunks = [];
5308
5598
  sealed = false;
5599
+ /** Appends a thought text chunk. Ignored if already sealed. */
5309
5600
  append(chunk) {
5310
5601
  if (this.sealed) return;
5311
5602
  this.chunks.push(chunk);
5312
5603
  }
5604
+ /** Marks the thought as complete and returns the full accumulated text. */
5313
5605
  seal() {
5314
5606
  this.sealed = true;
5315
5607
  return this.chunks.join("");
5316
5608
  }
5609
+ /** Returns the text accumulated so far without sealing. */
5317
5610
  getText() {
5318
5611
  return this.chunks.join("");
5319
5612
  }
5320
5613
  isSealed() {
5321
5614
  return this.sealed;
5322
5615
  }
5616
+ /** Resets the buffer for reuse in a new turn. */
5323
5617
  reset() {
5324
5618
  this.chunks = [];
5325
5619
  this.sealed = false;
@@ -5472,6 +5766,13 @@ var init_display_spec_builder = __esm({
5472
5766
  constructor(tunnelService) {
5473
5767
  this.tunnelService = tunnelService;
5474
5768
  }
5769
+ /**
5770
+ * Builds a display spec for a single tool call entry.
5771
+ *
5772
+ * Deduplicates fields to avoid repeating the same info (e.g., if the title
5773
+ * was derived from the command, the command field is omitted). For long
5774
+ * output, generates a viewer link via the tunnel service when available.
5775
+ */
5475
5776
  buildToolSpec(entry, mode, sessionContext) {
5476
5777
  const effectiveKind = entry.displayKind ?? (isApplyPatchOtherTool(entry.kind, entry.name, entry.rawInput) ? "edit" : entry.kind);
5477
5778
  const icon = KIND_ICONS[effectiveKind] ?? KIND_ICONS["other"] ?? "\u{1F6E0}\uFE0F";
@@ -5530,6 +5831,7 @@ var init_display_spec_builder = __esm({
5530
5831
  isHidden
5531
5832
  };
5532
5833
  }
5834
+ /** Builds a display spec for an agent thought. Content is only included at high verbosity. */
5533
5835
  buildThoughtSpec(content, mode) {
5534
5836
  const indicator = "Thinking...";
5535
5837
  return {
@@ -5551,6 +5853,7 @@ var init_output_mode_resolver = __esm({
5551
5853
  "use strict";
5552
5854
  VALID_MODES = /* @__PURE__ */ new Set(["low", "medium", "high"]);
5553
5855
  OutputModeResolver = class {
5856
+ /** Resolves the effective output mode by walking the override cascade. */
5554
5857
  resolve(configManager, adapterName, sessionId, sessionManager) {
5555
5858
  const config = configManager.get();
5556
5859
  let mode = toOutputMode(config.outputMode) ?? "medium";
@@ -8425,6 +8728,14 @@ var init_permissions = __esm({
8425
8728
  this.sendNotification = sendNotification;
8426
8729
  }
8427
8730
  pending = /* @__PURE__ */ new Map();
8731
+ /**
8732
+ * Send a permission request to the session's topic as an inline keyboard message,
8733
+ * and fire a notification to the Notifications topic so the user is alerted.
8734
+ *
8735
+ * Each button encodes `p:<callbackKey>:<optionId>`. The callbackKey is a short
8736
+ * nanoid that maps to the full pending state stored in-memory, avoiding the
8737
+ * 64-byte Telegram callback_data limit.
8738
+ */
8428
8739
  async sendPermissionRequest(session, request) {
8429
8740
  const threadId = Number(session.threadId);
8430
8741
  const callbackKey = nanoid4(8);
@@ -8459,6 +8770,12 @@ ${escapeHtml(request.description)}`,
8459
8770
  deepLink
8460
8771
  });
8461
8772
  }
8773
+ /**
8774
+ * Register the `p:` callback handler in the bot's middleware chain.
8775
+ *
8776
+ * Must be called during setup so grammY processes permission responses
8777
+ * before other generic callback handlers.
8778
+ */
8462
8779
  setupCallbackHandler() {
8463
8780
  this.bot.on("callback_query:data", async (ctx, next) => {
8464
8781
  const data = ctx.callbackQuery.data;
@@ -8617,7 +8934,10 @@ var init_activity = __esm({
8617
8934
  this.sessionId = sessionId;
8618
8935
  this.state = new ToolCardState({
8619
8936
  onFlush: (snapshot) => {
8620
- this.flushPromise = this.flushPromise.then(() => this._sendOrEdit(snapshot)).catch(() => {
8937
+ this.flushPromise = this.flushPromise.then(() => {
8938
+ if (this.aborted) return;
8939
+ return this._sendOrEdit(snapshot);
8940
+ }).catch(() => {
8621
8941
  });
8622
8942
  }
8623
8943
  });
@@ -8627,6 +8947,7 @@ var init_activity = __esm({
8627
8947
  lastSentText;
8628
8948
  flushPromise = Promise.resolve();
8629
8949
  overflowMsgIds = [];
8950
+ aborted = false;
8630
8951
  tracer;
8631
8952
  sessionId;
8632
8953
  updateFromSpec(spec) {
@@ -8643,6 +8964,7 @@ var init_activity = __esm({
8643
8964
  await this.flushPromise;
8644
8965
  }
8645
8966
  destroy() {
8967
+ this.aborted = true;
8646
8968
  this.state.destroy();
8647
8969
  }
8648
8970
  hasContent() {
@@ -8883,6 +9205,7 @@ var init_streaming = __esm({
8883
9205
  lastSentHtml = "";
8884
9206
  displayTruncated = false;
8885
9207
  tracer;
9208
+ /** Append a text chunk to the buffer and schedule a throttled flush. */
8886
9209
  append(text3) {
8887
9210
  if (!text3) return;
8888
9211
  this.buffer += text3;
@@ -8962,6 +9285,16 @@ var init_streaming = __esm({
8962
9285
  }
8963
9286
  }
8964
9287
  }
9288
+ /**
9289
+ * Flush the complete buffer as the final message for this turn.
9290
+ *
9291
+ * Cancels any pending debounce timer, waits for in-flight flushes to settle,
9292
+ * then sends the full content. If the HTML exceeds 4096 bytes, the content is
9293
+ * split at markdown boundaries to avoid breaking HTML tags mid-tag.
9294
+ *
9295
+ * Returns the Telegram message ID of the sent/edited message, or undefined
9296
+ * if nothing was sent (empty buffer or all network calls failed).
9297
+ */
8965
9298
  async finalize() {
8966
9299
  this.tracer?.log("telegram", { action: "draft:finalize", sessionId: this.sessionId, bufferLen: this.buffer.length, msgId: this.messageId });
8967
9300
  if (this.flushTimer) {
@@ -9033,9 +9366,14 @@ var init_streaming = __esm({
9033
9366
  this.tracer?.log("telegram", { action: "draft:finalize:split", sessionId: this.sessionId, chunks: mdChunks.length });
9034
9367
  return this.messageId;
9035
9368
  }
9369
+ /** Returns the Telegram message ID for this draft, or undefined if not yet sent. */
9036
9370
  getMessageId() {
9037
9371
  return this.messageId;
9038
9372
  }
9373
+ /**
9374
+ * Strip occurrences of `pattern` from the buffer and edit the message in-place.
9375
+ * Used by the TTS plugin to remove [TTS]...[/TTS] blocks after audio is sent.
9376
+ */
9039
9377
  async stripPattern(pattern) {
9040
9378
  if (!this.messageId || !this.buffer) return;
9041
9379
  const stripped = this.buffer.replace(pattern, "").trim();
@@ -9073,6 +9411,10 @@ var init_draft_manager = __esm({
9073
9411
  drafts = /* @__PURE__ */ new Map();
9074
9412
  textBuffers = /* @__PURE__ */ new Map();
9075
9413
  finalizedDrafts = /* @__PURE__ */ new Map();
9414
+ /**
9415
+ * Return the active draft for a session, creating one if it doesn't exist yet.
9416
+ * Only one draft per session exists at a time.
9417
+ */
9076
9418
  getOrCreate(sessionId, threadId, tracer = null) {
9077
9419
  let draft = this.drafts.get(sessionId);
9078
9420
  if (!draft) {
@@ -9101,7 +9443,12 @@ var init_draft_manager = __esm({
9101
9443
  );
9102
9444
  }
9103
9445
  /**
9104
- * Finalize the current draft and return the message ID.
9446
+ * Finalize the active draft for a session and retain a short-lived reference for post-send edits.
9447
+ *
9448
+ * Removes the draft from the active map before awaiting to prevent concurrent calls from
9449
+ * double-finalizing the same draft. If the draft produces a message ID, stores it as a
9450
+ * `FinalizedDraft` so `stripPattern` (e.g. TTS block removal) can still edit the message
9451
+ * after it has been sent.
9105
9452
  */
9106
9453
  async finalize(sessionId, _assistantSessionId) {
9107
9454
  const draft = this.drafts.get(sessionId);
@@ -9128,6 +9475,12 @@ var init_draft_manager = __esm({
9128
9475
  await finalized.draft.stripPattern(pattern);
9129
9476
  }
9130
9477
  }
9478
+ /**
9479
+ * Discard all draft state for a session without sending anything.
9480
+ *
9481
+ * Removes the active draft, text buffer, and finalized draft reference. Called when a
9482
+ * session ends or is reset and any unsent content should be silently dropped.
9483
+ */
9131
9484
  cleanup(sessionId) {
9132
9485
  this.drafts.delete(sessionId);
9133
9486
  this.textBuffers.delete(sessionId);
@@ -9146,14 +9499,19 @@ var init_skill_command_manager = __esm({
9146
9499
  init_log();
9147
9500
  log34 = createChildLogger({ module: "skill-commands" });
9148
9501
  SkillCommandManager = class {
9149
- // sessionId → pinned msgId
9150
9502
  constructor(bot, chatId, sendQueue, sessionManager) {
9151
9503
  this.bot = bot;
9152
9504
  this.chatId = chatId;
9153
9505
  this.sendQueue = sendQueue;
9154
9506
  this.sessionManager = sessionManager;
9155
9507
  }
9508
+ // sessionId → Telegram message ID of the pinned skills message
9156
9509
  messages = /* @__PURE__ */ new Map();
9510
+ /**
9511
+ * Send or update the pinned skill commands message for a session.
9512
+ * Creates a new pinned message if none exists; edits the existing one otherwise.
9513
+ * Passing an empty `commands` array removes the pinned message.
9514
+ */
9157
9515
  async send(sessionId, threadId, commands) {
9158
9516
  if (!this.messages.has(sessionId)) {
9159
9517
  const record = this.sessionManager.getSessionRecord(sessionId);
@@ -9534,7 +9892,11 @@ var init_adapter = __esm({
9534
9892
  _topicsInitialized = false;
9535
9893
  /** Background watcher timer — cancelled on stop() or when topics succeed */
9536
9894
  _prerequisiteWatcher = null;
9537
- /** Store control message ID in memory + persist to session record */
9895
+ /**
9896
+ * Persist the control message ID both in-memory and to the session record.
9897
+ * The control message is the pinned status card with bypass/TTS buttons; its ID
9898
+ * is needed after a restart to edit it when config changes.
9899
+ */
9538
9900
  storeControlMsgId(sessionId, msgId) {
9539
9901
  this.controlMsgIds.set(sessionId, msgId);
9540
9902
  const record = this.core.sessionManager.getSessionRecord(sessionId);
@@ -9599,6 +9961,12 @@ var init_adapter = __esm({
9599
9961
  this.telegramConfig = config;
9600
9962
  this.saveTopicIds = saveTopicIds;
9601
9963
  }
9964
+ /**
9965
+ * Set up the grammY bot, register all callback and message handlers, then perform
9966
+ * two-phase startup: Phase 1 starts polling immediately; Phase 2 checks group
9967
+ * prerequisites (bot is admin, topics are enabled) and creates/restores system topics.
9968
+ * If prerequisites are not met, a background watcher retries until they are.
9969
+ */
9602
9970
  async start() {
9603
9971
  this.bot = new Bot(this.telegramConfig.botToken, {
9604
9972
  client: {
@@ -10029,6 +10397,13 @@ OpenACP will automatically retry until this is resolved.`;
10029
10397
  };
10030
10398
  this._prerequisiteWatcher = setTimeout(retry, schedule[0]);
10031
10399
  }
10400
+ /**
10401
+ * Tear down the bot and release all associated resources.
10402
+ *
10403
+ * Cancels the background prerequisite watcher, destroys all per-session activity
10404
+ * trackers (which hold interval timers), removes eventBus listeners, clears the
10405
+ * send queue, and stops the grammY bot polling loop.
10406
+ */
10032
10407
  async stop() {
10033
10408
  if (this._prerequisiteWatcher !== null) {
10034
10409
  clearTimeout(this._prerequisiteWatcher);
@@ -10162,8 +10537,7 @@ ${lines.join("\n")}`;
10162
10537
  await this.draftManager.finalize(sessionId, assistantSession?.id);
10163
10538
  }
10164
10539
  if (sessionId) {
10165
- const tracker = this.sessionTrackers.get(sessionId);
10166
- if (tracker) await tracker.onNewPrompt();
10540
+ await this.drainAndResetTracker(sessionId);
10167
10541
  }
10168
10542
  ctx.replyWithChatAction("typing").catch(() => {
10169
10543
  });
@@ -10252,9 +10626,26 @@ ${lines.join("\n")}`;
10252
10626
  * its creation event. This queue ensures events are processed in the order they arrive.
10253
10627
  */
10254
10628
  _dispatchQueues = /* @__PURE__ */ new Map();
10629
+ /**
10630
+ * Drain pending event dispatches from the previous prompt, then reset the
10631
+ * activity tracker so late tool_call events don't leak into the new card.
10632
+ */
10633
+ async drainAndResetTracker(sessionId) {
10634
+ const pendingDispatch = this._dispatchQueues.get(sessionId);
10635
+ if (pendingDispatch) await pendingDispatch;
10636
+ const tracker = this.sessionTrackers.get(sessionId);
10637
+ if (tracker) await tracker.onNewPrompt();
10638
+ }
10255
10639
  getTracer(sessionId) {
10256
10640
  return this.core.sessionManager.getSession(sessionId)?.agentInstance?.debugTracer ?? null;
10257
10641
  }
10642
+ /**
10643
+ * Primary outbound dispatch method — routes an agent message to the session's Telegram topic.
10644
+ *
10645
+ * Wraps the base class `sendMessage` in a per-session promise chain (`_dispatchQueues`)
10646
+ * so that concurrent events fired from SessionBridge are serialized and delivered in the
10647
+ * order they arrive, preventing fast handlers from overtaking slower ones.
10648
+ */
10258
10649
  async sendMessage(sessionId, content) {
10259
10650
  const session = this.core.sessionManager.getSession(sessionId);
10260
10651
  if (!session) return;
@@ -10568,6 +10959,11 @@ Task completed.
10568
10959
  )
10569
10960
  );
10570
10961
  }
10962
+ /**
10963
+ * Render a PermissionRequest as an inline keyboard in the session topic and
10964
+ * notify the Notifications topic. Runs inside a sendQueue item, so
10965
+ * notification is fire-and-forget to avoid deadlock.
10966
+ */
10571
10967
  async sendPermissionRequest(sessionId, request) {
10572
10968
  this.getTracer(sessionId)?.log("telegram", { action: "permission:send", sessionId, requestId: request.id, description: request.description });
10573
10969
  log36.info({ sessionId, requestId: request.id }, "Permission request sent");
@@ -10577,6 +10973,11 @@ Task completed.
10577
10973
  () => this.permissionHandler.sendPermissionRequest(session, request)
10578
10974
  );
10579
10975
  }
10976
+ /**
10977
+ * Post a notification to the Notifications topic.
10978
+ * Assistant session notifications are suppressed — the assistant topic is
10979
+ * the user's primary interface and does not need a separate alert.
10980
+ */
10580
10981
  async sendNotification(notification) {
10581
10982
  this.getTracer(notification.sessionId)?.log("telegram", { action: "notification:send", sessionId: notification.sessionId, type: notification.type });
10582
10983
  if (notification.sessionId === this.core.assistantManager?.get("telegram")?.id) return;
@@ -10617,6 +11018,10 @@ Task completed.
10617
11018
  })
10618
11019
  );
10619
11020
  }
11021
+ /**
11022
+ * Create a new Telegram forum topic for a session and return its thread ID as a string.
11023
+ * Called by the core when a session is created via the API or CLI (not from the Telegram UI).
11024
+ */
10620
11025
  async createSessionThread(sessionId, name) {
10621
11026
  this.getTracer(sessionId)?.log("telegram", { action: "thread:create", sessionId, name });
10622
11027
  log36.info({ sessionId, name }, "Session topic created");
@@ -10624,6 +11029,10 @@ Task completed.
10624
11029
  await createSessionTopic(this.bot, this.telegramConfig.chatId, name)
10625
11030
  );
10626
11031
  }
11032
+ /**
11033
+ * Rename the forum topic for a session and update the session record's display name.
11034
+ * No-ops silently if the session doesn't have a threadId yet (e.g. still initializing).
11035
+ */
10627
11036
  async renameSessionThread(sessionId, newName) {
10628
11037
  this.getTracer(sessionId)?.log("telegram", { action: "thread:rename", sessionId, newName });
10629
11038
  const session = this.core.sessionManager.getSession(sessionId);
@@ -10641,6 +11050,7 @@ Task completed.
10641
11050
  );
10642
11051
  await this.core.sessionManager.patchRecord(sessionId, { name: newName });
10643
11052
  }
11053
+ /** Delete the forum topic associated with a session. */
10644
11054
  async deleteSessionThread(sessionId) {
10645
11055
  const record = this.core.sessionManager.getSessionRecord(sessionId);
10646
11056
  const platform2 = record?.platform;
@@ -10655,6 +11065,11 @@ Task completed.
10655
11065
  );
10656
11066
  }
10657
11067
  }
11068
+ /**
11069
+ * Display or update the pinned skill commands message for a session.
11070
+ * If the session's threadId is not yet set (e.g. session created from API),
11071
+ * the commands are queued and flushed once the thread becomes available.
11072
+ */
10658
11073
  async sendSkillCommands(sessionId, commands) {
10659
11074
  if (sessionId === this.core.assistantManager?.get("telegram")?.id) return;
10660
11075
  const session = this.core.sessionManager.getSession(sessionId);
@@ -10739,7 +11154,10 @@ Task completed.
10739
11154
  return;
10740
11155
  }
10741
11156
  const sid = await this.resolveSessionId(threadId);
10742
- if (sid) await this.draftManager.finalize(sid, this.core.assistantManager?.get("telegram")?.id);
11157
+ if (sid) {
11158
+ await this.draftManager.finalize(sid, this.core.assistantManager?.get("telegram")?.id);
11159
+ await this.drainAndResetTracker(sid);
11160
+ }
10743
11161
  this.core.handleMessage({
10744
11162
  channelId: "telegram",
10745
11163
  threadId: String(threadId),
@@ -10748,10 +11166,24 @@ Task completed.
10748
11166
  attachments: [att]
10749
11167
  }).catch((err) => log36.error({ err }, "handleMessage error"));
10750
11168
  }
11169
+ /**
11170
+ * Remove skill slash commands from the Telegram bot command list for a session.
11171
+ *
11172
+ * Clears any queued pending commands that hadn't been sent yet, then delegates
11173
+ * to `SkillCommandManager` to delete the commands from the Telegram API. Called
11174
+ * when a session with registered skill commands ends.
11175
+ */
10751
11176
  async cleanupSkillCommands(sessionId) {
10752
11177
  this._pendingSkillCommands.delete(sessionId);
10753
11178
  await this.skillManager.cleanup(sessionId);
10754
11179
  }
11180
+ /**
11181
+ * Clean up all adapter state associated with a session.
11182
+ *
11183
+ * Finalizes and discards any in-flight draft, destroys the activity tracker
11184
+ * (stopping ThinkingIndicator timers and finalizing any open ToolCard), and
11185
+ * clears pending skill commands. Called when a session ends or is reset.
11186
+ */
10755
11187
  async cleanupSessionState(sessionId) {
10756
11188
  this._pendingSkillCommands.delete(sessionId);
10757
11189
  await this.draftManager.finalize(sessionId, this.core.assistantManager?.get("telegram")?.id);
@@ -10762,9 +11194,22 @@ Task completed.
10762
11194
  this.sessionTrackers.delete(sessionId);
10763
11195
  }
10764
11196
  }
11197
+ /**
11198
+ * Remove `[TTS]...[/TTS]` blocks from the active or finalized draft for a session.
11199
+ *
11200
+ * The agent embeds these blocks so the speech plugin can extract the TTS text, but
11201
+ * they should never appear in the chat message. Called after TTS audio has been sent.
11202
+ */
10765
11203
  async stripTTSBlock(sessionId) {
10766
11204
  await this.draftManager.stripPattern(sessionId, /\[TTS\][\s\S]*?\[\/TTS\]/g);
10767
11205
  }
11206
+ /**
11207
+ * Archive a session by deleting its forum topic.
11208
+ *
11209
+ * Sets `session.archiving = true` to suppress any outgoing messages while the
11210
+ * topic is being torn down, finalizes pending drafts, cleans up all trackers,
11211
+ * then deletes the Telegram topic (which removes all messages).
11212
+ */
10768
11213
  async archiveSessionTopic(sessionId) {
10769
11214
  this.getTracer(sessionId)?.log("telegram", { action: "thread:archive", sessionId });
10770
11215
  const core = this.core;
@@ -10865,12 +11310,14 @@ var StderrCapture = class {
10865
11310
  this.maxLines = maxLines;
10866
11311
  }
10867
11312
  lines = [];
11313
+ /** Append a chunk of stderr output, splitting on newlines and trimming to maxLines. */
10868
11314
  append(chunk) {
10869
11315
  this.lines.push(...chunk.split("\n").filter(Boolean));
10870
11316
  if (this.lines.length > this.maxLines) {
10871
11317
  this.lines = this.lines.slice(-this.maxLines);
10872
11318
  }
10873
11319
  }
11320
+ /** Return all captured lines joined as a single string. */
10874
11321
  getLastLines() {
10875
11322
  return this.lines.join("\n");
10876
11323
  }
@@ -10891,13 +11338,18 @@ import fs4 from "fs";
10891
11338
  import path4 from "path";
10892
11339
  import ignore from "ignore";
10893
11340
  var DEFAULT_DENY_PATTERNS = [
11341
+ // Environment files — contain API keys, database URLs, etc.
10894
11342
  ".env",
10895
11343
  ".env.*",
11344
+ // Cryptographic keys
10896
11345
  "*.key",
10897
11346
  "*.pem",
11347
+ // SSH and cloud credentials
10898
11348
  ".ssh/",
10899
11349
  ".aws/",
11350
+ // OpenACP workspace — contains bot tokens and secrets
10900
11351
  ".openacp/",
11352
+ // Generic credential/secret files
10901
11353
  "**/credentials*",
10902
11354
  "**/secrets*",
10903
11355
  "**/*.secret"
@@ -10925,6 +11377,20 @@ var PathGuard = class {
10925
11377
  this.ig.add(options.ignorePatterns);
10926
11378
  }
10927
11379
  }
11380
+ /**
11381
+ * Checks whether an agent is allowed to access the given path.
11382
+ *
11383
+ * Validation order:
11384
+ * 1. Write to .openacpignore is always blocked (prevents agents from weakening their own restrictions)
11385
+ * 2. Path must be within cwd or an explicitly allowlisted path
11386
+ * 3. If within cwd but not allowlisted, path must not match any deny pattern
11387
+ *
11388
+ * @param targetPath - The path the agent is attempting to access.
11389
+ * @param operation - The operation type. Write operations are subject to stricter
11390
+ * restrictions than reads — specifically, writing to `.openacpignore` is blocked
11391
+ * (to prevent agents from weakening their own restrictions), while reading it is allowed.
11392
+ * @returns `{ allowed: true }` or `{ allowed: false, reason: "..." }`
11393
+ */
10928
11394
  validatePath(targetPath, operation) {
10929
11395
  const resolved = path4.resolve(targetPath);
10930
11396
  let realPath;
@@ -10960,6 +11426,7 @@ var PathGuard = class {
10960
11426
  }
10961
11427
  return { allowed: true, reason: "" };
10962
11428
  }
11429
+ /** Adds an additional allowed path at runtime (e.g. for file-service uploads). */
10963
11430
  addAllowedPath(p) {
10964
11431
  try {
10965
11432
  this.allowedPaths.push(fs4.realpathSync(path4.resolve(p)));
@@ -10967,6 +11434,10 @@ var PathGuard = class {
10967
11434
  this.allowedPaths.push(path4.resolve(p));
10968
11435
  }
10969
11436
  }
11437
+ /**
11438
+ * Loads additional deny patterns from .openacpignore in the workspace root.
11439
+ * Follows .gitignore syntax — blank lines and lines starting with # are skipped.
11440
+ */
10970
11441
  static loadIgnoreFile(cwd) {
10971
11442
  const ignorePath = path4.join(cwd, ".openacpignore");
10972
11443
  try {
@@ -10980,6 +11451,7 @@ var PathGuard = class {
10980
11451
 
10981
11452
  // src/core/security/env-filter.ts
10982
11453
  var DEFAULT_ENV_WHITELIST = [
11454
+ // Shell basics — agents need these to resolve commands and write temp files
10983
11455
  "PATH",
10984
11456
  "HOME",
10985
11457
  "SHELL",
@@ -10992,11 +11464,11 @@ var DEFAULT_ENV_WHITELIST = [
10992
11464
  "XDG_*",
10993
11465
  "NODE_ENV",
10994
11466
  "EDITOR",
10995
- // Git
11467
+ // Git — agents need git config and SSH access for code operations
10996
11468
  "GIT_*",
10997
11469
  "SSH_AUTH_SOCK",
10998
11470
  "SSH_AGENT_PID",
10999
- // Terminal rendering
11471
+ // Terminal rendering — ensures correct color output in agent responses
11000
11472
  "COLORTERM",
11001
11473
  "FORCE_COLOR",
11002
11474
  "NO_COLOR",
@@ -11031,6 +11503,7 @@ var TypedEmitter = class _TypedEmitter {
11031
11503
  listeners = /* @__PURE__ */ new Map();
11032
11504
  paused = false;
11033
11505
  buffer = [];
11506
+ /** Register a listener for the given event. Returns `this` for chaining. */
11034
11507
  on(event, listener) {
11035
11508
  let set = this.listeners.get(event);
11036
11509
  if (!set) {
@@ -11040,10 +11513,17 @@ var TypedEmitter = class _TypedEmitter {
11040
11513
  set.add(listener);
11041
11514
  return this;
11042
11515
  }
11516
+ /** Remove a specific listener for the given event. */
11043
11517
  off(event, listener) {
11044
11518
  this.listeners.get(event)?.delete(listener);
11045
11519
  return this;
11046
11520
  }
11521
+ /**
11522
+ * Emit an event to all registered listeners.
11523
+ *
11524
+ * When paused, events are buffered (up to MAX_BUFFER_SIZE) unless
11525
+ * the passthrough filter allows them through immediately.
11526
+ */
11047
11527
  emit(event, ...args) {
11048
11528
  if (this.paused) {
11049
11529
  if (this.passthroughFn?.(event, args)) {
@@ -11087,6 +11567,7 @@ var TypedEmitter = class _TypedEmitter {
11087
11567
  get bufferSize() {
11088
11568
  return this.buffer.length;
11089
11569
  }
11570
+ /** Remove all listeners for a specific event, or all events if none specified. */
11090
11571
  removeAllListeners(event) {
11091
11572
  if (event) {
11092
11573
  this.listeners.delete(event);
@@ -11094,6 +11575,7 @@ var TypedEmitter = class _TypedEmitter {
11094
11575
  this.listeners.clear();
11095
11576
  }
11096
11577
  }
11578
+ /** Deliver an event to listeners, isolating errors so one broken listener doesn't break others. */
11097
11579
  deliver(event, args) {
11098
11580
  const set = this.listeners.get(event);
11099
11581
  if (!set) return;
@@ -11158,6 +11640,11 @@ var TerminalManager = class {
11158
11640
  constructor(maxOutputBytes = 1024 * 1024) {
11159
11641
  this.maxOutputBytes = maxOutputBytes;
11160
11642
  }
11643
+ /**
11644
+ * Spawn a new terminal process. Runs terminal:beforeCreate middleware first
11645
+ * (which can modify command/args/env or block creation entirely).
11646
+ * Returns a terminalId for subsequent output/wait/kill operations.
11647
+ */
11161
11648
  async createTerminal(sessionId, params, middlewareChain) {
11162
11649
  let termCommand = params.command;
11163
11650
  let termArgs = params.args ?? [];
@@ -11239,6 +11726,7 @@ var TerminalManager = class {
11239
11726
  });
11240
11727
  return { terminalId };
11241
11728
  }
11729
+ /** Retrieve accumulated stdout/stderr output for a terminal. */
11242
11730
  getOutput(terminalId) {
11243
11731
  const state = this.terminals.get(terminalId);
11244
11732
  if (!state) {
@@ -11253,6 +11741,7 @@ var TerminalManager = class {
11253
11741
  } : void 0
11254
11742
  };
11255
11743
  }
11744
+ /** Block until the terminal process exits, returning exit code and signal. */
11256
11745
  async waitForExit(terminalId) {
11257
11746
  const state = this.terminals.get(terminalId);
11258
11747
  if (!state) {
@@ -11276,6 +11765,7 @@ var TerminalManager = class {
11276
11765
  }
11277
11766
  });
11278
11767
  }
11768
+ /** Send SIGTERM to a terminal process (graceful shutdown). */
11279
11769
  kill(terminalId) {
11280
11770
  const state = this.terminals.get(terminalId);
11281
11771
  if (!state) {
@@ -11283,6 +11773,7 @@ var TerminalManager = class {
11283
11773
  }
11284
11774
  state.process.kill("SIGTERM");
11285
11775
  }
11776
+ /** Force-kill (SIGKILL) and immediately remove a terminal from the registry. */
11286
11777
  release(terminalId) {
11287
11778
  const state = this.terminals.get(terminalId);
11288
11779
  if (!state) {
@@ -11291,6 +11782,7 @@ var TerminalManager = class {
11291
11782
  state.process.kill("SIGKILL");
11292
11783
  this.terminals.delete(terminalId);
11293
11784
  }
11785
+ /** Force-kill all terminals. Used during session/system teardown. */
11294
11786
  destroyAll() {
11295
11787
  for (const [, t] of this.terminals) {
11296
11788
  t.process.kill("SIGKILL");
@@ -11322,6 +11814,12 @@ var DebugTracer = class {
11322
11814
  }
11323
11815
  dirCreated = false;
11324
11816
  logDir;
11817
+ /**
11818
+ * Write a timestamped JSONL entry to the trace file for the given layer.
11819
+ *
11820
+ * Handles circular references gracefully and silently swallows errors —
11821
+ * debug logging must never crash the application.
11822
+ */
11325
11823
  log(layer, data) {
11326
11824
  try {
11327
11825
  if (!this.dirCreated) {
@@ -11442,9 +11940,13 @@ var AgentInstance = class _AgentInstance extends TypedEmitter {
11442
11940
  connection;
11443
11941
  child;
11444
11942
  stderrCapture;
11943
+ /** Manages terminal subprocesses that agents can spawn for shell commands. */
11445
11944
  terminalManager = new TerminalManager();
11945
+ /** Shared across all instances — resolves MCP server configs for ACP sessions. */
11446
11946
  static mcpManager = new McpManager();
11947
+ /** Guards against emitting crash events during intentional shutdown. */
11447
11948
  _destroying = false;
11949
+ /** Restricts agent file I/O to the workspace directory and explicitly allowed paths. */
11448
11950
  pathGuard;
11449
11951
  sessionId;
11450
11952
  agentName;
@@ -11454,16 +11956,36 @@ var AgentInstance = class _AgentInstance extends TypedEmitter {
11454
11956
  initialSessionResponse;
11455
11957
  middlewareChain;
11456
11958
  debugTracer = null;
11457
- /** Allow external callers (e.g. SessionFactory) to whitelist additional read paths */
11959
+ /**
11960
+ * Whitelist an additional filesystem path for agent read access.
11961
+ *
11962
+ * Called by SessionFactory to allow agents to read files outside the
11963
+ * workspace (e.g., the file-service upload directory for attachments).
11964
+ */
11458
11965
  addAllowedPath(p) {
11459
11966
  this.pathGuard.addAllowedPath(p);
11460
11967
  }
11461
- // Callback — set by core when wiring events
11968
+ // Callback — set by Session/Core when wiring events. Returns the selected
11969
+ // permission option ID. Default no-op auto-selects the first option.
11462
11970
  onPermissionRequest = async () => "";
11463
11971
  constructor(agentName) {
11464
11972
  super();
11465
11973
  this.agentName = agentName;
11466
11974
  }
11975
+ /**
11976
+ * Spawn the agent child process and complete the ACP protocol handshake.
11977
+ *
11978
+ * Steps:
11979
+ * 1. Resolve the agent command to a directly executable path
11980
+ * 2. Create a PathGuard scoped to the working directory
11981
+ * 3. Spawn the subprocess with a filtered environment (security: only whitelisted
11982
+ * env vars are passed to prevent leaking secrets like API keys)
11983
+ * 4. Wire stdin/stdout through debug-tracing Transform streams
11984
+ * 5. Convert Node streams → Web streams for the ACP SDK
11985
+ * 6. Perform the ACP `initialize` handshake and negotiate capabilities
11986
+ *
11987
+ * Does NOT create a session — callers must follow up with newSession or loadSession.
11988
+ */
11467
11989
  static async spawnSubprocess(agentDef, workingDirectory, allowedPaths = []) {
11468
11990
  const instance = new _AgentInstance(agentDef.name);
11469
11991
  const resolved = resolveAgentCommand(agentDef.command);
@@ -11562,6 +12084,13 @@ var AgentInstance = class _AgentInstance extends TypedEmitter {
11562
12084
  );
11563
12085
  return instance;
11564
12086
  }
12087
+ /**
12088
+ * Monitor the subprocess for unexpected exits and emit error events.
12089
+ *
12090
+ * Distinguishes intentional shutdown (SIGTERM/SIGINT during destroy) from
12091
+ * crashes (non-zero exit code or unexpected signal). Crash events include
12092
+ * captured stderr output for diagnostic context.
12093
+ */
11565
12094
  setupCrashDetection() {
11566
12095
  this.child.on("exit", (code, signal) => {
11567
12096
  if (this._destroying) return;
@@ -11584,6 +12113,18 @@ ${stderr}`
11584
12113
  log4.debug({ sessionId: this.sessionId }, "ACP connection closed");
11585
12114
  });
11586
12115
  }
12116
+ /**
12117
+ * Spawn a new agent subprocess and create a fresh ACP session.
12118
+ *
12119
+ * This is the primary entry point for starting an agent. It spawns the
12120
+ * subprocess, completes the ACP handshake, and calls `newSession` to
12121
+ * initialize the agent's working context (cwd, MCP servers).
12122
+ *
12123
+ * @param agentDef - Agent definition (command, args, env) from the catalog
12124
+ * @param workingDirectory - Workspace root the agent operates in
12125
+ * @param mcpServers - Optional MCP server configs to extend agent capabilities
12126
+ * @param allowedPaths - Extra filesystem paths the agent may access
12127
+ */
11587
12128
  static async spawn(agentDef, workingDirectory, mcpServers, allowedPaths) {
11588
12129
  log4.debug(
11589
12130
  { agentName: agentDef.name, command: agentDef.command },
@@ -11617,6 +12158,15 @@ ${stderr}`
11617
12158
  );
11618
12159
  return instance;
11619
12160
  }
12161
+ /**
12162
+ * Spawn a new subprocess and restore an existing agent session.
12163
+ *
12164
+ * Tries loadSession first (preferred, stable API), falls back to the
12165
+ * unstable resumeSession, and finally falls back to creating a brand-new
12166
+ * session if resume fails entirely (e.g., agent lost its state).
12167
+ *
12168
+ * @param agentSessionId - The agent-side session ID to restore
12169
+ */
11620
12170
  static async resume(agentDef, workingDirectory, agentSessionId, mcpServers, allowedPaths) {
11621
12171
  log4.debug({ agentName: agentDef.name, agentSessionId }, "Resuming agent");
11622
12172
  const spawnStart = Date.now();
@@ -11683,12 +12233,26 @@ ${stderr}`
11683
12233
  instance.setupCrashDetection();
11684
12234
  return instance;
11685
12235
  }
11686
- // createClient — implemented in Task 6b
12236
+ /**
12237
+ * Build the ACP Client callback object.
12238
+ *
12239
+ * The ACP SDK invokes these callbacks when the agent sends notifications
12240
+ * or requests. Each callback maps an ACP protocol message to either:
12241
+ * - An internal AgentEvent (emitted for Session/adapters to consume)
12242
+ * - A filesystem or terminal operation (executed on the agent's behalf)
12243
+ * - A permission request (proxied to the user via the adapter)
12244
+ */
11687
12245
  createClient(_agent) {
11688
12246
  const self = this;
11689
12247
  const MAX_OUTPUT_BYTES = 1024 * 1024;
11690
12248
  return {
11691
12249
  // ── Session updates ──────────────────────────────────────────────────
12250
+ // The agent streams its response as a series of session update events.
12251
+ // Each event type maps to an internal AgentEvent that Session relays
12252
+ // to adapters for rendering (text chunks, tool calls, usage stats, etc.).
12253
+ // Chunks are forwarded to Session individually as they arrive — no buffering
12254
+ // happens at this layer. If buffering is needed (e.g., to avoid rate limits),
12255
+ // it is the responsibility of the Session or adapter layer.
11692
12256
  async sessionUpdate(params) {
11693
12257
  const update = params.update;
11694
12258
  let event = null;
@@ -11817,6 +12381,10 @@ ${stderr}`
11817
12381
  }
11818
12382
  },
11819
12383
  // ── Permission requests ──────────────────────────────────────────────
12384
+ // The agent needs user approval before performing sensitive operations
12385
+ // (e.g., file writes, shell commands). This proxies the request up
12386
+ // through Session → PermissionGate → adapter → user, then returns
12387
+ // the user's chosen option ID back to the agent.
11820
12388
  async requestPermission(params) {
11821
12389
  const permissionRequest = {
11822
12390
  id: params.toolCall.toolCallId,
@@ -11833,6 +12401,9 @@ ${stderr}`
11833
12401
  };
11834
12402
  },
11835
12403
  // ── File operations ──────────────────────────────────────────────────
12404
+ // The agent reads/writes files through these callbacks rather than
12405
+ // accessing the filesystem directly. This allows PathGuard to enforce
12406
+ // workspace boundaries and middleware hooks to intercept I/O.
11836
12407
  async readTextFile(params) {
11837
12408
  const p = params;
11838
12409
  const pathCheck = self.pathGuard.validatePath(p.path, "read");
@@ -11868,6 +12439,8 @@ ${stderr}`
11868
12439
  return {};
11869
12440
  },
11870
12441
  // ── Terminal operations (delegated to TerminalManager) ─────────────
12442
+ // Agents can spawn shell commands via terminal operations. TerminalManager
12443
+ // handles subprocess lifecycle, output capture, and byte-limit enforcement.
11871
12444
  async createTerminal(params) {
11872
12445
  return self.terminalManager.createTerminal(
11873
12446
  self.sessionId,
@@ -11897,6 +12470,13 @@ ${stderr}`
11897
12470
  };
11898
12471
  }
11899
12472
  // ── New ACP methods ──────────────────────────────────────────────────
12473
+ /**
12474
+ * Update a session config option (mode, model, etc.) on the agent.
12475
+ *
12476
+ * Falls back to legacy `setSessionMode`/`unstable_setSessionModel` methods
12477
+ * for agents that haven't adopted the unified `session/set_config_option`
12478
+ * ACP method (detected via JSON-RPC -32601 "Method Not Found" error).
12479
+ */
11900
12480
  async setConfigOption(configId, value) {
11901
12481
  try {
11902
12482
  return await this.connection.setSessionConfigOption({
@@ -11924,12 +12504,14 @@ ${stderr}`
11924
12504
  throw err;
11925
12505
  }
11926
12506
  }
12507
+ /** List the agent's known sessions, optionally filtered by working directory. */
11927
12508
  async listSessions(cwd, cursor) {
11928
12509
  return await this.connection.listSessions({
11929
12510
  cwd: cwd ?? null,
11930
12511
  cursor: cursor ?? null
11931
12512
  });
11932
12513
  }
12514
+ /** Load an existing agent session by ID into this subprocess. */
11933
12515
  async loadSession(sessionId, cwd, mcpServers) {
11934
12516
  const resolvedMcp = _AgentInstance.mcpManager.resolve(mcpServers);
11935
12517
  return await this.connection.loadSession({
@@ -11938,9 +12520,11 @@ ${stderr}`
11938
12520
  mcpServers: resolvedMcp
11939
12521
  });
11940
12522
  }
12523
+ /** Trigger agent-managed authentication (e.g., OAuth flow). */
11941
12524
  async authenticate(methodId) {
11942
12525
  await this.connection.authenticate({ methodId });
11943
12526
  }
12527
+ /** Fork an existing session, creating a new branch with shared history. */
11944
12528
  async forkSession(sessionId, cwd, mcpServers) {
11945
12529
  const resolvedMcp = _AgentInstance.mcpManager.resolve(mcpServers);
11946
12530
  return await this.connection.unstable_forkSession({
@@ -11949,10 +12533,25 @@ ${stderr}`
11949
12533
  mcpServers: resolvedMcp
11950
12534
  });
11951
12535
  }
12536
+ /** Close a session on the agent side (cleanup agent-internal state). */
11952
12537
  async closeSession(sessionId) {
11953
12538
  await this.connection.unstable_closeSession({ sessionId });
11954
12539
  }
11955
12540
  // ── Prompt & lifecycle ──────────────────────────────────────────────
12541
+ /**
12542
+ * Send a user prompt to the agent and wait for the complete response.
12543
+ *
12544
+ * Builds ACP content blocks from the text and any attachments (images
12545
+ * are base64-encoded if the agent supports them, otherwise appended as
12546
+ * file paths). The promise resolves when the agent finishes responding;
12547
+ * streaming events arrive via the `agent_event` emitter during execution.
12548
+ *
12549
+ * Attachments that exceed size limits or use unsupported formats are
12550
+ * skipped with a note appended to the prompt text.
12551
+ *
12552
+ * Call `cancel()` to interrupt a running prompt; the agent will stop and
12553
+ * the promise resolves with partial results.
12554
+ */
11956
12555
  async prompt(text3, attachments) {
11957
12556
  const contentBlocks = [{ type: "text", text: text3 }];
11958
12557
  const capabilities = this.promptCapabilities ?? {};
@@ -12002,9 +12601,18 @@ ${skipNote}`;
12002
12601
  prompt: contentBlocks
12003
12602
  });
12004
12603
  }
12604
+ /** Cancel the currently running prompt. The agent should stop and return partial results. */
12005
12605
  async cancel() {
12006
12606
  await this.connection.cancel({ sessionId: this.sessionId });
12007
12607
  }
12608
+ /**
12609
+ * Gracefully shut down the agent subprocess.
12610
+ *
12611
+ * Sends SIGTERM first, giving the agent up to 10 seconds to clean up.
12612
+ * If the process hasn't exited by then, SIGKILL forces termination.
12613
+ * The timer is unref'd so it doesn't keep the Node process alive
12614
+ * during shutdown.
12615
+ */
12008
12616
  async destroy() {
12009
12617
  this._destroying = true;
12010
12618
  this.terminalManager.destroyAll();
@@ -12051,6 +12659,7 @@ var AgentManager = class {
12051
12659
  constructor(catalog) {
12052
12660
  this.catalog = catalog;
12053
12661
  }
12662
+ /** Return definitions for all installed agents. */
12054
12663
  getAvailableAgents() {
12055
12664
  const installed = this.catalog.getInstalledEntries();
12056
12665
  return Object.entries(installed).map(([key, agent]) => ({
@@ -12060,14 +12669,25 @@ var AgentManager = class {
12060
12669
  env: agent.env
12061
12670
  }));
12062
12671
  }
12672
+ /** Look up a single agent definition by its short name (e.g., "claude", "gemini"). */
12063
12673
  getAgent(name) {
12064
12674
  return this.catalog.resolve(name);
12065
12675
  }
12676
+ /**
12677
+ * Spawn a new agent subprocess with a fresh session.
12678
+ *
12679
+ * @throws If the agent is not installed — includes install instructions in the error message.
12680
+ */
12066
12681
  async spawn(agentName, workingDirectory, allowedPaths) {
12067
12682
  const agentDef = this.getAgent(agentName);
12068
12683
  if (!agentDef) throw new Error(`Agent "${agentName}" is not installed. Run "openacp agents install ${agentName}" to add it.`);
12069
12684
  return AgentInstance.spawn(agentDef, workingDirectory, void 0, allowedPaths);
12070
12685
  }
12686
+ /**
12687
+ * Spawn a subprocess and resume an existing agent session.
12688
+ *
12689
+ * Falls back to a new session if the agent cannot restore the given session ID.
12690
+ */
12071
12691
  async resume(agentName, workingDirectory, agentSessionId, allowedPaths) {
12072
12692
  const agentDef = this.getAgent(agentName);
12073
12693
  if (!agentDef) throw new Error(`Agent "${agentName}" is not installed. Run "openacp agents install ${agentName}" to add it.`);
@@ -12089,6 +12709,11 @@ var PromptQueue = class {
12089
12709
  abortController = null;
12090
12710
  /** Set when abort is triggered; drainNext waits for the current processor to settle before starting the next item. */
12091
12711
  processorSettled = null;
12712
+ /**
12713
+ * Add a prompt to the queue. If no prompt is currently processing, it runs
12714
+ * immediately. Otherwise, it's buffered and the returned promise resolves
12715
+ * only after the prompt finishes processing.
12716
+ */
12092
12717
  async enqueue(text3, attachments, routing, turnId) {
12093
12718
  if (this.processing) {
12094
12719
  return new Promise((resolve7) => {
@@ -12097,6 +12722,7 @@ var PromptQueue = class {
12097
12722
  }
12098
12723
  await this.process(text3, attachments, routing, turnId);
12099
12724
  }
12725
+ /** Run a single prompt through the processor, then drain the next queued item. */
12100
12726
  async process(text3, attachments, routing, turnId) {
12101
12727
  this.processing = true;
12102
12728
  this.abortController = new AbortController();
@@ -12124,12 +12750,17 @@ var PromptQueue = class {
12124
12750
  this.drainNext();
12125
12751
  }
12126
12752
  }
12753
+ /** Dequeue and process the next pending prompt, if any. Called after each prompt completes. */
12127
12754
  drainNext() {
12128
12755
  const next = this.queue.shift();
12129
12756
  if (next) {
12130
12757
  this.process(next.text, next.attachments, next.routing, next.turnId).then(next.resolve);
12131
12758
  }
12132
12759
  }
12760
+ /**
12761
+ * Abort the in-flight prompt and discard all queued prompts.
12762
+ * Pending promises are resolved (not rejected) so callers don't see unhandled rejections.
12763
+ */
12133
12764
  clear() {
12134
12765
  if (this.abortController) {
12135
12766
  this.abortController.abort();
@@ -12159,6 +12790,10 @@ var PermissionGate = class {
12159
12790
  constructor(timeoutMs) {
12160
12791
  this.timeoutMs = timeoutMs ?? DEFAULT_TIMEOUT_MS;
12161
12792
  }
12793
+ /**
12794
+ * Register a new permission request and return a promise that resolves with the
12795
+ * chosen option ID when the user responds, or rejects on timeout / supersession.
12796
+ */
12162
12797
  setPending(request) {
12163
12798
  if (!this.settled && this.rejectFn) {
12164
12799
  this.rejectFn(new Error("Superseded by new permission request"));
@@ -12177,6 +12812,7 @@ var PermissionGate = class {
12177
12812
  }
12178
12813
  });
12179
12814
  }
12815
+ /** Approve the pending request with the given option ID. No-op if already settled. */
12180
12816
  resolve(optionId) {
12181
12817
  if (this.settled || !this.resolveFn) return;
12182
12818
  this.settled = true;
@@ -12184,6 +12820,7 @@ var PermissionGate = class {
12184
12820
  this.resolveFn(optionId);
12185
12821
  this.cleanup();
12186
12822
  }
12823
+ /** Deny the pending request. No-op if already settled. */
12187
12824
  reject(reason) {
12188
12825
  if (this.settled || !this.rejectFn) return;
12189
12826
  this.settled = true;
@@ -12277,6 +12914,8 @@ var Session = class extends TypedEmitter {
12277
12914
  get agentInstance() {
12278
12915
  return this._agentInstance;
12279
12916
  }
12917
+ /** Setting agentInstance wires the agent→session event relay and commands buffer.
12918
+ * This happens both at construction and on agent switch (switchAgent). */
12280
12919
  set agentInstance(agent) {
12281
12920
  this._agentInstance = agent;
12282
12921
  this.wireAgentRelay();
@@ -12379,7 +13018,7 @@ var Session = class extends TypedEmitter {
12379
13018
  this.transition("finished");
12380
13019
  this.emit(SessionEv.SESSION_END, reason ?? "completed");
12381
13020
  }
12382
- /** Transition to cancelled — from active only (terminal session cancel) */
13021
+ /** Transition to cancelled — from active or error (terminal session cancel) */
12383
13022
  markCancelled() {
12384
13023
  this.transition("cancelled");
12385
13024
  }
@@ -12399,19 +13038,29 @@ var Session = class extends TypedEmitter {
12399
13038
  get queueDepth() {
12400
13039
  return this.queue.pending;
12401
13040
  }
13041
+ /** Whether a prompt is currently being processed by the agent */
12402
13042
  get promptRunning() {
12403
13043
  return this.queue.isProcessing;
12404
13044
  }
12405
13045
  // --- Context Injection ---
13046
+ /** Store context markdown to be prepended to the next prompt (used for session resume with history). */
12406
13047
  setContext(markdown) {
12407
13048
  this.pendingContext = markdown;
12408
13049
  }
12409
13050
  // --- Voice Mode ---
13051
+ /** Set TTS mode: "off" = disabled, "next" = one-shot (auto-resets after prompt), "on" = persistent. */
12410
13052
  setVoiceMode(mode) {
12411
13053
  this.voiceMode = mode;
12412
13054
  this.log.info({ voiceMode: mode }, "TTS mode changed");
12413
13055
  }
12414
13056
  // --- Public API ---
13057
+ /**
13058
+ * Enqueue a user prompt for serial processing.
13059
+ *
13060
+ * Runs the prompt through agent:beforePrompt middleware (which can modify or block),
13061
+ * then adds it to the PromptQueue. Returns a turnId that callers can use to correlate
13062
+ * queued/processing events before the prompt actually runs.
13063
+ */
12415
13064
  async enqueuePrompt(text3, attachments, routing, externalTurnId) {
12416
13065
  const turnId = externalTurnId ?? nanoid2(8);
12417
13066
  if (this.middlewareChain) {
@@ -12521,6 +13170,10 @@ ${text3}`;
12521
13170
  await this.autoName();
12522
13171
  }
12523
13172
  }
13173
+ /**
13174
+ * Transcribe audio attachments to text if the agent doesn't support audio natively.
13175
+ * Audio attachments are removed and their transcriptions are appended to the prompt text.
13176
+ */
12524
13177
  async maybeTranscribeAudio(text3, attachments) {
12525
13178
  if (!attachments?.length || !this.speechService) {
12526
13179
  return { text: text3, attachments };
@@ -12566,6 +13219,7 @@ ${result.text}` : result.text;
12566
13219
  attachments: remainingAttachments.length > 0 ? remainingAttachments : void 0
12567
13220
  };
12568
13221
  }
13222
+ /** Extract [TTS] block from agent response, synthesize speech, and emit audio_content event. */
12569
13223
  async processTTSResponse(responseText) {
12570
13224
  const match = TTS_BLOCK_REGEX.exec(responseText);
12571
13225
  if (!match?.[1]) {
@@ -12602,7 +13256,9 @@ ${result.text}` : result.text;
12602
13256
  this.log.warn({ err }, "TTS synthesis failed, skipping");
12603
13257
  }
12604
13258
  }
12605
- // NOTE: This injects a summary prompt into the agent's conversation history.
13259
+ // Sends a special prompt to the agent to generate a short session title.
13260
+ // The session emitter is paused (excluding non-agent_event emissions) so the naming
13261
+ // prompt's output is intercepted by a capture handler instead of being forwarded to adapters.
12606
13262
  async autoName() {
12607
13263
  let title = "";
12608
13264
  const captureHandler = (event) => {
@@ -12761,6 +13417,7 @@ ${result.text}` : result.text;
12761
13417
  this.applySpawnResponse(newAgent.initialSessionResponse, newAgent.agentCapabilities);
12762
13418
  this.log.info({ from: this.agentSwitchHistory.at(-1).agentName, to: agentName }, "Agent switched");
12763
13419
  }
13420
+ /** Tear down the session: reject pending permissions, clear queue, destroy agent subprocess. */
12764
13421
  async destroy() {
12765
13422
  this.log.info("Session destroyed");
12766
13423
  if (this.permissionGate.isPending) {
@@ -13055,6 +13712,12 @@ var MessageTransformer = class {
13055
13712
  constructor(tunnelService) {
13056
13713
  this.tunnelService = tunnelService;
13057
13714
  }
13715
+ /**
13716
+ * Convert an agent event to an outgoing message for adapter delivery.
13717
+ *
13718
+ * For tool events, enriches the metadata with diff stats and viewer links
13719
+ * when a tunnel service is available.
13720
+ */
13058
13721
  transform(event, sessionContext) {
13059
13722
  switch (event.type) {
13060
13723
  case "text":
@@ -13285,12 +13948,17 @@ var SessionManager = class {
13285
13948
  store;
13286
13949
  eventBus;
13287
13950
  middlewareChain;
13951
+ /**
13952
+ * Inject the EventBus after construction. Deferred because EventBus is created
13953
+ * after SessionManager during bootstrap, so it cannot be passed to the constructor.
13954
+ */
13288
13955
  setEventBus(eventBus) {
13289
13956
  this.eventBus = eventBus;
13290
13957
  }
13291
13958
  constructor(store = null) {
13292
13959
  this.store = store;
13293
13960
  }
13961
+ /** Create a new session by spawning an agent and persisting the initial record. */
13294
13962
  async createSession(channelId, agentName, workingDirectory, agentManager) {
13295
13963
  const agentInstance = await agentManager.spawn(agentName, workingDirectory);
13296
13964
  const session = new Session({
@@ -13318,9 +13986,11 @@ var SessionManager = class {
13318
13986
  }
13319
13987
  return session;
13320
13988
  }
13989
+ /** Look up a live session by its OpenACP session ID. */
13321
13990
  getSession(sessionId) {
13322
13991
  return this.sessions.get(sessionId);
13323
13992
  }
13993
+ /** Look up a live session by adapter channel and thread ID (checks per-adapter threadIds map first, then legacy fields). */
13324
13994
  getSessionByThread(channelId, threadId) {
13325
13995
  for (const session of this.sessions.values()) {
13326
13996
  const adapterThread = session.threadIds.get(channelId);
@@ -13331,6 +14001,7 @@ var SessionManager = class {
13331
14001
  }
13332
14002
  return void 0;
13333
14003
  }
14004
+ /** Look up a live session by the agent's internal session ID (assigned by the ACP subprocess). */
13334
14005
  getSessionByAgentSessionId(agentSessionId) {
13335
14006
  for (const session of this.sessions.values()) {
13336
14007
  if (session.agentSessionId === agentSessionId) {
@@ -13339,18 +14010,26 @@ var SessionManager = class {
13339
14010
  }
13340
14011
  return void 0;
13341
14012
  }
14013
+ /** Look up the persisted SessionRecord by the agent's internal session ID. */
13342
14014
  getRecordByAgentSessionId(agentSessionId) {
13343
14015
  return this.store?.findByAgentSessionId(agentSessionId);
13344
14016
  }
14017
+ /** Look up the persisted SessionRecord by channel and thread ID. */
13345
14018
  getRecordByThread(channelId, threadId) {
13346
14019
  return this.store?.findByPlatform(
13347
14020
  channelId,
13348
14021
  (p) => String(p.topicId) === threadId || p.threadId === threadId
13349
14022
  );
13350
14023
  }
14024
+ /** Register a session that was created externally (e.g. restored from store on startup). */
13351
14025
  registerSession(session) {
13352
14026
  this.sessions.set(session.id, session);
13353
14027
  }
14028
+ /**
14029
+ * Merge a partial update into the stored SessionRecord. If no record exists yet and
14030
+ * the patch includes `sessionId`, it is treated as an initial save.
14031
+ * Pass `{ immediate: true }` to flush the store to disk synchronously.
14032
+ */
13354
14033
  async patchRecord(sessionId, patch, options) {
13355
14034
  if (!this.store) return;
13356
14035
  const record = this.store.get(sessionId);
@@ -13363,9 +14042,11 @@ var SessionManager = class {
13363
14042
  this.store.flush();
13364
14043
  }
13365
14044
  }
14045
+ /** Retrieve the persisted SessionRecord for a given session ID. Returns undefined if no store or record not found. */
13366
14046
  getSessionRecord(sessionId) {
13367
14047
  return this.store?.get(sessionId);
13368
14048
  }
14049
+ /** Cancel a session: abort in-flight prompt, transition to cancelled, destroy agent, and persist. */
13369
14050
  async cancelSession(sessionId) {
13370
14051
  const session = this.sessions.get(sessionId);
13371
14052
  if (session) {
@@ -13388,11 +14069,16 @@ var SessionManager = class {
13388
14069
  });
13389
14070
  }
13390
14071
  }
14072
+ /** List live (in-memory) sessions, optionally filtered by channel. Excludes assistant sessions. */
13391
14073
  listSessions(channelId) {
13392
14074
  const all = Array.from(this.sessions.values()).filter((s) => !s.isAssistant);
13393
14075
  if (channelId) return all.filter((s) => s.channelId === channelId);
13394
14076
  return all;
13395
14077
  }
14078
+ /**
14079
+ * List all sessions (live + stored) as SessionSummary. Live sessions take precedence
14080
+ * over stored records — their real-time state (queueDepth, promptRunning) is used.
14081
+ */
13396
14082
  listAllSessions(channelId) {
13397
14083
  if (this.store) {
13398
14084
  let records = this.store.list().filter((r) => !r.isAssistant);
@@ -13454,6 +14140,7 @@ var SessionManager = class {
13454
14140
  isLive: true
13455
14141
  }));
13456
14142
  }
14143
+ /** List all stored SessionRecords, optionally filtered by status. Excludes assistant sessions. */
13457
14144
  listRecords(filter) {
13458
14145
  if (!this.store) return [];
13459
14146
  let records = this.store.list().filter((r) => !r.isAssistant);
@@ -13462,6 +14149,7 @@ var SessionManager = class {
13462
14149
  }
13463
14150
  return records;
13464
14151
  }
14152
+ /** Remove a session's stored record and emit a SESSION_DELETED event. */
13465
14153
  async removeRecord(sessionId) {
13466
14154
  if (!this.store) return;
13467
14155
  await this.store.remove(sessionId);
@@ -13565,7 +14253,10 @@ var SessionBridge = class {
13565
14253
  log6.error({ err, sessionId }, "Error in sendMessage middleware");
13566
14254
  }
13567
14255
  }
13568
- /** Determine if this bridge should forward the given event based on turn routing. */
14256
+ /**
14257
+ * Determine if this bridge should forward the given event based on turn routing.
14258
+ * System events are always forwarded; turn events are routed only to the target adapter.
14259
+ */
13569
14260
  shouldForward(event) {
13570
14261
  if (isSystemEvent(event)) return true;
13571
14262
  const ctx = this.session.activeTurnContext;
@@ -13574,6 +14265,13 @@ var SessionBridge = class {
13574
14265
  if (target === null) return false;
13575
14266
  return this.adapterId === target;
13576
14267
  }
14268
+ /**
14269
+ * Subscribe to session events and start forwarding them to the adapter.
14270
+ *
14271
+ * Wires: agent events → adapter dispatch, permission UI, lifecycle persistence
14272
+ * (status changes, naming, prompt count), and EventBus notifications.
14273
+ * Also replays any commands or config options that arrived before the bridge connected.
14274
+ */
13577
14275
  connect() {
13578
14276
  if (this.connected) return;
13579
14277
  this.connected = true;
@@ -13650,6 +14348,7 @@ var SessionBridge = class {
13650
14348
  this.session.emit(SessionEv.AGENT_EVENT, { type: "config_option_update", options: this.session.configOptions });
13651
14349
  }
13652
14350
  }
14351
+ /** Unsubscribe all session event listeners and clean up adapter state. */
13653
14352
  disconnect() {
13654
14353
  if (!this.connected) return;
13655
14354
  this.connected = false;
@@ -13922,6 +14621,10 @@ var SessionFactory = class {
13922
14621
  get speechService() {
13923
14622
  return typeof this.speechServiceAccessor === "function" ? this.speechServiceAccessor() : this.speechServiceAccessor;
13924
14623
  }
14624
+ /**
14625
+ * Create a new Session: spawn agent → create Session instance → hydrate ACP state → register.
14626
+ * Runs session:beforeCreate middleware (which can modify params or block creation).
14627
+ */
13925
14628
  async create(params) {
13926
14629
  let createParams = params;
13927
14630
  if (this.middlewareChain) {
@@ -14105,6 +14808,11 @@ var SessionFactory = class {
14105
14808
  this.resumeLocks.set(sessionId, resumePromise);
14106
14809
  return resumePromise;
14107
14810
  }
14811
+ /**
14812
+ * Attempt to resume a session from disk when a message arrives on a thread with
14813
+ * no live session. Deduplicates concurrent resume attempts for the same thread
14814
+ * via resumeLocks to avoid spawning multiple agents.
14815
+ */
14108
14816
  async lazyResume(channelId, threadId) {
14109
14817
  const store = this.sessionStore;
14110
14818
  if (!store || !this.createFullSession) return null;
@@ -14208,6 +14916,7 @@ var SessionFactory = class {
14208
14916
  this.resumeLocks.set(lockKey, resumePromise);
14209
14917
  return resumePromise;
14210
14918
  }
14919
+ /** Create a brand-new session, resolving agent name and workspace from config if not provided. */
14211
14920
  async handleNewSession(channelId, agentName, workspacePath, options) {
14212
14921
  if (!this.configManager || !this.agentCatalog || !this.createFullSession) {
14213
14922
  throw new Error("SessionFactory not fully initialized");
@@ -14252,6 +14961,7 @@ var SessionFactory = class {
14252
14961
  record.workingDir
14253
14962
  );
14254
14963
  }
14964
+ /** Create a session and inject conversation context from a ContextProvider (e.g., history from a previous session). */
14255
14965
  async createSessionWithContext(params) {
14256
14966
  if (!this.createFullSession) throw new Error("SessionFactory not fully initialized");
14257
14967
  let contextResult = null;
@@ -14278,6 +14988,7 @@ var SessionFactory = class {
14278
14988
  }
14279
14989
  return { session, contextResult };
14280
14990
  }
14991
+ /** Wire session-level side effects: usage tracking (via EventBus) and tunnel cleanup on session end. */
14281
14992
  wireSideEffects(session, deps) {
14282
14993
  session.on(SessionEv.AGENT_EVENT, (event) => {
14283
14994
  if (event.type !== "usage") return;
@@ -14358,6 +15069,11 @@ var JsonFileSessionStore = class {
14358
15069
  }
14359
15070
  return void 0;
14360
15071
  }
15072
+ /**
15073
+ * Find a session by its ACP agent session ID.
15074
+ * Checks current, original, and historical agent session IDs (from agent switches)
15075
+ * since the agent session ID changes on each switch.
15076
+ */
14361
15077
  findByAgentSessionId(agentSessionId) {
14362
15078
  for (const record of this.records.values()) {
14363
15079
  if (record.agentSessionId === agentSessionId || record.originalAgentSessionId === agentSessionId) {
@@ -14402,6 +15118,7 @@ var JsonFileSessionStore = class {
14402
15118
  if (!fs9.existsSync(dir)) fs9.mkdirSync(dir, { recursive: true });
14403
15119
  fs9.writeFileSync(this.filePath, JSON.stringify(data, null, 2));
14404
15120
  }
15121
+ /** Clean up timers and process listeners. Call on shutdown to prevent leaks. */
14405
15122
  destroy() {
14406
15123
  if (this.debounceTimer) clearTimeout(this.debounceTimer);
14407
15124
  if (this.cleanupInterval) clearInterval(this.cleanupInterval);
@@ -14437,7 +15154,11 @@ var JsonFileSessionStore = class {
14437
15154
  }
14438
15155
  }
14439
15156
  }
14440
- /** Migrate old SessionRecord format to new multi-adapter format. */
15157
+ /**
15158
+ * Migrate old SessionRecord format to new multi-adapter format.
15159
+ * Converts single-adapter `platform` field to per-adapter `platforms` map,
15160
+ * and initializes `attachedAdapters` for records created before multi-adapter support.
15161
+ */
14441
15162
  migrateRecord(record) {
14442
15163
  if (!record.platforms && record.platform && typeof record.platform === "object") {
14443
15164
  const platformData = record.platform;
@@ -14450,6 +15171,7 @@ var JsonFileSessionStore = class {
14450
15171
  }
14451
15172
  return record;
14452
15173
  }
15174
+ /** Remove expired session records (past TTL). Active and assistant sessions are preserved. */
14453
15175
  cleanup() {
14454
15176
  const cutoff = Date.now() - this.ttlDays * 24 * 60 * 60 * 1e3;
14455
15177
  let removed = 0;
@@ -14492,7 +15214,12 @@ var AgentSwitchHandler = class {
14492
15214
  constructor(deps) {
14493
15215
  this.deps = deps;
14494
15216
  }
15217
+ /** Prevents concurrent switch operations on the same session */
14495
15218
  switchingLocks = /* @__PURE__ */ new Set();
15219
+ /**
15220
+ * Switch a session to a different agent. Returns whether the previous
15221
+ * agent session was resumed or a new one was spawned.
15222
+ */
14496
15223
  async switch(sessionId, toAgent) {
14497
15224
  if (this.switchingLocks.has(sessionId)) {
14498
15225
  throw new Error("Switch already in progress");
@@ -14677,20 +15404,29 @@ var REGISTRY_URL = "https://cdn.agentclientprotocol.com/registry/v1/latest/regis
14677
15404
  var DEFAULT_TTL_HOURS = 24;
14678
15405
  var AgentCatalog = class {
14679
15406
  store;
15407
+ /** Agents available in the remote registry (cached in memory after load). */
14680
15408
  registryAgents = [];
14681
15409
  cachePath;
15410
+ /** Directory where binary agent archives are extracted to. */
14682
15411
  agentsDir;
14683
15412
  constructor(store, cachePath, agentsDir) {
14684
15413
  this.store = store;
14685
15414
  this.cachePath = cachePath;
14686
15415
  this.agentsDir = agentsDir;
14687
15416
  }
15417
+ /**
15418
+ * Load installed agents from disk and hydrate the registry from cache/snapshot.
15419
+ *
15420
+ * Also enriches installed agents with registry metadata — fixes agents that
15421
+ * were migrated from older config formats with incomplete data.
15422
+ */
14688
15423
  load() {
14689
15424
  this.store.load();
14690
15425
  this.loadRegistryFromCacheOrSnapshot();
14691
15426
  this.enrichInstalledFromRegistry();
14692
15427
  }
14693
15428
  // --- Registry ---
15429
+ /** Fetch the latest agent registry from the CDN and update the local cache. */
14694
15430
  async fetchRegistry() {
14695
15431
  try {
14696
15432
  log11.info("Fetching agent registry from CDN...");
@@ -14710,6 +15446,7 @@ var AgentCatalog = class {
14710
15446
  log11.warn({ err }, "Failed to fetch registry, using cached data");
14711
15447
  }
14712
15448
  }
15449
+ /** Re-fetch registry only if the local cache has expired (24-hour TTL). */
14713
15450
  async refreshRegistryIfStale() {
14714
15451
  if (this.isCacheStale()) {
14715
15452
  await this.fetchRegistry();
@@ -14721,6 +15458,7 @@ var AgentCatalog = class {
14721
15458
  getRegistryAgent(registryId) {
14722
15459
  return this.registryAgents.find((a) => a.id === registryId);
14723
15460
  }
15461
+ /** Find a registry agent by registry ID or by its short alias (e.g., "claude"). */
14724
15462
  findRegistryAgent(keyOrId) {
14725
15463
  const byId = this.registryAgents.find((a) => a.id === keyOrId);
14726
15464
  if (byId) return byId;
@@ -14737,6 +15475,15 @@ var AgentCatalog = class {
14737
15475
  return this.store.getAgent(key);
14738
15476
  }
14739
15477
  // --- Discovery ---
15478
+ /**
15479
+ * Build the unified list of all agents (installed + registry-only).
15480
+ *
15481
+ * Installed agents appear first with their live availability status.
15482
+ * Registry agents that aren't installed yet show whether a distribution
15483
+ * exists for the current platform. Missing external dependencies
15484
+ * (e.g., claude CLI) are surfaced as `missingDeps` for UI display
15485
+ * but do NOT block installation.
15486
+ */
14740
15487
  getAvailable() {
14741
15488
  const installed = this.getInstalledEntries();
14742
15489
  const items = [];
@@ -14777,6 +15524,7 @@ var AgentCatalog = class {
14777
15524
  }
14778
15525
  return items;
14779
15526
  }
15527
+ /** Check if an agent can be installed on this system (platform + dependencies). */
14780
15528
  checkAvailability(keyOrId) {
14781
15529
  const agent = this.findRegistryAgent(keyOrId);
14782
15530
  if (!agent) return { available: false, reason: "Not found in the agent registry." };
@@ -14787,6 +15535,12 @@ var AgentCatalog = class {
14787
15535
  return checkDependencies(agent.id);
14788
15536
  }
14789
15537
  // --- Install/Uninstall ---
15538
+ /**
15539
+ * Install an agent from the registry.
15540
+ *
15541
+ * Resolves the distribution (npx/uvx/binary), downloads binary archives
15542
+ * if needed, and persists the agent definition in the store.
15543
+ */
14790
15544
  async install(keyOrId, progress, force) {
14791
15545
  const agent = this.findRegistryAgent(keyOrId);
14792
15546
  if (!agent) {
@@ -14811,6 +15565,7 @@ var AgentCatalog = class {
14811
15565
  registerFallbackAgent(key, data) {
14812
15566
  this.store.addAgent(key, data);
14813
15567
  }
15568
+ /** Remove an installed agent and delete its binary directory if applicable. */
14814
15569
  async uninstall(key) {
14815
15570
  if (this.store.hasAgent(key)) {
14816
15571
  await uninstallAgent(key, this.store);
@@ -14819,6 +15574,7 @@ var AgentCatalog = class {
14819
15574
  return { ok: false, error: `"${key}" is not installed.` };
14820
15575
  }
14821
15576
  // --- Resolution (for AgentManager) ---
15577
+ /** Convert an installed agent's short key to an AgentDefinition for spawning. */
14822
15578
  resolve(key) {
14823
15579
  const agent = this.store.getAgent(key);
14824
15580
  if (!agent) return void 0;
@@ -14968,6 +15724,7 @@ var AgentStore = class {
14968
15724
  constructor(filePath) {
14969
15725
  this.filePath = filePath;
14970
15726
  }
15727
+ /** Load and validate the store from disk. Starts fresh if file is missing or invalid. */
14971
15728
  load() {
14972
15729
  if (!fs13.existsSync(this.filePath)) {
14973
15730
  this.data = { version: 1, installed: {} };
@@ -15007,6 +15764,11 @@ var AgentStore = class {
15007
15764
  hasAgent(key) {
15008
15765
  return key in this.data.installed;
15009
15766
  }
15767
+ /**
15768
+ * Persist the store to disk using atomic write (write to .tmp, then rename).
15769
+ * File permissions are restricted to owner-only (0o600) since the store
15770
+ * may contain agent binary paths and environment variables.
15771
+ */
15010
15772
  save() {
15011
15773
  fs13.mkdirSync(path12.dirname(this.filePath), { recursive: true });
15012
15774
  const tmpPath = this.filePath + ".tmp";
@@ -15086,6 +15848,10 @@ function resolveLoadOrder(plugins) {
15086
15848
  // src/core/plugin/service-registry.ts
15087
15849
  var ServiceRegistry = class {
15088
15850
  services = /* @__PURE__ */ new Map();
15851
+ /**
15852
+ * Register a service. Throws if the service name is already taken.
15853
+ * Use `registerOverride` to intentionally replace an existing service.
15854
+ */
15089
15855
  register(name, implementation, pluginName) {
15090
15856
  if (this.services.has(name)) {
15091
15857
  const existing = this.services.get(name);
@@ -15093,21 +15859,27 @@ var ServiceRegistry = class {
15093
15859
  }
15094
15860
  this.services.set(name, { implementation, pluginName });
15095
15861
  }
15862
+ /** Register a service, replacing any existing registration (used by override plugins). */
15096
15863
  registerOverride(name, implementation, pluginName) {
15097
15864
  this.services.set(name, { implementation, pluginName });
15098
15865
  }
15866
+ /** Retrieve a service by name. Returns undefined if not registered. */
15099
15867
  get(name) {
15100
15868
  return this.services.get(name)?.implementation;
15101
15869
  }
15870
+ /** Check whether a service is registered. */
15102
15871
  has(name) {
15103
15872
  return this.services.has(name);
15104
15873
  }
15874
+ /** List all registered services with their owning plugin names. */
15105
15875
  list() {
15106
15876
  return [...this.services.entries()].map(([name, { pluginName }]) => ({ name, pluginName }));
15107
15877
  }
15878
+ /** Remove a single service by name. */
15108
15879
  unregister(name) {
15109
15880
  this.services.delete(name);
15110
15881
  }
15882
+ /** Remove all services owned by a specific plugin (called during plugin unload). */
15111
15883
  unregisterByPlugin(pluginName) {
15112
15884
  for (const [name, entry] of this.services) {
15113
15885
  if (entry.pluginName === pluginName) {
@@ -15123,6 +15895,7 @@ var MiddlewareChain = class {
15123
15895
  chains = /* @__PURE__ */ new Map();
15124
15896
  errorHandler;
15125
15897
  errorTracker;
15898
+ /** Register a middleware handler for a hook. Handlers are kept sorted by priority. */
15126
15899
  add(hook, pluginName, opts) {
15127
15900
  const entry = {
15128
15901
  pluginName,
@@ -15137,6 +15910,15 @@ var MiddlewareChain = class {
15137
15910
  this.chains.set(hook, [entry]);
15138
15911
  }
15139
15912
  }
15913
+ /**
15914
+ * Execute the middleware chain for a hook, ending with the core handler.
15915
+ *
15916
+ * The chain is built recursively: each handler calls `next()` to invoke the
15917
+ * next handler, with the core handler at the end. If no middleware is registered,
15918
+ * the core handler runs directly.
15919
+ *
15920
+ * @returns The final payload, or `null` if any handler short-circuited.
15921
+ */
15140
15922
  async execute(hook, payload, coreHandler) {
15141
15923
  const handlers = this.chains.get(hook);
15142
15924
  if (!handlers || handlers.length === 0) {
@@ -15206,6 +15988,7 @@ var MiddlewareChain = class {
15206
15988
  const start = buildNext(0, payload);
15207
15989
  return start();
15208
15990
  }
15991
+ /** Remove all middleware handlers registered by a specific plugin. */
15209
15992
  removeAll(pluginName) {
15210
15993
  for (const [hook, handlers] of this.chains.entries()) {
15211
15994
  const filtered = handlers.filter((h) => h.pluginName !== pluginName);
@@ -15216,9 +15999,11 @@ var MiddlewareChain = class {
15216
15999
  }
15217
16000
  }
15218
16001
  }
16002
+ /** Set a callback for middleware errors (e.g., logging). */
15219
16003
  setErrorHandler(fn) {
15220
16004
  this.errorHandler = fn;
15221
16005
  }
16006
+ /** Attach an ErrorTracker for circuit-breaking misbehaving plugins. */
15222
16007
  setErrorTracker(tracker) {
15223
16008
  this.errorTracker = tracker;
15224
16009
  }
@@ -15230,10 +16015,15 @@ var ErrorTracker = class {
15230
16015
  disabled = /* @__PURE__ */ new Set();
15231
16016
  exempt = /* @__PURE__ */ new Set();
15232
16017
  config;
16018
+ /** Callback fired when a plugin is auto-disabled due to error budget exhaustion. */
15233
16019
  onDisabled;
15234
16020
  constructor(config) {
15235
16021
  this.config = { maxErrors: config?.maxErrors ?? 10, windowMs: config?.windowMs ?? 36e5 };
15236
16022
  }
16023
+ /**
16024
+ * Record an error for a plugin. If the error budget is exceeded,
16025
+ * the plugin is disabled and the `onDisabled` callback fires.
16026
+ */
15237
16027
  increment(pluginName) {
15238
16028
  if (this.exempt.has(pluginName)) return;
15239
16029
  const now = Date.now();
@@ -15250,13 +16040,16 @@ var ErrorTracker = class {
15250
16040
  this.onDisabled?.(pluginName, reason);
15251
16041
  }
15252
16042
  }
16043
+ /** Check if a plugin has been disabled due to errors. */
15253
16044
  isDisabled(pluginName) {
15254
16045
  return this.disabled.has(pluginName);
15255
16046
  }
16047
+ /** Re-enable a plugin and clear its error history. */
15256
16048
  reset(pluginName) {
15257
16049
  this.disabled.delete(pluginName);
15258
16050
  this.errors.delete(pluginName);
15259
16051
  }
16052
+ /** Mark a plugin as exempt from circuit-breaking (e.g., essential plugins). */
15260
16053
  setExempt(pluginName) {
15261
16054
  this.exempt.add(pluginName);
15262
16055
  }
@@ -15268,6 +16061,7 @@ import path13 from "path";
15268
16061
  var PluginStorageImpl = class {
15269
16062
  kvPath;
15270
16063
  dataDir;
16064
+ /** Serializes writes to prevent concurrent file corruption */
15271
16065
  writeChain = Promise.resolve();
15272
16066
  constructor(baseDir) {
15273
16067
  this.dataDir = path13.join(baseDir, "data");
@@ -15308,6 +16102,7 @@ var PluginStorageImpl = class {
15308
16102
  async list() {
15309
16103
  return Object.keys(this.readKv());
15310
16104
  }
16105
+ /** Returns the plugin's data directory, creating it lazily on first access. */
15311
16106
  getDataDir() {
15312
16107
  fs14.mkdirSync(this.dataDir, { recursive: true });
15313
16108
  return this.dataDir;
@@ -15481,6 +16276,11 @@ function createPluginContext(opts) {
15481
16276
  return core;
15482
16277
  },
15483
16278
  instanceRoot,
16279
+ /**
16280
+ * Called by LifecycleManager during plugin teardown.
16281
+ * Unregisters all event handlers, middleware, commands, and services
16282
+ * registered by this plugin, preventing leaks across reloads.
16283
+ */
15484
16284
  cleanup() {
15485
16285
  for (const { event, handler } of registeredListeners) {
15486
16286
  eventBus.off(event, handler);
@@ -15572,12 +16372,15 @@ var LifecycleManager = class {
15572
16372
  loadOrder = [];
15573
16373
  _loaded = /* @__PURE__ */ new Set();
15574
16374
  _failed = /* @__PURE__ */ new Set();
16375
+ /** Names of plugins that successfully completed setup(). */
15575
16376
  get loadedPlugins() {
15576
16377
  return [...this._loaded];
15577
16378
  }
16379
+ /** Names of plugins whose setup() threw an error. These plugins are skipped but don't crash the system. */
15578
16380
  get failedPlugins() {
15579
16381
  return [...this._failed];
15580
16382
  }
16383
+ /** The PluginRegistry tracking installed and enabled plugin state. */
15581
16384
  get registry() {
15582
16385
  return this.pluginRegistry;
15583
16386
  }
@@ -15624,6 +16427,12 @@ var LifecycleManager = class {
15624
16427
  return this;
15625
16428
  } };
15626
16429
  }
16430
+ /**
16431
+ * Boot a set of plugins in dependency order.
16432
+ *
16433
+ * Can be called multiple times (e.g., core plugins first, then dev plugins later).
16434
+ * Already-loaded plugins are included in dependency resolution but not re-booted.
16435
+ */
15627
16436
  async boot(plugins) {
15628
16437
  const newNames = new Set(plugins.map((p) => p.name));
15629
16438
  const allForResolution = [...this.loadOrder.filter((p) => !newNames.has(p.name)), ...plugins];
@@ -15735,6 +16544,11 @@ var LifecycleManager = class {
15735
16544
  }
15736
16545
  }
15737
16546
  }
16547
+ /**
16548
+ * Unload a single plugin: call teardown(), clean up its context
16549
+ * (listeners, middleware, services), and remove from tracked state.
16550
+ * Used for hot-reload: unload → rebuild → re-boot.
16551
+ */
15738
16552
  async unloadPlugin(name) {
15739
16553
  if (!this._loaded.has(name)) return;
15740
16554
  const plugin = this.loadOrder.find((p) => p.name === name);
@@ -15754,6 +16568,10 @@ var LifecycleManager = class {
15754
16568
  this.loadOrder = this.loadOrder.filter((p) => p.name !== name);
15755
16569
  this.eventBus?.emit(BusEvent.PLUGIN_UNLOADED, { name });
15756
16570
  }
16571
+ /**
16572
+ * Gracefully shut down all loaded plugins.
16573
+ * Teardown runs in reverse boot order so that dependencies outlive their dependents.
16574
+ */
15757
16575
  async shutdown() {
15758
16576
  const reversed = [...this.loadOrder].reverse();
15759
16577
  for (const plugin of reversed) {
@@ -15781,16 +16599,19 @@ init_log();
15781
16599
  var log13 = createChildLogger({ module: "menu-registry" });
15782
16600
  var MenuRegistry = class {
15783
16601
  items = /* @__PURE__ */ new Map();
16602
+ /** Register or replace a menu item by its unique ID. */
15784
16603
  register(item) {
15785
16604
  this.items.set(item.id, item);
15786
16605
  }
16606
+ /** Remove a menu item by ID. */
15787
16607
  unregister(id) {
15788
16608
  this.items.delete(id);
15789
16609
  }
16610
+ /** Look up a single menu item by ID. */
15790
16611
  getItem(id) {
15791
16612
  return this.items.get(id);
15792
16613
  }
15793
- /** Get all visible items sorted by priority */
16614
+ /** Get all visible items sorted by priority (lower number = shown first). */
15794
16615
  getItems() {
15795
16616
  return [...this.items.values()].filter((item) => {
15796
16617
  if (!item.visible) return true;
@@ -15869,19 +16690,31 @@ var log14 = createChildLogger({ module: "assistant-registry" });
15869
16690
  var AssistantRegistry = class {
15870
16691
  sections = /* @__PURE__ */ new Map();
15871
16692
  _instanceRoot = "";
15872
- /** Set the instance root path used in assistant guidelines */
16693
+ /** Set the instance root path used in assistant guidelines. */
15873
16694
  setInstanceRoot(root) {
15874
16695
  this._instanceRoot = root;
15875
16696
  }
16697
+ /** Register a prompt section. Overwrites any existing section with the same id. */
15876
16698
  register(section) {
15877
16699
  if (this.sections.has(section.id)) {
15878
16700
  log14.warn({ id: section.id }, "Assistant section overwritten");
15879
16701
  }
15880
16702
  this.sections.set(section.id, section);
15881
16703
  }
16704
+ /** Remove a previously registered section by id. */
15882
16705
  unregister(id) {
15883
16706
  this.sections.delete(id);
15884
16707
  }
16708
+ /**
16709
+ * Compose the full system prompt from all registered sections.
16710
+ *
16711
+ * Sections are sorted by priority (ascending), each contributing a titled
16712
+ * markdown block. If a section's `buildContext()` throws, it is skipped
16713
+ * gracefully so one broken section doesn't break the entire prompt.
16714
+ *
16715
+ * If `channelId` is provided, a "Current Channel" block is injected at the
16716
+ * top of the prompt so the assistant can adapt its behavior to the platform.
16717
+ */
15885
16718
  buildSystemPrompt(channelId) {
15886
16719
  const sorted = [...this.sections.values()].sort((a, b) => a.priority - b.priority);
15887
16720
  const parts = [ASSISTANT_PREAMBLE];
@@ -15918,6 +16751,13 @@ var AssistantManager = class {
15918
16751
  }
15919
16752
  sessions = /* @__PURE__ */ new Map();
15920
16753
  pendingSystemPrompts = /* @__PURE__ */ new Map();
16754
+ /**
16755
+ * Returns the assistant session for a channel, creating one if needed.
16756
+ *
16757
+ * If a persisted assistant session exists in the store, it is reused
16758
+ * (same session ID) to preserve conversation history. The system prompt
16759
+ * is always rebuilt fresh and deferred until the first user message.
16760
+ */
15921
16761
  async getOrSpawn(channelId, threadId) {
15922
16762
  const existing = this.core.sessionStore?.findAssistant(channelId);
15923
16763
  const session = await this.core.createSession({
@@ -15938,6 +16778,7 @@ var AssistantManager = class {
15938
16778
  );
15939
16779
  return session;
15940
16780
  }
16781
+ /** Returns the active assistant session for a channel, or null if none exists. */
15941
16782
  get(channelId) {
15942
16783
  return this.sessions.get(channelId) ?? null;
15943
16784
  }
@@ -15950,6 +16791,7 @@ var AssistantManager = class {
15950
16791
  if (prompt) this.pendingSystemPrompts.delete(channelId);
15951
16792
  return prompt;
15952
16793
  }
16794
+ /** Checks whether a given session ID belongs to the built-in assistant. */
15953
16795
  isAssistant(sessionId) {
15954
16796
  for (const s of this.sessions.values()) {
15955
16797
  if (s.id === sessionId) return true;
@@ -16176,30 +17018,49 @@ var OpenACPCore = class {
16176
17018
  menuRegistry = new MenuRegistry();
16177
17019
  assistantRegistry = new AssistantRegistry();
16178
17020
  assistantManager;
16179
- // --- Lazy getters: resolve from ServiceRegistry (populated by plugins during boot) ---
17021
+ // Services (security, notifications, speech, etc.) are provided by plugins that
17022
+ // register during boot. Core accesses them lazily via ServiceRegistry so it doesn't
17023
+ // need compile-time dependencies on plugin implementations.
17024
+ /** @throws if the service hasn't been registered by its plugin yet */
16180
17025
  getService(name) {
16181
17026
  const svc = this.lifecycleManager.serviceRegistry.get(name);
16182
17027
  if (!svc) throw new Error(`Service '${name}' not registered \u2014 is the ${name} plugin loaded?`);
16183
17028
  return svc;
16184
17029
  }
17030
+ /** Access control and rate-limiting guard (provided by security plugin). */
16185
17031
  get securityGuard() {
16186
17032
  return this.getService("security");
16187
17033
  }
17034
+ /** Cross-session notification delivery (provided by notifications plugin). */
16188
17035
  get notificationManager() {
16189
17036
  return this.getService("notifications");
16190
17037
  }
17038
+ /** File I/O service for agent attachment storage (provided by file-service plugin). */
16191
17039
  get fileService() {
16192
17040
  return this.getService("file-service");
16193
17041
  }
17042
+ /** Text-to-speech / speech-to-text engine (provided by speech plugin). */
16194
17043
  get speechService() {
16195
17044
  return this.getService("speech");
16196
17045
  }
17046
+ /** Conversation history builder for context injection (provided by context plugin). */
16197
17047
  get contextManager() {
16198
17048
  return this.getService("context");
16199
17049
  }
17050
+ /** Per-plugin persistent settings (e.g. API keys). */
16200
17051
  get settingsManager() {
16201
17052
  return this.lifecycleManager.settingsManager;
16202
17053
  }
17054
+ /**
17055
+ * Bootstrap all core subsystems. The boot order matters:
17056
+ * 1. AgentCatalog + AgentManager (agent definitions)
17057
+ * 2. SessionStore + SessionManager (session persistence and lookup)
17058
+ * 3. EventBus (inter-module communication)
17059
+ * 4. SessionFactory (session creation pipeline)
17060
+ * 5. LifecycleManager (plugin infrastructure)
17061
+ * 6. Wire middleware chain into factory + manager
17062
+ * 7. AgentSwitchHandler, config listeners, menu/assistant registries
17063
+ */
16203
17064
  constructor(configManager, ctx) {
16204
17065
  this.configManager = configManager;
16205
17066
  this.instanceContext = ctx;
@@ -16310,16 +17171,28 @@ var OpenACPCore = class {
16310
17171
  this.lifecycleManager.serviceRegistry.register("menu-registry", this.menuRegistry, "core");
16311
17172
  this.lifecycleManager.serviceRegistry.register("assistant-registry", this.assistantRegistry, "core");
16312
17173
  }
17174
+ /** Optional tunnel for generating public URLs (code viewer links, etc.). */
16313
17175
  get tunnelService() {
16314
17176
  return this._tunnelService;
16315
17177
  }
17178
+ /** Propagate tunnel service to MessageTransformer so it can generate viewer links. */
16316
17179
  set tunnelService(service) {
16317
17180
  this._tunnelService = service;
16318
17181
  this.messageTransformer.tunnelService = service;
16319
17182
  }
17183
+ /**
17184
+ * Register a messaging adapter (e.g. Telegram, Slack, SSE).
17185
+ *
17186
+ * Adapters must be registered before `start()`. The adapter name serves as its
17187
+ * channel ID throughout the system — used in session records, bridge keys, and routing.
17188
+ */
16320
17189
  registerAdapter(name, adapter) {
16321
17190
  this.adapters.set(name, adapter);
16322
17191
  }
17192
+ /**
17193
+ * Start all registered adapters. Adapters that fail are logged but do not
17194
+ * prevent others from starting. Throws only if ALL adapters fail.
17195
+ */
16323
17196
  async start() {
16324
17197
  this.agentCatalog.refreshRegistryIfStale().catch((err) => {
16325
17198
  log16.warn({ err }, "Background registry refresh failed");
@@ -16339,6 +17212,10 @@ var OpenACPCore = class {
16339
17212
  );
16340
17213
  }
16341
17214
  }
17215
+ /**
17216
+ * Graceful shutdown: notify users, persist session state, stop adapters.
17217
+ * Agent subprocesses are not explicitly killed — they exit with the parent process.
17218
+ */
16342
17219
  async stop() {
16343
17220
  try {
16344
17221
  const nm = this.lifecycleManager.serviceRegistry.get("notifications");
@@ -16357,6 +17234,13 @@ var OpenACPCore = class {
16357
17234
  }
16358
17235
  }
16359
17236
  // --- Archive ---
17237
+ /**
17238
+ * Archive a session: delete its adapter topic/thread and cancel the session.
17239
+ *
17240
+ * Only sessions in archivable states (active, cancelled, error) can be archived —
17241
+ * initializing and finished sessions are excluded.
17242
+ * The adapter handles platform-side cleanup (e.g. deleting a Telegram topic).
17243
+ */
16360
17244
  async archiveSession(sessionId) {
16361
17245
  const session = this.sessionManager.getSession(sessionId);
16362
17246
  if (!session) return { ok: false, error: "Session not found (must be in memory)" };
@@ -16376,6 +17260,18 @@ var OpenACPCore = class {
16376
17260
  }
16377
17261
  }
16378
17262
  // --- Message Routing ---
17263
+ /**
17264
+ * Route an incoming platform message to the appropriate session.
17265
+ *
17266
+ * Flow:
17267
+ * 1. Run `message:incoming` middleware (plugins can modify or block)
17268
+ * 2. SecurityGuard checks user access and per-user session limits
17269
+ * 3. Find session by channel+thread (in-memory lookup, then lazy resume from disk)
17270
+ * 4. For assistant sessions, prepend any deferred system prompt
17271
+ * 5. Emit `message:queued` for SSE clients, then enqueue the prompt on the session
17272
+ *
17273
+ * If no session is found, the user is told to start one with /new.
17274
+ */
16379
17275
  async handleMessage(message) {
16380
17276
  log16.debug(
16381
17277
  {
@@ -16457,6 +17353,17 @@ ${text3}`;
16457
17353
  }
16458
17354
  }
16459
17355
  // --- Unified Session Creation Pipeline ---
17356
+ /**
17357
+ * Create (or resume) a session with full wiring: agent, adapter thread, bridge, persistence.
17358
+ *
17359
+ * This is the single entry point for session creation. The pipeline:
17360
+ * 1. SessionFactory spawns/resumes the agent process
17361
+ * 2. Adapter creates a thread/topic if requested
17362
+ * 3. Initial session record is persisted (so lazy resume can find it by threadId)
17363
+ * 4. SessionBridge connects agent events to the adapter
17364
+ * 5. For headless sessions (no adapter), fallback event handlers are wired inline
17365
+ * 6. Side effects (usage tracking, tunnel cleanup) are attached
17366
+ */
16460
17367
  async createSession(params) {
16461
17368
  const session = await this.sessionFactory.create(params);
16462
17369
  if (params.threadId) {
@@ -16579,9 +17486,16 @@ ${text3}`;
16579
17486
  );
16580
17487
  return session;
16581
17488
  }
17489
+ /** Convenience wrapper: create a new session with default agent/workspace resolution. */
16582
17490
  async handleNewSession(channelId, agentName, workspacePath, options) {
16583
17491
  return this.sessionFactory.handleNewSession(channelId, agentName, workspacePath, options);
16584
17492
  }
17493
+ /**
17494
+ * Adopt an externally-started agent session (e.g. from a CLI `openacp adopt` command).
17495
+ *
17496
+ * Validates that the agent supports resume, checks session limits, avoids duplicates,
17497
+ * then creates a full session via the unified pipeline with resume semantics.
17498
+ */
16585
17499
  async adoptSession(agentName, agentSessionId, cwd, channelId) {
16586
17500
  const caps = getAgentCapabilities(agentName);
16587
17501
  if (!caps.supportsResume) {
@@ -16692,22 +17606,33 @@ ${text3}`;
16692
17606
  status: "adopted"
16693
17607
  };
16694
17608
  }
17609
+ /** Start a new chat within the same agent and workspace as the current session's thread. */
16695
17610
  async handleNewChat(channelId, currentThreadId) {
16696
17611
  return this.sessionFactory.handleNewChat(channelId, currentThreadId);
16697
17612
  }
17613
+ /** Create a session and inject conversation context from a prior session or repo. */
16698
17614
  async createSessionWithContext(params) {
16699
17615
  return this.sessionFactory.createSessionWithContext(params);
16700
17616
  }
16701
17617
  // --- Agent Switch ---
17618
+ /** Switch a session's active agent. Delegates to AgentSwitchHandler for state coordination. */
16702
17619
  async switchSessionAgent(sessionId, toAgent) {
16703
17620
  return this.agentSwitchHandler.switch(sessionId, toAgent);
16704
17621
  }
17622
+ /** Find a session by channel+thread, resuming from disk if not in memory. */
16705
17623
  async getOrResumeSession(channelId, threadId) {
16706
17624
  return this.sessionFactory.getOrResume(channelId, threadId);
16707
17625
  }
17626
+ /** Find a session by ID, resuming from disk if not in memory. */
16708
17627
  async getOrResumeSessionById(sessionId) {
16709
17628
  return this.sessionFactory.getOrResumeById(sessionId);
16710
17629
  }
17630
+ /**
17631
+ * Attach an additional adapter to an existing session (multi-adapter support).
17632
+ *
17633
+ * Creates a thread on the target adapter and connects a SessionBridge so the
17634
+ * session's agent events are forwarded to both the primary and attached adapters.
17635
+ */
16711
17636
  async attachAdapter(sessionId, adapterId) {
16712
17637
  const session = this.sessionManager.getSession(sessionId);
16713
17638
  if (!session) throw new Error(`Session ${sessionId} not found`);
@@ -16731,6 +17656,10 @@ ${text3}`;
16731
17656
  });
16732
17657
  return { threadId };
16733
17658
  }
17659
+ /**
17660
+ * Detach a secondary adapter from a session. The primary adapter (channelId) cannot
17661
+ * be detached. Disconnects the bridge and cleans up thread mappings.
17662
+ */
16734
17663
  async detachAdapter(sessionId, adapterId) {
16735
17664
  const session = this.sessionManager.getSession(sessionId);
16736
17665
  if (!session) throw new Error(`Session ${sessionId} not found`);
@@ -16763,6 +17692,7 @@ ${text3}`;
16763
17692
  platforms: this.buildPlatformsFromSession(session)
16764
17693
  });
16765
17694
  }
17695
+ /** Build the platforms map (adapter → thread/topic IDs) for persistence. */
16766
17696
  buildPlatformsFromSession(session) {
16767
17697
  const platforms = {};
16768
17698
  for (const [adapterId, threadId] of session.threadIds) {
@@ -16794,8 +17724,13 @@ ${text3}`;
16794
17724
  const bridge = this.createBridge(session, adapter, session.channelId);
16795
17725
  bridge.connect();
16796
17726
  }
16797
- /** Create a SessionBridge for the given session and adapter.
16798
- * Disconnects any existing bridge for the same adapter+session first. */
17727
+ /**
17728
+ * Create a SessionBridge for the given session and adapter.
17729
+ *
17730
+ * The bridge subscribes to Session events (agent output, status changes, naming)
17731
+ * and forwards them to the adapter for platform delivery. Disconnects any existing
17732
+ * bridge for the same adapter+session first to avoid duplicate event handlers.
17733
+ */
16799
17734
  createBridge(session, adapter, adapterId) {
16800
17735
  const id = adapterId ?? adapter.name;
16801
17736
  const key = this.bridgeKey(id, session.id);
@@ -16899,10 +17834,15 @@ var CommandRegistry = class _CommandRegistry {
16899
17834
  return this.getAll().filter((cmd) => cmd.category === category);
16900
17835
  }
16901
17836
  /**
16902
- * Parse and execute a command string.
16903
- * @param commandString - Full command string, e.g. "/greet hello world"
16904
- * @param baseArgs - Base arguments (channelId, userId, etc.)
16905
- * @returns CommandResponse
17837
+ * Parse and execute a command string (e.g. "/greet hello world").
17838
+ *
17839
+ * Resolution order:
17840
+ * 1. Adapter-specific override (e.g. Telegram's version of /new)
17841
+ * 2. Short name or qualified name in the main registry
17842
+ *
17843
+ * Strips Telegram-style bot mentions (e.g. "/help@MyBot" → "help").
17844
+ * Returns `{ type: 'delegated' }` if the handler returns null/undefined
17845
+ * (meaning it handled the response itself, e.g. via assistant).
16906
17846
  */
16907
17847
  async execute(commandString, baseArgs) {
16908
17848
  const trimmed = commandString.trim();
@@ -18007,6 +18947,10 @@ var TopicManager = class {
18007
18947
  this.adapter = adapter;
18008
18948
  this.systemTopicIds = systemTopicIds;
18009
18949
  }
18950
+ /**
18951
+ * List user-facing session topics, excluding system topics.
18952
+ * Optionally filtered to specific status values.
18953
+ */
18010
18954
  listTopics(filter) {
18011
18955
  const records = this.sessionManager.listRecords(filter);
18012
18956
  return records.filter((r) => !this.isSystemTopic(r)).filter((r) => !filter?.statuses?.length || filter.statuses.includes(r.status)).map((r) => ({
@@ -18018,6 +18962,12 @@ var TopicManager = class {
18018
18962
  lastActiveAt: r.lastActiveAt
18019
18963
  }));
18020
18964
  }
18965
+ /**
18966
+ * Delete a session topic and its session record.
18967
+ *
18968
+ * Returns `needsConfirmation: true` when the session is still active and
18969
+ * `options.confirmed` was not set — callers must ask the user before proceeding.
18970
+ */
18021
18971
  async deleteTopic(sessionId, options) {
18022
18972
  const records = this.sessionManager.listRecords();
18023
18973
  const record = records.find((r) => r.sessionId === sessionId);
@@ -18045,6 +18995,10 @@ var TopicManager = class {
18045
18995
  await this.sessionManager.removeRecord(sessionId);
18046
18996
  return { ok: true, topicId };
18047
18997
  }
18998
+ /**
18999
+ * Bulk-delete topics by status (default: finished, error, cancelled).
19000
+ * Active/initializing sessions are cancelled before deletion to prevent orphaned processes.
19001
+ */
18048
19002
  async cleanup(statuses) {
18049
19003
  const targetStatuses = statuses?.length ? statuses : ["finished", "error", "cancelled"];
18050
19004
  const records = this.sessionManager.listRecords({ statuses: targetStatuses });
@@ -18102,6 +19056,7 @@ var StreamAdapter = class {
18102
19056
  ...config
18103
19057
  };
18104
19058
  }
19059
+ /** Wraps the outgoing message as a StreamEvent and emits it to the session's listeners. */
18105
19060
  async sendMessage(sessionId, content) {
18106
19061
  await this.emit(sessionId, {
18107
19062
  type: content.type,
@@ -18110,6 +19065,7 @@ var StreamAdapter = class {
18110
19065
  timestamp: Date.now()
18111
19066
  });
18112
19067
  }
19068
+ /** Emits a permission request event so the client can render approve/deny UI. */
18113
19069
  async sendPermissionRequest(sessionId, request) {
18114
19070
  await this.emit(sessionId, {
18115
19071
  type: "permission_request",
@@ -18118,6 +19074,7 @@ var StreamAdapter = class {
18118
19074
  timestamp: Date.now()
18119
19075
  });
18120
19076
  }
19077
+ /** Broadcasts a notification to all connected clients (not scoped to a session). */
18121
19078
  async sendNotification(notification) {
18122
19079
  await this.broadcast({
18123
19080
  type: "notification",
@@ -18125,9 +19082,14 @@ var StreamAdapter = class {
18125
19082
  timestamp: Date.now()
18126
19083
  });
18127
19084
  }
19085
+ /**
19086
+ * No-op for stream adapters — threads are a platform concept (Telegram topics, Slack threads).
19087
+ * Stream clients manage their own session UI.
19088
+ */
18128
19089
  async createSessionThread(_sessionId, _name) {
18129
19090
  return "";
18130
19091
  }
19092
+ /** Emits a rename event so connected clients can update their session title. */
18131
19093
  async renameSessionThread(sessionId, name) {
18132
19094
  await this.emit(sessionId, {
18133
19095
  type: "session_rename",
@@ -18152,20 +19114,27 @@ var Draft = class {
18152
19114
  }
18153
19115
  buffer = "";
18154
19116
  _messageId;
19117
+ /** Guards against concurrent first-flush — ensures only one sendMessage creates the draft. */
18155
19118
  firstFlushPending = false;
18156
19119
  flushTimer;
18157
19120
  flushPromise = Promise.resolve();
18158
19121
  get isEmpty() {
18159
19122
  return !this.buffer;
18160
19123
  }
19124
+ /** Platform message ID, set after the first successful flush. */
18161
19125
  get messageId() {
18162
19126
  return this._messageId;
18163
19127
  }
19128
+ /** Appends streaming text to the buffer and schedules a flush. */
18164
19129
  append(text3) {
18165
19130
  if (!text3) return;
18166
19131
  this.buffer += text3;
18167
19132
  this.scheduleFlush();
18168
19133
  }
19134
+ /**
19135
+ * Flushes any remaining buffered text and returns the platform message ID.
19136
+ * Called when the streaming response completes.
19137
+ */
18169
19138
  async finalize() {
18170
19139
  if (this.flushTimer) {
18171
19140
  clearTimeout(this.flushTimer);
@@ -18177,6 +19146,7 @@ var Draft = class {
18177
19146
  }
18178
19147
  return this._messageId;
18179
19148
  }
19149
+ /** Discards buffered text and cancels any pending flush. */
18180
19150
  destroy() {
18181
19151
  if (this.flushTimer) {
18182
19152
  clearTimeout(this.flushTimer);
@@ -18219,6 +19189,7 @@ var DraftManager = class {
18219
19189
  this.config = config;
18220
19190
  }
18221
19191
  drafts = /* @__PURE__ */ new Map();
19192
+ /** Returns the existing draft for a session, or creates a new one. */
18222
19193
  getOrCreate(sessionId) {
18223
19194
  let draft = this.drafts.get(sessionId);
18224
19195
  if (!draft) {
@@ -18227,15 +19198,18 @@ var DraftManager = class {
18227
19198
  }
18228
19199
  return draft;
18229
19200
  }
19201
+ /** Finalizes and removes the draft for a session. */
18230
19202
  async finalize(sessionId) {
18231
19203
  const draft = this.drafts.get(sessionId);
18232
19204
  if (!draft) return;
18233
19205
  await draft.finalize();
18234
19206
  this.drafts.delete(sessionId);
18235
19207
  }
19208
+ /** Finalizes all active drafts (e.g., during adapter shutdown). */
18236
19209
  async finalizeAll() {
18237
19210
  await Promise.all([...this.drafts.values()].map((d) => d.finalize()));
18238
19211
  }
19212
+ /** Destroys a draft without flushing (e.g., on session error). */
18239
19213
  destroy(sessionId) {
18240
19214
  const draft = this.drafts.get(sessionId);
18241
19215
  if (draft) {
@@ -18243,6 +19217,7 @@ var DraftManager = class {
18243
19217
  this.drafts.delete(sessionId);
18244
19218
  }
18245
19219
  }
19220
+ /** Destroys all drafts without flushing. */
18246
19221
  destroyAll() {
18247
19222
  for (const draft of this.drafts.values()) {
18248
19223
  draft.destroy();
@@ -18254,12 +19229,14 @@ var DraftManager = class {
18254
19229
  // src/core/adapter-primitives/primitives/tool-call-tracker.ts
18255
19230
  var ToolCallTracker = class {
18256
19231
  sessions = /* @__PURE__ */ new Map();
19232
+ /** Registers a new tool call and associates it with its platform message ID. */
18257
19233
  track(sessionId, meta, messageId) {
18258
19234
  if (!this.sessions.has(sessionId)) {
18259
19235
  this.sessions.set(sessionId, /* @__PURE__ */ new Map());
18260
19236
  }
18261
19237
  this.sessions.get(sessionId).set(meta.id, { ...meta, messageId });
18262
19238
  }
19239
+ /** Updates a tracked tool call's status and optional metadata. Returns null if not found. */
18263
19240
  update(sessionId, toolId, status, patch) {
18264
19241
  const tool = this.sessions.get(sessionId)?.get(toolId);
18265
19242
  if (!tool) return null;
@@ -18270,10 +19247,12 @@ var ToolCallTracker = class {
18270
19247
  if (patch?.kind) tool.kind = patch.kind;
18271
19248
  return tool;
18272
19249
  }
19250
+ /** Returns all tracked tool calls for a session (regardless of status). */
18273
19251
  getActive(sessionId) {
18274
19252
  const session = this.sessions.get(sessionId);
18275
19253
  return session ? [...session.values()] : [];
18276
19254
  }
19255
+ /** Removes all tracked tool calls for a session (called at turn end). */
18277
19256
  clear(sessionId) {
18278
19257
  this.sessions.delete(sessionId);
18279
19258
  }
@@ -18288,6 +19267,7 @@ var ActivityTracker = class {
18288
19267
  this.config = config;
18289
19268
  }
18290
19269
  sessions = /* @__PURE__ */ new Map();
19270
+ /** Shows the typing indicator and starts the periodic refresh timer. */
18291
19271
  onThinkingStart(sessionId, callbacks) {
18292
19272
  this.cleanup(sessionId);
18293
19273
  const state = {
@@ -18303,6 +19283,7 @@ var ActivityTracker = class {
18303
19283
  this.startRefresh(sessionId, state);
18304
19284
  }, 0);
18305
19285
  }
19286
+ /** Dismisses the typing indicator when the agent starts producing text output. */
18306
19287
  onTextStart(sessionId) {
18307
19288
  const state = this.sessions.get(sessionId);
18308
19289
  if (!state || state.dismissed) return;
@@ -18311,9 +19292,11 @@ var ActivityTracker = class {
18311
19292
  state.callbacks.removeThinkingIndicator().catch(() => {
18312
19293
  });
18313
19294
  }
19295
+ /** Cleans up the typing indicator when the session ends. */
18314
19296
  onSessionEnd(sessionId) {
18315
19297
  this.cleanup(sessionId);
18316
19298
  }
19299
+ /** Cleans up all sessions (e.g., during adapter shutdown). */
18317
19300
  destroy() {
18318
19301
  for (const [id] of this.sessions) {
18319
19302
  this.cleanup(id);