@openacp/cli 0.4.10 → 0.5.0

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.
Files changed (82) hide show
  1. package/README.md +57 -4
  2. package/dist/agent-catalog-4IAJ7HEG.js +10 -0
  3. package/dist/agent-registry-B5YAMA4T.js +8 -0
  4. package/dist/agent-store-ZBXGOFPH.js +8 -0
  5. package/dist/api-client-UN7BXQOQ.js +11 -0
  6. package/dist/{autostart-DZ3MHHMM.js → autostart-K73RQZVV.js} +3 -3
  7. package/dist/chunk-5HGXUCMX.js +83 -0
  8. package/dist/chunk-5HGXUCMX.js.map +1 -0
  9. package/dist/{chunk-UAUTLC4E.js → chunk-D73LCTPF.js} +75 -37
  10. package/dist/chunk-D73LCTPF.js.map +1 -0
  11. package/dist/{chunk-LYKCQTH5.js → chunk-ESOPMQAY.js} +5 -1
  12. package/dist/chunk-ESOPMQAY.js.map +1 -0
  13. package/dist/{chunk-KPI4HGJC.js → chunk-FWN3UIRT.js} +1631 -970
  14. package/dist/chunk-FWN3UIRT.js.map +1 -0
  15. package/dist/chunk-IRGYTNLP.js +650 -0
  16. package/dist/chunk-IRGYTNLP.js.map +1 -0
  17. package/dist/{chunk-ZRFBLD3W.js → chunk-JRF4G4X7.js} +71 -25
  18. package/dist/chunk-JRF4G4X7.js.map +1 -0
  19. package/dist/{chunk-6MJLVZXV.js → chunk-LAFKARV3.js} +58 -21
  20. package/dist/{chunk-6MJLVZXV.js.map → chunk-LAFKARV3.js.map} +1 -1
  21. package/dist/chunk-NAMYZIS5.js +1 -0
  22. package/dist/{chunk-HZD3CGPK.js → chunk-NDR5JCS7.js} +3 -3
  23. package/dist/chunk-OORPX73T.js +30 -0
  24. package/dist/chunk-OORPX73T.js.map +1 -0
  25. package/dist/{chunk-V3BA2MJ6.js → chunk-RF3DUYFO.js} +2 -2
  26. package/dist/chunk-S3DRLJPM.js +422 -0
  27. package/dist/chunk-S3DRLJPM.js.map +1 -0
  28. package/dist/chunk-UG6X672R.js +90 -0
  29. package/dist/chunk-UG6X672R.js.map +1 -0
  30. package/dist/{chunk-C6YIUTGR.js → chunk-VBEWSWVL.js} +2 -2
  31. package/dist/{chunk-MRKYJ422.js → chunk-X6LLG7XN.js} +2 -2
  32. package/dist/chunk-XJJ7LPXP.js +85 -0
  33. package/dist/chunk-XJJ7LPXP.js.map +1 -0
  34. package/dist/chunk-Z46LGZ7R.js +110 -0
  35. package/dist/chunk-Z46LGZ7R.js.map +1 -0
  36. package/dist/cli.js +313 -52
  37. package/dist/cli.js.map +1 -1
  38. package/dist/{config-H2DSEHNW.js → config-PCPIBPUA.js} +3 -3
  39. package/dist/config-editor-5L7AJ5AF.js +12 -0
  40. package/dist/config-editor-5L7AJ5AF.js.map +1 -0
  41. package/dist/config-registry-SNKA2EH2.js +17 -0
  42. package/dist/config-registry-SNKA2EH2.js.map +1 -0
  43. package/dist/{daemon-VF6HJQXD.js → daemon-JZLFRUW6.js} +4 -4
  44. package/dist/daemon-JZLFRUW6.js.map +1 -0
  45. package/dist/data/registry-snapshot.json +876 -0
  46. package/dist/doctor-N2HKKUUQ.js +9 -0
  47. package/dist/doctor-N2HKKUUQ.js.map +1 -0
  48. package/dist/index.d.ts +212 -43
  49. package/dist/index.js +43 -14
  50. package/dist/install-cloudflared-BTGUD7SW.js +8 -0
  51. package/dist/install-cloudflared-BTGUD7SW.js.map +1 -0
  52. package/dist/log-SPS2S6FO.js +19 -0
  53. package/dist/log-SPS2S6FO.js.map +1 -0
  54. package/dist/{main-G6XDM7EZ.js → main-37GLOJ7G.js} +21 -15
  55. package/dist/{main-G6XDM7EZ.js.map → main-37GLOJ7G.js.map} +1 -1
  56. package/dist/menu-6RCPBVGQ.js +15 -0
  57. package/dist/menu-6RCPBVGQ.js.map +1 -0
  58. package/dist/{setup-FCVL75K6.js → setup-QAS3QW3M.js} +5 -4
  59. package/dist/setup-QAS3QW3M.js.map +1 -0
  60. package/dist/{tunnel-service-DASSH7OA.js → tunnel-service-LEVPLXAZ.js} +3 -3
  61. package/package.json +10 -2
  62. package/dist/agent-registry-7HC6D4CH.js +0 -7
  63. package/dist/chunk-KPI4HGJC.js.map +0 -1
  64. package/dist/chunk-LYKCQTH5.js.map +0 -1
  65. package/dist/chunk-UAUTLC4E.js.map +0 -1
  66. package/dist/chunk-VA2M52CM.js +0 -15
  67. package/dist/chunk-VA2M52CM.js.map +0 -1
  68. package/dist/chunk-ZRFBLD3W.js.map +0 -1
  69. package/dist/config-editor-SKS4LJLT.js +0 -11
  70. package/dist/install-cloudflared-ILUXKLAC.js +0 -8
  71. /package/dist/{agent-registry-7HC6D4CH.js.map → agent-catalog-4IAJ7HEG.js.map} +0 -0
  72. /package/dist/{autostart-DZ3MHHMM.js.map → agent-registry-B5YAMA4T.js.map} +0 -0
  73. /package/dist/{config-H2DSEHNW.js.map → agent-store-ZBXGOFPH.js.map} +0 -0
  74. /package/dist/{config-editor-SKS4LJLT.js.map → api-client-UN7BXQOQ.js.map} +0 -0
  75. /package/dist/{daemon-VF6HJQXD.js.map → autostart-K73RQZVV.js.map} +0 -0
  76. /package/dist/{install-cloudflared-ILUXKLAC.js.map → chunk-NAMYZIS5.js.map} +0 -0
  77. /package/dist/{chunk-HZD3CGPK.js.map → chunk-NDR5JCS7.js.map} +0 -0
  78. /package/dist/{chunk-V3BA2MJ6.js.map → chunk-RF3DUYFO.js.map} +0 -0
  79. /package/dist/{chunk-C6YIUTGR.js.map → chunk-VBEWSWVL.js.map} +0 -0
  80. /package/dist/{chunk-MRKYJ422.js.map → chunk-X6LLG7XN.js.map} +0 -0
  81. /package/dist/{setup-FCVL75K6.js.map → config-PCPIBPUA.js.map} +0 -0
  82. /package/dist/{tunnel-service-DASSH7OA.js.map → tunnel-service-LEVPLXAZ.js.map} +0 -0
@@ -1,10 +1,29 @@
1
+ import {
2
+ buildMenuKeyboard,
3
+ buildSkillMessages,
4
+ handleClear,
5
+ handleHelp,
6
+ handleMenu
7
+ } from "./chunk-UG6X672R.js";
8
+ import {
9
+ AgentCatalog
10
+ } from "./chunk-S3DRLJPM.js";
1
11
  import {
2
12
  getAgentCapabilities
3
- } from "./chunk-VA2M52CM.js";
13
+ } from "./chunk-XJJ7LPXP.js";
14
+ import {
15
+ DoctorEngine
16
+ } from "./chunk-IRGYTNLP.js";
17
+ import {
18
+ getConfigValue,
19
+ getSafeFields,
20
+ isHotReloadable,
21
+ resolveOptions
22
+ } from "./chunk-Z46LGZ7R.js";
4
23
  import {
5
24
  createChildLogger,
6
25
  createSessionLogger
7
- } from "./chunk-LYKCQTH5.js";
26
+ } from "./chunk-ESOPMQAY.js";
8
27
 
9
28
  // src/core/streams.ts
10
29
  function nodeToWebWritable(nodeStream) {
@@ -49,7 +68,7 @@ var StderrCapture = class {
49
68
  };
50
69
 
51
70
  // src/core/agent-instance.ts
52
- import { spawn, execSync } from "child_process";
71
+ import { spawn, execFileSync } from "child_process";
53
72
  import { Transform } from "stream";
54
73
  import fs from "fs";
55
74
  import path from "path";
@@ -100,7 +119,7 @@ function resolveAgentCommand(cmd) {
100
119
  }
101
120
  }
102
121
  try {
103
- const fullPath = execSync(`which ${cmd}`, { encoding: "utf-8" }).trim();
122
+ const fullPath = execFileSync("which", [cmd], { encoding: "utf-8" }).trim();
104
123
  if (fullPath) {
105
124
  const content = fs.readFileSync(fullPath, "utf-8");
106
125
  if (content.startsWith("#!/usr/bin/env node")) {
@@ -478,31 +497,29 @@ ${stderr}`
478
497
 
479
498
  // src/core/agent-manager.ts
480
499
  var AgentManager = class {
481
- constructor(config) {
482
- this.config = config;
500
+ constructor(catalog) {
501
+ this.catalog = catalog;
483
502
  }
484
503
  getAvailableAgents() {
485
- return Object.entries(this.config.agents).map(([name, cfg]) => ({
486
- name,
487
- command: cfg.command,
488
- args: cfg.args,
489
- workingDirectory: cfg.workingDirectory,
490
- env: cfg.env
504
+ const installed = this.catalog.getInstalledEntries();
505
+ return Object.entries(installed).map(([key, agent]) => ({
506
+ name: key,
507
+ command: agent.command,
508
+ args: agent.args,
509
+ env: agent.env
491
510
  }));
492
511
  }
493
512
  getAgent(name) {
494
- const cfg = this.config.agents[name];
495
- if (!cfg) return void 0;
496
- return { name, ...cfg };
513
+ return this.catalog.resolve(name);
497
514
  }
498
515
  async spawn(agentName, workingDirectory) {
499
516
  const agentDef = this.getAgent(agentName);
500
- if (!agentDef) throw new Error(`Agent "${agentName}" not found in config`);
517
+ if (!agentDef) throw new Error(`Agent "${agentName}" is not installed. Run "openacp agents install ${agentName}" to add it.`);
501
518
  return AgentInstance.spawn(agentDef, workingDirectory);
502
519
  }
503
520
  async resume(agentName, workingDirectory, agentSessionId) {
504
521
  const agentDef = this.getAgent(agentName);
505
- if (!agentDef) throw new Error(`Agent "${agentName}" not found in config`);
522
+ if (!agentDef) throw new Error(`Agent "${agentName}" is not installed. Run "openacp agents install ${agentName}" to add it.`);
506
523
  return AgentInstance.resume(agentDef, workingDirectory, agentSessionId);
507
524
  }
508
525
  };
@@ -629,28 +646,40 @@ var PromptQueue = class {
629
646
  };
630
647
 
631
648
  // src/core/permission-gate.ts
649
+ var DEFAULT_TIMEOUT_MS = 10 * 60 * 1e3;
632
650
  var PermissionGate = class {
633
651
  request;
634
652
  resolveFn;
635
653
  rejectFn;
636
654
  settled = false;
655
+ timeoutTimer;
656
+ timeoutMs;
657
+ constructor(timeoutMs) {
658
+ this.timeoutMs = timeoutMs ?? DEFAULT_TIMEOUT_MS;
659
+ }
637
660
  setPending(request) {
638
661
  this.request = request;
639
662
  this.settled = false;
663
+ this.clearTimeout();
640
664
  return new Promise((resolve2, reject) => {
641
665
  this.resolveFn = resolve2;
642
666
  this.rejectFn = reject;
667
+ this.timeoutTimer = setTimeout(() => {
668
+ this.reject("Permission request timed out (no response received)");
669
+ }, this.timeoutMs);
643
670
  });
644
671
  }
645
672
  resolve(optionId) {
646
673
  if (this.settled || !this.resolveFn) return;
647
674
  this.settled = true;
675
+ this.clearTimeout();
648
676
  this.resolveFn(optionId);
649
677
  this.cleanup();
650
678
  }
651
679
  reject(reason) {
652
680
  if (this.settled || !this.rejectFn) return;
653
681
  this.settled = true;
682
+ this.clearTimeout();
654
683
  this.rejectFn(new Error(reason ?? "Permission rejected"));
655
684
  this.cleanup();
656
685
  }
@@ -664,6 +693,12 @@ var PermissionGate = class {
664
693
  get requestId() {
665
694
  return this.request?.id;
666
695
  }
696
+ clearTimeout() {
697
+ if (this.timeoutTimer) {
698
+ clearTimeout(this.timeoutTimer);
699
+ this.timeoutTimer = void 0;
700
+ }
701
+ }
667
702
  cleanup() {
668
703
  this.request = void 0;
669
704
  this.resolveFn = void 0;
@@ -674,6 +709,13 @@ var PermissionGate = class {
674
709
  // src/core/session.ts
675
710
  import { nanoid } from "nanoid";
676
711
  var moduleLog = createChildLogger({ module: "session" });
712
+ var VALID_TRANSITIONS = {
713
+ initializing: /* @__PURE__ */ new Set(["active", "error"]),
714
+ active: /* @__PURE__ */ new Set(["error", "finished", "cancelled"]),
715
+ error: /* @__PURE__ */ new Set(["active"]),
716
+ cancelled: /* @__PURE__ */ new Set(["active"]),
717
+ finished: /* @__PURE__ */ new Set()
718
+ };
677
719
  var Session = class extends TypedEmitter {
678
720
  id;
679
721
  channelId;
@@ -682,11 +724,9 @@ var Session = class extends TypedEmitter {
682
724
  workingDirectory;
683
725
  agentInstance;
684
726
  agentSessionId = "";
685
- status = "initializing";
727
+ _status = "initializing";
686
728
  name;
687
729
  createdAt = /* @__PURE__ */ new Date();
688
- adapter;
689
- // Set by wireSessionEvents for renaming
690
730
  dangerousMode = false;
691
731
  log;
692
732
  permissionGate = new PermissionGate();
@@ -703,21 +743,44 @@ var Session = class extends TypedEmitter {
703
743
  this.queue = new PromptQueue(
704
744
  (text) => this.processPrompt(text),
705
745
  (err) => {
706
- this.status = "error";
746
+ this.fail("Prompt execution failed");
707
747
  this.log.error({ err }, "Prompt execution failed");
708
748
  }
709
749
  );
710
750
  }
711
- // --- Backward-compatible properties ---
712
- /** @deprecated Use permissionGate directly */
713
- get pendingPermission() {
714
- if (!this.permissionGate.isPending) return void 0;
715
- return {
716
- requestId: this.permissionGate.requestId,
717
- resolve: (optionId) => this.permissionGate.resolve(optionId)
718
- };
719
- }
720
- set pendingPermission(val) {
751
+ // --- State Machine ---
752
+ get status() {
753
+ return this._status;
754
+ }
755
+ /** Transition to active — from initializing, error, or cancelled */
756
+ activate() {
757
+ this.transition("active");
758
+ }
759
+ /** Transition to error — from initializing or active */
760
+ fail(reason) {
761
+ this.transition("error");
762
+ this.emit("error", new Error(reason));
763
+ }
764
+ /** Transition to finished — from active only. Emits session_end for backward compat. */
765
+ finish(reason) {
766
+ this.transition("finished");
767
+ this.emit("session_end", reason ?? "completed");
768
+ }
769
+ /** Transition to cancelled — from active only (terminal session cancel) */
770
+ markCancelled() {
771
+ this.transition("cancelled");
772
+ }
773
+ transition(to) {
774
+ const from = this._status;
775
+ const allowed = VALID_TRANSITIONS[from];
776
+ if (!allowed?.has(to)) {
777
+ throw new Error(
778
+ `Invalid session transition: ${from} \u2192 ${to}`
779
+ );
780
+ }
781
+ this._status = to;
782
+ this.log.debug({ from, to }, "Session status transition");
783
+ this.emit("status_change", from, to);
721
784
  }
722
785
  /** Number of prompts waiting in queue */
723
786
  get queueDepth() {
@@ -735,7 +798,9 @@ var Session = class extends TypedEmitter {
735
798
  await this.runWarmup();
736
799
  return;
737
800
  }
738
- this.status = "active";
801
+ if (this._status === "initializing") {
802
+ this.activate();
803
+ }
739
804
  const promptStart = Date.now();
740
805
  this.log.debug("Prompt execution started");
741
806
  await this.agentInstance.prompt(text);
@@ -760,9 +825,7 @@ var Session = class extends TypedEmitter {
760
825
  );
761
826
  this.name = title.trim().slice(0, 50) || `Session ${this.id.slice(0, 6)}`;
762
827
  this.log.info({ name: this.name }, "Session auto-named");
763
- if (this.adapter && this.name) {
764
- await this.adapter.renameSessionThread(this.id, this.name);
765
- }
828
+ this.emit("named", this.name);
766
829
  } catch {
767
830
  this.name = `Session ${this.id.slice(0, 6)}`;
768
831
  } finally {
@@ -781,7 +844,7 @@ var Session = class extends TypedEmitter {
781
844
  try {
782
845
  const start = Date.now();
783
846
  await this.agentInstance.prompt('Reply with only "ready".');
784
- this.status = "active";
847
+ this.activate();
785
848
  this.log.info({ durationMs: Date.now() - start }, "Warm-up complete");
786
849
  } catch (err) {
787
850
  this.log.error({ err }, "Warm-up failed");
@@ -790,11 +853,11 @@ var Session = class extends TypedEmitter {
790
853
  this.resume();
791
854
  }
792
855
  }
793
- async cancel() {
856
+ /** Cancel the current prompt and clear the queue. Stays in active state. */
857
+ async abortPrompt() {
794
858
  this.queue.clear();
795
- this.log.info("Session cancelled");
859
+ this.log.info("Prompt aborted");
796
860
  await this.agentInstance.cancel();
797
- this.status = "active";
798
861
  }
799
862
  async destroy() {
800
863
  this.log.info("Session destroyed");
@@ -867,42 +930,13 @@ var SessionManager = class {
867
930
  registerSession(session) {
868
931
  this.sessions.set(session.id, session);
869
932
  }
870
- async updateSessionPlatform(sessionId, platform) {
871
- if (!this.store) return;
872
- const record = this.store.get(sessionId);
873
- if (record) {
874
- await this.store.save({ ...record, platform });
875
- }
876
- }
877
- async updateSessionActivity(sessionId) {
878
- if (!this.store) return;
879
- const record = this.store.get(sessionId);
880
- if (record) {
881
- await this.store.save({
882
- ...record,
883
- lastActiveAt: (/* @__PURE__ */ new Date()).toISOString()
884
- });
885
- }
886
- }
887
- async updateSessionStatus(sessionId, status) {
888
- if (!this.store) return;
889
- const record = this.store.get(sessionId);
890
- if (record) {
891
- await this.store.save({ ...record, status });
892
- }
893
- }
894
- async updateSessionDangerousMode(sessionId, dangerousMode) {
895
- if (!this.store) return;
896
- const record = this.store.get(sessionId);
897
- if (record) {
898
- await this.store.save({ ...record, dangerousMode });
899
- }
900
- }
901
- async updateSessionName(sessionId, name) {
933
+ async patchRecord(sessionId, patch) {
902
934
  if (!this.store) return;
903
935
  const record = this.store.get(sessionId);
904
936
  if (record) {
905
- await this.store.save({ ...record, name });
937
+ await this.store.save({ ...record, ...patch });
938
+ } else if (patch.sessionId) {
939
+ await this.store.save(patch);
906
940
  }
907
941
  }
908
942
  getSessionRecord(sessionId) {
@@ -911,7 +945,8 @@ var SessionManager = class {
911
945
  async cancelSession(sessionId) {
912
946
  const session = this.sessions.get(sessionId);
913
947
  if (session) {
914
- await session.cancel();
948
+ await session.abortPrompt();
949
+ session.markCancelled();
915
950
  }
916
951
  if (this.store) {
917
952
  const record = this.store.get(sessionId);
@@ -953,6 +988,133 @@ var SessionManager = class {
953
988
  }
954
989
  };
955
990
 
991
+ // src/core/session-bridge.ts
992
+ var log2 = createChildLogger({ module: "session-bridge" });
993
+ var SessionBridge = class {
994
+ constructor(session, adapter, deps) {
995
+ this.session = session;
996
+ this.adapter = adapter;
997
+ this.deps = deps;
998
+ }
999
+ connected = false;
1000
+ agentEventHandler;
1001
+ statusChangeHandler;
1002
+ namedHandler;
1003
+ connect() {
1004
+ if (this.connected) return;
1005
+ this.connected = true;
1006
+ this.wireAgentToSession();
1007
+ this.wireSessionToAdapter();
1008
+ this.wirePermissions();
1009
+ this.wireLifecycle();
1010
+ }
1011
+ disconnect() {
1012
+ if (!this.connected) return;
1013
+ this.connected = false;
1014
+ if (this.agentEventHandler) {
1015
+ this.session.off("agent_event", this.agentEventHandler);
1016
+ }
1017
+ if (this.statusChangeHandler) {
1018
+ this.session.off("status_change", this.statusChangeHandler);
1019
+ }
1020
+ if (this.namedHandler) {
1021
+ this.session.off("named", this.namedHandler);
1022
+ }
1023
+ this.session.agentInstance.onSessionUpdate = () => {
1024
+ };
1025
+ this.session.agentInstance.onPermissionRequest = async () => "";
1026
+ }
1027
+ wireAgentToSession() {
1028
+ this.session.agentInstance.onSessionUpdate = (event) => {
1029
+ this.session.emit("agent_event", event);
1030
+ };
1031
+ }
1032
+ wireSessionToAdapter() {
1033
+ const session = this.session;
1034
+ const ctx = {
1035
+ get id() {
1036
+ return session.id;
1037
+ },
1038
+ get workingDirectory() {
1039
+ return session.workingDirectory;
1040
+ }
1041
+ };
1042
+ this.agentEventHandler = (event) => {
1043
+ switch (event.type) {
1044
+ case "text":
1045
+ case "thought":
1046
+ case "tool_call":
1047
+ case "tool_update":
1048
+ case "plan":
1049
+ case "usage":
1050
+ this.adapter.sendMessage(
1051
+ this.session.id,
1052
+ this.deps.messageTransformer.transform(event, ctx)
1053
+ );
1054
+ break;
1055
+ case "session_end":
1056
+ this.session.finish(event.reason);
1057
+ this.adapter.cleanupSkillCommands(this.session.id);
1058
+ this.adapter.sendMessage(
1059
+ this.session.id,
1060
+ this.deps.messageTransformer.transform(event)
1061
+ );
1062
+ this.deps.notificationManager.notify(this.session.channelId, {
1063
+ sessionId: this.session.id,
1064
+ sessionName: this.session.name,
1065
+ type: "completed",
1066
+ summary: `Session "${this.session.name || this.session.id}" completed`
1067
+ });
1068
+ break;
1069
+ case "error":
1070
+ this.session.fail(event.message);
1071
+ this.adapter.cleanupSkillCommands(this.session.id);
1072
+ this.adapter.sendMessage(
1073
+ this.session.id,
1074
+ this.deps.messageTransformer.transform(event)
1075
+ );
1076
+ this.deps.notificationManager.notify(this.session.channelId, {
1077
+ sessionId: this.session.id,
1078
+ sessionName: this.session.name,
1079
+ type: "error",
1080
+ summary: event.message
1081
+ });
1082
+ break;
1083
+ case "commands_update":
1084
+ log2.debug({ commands: event.commands }, "Commands available");
1085
+ this.adapter.sendSkillCommands(this.session.id, event.commands);
1086
+ break;
1087
+ }
1088
+ };
1089
+ this.session.on("agent_event", this.agentEventHandler);
1090
+ }
1091
+ wirePermissions() {
1092
+ this.session.agentInstance.onPermissionRequest = async (request) => {
1093
+ this.session.emit("permission_request", request);
1094
+ const promise = this.session.permissionGate.setPending(request);
1095
+ await this.adapter.sendPermissionRequest(this.session.id, request);
1096
+ return promise;
1097
+ };
1098
+ }
1099
+ wireLifecycle() {
1100
+ this.statusChangeHandler = (from, to) => {
1101
+ this.deps.sessionManager.patchRecord(this.session.id, {
1102
+ status: to,
1103
+ lastActiveAt: (/* @__PURE__ */ new Date()).toISOString()
1104
+ });
1105
+ if (to === "finished" || to === "cancelled") {
1106
+ queueMicrotask(() => this.disconnect());
1107
+ }
1108
+ };
1109
+ this.session.on("status_change", this.statusChangeHandler);
1110
+ this.namedHandler = (name) => {
1111
+ this.deps.sessionManager.patchRecord(this.session.id, { name });
1112
+ this.adapter.renameSessionThread(this.session.id, name);
1113
+ };
1114
+ this.session.on("named", this.namedHandler);
1115
+ }
1116
+ };
1117
+
956
1118
  // src/core/notification.ts
957
1119
  var NotificationManager = class {
958
1120
  constructor(adapters) {
@@ -1062,7 +1224,7 @@ function parseContent(content) {
1062
1224
  }
1063
1225
 
1064
1226
  // src/core/message-transformer.ts
1065
- var log2 = createChildLogger({ module: "message-transformer" });
1227
+ var log3 = createChildLogger({ module: "message-transformer" });
1066
1228
  var MessageTransformer = class {
1067
1229
  constructor(tunnelService) {
1068
1230
  this.tunnelService = tunnelService;
@@ -1124,7 +1286,7 @@ var MessageTransformer = class {
1124
1286
  if (!this.tunnelService || !sessionContext) return;
1125
1287
  const name = "name" in event ? event.name || "" : "";
1126
1288
  const kind = "kind" in event ? event.kind : void 0;
1127
- log2.debug(
1289
+ log3.debug(
1128
1290
  { name, kind, status: event.status, hasContent: !!event.content },
1129
1291
  "enrichWithViewerLinks: inspecting event"
1130
1292
  );
@@ -1136,7 +1298,7 @@ var MessageTransformer = class {
1136
1298
  event.meta
1137
1299
  );
1138
1300
  if (!fileInfo) return;
1139
- log2.info(
1301
+ log3.info(
1140
1302
  {
1141
1303
  name,
1142
1304
  kind,
@@ -1178,7 +1340,7 @@ import os from "os";
1178
1340
  // src/core/session-store.ts
1179
1341
  import fs2 from "fs";
1180
1342
  import path2 from "path";
1181
- var log3 = createChildLogger({ module: "session-store" });
1343
+ var log4 = createChildLogger({ module: "session-store" });
1182
1344
  var DEBOUNCE_MS = 2e3;
1183
1345
  var JsonFileSessionStore = class {
1184
1346
  records = /* @__PURE__ */ new Map();
@@ -1260,7 +1422,7 @@ var JsonFileSessionStore = class {
1260
1422
  fs2.readFileSync(this.filePath, "utf-8")
1261
1423
  );
1262
1424
  if (raw.version !== 1) {
1263
- log3.warn(
1425
+ log4.warn(
1264
1426
  { version: raw.version },
1265
1427
  "Unknown session store version, skipping load"
1266
1428
  );
@@ -1269,9 +1431,9 @@ var JsonFileSessionStore = class {
1269
1431
  for (const [id, record] of Object.entries(raw.sessions)) {
1270
1432
  this.records.set(id, record);
1271
1433
  }
1272
- log3.info({ count: this.records.size }, "Loaded session records");
1434
+ log4.info({ count: this.records.size }, "Loaded session records");
1273
1435
  } catch (err) {
1274
- log3.error({ err }, "Failed to load session store");
1436
+ log4.error({ err }, "Failed to load session store");
1275
1437
  }
1276
1438
  }
1277
1439
  cleanup() {
@@ -1287,7 +1449,7 @@ var JsonFileSessionStore = class {
1287
1449
  }
1288
1450
  }
1289
1451
  if (removed > 0) {
1290
- log3.info({ removed }, "Cleaned up expired session records");
1452
+ log4.info({ removed }, "Cleaned up expired session records");
1291
1453
  this.scheduleDiskWrite();
1292
1454
  }
1293
1455
  }
@@ -1300,9 +1462,10 @@ var JsonFileSessionStore = class {
1300
1462
  };
1301
1463
 
1302
1464
  // src/core/core.ts
1303
- var log4 = createChildLogger({ module: "core" });
1465
+ var log5 = createChildLogger({ module: "core" });
1304
1466
  var OpenACPCore = class {
1305
1467
  configManager;
1468
+ agentCatalog;
1306
1469
  agentManager;
1307
1470
  sessionManager;
1308
1471
  notificationManager;
@@ -1316,7 +1479,9 @@ var OpenACPCore = class {
1316
1479
  constructor(configManager) {
1317
1480
  this.configManager = configManager;
1318
1481
  const config = configManager.get();
1319
- this.agentManager = new AgentManager(config);
1482
+ this.agentCatalog = new AgentCatalog();
1483
+ this.agentCatalog.load();
1484
+ this.agentManager = new AgentManager(this.agentCatalog);
1320
1485
  const storePath = path3.join(os.homedir(), ".openacp", "sessions.json");
1321
1486
  this.sessionStore = new JsonFileSessionStore(
1322
1487
  storePath,
@@ -1325,6 +1490,13 @@ var OpenACPCore = class {
1325
1490
  this.sessionManager = new SessionManager(this.sessionStore);
1326
1491
  this.notificationManager = new NotificationManager(this.adapters);
1327
1492
  this.messageTransformer = new MessageTransformer();
1493
+ this.configManager.on("config:changed", async ({ path: configPath, value }) => {
1494
+ if (configPath === "logging.level" && typeof value === "string") {
1495
+ const { setLogLevel: setLogLevel2 } = await import("./log-SPS2S6FO.js");
1496
+ setLogLevel2(value);
1497
+ log5.info({ level: value }, "Log level changed at runtime");
1498
+ }
1499
+ });
1328
1500
  }
1329
1501
  get tunnelService() {
1330
1502
  return this._tunnelService;
@@ -1337,6 +1509,9 @@ var OpenACPCore = class {
1337
1509
  this.adapters.set(name, adapter);
1338
1510
  }
1339
1511
  async start() {
1512
+ this.agentCatalog.refreshRegistryIfStale().catch((err) => {
1513
+ log5.warn({ err }, "Background registry refresh failed");
1514
+ });
1340
1515
  for (const adapter of this.adapters.values()) {
1341
1516
  await adapter.start();
1342
1517
  }
@@ -1358,7 +1533,7 @@ var OpenACPCore = class {
1358
1533
  // --- Message Routing ---
1359
1534
  async handleMessage(message) {
1360
1535
  const config = this.configManager.get();
1361
- log4.debug(
1536
+ log5.debug(
1362
1537
  {
1363
1538
  channelId: message.channelId,
1364
1539
  threadId: message.threadId,
@@ -1368,7 +1543,7 @@ var OpenACPCore = class {
1368
1543
  );
1369
1544
  if (config.security.allowedUserIds.length > 0) {
1370
1545
  if (!config.security.allowedUserIds.includes(message.userId)) {
1371
- log4.warn(
1546
+ log5.warn(
1372
1547
  { userId: message.userId },
1373
1548
  "Rejected message from unauthorized user"
1374
1549
  );
@@ -1377,7 +1552,7 @@ var OpenACPCore = class {
1377
1552
  }
1378
1553
  const activeSessions = this.sessionManager.listSessions().filter((s) => s.status === "active" || s.status === "initializing");
1379
1554
  if (activeSessions.length >= config.security.maxConcurrentSessions) {
1380
- log4.warn(
1555
+ log5.warn(
1381
1556
  {
1382
1557
  userId: message.userId,
1383
1558
  currentCount: activeSessions.length,
@@ -1401,28 +1576,88 @@ var OpenACPCore = class {
1401
1576
  if (!session) {
1402
1577
  session = await this.lazyResume(message) ?? void 0;
1403
1578
  }
1404
- if (!session) return;
1405
- this.sessionManager.updateSessionActivity(session.id);
1579
+ if (!session) {
1580
+ log5.warn(
1581
+ { channelId: message.channelId, threadId: message.threadId },
1582
+ "No session found for thread (in-memory miss + lazy resume returned null)"
1583
+ );
1584
+ return;
1585
+ }
1586
+ this.sessionManager.patchRecord(session.id, { lastActiveAt: (/* @__PURE__ */ new Date()).toISOString() });
1406
1587
  await session.enqueuePrompt(message.text);
1407
1588
  }
1589
+ // --- Unified Session Creation Pipeline ---
1590
+ async createSession(params) {
1591
+ const agentInstance = params.resumeAgentSessionId ? await this.agentManager.resume(
1592
+ params.agentName,
1593
+ params.workingDirectory,
1594
+ params.resumeAgentSessionId
1595
+ ) : await this.agentManager.spawn(
1596
+ params.agentName,
1597
+ params.workingDirectory
1598
+ );
1599
+ const session = new Session({
1600
+ id: params.existingSessionId,
1601
+ channelId: params.channelId,
1602
+ agentName: params.agentName,
1603
+ workingDirectory: params.workingDirectory,
1604
+ agentInstance
1605
+ });
1606
+ session.agentSessionId = agentInstance.sessionId;
1607
+ if (params.initialName) {
1608
+ session.name = params.initialName;
1609
+ }
1610
+ this.sessionManager.registerSession(session);
1611
+ const adapter = this.adapters.get(params.channelId);
1612
+ if (params.createThread && adapter) {
1613
+ const threadId = await adapter.createSessionThread(
1614
+ session.id,
1615
+ params.initialName ?? `\u{1F504} ${params.agentName} \u2014 New Session`
1616
+ );
1617
+ session.threadId = threadId;
1618
+ }
1619
+ if (adapter) {
1620
+ const bridge = this.createBridge(session, adapter);
1621
+ bridge.connect();
1622
+ }
1623
+ const existingRecord = this.sessionStore?.get(session.id);
1624
+ const platform = {
1625
+ ...existingRecord?.platform ?? {}
1626
+ };
1627
+ if (session.threadId) {
1628
+ platform.topicId = Number(session.threadId);
1629
+ }
1630
+ await this.sessionManager.patchRecord(session.id, {
1631
+ sessionId: session.id,
1632
+ agentSessionId: agentInstance.sessionId,
1633
+ agentName: params.agentName,
1634
+ workingDir: params.workingDirectory,
1635
+ channelId: params.channelId,
1636
+ status: session.status,
1637
+ createdAt: session.createdAt.toISOString(),
1638
+ lastActiveAt: (/* @__PURE__ */ new Date()).toISOString(),
1639
+ name: session.name,
1640
+ platform
1641
+ });
1642
+ log5.info(
1643
+ { sessionId: session.id, agentName: params.agentName },
1644
+ "Session created via pipeline"
1645
+ );
1646
+ return session;
1647
+ }
1408
1648
  async handleNewSession(channelId, agentName, workspacePath) {
1409
1649
  const config = this.configManager.get();
1410
1650
  const resolvedAgent = agentName || config.defaultAgent;
1411
- log4.info({ channelId, agentName: resolvedAgent }, "New session request");
1651
+ log5.info({ channelId, agentName: resolvedAgent }, "New session request");
1652
+ const agentDef = this.agentCatalog.resolve(resolvedAgent);
1412
1653
  const resolvedWorkspace = this.configManager.resolveWorkspace(
1413
- workspacePath || config.agents[resolvedAgent]?.workingDirectory
1654
+ workspacePath || agentDef?.workingDirectory
1414
1655
  );
1415
- const session = await this.sessionManager.createSession(
1656
+ return this.createSession({
1416
1657
  channelId,
1417
- resolvedAgent,
1418
- resolvedWorkspace,
1419
- this.agentManager
1420
- );
1421
- const adapter = this.adapters.get(channelId);
1422
- if (adapter) {
1423
- this.wireSessionEvents(session, adapter);
1424
- }
1425
- return session;
1658
+ agentName: resolvedAgent,
1659
+ workingDirectory: resolvedWorkspace
1660
+ });
1426
1661
  }
1427
1662
  async adoptSession(agentName, agentSessionId, cwd) {
1428
1663
  const caps = getAgentCapabilities(agentName);
@@ -1445,10 +1680,10 @@ var OpenACPCore = class {
1445
1680
  if (existingRecord) {
1446
1681
  const platform = existingRecord.platform;
1447
1682
  if (platform?.topicId) {
1448
- const adapter2 = this.adapters.values().next().value;
1449
- if (adapter2) {
1683
+ const adapter = this.adapters.values().next().value;
1684
+ if (adapter) {
1450
1685
  try {
1451
- await adapter2.sendMessage(existingRecord.sessionId, {
1686
+ await adapter.sendMessage(existingRecord.sessionId, {
1452
1687
  type: "text",
1453
1688
  text: "Session resumed from CLI."
1454
1689
  });
@@ -1463,9 +1698,21 @@ var OpenACPCore = class {
1463
1698
  };
1464
1699
  }
1465
1700
  }
1466
- let agentInstance;
1701
+ const firstEntry = this.adapters.entries().next().value;
1702
+ if (!firstEntry) {
1703
+ return { ok: false, error: "no_adapter", message: "No channel adapter registered" };
1704
+ }
1705
+ const [adapterChannelId] = firstEntry;
1706
+ let session;
1467
1707
  try {
1468
- agentInstance = await this.agentManager.resume(agentName, cwd, agentSessionId);
1708
+ session = await this.createSession({
1709
+ channelId: adapterChannelId,
1710
+ agentName,
1711
+ workingDirectory: cwd,
1712
+ resumeAgentSessionId: agentSessionId,
1713
+ createThread: true,
1714
+ initialName: "Adopted session"
1715
+ });
1469
1716
  } catch (err) {
1470
1717
  return {
1471
1718
  ok: false,
@@ -1473,43 +1720,14 @@ var OpenACPCore = class {
1473
1720
  message: `Failed to resume session: ${err instanceof Error ? err.message : String(err)}`
1474
1721
  };
1475
1722
  }
1476
- const session = new Session({
1477
- channelId: "api",
1478
- agentName,
1479
- workingDirectory: cwd,
1480
- agentInstance
1723
+ await this.sessionManager.patchRecord(session.id, {
1724
+ originalAgentSessionId: agentSessionId,
1725
+ platform: { topicId: Number(session.threadId) }
1481
1726
  });
1482
- session.agentSessionId = agentInstance.sessionId;
1483
- this.sessionManager.registerSession(session);
1484
- const firstEntry = this.adapters.entries().next().value;
1485
- if (!firstEntry) {
1486
- await session.destroy();
1487
- return { ok: false, error: "no_adapter", message: "No channel adapter registered" };
1488
- }
1489
- const [adapterChannelId, adapter] = firstEntry;
1490
- const threadId = await adapter.createSessionThread(session.id, session.name ?? "Adopted session");
1491
- session.channelId = adapterChannelId;
1492
- session.threadId = threadId;
1493
- this.wireSessionEvents(session, adapter);
1494
- if (this.sessionStore) {
1495
- await this.sessionStore.save({
1496
- sessionId: session.id,
1497
- agentSessionId: agentInstance.sessionId,
1498
- originalAgentSessionId: agentSessionId,
1499
- agentName,
1500
- workingDir: cwd,
1501
- channelId: adapterChannelId,
1502
- status: "active",
1503
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1504
- lastActiveAt: (/* @__PURE__ */ new Date()).toISOString(),
1505
- name: session.name,
1506
- platform: { topicId: Number(threadId) }
1507
- });
1508
- }
1509
1727
  return {
1510
1728
  ok: true,
1511
1729
  sessionId: session.id,
1512
- threadId,
1730
+ threadId: session.threadId,
1513
1731
  status: "adopted"
1514
1732
  };
1515
1733
  }
@@ -1544,45 +1762,54 @@ var OpenACPCore = class {
1544
1762
  message.channelId,
1545
1763
  (p) => String(p.topicId) === message.threadId
1546
1764
  );
1547
- if (!record) return null;
1548
- if (record.status === "cancelled" || record.status === "error") return null;
1765
+ if (!record) {
1766
+ log5.debug(
1767
+ { threadId: message.threadId, channelId: message.channelId },
1768
+ "No session record found for thread"
1769
+ );
1770
+ return null;
1771
+ }
1772
+ if (record.status === "cancelled" || record.status === "error") {
1773
+ log5.debug(
1774
+ { threadId: message.threadId, sessionId: record.sessionId, status: record.status },
1775
+ "Skipping resume of cancelled/error session"
1776
+ );
1777
+ return null;
1778
+ }
1779
+ log5.info(
1780
+ { threadId: message.threadId, sessionId: record.sessionId, status: record.status },
1781
+ "Lazy resume: found record, attempting resume"
1782
+ );
1549
1783
  const resumePromise = (async () => {
1550
1784
  try {
1551
- const agentInstance = await this.agentManager.resume(
1552
- record.agentName,
1553
- record.workingDir,
1554
- record.agentSessionId
1555
- );
1556
- const session = new Session({
1557
- id: record.sessionId,
1785
+ const session = await this.createSession({
1558
1786
  channelId: record.channelId,
1559
1787
  agentName: record.agentName,
1560
1788
  workingDirectory: record.workingDir,
1561
- agentInstance
1789
+ resumeAgentSessionId: record.agentSessionId,
1790
+ existingSessionId: record.sessionId,
1791
+ initialName: record.name
1562
1792
  });
1563
1793
  session.threadId = message.threadId;
1564
- session.agentSessionId = agentInstance.sessionId;
1565
- session.status = "active";
1566
- session.name = record.name;
1794
+ session.activate();
1567
1795
  session.dangerousMode = record.dangerousMode ?? false;
1568
- this.sessionManager.registerSession(session);
1569
- const adapter = this.adapters.get(message.channelId);
1570
- if (adapter) {
1571
- this.wireSessionEvents(session, adapter);
1572
- }
1573
- await store.save({
1574
- ...record,
1575
- agentSessionId: agentInstance.sessionId,
1576
- status: "active",
1577
- lastActiveAt: (/* @__PURE__ */ new Date()).toISOString()
1578
- });
1579
- log4.info(
1796
+ log5.info(
1580
1797
  { sessionId: session.id, threadId: message.threadId },
1581
1798
  "Lazy resume successful"
1582
1799
  );
1583
1800
  return session;
1584
1801
  } catch (err) {
1585
- log4.error({ err, record }, "Lazy resume failed");
1802
+ log5.error({ err, record }, "Lazy resume failed");
1803
+ const adapter = this.adapters.get(message.channelId);
1804
+ if (adapter) {
1805
+ try {
1806
+ await adapter.sendMessage(message.threadId, {
1807
+ type: "error",
1808
+ text: `\u26A0\uFE0F Failed to resume session: ${err instanceof Error ? err.message : String(err)}`
1809
+ });
1810
+ } catch {
1811
+ }
1812
+ }
1586
1813
  return null;
1587
1814
  } finally {
1588
1815
  this.resumeLocks.delete(lockKey);
@@ -1592,73 +1819,12 @@ var OpenACPCore = class {
1592
1819
  return resumePromise;
1593
1820
  }
1594
1821
  // --- Event Wiring ---
1595
- // Public adapters call this for assistant session wiring
1596
- wireSessionEvents(session, adapter) {
1597
- session.adapter = adapter;
1598
- session.agentInstance.onSessionUpdate = (event) => {
1599
- session.emit("agent_event", event);
1600
- };
1601
- session.agentInstance.onPermissionRequest = async (request) => {
1602
- session.emit("permission_request", request);
1603
- const promise = session.permissionGate.setPending(request);
1604
- await adapter.sendPermissionRequest(session.id, request);
1605
- return promise;
1606
- };
1607
- const sessionContext = {
1608
- get id() {
1609
- return session.id;
1610
- },
1611
- get workingDirectory() {
1612
- return session.workingDirectory;
1613
- }
1614
- };
1615
- session.on("agent_event", (event) => {
1616
- switch (event.type) {
1617
- case "text":
1618
- case "thought":
1619
- case "tool_call":
1620
- case "tool_update":
1621
- case "plan":
1622
- case "usage":
1623
- adapter.sendMessage(
1624
- session.id,
1625
- this.messageTransformer.transform(event, sessionContext)
1626
- );
1627
- break;
1628
- case "session_end":
1629
- session.status = "finished";
1630
- this.sessionManager.updateSessionStatus(session.id, "finished");
1631
- adapter.cleanupSkillCommands(session.id);
1632
- adapter.sendMessage(
1633
- session.id,
1634
- this.messageTransformer.transform(event)
1635
- );
1636
- this.notificationManager.notify(session.channelId, {
1637
- sessionId: session.id,
1638
- sessionName: session.name,
1639
- type: "completed",
1640
- summary: `Session "${session.name || session.id}" completed`
1641
- });
1642
- break;
1643
- case "error":
1644
- this.sessionManager.updateSessionStatus(session.id, "error");
1645
- adapter.cleanupSkillCommands(session.id);
1646
- adapter.sendMessage(
1647
- session.id,
1648
- this.messageTransformer.transform(event)
1649
- );
1650
- this.notificationManager.notify(session.channelId, {
1651
- sessionId: session.id,
1652
- sessionName: session.name,
1653
- type: "error",
1654
- summary: event.message
1655
- });
1656
- break;
1657
- case "commands_update":
1658
- log4.debug({ commands: event.commands }, "Commands available");
1659
- adapter.sendSkillCommands(session.id, event.commands);
1660
- break;
1661
- }
1822
+ /** Create a SessionBridge for the given session and adapter */
1823
+ createBridge(session, adapter) {
1824
+ return new SessionBridge(session, adapter, {
1825
+ messageTransformer: this.messageTransformer,
1826
+ notificationManager: this.notificationManager,
1827
+ sessionManager: this.sessionManager
1662
1828
  });
1663
1829
  }
1664
1830
  };
@@ -1684,7 +1850,7 @@ import * as fs3 from "fs";
1684
1850
  import * as path4 from "path";
1685
1851
  import * as os2 from "os";
1686
1852
  import { fileURLToPath } from "url";
1687
- var log5 = createChildLogger({ module: "api-server" });
1853
+ var log6 = createChildLogger({ module: "api-server" });
1688
1854
  var DEFAULT_PORT_FILE = path4.join(os2.homedir(), ".openacp", "api.port");
1689
1855
  var cachedVersion;
1690
1856
  function getVersion() {
@@ -1730,7 +1896,7 @@ var ApiServer = class {
1730
1896
  await new Promise((resolve2, reject) => {
1731
1897
  this.server.on("error", (err) => {
1732
1898
  if (err.code === "EADDRINUSE") {
1733
- log5.warn({ port: this.config.port }, "API port in use, continuing without API server");
1899
+ log6.warn({ port: this.config.port }, "API port in use, continuing without API server");
1734
1900
  this.server = null;
1735
1901
  resolve2();
1736
1902
  } else {
@@ -1743,7 +1909,7 @@ var ApiServer = class {
1743
1909
  this.actualPort = addr.port;
1744
1910
  }
1745
1911
  this.writePortFile();
1746
- log5.info({ host: this.config.host, port: this.actualPort }, "API server listening");
1912
+ log6.info({ host: this.config.host, port: this.actualPort }, "API server listening");
1747
1913
  resolve2();
1748
1914
  });
1749
1915
  });
@@ -1799,6 +1965,8 @@ var ApiServer = class {
1799
1965
  await this.handleHealth(res);
1800
1966
  } else if (method === "GET" && url === "/api/version") {
1801
1967
  await this.handleVersion(res);
1968
+ } else if (method === "GET" && url === "/api/config/editable") {
1969
+ await this.handleGetEditableConfig(res);
1802
1970
  } else if (method === "GET" && url === "/api/config") {
1803
1971
  await this.handleGetConfig(res);
1804
1972
  } else if (method === "PATCH" && url === "/api/config") {
@@ -1822,7 +1990,7 @@ var ApiServer = class {
1822
1990
  this.sendJson(res, 404, { error: "Not found" });
1823
1991
  }
1824
1992
  } catch (err) {
1825
- log5.error({ err }, "API request error");
1993
+ log6.error({ err }, "API request error");
1826
1994
  this.sendJson(res, 500, { error: "Internal server error" });
1827
1995
  }
1828
1996
  }
@@ -1850,24 +2018,25 @@ var ApiServer = class {
1850
2018
  }
1851
2019
  const [adapterId, adapter] = this.core.adapters.entries().next().value ?? [null, null];
1852
2020
  const channelId = adapterId ?? "api";
1853
- const session = await this.core.handleNewSession(channelId, agent, workspace);
1854
- if (adapter) {
1855
- try {
1856
- const threadId = await adapter.createSessionThread(session.id, `\u{1F504} ${session.agentName} \u2014 New Session`);
1857
- session.threadId = threadId;
1858
- this.core.wireSessionEvents(session, adapter);
1859
- } catch (err) {
1860
- log5.warn({ err, sessionId: session.id }, "Failed to create session thread on adapter, running headless");
1861
- }
1862
- }
2021
+ const resolvedAgent = agent || config.defaultAgent;
2022
+ const resolvedWorkspace = this.core.configManager.resolveWorkspace(
2023
+ workspace || config.agents[resolvedAgent]?.workingDirectory
2024
+ );
2025
+ const session = await this.core.createSession({
2026
+ channelId,
2027
+ agentName: resolvedAgent,
2028
+ workingDirectory: resolvedWorkspace,
2029
+ createThread: !!adapter,
2030
+ initialName: `\u{1F504} ${resolvedAgent} \u2014 New Session`
2031
+ });
1863
2032
  if (!adapter) {
1864
2033
  session.agentInstance.onPermissionRequest = async (request) => {
1865
2034
  const allowOption = request.options.find((o) => o.isAllow);
1866
- log5.debug({ sessionId: session.id, permissionId: request.id, option: allowOption?.id }, "Auto-approving permission for API session");
2035
+ log6.debug({ sessionId: session.id, permissionId: request.id, option: allowOption?.id }, "Auto-approving permission for API session");
1867
2036
  return allowOption?.id ?? request.options[0]?.id ?? "";
1868
2037
  };
1869
2038
  }
1870
- session.warmup().catch((err) => log5.warn({ err, sessionId: session.id }, "API session warmup failed"));
2039
+ session.warmup().catch((err) => log6.warn({ err, sessionId: session.id }, "API session warmup failed"));
1871
2040
  this.sendJson(res, 200, {
1872
2041
  sessionId: session.id,
1873
2042
  agent: session.agentName,
@@ -1949,7 +2118,7 @@ var ApiServer = class {
1949
2118
  return;
1950
2119
  }
1951
2120
  session.dangerousMode = enabled;
1952
- await this.core.sessionManager.updateSessionDangerousMode(sessionId, enabled);
2121
+ await this.core.sessionManager.patchRecord(sessionId, { dangerousMode: enabled });
1953
2122
  this.sendJson(res, 200, { ok: true, dangerousMode: enabled });
1954
2123
  }
1955
2124
  async handleHealth(res) {
@@ -1977,6 +2146,21 @@ var ApiServer = class {
1977
2146
  async handleVersion(res) {
1978
2147
  this.sendJson(res, 200, { version: getVersion() });
1979
2148
  }
2149
+ async handleGetEditableConfig(res) {
2150
+ const { getSafeFields: getSafeFields2, resolveOptions: resolveOptions2, getConfigValue: getConfigValue2 } = await import("./config-registry-SNKA2EH2.js");
2151
+ const config = this.core.configManager.get();
2152
+ const safeFields = getSafeFields2();
2153
+ const fields = safeFields.map((def) => ({
2154
+ path: def.path,
2155
+ displayName: def.displayName,
2156
+ group: def.group,
2157
+ type: def.type,
2158
+ options: resolveOptions2(def, config),
2159
+ value: getConfigValue2(config, def.path),
2160
+ hotReload: def.hotReload
2161
+ }));
2162
+ this.sendJson(res, 200, { fields });
2163
+ }
1980
2164
  async handleGetConfig(res) {
1981
2165
  const config = this.core.configManager.get();
1982
2166
  this.sendJson(res, 200, { config: redactConfig(config) });
@@ -2017,7 +2201,7 @@ var ApiServer = class {
2017
2201
  return;
2018
2202
  }
2019
2203
  target[lastKey] = value;
2020
- const { ConfigSchema } = await import("./config-H2DSEHNW.js");
2204
+ const { ConfigSchema } = await import("./config-PCPIBPUA.js");
2021
2205
  const result = ConfigSchema.safeParse(cloned);
2022
2206
  if (!result.success) {
2023
2207
  this.sendJson(res, 400, {
@@ -2033,12 +2217,9 @@ var ApiServer = class {
2033
2217
  updateTarget = updateTarget[parts[i]];
2034
2218
  }
2035
2219
  updateTarget[lastKey] = value;
2036
- await this.core.configManager.save(updates);
2037
- const RESTART_PREFIXES = ["api.port", "api.host", "runMode", "channels.", "tunnel.", "agents."];
2038
- const needsRestart = RESTART_PREFIXES.some(
2039
- (prefix) => configPath.startsWith(prefix) || configPath === prefix.replace(/\.$/, "")
2040
- // exact match for non-wildcard
2041
- );
2220
+ await this.core.configManager.save(updates, configPath);
2221
+ const { isHotReloadable: isHotReloadable2 } = await import("./config-registry-SNKA2EH2.js");
2222
+ const needsRestart = !isHotReloadable2(configPath);
2042
2223
  this.sendJson(res, 200, {
2043
2224
  ok: true,
2044
2225
  needsRestart,
@@ -2097,7 +2278,7 @@ var ApiServer = class {
2097
2278
  this.sendJson(res, 404, { error: `Session "${sessionId}" not found` });
2098
2279
  return;
2099
2280
  }
2100
- await session.cancel();
2281
+ await session.abortPrompt();
2101
2282
  this.sendJson(res, 200, { ok: true });
2102
2283
  }
2103
2284
  async handleListSessions(res) {
@@ -2206,7 +2387,7 @@ var ApiServer = class {
2206
2387
  };
2207
2388
 
2208
2389
  // src/core/topic-manager.ts
2209
- var log6 = createChildLogger({ module: "topic-manager" });
2390
+ var log7 = createChildLogger({ module: "topic-manager" });
2210
2391
  var TopicManager = class {
2211
2392
  constructor(sessionManager, adapter, systemTopicIds) {
2212
2393
  this.sessionManager = sessionManager;
@@ -2245,7 +2426,7 @@ var TopicManager = class {
2245
2426
  try {
2246
2427
  await this.adapter.deleteSessionThread(sessionId);
2247
2428
  } catch (err) {
2248
- log6.warn({ err, sessionId, topicId }, "Failed to delete platform thread, removing record anyway");
2429
+ log7.warn({ err, sessionId, topicId }, "Failed to delete platform thread, removing record anyway");
2249
2430
  }
2250
2431
  }
2251
2432
  await this.sessionManager.removeRecord(sessionId);
@@ -2268,7 +2449,7 @@ var TopicManager = class {
2268
2449
  try {
2269
2450
  await this.adapter.deleteSessionThread(record.sessionId);
2270
2451
  } catch (err) {
2271
- log6.warn({ err, sessionId: record.sessionId }, "Failed to delete platform thread during cleanup");
2452
+ log7.warn({ err, sessionId: record.sessionId }, "Failed to delete platform thread during cleanup");
2272
2453
  }
2273
2454
  }
2274
2455
  await this.sessionManager.removeRecord(record.sessionId);
@@ -2289,6 +2470,40 @@ var TopicManager = class {
2289
2470
  // src/adapters/telegram/adapter.ts
2290
2471
  import { Bot } from "grammy";
2291
2472
 
2473
+ // src/adapters/telegram/topics.ts
2474
+ async function ensureTopics(bot, chatId, config, saveConfig) {
2475
+ let notificationTopicId = config.notificationTopicId;
2476
+ let assistantTopicId = config.assistantTopicId;
2477
+ if (notificationTopicId === null) {
2478
+ const topic = await bot.api.createForumTopic(chatId, "\u{1F4CB} Notifications");
2479
+ notificationTopicId = topic.message_thread_id;
2480
+ await saveConfig({ notificationTopicId });
2481
+ }
2482
+ if (assistantTopicId === null) {
2483
+ const topic = await bot.api.createForumTopic(chatId, "\u{1F916} Assistant");
2484
+ assistantTopicId = topic.message_thread_id;
2485
+ await saveConfig({ assistantTopicId });
2486
+ }
2487
+ return { notificationTopicId, assistantTopicId };
2488
+ }
2489
+ async function createSessionTopic(bot, chatId, name) {
2490
+ const topic = await bot.api.createForumTopic(chatId, name);
2491
+ return topic.message_thread_id;
2492
+ }
2493
+ async function renameSessionTopic(bot, chatId, threadId, name) {
2494
+ try {
2495
+ await bot.api.editForumTopic(chatId, threadId, { name });
2496
+ } catch {
2497
+ }
2498
+ }
2499
+ function buildDeepLink(chatId, messageId) {
2500
+ const cleanId = String(chatId).replace("-100", "");
2501
+ return `https://t.me/c/${cleanId}/${messageId}`;
2502
+ }
2503
+
2504
+ // src/adapters/telegram/commands/new-session.ts
2505
+ import { InlineKeyboard as InlineKeyboard2 } from "grammy";
2506
+
2292
2507
  // src/adapters/telegram/formatting.ts
2293
2508
  function escapeHtml(text) {
2294
2509
  if (!text) return "";
@@ -2452,224 +2667,9 @@ function splitMessage(text, maxLength = 3800) {
2452
2667
  return chunks;
2453
2668
  }
2454
2669
 
2455
- // src/adapters/telegram/streaming.ts
2456
- var FLUSH_INTERVAL = 5e3;
2457
- var MessageDraft = class {
2458
- constructor(bot, chatId, threadId, sendQueue, sessionId) {
2459
- this.bot = bot;
2460
- this.chatId = chatId;
2461
- this.threadId = threadId;
2462
- this.sendQueue = sendQueue;
2463
- this.sessionId = sessionId;
2464
- }
2465
- buffer = "";
2466
- messageId;
2467
- firstFlushPending = false;
2468
- flushTimer;
2469
- flushPromise = Promise.resolve();
2470
- lastSentBuffer = "";
2471
- displayTruncated = false;
2472
- append(text) {
2473
- if (!text) return;
2474
- this.buffer += text;
2475
- this.scheduleFlush();
2476
- }
2477
- scheduleFlush() {
2478
- if (this.flushTimer) return;
2479
- this.flushTimer = setTimeout(() => {
2480
- this.flushTimer = void 0;
2481
- this.flushPromise = this.flushPromise.then(() => this.flush()).catch(() => {
2482
- });
2483
- }, FLUSH_INTERVAL);
2484
- }
2485
- async flush() {
2486
- if (!this.buffer) return;
2487
- if (this.firstFlushPending) return;
2488
- const snapshot = this.buffer;
2489
- let html = markdownToTelegramHtml(snapshot);
2490
- if (!html) return;
2491
- let truncated = false;
2492
- if (html.length > 4096) {
2493
- const ratio = 4e3 / html.length;
2494
- const targetLen = Math.floor(snapshot.length * ratio);
2495
- let cutAt = snapshot.lastIndexOf("\n", targetLen);
2496
- if (cutAt < targetLen * 0.5) cutAt = targetLen;
2497
- html = markdownToTelegramHtml(snapshot.slice(0, cutAt) + "\n\u2026");
2498
- truncated = true;
2499
- if (html.length > 4096) {
2500
- html = html.slice(0, 4090) + "\n\u2026";
2501
- }
2502
- }
2503
- if (!this.messageId) {
2504
- this.firstFlushPending = true;
2505
- try {
2506
- const result = await this.sendQueue.enqueue(
2507
- () => this.bot.api.sendMessage(this.chatId, html, {
2508
- message_thread_id: this.threadId,
2509
- parse_mode: "HTML",
2510
- disable_notification: true
2511
- }),
2512
- { type: "other" }
2513
- );
2514
- if (result) {
2515
- this.messageId = result.message_id;
2516
- if (!truncated) {
2517
- this.lastSentBuffer = snapshot;
2518
- this.displayTruncated = false;
2519
- } else {
2520
- this.displayTruncated = true;
2521
- }
2522
- }
2523
- } catch {
2524
- } finally {
2525
- this.firstFlushPending = false;
2526
- }
2527
- } else {
2528
- try {
2529
- const result = await this.sendQueue.enqueue(
2530
- () => this.bot.api.editMessageText(this.chatId, this.messageId, html, {
2531
- parse_mode: "HTML"
2532
- }),
2533
- { type: "text", key: this.sessionId }
2534
- );
2535
- if (result !== void 0) {
2536
- if (!truncated) {
2537
- this.lastSentBuffer = snapshot;
2538
- this.displayTruncated = false;
2539
- } else {
2540
- this.displayTruncated = true;
2541
- }
2542
- }
2543
- } catch {
2544
- }
2545
- }
2546
- }
2547
- async finalize() {
2548
- if (this.flushTimer) {
2549
- clearTimeout(this.flushTimer);
2550
- this.flushTimer = void 0;
2551
- }
2552
- await this.flushPromise;
2553
- if (!this.buffer) return this.messageId;
2554
- if (this.messageId && this.buffer === this.lastSentBuffer && !this.displayTruncated) {
2555
- return this.messageId;
2556
- }
2557
- const fullHtml = markdownToTelegramHtml(this.buffer);
2558
- if (fullHtml.length <= 4096) {
2559
- try {
2560
- if (this.messageId) {
2561
- await this.sendQueue.enqueue(
2562
- () => this.bot.api.editMessageText(this.chatId, this.messageId, fullHtml, {
2563
- parse_mode: "HTML"
2564
- }),
2565
- { type: "other" }
2566
- );
2567
- } else {
2568
- const msg = await this.sendQueue.enqueue(
2569
- () => this.bot.api.sendMessage(this.chatId, fullHtml, {
2570
- message_thread_id: this.threadId,
2571
- parse_mode: "HTML",
2572
- disable_notification: true
2573
- }),
2574
- { type: "other" }
2575
- );
2576
- if (msg) this.messageId = msg.message_id;
2577
- }
2578
- return this.messageId;
2579
- } catch {
2580
- }
2581
- }
2582
- const mdChunks = splitMessage(this.buffer);
2583
- for (let i = 0; i < mdChunks.length; i++) {
2584
- const html = markdownToTelegramHtml(mdChunks[i]);
2585
- try {
2586
- if (i === 0 && this.messageId) {
2587
- await this.sendQueue.enqueue(
2588
- () => this.bot.api.editMessageText(this.chatId, this.messageId, html, {
2589
- parse_mode: "HTML"
2590
- }),
2591
- { type: "other" }
2592
- );
2593
- } else {
2594
- const msg = await this.sendQueue.enqueue(
2595
- () => this.bot.api.sendMessage(this.chatId, html, {
2596
- message_thread_id: this.threadId,
2597
- parse_mode: "HTML",
2598
- disable_notification: true
2599
- }),
2600
- { type: "other" }
2601
- );
2602
- if (msg) {
2603
- this.messageId = msg.message_id;
2604
- }
2605
- }
2606
- } catch {
2607
- try {
2608
- if (i === 0 && this.messageId) {
2609
- await this.sendQueue.enqueue(
2610
- () => this.bot.api.editMessageText(this.chatId, this.messageId, mdChunks[i].slice(0, 4096)),
2611
- { type: "other" }
2612
- );
2613
- } else {
2614
- const msg = await this.sendQueue.enqueue(
2615
- () => this.bot.api.sendMessage(this.chatId, mdChunks[i].slice(0, 4096), {
2616
- message_thread_id: this.threadId,
2617
- disable_notification: true
2618
- }),
2619
- { type: "other" }
2620
- );
2621
- if (msg) {
2622
- this.messageId = msg.message_id;
2623
- }
2624
- }
2625
- } catch {
2626
- }
2627
- }
2628
- }
2629
- return this.messageId;
2630
- }
2631
- getMessageId() {
2632
- return this.messageId;
2633
- }
2634
- };
2635
-
2636
- // src/adapters/telegram/topics.ts
2637
- async function ensureTopics(bot, chatId, config, saveConfig) {
2638
- let notificationTopicId = config.notificationTopicId;
2639
- let assistantTopicId = config.assistantTopicId;
2640
- if (notificationTopicId === null) {
2641
- const topic = await bot.api.createForumTopic(chatId, "\u{1F4CB} Notifications");
2642
- notificationTopicId = topic.message_thread_id;
2643
- await saveConfig({ notificationTopicId });
2644
- }
2645
- if (assistantTopicId === null) {
2646
- const topic = await bot.api.createForumTopic(chatId, "\u{1F916} Assistant");
2647
- assistantTopicId = topic.message_thread_id;
2648
- await saveConfig({ assistantTopicId });
2649
- }
2650
- return { notificationTopicId, assistantTopicId };
2651
- }
2652
- async function createSessionTopic(bot, chatId, name) {
2653
- const topic = await bot.api.createForumTopic(chatId, name);
2654
- return topic.message_thread_id;
2655
- }
2656
- async function renameSessionTopic(bot, chatId, threadId, name) {
2657
- try {
2658
- await bot.api.editForumTopic(chatId, threadId, { name });
2659
- } catch {
2660
- }
2661
- }
2662
- function buildDeepLink(chatId, messageId) {
2663
- const cleanId = String(chatId).replace("-100", "");
2664
- return `https://t.me/c/${cleanId}/${messageId}`;
2665
- }
2666
-
2667
- // src/adapters/telegram/commands/new-session.ts
2668
- import { InlineKeyboard as InlineKeyboard2 } from "grammy";
2669
-
2670
2670
  // src/adapters/telegram/commands/admin.ts
2671
2671
  import { InlineKeyboard } from "grammy";
2672
- var log8 = createChildLogger({ module: "telegram-cmd-admin" });
2672
+ var log9 = createChildLogger({ module: "telegram-cmd-admin" });
2673
2673
  function buildDangerousModeKeyboard(sessionId, enabled) {
2674
2674
  return new InlineKeyboard().text(
2675
2675
  enabled ? "\u{1F510} Disable Dangerous Mode" : "\u2620\uFE0F Enable Dangerous Mode",
@@ -2682,8 +2682,8 @@ function setupDangerousModeCallbacks(bot, core) {
2682
2682
  const session = core.sessionManager.getSession(sessionId);
2683
2683
  if (session) {
2684
2684
  session.dangerousMode = !session.dangerousMode;
2685
- log8.info({ sessionId, dangerousMode: session.dangerousMode }, "Dangerous mode toggled via button");
2686
- core.sessionManager.updateSessionDangerousMode(sessionId, session.dangerousMode).catch(() => {
2685
+ log9.info({ sessionId, dangerousMode: session.dangerousMode }, "Dangerous mode toggled via button");
2686
+ core.sessionManager.patchRecord(sessionId, { dangerousMode: session.dangerousMode }).catch(() => {
2687
2687
  });
2688
2688
  const toastText2 = session.dangerousMode ? "\u2620\uFE0F Dangerous mode enabled \u2014 permissions auto-approved" : "\u{1F510} Dangerous mode disabled \u2014 permissions shown normally";
2689
2689
  try {
@@ -2707,9 +2707,9 @@ function setupDangerousModeCallbacks(bot, core) {
2707
2707
  return;
2708
2708
  }
2709
2709
  const newDangerousMode = !(record.dangerousMode ?? false);
2710
- core.sessionManager.updateSessionDangerousMode(sessionId, newDangerousMode).catch(() => {
2710
+ core.sessionManager.patchRecord(sessionId, { dangerousMode: newDangerousMode }).catch(() => {
2711
2711
  });
2712
- log8.info({ sessionId, dangerousMode: newDangerousMode }, "Dangerous mode toggled via button (store-only, session not in memory)");
2712
+ log9.info({ sessionId, dangerousMode: newDangerousMode }, "Dangerous mode toggled via button (store-only, session not in memory)");
2713
2713
  const toastText = newDangerousMode ? "\u2620\uFE0F Dangerous mode enabled \u2014 permissions auto-approved" : "\u{1F510} Dangerous mode disabled \u2014 permissions shown normally";
2714
2714
  try {
2715
2715
  await ctx.answerCallbackQuery({ text: toastText });
@@ -2736,7 +2736,7 @@ async function handleEnableDangerous(ctx, core) {
2736
2736
  return;
2737
2737
  }
2738
2738
  session.dangerousMode = true;
2739
- core.sessionManager.updateSessionDangerousMode(session.id, true).catch(() => {
2739
+ core.sessionManager.patchRecord(session.id, { dangerousMode: true }).catch(() => {
2740
2740
  });
2741
2741
  } else {
2742
2742
  const record = core.sessionManager.getRecordByThread("telegram", String(threadId));
@@ -2748,7 +2748,7 @@ async function handleEnableDangerous(ctx, core) {
2748
2748
  await ctx.reply("\u2620\uFE0F Dangerous mode is already enabled.", { parse_mode: "HTML" });
2749
2749
  return;
2750
2750
  }
2751
- core.sessionManager.updateSessionDangerousMode(record.sessionId, true).catch(() => {
2751
+ core.sessionManager.patchRecord(record.sessionId, { dangerousMode: true }).catch(() => {
2752
2752
  });
2753
2753
  }
2754
2754
  await ctx.reply(
@@ -2773,7 +2773,7 @@ async function handleDisableDangerous(ctx, core) {
2773
2773
  return;
2774
2774
  }
2775
2775
  session.dangerousMode = false;
2776
- core.sessionManager.updateSessionDangerousMode(session.id, false).catch(() => {
2776
+ core.sessionManager.patchRecord(session.id, { dangerousMode: false }).catch(() => {
2777
2777
  });
2778
2778
  } else {
2779
2779
  const record = core.sessionManager.getRecordByThread("telegram", String(threadId));
@@ -2785,7 +2785,7 @@ async function handleDisableDangerous(ctx, core) {
2785
2785
  await ctx.reply("\u{1F510} Dangerous mode is already disabled.", { parse_mode: "HTML" });
2786
2786
  return;
2787
2787
  }
2788
- core.sessionManager.updateSessionDangerousMode(record.sessionId, false).catch(() => {
2788
+ core.sessionManager.patchRecord(record.sessionId, { dangerousMode: false }).catch(() => {
2789
2789
  });
2790
2790
  }
2791
2791
  await ctx.reply("\u{1F510} <b>Dangerous mode disabled</b>\n\nPermission requests will be shown normally.", { parse_mode: "HTML" });
@@ -2838,7 +2838,7 @@ async function handleRestart(ctx, core) {
2838
2838
  }
2839
2839
 
2840
2840
  // src/adapters/telegram/commands/new-session.ts
2841
- var log9 = createChildLogger({ module: "telegram-cmd-new-session" });
2841
+ var log10 = createChildLogger({ module: "telegram-cmd-new-session" });
2842
2842
  var pendingNewSessions = /* @__PURE__ */ new Map();
2843
2843
  var PENDING_TIMEOUT_MS = 5 * 60 * 1e3;
2844
2844
  function cleanupPending(userId) {
@@ -2870,37 +2870,12 @@ async function handleNew(ctx, core, chatId, assistant) {
2870
2870
  return;
2871
2871
  }
2872
2872
  }
2873
- const userId = ctx.from?.id;
2874
- if (!userId) return;
2875
- const agents = core.agentManager.getAvailableAgents();
2876
- const config = core.configManager.get();
2877
- if (agentName || agents.length === 1) {
2878
- const selectedAgent = agentName || config.defaultAgent;
2879
- await startWorkspaceStep(ctx, core, chatId, userId, selectedAgent);
2880
- return;
2881
- }
2882
- const keyboard = new InlineKeyboard2();
2883
- for (const agent of agents) {
2884
- const label = agent.name === config.defaultAgent ? `${agent.name} (default)` : agent.name;
2885
- keyboard.text(label, `m:new:agent:${agent.name}`).row();
2886
- }
2887
- keyboard.text("\u274C Cancel", "m:new:cancel");
2888
- const msg = await ctx.reply(
2889
- `\u{1F916} <b>Choose an agent:</b>`,
2890
- { parse_mode: "HTML", reply_markup: keyboard }
2891
- );
2892
- cleanupPending(userId);
2893
- pendingNewSessions.set(userId, {
2894
- step: "agent",
2895
- messageId: msg.message_id,
2896
- threadId: currentThreadId,
2897
- timer: setTimeout(() => pendingNewSessions.delete(userId), PENDING_TIMEOUT_MS)
2898
- });
2873
+ await showAgentPicker(ctx, core, chatId, agentName);
2899
2874
  }
2900
2875
  async function startWorkspaceStep(ctx, core, chatId, userId, agentName) {
2901
2876
  const config = core.configManager.get();
2902
2877
  const baseDir = config.workspace.baseDir;
2903
- const keyboard = new InlineKeyboard2().text(`\u{1F4C1} Use ${baseDir}`, "m:new:ws:default").row().text("\u270F\uFE0F Enter project path", "m:new:ws:custom").row().text("\u274C Cancel", "m:new:cancel");
2878
+ const keyboard = new InlineKeyboard2().text(`\u{1F4C1} Use ${baseDir}`, "m:new:ws:default").row().text("\u270F\uFE0F Enter project path", "m:new:ws:custom");
2904
2879
  const text = `\u{1F4C1} <b>Where should ${escapeHtml(agentName)} work?</b>
2905
2880
 
2906
2881
  Enter the path to your project folder \u2014 the agent will read, write, and run code there.
@@ -2962,7 +2937,7 @@ async function startConfirmStep(ctx, chatId, userId, agentName, workspace) {
2962
2937
  });
2963
2938
  }
2964
2939
  async function createSessionDirect(ctx, core, chatId, agentName, workspace) {
2965
- log9.info({ userId: ctx.from?.id, agentName, workspace }, "New session command (direct)");
2940
+ log10.info({ userId: ctx.from?.id, agentName, workspace }, "New session command (direct)");
2966
2941
  let threadId;
2967
2942
  try {
2968
2943
  const topicName = `\u{1F504} New Session`;
@@ -2973,7 +2948,7 @@ async function createSessionDirect(ctx, core, chatId, agentName, workspace) {
2973
2948
  });
2974
2949
  const session = await core.handleNewSession("telegram", agentName, workspace);
2975
2950
  session.threadId = String(threadId);
2976
- await core.sessionManager.updateSessionPlatform(session.id, { topicId: threadId });
2951
+ await core.sessionManager.patchRecord(session.id, { platform: { topicId: threadId } });
2977
2952
  const finalName = `\u{1F504} ${session.agentName} \u2014 New Session`;
2978
2953
  try {
2979
2954
  await ctx.api.editForumTopic(chatId, threadId, { name: finalName });
@@ -2992,10 +2967,10 @@ This is your coding session \u2014 chat here to work with the agent.`,
2992
2967
  reply_markup: buildDangerousModeKeyboard(session.id, false)
2993
2968
  }
2994
2969
  );
2995
- session.warmup().catch((err) => log9.error({ err }, "Warm-up error"));
2970
+ session.warmup().catch((err) => log10.error({ err }, "Warm-up error"));
2996
2971
  return threadId ?? null;
2997
2972
  } catch (err) {
2998
- log9.error({ err }, "Session creation failed");
2973
+ log10.error({ err }, "Session creation failed");
2999
2974
  if (threadId) {
3000
2975
  try {
3001
2976
  await ctx.api.deleteForumTopic(chatId, threadId);
@@ -3059,9 +3034,9 @@ async function handleNewChat(ctx, core, chatId) {
3059
3034
  workspace
3060
3035
  );
3061
3036
  session.threadId = String(newThreadId);
3062
- await core.sessionManager.updateSessionPlatform(session.id, {
3037
+ await core.sessionManager.patchRecord(session.id, { platform: {
3063
3038
  topicId: newThreadId
3064
- });
3039
+ } });
3065
3040
  await ctx.api.sendMessage(
3066
3041
  chatId,
3067
3042
  `\u2705 New chat (same agent &amp; workspace)
@@ -3073,7 +3048,7 @@ async function handleNewChat(ctx, core, chatId) {
3073
3048
  reply_markup: buildDangerousModeKeyboard(session.id, false)
3074
3049
  }
3075
3050
  );
3076
- session.warmup().catch((err) => log9.error({ err }, "Warm-up error"));
3051
+ session.warmup().catch((err) => log10.error({ err }, "Warm-up error"));
3077
3052
  } catch (err) {
3078
3053
  if (newThreadId) {
3079
3054
  try {
@@ -3099,12 +3074,12 @@ async function executeNewSession(bot, core, chatId, agentName, workspace) {
3099
3074
  workspace
3100
3075
  );
3101
3076
  session.threadId = String(threadId);
3102
- await core.sessionManager.updateSessionPlatform(session.id, {
3077
+ await core.sessionManager.patchRecord(session.id, { platform: {
3103
3078
  topicId: threadId
3104
- });
3079
+ } });
3105
3080
  const finalName = `\u{1F504} ${session.agentName} \u2014 New Session`;
3106
3081
  await renameSessionTopic(bot, chatId, threadId, finalName);
3107
- session.warmup().catch((err) => log9.error({ err }, "Warm-up error"));
3082
+ session.warmup().catch((err) => log10.error({ err }, "Warm-up error"));
3108
3083
  return { session, threadId, firstMsgId };
3109
3084
  } catch (err) {
3110
3085
  try {
@@ -3135,30 +3110,35 @@ async function handlePendingWorkspaceInput(ctx, core, chatId, assistantTopicId)
3135
3110
  return true;
3136
3111
  }
3137
3112
  async function startInteractiveNewSession(ctx, core, chatId, agentName) {
3113
+ await showAgentPicker(ctx, core, chatId, agentName);
3114
+ }
3115
+ async function showAgentPicker(ctx, core, chatId, agentName) {
3138
3116
  const userId = ctx.from?.id;
3139
3117
  if (!userId) return;
3140
- const agents = core.agentManager.getAvailableAgents();
3118
+ const installedEntries = core.agentCatalog.getInstalledEntries();
3119
+ const agentKeys = Object.keys(installedEntries);
3141
3120
  const config = core.configManager.get();
3142
- if (agentName || agents.length === 1) {
3121
+ if (agentName || agentKeys.length === 1) {
3143
3122
  const selectedAgent = agentName || config.defaultAgent;
3144
3123
  await startWorkspaceStep(ctx, core, chatId, userId, selectedAgent);
3145
3124
  return;
3146
3125
  }
3147
3126
  const keyboard = new InlineKeyboard2();
3148
- for (const agent of agents) {
3149
- const label = agent.name === config.defaultAgent ? `${agent.name} (default)` : agent.name;
3150
- keyboard.text(label, `m:new:agent:${agent.name}`).row();
3127
+ for (const key of agentKeys) {
3128
+ const agent = installedEntries[key];
3129
+ const label = key === config.defaultAgent ? `${agent.name} (default)` : agent.name;
3130
+ keyboard.text(label, `m:new:agent:${key}`).row();
3151
3131
  }
3152
- keyboard.text("\u274C Cancel", "m:new:cancel");
3153
3132
  const msg = await ctx.reply(
3154
3133
  `\u{1F916} <b>Choose an agent:</b>`,
3155
3134
  { parse_mode: "HTML", reply_markup: keyboard }
3156
3135
  );
3157
3136
  cleanupPending(userId);
3137
+ const threadId = ctx.message?.message_thread_id ?? ctx.callbackQuery?.message?.message_thread_id;
3158
3138
  pendingNewSessions.set(userId, {
3159
3139
  step: "agent",
3160
3140
  messageId: msg.message_id,
3161
- threadId: ctx.callbackQuery?.message?.message_thread_id,
3141
+ threadId,
3162
3142
  timer: setTimeout(() => pendingNewSessions.delete(userId), PENDING_TIMEOUT_MS)
3163
3143
  });
3164
3144
  }
@@ -3247,7 +3227,7 @@ Or just the folder name like <code>my-project</code> (will use ${core.configMana
3247
3227
 
3248
3228
  // src/adapters/telegram/commands/session.ts
3249
3229
  import { InlineKeyboard as InlineKeyboard3 } from "grammy";
3250
- var log10 = createChildLogger({ module: "telegram-cmd-session" });
3230
+ var log11 = createChildLogger({ module: "telegram-cmd-session" });
3251
3231
  async function handleCancel(ctx, core, assistant) {
3252
3232
  const threadId = ctx.message?.message_thread_id;
3253
3233
  if (!threadId) return;
@@ -3265,14 +3245,14 @@ async function handleCancel(ctx, core, assistant) {
3265
3245
  String(threadId)
3266
3246
  );
3267
3247
  if (session) {
3268
- log10.info({ sessionId: session.id }, "Cancel session command");
3269
- await session.cancel();
3248
+ log11.info({ sessionId: session.id }, "Cancel session command");
3249
+ await session.abortPrompt();
3270
3250
  await ctx.reply("\u26D4 Session cancelled.", { parse_mode: "HTML" });
3271
3251
  return;
3272
3252
  }
3273
3253
  const record = core.sessionManager.getRecordByThread("telegram", String(threadId));
3274
3254
  if (record && record.status !== "cancelled" && record.status !== "error") {
3275
- log10.info({ sessionId: record.sessionId }, "Cancel session command (from store)");
3255
+ log11.info({ sessionId: record.sessionId }, "Cancel session command (from store)");
3276
3256
  await core.sessionManager.cancelSession(record.sessionId);
3277
3257
  await ctx.reply("\u26D4 Session cancelled.", { parse_mode: "HTML" });
3278
3258
  }
@@ -3356,8 +3336,8 @@ async function handleTopics(ctx, core) {
3356
3336
  const truncated = records.length > MAX_DISPLAY ? `
3357
3337
 
3358
3338
  <i>...and ${records.length - MAX_DISPLAY} more</i>` : "";
3359
- const finishedCount = records.filter((r) => r.status === "finished").length;
3360
- const errorCount = records.filter((r) => r.status === "error" || r.status === "cancelled").length;
3339
+ const finishedCount = allRecords.filter((r) => r.status === "finished").length;
3340
+ const errorCount = allRecords.filter((r) => r.status === "error" || r.status === "cancelled").length;
3361
3341
  const keyboard = new InlineKeyboard3();
3362
3342
  if (finishedCount > 0) {
3363
3343
  keyboard.text(`Cleanup finished (${finishedCount})`, "m:cleanup:finished").row();
@@ -3368,7 +3348,7 @@ async function handleTopics(ctx, core) {
3368
3348
  if (finishedCount + errorCount > 0) {
3369
3349
  keyboard.text(`Cleanup all non-active (${finishedCount + errorCount})`, "m:cleanup:all").row();
3370
3350
  }
3371
- keyboard.text(`\u26A0\uFE0F Cleanup ALL (${records.length})`, "m:cleanup:everything").row();
3351
+ keyboard.text(`\u26A0\uFE0F Cleanup ALL (${allRecords.length})`, "m:cleanup:everything").row();
3372
3352
  keyboard.text("Refresh", "m:topics");
3373
3353
  await ctx.reply(
3374
3354
  `${header}
@@ -3377,17 +3357,14 @@ ${lines.join("\n")}${truncated}`,
3377
3357
  { parse_mode: "HTML", reply_markup: keyboard }
3378
3358
  );
3379
3359
  } catch (err) {
3380
- log10.error({ err }, "handleTopics error");
3360
+ log11.error({ err }, "handleTopics error");
3381
3361
  await ctx.reply("\u274C Failed to list sessions.", { parse_mode: "HTML" }).catch(() => {
3382
3362
  });
3383
3363
  }
3384
3364
  }
3385
3365
  async function handleCleanup(ctx, core, chatId, statuses) {
3386
3366
  const allRecords = core.sessionManager.listRecords();
3387
- const cleanable = allRecords.filter((r) => {
3388
- const platform = r.platform;
3389
- return !!platform?.topicId && statuses.includes(r.status);
3390
- });
3367
+ const cleanable = allRecords.filter((r) => statuses.includes(r.status));
3391
3368
  if (cleanable.length === 0) {
3392
3369
  await ctx.reply("Nothing to clean up.", { parse_mode: "HTML" });
3393
3370
  return;
@@ -3401,13 +3378,13 @@ async function handleCleanup(ctx, core, chatId, statuses) {
3401
3378
  try {
3402
3379
  await ctx.api.deleteForumTopic(chatId, topicId);
3403
3380
  } catch (err) {
3404
- log10.warn({ err, sessionId: record.sessionId, topicId }, "Failed to delete forum topic during cleanup");
3381
+ log11.warn({ err, sessionId: record.sessionId, topicId }, "Failed to delete forum topic during cleanup");
3405
3382
  }
3406
3383
  }
3407
3384
  await core.sessionManager.removeRecord(record.sessionId);
3408
3385
  deleted++;
3409
3386
  } catch (err) {
3410
- log10.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
3387
+ log11.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
3411
3388
  failed++;
3412
3389
  }
3413
3390
  }
@@ -3420,8 +3397,7 @@ async function handleCleanupEverything(ctx, core, chatId, systemTopicIds) {
3420
3397
  const allRecords = core.sessionManager.listRecords();
3421
3398
  const cleanable = allRecords.filter((r) => {
3422
3399
  const platform = r.platform;
3423
- if (!platform?.topicId) return false;
3424
- if (systemTopicIds && (platform.topicId === systemTopicIds.notificationTopicId || platform.topicId === systemTopicIds.assistantTopicId)) return false;
3400
+ if (systemTopicIds && platform?.topicId && (platform.topicId === systemTopicIds.notificationTopicId || platform.topicId === systemTopicIds.assistantTopicId)) return false;
3425
3401
  return true;
3426
3402
  });
3427
3403
  if (cleanable.length === 0) {
@@ -3464,8 +3440,7 @@ async function handleCleanupEverythingConfirmed(ctx, core, chatId, systemTopicId
3464
3440
  const allRecords = core.sessionManager.listRecords();
3465
3441
  const cleanable = allRecords.filter((r) => {
3466
3442
  const platform = r.platform;
3467
- if (!platform?.topicId) return false;
3468
- if (systemTopicIds && (platform.topicId === systemTopicIds.notificationTopicId || platform.topicId === systemTopicIds.assistantTopicId)) return false;
3443
+ if (systemTopicIds && platform?.topicId && (platform.topicId === systemTopicIds.notificationTopicId || platform.topicId === systemTopicIds.assistantTopicId)) return false;
3469
3444
  return true;
3470
3445
  });
3471
3446
  if (cleanable.length === 0) {
@@ -3480,7 +3455,7 @@ async function handleCleanupEverythingConfirmed(ctx, core, chatId, systemTopicId
3480
3455
  try {
3481
3456
  await core.sessionManager.cancelSession(record.sessionId);
3482
3457
  } catch (err) {
3483
- log10.warn({ err, sessionId: record.sessionId }, "Failed to cancel session during cleanup");
3458
+ log11.warn({ err, sessionId: record.sessionId }, "Failed to cancel session during cleanup");
3484
3459
  }
3485
3460
  }
3486
3461
  const topicId = record.platform?.topicId;
@@ -3488,13 +3463,13 @@ async function handleCleanupEverythingConfirmed(ctx, core, chatId, systemTopicId
3488
3463
  try {
3489
3464
  await ctx.api.deleteForumTopic(chatId, topicId);
3490
3465
  } catch (err) {
3491
- log10.warn({ err, sessionId: record.sessionId, topicId }, "Failed to delete forum topic during cleanup");
3466
+ log11.warn({ err, sessionId: record.sessionId, topicId }, "Failed to delete forum topic during cleanup");
3492
3467
  }
3493
3468
  }
3494
3469
  await core.sessionManager.removeRecord(record.sessionId);
3495
3470
  deleted++;
3496
3471
  } catch (err) {
3497
- log10.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
3472
+ log11.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
3498
3473
  failed++;
3499
3474
  }
3500
3475
  }
@@ -3507,7 +3482,7 @@ async function executeCancelSession(core, excludeSessionId) {
3507
3482
  const sessions = core.sessionManager.listSessions("telegram").filter((s) => s.status === "active" && s.id !== excludeSessionId).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
3508
3483
  const session = sessions[0];
3509
3484
  if (!session) return null;
3510
- await session.cancel();
3485
+ await session.abortPrompt();
3511
3486
  return session;
3512
3487
  }
3513
3488
  function setupSessionCallbacks(bot, core, chatId, systemTopicIds) {
@@ -3537,100 +3512,213 @@ function setupSessionCallbacks(bot, core, chatId, systemTopicIds) {
3537
3512
  });
3538
3513
  }
3539
3514
 
3540
- // src/adapters/telegram/commands/menu.ts
3515
+ // src/adapters/telegram/commands/agents.ts
3541
3516
  import { InlineKeyboard as InlineKeyboard4 } from "grammy";
3542
- function buildMenuKeyboard() {
3543
- return new InlineKeyboard4().text("\u{1F195} New Session", "m:new").text("\u{1F4CB} Sessions", "m:topics").row().text("\u{1F4CA} Status", "m:status").text("\u{1F916} Agents", "m:agents").row().text("\u{1F517} Integrate", "m:integrate").text("\u2753 Help", "m:help").row().text("\u{1F504} Restart", "m:restart").text("\u2B06\uFE0F Update", "m:update");
3544
- }
3545
- async function handleMenu(ctx) {
3546
- await ctx.reply(`<b>OpenACP Menu</b>
3547
- Choose an action:`, {
3548
- parse_mode: "HTML",
3549
- reply_markup: buildMenuKeyboard()
3550
- });
3517
+ var AGENTS_PER_PAGE = 6;
3518
+ async function handleAgents(ctx, core, page = 0) {
3519
+ const catalog = core.agentCatalog;
3520
+ const items = catalog.getAvailable();
3521
+ const installed = items.filter((i) => i.installed);
3522
+ const available = items.filter((i) => !i.installed);
3523
+ let text = "<b>\u{1F916} Agents</b>\n\n";
3524
+ if (installed.length > 0) {
3525
+ text += "<b>Installed:</b>\n";
3526
+ for (const item of installed) {
3527
+ text += `\u2705 <b>${escapeHtml(item.name)}</b>`;
3528
+ if (item.description) {
3529
+ text += ` \u2014 <i>${escapeHtml(truncate(item.description, 50))}</i>`;
3530
+ }
3531
+ text += "\n";
3532
+ }
3533
+ text += "\n";
3534
+ }
3535
+ if (available.length > 0) {
3536
+ const totalPages = Math.ceil(available.length / AGENTS_PER_PAGE);
3537
+ const safePage = Math.max(0, Math.min(page, totalPages - 1));
3538
+ const pageItems = available.slice(safePage * AGENTS_PER_PAGE, (safePage + 1) * AGENTS_PER_PAGE);
3539
+ text += `<b>Available to install:</b>`;
3540
+ if (totalPages > 1) {
3541
+ text += ` (${safePage + 1}/${totalPages})`;
3542
+ }
3543
+ text += "\n";
3544
+ for (const item of pageItems) {
3545
+ if (item.available) {
3546
+ text += `\u2B07\uFE0F <b>${escapeHtml(item.name)}</b>`;
3547
+ } else {
3548
+ const deps = item.missingDeps?.join(", ") ?? "requirements not met";
3549
+ text += `\u26A0\uFE0F <b>${escapeHtml(item.name)}</b> <i>(needs: ${escapeHtml(deps)})</i>`;
3550
+ }
3551
+ if (item.description) {
3552
+ text += `
3553
+ <i>${escapeHtml(truncate(item.description, 60))}</i>`;
3554
+ }
3555
+ text += "\n";
3556
+ }
3557
+ const keyboard = new InlineKeyboard4();
3558
+ const installable = pageItems.filter((i) => i.available);
3559
+ for (let i = 0; i < installable.length; i += 2) {
3560
+ const row = installable.slice(i, i + 2);
3561
+ for (const item of row) {
3562
+ keyboard.text(`\u2B07\uFE0F ${item.name}`, `ag:install:${item.key}`);
3563
+ }
3564
+ keyboard.row();
3565
+ }
3566
+ if (totalPages > 1) {
3567
+ if (safePage > 0) {
3568
+ keyboard.text("\u25C0\uFE0F Prev", `ag:page:${safePage - 1}`);
3569
+ }
3570
+ if (safePage < totalPages - 1) {
3571
+ keyboard.text("Next \u25B6\uFE0F", `ag:page:${safePage + 1}`);
3572
+ }
3573
+ keyboard.row();
3574
+ }
3575
+ if (available.some((i) => !i.available)) {
3576
+ text += "\n\u{1F4A1} <i>Agents marked \u26A0\uFE0F need additional setup. Use</i> <code>openacp agents info &lt;name&gt;</code> <i>for details.</i>\n";
3577
+ }
3578
+ await ctx.reply(text, { parse_mode: "HTML", reply_markup: keyboard });
3579
+ } else {
3580
+ text += "<i>All agents are already installed!</i>";
3581
+ await ctx.reply(text, { parse_mode: "HTML" });
3582
+ }
3551
3583
  }
3552
- async function handleAgents(ctx, core) {
3553
- const agents = core.agentManager.getAvailableAgents();
3554
- const defaultAgent = core.configManager.get().defaultAgent;
3555
- const lines = agents.map(
3556
- (a) => `\u2022 <b>${escapeHtml(a.name)}</b>${a.name === defaultAgent ? " (default)" : ""}
3557
- <code>${escapeHtml(a.command)} ${a.args.map((arg) => escapeHtml(arg)).join(" ")}</code>`
3558
- );
3559
- const text = lines.length > 0 ? `<b>Available Agents:</b>
3560
-
3561
- ${lines.join("\n")}` : `<b>Available Agents:</b>
3562
-
3563
- No agents configured.`;
3564
- await ctx.reply(text, { parse_mode: "HTML" });
3584
+ async function handleInstall(ctx, core) {
3585
+ const text = (ctx.message?.text ?? "").trim();
3586
+ const parts = text.split(/\s+/);
3587
+ const nameOrId = parts[1];
3588
+ if (!nameOrId) {
3589
+ await ctx.reply(
3590
+ "\u{1F4E6} <b>Install an agent</b>\n\nUsage: <code>/install &lt;agent-name&gt;</code>\nExample: <code>/install gemini</code>\n\nUse /agents to browse available agents.",
3591
+ { parse_mode: "HTML" }
3592
+ );
3593
+ return;
3594
+ }
3595
+ await installAgentWithProgress(ctx, core, nameOrId);
3565
3596
  }
3566
- async function handleHelp(ctx) {
3567
- await ctx.reply(
3568
- `\u{1F4D6} <b>OpenACP Help</b>
3569
-
3570
- \u{1F680} <b>Getting Started</b>
3571
- Tap \u{1F195} New Session to start coding with AI.
3572
- Each session gets its own topic \u2014 chat there to work with the agent.
3573
-
3574
- \u{1F4A1} <b>Common Tasks</b>
3575
- /new [agent] [workspace] \u2014 Create new session
3576
- /cancel \u2014 Cancel session (in session topic)
3577
- /status \u2014 Show session or system status
3578
- /sessions \u2014 List all sessions
3579
- /agents \u2014 List available agents
3580
-
3581
- \u2699\uFE0F <b>System</b>
3582
- /restart \u2014 Restart OpenACP
3583
- /update \u2014 Update to latest version
3584
- /integrate \u2014 Manage agent integrations
3585
- /menu \u2014 Show action menu
3586
-
3587
- \u{1F512} <b>Session Options</b>
3588
- /enable_dangerous \u2014 Auto-approve permissions
3589
- /disable_dangerous \u2014 Restore permission prompts
3590
- /handoff \u2014 Continue session in terminal
3591
- /clear \u2014 Clear assistant history
3592
-
3593
- \u{1F4AC} Need help? Just ask me in this topic!`,
3594
- { parse_mode: "HTML" }
3595
- );
3596
- }
3597
- async function handleClear(ctx, assistant) {
3598
- if (!assistant) {
3599
- await ctx.reply("\u26A0\uFE0F Assistant is not available.", { parse_mode: "HTML" });
3600
- return;
3601
- }
3602
- const threadId = ctx.message?.message_thread_id;
3603
- if (threadId !== assistant.topicId) {
3604
- await ctx.reply("\u2139\uFE0F /clear only works in the Assistant topic.", { parse_mode: "HTML" });
3597
+ async function handleAgentCallback(ctx, core) {
3598
+ const data = ctx.callbackQuery?.data ?? "";
3599
+ await ctx.answerCallbackQuery();
3600
+ if (data.startsWith("ag:install:")) {
3601
+ const nameOrId = data.replace("ag:install:", "");
3602
+ await installAgentWithProgress(ctx, core, nameOrId);
3605
3603
  return;
3606
3604
  }
3607
- await ctx.reply("\u{1F504} Clearing assistant history...", { parse_mode: "HTML" });
3608
- try {
3609
- await assistant.respawn();
3610
- await ctx.reply("\u2705 Assistant history cleared.", { parse_mode: "HTML" });
3611
- } catch (err) {
3612
- const message = err instanceof Error ? err.message : String(err);
3613
- await ctx.reply(`\u274C Failed to clear: <code>${message}</code>`, { parse_mode: "HTML" });
3605
+ if (data.startsWith("ag:page:")) {
3606
+ const page = parseInt(data.replace("ag:page:", ""), 10);
3607
+ try {
3608
+ const catalog = core.agentCatalog;
3609
+ const items = catalog.getAvailable();
3610
+ const installed = items.filter((i) => i.installed);
3611
+ const available = items.filter((i) => !i.installed);
3612
+ let text = "<b>\u{1F916} Agents</b>\n\n";
3613
+ if (installed.length > 0) {
3614
+ text += "<b>Installed:</b>\n";
3615
+ for (const item of installed) {
3616
+ text += `\u2705 <b>${escapeHtml(item.name)}</b>`;
3617
+ if (item.description) {
3618
+ text += ` \u2014 <i>${escapeHtml(truncate(item.description, 50))}</i>`;
3619
+ }
3620
+ text += "\n";
3621
+ }
3622
+ text += "\n";
3623
+ }
3624
+ const totalPages = Math.ceil(available.length / AGENTS_PER_PAGE);
3625
+ const safePage = Math.max(0, Math.min(page, totalPages - 1));
3626
+ const pageItems = available.slice(safePage * AGENTS_PER_PAGE, (safePage + 1) * AGENTS_PER_PAGE);
3627
+ text += `<b>Available to install:</b>`;
3628
+ if (totalPages > 1) {
3629
+ text += ` (${safePage + 1}/${totalPages})`;
3630
+ }
3631
+ text += "\n";
3632
+ for (const item of pageItems) {
3633
+ if (item.available) {
3634
+ text += `\u2B07\uFE0F <b>${escapeHtml(item.name)}</b>`;
3635
+ } else {
3636
+ const deps = item.missingDeps?.join(", ") ?? "requirements not met";
3637
+ text += `\u26A0\uFE0F <b>${escapeHtml(item.name)}</b> <i>(needs: ${escapeHtml(deps)})</i>`;
3638
+ }
3639
+ if (item.description) {
3640
+ text += `
3641
+ <i>${escapeHtml(truncate(item.description, 60))}</i>`;
3642
+ }
3643
+ text += "\n";
3644
+ }
3645
+ const keyboard = new InlineKeyboard4();
3646
+ const installable = pageItems.filter((i) => i.available);
3647
+ for (let i = 0; i < installable.length; i += 2) {
3648
+ const row = installable.slice(i, i + 2);
3649
+ for (const item of row) {
3650
+ keyboard.text(`\u2B07\uFE0F ${item.name}`, `ag:install:${item.key}`);
3651
+ }
3652
+ keyboard.row();
3653
+ }
3654
+ if (totalPages > 1) {
3655
+ if (safePage > 0) {
3656
+ keyboard.text("\u25C0\uFE0F Prev", `ag:page:${safePage - 1}`);
3657
+ }
3658
+ if (safePage < totalPages - 1) {
3659
+ keyboard.text("Next \u25B6\uFE0F", `ag:page:${safePage + 1}`);
3660
+ }
3661
+ keyboard.row();
3662
+ }
3663
+ await ctx.editMessageText(text, { parse_mode: "HTML", reply_markup: keyboard });
3664
+ } catch {
3665
+ }
3614
3666
  }
3615
3667
  }
3616
- var TELEGRAM_MSG_LIMIT = 4096;
3617
- function buildSkillMessages(commands) {
3618
- const sorted = [...commands].sort((a, b) => a.name.localeCompare(b.name));
3619
- const header = "\u{1F6E0} <b>Available Skills</b>\n";
3620
- const lines = sorted.map((c) => `<code>/${c.name}</code>`);
3621
- const messages = [];
3622
- let current = header;
3623
- for (const line of lines) {
3624
- const candidate = current + "\n" + line;
3625
- if (candidate.length > TELEGRAM_MSG_LIMIT) {
3626
- messages.push(current);
3627
- current = line;
3628
- } else {
3629
- current = candidate;
3668
+ async function installAgentWithProgress(ctx, core, nameOrId) {
3669
+ const catalog = core.agentCatalog;
3670
+ const msg = await ctx.reply(`\u23F3 Installing <b>${escapeHtml(nameOrId)}</b>...`, { parse_mode: "HTML" });
3671
+ let lastEdit = 0;
3672
+ const EDIT_THROTTLE_MS = 1500;
3673
+ const progress = {
3674
+ onStart(_id, _name) {
3675
+ },
3676
+ async onStep(step) {
3677
+ const now = Date.now();
3678
+ if (now - lastEdit > EDIT_THROTTLE_MS) {
3679
+ lastEdit = now;
3680
+ try {
3681
+ await ctx.api.editMessageText(msg.chat.id, msg.message_id, `\u23F3 <b>${escapeHtml(nameOrId)}</b>: ${escapeHtml(step)}`, { parse_mode: "HTML" });
3682
+ } catch {
3683
+ }
3684
+ }
3685
+ },
3686
+ async onDownloadProgress(percent) {
3687
+ const now = Date.now();
3688
+ if (now - lastEdit > EDIT_THROTTLE_MS) {
3689
+ lastEdit = now;
3690
+ try {
3691
+ const bar = buildProgressBar(percent);
3692
+ await ctx.api.editMessageText(msg.chat.id, msg.message_id, `\u23F3 <b>${escapeHtml(nameOrId)}</b>
3693
+ Downloading... ${bar} ${percent}%`, { parse_mode: "HTML" });
3694
+ } catch {
3695
+ }
3696
+ }
3697
+ },
3698
+ async onSuccess(name) {
3699
+ try {
3700
+ const keyboard = new InlineKeyboard4().text(`\u{1F680} Start session with ${name}`, `na:${nameOrId}`);
3701
+ await ctx.api.editMessageText(msg.chat.id, msg.message_id, `\u2705 <b>${escapeHtml(name)}</b> installed!`, { parse_mode: "HTML", reply_markup: keyboard });
3702
+ } catch {
3703
+ }
3704
+ },
3705
+ async onError(error) {
3706
+ try {
3707
+ await ctx.api.editMessageText(msg.chat.id, msg.message_id, `\u274C ${escapeHtml(error)}`, { parse_mode: "HTML" });
3708
+ } catch {
3709
+ }
3630
3710
  }
3631
- }
3632
- if (current) messages.push(current);
3633
- return messages;
3711
+ };
3712
+ await catalog.install(nameOrId, progress);
3713
+ }
3714
+ function truncate(text, maxLen) {
3715
+ if (text.length <= maxLen) return text;
3716
+ return text.slice(0, maxLen - 1) + "\u2026";
3717
+ }
3718
+ function buildProgressBar(percent) {
3719
+ const filled = Math.round(percent / 10);
3720
+ const empty = 10 - filled;
3721
+ return "\u2588".repeat(filled) + "\u2591".repeat(empty);
3634
3722
  }
3635
3723
 
3636
3724
  // src/adapters/telegram/commands/integrate.ts
@@ -3751,6 +3839,298 @@ ${resultText}`,
3751
3839
  });
3752
3840
  }
3753
3841
 
3842
+ // src/adapters/telegram/commands/settings.ts
3843
+ import { InlineKeyboard as InlineKeyboard6 } from "grammy";
3844
+ var log12 = createChildLogger({ module: "telegram-settings" });
3845
+ function buildSettingsKeyboard(core) {
3846
+ const config = core.configManager.get();
3847
+ const fields = getSafeFields();
3848
+ const kb = new InlineKeyboard6();
3849
+ for (const field of fields) {
3850
+ const value = getConfigValue(config, field.path);
3851
+ const label = formatFieldLabel(field, value);
3852
+ if (field.type === "toggle") {
3853
+ kb.text(`${label}`, `s:toggle:${field.path}`).row();
3854
+ } else if (field.type === "select") {
3855
+ kb.text(`${label}`, `s:select:${field.path}`).row();
3856
+ } else {
3857
+ kb.text(`${label}`, `s:input:${field.path}`).row();
3858
+ }
3859
+ }
3860
+ kb.text("\u25C0\uFE0F Back to Menu", "s:back");
3861
+ return kb;
3862
+ }
3863
+ function formatFieldLabel(field, value) {
3864
+ const icons = {
3865
+ agent: "\u{1F916}",
3866
+ logging: "\u{1F4DD}",
3867
+ tunnel: "\u{1F517}",
3868
+ security: "\u{1F512}",
3869
+ workspace: "\u{1F4C1}",
3870
+ storage: "\u{1F4BE}"
3871
+ };
3872
+ const icon = icons[field.group] ?? "\u2699\uFE0F";
3873
+ if (field.type === "toggle") {
3874
+ return `${icon} ${field.displayName}: ${value ? "ON" : "OFF"}`;
3875
+ }
3876
+ return `${icon} ${field.displayName}: ${String(value)}`;
3877
+ }
3878
+ async function handleSettings(ctx, core) {
3879
+ const kb = buildSettingsKeyboard(core);
3880
+ await ctx.reply(`<b>\u2699\uFE0F Settings</b>
3881
+ Tap to change:`, {
3882
+ parse_mode: "HTML",
3883
+ reply_markup: kb
3884
+ });
3885
+ }
3886
+ function setupSettingsCallbacks(bot, core, getAssistantSession) {
3887
+ bot.callbackQuery(/^s:toggle:/, async (ctx) => {
3888
+ const fieldPath = ctx.callbackQuery.data.replace("s:toggle:", "");
3889
+ const config = core.configManager.get();
3890
+ const currentValue = getConfigValue(config, fieldPath);
3891
+ const newValue = !currentValue;
3892
+ try {
3893
+ const updates = buildNestedUpdate(fieldPath, newValue);
3894
+ await core.configManager.save(updates, fieldPath);
3895
+ const toast = isHotReloadable(fieldPath) ? `\u2705 ${fieldPath} = ${newValue}` : `\u2705 ${fieldPath} = ${newValue} (restart needed)`;
3896
+ try {
3897
+ await ctx.answerCallbackQuery({ text: toast });
3898
+ } catch {
3899
+ }
3900
+ try {
3901
+ await ctx.editMessageReplyMarkup({ reply_markup: buildSettingsKeyboard(core) });
3902
+ } catch {
3903
+ }
3904
+ } catch (err) {
3905
+ log12.error({ err, fieldPath }, "Failed to toggle config");
3906
+ try {
3907
+ await ctx.answerCallbackQuery({ text: "\u274C Failed to update" });
3908
+ } catch {
3909
+ }
3910
+ }
3911
+ });
3912
+ bot.callbackQuery(/^s:select:/, async (ctx) => {
3913
+ const fieldPath = ctx.callbackQuery.data.replace("s:select:", "");
3914
+ const config = core.configManager.get();
3915
+ const fieldDef = getSafeFields().find((f) => f.path === fieldPath);
3916
+ if (!fieldDef) return;
3917
+ const options = resolveOptions(fieldDef, config) ?? [];
3918
+ const currentValue = getConfigValue(config, fieldPath);
3919
+ const kb = new InlineKeyboard6();
3920
+ for (const opt of options) {
3921
+ const marker = opt === String(currentValue) ? " \u2713" : "";
3922
+ kb.text(`${opt}${marker}`, `s:pick:${fieldPath}:${opt}`).row();
3923
+ }
3924
+ kb.text("\u25C0\uFE0F Back", "s:back:refresh");
3925
+ try {
3926
+ await ctx.answerCallbackQuery();
3927
+ } catch {
3928
+ }
3929
+ try {
3930
+ await ctx.editMessageText(`<b>\u2699\uFE0F ${fieldDef.displayName}</b>
3931
+ Select a value:`, {
3932
+ parse_mode: "HTML",
3933
+ reply_markup: kb
3934
+ });
3935
+ } catch {
3936
+ }
3937
+ });
3938
+ bot.callbackQuery(/^s:pick:/, async (ctx) => {
3939
+ const parts = ctx.callbackQuery.data.replace("s:pick:", "").split(":");
3940
+ const fieldPath = parts.slice(0, -1).join(":");
3941
+ const newValue = parts[parts.length - 1];
3942
+ try {
3943
+ const updates = buildNestedUpdate(fieldPath, newValue);
3944
+ await core.configManager.save(updates, fieldPath);
3945
+ try {
3946
+ await ctx.answerCallbackQuery({ text: `\u2705 ${fieldPath} = ${newValue}` });
3947
+ } catch {
3948
+ }
3949
+ try {
3950
+ await ctx.editMessageText(`<b>\u2699\uFE0F Settings</b>
3951
+ Tap to change:`, {
3952
+ parse_mode: "HTML",
3953
+ reply_markup: buildSettingsKeyboard(core)
3954
+ });
3955
+ } catch {
3956
+ }
3957
+ } catch (err) {
3958
+ log12.error({ err, fieldPath }, "Failed to set config");
3959
+ try {
3960
+ await ctx.answerCallbackQuery({ text: "\u274C Failed to update" });
3961
+ } catch {
3962
+ }
3963
+ }
3964
+ });
3965
+ bot.callbackQuery(/^s:input:/, async (ctx) => {
3966
+ const fieldPath = ctx.callbackQuery.data.replace("s:input:", "");
3967
+ const config = core.configManager.get();
3968
+ const fieldDef = getSafeFields().find((f) => f.path === fieldPath);
3969
+ if (!fieldDef) return;
3970
+ const currentValue = getConfigValue(config, fieldPath);
3971
+ const assistant = getAssistantSession();
3972
+ if (!assistant) {
3973
+ try {
3974
+ await ctx.answerCallbackQuery({ text: "\u26A0\uFE0F Start the assistant first (/assistant)" });
3975
+ } catch {
3976
+ }
3977
+ return;
3978
+ }
3979
+ try {
3980
+ await ctx.answerCallbackQuery({ text: `Delegating to assistant...` });
3981
+ } catch {
3982
+ }
3983
+ const prompt = `User wants to change ${fieldDef.displayName} (config path: ${fieldPath}). Current value: ${JSON.stringify(currentValue)}. Ask them for the new value and apply it using: openacp config set ${fieldPath} <value>`;
3984
+ await assistant.enqueuePrompt(prompt);
3985
+ });
3986
+ bot.callbackQuery("s:back", async (ctx) => {
3987
+ try {
3988
+ await ctx.answerCallbackQuery();
3989
+ } catch {
3990
+ }
3991
+ const { buildMenuKeyboard: buildMenuKeyboard3 } = await import("./menu-6RCPBVGQ.js");
3992
+ try {
3993
+ await ctx.editMessageText(`<b>OpenACP Menu</b>
3994
+ Choose an action:`, {
3995
+ parse_mode: "HTML",
3996
+ reply_markup: buildMenuKeyboard3()
3997
+ });
3998
+ } catch {
3999
+ }
4000
+ });
4001
+ bot.callbackQuery("s:back:refresh", async (ctx) => {
4002
+ try {
4003
+ await ctx.answerCallbackQuery();
4004
+ } catch {
4005
+ }
4006
+ try {
4007
+ await ctx.editMessageText(`<b>\u2699\uFE0F Settings</b>
4008
+ Tap to change:`, {
4009
+ parse_mode: "HTML",
4010
+ reply_markup: buildSettingsKeyboard(core)
4011
+ });
4012
+ } catch {
4013
+ }
4014
+ });
4015
+ }
4016
+ function buildNestedUpdate(dotPath, value) {
4017
+ const parts = dotPath.split(".");
4018
+ const result = {};
4019
+ let target = result;
4020
+ for (let i = 0; i < parts.length - 1; i++) {
4021
+ target[parts[i]] = {};
4022
+ target = target[parts[i]];
4023
+ }
4024
+ target[parts[parts.length - 1]] = value;
4025
+ return result;
4026
+ }
4027
+
4028
+ // src/adapters/telegram/commands/doctor.ts
4029
+ import { InlineKeyboard as InlineKeyboard7 } from "grammy";
4030
+ var log13 = createChildLogger({ module: "telegram-cmd-doctor" });
4031
+ var pendingFixesStore = /* @__PURE__ */ new Map();
4032
+ function renderReport(report) {
4033
+ const icons = { pass: "\u2705", warn: "\u26A0\uFE0F", fail: "\u274C" };
4034
+ const lines = ["\u{1FA7A} <b>OpenACP Doctor</b>\n"];
4035
+ for (const category of report.categories) {
4036
+ lines.push(`<b>${category.name}</b>`);
4037
+ for (const result of category.results) {
4038
+ lines.push(` ${icons[result.status]} ${escapeHtml2(result.message)}`);
4039
+ }
4040
+ lines.push("");
4041
+ }
4042
+ const { passed, warnings, failed, fixed } = report.summary;
4043
+ const fixedStr = fixed > 0 ? `, ${fixed} fixed` : "";
4044
+ lines.push(`<b>Result:</b> ${passed} passed, ${warnings} warnings, ${failed} failed${fixedStr}`);
4045
+ let keyboard;
4046
+ if (report.pendingFixes.length > 0) {
4047
+ keyboard = new InlineKeyboard7();
4048
+ for (let i = 0; i < report.pendingFixes.length; i++) {
4049
+ const label = `\u{1F527} Fix: ${report.pendingFixes[i].message.slice(0, 30)}`;
4050
+ keyboard.text(label, `m:doctor:fix:${i}`).row();
4051
+ }
4052
+ }
4053
+ return { text: lines.join("\n"), keyboard };
4054
+ }
4055
+ function escapeHtml2(text) {
4056
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
4057
+ }
4058
+ async function handleDoctor(ctx) {
4059
+ const statusMsg = await ctx.reply("\u{1FA7A} Running diagnostics...", { parse_mode: "HTML" });
4060
+ try {
4061
+ const engine = new DoctorEngine();
4062
+ const report = await engine.runAll();
4063
+ const { text, keyboard } = renderReport(report);
4064
+ const storeKey = `${ctx.chat.id}:${statusMsg.message_id}`;
4065
+ if (report.pendingFixes.length > 0) {
4066
+ pendingFixesStore.set(storeKey, report.pendingFixes);
4067
+ }
4068
+ await ctx.api.editMessageText(ctx.chat.id, statusMsg.message_id, text, {
4069
+ parse_mode: "HTML",
4070
+ reply_markup: keyboard
4071
+ });
4072
+ } catch (err) {
4073
+ log13.error({ err }, "Doctor command failed");
4074
+ await ctx.api.editMessageText(
4075
+ ctx.chat.id,
4076
+ statusMsg.message_id,
4077
+ `\u274C Doctor failed: ${err instanceof Error ? err.message : String(err)}`,
4078
+ { parse_mode: "HTML" }
4079
+ );
4080
+ }
4081
+ }
4082
+ function setupDoctorCallbacks(bot) {
4083
+ bot.callbackQuery(/^m:doctor:fix:/, async (ctx) => {
4084
+ const data = ctx.callbackQuery.data;
4085
+ const index = parseInt(data.replace("m:doctor:fix:", ""), 10);
4086
+ const chatId = ctx.callbackQuery.message?.chat.id;
4087
+ const messageId = ctx.callbackQuery.message?.message_id;
4088
+ try {
4089
+ await ctx.answerCallbackQuery({ text: "Applying fix..." });
4090
+ } catch {
4091
+ }
4092
+ if (chatId === void 0 || messageId === void 0) return;
4093
+ const storeKey = `${chatId}:${messageId}`;
4094
+ const fixes = pendingFixesStore.get(storeKey);
4095
+ if (!fixes || index < 0 || index >= fixes.length) {
4096
+ try {
4097
+ await ctx.answerCallbackQuery({ text: "Fix no longer available" });
4098
+ } catch {
4099
+ }
4100
+ return;
4101
+ }
4102
+ const pending = fixes[index];
4103
+ try {
4104
+ const result = await pending.fix();
4105
+ if (result.success) {
4106
+ const engine = new DoctorEngine();
4107
+ const report = await engine.runAll();
4108
+ const { text, keyboard } = renderReport(report);
4109
+ if (report.pendingFixes.length > 0) {
4110
+ pendingFixesStore.set(storeKey, report.pendingFixes);
4111
+ } else {
4112
+ pendingFixesStore.delete(storeKey);
4113
+ }
4114
+ await ctx.editMessageText(text, { parse_mode: "HTML", reply_markup: keyboard });
4115
+ } else {
4116
+ try {
4117
+ await ctx.answerCallbackQuery({ text: `Fix failed: ${result.message}` });
4118
+ } catch {
4119
+ }
4120
+ }
4121
+ } catch (err) {
4122
+ log13.error({ err, index }, "Doctor fix callback failed");
4123
+ }
4124
+ });
4125
+ bot.callbackQuery("m:doctor", async (ctx) => {
4126
+ try {
4127
+ await ctx.answerCallbackQuery();
4128
+ } catch {
4129
+ }
4130
+ await handleDoctor(ctx);
4131
+ });
4132
+ }
4133
+
3754
4134
  // src/adapters/telegram/commands/index.ts
3755
4135
  function setupCommands(bot, core, chatId, assistant) {
3756
4136
  bot.command("new", (ctx) => handleNew(ctx, core, chatId, assistant));
@@ -3759,6 +4139,7 @@ function setupCommands(bot, core, chatId, assistant) {
3759
4139
  bot.command("status", (ctx) => handleStatus(ctx, core));
3760
4140
  bot.command("sessions", (ctx) => handleTopics(ctx, core));
3761
4141
  bot.command("agents", (ctx) => handleAgents(ctx, core));
4142
+ bot.command("install", (ctx) => handleInstall(ctx, core));
3762
4143
  bot.command("help", (ctx) => handleHelp(ctx));
3763
4144
  bot.command("menu", (ctx) => handleMenu(ctx));
3764
4145
  bot.command("enable_dangerous", (ctx) => handleEnableDangerous(ctx, core));
@@ -3767,10 +4148,19 @@ function setupCommands(bot, core, chatId, assistant) {
3767
4148
  bot.command("update", (ctx) => handleUpdate(ctx, core));
3768
4149
  bot.command("integrate", (ctx) => handleIntegrate(ctx, core));
3769
4150
  bot.command("clear", (ctx) => handleClear(ctx, assistant));
4151
+ bot.command("doctor", (ctx) => handleDoctor(ctx));
3770
4152
  }
3771
- function setupAllCallbacks(bot, core, chatId, systemTopicIds) {
4153
+ function setupAllCallbacks(bot, core, chatId, systemTopicIds, getAssistantSession) {
3772
4154
  setupNewSessionCallbacks(bot, core, chatId);
3773
4155
  setupSessionCallbacks(bot, core, chatId, systemTopicIds);
4156
+ setupSettingsCallbacks(bot, core, getAssistantSession ?? (() => void 0));
4157
+ setupDoctorCallbacks(bot);
4158
+ bot.callbackQuery(/^ag:/, (ctx) => handleAgentCallback(ctx, core));
4159
+ bot.callbackQuery(/^na:/, async (ctx) => {
4160
+ const agentKey = ctx.callbackQuery.data.replace("na:", "");
4161
+ await ctx.answerCallbackQuery();
4162
+ await createSessionDirect(ctx, core, chatId, agentKey, core.configManager.get().workspace.baseDir);
4163
+ });
3774
4164
  bot.callbackQuery(/^m:/, async (ctx) => {
3775
4165
  const data = ctx.callbackQuery.data;
3776
4166
  try {
@@ -3802,6 +4192,9 @@ function setupAllCallbacks(bot, core, chatId, systemTopicIds) {
3802
4192
  case "m:topics":
3803
4193
  await handleTopics(ctx, core);
3804
4194
  break;
4195
+ case "m:settings":
4196
+ await handleSettings(ctx, core);
4197
+ break;
3805
4198
  }
3806
4199
  });
3807
4200
  }
@@ -3812,6 +4205,7 @@ var STATIC_COMMANDS = [
3812
4205
  { command: "status", description: "Show status" },
3813
4206
  { command: "sessions", description: "List all sessions" },
3814
4207
  { command: "agents", description: "List available agents" },
4208
+ { command: "install", description: "Install a new agent" },
3815
4209
  { command: "help", description: "Help" },
3816
4210
  { command: "menu", description: "Show menu" },
3817
4211
  { command: "enable_dangerous", description: "Auto-approve all permission requests (session only)" },
@@ -3820,13 +4214,14 @@ var STATIC_COMMANDS = [
3820
4214
  { command: "handoff", description: "Continue this session in your terminal" },
3821
4215
  { command: "clear", description: "Clear assistant history" },
3822
4216
  { command: "restart", description: "Restart OpenACP" },
3823
- { command: "update", description: "Update to latest version and restart" }
4217
+ { command: "update", description: "Update to latest version and restart" },
4218
+ { command: "doctor", description: "Run system diagnostics" }
3824
4219
  ];
3825
4220
 
3826
4221
  // src/adapters/telegram/permissions.ts
3827
- import { InlineKeyboard as InlineKeyboard6 } from "grammy";
4222
+ import { InlineKeyboard as InlineKeyboard8 } from "grammy";
3828
4223
  import { nanoid as nanoid2 } from "nanoid";
3829
- var log11 = createChildLogger({ module: "telegram-permissions" });
4224
+ var log14 = createChildLogger({ module: "telegram-permissions" });
3830
4225
  var PermissionHandler = class {
3831
4226
  constructor(bot, chatId, getSession, sendNotification) {
3832
4227
  this.bot = bot;
@@ -3843,7 +4238,7 @@ var PermissionHandler = class {
3843
4238
  requestId: request.id,
3844
4239
  options: request.options.map((o) => ({ id: o.id, isAllow: o.isAllow }))
3845
4240
  });
3846
- const keyboard = new InlineKeyboard6();
4241
+ const keyboard = new InlineKeyboard8();
3847
4242
  for (const option of request.options) {
3848
4243
  const emoji = option.isAllow ? "\u2705" : "\u274C";
3849
4244
  keyboard.text(`${emoji} ${option.label}`, `p:${callbackKey}:${option.id}`);
@@ -3886,7 +4281,7 @@ ${escapeHtml(request.description)}`,
3886
4281
  }
3887
4282
  const session = this.getSession(pending.sessionId);
3888
4283
  const isAllow = pending.options.find((o) => o.id === optionId)?.isAllow ?? false;
3889
- log11.info({ requestId: pending.requestId, optionId, isAllow }, "Permission responded");
4284
+ log14.info({ requestId: pending.requestId, optionId, isAllow }, "Permission responded");
3890
4285
  if (session?.permissionGate.requestId === pending.requestId) {
3891
4286
  session.permissionGate.resolve(optionId);
3892
4287
  }
@@ -4268,20 +4663,19 @@ Session logs auto-cleanup: 30 days (configurable via \`logging.sessionLogRetenti
4268
4663
  `;
4269
4664
 
4270
4665
  // src/adapters/telegram/assistant.ts
4271
- var log12 = createChildLogger({ module: "telegram-assistant" });
4666
+ var log15 = createChildLogger({ module: "telegram-assistant" });
4272
4667
  async function spawnAssistant(core, adapter, assistantTopicId) {
4273
4668
  const config = core.configManager.get();
4274
- log12.info({ agent: config.defaultAgent }, "Creating assistant session...");
4275
- const session = await core.sessionManager.createSession(
4276
- "telegram",
4277
- config.defaultAgent,
4278
- core.configManager.resolveWorkspace(),
4279
- core.agentManager
4280
- );
4669
+ log15.info({ agent: config.defaultAgent }, "Creating assistant session...");
4670
+ const session = await core.createSession({
4671
+ channelId: "telegram",
4672
+ agentName: config.defaultAgent,
4673
+ workingDirectory: core.configManager.resolveWorkspace(),
4674
+ initialName: "Assistant"
4675
+ // Prevent auto-naming from triggering after system prompt
4676
+ });
4281
4677
  session.threadId = String(assistantTopicId);
4282
- session.name = "Assistant";
4283
- log12.info({ sessionId: session.id }, "Assistant agent spawned");
4284
- core.wireSessionEvents(session, adapter);
4678
+ log15.info({ sessionId: session.id }, "Assistant agent spawned");
4285
4679
  const allRecords = core.sessionManager.listRecords();
4286
4680
  const activeCount = allRecords.filter((r) => r.status === "active" || r.status === "initializing").length;
4287
4681
  const statusCounts = /* @__PURE__ */ new Map();
@@ -4297,9 +4691,9 @@ async function spawnAssistant(core, adapter, assistantTopicId) {
4297
4691
  };
4298
4692
  const systemPrompt = buildAssistantSystemPrompt(ctx);
4299
4693
  const ready = session.enqueuePrompt(systemPrompt).then(() => {
4300
- log12.info({ sessionId: session.id }, "Assistant system prompt completed");
4694
+ log15.info({ sessionId: session.id }, "Assistant system prompt completed");
4301
4695
  }).catch((err) => {
4302
- log12.warn({ err }, "Assistant system prompt failed");
4696
+ log15.warn({ err }, "Assistant system prompt failed");
4303
4697
  });
4304
4698
  return { session, ready };
4305
4699
  }
@@ -4367,8 +4761,10 @@ function buildAssistantSystemPrompt(ctx) {
4367
4761
  - Execute: \`openacp api cleanup --status <statuses>\`
4368
4762
 
4369
4763
  ### Configuration
4370
- - View: \`openacp api config\`
4371
- - Update: \`openacp api config set <key> <value>\`
4764
+ - View: \`openacp config\` (or \`openacp api config\` \u2014 deprecated)
4765
+ - Update: \`openacp config set <key> <value>\`
4766
+ - When user asks about "settings" or "config", use \`openacp config set\` directly
4767
+ - When receiving a delegated request from the Settings menu, ask user for the new value, then apply with \`openacp config set <path> <value>\`
4372
4768
 
4373
4769
  ### Restart / Update
4374
4770
  - Always ask for confirmation \u2014 these are disruptive actions
@@ -4398,8 +4794,10 @@ openacp api cleanup --status finished,error
4398
4794
 
4399
4795
  # System
4400
4796
  openacp api health # System health
4401
- openacp api config # Show config
4402
- openacp api config set <key> <value> # Update config
4797
+ openacp config # Edit config (interactive)
4798
+ openacp config set <key> <value> # Update config value
4799
+ openacp api config # Show config (deprecated)
4800
+ openacp api config set <key> <value> # Update config (deprecated)
4403
4801
  openacp api adapters # List adapters
4404
4802
  openacp api tunnel # Tunnel status
4405
4803
  openacp api notify "message" # Send notification
@@ -4432,7 +4830,7 @@ function redirectToAssistant(chatId, assistantTopicId) {
4432
4830
  }
4433
4831
 
4434
4832
  // src/adapters/telegram/activity.ts
4435
- var log13 = createChildLogger({ module: "telegram:activity" });
4833
+ var log16 = createChildLogger({ module: "telegram:activity" });
4436
4834
  var THINKING_REFRESH_MS = 15e3;
4437
4835
  var THINKING_MAX_MS = 3 * 60 * 1e3;
4438
4836
  var ThinkingIndicator = class {
@@ -4464,7 +4862,7 @@ var ThinkingIndicator = class {
4464
4862
  this.startRefreshTimer();
4465
4863
  }
4466
4864
  } catch (err) {
4467
- log13.warn({ err }, "ThinkingIndicator.show() failed");
4865
+ log16.warn({ err }, "ThinkingIndicator.show() failed");
4468
4866
  } finally {
4469
4867
  this.sending = false;
4470
4868
  }
@@ -4537,7 +4935,7 @@ var UsageMessage = class {
4537
4935
  if (result) this.msgId = result.message_id;
4538
4936
  }
4539
4937
  } catch (err) {
4540
- log13.warn({ err }, "UsageMessage.send() failed");
4938
+ log16.warn({ err }, "UsageMessage.send() failed");
4541
4939
  }
4542
4940
  }
4543
4941
  getMsgId() {
@@ -4550,7 +4948,7 @@ var UsageMessage = class {
4550
4948
  try {
4551
4949
  await this.sendQueue.enqueue(() => this.api.deleteMessage(this.chatId, id));
4552
4950
  } catch (err) {
4553
- log13.warn({ err }, "UsageMessage.delete() failed");
4951
+ log16.warn({ err }, "UsageMessage.delete() failed");
4554
4952
  }
4555
4953
  }
4556
4954
  };
@@ -4636,7 +5034,7 @@ var PlanCard = class {
4636
5034
  if (result) this.msgId = result.message_id;
4637
5035
  }
4638
5036
  } catch (err) {
4639
- log13.warn({ err }, "PlanCard flush failed");
5037
+ log16.warn({ err }, "PlanCard flush failed");
4640
5038
  }
4641
5039
  }
4642
5040
  };
@@ -4699,7 +5097,7 @@ var ActivityTracker = class {
4699
5097
  })
4700
5098
  );
4701
5099
  } catch (err) {
4702
- log13.warn({ err }, "ActivityTracker.onComplete() Done send failed");
5100
+ log16.warn({ err }, "ActivityTracker.onComplete() Done send failed");
4703
5101
  }
4704
5102
  }
4705
5103
  }
@@ -4782,7 +5180,7 @@ var TelegramSendQueue = class {
4782
5180
 
4783
5181
  // src/adapters/telegram/action-detect.ts
4784
5182
  import { nanoid as nanoid3 } from "nanoid";
4785
- import { InlineKeyboard as InlineKeyboard7 } from "grammy";
5183
+ import { InlineKeyboard as InlineKeyboard9 } from "grammy";
4786
5184
  var CMD_NEW_RE = /\/new(?:\s+([^\s\u0080-\uFFFF]+)(?:\s+([^\s\u0080-\uFFFF]+))?)?/;
4787
5185
  var CMD_CANCEL_RE = /\/cancel\b/;
4788
5186
  var KW_NEW_RE = /(?:create|new)\s+session/i;
@@ -4829,7 +5227,7 @@ function removeAction(id) {
4829
5227
  actionMap.delete(id);
4830
5228
  }
4831
5229
  function buildActionKeyboard(actionId, action) {
4832
- const keyboard = new InlineKeyboard7();
5230
+ const keyboard = new InlineKeyboard9();
4833
5231
  if (action.action === "new_session") {
4834
5232
  keyboard.text("\u2705 Create session", `a:${actionId}`);
4835
5233
  keyboard.text("\u274C Cancel", `a:dismiss:${actionId}`);
@@ -4936,79 +5334,525 @@ function setupActionCallbacks(bot, core, chatId, getAssistantSessionId) {
4936
5334
  });
4937
5335
  }
4938
5336
 
4939
- // src/adapters/telegram/adapter.ts
4940
- var log14 = createChildLogger({ module: "telegram" });
4941
- function patchedFetch(input, init) {
4942
- if (init?.signal && !(init.signal instanceof AbortSignal)) {
4943
- const nativeController = new AbortController();
4944
- const polyfillSignal = init.signal;
4945
- if (polyfillSignal.aborted) {
4946
- nativeController.abort();
4947
- } else {
4948
- polyfillSignal.addEventListener("abort", () => nativeController.abort());
4949
- }
4950
- init = { ...init, signal: nativeController.signal };
5337
+ // src/adapters/telegram/tool-call-tracker.ts
5338
+ var log17 = createChildLogger({ module: "tool-call-tracker" });
5339
+ var ToolCallTracker = class {
5340
+ constructor(bot, chatId, sendQueue) {
5341
+ this.bot = bot;
5342
+ this.chatId = chatId;
5343
+ this.sendQueue = sendQueue;
4951
5344
  }
4952
- return fetch(input, init);
4953
- }
4954
- var TelegramAdapter = class extends ChannelAdapter {
4955
- bot;
4956
- telegramConfig;
4957
- sessionDrafts = /* @__PURE__ */ new Map();
4958
- sessionTextBuffers = /* @__PURE__ */ new Map();
4959
- toolCallMessages = /* @__PURE__ */ new Map();
4960
- // sessionId → (toolCallId → state)
4961
- permissionHandler;
4962
- assistantSession = null;
4963
- assistantInitializing = false;
4964
- notificationTopicId;
4965
- assistantTopicId;
4966
- skillMessages = /* @__PURE__ */ new Map();
4967
- // sessionId → pinned messageId
4968
- sendQueue = new TelegramSendQueue(3e3);
4969
- sessionTrackers = /* @__PURE__ */ new Map();
4970
- getOrCreateTracker(sessionId, threadId) {
4971
- let tracker = this.sessionTrackers.get(sessionId);
4972
- if (!tracker) {
4973
- tracker = new ActivityTracker(
4974
- this.bot.api,
4975
- this.telegramConfig.chatId,
4976
- threadId,
4977
- this.sendQueue
5345
+ sessions = /* @__PURE__ */ new Map();
5346
+ async trackNewCall(sessionId, threadId, meta) {
5347
+ if (!this.sessions.has(sessionId)) {
5348
+ this.sessions.set(sessionId, /* @__PURE__ */ new Map());
5349
+ }
5350
+ let resolveReady;
5351
+ const ready = new Promise((r) => {
5352
+ resolveReady = r;
5353
+ });
5354
+ this.sessions.get(sessionId).set(meta.id, {
5355
+ msgId: 0,
5356
+ name: meta.name,
5357
+ kind: meta.kind,
5358
+ viewerLinks: meta.viewerLinks,
5359
+ viewerFilePath: meta.viewerFilePath,
5360
+ ready
5361
+ });
5362
+ const msg = await this.sendQueue.enqueue(
5363
+ () => this.bot.api.sendMessage(
5364
+ this.chatId,
5365
+ formatToolCall(meta),
5366
+ {
5367
+ message_thread_id: threadId,
5368
+ parse_mode: "HTML",
5369
+ disable_notification: true
5370
+ }
5371
+ )
5372
+ );
5373
+ const toolEntry = this.sessions.get(sessionId).get(meta.id);
5374
+ toolEntry.msgId = msg.message_id;
5375
+ resolveReady();
5376
+ }
5377
+ async updateCall(sessionId, meta) {
5378
+ const toolState = this.sessions.get(sessionId)?.get(meta.id);
5379
+ if (!toolState) return;
5380
+ if (meta.viewerLinks) {
5381
+ toolState.viewerLinks = meta.viewerLinks;
5382
+ log17.debug({ toolId: meta.id, viewerLinks: meta.viewerLinks }, "Accumulated viewerLinks");
5383
+ }
5384
+ if (meta.viewerFilePath) toolState.viewerFilePath = meta.viewerFilePath;
5385
+ if (meta.name) toolState.name = meta.name;
5386
+ if (meta.kind) toolState.kind = meta.kind;
5387
+ const isTerminal = meta.status === "completed" || meta.status === "failed";
5388
+ if (!isTerminal) return;
5389
+ await toolState.ready;
5390
+ log17.debug(
5391
+ {
5392
+ toolId: meta.id,
5393
+ status: meta.status,
5394
+ hasViewerLinks: !!toolState.viewerLinks,
5395
+ viewerLinks: toolState.viewerLinks,
5396
+ name: toolState.name,
5397
+ msgId: toolState.msgId
5398
+ },
5399
+ "Tool completed, preparing edit"
5400
+ );
5401
+ const merged = {
5402
+ ...meta,
5403
+ name: toolState.name,
5404
+ kind: toolState.kind,
5405
+ viewerLinks: toolState.viewerLinks,
5406
+ viewerFilePath: toolState.viewerFilePath
5407
+ };
5408
+ const formattedText = formatToolUpdate(merged);
5409
+ try {
5410
+ await this.sendQueue.enqueue(
5411
+ () => this.bot.api.editMessageText(
5412
+ this.chatId,
5413
+ toolState.msgId,
5414
+ formattedText,
5415
+ { parse_mode: "HTML" }
5416
+ )
5417
+ );
5418
+ } catch (err) {
5419
+ log17.warn(
5420
+ {
5421
+ err,
5422
+ msgId: toolState.msgId,
5423
+ textLen: formattedText.length,
5424
+ hasViewerLinks: !!merged.viewerLinks
5425
+ },
5426
+ "Tool update edit failed"
4978
5427
  );
4979
- this.sessionTrackers.set(sessionId, tracker);
4980
5428
  }
4981
- return tracker;
4982
5429
  }
4983
- constructor(core, config) {
4984
- super(core, config);
4985
- this.telegramConfig = config;
5430
+ cleanup(sessionId) {
5431
+ this.sessions.delete(sessionId);
4986
5432
  }
4987
- async start() {
4988
- this.bot = new Bot(this.telegramConfig.botToken, { client: { fetch: patchedFetch } });
4989
- this.bot.catch((err) => {
4990
- const rootCause = err.error instanceof Error ? err.error : err;
4991
- log14.error({ err: rootCause }, "Telegram bot error");
4992
- });
4993
- this.bot.api.config.use(async (prev, method, payload, signal) => {
4994
- const maxRetries = 3;
4995
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
4996
- const result = await prev(method, payload, signal);
4997
- if (result.ok || result.error_code !== 429 || attempt === maxRetries) {
4998
- return result;
4999
- }
5000
- const retryAfter = (result.parameters?.retry_after ?? 5) + 1;
5001
- const rateLimitedMethods = ["sendMessage", "editMessageText", "editMessageReplyMarkup"];
5002
- if (rateLimitedMethods.includes(method)) {
5003
- this.sendQueue.onRateLimited();
5004
- }
5005
- log14.warn(
5006
- { method, retryAfter, attempt: attempt + 1 },
5007
- "Rate limited by Telegram, retrying"
5008
- );
5009
- await new Promise((r) => setTimeout(r, retryAfter * 1e3));
5010
- }
5011
- return prev(method, payload, signal);
5433
+ };
5434
+
5435
+ // src/adapters/telegram/streaming.ts
5436
+ var FLUSH_INTERVAL = 5e3;
5437
+ var MessageDraft = class {
5438
+ constructor(bot, chatId, threadId, sendQueue, sessionId) {
5439
+ this.bot = bot;
5440
+ this.chatId = chatId;
5441
+ this.threadId = threadId;
5442
+ this.sendQueue = sendQueue;
5443
+ this.sessionId = sessionId;
5444
+ }
5445
+ buffer = "";
5446
+ messageId;
5447
+ firstFlushPending = false;
5448
+ flushTimer;
5449
+ flushPromise = Promise.resolve();
5450
+ lastSentBuffer = "";
5451
+ displayTruncated = false;
5452
+ append(text) {
5453
+ if (!text) return;
5454
+ this.buffer += text;
5455
+ this.scheduleFlush();
5456
+ }
5457
+ scheduleFlush() {
5458
+ if (this.flushTimer) return;
5459
+ this.flushTimer = setTimeout(() => {
5460
+ this.flushTimer = void 0;
5461
+ this.flushPromise = this.flushPromise.then(() => this.flush()).catch(() => {
5462
+ });
5463
+ }, FLUSH_INTERVAL);
5464
+ }
5465
+ async flush() {
5466
+ if (!this.buffer) return;
5467
+ if (this.firstFlushPending) return;
5468
+ const snapshot = this.buffer;
5469
+ let html = markdownToTelegramHtml(snapshot);
5470
+ if (!html) return;
5471
+ let truncated = false;
5472
+ if (html.length > 4096) {
5473
+ const ratio = 4e3 / html.length;
5474
+ const targetLen = Math.floor(snapshot.length * ratio);
5475
+ let cutAt = snapshot.lastIndexOf("\n", targetLen);
5476
+ if (cutAt < targetLen * 0.5) cutAt = targetLen;
5477
+ html = markdownToTelegramHtml(snapshot.slice(0, cutAt) + "\n\u2026");
5478
+ truncated = true;
5479
+ if (html.length > 4096) {
5480
+ html = html.slice(0, 4090) + "\n\u2026";
5481
+ }
5482
+ }
5483
+ if (!this.messageId) {
5484
+ this.firstFlushPending = true;
5485
+ try {
5486
+ const result = await this.sendQueue.enqueue(
5487
+ () => this.bot.api.sendMessage(this.chatId, html, {
5488
+ message_thread_id: this.threadId,
5489
+ parse_mode: "HTML",
5490
+ disable_notification: true
5491
+ }),
5492
+ { type: "other" }
5493
+ );
5494
+ if (result) {
5495
+ this.messageId = result.message_id;
5496
+ if (!truncated) {
5497
+ this.lastSentBuffer = snapshot;
5498
+ this.displayTruncated = false;
5499
+ } else {
5500
+ this.displayTruncated = true;
5501
+ }
5502
+ }
5503
+ } catch {
5504
+ } finally {
5505
+ this.firstFlushPending = false;
5506
+ }
5507
+ } else {
5508
+ try {
5509
+ const result = await this.sendQueue.enqueue(
5510
+ () => this.bot.api.editMessageText(this.chatId, this.messageId, html, {
5511
+ parse_mode: "HTML"
5512
+ }),
5513
+ { type: "text", key: this.sessionId }
5514
+ );
5515
+ if (result !== void 0) {
5516
+ if (!truncated) {
5517
+ this.lastSentBuffer = snapshot;
5518
+ this.displayTruncated = false;
5519
+ } else {
5520
+ this.displayTruncated = true;
5521
+ }
5522
+ }
5523
+ } catch {
5524
+ }
5525
+ }
5526
+ }
5527
+ async finalize() {
5528
+ if (this.flushTimer) {
5529
+ clearTimeout(this.flushTimer);
5530
+ this.flushTimer = void 0;
5531
+ }
5532
+ await this.flushPromise;
5533
+ if (!this.buffer) return this.messageId;
5534
+ if (this.messageId && this.buffer === this.lastSentBuffer && !this.displayTruncated) {
5535
+ return this.messageId;
5536
+ }
5537
+ const fullHtml = markdownToTelegramHtml(this.buffer);
5538
+ if (fullHtml.length <= 4096) {
5539
+ try {
5540
+ if (this.messageId) {
5541
+ await this.sendQueue.enqueue(
5542
+ () => this.bot.api.editMessageText(this.chatId, this.messageId, fullHtml, {
5543
+ parse_mode: "HTML"
5544
+ }),
5545
+ { type: "other" }
5546
+ );
5547
+ } else {
5548
+ const msg = await this.sendQueue.enqueue(
5549
+ () => this.bot.api.sendMessage(this.chatId, fullHtml, {
5550
+ message_thread_id: this.threadId,
5551
+ parse_mode: "HTML",
5552
+ disable_notification: true
5553
+ }),
5554
+ { type: "other" }
5555
+ );
5556
+ if (msg) this.messageId = msg.message_id;
5557
+ }
5558
+ return this.messageId;
5559
+ } catch {
5560
+ }
5561
+ }
5562
+ const mdChunks = splitMessage(this.buffer);
5563
+ for (let i = 0; i < mdChunks.length; i++) {
5564
+ const html = markdownToTelegramHtml(mdChunks[i]);
5565
+ try {
5566
+ if (i === 0 && this.messageId) {
5567
+ await this.sendQueue.enqueue(
5568
+ () => this.bot.api.editMessageText(this.chatId, this.messageId, html, {
5569
+ parse_mode: "HTML"
5570
+ }),
5571
+ { type: "other" }
5572
+ );
5573
+ } else {
5574
+ const msg = await this.sendQueue.enqueue(
5575
+ () => this.bot.api.sendMessage(this.chatId, html, {
5576
+ message_thread_id: this.threadId,
5577
+ parse_mode: "HTML",
5578
+ disable_notification: true
5579
+ }),
5580
+ { type: "other" }
5581
+ );
5582
+ if (msg) {
5583
+ this.messageId = msg.message_id;
5584
+ }
5585
+ }
5586
+ } catch {
5587
+ try {
5588
+ if (i === 0 && this.messageId) {
5589
+ await this.sendQueue.enqueue(
5590
+ () => this.bot.api.editMessageText(this.chatId, this.messageId, mdChunks[i].slice(0, 4096)),
5591
+ { type: "other" }
5592
+ );
5593
+ } else {
5594
+ const msg = await this.sendQueue.enqueue(
5595
+ () => this.bot.api.sendMessage(this.chatId, mdChunks[i].slice(0, 4096), {
5596
+ message_thread_id: this.threadId,
5597
+ disable_notification: true
5598
+ }),
5599
+ { type: "other" }
5600
+ );
5601
+ if (msg) {
5602
+ this.messageId = msg.message_id;
5603
+ }
5604
+ }
5605
+ } catch {
5606
+ }
5607
+ }
5608
+ }
5609
+ return this.messageId;
5610
+ }
5611
+ getMessageId() {
5612
+ return this.messageId;
5613
+ }
5614
+ };
5615
+
5616
+ // src/adapters/telegram/draft-manager.ts
5617
+ var DraftManager = class {
5618
+ constructor(bot, chatId, sendQueue) {
5619
+ this.bot = bot;
5620
+ this.chatId = chatId;
5621
+ this.sendQueue = sendQueue;
5622
+ }
5623
+ drafts = /* @__PURE__ */ new Map();
5624
+ textBuffers = /* @__PURE__ */ new Map();
5625
+ getOrCreate(sessionId, threadId) {
5626
+ let draft = this.drafts.get(sessionId);
5627
+ if (!draft) {
5628
+ draft = new MessageDraft(
5629
+ this.bot,
5630
+ this.chatId,
5631
+ threadId,
5632
+ this.sendQueue,
5633
+ sessionId
5634
+ );
5635
+ this.drafts.set(sessionId, draft);
5636
+ }
5637
+ return draft;
5638
+ }
5639
+ hasDraft(sessionId) {
5640
+ return this.drafts.has(sessionId);
5641
+ }
5642
+ appendText(sessionId, text) {
5643
+ this.textBuffers.set(
5644
+ sessionId,
5645
+ (this.textBuffers.get(sessionId) ?? "") + text
5646
+ );
5647
+ }
5648
+ /**
5649
+ * Finalize the current draft and return the message ID.
5650
+ * Optionally detects actions in assistant responses.
5651
+ */
5652
+ async finalize(sessionId, assistantSessionId) {
5653
+ const draft = this.drafts.get(sessionId);
5654
+ if (!draft) return;
5655
+ this.drafts.delete(sessionId);
5656
+ const finalMsgId = await draft.finalize();
5657
+ if (assistantSessionId && sessionId === assistantSessionId) {
5658
+ const fullText = this.textBuffers.get(sessionId);
5659
+ this.textBuffers.delete(sessionId);
5660
+ if (fullText && finalMsgId) {
5661
+ const detected = detectAction(fullText);
5662
+ if (detected) {
5663
+ const actionId = storeAction(detected);
5664
+ const keyboard = buildActionKeyboard(actionId, detected);
5665
+ try {
5666
+ await this.bot.api.editMessageReplyMarkup(
5667
+ this.chatId,
5668
+ finalMsgId,
5669
+ { reply_markup: keyboard }
5670
+ );
5671
+ } catch {
5672
+ }
5673
+ }
5674
+ }
5675
+ } else {
5676
+ this.textBuffers.delete(sessionId);
5677
+ }
5678
+ }
5679
+ cleanup(sessionId) {
5680
+ this.drafts.delete(sessionId);
5681
+ this.textBuffers.delete(sessionId);
5682
+ }
5683
+ };
5684
+
5685
+ // src/adapters/telegram/skill-command-manager.ts
5686
+ var log18 = createChildLogger({ module: "skill-commands" });
5687
+ var SkillCommandManager = class {
5688
+ // sessionId → pinned msgId
5689
+ constructor(bot, chatId, sendQueue, sessionManager) {
5690
+ this.bot = bot;
5691
+ this.chatId = chatId;
5692
+ this.sendQueue = sendQueue;
5693
+ this.sessionManager = sessionManager;
5694
+ }
5695
+ messages = /* @__PURE__ */ new Map();
5696
+ async send(sessionId, threadId, commands) {
5697
+ if (!this.messages.has(sessionId)) {
5698
+ const record = this.sessionManager.getSessionRecord(sessionId);
5699
+ const platform = record?.platform;
5700
+ if (platform?.skillMsgId) {
5701
+ this.messages.set(sessionId, platform.skillMsgId);
5702
+ }
5703
+ }
5704
+ if (commands.length === 0) {
5705
+ await this.cleanup(sessionId);
5706
+ return;
5707
+ }
5708
+ const messages = buildSkillMessages(commands);
5709
+ const existingMsgId = this.messages.get(sessionId);
5710
+ if (existingMsgId) {
5711
+ try {
5712
+ await this.bot.api.editMessageText(
5713
+ this.chatId,
5714
+ existingMsgId,
5715
+ messages[0],
5716
+ { parse_mode: "HTML" }
5717
+ );
5718
+ return;
5719
+ } catch (err) {
5720
+ const msg = err instanceof Error ? err.message : "";
5721
+ if (msg.includes("message is not modified")) return;
5722
+ try {
5723
+ await this.bot.api.deleteMessage(this.chatId, existingMsgId);
5724
+ } catch {
5725
+ }
5726
+ this.messages.delete(sessionId);
5727
+ }
5728
+ }
5729
+ try {
5730
+ let firstMsgId;
5731
+ for (const text of messages) {
5732
+ const msg = await this.sendQueue.enqueue(
5733
+ () => this.bot.api.sendMessage(this.chatId, text, {
5734
+ message_thread_id: threadId,
5735
+ parse_mode: "HTML",
5736
+ disable_notification: true
5737
+ })
5738
+ );
5739
+ if (!firstMsgId) firstMsgId = msg.message_id;
5740
+ }
5741
+ this.messages.set(sessionId, firstMsgId);
5742
+ const record = this.sessionManager.getSessionRecord(sessionId);
5743
+ if (record) {
5744
+ await this.sessionManager.patchRecord(sessionId, {
5745
+ platform: { ...record.platform, skillMsgId: firstMsgId }
5746
+ });
5747
+ }
5748
+ await this.bot.api.pinChatMessage(this.chatId, firstMsgId, {
5749
+ disable_notification: true
5750
+ });
5751
+ } catch (err) {
5752
+ log18.error({ err, sessionId }, "Failed to send skill commands");
5753
+ }
5754
+ }
5755
+ async cleanup(sessionId) {
5756
+ const msgId = this.messages.get(sessionId);
5757
+ if (!msgId) return;
5758
+ try {
5759
+ await this.bot.api.editMessageText(
5760
+ this.chatId,
5761
+ msgId,
5762
+ "\u{1F6E0} <i>Session ended</i>",
5763
+ { parse_mode: "HTML" }
5764
+ );
5765
+ await this.bot.api.unpinChatMessage(this.chatId, msgId);
5766
+ } catch {
5767
+ }
5768
+ this.messages.delete(sessionId);
5769
+ const record = this.sessionManager.getSessionRecord(sessionId);
5770
+ if (record) {
5771
+ const { skillMsgId: _removed, ...rest } = record.platform;
5772
+ await this.sessionManager.patchRecord(sessionId, { platform: rest });
5773
+ }
5774
+ }
5775
+ };
5776
+
5777
+ // src/adapters/telegram/adapter.ts
5778
+ var log19 = createChildLogger({ module: "telegram" });
5779
+ function patchedFetch(input, init) {
5780
+ if (init?.signal && !(init.signal instanceof AbortSignal)) {
5781
+ const nativeController = new AbortController();
5782
+ const polyfillSignal = init.signal;
5783
+ if (polyfillSignal.aborted) {
5784
+ nativeController.abort();
5785
+ } else {
5786
+ polyfillSignal.addEventListener("abort", () => nativeController.abort());
5787
+ }
5788
+ init = { ...init, signal: nativeController.signal };
5789
+ }
5790
+ return fetch(input, init);
5791
+ }
5792
+ var TelegramAdapter = class extends ChannelAdapter {
5793
+ bot;
5794
+ telegramConfig;
5795
+ permissionHandler;
5796
+ assistantSession = null;
5797
+ assistantInitializing = false;
5798
+ notificationTopicId;
5799
+ assistantTopicId;
5800
+ sendQueue = new TelegramSendQueue(3e3);
5801
+ // Extracted managers
5802
+ toolTracker;
5803
+ draftManager;
5804
+ skillManager;
5805
+ sessionTrackers = /* @__PURE__ */ new Map();
5806
+ getOrCreateTracker(sessionId, threadId) {
5807
+ let tracker = this.sessionTrackers.get(sessionId);
5808
+ if (!tracker) {
5809
+ tracker = new ActivityTracker(
5810
+ this.bot.api,
5811
+ this.telegramConfig.chatId,
5812
+ threadId,
5813
+ this.sendQueue
5814
+ );
5815
+ this.sessionTrackers.set(sessionId, tracker);
5816
+ }
5817
+ return tracker;
5818
+ }
5819
+ constructor(core, config) {
5820
+ super(core, config);
5821
+ this.telegramConfig = config;
5822
+ }
5823
+ async start() {
5824
+ this.bot = new Bot(this.telegramConfig.botToken, { client: { fetch: patchedFetch } });
5825
+ this.toolTracker = new ToolCallTracker(this.bot, this.telegramConfig.chatId, this.sendQueue);
5826
+ this.draftManager = new DraftManager(this.bot, this.telegramConfig.chatId, this.sendQueue);
5827
+ this.skillManager = new SkillCommandManager(
5828
+ this.bot,
5829
+ this.telegramConfig.chatId,
5830
+ this.sendQueue,
5831
+ this.core.sessionManager
5832
+ );
5833
+ this.bot.catch((err) => {
5834
+ const rootCause = err.error instanceof Error ? err.error : err;
5835
+ log19.error({ err: rootCause }, "Telegram bot error");
5836
+ });
5837
+ this.bot.api.config.use(async (prev, method, payload, signal) => {
5838
+ const maxRetries = 3;
5839
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
5840
+ const result = await prev(method, payload, signal);
5841
+ if (result.ok || result.error_code !== 429 || attempt === maxRetries) {
5842
+ return result;
5843
+ }
5844
+ const retryAfter = (result.parameters?.retry_after ?? 5) + 1;
5845
+ const rateLimitedMethods = ["sendMessage", "editMessageText", "editMessageReplyMarkup"];
5846
+ if (rateLimitedMethods.includes(method)) {
5847
+ this.sendQueue.onRateLimited();
5848
+ }
5849
+ log19.warn(
5850
+ { method, retryAfter, attempt: attempt + 1 },
5851
+ "Rate limited by Telegram, retrying"
5852
+ );
5853
+ await new Promise((r) => setTimeout(r, retryAfter * 1e3));
5854
+ }
5855
+ return prev(method, payload, signal);
5012
5856
  });
5013
5857
  this.bot.api.config.use((prev, method, payload, signal) => {
5014
5858
  if (method === "getUpdates") {
@@ -5058,7 +5902,14 @@ var TelegramAdapter = class extends ChannelAdapter {
5058
5902
  this.bot,
5059
5903
  this.core,
5060
5904
  this.telegramConfig.chatId,
5061
- { notificationTopicId: this.notificationTopicId, assistantTopicId: this.assistantTopicId }
5905
+ { notificationTopicId: this.notificationTopicId, assistantTopicId: this.assistantTopicId },
5906
+ () => {
5907
+ if (!this.assistantSession) return void 0;
5908
+ return {
5909
+ topicId: this.assistantTopicId,
5910
+ enqueuePrompt: (p) => this.assistantSession.enqueuePrompt(p)
5911
+ };
5912
+ }
5062
5913
  );
5063
5914
  setupCommands(
5064
5915
  this.bot,
@@ -5105,7 +5956,7 @@ var TelegramAdapter = class extends ChannelAdapter {
5105
5956
  });
5106
5957
  return;
5107
5958
  }
5108
- const { getAgentCapabilities: getAgentCapabilities2 } = await import("./agent-registry-7HC6D4CH.js");
5959
+ const { getAgentCapabilities: getAgentCapabilities2 } = await import("./agent-registry-B5YAMA4T.js");
5109
5960
  const caps = getAgentCapabilities2(agentName);
5110
5961
  if (!caps.supportsResume || !caps.resumeCommand) {
5111
5962
  await ctx.reply("This agent does not support session transfer.", {
@@ -5127,7 +5978,7 @@ var TelegramAdapter = class extends ChannelAdapter {
5127
5978
  this.setupRoutes();
5128
5979
  this.bot.start({
5129
5980
  allowed_updates: ["message", "callback_query"],
5130
- onStart: () => log14.info(
5981
+ onStart: () => log19.info(
5131
5982
  { chatId: this.telegramConfig.chatId },
5132
5983
  "Telegram bot started"
5133
5984
  )
@@ -5149,10 +6000,10 @@ var TelegramAdapter = class extends ChannelAdapter {
5149
6000
  reply_markup: buildMenuKeyboard()
5150
6001
  });
5151
6002
  } catch (err) {
5152
- log14.warn({ err }, "Failed to send welcome message");
6003
+ log19.warn({ err }, "Failed to send welcome message");
5153
6004
  }
5154
6005
  try {
5155
- log14.info("Spawning assistant session...");
6006
+ log19.info("Spawning assistant session...");
5156
6007
  const { session, ready } = await spawnAssistant(
5157
6008
  this.core,
5158
6009
  this,
@@ -5160,13 +6011,13 @@ var TelegramAdapter = class extends ChannelAdapter {
5160
6011
  );
5161
6012
  this.assistantSession = session;
5162
6013
  this.assistantInitializing = true;
5163
- log14.info({ sessionId: session.id }, "Assistant session ready, system prompt running in background");
6014
+ log19.info({ sessionId: session.id }, "Assistant session ready, system prompt running in background");
5164
6015
  ready.then(() => {
5165
6016
  this.assistantInitializing = false;
5166
- log14.info({ sessionId: session.id }, "Assistant ready for user messages");
6017
+ log19.info({ sessionId: session.id }, "Assistant ready for user messages");
5167
6018
  });
5168
6019
  } catch (err) {
5169
- log14.error({ err }, "Failed to spawn assistant");
6020
+ log19.error({ err }, "Failed to spawn assistant");
5170
6021
  this.bot.api.sendMessage(
5171
6022
  this.telegramConfig.chatId,
5172
6023
  `\u26A0\uFE0F <b>Failed to start assistant session.</b>
@@ -5182,7 +6033,7 @@ var TelegramAdapter = class extends ChannelAdapter {
5182
6033
  await this.assistantSession.destroy();
5183
6034
  }
5184
6035
  await this.bot.stop();
5185
- log14.info("Telegram bot stopped");
6036
+ log19.info("Telegram bot stopped");
5186
6037
  }
5187
6038
  setupRoutes() {
5188
6039
  this.bot.on("message:text", async (ctx) => {
@@ -5204,16 +6055,16 @@ var TelegramAdapter = class extends ChannelAdapter {
5204
6055
  await ctx.reply("\u26A0\uFE0F Assistant is not available yet. Please try again shortly.", { parse_mode: "HTML" });
5205
6056
  return;
5206
6057
  }
5207
- await this.finalizeDraft(this.assistantSession.id);
6058
+ await this.draftManager.finalize(this.assistantSession.id, this.assistantSession.id);
5208
6059
  ctx.replyWithChatAction("typing").catch(() => {
5209
6060
  });
5210
6061
  handleAssistantMessage(this.assistantSession, ctx.message.text).catch(
5211
- (err) => log14.error({ err }, "Assistant error")
6062
+ (err) => log19.error({ err }, "Assistant error")
5212
6063
  );
5213
6064
  return;
5214
6065
  }
5215
6066
  const sessionId = this.core.sessionManager.getSessionByThread("telegram", String(threadId))?.id;
5216
- if (sessionId) await this.finalizeDraft(sessionId);
6067
+ if (sessionId) await this.draftManager.finalize(sessionId, this.assistantSession?.id);
5217
6068
  if (sessionId) {
5218
6069
  const tracker = this.sessionTrackers.get(sessionId);
5219
6070
  if (tracker) await tracker.onNewPrompt();
@@ -5225,19 +6076,17 @@ var TelegramAdapter = class extends ChannelAdapter {
5225
6076
  threadId: String(threadId),
5226
6077
  userId: String(ctx.from.id),
5227
6078
  text: ctx.message.text
5228
- }).catch((err) => log14.error({ err }, "handleMessage error"));
6079
+ }).catch((err) => log19.error({ err }, "handleMessage error"));
5229
6080
  });
5230
6081
  }
5231
6082
  // --- ChannelAdapter implementations ---
5232
6083
  async sendMessage(sessionId, content) {
5233
6084
  if (this.assistantInitializing && sessionId === this.assistantSession?.id) return;
5234
- const session = this.core.sessionManager.getSession(
5235
- sessionId
5236
- );
6085
+ const session = this.core.sessionManager.getSession(sessionId);
5237
6086
  if (!session) return;
5238
6087
  const threadId = Number(session.threadId);
5239
6088
  if (!threadId || isNaN(threadId)) {
5240
- log14.warn({ sessionId, threadId: session.threadId }, "Session has no valid threadId, skipping message");
6089
+ log19.warn({ sessionId, threadId: session.threadId }, "Session has no valid threadId, skipping message");
5241
6090
  return;
5242
6091
  }
5243
6092
  switch (content.type) {
@@ -5247,105 +6096,32 @@ var TelegramAdapter = class extends ChannelAdapter {
5247
6096
  break;
5248
6097
  }
5249
6098
  case "text": {
5250
- let draft = this.sessionDrafts.get(sessionId);
5251
- if (!draft) {
6099
+ if (!this.draftManager.hasDraft(sessionId)) {
5252
6100
  const tracker = this.getOrCreateTracker(sessionId, threadId);
5253
6101
  await tracker.onTextStart();
5254
- draft = new MessageDraft(
5255
- this.bot,
5256
- this.telegramConfig.chatId,
5257
- threadId,
5258
- this.sendQueue,
5259
- sessionId
5260
- );
5261
- this.sessionDrafts.set(sessionId, draft);
5262
6102
  }
6103
+ const draft = this.draftManager.getOrCreate(sessionId, threadId);
5263
6104
  draft.append(content.text);
5264
- this.sessionTextBuffers.set(
5265
- sessionId,
5266
- (this.sessionTextBuffers.get(sessionId) ?? "") + content.text
5267
- );
6105
+ this.draftManager.appendText(sessionId, content.text);
5268
6106
  break;
5269
6107
  }
5270
6108
  case "tool_call": {
5271
6109
  const tracker = this.getOrCreateTracker(sessionId, threadId);
5272
6110
  await tracker.onToolCall();
5273
- await this.finalizeDraft(sessionId);
6111
+ await this.draftManager.finalize(sessionId, this.assistantSession?.id);
5274
6112
  const meta = content.metadata;
5275
- if (!this.toolCallMessages.has(sessionId)) {
5276
- this.toolCallMessages.set(sessionId, /* @__PURE__ */ new Map());
5277
- }
5278
- let resolveReady;
5279
- const ready = new Promise((r) => {
5280
- resolveReady = r;
5281
- });
5282
- this.toolCallMessages.get(sessionId).set(meta.id, {
5283
- msgId: 0,
5284
- name: meta.name,
5285
- kind: meta.kind,
5286
- viewerLinks: meta.viewerLinks,
5287
- viewerFilePath: content.metadata?.viewerFilePath,
5288
- ready
6113
+ await this.toolTracker.trackNewCall(sessionId, threadId, {
6114
+ ...meta,
6115
+ viewerFilePath: content.metadata?.viewerFilePath
5289
6116
  });
5290
- const msg = await this.sendQueue.enqueue(
5291
- () => this.bot.api.sendMessage(
5292
- this.telegramConfig.chatId,
5293
- formatToolCall(meta),
5294
- {
5295
- message_thread_id: threadId,
5296
- parse_mode: "HTML",
5297
- disable_notification: true
5298
- }
5299
- )
5300
- );
5301
- const toolEntry = this.toolCallMessages.get(sessionId).get(meta.id);
5302
- toolEntry.msgId = msg.message_id;
5303
- resolveReady();
5304
6117
  break;
5305
6118
  }
5306
6119
  case "tool_update": {
5307
6120
  const meta = content.metadata;
5308
- const toolState = this.toolCallMessages.get(sessionId)?.get(meta.id);
5309
- if (toolState) {
5310
- if (meta.viewerLinks) {
5311
- toolState.viewerLinks = meta.viewerLinks;
5312
- log14.debug({ toolId: meta.id, viewerLinks: meta.viewerLinks }, "Accumulated viewerLinks");
5313
- }
5314
- const viewerFilePath = content.metadata?.viewerFilePath;
5315
- if (viewerFilePath) toolState.viewerFilePath = viewerFilePath;
5316
- if (meta.name) toolState.name = meta.name;
5317
- if (meta.kind) toolState.kind = meta.kind;
5318
- const isTerminal = meta.status === "completed" || meta.status === "failed";
5319
- if (!isTerminal) break;
5320
- await toolState.ready;
5321
- log14.debug(
5322
- { toolId: meta.id, status: meta.status, hasViewerLinks: !!toolState.viewerLinks, viewerLinks: toolState.viewerLinks, name: toolState.name, msgId: toolState.msgId },
5323
- "Tool completed, preparing edit"
5324
- );
5325
- const merged = {
5326
- ...meta,
5327
- name: toolState.name,
5328
- kind: toolState.kind,
5329
- viewerLinks: toolState.viewerLinks,
5330
- viewerFilePath: toolState.viewerFilePath
5331
- };
5332
- const formattedText = formatToolUpdate(merged);
5333
- try {
5334
- await this.sendQueue.enqueue(
5335
- () => this.bot.api.editMessageText(
5336
- this.telegramConfig.chatId,
5337
- toolState.msgId,
5338
- formattedText,
5339
- { parse_mode: "HTML" }
5340
- )
5341
- );
5342
- } catch (err) {
5343
- log14.warn(
5344
- { err, msgId: toolState.msgId, textLen: formattedText.length, hasViewerLinks: !!merged.viewerLinks },
5345
- "Tool update edit failed"
5346
- );
5347
- }
5348
- }
6121
+ await this.toolTracker.updateCall(sessionId, {
6122
+ ...meta,
6123
+ viewerFilePath: content.metadata?.viewerFilePath
6124
+ });
5349
6125
  break;
5350
6126
  }
5351
6127
  case "plan": {
@@ -5362,7 +6138,7 @@ var TelegramAdapter = class extends ChannelAdapter {
5362
6138
  }
5363
6139
  case "usage": {
5364
6140
  const meta = content.metadata;
5365
- await this.finalizeDraft(sessionId);
6141
+ await this.draftManager.finalize(sessionId, this.assistantSession?.id);
5366
6142
  const tracker = this.getOrCreateTracker(sessionId, threadId);
5367
6143
  await tracker.sendUsage(meta);
5368
6144
  if (this.notificationTopicId && sessionId !== this.assistantSession?.id) {
@@ -5388,10 +6164,10 @@ Task completed.
5388
6164
  break;
5389
6165
  }
5390
6166
  case "session_end": {
5391
- await this.finalizeDraft(sessionId);
5392
- this.sessionDrafts.delete(sessionId);
5393
- this.toolCallMessages.delete(sessionId);
5394
- await this.cleanupSkillCommands(sessionId);
6167
+ await this.draftManager.finalize(sessionId, this.assistantSession?.id);
6168
+ this.draftManager.cleanup(sessionId);
6169
+ this.toolTracker.cleanup(sessionId);
6170
+ await this.skillManager.cleanup(sessionId);
5395
6171
  const tracker = this.sessionTrackers.get(sessionId);
5396
6172
  if (tracker) {
5397
6173
  await tracker.onComplete();
@@ -5413,7 +6189,7 @@ Task completed.
5413
6189
  break;
5414
6190
  }
5415
6191
  case "error": {
5416
- await this.finalizeDraft(sessionId);
6192
+ await this.draftManager.finalize(sessionId, this.assistantSession?.id);
5417
6193
  const tracker = this.sessionTrackers.get(sessionId);
5418
6194
  if (tracker) {
5419
6195
  tracker.destroy();
@@ -5435,15 +6211,13 @@ Task completed.
5435
6211
  }
5436
6212
  }
5437
6213
  async sendPermissionRequest(sessionId, request) {
5438
- log14.info({ sessionId, requestId: request.id }, "Permission request sent");
5439
- const session = this.core.sessionManager.getSession(
5440
- sessionId
5441
- );
6214
+ log19.info({ sessionId, requestId: request.id }, "Permission request sent");
6215
+ const session = this.core.sessionManager.getSession(sessionId);
5442
6216
  if (!session) return;
5443
6217
  if (request.description.includes("openacp")) {
5444
6218
  const allowOption = request.options.find((o) => o.isAllow);
5445
6219
  if (allowOption && session.permissionGate.requestId === request.id) {
5446
- log14.info({ sessionId, requestId: request.id }, "Auto-approving openacp command");
6220
+ log19.info({ sessionId, requestId: request.id }, "Auto-approving openacp command");
5447
6221
  session.permissionGate.resolve(allowOption.id);
5448
6222
  }
5449
6223
  return;
@@ -5451,7 +6225,7 @@ Task completed.
5451
6225
  if (session.dangerousMode) {
5452
6226
  const allowOption = request.options.find((o) => o.isAllow);
5453
6227
  if (allowOption && session.permissionGate.requestId === request.id) {
5454
- log14.info({ sessionId, requestId: request.id, optionId: allowOption.id }, "Dangerous mode: auto-approving permission");
6228
+ log19.info({ sessionId, requestId: request.id, optionId: allowOption.id }, "Dangerous mode: auto-approving permission");
5455
6229
  session.permissionGate.resolve(allowOption.id);
5456
6230
  }
5457
6231
  return;
@@ -5462,7 +6236,7 @@ Task completed.
5462
6236
  }
5463
6237
  async sendNotification(notification) {
5464
6238
  if (notification.sessionId === this.assistantSession?.id) return;
5465
- log14.info(
6239
+ log19.info(
5466
6240
  { sessionId: notification.sessionId, type: notification.type },
5467
6241
  "Notification sent"
5468
6242
  );
@@ -5498,15 +6272,13 @@ Task completed.
5498
6272
  );
5499
6273
  }
5500
6274
  async createSessionThread(sessionId, name) {
5501
- log14.info({ sessionId, name }, "Session topic created");
6275
+ log19.info({ sessionId, name }, "Session topic created");
5502
6276
  return String(
5503
6277
  await createSessionTopic(this.bot, this.telegramConfig.chatId, name)
5504
6278
  );
5505
6279
  }
5506
6280
  async renameSessionThread(sessionId, newName) {
5507
- const session = this.core.sessionManager.getSession(
5508
- sessionId
5509
- );
6281
+ const session = this.core.sessionManager.getSession(sessionId);
5510
6282
  if (!session) return;
5511
6283
  await renameSessionTopic(
5512
6284
  this.bot,
@@ -5514,10 +6286,7 @@ Task completed.
5514
6286
  Number(session.threadId),
5515
6287
  newName
5516
6288
  );
5517
- await this.core.sessionManager.updateSessionName(
5518
- sessionId,
5519
- newName
5520
- );
6289
+ await this.core.sessionManager.patchRecord(sessionId, { name: newName });
5521
6290
  }
5522
6291
  async deleteSessionThread(sessionId) {
5523
6292
  const record = this.core.sessionManager.getSessionRecord(sessionId);
@@ -5527,7 +6296,7 @@ Task completed.
5527
6296
  try {
5528
6297
  await this.bot.api.deleteForumTopic(this.telegramConfig.chatId, topicId);
5529
6298
  } catch (err) {
5530
- log14.warn({ err, sessionId, topicId }, "Failed to delete forum topic (may already be deleted)");
6299
+ log19.warn({ err, sessionId, topicId }, "Failed to delete forum topic (may already be deleted)");
5531
6300
  }
5532
6301
  }
5533
6302
  async sendSkillCommands(sessionId, commands) {
@@ -5536,119 +6305,10 @@ Task completed.
5536
6305
  if (!session) return;
5537
6306
  const threadId = Number(session.threadId);
5538
6307
  if (!threadId) return;
5539
- if (!this.skillMessages.has(sessionId)) {
5540
- const record = this.core.sessionManager.getSessionRecord(sessionId);
5541
- const platform = record?.platform;
5542
- if (platform?.skillMsgId) {
5543
- this.skillMessages.set(sessionId, platform.skillMsgId);
5544
- }
5545
- }
5546
- if (commands.length === 0) {
5547
- await this.cleanupSkillCommands(sessionId);
5548
- return;
5549
- }
5550
- const messages = buildSkillMessages(commands);
5551
- const existingMsgId = this.skillMessages.get(sessionId);
5552
- if (existingMsgId) {
5553
- try {
5554
- await this.bot.api.editMessageText(
5555
- this.telegramConfig.chatId,
5556
- existingMsgId,
5557
- messages[0],
5558
- { parse_mode: "HTML" }
5559
- );
5560
- return;
5561
- } catch (err) {
5562
- const msg = err instanceof Error ? err.message : "";
5563
- if (msg.includes("message is not modified")) {
5564
- return;
5565
- }
5566
- try {
5567
- await this.bot.api.deleteMessage(this.telegramConfig.chatId, existingMsgId);
5568
- } catch {
5569
- }
5570
- this.skillMessages.delete(sessionId);
5571
- }
5572
- }
5573
- try {
5574
- let firstMsgId;
5575
- for (const text of messages) {
5576
- const msg = await this.sendQueue.enqueue(
5577
- () => this.bot.api.sendMessage(
5578
- this.telegramConfig.chatId,
5579
- text,
5580
- {
5581
- message_thread_id: threadId,
5582
- parse_mode: "HTML",
5583
- disable_notification: true
5584
- }
5585
- )
5586
- );
5587
- if (!firstMsgId) firstMsgId = msg.message_id;
5588
- }
5589
- this.skillMessages.set(sessionId, firstMsgId);
5590
- const record = this.core.sessionManager.getSessionRecord(sessionId);
5591
- if (record) {
5592
- await this.core.sessionManager.updateSessionPlatform(
5593
- sessionId,
5594
- { ...record.platform, skillMsgId: firstMsgId }
5595
- );
5596
- }
5597
- await this.bot.api.pinChatMessage(
5598
- this.telegramConfig.chatId,
5599
- firstMsgId,
5600
- { disable_notification: true }
5601
- );
5602
- } catch (err) {
5603
- log14.error({ err, sessionId }, "Failed to send skill commands");
5604
- }
6308
+ await this.skillManager.send(sessionId, threadId, commands);
5605
6309
  }
5606
6310
  async cleanupSkillCommands(sessionId) {
5607
- const msgId = this.skillMessages.get(sessionId);
5608
- if (!msgId) return;
5609
- try {
5610
- await this.bot.api.editMessageText(
5611
- this.telegramConfig.chatId,
5612
- msgId,
5613
- "\u{1F6E0} <i>Session ended</i>",
5614
- { parse_mode: "HTML" }
5615
- );
5616
- await this.bot.api.unpinChatMessage(this.telegramConfig.chatId, msgId);
5617
- } catch {
5618
- }
5619
- this.skillMessages.delete(sessionId);
5620
- const record = this.core.sessionManager.getSessionRecord(sessionId);
5621
- if (record) {
5622
- const { skillMsgId: _removed, ...rest } = record.platform;
5623
- await this.core.sessionManager.updateSessionPlatform(sessionId, rest);
5624
- }
5625
- }
5626
- async finalizeDraft(sessionId) {
5627
- const draft = this.sessionDrafts.get(sessionId);
5628
- if (!draft) return;
5629
- this.sessionDrafts.delete(sessionId);
5630
- const finalMsgId = await draft.finalize();
5631
- if (sessionId === this.assistantSession?.id) {
5632
- const fullText = this.sessionTextBuffers.get(sessionId);
5633
- this.sessionTextBuffers.delete(sessionId);
5634
- if (fullText && finalMsgId) {
5635
- const detected = detectAction(fullText);
5636
- if (detected) {
5637
- const actionId = storeAction(detected);
5638
- const keyboard = buildActionKeyboard(actionId, detected);
5639
- try {
5640
- await this.bot.api.editMessageReplyMarkup(
5641
- this.telegramConfig.chatId,
5642
- finalMsgId,
5643
- { reply_markup: keyboard }
5644
- );
5645
- } catch {
5646
- }
5647
- }
5648
- }
5649
- } else {
5650
- this.sessionTextBuffers.delete(sessionId);
5651
- }
6311
+ await this.skillManager.cleanup(sessionId);
5652
6312
  }
5653
6313
  };
5654
6314
 
@@ -5663,6 +6323,7 @@ export {
5663
6323
  PermissionGate,
5664
6324
  Session,
5665
6325
  SessionManager,
6326
+ SessionBridge,
5666
6327
  NotificationManager,
5667
6328
  MessageTransformer,
5668
6329
  OpenACPCore,
@@ -5671,4 +6332,4 @@ export {
5671
6332
  TopicManager,
5672
6333
  TelegramAdapter
5673
6334
  };
5674
- //# sourceMappingURL=chunk-KPI4HGJC.js.map
6335
+ //# sourceMappingURL=chunk-FWN3UIRT.js.map