@openacp/cli 0.4.10 → 0.4.11

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 (63) hide show
  1. package/README.md +17 -2
  2. package/dist/api-client-UN7BXQOQ.js +11 -0
  3. package/dist/{autostart-DZ3MHHMM.js → autostart-K73RQZVV.js} +3 -3
  4. package/dist/chunk-3DIPXFZJ.js +650 -0
  5. package/dist/chunk-3DIPXFZJ.js.map +1 -0
  6. package/dist/{chunk-KPI4HGJC.js → chunk-66RVSUAR.js} +1423 -1141
  7. package/dist/chunk-66RVSUAR.js.map +1 -0
  8. package/dist/chunk-BGKQHQB4.js +276 -0
  9. package/dist/chunk-BGKQHQB4.js.map +1 -0
  10. package/dist/chunk-C33LTDZV.js +97 -0
  11. package/dist/chunk-C33LTDZV.js.map +1 -0
  12. package/dist/{chunk-LYKCQTH5.js → chunk-ESOPMQAY.js} +5 -1
  13. package/dist/chunk-ESOPMQAY.js.map +1 -0
  14. package/dist/{chunk-6MJLVZXV.js → chunk-FKOARMAE.js} +58 -21
  15. package/dist/{chunk-6MJLVZXV.js.map → chunk-FKOARMAE.js.map} +1 -1
  16. package/dist/chunk-OORPX73T.js +30 -0
  17. package/dist/chunk-OORPX73T.js.map +1 -0
  18. package/dist/{chunk-V3BA2MJ6.js → chunk-RF3DUYFO.js} +2 -2
  19. package/dist/{chunk-UAUTLC4E.js → chunk-W7QQA6CW.js} +4 -4
  20. package/dist/{chunk-ZRFBLD3W.js → chunk-WYZFGHHI.js} +14 -4
  21. package/dist/chunk-WYZFGHHI.js.map +1 -0
  22. package/dist/{chunk-MRKYJ422.js → chunk-X6LLG7XN.js} +2 -2
  23. package/dist/{chunk-C6YIUTGR.js → chunk-YRJEZD7R.js} +2 -2
  24. package/dist/{chunk-HZD3CGPK.js → chunk-ZW444AQY.js} +3 -3
  25. package/dist/cli.js +141 -52
  26. package/dist/cli.js.map +1 -1
  27. package/dist/{config-H2DSEHNW.js → config-XURP6B3S.js} +3 -3
  28. package/dist/config-editor-AALY3URF.js +11 -0
  29. package/dist/config-registry-OGX4YM2U.js +17 -0
  30. package/dist/{daemon-VF6HJQXD.js → daemon-GWJM2S4A.js} +4 -4
  31. package/dist/doctor-X477CVZN.js +9 -0
  32. package/dist/doctor-X477CVZN.js.map +1 -0
  33. package/dist/index.d.ts +76 -27
  34. package/dist/index.js +33 -13
  35. package/dist/install-cloudflared-BTGUD7SW.js +8 -0
  36. package/dist/install-cloudflared-BTGUD7SW.js.map +1 -0
  37. package/dist/log-SPS2S6FO.js +19 -0
  38. package/dist/log-SPS2S6FO.js.map +1 -0
  39. package/dist/{main-G6XDM7EZ.js → main-2QKD2EI2.js} +17 -14
  40. package/dist/{main-G6XDM7EZ.js.map → main-2QKD2EI2.js.map} +1 -1
  41. package/dist/menu-CARRTW2F.js +17 -0
  42. package/dist/menu-CARRTW2F.js.map +1 -0
  43. package/dist/{setup-FCVL75K6.js → setup-TTOL7XAN.js} +4 -4
  44. package/dist/setup-TTOL7XAN.js.map +1 -0
  45. package/dist/{tunnel-service-DASSH7OA.js → tunnel-service-LEVPLXAZ.js} +3 -3
  46. package/package.json +1 -1
  47. package/dist/chunk-KPI4HGJC.js.map +0 -1
  48. package/dist/chunk-LYKCQTH5.js.map +0 -1
  49. package/dist/chunk-ZRFBLD3W.js.map +0 -1
  50. package/dist/config-editor-SKS4LJLT.js +0 -11
  51. package/dist/install-cloudflared-ILUXKLAC.js +0 -8
  52. /package/dist/{autostart-DZ3MHHMM.js.map → api-client-UN7BXQOQ.js.map} +0 -0
  53. /package/dist/{config-H2DSEHNW.js.map → autostart-K73RQZVV.js.map} +0 -0
  54. /package/dist/{chunk-V3BA2MJ6.js.map → chunk-RF3DUYFO.js.map} +0 -0
  55. /package/dist/{chunk-UAUTLC4E.js.map → chunk-W7QQA6CW.js.map} +0 -0
  56. /package/dist/{chunk-MRKYJ422.js.map → chunk-X6LLG7XN.js.map} +0 -0
  57. /package/dist/{chunk-C6YIUTGR.js.map → chunk-YRJEZD7R.js.map} +0 -0
  58. /package/dist/{chunk-HZD3CGPK.js.map → chunk-ZW444AQY.js.map} +0 -0
  59. /package/dist/{config-editor-SKS4LJLT.js.map → config-XURP6B3S.js.map} +0 -0
  60. /package/dist/{daemon-VF6HJQXD.js.map → config-editor-AALY3URF.js.map} +0 -0
  61. /package/dist/{install-cloudflared-ILUXKLAC.js.map → config-registry-OGX4YM2U.js.map} +0 -0
  62. /package/dist/{setup-FCVL75K6.js.map → daemon-GWJM2S4A.js.map} +0 -0
  63. /package/dist/{tunnel-service-DASSH7OA.js.map → tunnel-service-LEVPLXAZ.js.map} +0 -0
@@ -1,10 +1,33 @@
1
+ import {
2
+ DoctorEngine
3
+ } from "./chunk-3DIPXFZJ.js";
1
4
  import {
2
5
  getAgentCapabilities
3
6
  } from "./chunk-VA2M52CM.js";
7
+ import {
8
+ buildMenuKeyboard,
9
+ buildSkillMessages,
10
+ escapeHtml,
11
+ formatToolCall,
12
+ formatToolUpdate,
13
+ formatUsage,
14
+ handleAgents,
15
+ handleClear,
16
+ handleHelp,
17
+ handleMenu,
18
+ markdownToTelegramHtml,
19
+ splitMessage
20
+ } from "./chunk-BGKQHQB4.js";
21
+ import {
22
+ getConfigValue,
23
+ getSafeFields,
24
+ isHotReloadable,
25
+ resolveOptions
26
+ } from "./chunk-C33LTDZV.js";
4
27
  import {
5
28
  createChildLogger,
6
29
  createSessionLogger
7
- } from "./chunk-LYKCQTH5.js";
30
+ } from "./chunk-ESOPMQAY.js";
8
31
 
9
32
  // src/core/streams.ts
10
33
  function nodeToWebWritable(nodeStream) {
@@ -674,6 +697,13 @@ var PermissionGate = class {
674
697
  // src/core/session.ts
675
698
  import { nanoid } from "nanoid";
676
699
  var moduleLog = createChildLogger({ module: "session" });
700
+ var VALID_TRANSITIONS = {
701
+ initializing: /* @__PURE__ */ new Set(["active", "error"]),
702
+ active: /* @__PURE__ */ new Set(["error", "finished", "cancelled"]),
703
+ error: /* @__PURE__ */ new Set(["active"]),
704
+ cancelled: /* @__PURE__ */ new Set(["active"]),
705
+ finished: /* @__PURE__ */ new Set()
706
+ };
677
707
  var Session = class extends TypedEmitter {
678
708
  id;
679
709
  channelId;
@@ -682,11 +712,9 @@ var Session = class extends TypedEmitter {
682
712
  workingDirectory;
683
713
  agentInstance;
684
714
  agentSessionId = "";
685
- status = "initializing";
715
+ _status = "initializing";
686
716
  name;
687
717
  createdAt = /* @__PURE__ */ new Date();
688
- adapter;
689
- // Set by wireSessionEvents for renaming
690
718
  dangerousMode = false;
691
719
  log;
692
720
  permissionGate = new PermissionGate();
@@ -703,21 +731,44 @@ var Session = class extends TypedEmitter {
703
731
  this.queue = new PromptQueue(
704
732
  (text) => this.processPrompt(text),
705
733
  (err) => {
706
- this.status = "error";
734
+ this.fail("Prompt execution failed");
707
735
  this.log.error({ err }, "Prompt execution failed");
708
736
  }
709
737
  );
710
738
  }
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) {
739
+ // --- State Machine ---
740
+ get status() {
741
+ return this._status;
742
+ }
743
+ /** Transition to active — from initializing, error, or cancelled */
744
+ activate() {
745
+ this.transition("active");
746
+ }
747
+ /** Transition to error — from initializing or active */
748
+ fail(reason) {
749
+ this.transition("error");
750
+ this.emit("error", new Error(reason));
751
+ }
752
+ /** Transition to finished — from active only. Emits session_end for backward compat. */
753
+ finish(reason) {
754
+ this.transition("finished");
755
+ this.emit("session_end", reason ?? "completed");
756
+ }
757
+ /** Transition to cancelled — from active only (terminal session cancel) */
758
+ markCancelled() {
759
+ this.transition("cancelled");
760
+ }
761
+ transition(to) {
762
+ const from = this._status;
763
+ const allowed = VALID_TRANSITIONS[from];
764
+ if (!allowed?.has(to)) {
765
+ throw new Error(
766
+ `Invalid session transition: ${from} \u2192 ${to}`
767
+ );
768
+ }
769
+ this._status = to;
770
+ this.log.debug({ from, to }, "Session status transition");
771
+ this.emit("status_change", from, to);
721
772
  }
722
773
  /** Number of prompts waiting in queue */
723
774
  get queueDepth() {
@@ -735,7 +786,9 @@ var Session = class extends TypedEmitter {
735
786
  await this.runWarmup();
736
787
  return;
737
788
  }
738
- this.status = "active";
789
+ if (this._status === "initializing") {
790
+ this.activate();
791
+ }
739
792
  const promptStart = Date.now();
740
793
  this.log.debug("Prompt execution started");
741
794
  await this.agentInstance.prompt(text);
@@ -760,9 +813,7 @@ var Session = class extends TypedEmitter {
760
813
  );
761
814
  this.name = title.trim().slice(0, 50) || `Session ${this.id.slice(0, 6)}`;
762
815
  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
- }
816
+ this.emit("named", this.name);
766
817
  } catch {
767
818
  this.name = `Session ${this.id.slice(0, 6)}`;
768
819
  } finally {
@@ -781,7 +832,7 @@ var Session = class extends TypedEmitter {
781
832
  try {
782
833
  const start = Date.now();
783
834
  await this.agentInstance.prompt('Reply with only "ready".');
784
- this.status = "active";
835
+ this.activate();
785
836
  this.log.info({ durationMs: Date.now() - start }, "Warm-up complete");
786
837
  } catch (err) {
787
838
  this.log.error({ err }, "Warm-up failed");
@@ -790,11 +841,11 @@ var Session = class extends TypedEmitter {
790
841
  this.resume();
791
842
  }
792
843
  }
793
- async cancel() {
844
+ /** Cancel the current prompt and clear the queue. Stays in active state. */
845
+ async abortPrompt() {
794
846
  this.queue.clear();
795
- this.log.info("Session cancelled");
847
+ this.log.info("Prompt aborted");
796
848
  await this.agentInstance.cancel();
797
- this.status = "active";
798
849
  }
799
850
  async destroy() {
800
851
  this.log.info("Session destroyed");
@@ -867,42 +918,13 @@ var SessionManager = class {
867
918
  registerSession(session) {
868
919
  this.sessions.set(session.id, session);
869
920
  }
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) {
921
+ async patchRecord(sessionId, patch) {
902
922
  if (!this.store) return;
903
923
  const record = this.store.get(sessionId);
904
924
  if (record) {
905
- await this.store.save({ ...record, name });
925
+ await this.store.save({ ...record, ...patch });
926
+ } else if (patch.sessionId) {
927
+ await this.store.save(patch);
906
928
  }
907
929
  }
908
930
  getSessionRecord(sessionId) {
@@ -911,7 +933,8 @@ var SessionManager = class {
911
933
  async cancelSession(sessionId) {
912
934
  const session = this.sessions.get(sessionId);
913
935
  if (session) {
914
- await session.cancel();
936
+ await session.abortPrompt();
937
+ session.markCancelled();
915
938
  }
916
939
  if (this.store) {
917
940
  const record = this.store.get(sessionId);
@@ -953,6 +976,133 @@ var SessionManager = class {
953
976
  }
954
977
  };
955
978
 
979
+ // src/core/session-bridge.ts
980
+ var log2 = createChildLogger({ module: "session-bridge" });
981
+ var SessionBridge = class {
982
+ constructor(session, adapter, deps) {
983
+ this.session = session;
984
+ this.adapter = adapter;
985
+ this.deps = deps;
986
+ }
987
+ connected = false;
988
+ agentEventHandler;
989
+ statusChangeHandler;
990
+ namedHandler;
991
+ connect() {
992
+ if (this.connected) return;
993
+ this.connected = true;
994
+ this.wireAgentToSession();
995
+ this.wireSessionToAdapter();
996
+ this.wirePermissions();
997
+ this.wireLifecycle();
998
+ }
999
+ disconnect() {
1000
+ if (!this.connected) return;
1001
+ this.connected = false;
1002
+ if (this.agentEventHandler) {
1003
+ this.session.off("agent_event", this.agentEventHandler);
1004
+ }
1005
+ if (this.statusChangeHandler) {
1006
+ this.session.off("status_change", this.statusChangeHandler);
1007
+ }
1008
+ if (this.namedHandler) {
1009
+ this.session.off("named", this.namedHandler);
1010
+ }
1011
+ this.session.agentInstance.onSessionUpdate = () => {
1012
+ };
1013
+ this.session.agentInstance.onPermissionRequest = async () => "";
1014
+ }
1015
+ wireAgentToSession() {
1016
+ this.session.agentInstance.onSessionUpdate = (event) => {
1017
+ this.session.emit("agent_event", event);
1018
+ };
1019
+ }
1020
+ wireSessionToAdapter() {
1021
+ const session = this.session;
1022
+ const ctx = {
1023
+ get id() {
1024
+ return session.id;
1025
+ },
1026
+ get workingDirectory() {
1027
+ return session.workingDirectory;
1028
+ }
1029
+ };
1030
+ this.agentEventHandler = (event) => {
1031
+ switch (event.type) {
1032
+ case "text":
1033
+ case "thought":
1034
+ case "tool_call":
1035
+ case "tool_update":
1036
+ case "plan":
1037
+ case "usage":
1038
+ this.adapter.sendMessage(
1039
+ this.session.id,
1040
+ this.deps.messageTransformer.transform(event, ctx)
1041
+ );
1042
+ break;
1043
+ case "session_end":
1044
+ this.session.finish(event.reason);
1045
+ this.adapter.cleanupSkillCommands(this.session.id);
1046
+ this.adapter.sendMessage(
1047
+ this.session.id,
1048
+ this.deps.messageTransformer.transform(event)
1049
+ );
1050
+ this.deps.notificationManager.notify(this.session.channelId, {
1051
+ sessionId: this.session.id,
1052
+ sessionName: this.session.name,
1053
+ type: "completed",
1054
+ summary: `Session "${this.session.name || this.session.id}" completed`
1055
+ });
1056
+ break;
1057
+ case "error":
1058
+ this.session.fail(event.message);
1059
+ this.adapter.cleanupSkillCommands(this.session.id);
1060
+ this.adapter.sendMessage(
1061
+ this.session.id,
1062
+ this.deps.messageTransformer.transform(event)
1063
+ );
1064
+ this.deps.notificationManager.notify(this.session.channelId, {
1065
+ sessionId: this.session.id,
1066
+ sessionName: this.session.name,
1067
+ type: "error",
1068
+ summary: event.message
1069
+ });
1070
+ break;
1071
+ case "commands_update":
1072
+ log2.debug({ commands: event.commands }, "Commands available");
1073
+ this.adapter.sendSkillCommands(this.session.id, event.commands);
1074
+ break;
1075
+ }
1076
+ };
1077
+ this.session.on("agent_event", this.agentEventHandler);
1078
+ }
1079
+ wirePermissions() {
1080
+ this.session.agentInstance.onPermissionRequest = async (request) => {
1081
+ this.session.emit("permission_request", request);
1082
+ const promise = this.session.permissionGate.setPending(request);
1083
+ await this.adapter.sendPermissionRequest(this.session.id, request);
1084
+ return promise;
1085
+ };
1086
+ }
1087
+ wireLifecycle() {
1088
+ this.statusChangeHandler = (from, to) => {
1089
+ this.deps.sessionManager.patchRecord(this.session.id, {
1090
+ status: to,
1091
+ lastActiveAt: (/* @__PURE__ */ new Date()).toISOString()
1092
+ });
1093
+ if (to === "finished" || to === "cancelled") {
1094
+ queueMicrotask(() => this.disconnect());
1095
+ }
1096
+ };
1097
+ this.session.on("status_change", this.statusChangeHandler);
1098
+ this.namedHandler = (name) => {
1099
+ this.deps.sessionManager.patchRecord(this.session.id, { name });
1100
+ this.adapter.renameSessionThread(this.session.id, name);
1101
+ };
1102
+ this.session.on("named", this.namedHandler);
1103
+ }
1104
+ };
1105
+
956
1106
  // src/core/notification.ts
957
1107
  var NotificationManager = class {
958
1108
  constructor(adapters) {
@@ -1062,7 +1212,7 @@ function parseContent(content) {
1062
1212
  }
1063
1213
 
1064
1214
  // src/core/message-transformer.ts
1065
- var log2 = createChildLogger({ module: "message-transformer" });
1215
+ var log3 = createChildLogger({ module: "message-transformer" });
1066
1216
  var MessageTransformer = class {
1067
1217
  constructor(tunnelService) {
1068
1218
  this.tunnelService = tunnelService;
@@ -1124,7 +1274,7 @@ var MessageTransformer = class {
1124
1274
  if (!this.tunnelService || !sessionContext) return;
1125
1275
  const name = "name" in event ? event.name || "" : "";
1126
1276
  const kind = "kind" in event ? event.kind : void 0;
1127
- log2.debug(
1277
+ log3.debug(
1128
1278
  { name, kind, status: event.status, hasContent: !!event.content },
1129
1279
  "enrichWithViewerLinks: inspecting event"
1130
1280
  );
@@ -1136,7 +1286,7 @@ var MessageTransformer = class {
1136
1286
  event.meta
1137
1287
  );
1138
1288
  if (!fileInfo) return;
1139
- log2.info(
1289
+ log3.info(
1140
1290
  {
1141
1291
  name,
1142
1292
  kind,
@@ -1178,7 +1328,7 @@ import os from "os";
1178
1328
  // src/core/session-store.ts
1179
1329
  import fs2 from "fs";
1180
1330
  import path2 from "path";
1181
- var log3 = createChildLogger({ module: "session-store" });
1331
+ var log4 = createChildLogger({ module: "session-store" });
1182
1332
  var DEBOUNCE_MS = 2e3;
1183
1333
  var JsonFileSessionStore = class {
1184
1334
  records = /* @__PURE__ */ new Map();
@@ -1260,7 +1410,7 @@ var JsonFileSessionStore = class {
1260
1410
  fs2.readFileSync(this.filePath, "utf-8")
1261
1411
  );
1262
1412
  if (raw.version !== 1) {
1263
- log3.warn(
1413
+ log4.warn(
1264
1414
  { version: raw.version },
1265
1415
  "Unknown session store version, skipping load"
1266
1416
  );
@@ -1269,9 +1419,9 @@ var JsonFileSessionStore = class {
1269
1419
  for (const [id, record] of Object.entries(raw.sessions)) {
1270
1420
  this.records.set(id, record);
1271
1421
  }
1272
- log3.info({ count: this.records.size }, "Loaded session records");
1422
+ log4.info({ count: this.records.size }, "Loaded session records");
1273
1423
  } catch (err) {
1274
- log3.error({ err }, "Failed to load session store");
1424
+ log4.error({ err }, "Failed to load session store");
1275
1425
  }
1276
1426
  }
1277
1427
  cleanup() {
@@ -1287,7 +1437,7 @@ var JsonFileSessionStore = class {
1287
1437
  }
1288
1438
  }
1289
1439
  if (removed > 0) {
1290
- log3.info({ removed }, "Cleaned up expired session records");
1440
+ log4.info({ removed }, "Cleaned up expired session records");
1291
1441
  this.scheduleDiskWrite();
1292
1442
  }
1293
1443
  }
@@ -1300,7 +1450,7 @@ var JsonFileSessionStore = class {
1300
1450
  };
1301
1451
 
1302
1452
  // src/core/core.ts
1303
- var log4 = createChildLogger({ module: "core" });
1453
+ var log5 = createChildLogger({ module: "core" });
1304
1454
  var OpenACPCore = class {
1305
1455
  configManager;
1306
1456
  agentManager;
@@ -1325,6 +1475,13 @@ var OpenACPCore = class {
1325
1475
  this.sessionManager = new SessionManager(this.sessionStore);
1326
1476
  this.notificationManager = new NotificationManager(this.adapters);
1327
1477
  this.messageTransformer = new MessageTransformer();
1478
+ this.configManager.on("config:changed", async ({ path: configPath, value }) => {
1479
+ if (configPath === "logging.level" && typeof value === "string") {
1480
+ const { setLogLevel: setLogLevel2 } = await import("./log-SPS2S6FO.js");
1481
+ setLogLevel2(value);
1482
+ log5.info({ level: value }, "Log level changed at runtime");
1483
+ }
1484
+ });
1328
1485
  }
1329
1486
  get tunnelService() {
1330
1487
  return this._tunnelService;
@@ -1358,7 +1515,7 @@ var OpenACPCore = class {
1358
1515
  // --- Message Routing ---
1359
1516
  async handleMessage(message) {
1360
1517
  const config = this.configManager.get();
1361
- log4.debug(
1518
+ log5.debug(
1362
1519
  {
1363
1520
  channelId: message.channelId,
1364
1521
  threadId: message.threadId,
@@ -1368,7 +1525,7 @@ var OpenACPCore = class {
1368
1525
  );
1369
1526
  if (config.security.allowedUserIds.length > 0) {
1370
1527
  if (!config.security.allowedUserIds.includes(message.userId)) {
1371
- log4.warn(
1528
+ log5.warn(
1372
1529
  { userId: message.userId },
1373
1530
  "Rejected message from unauthorized user"
1374
1531
  );
@@ -1377,7 +1534,7 @@ var OpenACPCore = class {
1377
1534
  }
1378
1535
  const activeSessions = this.sessionManager.listSessions().filter((s) => s.status === "active" || s.status === "initializing");
1379
1536
  if (activeSessions.length >= config.security.maxConcurrentSessions) {
1380
- log4.warn(
1537
+ log5.warn(
1381
1538
  {
1382
1539
  userId: message.userId,
1383
1540
  currentCount: activeSessions.length,
@@ -1401,28 +1558,87 @@ var OpenACPCore = class {
1401
1558
  if (!session) {
1402
1559
  session = await this.lazyResume(message) ?? void 0;
1403
1560
  }
1404
- if (!session) return;
1405
- this.sessionManager.updateSessionActivity(session.id);
1561
+ if (!session) {
1562
+ log5.warn(
1563
+ { channelId: message.channelId, threadId: message.threadId },
1564
+ "No session found for thread (in-memory miss + lazy resume returned null)"
1565
+ );
1566
+ return;
1567
+ }
1568
+ this.sessionManager.patchRecord(session.id, { lastActiveAt: (/* @__PURE__ */ new Date()).toISOString() });
1406
1569
  await session.enqueuePrompt(message.text);
1407
1570
  }
1571
+ // --- Unified Session Creation Pipeline ---
1572
+ async createSession(params) {
1573
+ const agentInstance = params.resumeAgentSessionId ? await this.agentManager.resume(
1574
+ params.agentName,
1575
+ params.workingDirectory,
1576
+ params.resumeAgentSessionId
1577
+ ) : await this.agentManager.spawn(
1578
+ params.agentName,
1579
+ params.workingDirectory
1580
+ );
1581
+ const session = new Session({
1582
+ id: params.existingSessionId,
1583
+ channelId: params.channelId,
1584
+ agentName: params.agentName,
1585
+ workingDirectory: params.workingDirectory,
1586
+ agentInstance
1587
+ });
1588
+ session.agentSessionId = agentInstance.sessionId;
1589
+ if (params.initialName) {
1590
+ session.name = params.initialName;
1591
+ }
1592
+ this.sessionManager.registerSession(session);
1593
+ const adapter = this.adapters.get(params.channelId);
1594
+ if (params.createThread && adapter) {
1595
+ const threadId = await adapter.createSessionThread(
1596
+ session.id,
1597
+ params.initialName ?? `\u{1F504} ${params.agentName} \u2014 New Session`
1598
+ );
1599
+ session.threadId = threadId;
1600
+ }
1601
+ if (adapter) {
1602
+ const bridge = this.createBridge(session, adapter);
1603
+ bridge.connect();
1604
+ }
1605
+ const existingRecord = this.sessionStore?.get(session.id);
1606
+ const platform = {
1607
+ ...existingRecord?.platform ?? {}
1608
+ };
1609
+ if (session.threadId) {
1610
+ platform.topicId = Number(session.threadId);
1611
+ }
1612
+ await this.sessionManager.patchRecord(session.id, {
1613
+ sessionId: session.id,
1614
+ agentSessionId: agentInstance.sessionId,
1615
+ agentName: params.agentName,
1616
+ workingDir: params.workingDirectory,
1617
+ channelId: params.channelId,
1618
+ status: session.status,
1619
+ createdAt: session.createdAt.toISOString(),
1620
+ lastActiveAt: (/* @__PURE__ */ new Date()).toISOString(),
1621
+ name: session.name,
1622
+ platform
1623
+ });
1624
+ log5.info(
1625
+ { sessionId: session.id, agentName: params.agentName },
1626
+ "Session created via pipeline"
1627
+ );
1628
+ return session;
1629
+ }
1408
1630
  async handleNewSession(channelId, agentName, workspacePath) {
1409
1631
  const config = this.configManager.get();
1410
1632
  const resolvedAgent = agentName || config.defaultAgent;
1411
- log4.info({ channelId, agentName: resolvedAgent }, "New session request");
1633
+ log5.info({ channelId, agentName: resolvedAgent }, "New session request");
1412
1634
  const resolvedWorkspace = this.configManager.resolveWorkspace(
1413
1635
  workspacePath || config.agents[resolvedAgent]?.workingDirectory
1414
1636
  );
1415
- const session = await this.sessionManager.createSession(
1637
+ return this.createSession({
1416
1638
  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;
1639
+ agentName: resolvedAgent,
1640
+ workingDirectory: resolvedWorkspace
1641
+ });
1426
1642
  }
1427
1643
  async adoptSession(agentName, agentSessionId, cwd) {
1428
1644
  const caps = getAgentCapabilities(agentName);
@@ -1445,10 +1661,10 @@ var OpenACPCore = class {
1445
1661
  if (existingRecord) {
1446
1662
  const platform = existingRecord.platform;
1447
1663
  if (platform?.topicId) {
1448
- const adapter2 = this.adapters.values().next().value;
1449
- if (adapter2) {
1664
+ const adapter = this.adapters.values().next().value;
1665
+ if (adapter) {
1450
1666
  try {
1451
- await adapter2.sendMessage(existingRecord.sessionId, {
1667
+ await adapter.sendMessage(existingRecord.sessionId, {
1452
1668
  type: "text",
1453
1669
  text: "Session resumed from CLI."
1454
1670
  });
@@ -1463,9 +1679,21 @@ var OpenACPCore = class {
1463
1679
  };
1464
1680
  }
1465
1681
  }
1466
- let agentInstance;
1682
+ const firstEntry = this.adapters.entries().next().value;
1683
+ if (!firstEntry) {
1684
+ return { ok: false, error: "no_adapter", message: "No channel adapter registered" };
1685
+ }
1686
+ const [adapterChannelId] = firstEntry;
1687
+ let session;
1467
1688
  try {
1468
- agentInstance = await this.agentManager.resume(agentName, cwd, agentSessionId);
1689
+ session = await this.createSession({
1690
+ channelId: adapterChannelId,
1691
+ agentName,
1692
+ workingDirectory: cwd,
1693
+ resumeAgentSessionId: agentSessionId,
1694
+ createThread: true,
1695
+ initialName: "Adopted session"
1696
+ });
1469
1697
  } catch (err) {
1470
1698
  return {
1471
1699
  ok: false,
@@ -1473,43 +1701,14 @@ var OpenACPCore = class {
1473
1701
  message: `Failed to resume session: ${err instanceof Error ? err.message : String(err)}`
1474
1702
  };
1475
1703
  }
1476
- const session = new Session({
1477
- channelId: "api",
1478
- agentName,
1479
- workingDirectory: cwd,
1480
- agentInstance
1704
+ await this.sessionManager.patchRecord(session.id, {
1705
+ originalAgentSessionId: agentSessionId,
1706
+ platform: { topicId: Number(session.threadId) }
1481
1707
  });
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
1708
  return {
1510
1709
  ok: true,
1511
1710
  sessionId: session.id,
1512
- threadId,
1711
+ threadId: session.threadId,
1513
1712
  status: "adopted"
1514
1713
  };
1515
1714
  }
@@ -1544,45 +1743,54 @@ var OpenACPCore = class {
1544
1743
  message.channelId,
1545
1744
  (p) => String(p.topicId) === message.threadId
1546
1745
  );
1547
- if (!record) return null;
1548
- if (record.status === "cancelled" || record.status === "error") return null;
1746
+ if (!record) {
1747
+ log5.debug(
1748
+ { threadId: message.threadId, channelId: message.channelId },
1749
+ "No session record found for thread"
1750
+ );
1751
+ return null;
1752
+ }
1753
+ if (record.status === "cancelled" || record.status === "error") {
1754
+ log5.debug(
1755
+ { threadId: message.threadId, sessionId: record.sessionId, status: record.status },
1756
+ "Skipping resume of cancelled/error session"
1757
+ );
1758
+ return null;
1759
+ }
1760
+ log5.info(
1761
+ { threadId: message.threadId, sessionId: record.sessionId, status: record.status },
1762
+ "Lazy resume: found record, attempting resume"
1763
+ );
1549
1764
  const resumePromise = (async () => {
1550
1765
  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,
1766
+ const session = await this.createSession({
1558
1767
  channelId: record.channelId,
1559
1768
  agentName: record.agentName,
1560
1769
  workingDirectory: record.workingDir,
1561
- agentInstance
1770
+ resumeAgentSessionId: record.agentSessionId,
1771
+ existingSessionId: record.sessionId,
1772
+ initialName: record.name
1562
1773
  });
1563
1774
  session.threadId = message.threadId;
1564
- session.agentSessionId = agentInstance.sessionId;
1565
- session.status = "active";
1566
- session.name = record.name;
1775
+ session.activate();
1567
1776
  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(
1777
+ log5.info(
1580
1778
  { sessionId: session.id, threadId: message.threadId },
1581
1779
  "Lazy resume successful"
1582
1780
  );
1583
1781
  return session;
1584
1782
  } catch (err) {
1585
- log4.error({ err, record }, "Lazy resume failed");
1783
+ log5.error({ err, record }, "Lazy resume failed");
1784
+ const adapter = this.adapters.get(message.channelId);
1785
+ if (adapter) {
1786
+ try {
1787
+ await adapter.sendMessage(message.threadId, {
1788
+ type: "error",
1789
+ text: `\u26A0\uFE0F Failed to resume session: ${err instanceof Error ? err.message : String(err)}`
1790
+ });
1791
+ } catch {
1792
+ }
1793
+ }
1586
1794
  return null;
1587
1795
  } finally {
1588
1796
  this.resumeLocks.delete(lockKey);
@@ -1592,73 +1800,12 @@ var OpenACPCore = class {
1592
1800
  return resumePromise;
1593
1801
  }
1594
1802
  // --- 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
- }
1803
+ /** Create a SessionBridge for the given session and adapter */
1804
+ createBridge(session, adapter) {
1805
+ return new SessionBridge(session, adapter, {
1806
+ messageTransformer: this.messageTransformer,
1807
+ notificationManager: this.notificationManager,
1808
+ sessionManager: this.sessionManager
1662
1809
  });
1663
1810
  }
1664
1811
  };
@@ -1684,7 +1831,7 @@ import * as fs3 from "fs";
1684
1831
  import * as path4 from "path";
1685
1832
  import * as os2 from "os";
1686
1833
  import { fileURLToPath } from "url";
1687
- var log5 = createChildLogger({ module: "api-server" });
1834
+ var log6 = createChildLogger({ module: "api-server" });
1688
1835
  var DEFAULT_PORT_FILE = path4.join(os2.homedir(), ".openacp", "api.port");
1689
1836
  var cachedVersion;
1690
1837
  function getVersion() {
@@ -1730,7 +1877,7 @@ var ApiServer = class {
1730
1877
  await new Promise((resolve2, reject) => {
1731
1878
  this.server.on("error", (err) => {
1732
1879
  if (err.code === "EADDRINUSE") {
1733
- log5.warn({ port: this.config.port }, "API port in use, continuing without API server");
1880
+ log6.warn({ port: this.config.port }, "API port in use, continuing without API server");
1734
1881
  this.server = null;
1735
1882
  resolve2();
1736
1883
  } else {
@@ -1743,7 +1890,7 @@ var ApiServer = class {
1743
1890
  this.actualPort = addr.port;
1744
1891
  }
1745
1892
  this.writePortFile();
1746
- log5.info({ host: this.config.host, port: this.actualPort }, "API server listening");
1893
+ log6.info({ host: this.config.host, port: this.actualPort }, "API server listening");
1747
1894
  resolve2();
1748
1895
  });
1749
1896
  });
@@ -1799,6 +1946,8 @@ var ApiServer = class {
1799
1946
  await this.handleHealth(res);
1800
1947
  } else if (method === "GET" && url === "/api/version") {
1801
1948
  await this.handleVersion(res);
1949
+ } else if (method === "GET" && url === "/api/config/editable") {
1950
+ await this.handleGetEditableConfig(res);
1802
1951
  } else if (method === "GET" && url === "/api/config") {
1803
1952
  await this.handleGetConfig(res);
1804
1953
  } else if (method === "PATCH" && url === "/api/config") {
@@ -1822,7 +1971,7 @@ var ApiServer = class {
1822
1971
  this.sendJson(res, 404, { error: "Not found" });
1823
1972
  }
1824
1973
  } catch (err) {
1825
- log5.error({ err }, "API request error");
1974
+ log6.error({ err }, "API request error");
1826
1975
  this.sendJson(res, 500, { error: "Internal server error" });
1827
1976
  }
1828
1977
  }
@@ -1850,24 +1999,25 @@ var ApiServer = class {
1850
1999
  }
1851
2000
  const [adapterId, adapter] = this.core.adapters.entries().next().value ?? [null, null];
1852
2001
  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
- }
2002
+ const resolvedAgent = agent || config.defaultAgent;
2003
+ const resolvedWorkspace = this.core.configManager.resolveWorkspace(
2004
+ workspace || config.agents[resolvedAgent]?.workingDirectory
2005
+ );
2006
+ const session = await this.core.createSession({
2007
+ channelId,
2008
+ agentName: resolvedAgent,
2009
+ workingDirectory: resolvedWorkspace,
2010
+ createThread: !!adapter,
2011
+ initialName: `\u{1F504} ${resolvedAgent} \u2014 New Session`
2012
+ });
1863
2013
  if (!adapter) {
1864
2014
  session.agentInstance.onPermissionRequest = async (request) => {
1865
2015
  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");
2016
+ log6.debug({ sessionId: session.id, permissionId: request.id, option: allowOption?.id }, "Auto-approving permission for API session");
1867
2017
  return allowOption?.id ?? request.options[0]?.id ?? "";
1868
2018
  };
1869
2019
  }
1870
- session.warmup().catch((err) => log5.warn({ err, sessionId: session.id }, "API session warmup failed"));
2020
+ session.warmup().catch((err) => log6.warn({ err, sessionId: session.id }, "API session warmup failed"));
1871
2021
  this.sendJson(res, 200, {
1872
2022
  sessionId: session.id,
1873
2023
  agent: session.agentName,
@@ -1949,7 +2099,7 @@ var ApiServer = class {
1949
2099
  return;
1950
2100
  }
1951
2101
  session.dangerousMode = enabled;
1952
- await this.core.sessionManager.updateSessionDangerousMode(sessionId, enabled);
2102
+ await this.core.sessionManager.patchRecord(sessionId, { dangerousMode: enabled });
1953
2103
  this.sendJson(res, 200, { ok: true, dangerousMode: enabled });
1954
2104
  }
1955
2105
  async handleHealth(res) {
@@ -1977,6 +2127,21 @@ var ApiServer = class {
1977
2127
  async handleVersion(res) {
1978
2128
  this.sendJson(res, 200, { version: getVersion() });
1979
2129
  }
2130
+ async handleGetEditableConfig(res) {
2131
+ const { getSafeFields: getSafeFields2, resolveOptions: resolveOptions2, getConfigValue: getConfigValue2 } = await import("./config-registry-OGX4YM2U.js");
2132
+ const config = this.core.configManager.get();
2133
+ const safeFields = getSafeFields2();
2134
+ const fields = safeFields.map((def) => ({
2135
+ path: def.path,
2136
+ displayName: def.displayName,
2137
+ group: def.group,
2138
+ type: def.type,
2139
+ options: resolveOptions2(def, config),
2140
+ value: getConfigValue2(config, def.path),
2141
+ hotReload: def.hotReload
2142
+ }));
2143
+ this.sendJson(res, 200, { fields });
2144
+ }
1980
2145
  async handleGetConfig(res) {
1981
2146
  const config = this.core.configManager.get();
1982
2147
  this.sendJson(res, 200, { config: redactConfig(config) });
@@ -2017,7 +2182,7 @@ var ApiServer = class {
2017
2182
  return;
2018
2183
  }
2019
2184
  target[lastKey] = value;
2020
- const { ConfigSchema } = await import("./config-H2DSEHNW.js");
2185
+ const { ConfigSchema } = await import("./config-XURP6B3S.js");
2021
2186
  const result = ConfigSchema.safeParse(cloned);
2022
2187
  if (!result.success) {
2023
2188
  this.sendJson(res, 400, {
@@ -2033,12 +2198,9 @@ var ApiServer = class {
2033
2198
  updateTarget = updateTarget[parts[i]];
2034
2199
  }
2035
2200
  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
- );
2201
+ await this.core.configManager.save(updates, configPath);
2202
+ const { isHotReloadable: isHotReloadable2 } = await import("./config-registry-OGX4YM2U.js");
2203
+ const needsRestart = !isHotReloadable2(configPath);
2042
2204
  this.sendJson(res, 200, {
2043
2205
  ok: true,
2044
2206
  needsRestart,
@@ -2097,7 +2259,7 @@ var ApiServer = class {
2097
2259
  this.sendJson(res, 404, { error: `Session "${sessionId}" not found` });
2098
2260
  return;
2099
2261
  }
2100
- await session.cancel();
2262
+ await session.abortPrompt();
2101
2263
  this.sendJson(res, 200, { ok: true });
2102
2264
  }
2103
2265
  async handleListSessions(res) {
@@ -2206,7 +2368,7 @@ var ApiServer = class {
2206
2368
  };
2207
2369
 
2208
2370
  // src/core/topic-manager.ts
2209
- var log6 = createChildLogger({ module: "topic-manager" });
2371
+ var log7 = createChildLogger({ module: "topic-manager" });
2210
2372
  var TopicManager = class {
2211
2373
  constructor(sessionManager, adapter, systemTopicIds) {
2212
2374
  this.sessionManager = sessionManager;
@@ -2245,7 +2407,7 @@ var TopicManager = class {
2245
2407
  try {
2246
2408
  await this.adapter.deleteSessionThread(sessionId);
2247
2409
  } catch (err) {
2248
- log6.warn({ err, sessionId, topicId }, "Failed to delete platform thread, removing record anyway");
2410
+ log7.warn({ err, sessionId, topicId }, "Failed to delete platform thread, removing record anyway");
2249
2411
  }
2250
2412
  }
2251
2413
  await this.sessionManager.removeRecord(sessionId);
@@ -2268,7 +2430,7 @@ var TopicManager = class {
2268
2430
  try {
2269
2431
  await this.adapter.deleteSessionThread(record.sessionId);
2270
2432
  } catch (err) {
2271
- log6.warn({ err, sessionId: record.sessionId }, "Failed to delete platform thread during cleanup");
2433
+ log7.warn({ err, sessionId: record.sessionId }, "Failed to delete platform thread during cleanup");
2272
2434
  }
2273
2435
  }
2274
2436
  await this.sessionManager.removeRecord(record.sessionId);
@@ -2289,379 +2451,35 @@ var TopicManager = class {
2289
2451
  // src/adapters/telegram/adapter.ts
2290
2452
  import { Bot } from "grammy";
2291
2453
 
2292
- // src/adapters/telegram/formatting.ts
2293
- function escapeHtml(text) {
2294
- if (!text) return "";
2295
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2296
- }
2297
- function markdownToTelegramHtml(md) {
2298
- const codeBlocks = [];
2299
- const inlineCodes = [];
2300
- let text = md.replace(/```(\w*)\n?([\s\S]*?)```/g, (_match, lang, code) => {
2301
- const index = codeBlocks.length;
2302
- const escapedCode = escapeHtml(code);
2303
- const langAttr = lang ? ` class="language-${escapeHtml(lang)}"` : "";
2304
- codeBlocks.push(`<pre><code${langAttr}>${escapedCode}</code></pre>`);
2305
- return `\0CODE_BLOCK_${index}\0`;
2306
- });
2307
- text = text.replace(/`([^`]+)`/g, (_match, code) => {
2308
- const index = inlineCodes.length;
2309
- inlineCodes.push(`<code>${escapeHtml(code)}</code>`);
2310
- return `\0INLINE_CODE_${index}\0`;
2311
- });
2312
- text = escapeHtml(text);
2313
- text = text.replace(/\*\*(.+?)\*\*/g, "<b>$1</b>");
2314
- text = text.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, "<i>$1</i>");
2315
- text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
2316
- text = text.replace(/\x00CODE_BLOCK_(\d+)\x00/g, (_match, idx) => {
2317
- return codeBlocks[parseInt(idx, 10)];
2318
- });
2319
- text = text.replace(/\x00INLINE_CODE_(\d+)\x00/g, (_match, idx) => {
2320
- return inlineCodes[parseInt(idx, 10)];
2321
- });
2322
- return text;
2323
- }
2324
- var STATUS_ICON = {
2325
- pending: "\u23F3",
2326
- in_progress: "\u{1F504}",
2327
- completed: "\u2705",
2328
- failed: "\u274C"
2329
- };
2330
- var KIND_ICON = {
2331
- read: "\u{1F4D6}",
2332
- edit: "\u270F\uFE0F",
2333
- delete: "\u{1F5D1}\uFE0F",
2334
- execute: "\u25B6\uFE0F",
2335
- search: "\u{1F50D}",
2336
- fetch: "\u{1F310}",
2337
- think: "\u{1F9E0}",
2338
- move: "\u{1F4E6}",
2339
- other: "\u{1F6E0}\uFE0F"
2340
- };
2341
- function extractContentText(content, depth = 0) {
2342
- if (!content || depth > 5) return "";
2343
- if (typeof content === "string") return content;
2344
- if (Array.isArray(content)) {
2345
- return content.map((c) => extractContentText(c, depth + 1)).filter(Boolean).join("\n");
2346
- }
2347
- if (typeof content === "object" && content !== null) {
2348
- const c = content;
2349
- if (c.type === "text" && typeof c.text === "string") return c.text;
2350
- if (typeof c.text === "string") return c.text;
2351
- if (typeof c.content === "string") return c.content;
2352
- if (c.content && typeof c.content === "object") return extractContentText(c.content, depth + 1);
2353
- if (c.input) return extractContentText(c.input, depth + 1);
2354
- if (c.output) return extractContentText(c.output, depth + 1);
2355
- const keys = Object.keys(c).filter((k) => k !== "type");
2356
- if (keys.length === 0) return "";
2357
- return JSON.stringify(c, null, 2);
2358
- }
2359
- return String(content);
2360
- }
2361
- function truncateContent(text, maxLen = 3800) {
2362
- if (text.length <= maxLen) return text;
2363
- return text.slice(0, maxLen) + "\n\u2026 (truncated)";
2364
- }
2365
- function formatToolCall(tool) {
2366
- const si = STATUS_ICON[tool.status || ""] || "\u{1F527}";
2367
- const ki = KIND_ICON[tool.kind || ""] || "\u{1F6E0}\uFE0F";
2368
- let text = `${si} ${ki} <b>${escapeHtml(tool.name || "Tool")}</b>`;
2369
- text += formatViewerLinks(tool.viewerLinks, tool.viewerFilePath);
2370
- if (!tool.viewerLinks) {
2371
- const details = extractContentText(tool.content);
2372
- if (details) {
2373
- text += `
2374
- <pre>${escapeHtml(truncateContent(details))}</pre>`;
2375
- }
2454
+ // src/adapters/telegram/topics.ts
2455
+ async function ensureTopics(bot, chatId, config, saveConfig) {
2456
+ let notificationTopicId = config.notificationTopicId;
2457
+ let assistantTopicId = config.assistantTopicId;
2458
+ if (notificationTopicId === null) {
2459
+ const topic = await bot.api.createForumTopic(chatId, "\u{1F4CB} Notifications");
2460
+ notificationTopicId = topic.message_thread_id;
2461
+ await saveConfig({ notificationTopicId });
2376
2462
  }
2377
- return text;
2378
- }
2379
- function formatToolUpdate(update) {
2380
- const si = STATUS_ICON[update.status] || "\u{1F527}";
2381
- const ki = KIND_ICON[update.kind || ""] || "\u{1F6E0}\uFE0F";
2382
- const name = update.name || "Tool";
2383
- let text = `${si} ${ki} <b>${escapeHtml(name)}</b>`;
2384
- text += formatViewerLinks(update.viewerLinks, update.viewerFilePath);
2385
- if (!update.viewerLinks) {
2386
- const details = extractContentText(update.content);
2387
- if (details) {
2388
- text += `
2389
- <pre>${escapeHtml(truncateContent(details))}</pre>`;
2390
- }
2463
+ if (assistantTopicId === null) {
2464
+ const topic = await bot.api.createForumTopic(chatId, "\u{1F916} Assistant");
2465
+ assistantTopicId = topic.message_thread_id;
2466
+ await saveConfig({ assistantTopicId });
2391
2467
  }
2392
- return text;
2393
- }
2394
- function formatViewerLinks(links, filePath) {
2395
- if (!links) return "";
2396
- const fileName = filePath ? filePath.split("/").pop() || filePath : "";
2397
- let text = "\n";
2398
- if (links.file) text += `
2399
- \u{1F4C4} <a href="${escapeHtml(links.file)}">View ${escapeHtml(fileName || "file")}</a>`;
2400
- if (links.diff) text += `
2401
- \u{1F4DD} <a href="${escapeHtml(links.diff)}">View diff${fileName ? ` \u2014 ${escapeHtml(fileName)}` : ""}</a>`;
2402
- return text;
2403
- }
2404
- function formatTokens(n) {
2405
- return n >= 1e3 ? `${Math.round(n / 1e3)}k` : String(n);
2406
- }
2407
- function progressBar(ratio) {
2408
- const filled = Math.round(Math.min(ratio, 1) * 10);
2409
- return "\u2593".repeat(filled) + "\u2591".repeat(10 - filled);
2468
+ return { notificationTopicId, assistantTopicId };
2410
2469
  }
2411
- function formatUsage(usage) {
2412
- const { tokensUsed, contextSize } = usage;
2413
- if (tokensUsed == null) return "\u{1F4CA} Usage data unavailable";
2414
- if (contextSize == null) return `\u{1F4CA} ${formatTokens(tokensUsed)} tokens`;
2415
- const ratio = tokensUsed / contextSize;
2416
- const pct = Math.round(ratio * 100);
2417
- const bar = progressBar(ratio);
2418
- const emoji = pct >= 85 ? "\u26A0\uFE0F" : "\u{1F4CA}";
2419
- return `${emoji} ${formatTokens(tokensUsed)} / ${formatTokens(contextSize)} tokens
2420
- ${bar} ${pct}%`;
2470
+ async function createSessionTopic(bot, chatId, name) {
2471
+ const topic = await bot.api.createForumTopic(chatId, name);
2472
+ return topic.message_thread_id;
2421
2473
  }
2422
- function splitMessage(text, maxLength = 3800) {
2423
- if (text.length <= maxLength) return [text];
2424
- const chunks = [];
2425
- let remaining = text;
2426
- while (remaining.length > 0) {
2427
- if (remaining.length <= maxLength) {
2428
- chunks.push(remaining);
2429
- break;
2430
- }
2431
- const wouldLeaveSmall = remaining.length < maxLength * 1.3;
2432
- const searchLimit = wouldLeaveSmall ? Math.floor(remaining.length / 2) + 300 : maxLength;
2433
- let splitAt = remaining.lastIndexOf("\n\n", searchLimit);
2434
- if (splitAt === -1 || splitAt < searchLimit * 0.2) {
2435
- splitAt = remaining.lastIndexOf("\n", searchLimit);
2436
- }
2437
- if (splitAt === -1 || splitAt < searchLimit * 0.2) {
2438
- splitAt = searchLimit;
2439
- }
2440
- const candidate = remaining.slice(0, splitAt);
2441
- const fences = candidate.match(/```/g);
2442
- if (fences && fences.length % 2 !== 0) {
2443
- const closingFence = remaining.indexOf("```", splitAt);
2444
- if (closingFence !== -1) {
2445
- const afterFence = remaining.indexOf("\n", closingFence + 3);
2446
- splitAt = afterFence !== -1 ? afterFence + 1 : closingFence + 3;
2447
- }
2448
- }
2449
- chunks.push(remaining.slice(0, splitAt));
2450
- remaining = remaining.slice(splitAt).replace(/^\n+/, "");
2474
+ async function renameSessionTopic(bot, chatId, threadId, name) {
2475
+ try {
2476
+ await bot.api.editForumTopic(chatId, threadId, { name });
2477
+ } catch {
2451
2478
  }
2452
- return chunks;
2453
- }
2454
-
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}`;
2479
+ }
2480
+ function buildDeepLink(chatId, messageId) {
2481
+ const cleanId = String(chatId).replace("-100", "");
2482
+ return `https://t.me/c/${cleanId}/${messageId}`;
2665
2483
  }
2666
2484
 
2667
2485
  // src/adapters/telegram/commands/new-session.ts
@@ -2669,7 +2487,7 @@ import { InlineKeyboard as InlineKeyboard2 } from "grammy";
2669
2487
 
2670
2488
  // src/adapters/telegram/commands/admin.ts
2671
2489
  import { InlineKeyboard } from "grammy";
2672
- var log8 = createChildLogger({ module: "telegram-cmd-admin" });
2490
+ var log9 = createChildLogger({ module: "telegram-cmd-admin" });
2673
2491
  function buildDangerousModeKeyboard(sessionId, enabled) {
2674
2492
  return new InlineKeyboard().text(
2675
2493
  enabled ? "\u{1F510} Disable Dangerous Mode" : "\u2620\uFE0F Enable Dangerous Mode",
@@ -2682,8 +2500,8 @@ function setupDangerousModeCallbacks(bot, core) {
2682
2500
  const session = core.sessionManager.getSession(sessionId);
2683
2501
  if (session) {
2684
2502
  session.dangerousMode = !session.dangerousMode;
2685
- log8.info({ sessionId, dangerousMode: session.dangerousMode }, "Dangerous mode toggled via button");
2686
- core.sessionManager.updateSessionDangerousMode(sessionId, session.dangerousMode).catch(() => {
2503
+ log9.info({ sessionId, dangerousMode: session.dangerousMode }, "Dangerous mode toggled via button");
2504
+ core.sessionManager.patchRecord(sessionId, { dangerousMode: session.dangerousMode }).catch(() => {
2687
2505
  });
2688
2506
  const toastText2 = session.dangerousMode ? "\u2620\uFE0F Dangerous mode enabled \u2014 permissions auto-approved" : "\u{1F510} Dangerous mode disabled \u2014 permissions shown normally";
2689
2507
  try {
@@ -2707,9 +2525,9 @@ function setupDangerousModeCallbacks(bot, core) {
2707
2525
  return;
2708
2526
  }
2709
2527
  const newDangerousMode = !(record.dangerousMode ?? false);
2710
- core.sessionManager.updateSessionDangerousMode(sessionId, newDangerousMode).catch(() => {
2528
+ core.sessionManager.patchRecord(sessionId, { dangerousMode: newDangerousMode }).catch(() => {
2711
2529
  });
2712
- log8.info({ sessionId, dangerousMode: newDangerousMode }, "Dangerous mode toggled via button (store-only, session not in memory)");
2530
+ log9.info({ sessionId, dangerousMode: newDangerousMode }, "Dangerous mode toggled via button (store-only, session not in memory)");
2713
2531
  const toastText = newDangerousMode ? "\u2620\uFE0F Dangerous mode enabled \u2014 permissions auto-approved" : "\u{1F510} Dangerous mode disabled \u2014 permissions shown normally";
2714
2532
  try {
2715
2533
  await ctx.answerCallbackQuery({ text: toastText });
@@ -2736,7 +2554,7 @@ async function handleEnableDangerous(ctx, core) {
2736
2554
  return;
2737
2555
  }
2738
2556
  session.dangerousMode = true;
2739
- core.sessionManager.updateSessionDangerousMode(session.id, true).catch(() => {
2557
+ core.sessionManager.patchRecord(session.id, { dangerousMode: true }).catch(() => {
2740
2558
  });
2741
2559
  } else {
2742
2560
  const record = core.sessionManager.getRecordByThread("telegram", String(threadId));
@@ -2748,7 +2566,7 @@ async function handleEnableDangerous(ctx, core) {
2748
2566
  await ctx.reply("\u2620\uFE0F Dangerous mode is already enabled.", { parse_mode: "HTML" });
2749
2567
  return;
2750
2568
  }
2751
- core.sessionManager.updateSessionDangerousMode(record.sessionId, true).catch(() => {
2569
+ core.sessionManager.patchRecord(record.sessionId, { dangerousMode: true }).catch(() => {
2752
2570
  });
2753
2571
  }
2754
2572
  await ctx.reply(
@@ -2773,7 +2591,7 @@ async function handleDisableDangerous(ctx, core) {
2773
2591
  return;
2774
2592
  }
2775
2593
  session.dangerousMode = false;
2776
- core.sessionManager.updateSessionDangerousMode(session.id, false).catch(() => {
2594
+ core.sessionManager.patchRecord(session.id, { dangerousMode: false }).catch(() => {
2777
2595
  });
2778
2596
  } else {
2779
2597
  const record = core.sessionManager.getRecordByThread("telegram", String(threadId));
@@ -2785,7 +2603,7 @@ async function handleDisableDangerous(ctx, core) {
2785
2603
  await ctx.reply("\u{1F510} Dangerous mode is already disabled.", { parse_mode: "HTML" });
2786
2604
  return;
2787
2605
  }
2788
- core.sessionManager.updateSessionDangerousMode(record.sessionId, false).catch(() => {
2606
+ core.sessionManager.patchRecord(record.sessionId, { dangerousMode: false }).catch(() => {
2789
2607
  });
2790
2608
  }
2791
2609
  await ctx.reply("\u{1F510} <b>Dangerous mode disabled</b>\n\nPermission requests will be shown normally.", { parse_mode: "HTML" });
@@ -2838,7 +2656,7 @@ async function handleRestart(ctx, core) {
2838
2656
  }
2839
2657
 
2840
2658
  // src/adapters/telegram/commands/new-session.ts
2841
- var log9 = createChildLogger({ module: "telegram-cmd-new-session" });
2659
+ var log10 = createChildLogger({ module: "telegram-cmd-new-session" });
2842
2660
  var pendingNewSessions = /* @__PURE__ */ new Map();
2843
2661
  var PENDING_TIMEOUT_MS = 5 * 60 * 1e3;
2844
2662
  function cleanupPending(userId) {
@@ -2962,7 +2780,7 @@ async function startConfirmStep(ctx, chatId, userId, agentName, workspace) {
2962
2780
  });
2963
2781
  }
2964
2782
  async function createSessionDirect(ctx, core, chatId, agentName, workspace) {
2965
- log9.info({ userId: ctx.from?.id, agentName, workspace }, "New session command (direct)");
2783
+ log10.info({ userId: ctx.from?.id, agentName, workspace }, "New session command (direct)");
2966
2784
  let threadId;
2967
2785
  try {
2968
2786
  const topicName = `\u{1F504} New Session`;
@@ -2973,7 +2791,7 @@ async function createSessionDirect(ctx, core, chatId, agentName, workspace) {
2973
2791
  });
2974
2792
  const session = await core.handleNewSession("telegram", agentName, workspace);
2975
2793
  session.threadId = String(threadId);
2976
- await core.sessionManager.updateSessionPlatform(session.id, { topicId: threadId });
2794
+ await core.sessionManager.patchRecord(session.id, { platform: { topicId: threadId } });
2977
2795
  const finalName = `\u{1F504} ${session.agentName} \u2014 New Session`;
2978
2796
  try {
2979
2797
  await ctx.api.editForumTopic(chatId, threadId, { name: finalName });
@@ -2992,10 +2810,10 @@ This is your coding session \u2014 chat here to work with the agent.`,
2992
2810
  reply_markup: buildDangerousModeKeyboard(session.id, false)
2993
2811
  }
2994
2812
  );
2995
- session.warmup().catch((err) => log9.error({ err }, "Warm-up error"));
2813
+ session.warmup().catch((err) => log10.error({ err }, "Warm-up error"));
2996
2814
  return threadId ?? null;
2997
2815
  } catch (err) {
2998
- log9.error({ err }, "Session creation failed");
2816
+ log10.error({ err }, "Session creation failed");
2999
2817
  if (threadId) {
3000
2818
  try {
3001
2819
  await ctx.api.deleteForumTopic(chatId, threadId);
@@ -3059,9 +2877,9 @@ async function handleNewChat(ctx, core, chatId) {
3059
2877
  workspace
3060
2878
  );
3061
2879
  session.threadId = String(newThreadId);
3062
- await core.sessionManager.updateSessionPlatform(session.id, {
2880
+ await core.sessionManager.patchRecord(session.id, { platform: {
3063
2881
  topicId: newThreadId
3064
- });
2882
+ } });
3065
2883
  await ctx.api.sendMessage(
3066
2884
  chatId,
3067
2885
  `\u2705 New chat (same agent &amp; workspace)
@@ -3073,7 +2891,7 @@ async function handleNewChat(ctx, core, chatId) {
3073
2891
  reply_markup: buildDangerousModeKeyboard(session.id, false)
3074
2892
  }
3075
2893
  );
3076
- session.warmup().catch((err) => log9.error({ err }, "Warm-up error"));
2894
+ session.warmup().catch((err) => log10.error({ err }, "Warm-up error"));
3077
2895
  } catch (err) {
3078
2896
  if (newThreadId) {
3079
2897
  try {
@@ -3099,12 +2917,12 @@ async function executeNewSession(bot, core, chatId, agentName, workspace) {
3099
2917
  workspace
3100
2918
  );
3101
2919
  session.threadId = String(threadId);
3102
- await core.sessionManager.updateSessionPlatform(session.id, {
2920
+ await core.sessionManager.patchRecord(session.id, { platform: {
3103
2921
  topicId: threadId
3104
- });
2922
+ } });
3105
2923
  const finalName = `\u{1F504} ${session.agentName} \u2014 New Session`;
3106
2924
  await renameSessionTopic(bot, chatId, threadId, finalName);
3107
- session.warmup().catch((err) => log9.error({ err }, "Warm-up error"));
2925
+ session.warmup().catch((err) => log10.error({ err }, "Warm-up error"));
3108
2926
  return { session, threadId, firstMsgId };
3109
2927
  } catch (err) {
3110
2928
  try {
@@ -3247,7 +3065,7 @@ Or just the folder name like <code>my-project</code> (will use ${core.configMana
3247
3065
 
3248
3066
  // src/adapters/telegram/commands/session.ts
3249
3067
  import { InlineKeyboard as InlineKeyboard3 } from "grammy";
3250
- var log10 = createChildLogger({ module: "telegram-cmd-session" });
3068
+ var log11 = createChildLogger({ module: "telegram-cmd-session" });
3251
3069
  async function handleCancel(ctx, core, assistant) {
3252
3070
  const threadId = ctx.message?.message_thread_id;
3253
3071
  if (!threadId) return;
@@ -3265,14 +3083,14 @@ async function handleCancel(ctx, core, assistant) {
3265
3083
  String(threadId)
3266
3084
  );
3267
3085
  if (session) {
3268
- log10.info({ sessionId: session.id }, "Cancel session command");
3269
- await session.cancel();
3086
+ log11.info({ sessionId: session.id }, "Cancel session command");
3087
+ await session.abortPrompt();
3270
3088
  await ctx.reply("\u26D4 Session cancelled.", { parse_mode: "HTML" });
3271
3089
  return;
3272
3090
  }
3273
3091
  const record = core.sessionManager.getRecordByThread("telegram", String(threadId));
3274
3092
  if (record && record.status !== "cancelled" && record.status !== "error") {
3275
- log10.info({ sessionId: record.sessionId }, "Cancel session command (from store)");
3093
+ log11.info({ sessionId: record.sessionId }, "Cancel session command (from store)");
3276
3094
  await core.sessionManager.cancelSession(record.sessionId);
3277
3095
  await ctx.reply("\u26D4 Session cancelled.", { parse_mode: "HTML" });
3278
3096
  }
@@ -3356,8 +3174,8 @@ async function handleTopics(ctx, core) {
3356
3174
  const truncated = records.length > MAX_DISPLAY ? `
3357
3175
 
3358
3176
  <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;
3177
+ const finishedCount = allRecords.filter((r) => r.status === "finished").length;
3178
+ const errorCount = allRecords.filter((r) => r.status === "error" || r.status === "cancelled").length;
3361
3179
  const keyboard = new InlineKeyboard3();
3362
3180
  if (finishedCount > 0) {
3363
3181
  keyboard.text(`Cleanup finished (${finishedCount})`, "m:cleanup:finished").row();
@@ -3368,7 +3186,7 @@ async function handleTopics(ctx, core) {
3368
3186
  if (finishedCount + errorCount > 0) {
3369
3187
  keyboard.text(`Cleanup all non-active (${finishedCount + errorCount})`, "m:cleanup:all").row();
3370
3188
  }
3371
- keyboard.text(`\u26A0\uFE0F Cleanup ALL (${records.length})`, "m:cleanup:everything").row();
3189
+ keyboard.text(`\u26A0\uFE0F Cleanup ALL (${allRecords.length})`, "m:cleanup:everything").row();
3372
3190
  keyboard.text("Refresh", "m:topics");
3373
3191
  await ctx.reply(
3374
3192
  `${header}
@@ -3377,17 +3195,14 @@ ${lines.join("\n")}${truncated}`,
3377
3195
  { parse_mode: "HTML", reply_markup: keyboard }
3378
3196
  );
3379
3197
  } catch (err) {
3380
- log10.error({ err }, "handleTopics error");
3198
+ log11.error({ err }, "handleTopics error");
3381
3199
  await ctx.reply("\u274C Failed to list sessions.", { parse_mode: "HTML" }).catch(() => {
3382
3200
  });
3383
3201
  }
3384
3202
  }
3385
3203
  async function handleCleanup(ctx, core, chatId, statuses) {
3386
3204
  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
- });
3205
+ const cleanable = allRecords.filter((r) => statuses.includes(r.status));
3391
3206
  if (cleanable.length === 0) {
3392
3207
  await ctx.reply("Nothing to clean up.", { parse_mode: "HTML" });
3393
3208
  return;
@@ -3401,13 +3216,13 @@ async function handleCleanup(ctx, core, chatId, statuses) {
3401
3216
  try {
3402
3217
  await ctx.api.deleteForumTopic(chatId, topicId);
3403
3218
  } catch (err) {
3404
- log10.warn({ err, sessionId: record.sessionId, topicId }, "Failed to delete forum topic during cleanup");
3219
+ log11.warn({ err, sessionId: record.sessionId, topicId }, "Failed to delete forum topic during cleanup");
3405
3220
  }
3406
3221
  }
3407
3222
  await core.sessionManager.removeRecord(record.sessionId);
3408
3223
  deleted++;
3409
3224
  } catch (err) {
3410
- log10.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
3225
+ log11.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
3411
3226
  failed++;
3412
3227
  }
3413
3228
  }
@@ -3420,8 +3235,7 @@ async function handleCleanupEverything(ctx, core, chatId, systemTopicIds) {
3420
3235
  const allRecords = core.sessionManager.listRecords();
3421
3236
  const cleanable = allRecords.filter((r) => {
3422
3237
  const platform = r.platform;
3423
- if (!platform?.topicId) return false;
3424
- if (systemTopicIds && (platform.topicId === systemTopicIds.notificationTopicId || platform.topicId === systemTopicIds.assistantTopicId)) return false;
3238
+ if (systemTopicIds && platform?.topicId && (platform.topicId === systemTopicIds.notificationTopicId || platform.topicId === systemTopicIds.assistantTopicId)) return false;
3425
3239
  return true;
3426
3240
  });
3427
3241
  if (cleanable.length === 0) {
@@ -3464,8 +3278,7 @@ async function handleCleanupEverythingConfirmed(ctx, core, chatId, systemTopicId
3464
3278
  const allRecords = core.sessionManager.listRecords();
3465
3279
  const cleanable = allRecords.filter((r) => {
3466
3280
  const platform = r.platform;
3467
- if (!platform?.topicId) return false;
3468
- if (systemTopicIds && (platform.topicId === systemTopicIds.notificationTopicId || platform.topicId === systemTopicIds.assistantTopicId)) return false;
3281
+ if (systemTopicIds && platform?.topicId && (platform.topicId === systemTopicIds.notificationTopicId || platform.topicId === systemTopicIds.assistantTopicId)) return false;
3469
3282
  return true;
3470
3283
  });
3471
3284
  if (cleanable.length === 0) {
@@ -3480,7 +3293,7 @@ async function handleCleanupEverythingConfirmed(ctx, core, chatId, systemTopicId
3480
3293
  try {
3481
3294
  await core.sessionManager.cancelSession(record.sessionId);
3482
3295
  } catch (err) {
3483
- log10.warn({ err, sessionId: record.sessionId }, "Failed to cancel session during cleanup");
3296
+ log11.warn({ err, sessionId: record.sessionId }, "Failed to cancel session during cleanup");
3484
3297
  }
3485
3298
  }
3486
3299
  const topicId = record.platform?.topicId;
@@ -3488,13 +3301,13 @@ async function handleCleanupEverythingConfirmed(ctx, core, chatId, systemTopicId
3488
3301
  try {
3489
3302
  await ctx.api.deleteForumTopic(chatId, topicId);
3490
3303
  } catch (err) {
3491
- log10.warn({ err, sessionId: record.sessionId, topicId }, "Failed to delete forum topic during cleanup");
3304
+ log11.warn({ err, sessionId: record.sessionId, topicId }, "Failed to delete forum topic during cleanup");
3492
3305
  }
3493
3306
  }
3494
3307
  await core.sessionManager.removeRecord(record.sessionId);
3495
3308
  deleted++;
3496
3309
  } catch (err) {
3497
- log10.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
3310
+ log11.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
3498
3311
  failed++;
3499
3312
  }
3500
3313
  }
@@ -3507,7 +3320,7 @@ async function executeCancelSession(core, excludeSessionId) {
3507
3320
  const sessions = core.sessionManager.listSessions("telegram").filter((s) => s.status === "active" && s.id !== excludeSessionId).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
3508
3321
  const session = sessions[0];
3509
3322
  if (!session) return null;
3510
- await session.cancel();
3323
+ await session.abortPrompt();
3511
3324
  return session;
3512
3325
  }
3513
3326
  function setupSessionCallbacks(bot, core, chatId, systemTopicIds) {
@@ -3537,108 +3350,12 @@ function setupSessionCallbacks(bot, core, chatId, systemTopicIds) {
3537
3350
  });
3538
3351
  }
3539
3352
 
3540
- // src/adapters/telegram/commands/menu.ts
3541
- 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
- });
3551
- }
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" });
3565
- }
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" });
3605
- return;
3606
- }
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" });
3614
- }
3615
- }
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;
3630
- }
3631
- }
3632
- if (current) messages.push(current);
3633
- return messages;
3634
- }
3635
-
3636
3353
  // src/adapters/telegram/commands/integrate.ts
3637
- import { InlineKeyboard as InlineKeyboard5 } from "grammy";
3354
+ import { InlineKeyboard as InlineKeyboard4 } from "grammy";
3638
3355
  async function handleIntegrate(ctx, _core) {
3639
3356
  const { listIntegrations } = await import("./integrate-WUPLRJD3.js");
3640
3357
  const agents = listIntegrations();
3641
- const keyboard = new InlineKeyboard5();
3358
+ const keyboard = new InlineKeyboard4();
3642
3359
  for (const agent of agents) {
3643
3360
  keyboard.text(`\u{1F916} ${agent}`, `i:agent:${agent}`).row();
3644
3361
  }
@@ -3650,7 +3367,7 @@ Select an agent to manage its integrations.`,
3650
3367
  );
3651
3368
  }
3652
3369
  function buildAgentItemsKeyboard(agentName, items) {
3653
- const keyboard = new InlineKeyboard5();
3370
+ const keyboard = new InlineKeyboard4();
3654
3371
  for (const item of items) {
3655
3372
  const installed = item.isInstalled();
3656
3373
  keyboard.text(
@@ -3671,7 +3388,7 @@ function setupIntegrateCallbacks(bot, core) {
3671
3388
  if (data === "i:back") {
3672
3389
  const { listIntegrations } = await import("./integrate-WUPLRJD3.js");
3673
3390
  const agents = listIntegrations();
3674
- const keyboard2 = new InlineKeyboard5();
3391
+ const keyboard2 = new InlineKeyboard4();
3675
3392
  for (const agent of agents) {
3676
3393
  keyboard2.text(`\u{1F916} ${agent}`, `i:agent:${agent}`).row();
3677
3394
  }
@@ -3751,15 +3468,307 @@ ${resultText}`,
3751
3468
  });
3752
3469
  }
3753
3470
 
3754
- // src/adapters/telegram/commands/index.ts
3755
- function setupCommands(bot, core, chatId, assistant) {
3756
- bot.command("new", (ctx) => handleNew(ctx, core, chatId, assistant));
3757
- bot.command("newchat", (ctx) => handleNewChat(ctx, core, chatId));
3758
- bot.command("cancel", (ctx) => handleCancel(ctx, core, assistant));
3759
- bot.command("status", (ctx) => handleStatus(ctx, core));
3760
- bot.command("sessions", (ctx) => handleTopics(ctx, core));
3761
- bot.command("agents", (ctx) => handleAgents(ctx, core));
3762
- bot.command("help", (ctx) => handleHelp(ctx));
3471
+ // src/adapters/telegram/commands/settings.ts
3472
+ import { InlineKeyboard as InlineKeyboard5 } from "grammy";
3473
+ var log12 = createChildLogger({ module: "telegram-settings" });
3474
+ function buildSettingsKeyboard(core) {
3475
+ const config = core.configManager.get();
3476
+ const fields = getSafeFields();
3477
+ const kb = new InlineKeyboard5();
3478
+ for (const field of fields) {
3479
+ const value = getConfigValue(config, field.path);
3480
+ const label = formatFieldLabel(field, value);
3481
+ if (field.type === "toggle") {
3482
+ kb.text(`${label}`, `s:toggle:${field.path}`).row();
3483
+ } else if (field.type === "select") {
3484
+ kb.text(`${label}`, `s:select:${field.path}`).row();
3485
+ } else {
3486
+ kb.text(`${label}`, `s:input:${field.path}`).row();
3487
+ }
3488
+ }
3489
+ kb.text("\u25C0\uFE0F Back to Menu", "s:back");
3490
+ return kb;
3491
+ }
3492
+ function formatFieldLabel(field, value) {
3493
+ const icons = {
3494
+ agent: "\u{1F916}",
3495
+ logging: "\u{1F4DD}",
3496
+ tunnel: "\u{1F517}",
3497
+ security: "\u{1F512}",
3498
+ workspace: "\u{1F4C1}",
3499
+ storage: "\u{1F4BE}"
3500
+ };
3501
+ const icon = icons[field.group] ?? "\u2699\uFE0F";
3502
+ if (field.type === "toggle") {
3503
+ return `${icon} ${field.displayName}: ${value ? "ON" : "OFF"}`;
3504
+ }
3505
+ return `${icon} ${field.displayName}: ${String(value)}`;
3506
+ }
3507
+ async function handleSettings(ctx, core) {
3508
+ const kb = buildSettingsKeyboard(core);
3509
+ await ctx.reply(`<b>\u2699\uFE0F Settings</b>
3510
+ Tap to change:`, {
3511
+ parse_mode: "HTML",
3512
+ reply_markup: kb
3513
+ });
3514
+ }
3515
+ function setupSettingsCallbacks(bot, core, getAssistantSession) {
3516
+ bot.callbackQuery(/^s:toggle:/, async (ctx) => {
3517
+ const fieldPath = ctx.callbackQuery.data.replace("s:toggle:", "");
3518
+ const config = core.configManager.get();
3519
+ const currentValue = getConfigValue(config, fieldPath);
3520
+ const newValue = !currentValue;
3521
+ try {
3522
+ const updates = buildNestedUpdate(fieldPath, newValue);
3523
+ await core.configManager.save(updates, fieldPath);
3524
+ const toast = isHotReloadable(fieldPath) ? `\u2705 ${fieldPath} = ${newValue}` : `\u2705 ${fieldPath} = ${newValue} (restart needed)`;
3525
+ try {
3526
+ await ctx.answerCallbackQuery({ text: toast });
3527
+ } catch {
3528
+ }
3529
+ try {
3530
+ await ctx.editMessageReplyMarkup({ reply_markup: buildSettingsKeyboard(core) });
3531
+ } catch {
3532
+ }
3533
+ } catch (err) {
3534
+ log12.error({ err, fieldPath }, "Failed to toggle config");
3535
+ try {
3536
+ await ctx.answerCallbackQuery({ text: "\u274C Failed to update" });
3537
+ } catch {
3538
+ }
3539
+ }
3540
+ });
3541
+ bot.callbackQuery(/^s:select:/, async (ctx) => {
3542
+ const fieldPath = ctx.callbackQuery.data.replace("s:select:", "");
3543
+ const config = core.configManager.get();
3544
+ const fieldDef = getSafeFields().find((f) => f.path === fieldPath);
3545
+ if (!fieldDef) return;
3546
+ const options = resolveOptions(fieldDef, config) ?? [];
3547
+ const currentValue = getConfigValue(config, fieldPath);
3548
+ const kb = new InlineKeyboard5();
3549
+ for (const opt of options) {
3550
+ const marker = opt === String(currentValue) ? " \u2713" : "";
3551
+ kb.text(`${opt}${marker}`, `s:pick:${fieldPath}:${opt}`).row();
3552
+ }
3553
+ kb.text("\u25C0\uFE0F Back", "s:back:refresh");
3554
+ try {
3555
+ await ctx.answerCallbackQuery();
3556
+ } catch {
3557
+ }
3558
+ try {
3559
+ await ctx.editMessageText(`<b>\u2699\uFE0F ${fieldDef.displayName}</b>
3560
+ Select a value:`, {
3561
+ parse_mode: "HTML",
3562
+ reply_markup: kb
3563
+ });
3564
+ } catch {
3565
+ }
3566
+ });
3567
+ bot.callbackQuery(/^s:pick:/, async (ctx) => {
3568
+ const parts = ctx.callbackQuery.data.replace("s:pick:", "").split(":");
3569
+ const fieldPath = parts.slice(0, -1).join(":");
3570
+ const newValue = parts[parts.length - 1];
3571
+ try {
3572
+ const updates = buildNestedUpdate(fieldPath, newValue);
3573
+ await core.configManager.save(updates, fieldPath);
3574
+ try {
3575
+ await ctx.answerCallbackQuery({ text: `\u2705 ${fieldPath} = ${newValue}` });
3576
+ } catch {
3577
+ }
3578
+ try {
3579
+ await ctx.editMessageText(`<b>\u2699\uFE0F Settings</b>
3580
+ Tap to change:`, {
3581
+ parse_mode: "HTML",
3582
+ reply_markup: buildSettingsKeyboard(core)
3583
+ });
3584
+ } catch {
3585
+ }
3586
+ } catch (err) {
3587
+ log12.error({ err, fieldPath }, "Failed to set config");
3588
+ try {
3589
+ await ctx.answerCallbackQuery({ text: "\u274C Failed to update" });
3590
+ } catch {
3591
+ }
3592
+ }
3593
+ });
3594
+ bot.callbackQuery(/^s:input:/, async (ctx) => {
3595
+ const fieldPath = ctx.callbackQuery.data.replace("s:input:", "");
3596
+ const config = core.configManager.get();
3597
+ const fieldDef = getSafeFields().find((f) => f.path === fieldPath);
3598
+ if (!fieldDef) return;
3599
+ const currentValue = getConfigValue(config, fieldPath);
3600
+ const assistant = getAssistantSession();
3601
+ if (!assistant) {
3602
+ try {
3603
+ await ctx.answerCallbackQuery({ text: "\u26A0\uFE0F Start the assistant first (/assistant)" });
3604
+ } catch {
3605
+ }
3606
+ return;
3607
+ }
3608
+ try {
3609
+ await ctx.answerCallbackQuery({ text: `Delegating to assistant...` });
3610
+ } catch {
3611
+ }
3612
+ 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>`;
3613
+ await assistant.enqueuePrompt(prompt);
3614
+ });
3615
+ bot.callbackQuery("s:back", async (ctx) => {
3616
+ try {
3617
+ await ctx.answerCallbackQuery();
3618
+ } catch {
3619
+ }
3620
+ const { buildMenuKeyboard: buildMenuKeyboard3 } = await import("./menu-CARRTW2F.js");
3621
+ try {
3622
+ await ctx.editMessageText(`<b>OpenACP Menu</b>
3623
+ Choose an action:`, {
3624
+ parse_mode: "HTML",
3625
+ reply_markup: buildMenuKeyboard3()
3626
+ });
3627
+ } catch {
3628
+ }
3629
+ });
3630
+ bot.callbackQuery("s:back:refresh", async (ctx) => {
3631
+ try {
3632
+ await ctx.answerCallbackQuery();
3633
+ } catch {
3634
+ }
3635
+ try {
3636
+ await ctx.editMessageText(`<b>\u2699\uFE0F Settings</b>
3637
+ Tap to change:`, {
3638
+ parse_mode: "HTML",
3639
+ reply_markup: buildSettingsKeyboard(core)
3640
+ });
3641
+ } catch {
3642
+ }
3643
+ });
3644
+ }
3645
+ function buildNestedUpdate(dotPath, value) {
3646
+ const parts = dotPath.split(".");
3647
+ const result = {};
3648
+ let target = result;
3649
+ for (let i = 0; i < parts.length - 1; i++) {
3650
+ target[parts[i]] = {};
3651
+ target = target[parts[i]];
3652
+ }
3653
+ target[parts[parts.length - 1]] = value;
3654
+ return result;
3655
+ }
3656
+
3657
+ // src/adapters/telegram/commands/doctor.ts
3658
+ import { InlineKeyboard as InlineKeyboard6 } from "grammy";
3659
+ var log13 = createChildLogger({ module: "telegram-cmd-doctor" });
3660
+ var pendingFixesStore = /* @__PURE__ */ new Map();
3661
+ function renderReport(report) {
3662
+ const icons = { pass: "\u2705", warn: "\u26A0\uFE0F", fail: "\u274C" };
3663
+ const lines = ["\u{1FA7A} <b>OpenACP Doctor</b>\n"];
3664
+ for (const category of report.categories) {
3665
+ lines.push(`<b>${category.name}</b>`);
3666
+ for (const result of category.results) {
3667
+ lines.push(` ${icons[result.status]} ${escapeHtml2(result.message)}`);
3668
+ }
3669
+ lines.push("");
3670
+ }
3671
+ const { passed, warnings, failed, fixed } = report.summary;
3672
+ const fixedStr = fixed > 0 ? `, ${fixed} fixed` : "";
3673
+ lines.push(`<b>Result:</b> ${passed} passed, ${warnings} warnings, ${failed} failed${fixedStr}`);
3674
+ let keyboard;
3675
+ if (report.pendingFixes.length > 0) {
3676
+ keyboard = new InlineKeyboard6();
3677
+ for (let i = 0; i < report.pendingFixes.length; i++) {
3678
+ const label = `\u{1F527} Fix: ${report.pendingFixes[i].message.slice(0, 30)}`;
3679
+ keyboard.text(label, `m:doctor:fix:${i}`).row();
3680
+ }
3681
+ }
3682
+ return { text: lines.join("\n"), keyboard };
3683
+ }
3684
+ function escapeHtml2(text) {
3685
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3686
+ }
3687
+ async function handleDoctor(ctx) {
3688
+ const statusMsg = await ctx.reply("\u{1FA7A} Running diagnostics...", { parse_mode: "HTML" });
3689
+ try {
3690
+ const engine = new DoctorEngine();
3691
+ const report = await engine.runAll();
3692
+ const { text, keyboard } = renderReport(report);
3693
+ const storeKey = `${ctx.chat.id}:${statusMsg.message_id}`;
3694
+ if (report.pendingFixes.length > 0) {
3695
+ pendingFixesStore.set(storeKey, report.pendingFixes);
3696
+ }
3697
+ await ctx.api.editMessageText(ctx.chat.id, statusMsg.message_id, text, {
3698
+ parse_mode: "HTML",
3699
+ reply_markup: keyboard
3700
+ });
3701
+ } catch (err) {
3702
+ log13.error({ err }, "Doctor command failed");
3703
+ await ctx.api.editMessageText(
3704
+ ctx.chat.id,
3705
+ statusMsg.message_id,
3706
+ `\u274C Doctor failed: ${err instanceof Error ? err.message : String(err)}`,
3707
+ { parse_mode: "HTML" }
3708
+ );
3709
+ }
3710
+ }
3711
+ function setupDoctorCallbacks(bot) {
3712
+ bot.callbackQuery(/^m:doctor:fix:/, async (ctx) => {
3713
+ const data = ctx.callbackQuery.data;
3714
+ const index = parseInt(data.replace("m:doctor:fix:", ""), 10);
3715
+ const chatId = ctx.callbackQuery.message?.chat.id;
3716
+ const messageId = ctx.callbackQuery.message?.message_id;
3717
+ try {
3718
+ await ctx.answerCallbackQuery({ text: "Applying fix..." });
3719
+ } catch {
3720
+ }
3721
+ if (chatId === void 0 || messageId === void 0) return;
3722
+ const storeKey = `${chatId}:${messageId}`;
3723
+ const fixes = pendingFixesStore.get(storeKey);
3724
+ if (!fixes || index < 0 || index >= fixes.length) {
3725
+ try {
3726
+ await ctx.answerCallbackQuery({ text: "Fix no longer available" });
3727
+ } catch {
3728
+ }
3729
+ return;
3730
+ }
3731
+ const pending = fixes[index];
3732
+ try {
3733
+ const result = await pending.fix();
3734
+ if (result.success) {
3735
+ const engine = new DoctorEngine();
3736
+ const report = await engine.runAll();
3737
+ const { text, keyboard } = renderReport(report);
3738
+ if (report.pendingFixes.length > 0) {
3739
+ pendingFixesStore.set(storeKey, report.pendingFixes);
3740
+ } else {
3741
+ pendingFixesStore.delete(storeKey);
3742
+ }
3743
+ await ctx.editMessageText(text, { parse_mode: "HTML", reply_markup: keyboard });
3744
+ } else {
3745
+ try {
3746
+ await ctx.answerCallbackQuery({ text: `Fix failed: ${result.message}` });
3747
+ } catch {
3748
+ }
3749
+ }
3750
+ } catch (err) {
3751
+ log13.error({ err, index }, "Doctor fix callback failed");
3752
+ }
3753
+ });
3754
+ bot.callbackQuery("m:doctor", async (ctx) => {
3755
+ try {
3756
+ await ctx.answerCallbackQuery();
3757
+ } catch {
3758
+ }
3759
+ await handleDoctor(ctx);
3760
+ });
3761
+ }
3762
+
3763
+ // src/adapters/telegram/commands/index.ts
3764
+ function setupCommands(bot, core, chatId, assistant) {
3765
+ bot.command("new", (ctx) => handleNew(ctx, core, chatId, assistant));
3766
+ bot.command("newchat", (ctx) => handleNewChat(ctx, core, chatId));
3767
+ bot.command("cancel", (ctx) => handleCancel(ctx, core, assistant));
3768
+ bot.command("status", (ctx) => handleStatus(ctx, core));
3769
+ bot.command("sessions", (ctx) => handleTopics(ctx, core));
3770
+ bot.command("agents", (ctx) => handleAgents(ctx, core));
3771
+ bot.command("help", (ctx) => handleHelp(ctx));
3763
3772
  bot.command("menu", (ctx) => handleMenu(ctx));
3764
3773
  bot.command("enable_dangerous", (ctx) => handleEnableDangerous(ctx, core));
3765
3774
  bot.command("disable_dangerous", (ctx) => handleDisableDangerous(ctx, core));
@@ -3767,10 +3776,13 @@ function setupCommands(bot, core, chatId, assistant) {
3767
3776
  bot.command("update", (ctx) => handleUpdate(ctx, core));
3768
3777
  bot.command("integrate", (ctx) => handleIntegrate(ctx, core));
3769
3778
  bot.command("clear", (ctx) => handleClear(ctx, assistant));
3779
+ bot.command("doctor", (ctx) => handleDoctor(ctx));
3770
3780
  }
3771
- function setupAllCallbacks(bot, core, chatId, systemTopicIds) {
3781
+ function setupAllCallbacks(bot, core, chatId, systemTopicIds, getAssistantSession) {
3772
3782
  setupNewSessionCallbacks(bot, core, chatId);
3773
3783
  setupSessionCallbacks(bot, core, chatId, systemTopicIds);
3784
+ setupSettingsCallbacks(bot, core, getAssistantSession ?? (() => void 0));
3785
+ setupDoctorCallbacks(bot);
3774
3786
  bot.callbackQuery(/^m:/, async (ctx) => {
3775
3787
  const data = ctx.callbackQuery.data;
3776
3788
  try {
@@ -3802,6 +3814,9 @@ function setupAllCallbacks(bot, core, chatId, systemTopicIds) {
3802
3814
  case "m:topics":
3803
3815
  await handleTopics(ctx, core);
3804
3816
  break;
3817
+ case "m:settings":
3818
+ await handleSettings(ctx, core);
3819
+ break;
3805
3820
  }
3806
3821
  });
3807
3822
  }
@@ -3820,13 +3835,14 @@ var STATIC_COMMANDS = [
3820
3835
  { command: "handoff", description: "Continue this session in your terminal" },
3821
3836
  { command: "clear", description: "Clear assistant history" },
3822
3837
  { command: "restart", description: "Restart OpenACP" },
3823
- { command: "update", description: "Update to latest version and restart" }
3838
+ { command: "update", description: "Update to latest version and restart" },
3839
+ { command: "doctor", description: "Run system diagnostics" }
3824
3840
  ];
3825
3841
 
3826
3842
  // src/adapters/telegram/permissions.ts
3827
- import { InlineKeyboard as InlineKeyboard6 } from "grammy";
3843
+ import { InlineKeyboard as InlineKeyboard7 } from "grammy";
3828
3844
  import { nanoid as nanoid2 } from "nanoid";
3829
- var log11 = createChildLogger({ module: "telegram-permissions" });
3845
+ var log14 = createChildLogger({ module: "telegram-permissions" });
3830
3846
  var PermissionHandler = class {
3831
3847
  constructor(bot, chatId, getSession, sendNotification) {
3832
3848
  this.bot = bot;
@@ -3843,7 +3859,7 @@ var PermissionHandler = class {
3843
3859
  requestId: request.id,
3844
3860
  options: request.options.map((o) => ({ id: o.id, isAllow: o.isAllow }))
3845
3861
  });
3846
- const keyboard = new InlineKeyboard6();
3862
+ const keyboard = new InlineKeyboard7();
3847
3863
  for (const option of request.options) {
3848
3864
  const emoji = option.isAllow ? "\u2705" : "\u274C";
3849
3865
  keyboard.text(`${emoji} ${option.label}`, `p:${callbackKey}:${option.id}`);
@@ -3886,7 +3902,7 @@ ${escapeHtml(request.description)}`,
3886
3902
  }
3887
3903
  const session = this.getSession(pending.sessionId);
3888
3904
  const isAllow = pending.options.find((o) => o.id === optionId)?.isAllow ?? false;
3889
- log11.info({ requestId: pending.requestId, optionId, isAllow }, "Permission responded");
3905
+ log14.info({ requestId: pending.requestId, optionId, isAllow }, "Permission responded");
3890
3906
  if (session?.permissionGate.requestId === pending.requestId) {
3891
3907
  session.permissionGate.resolve(optionId);
3892
3908
  }
@@ -4268,20 +4284,19 @@ Session logs auto-cleanup: 30 days (configurable via \`logging.sessionLogRetenti
4268
4284
  `;
4269
4285
 
4270
4286
  // src/adapters/telegram/assistant.ts
4271
- var log12 = createChildLogger({ module: "telegram-assistant" });
4287
+ var log15 = createChildLogger({ module: "telegram-assistant" });
4272
4288
  async function spawnAssistant(core, adapter, assistantTopicId) {
4273
4289
  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
- );
4290
+ log15.info({ agent: config.defaultAgent }, "Creating assistant session...");
4291
+ const session = await core.createSession({
4292
+ channelId: "telegram",
4293
+ agentName: config.defaultAgent,
4294
+ workingDirectory: core.configManager.resolveWorkspace(),
4295
+ initialName: "Assistant"
4296
+ // Prevent auto-naming from triggering after system prompt
4297
+ });
4281
4298
  session.threadId = String(assistantTopicId);
4282
- session.name = "Assistant";
4283
- log12.info({ sessionId: session.id }, "Assistant agent spawned");
4284
- core.wireSessionEvents(session, adapter);
4299
+ log15.info({ sessionId: session.id }, "Assistant agent spawned");
4285
4300
  const allRecords = core.sessionManager.listRecords();
4286
4301
  const activeCount = allRecords.filter((r) => r.status === "active" || r.status === "initializing").length;
4287
4302
  const statusCounts = /* @__PURE__ */ new Map();
@@ -4297,9 +4312,9 @@ async function spawnAssistant(core, adapter, assistantTopicId) {
4297
4312
  };
4298
4313
  const systemPrompt = buildAssistantSystemPrompt(ctx);
4299
4314
  const ready = session.enqueuePrompt(systemPrompt).then(() => {
4300
- log12.info({ sessionId: session.id }, "Assistant system prompt completed");
4315
+ log15.info({ sessionId: session.id }, "Assistant system prompt completed");
4301
4316
  }).catch((err) => {
4302
- log12.warn({ err }, "Assistant system prompt failed");
4317
+ log15.warn({ err }, "Assistant system prompt failed");
4303
4318
  });
4304
4319
  return { session, ready };
4305
4320
  }
@@ -4367,8 +4382,10 @@ function buildAssistantSystemPrompt(ctx) {
4367
4382
  - Execute: \`openacp api cleanup --status <statuses>\`
4368
4383
 
4369
4384
  ### Configuration
4370
- - View: \`openacp api config\`
4371
- - Update: \`openacp api config set <key> <value>\`
4385
+ - View: \`openacp config\` (or \`openacp api config\` \u2014 deprecated)
4386
+ - Update: \`openacp config set <key> <value>\`
4387
+ - When user asks about "settings" or "config", use \`openacp config set\` directly
4388
+ - When receiving a delegated request from the Settings menu, ask user for the new value, then apply with \`openacp config set <path> <value>\`
4372
4389
 
4373
4390
  ### Restart / Update
4374
4391
  - Always ask for confirmation \u2014 these are disruptive actions
@@ -4398,8 +4415,10 @@ openacp api cleanup --status finished,error
4398
4415
 
4399
4416
  # System
4400
4417
  openacp api health # System health
4401
- openacp api config # Show config
4402
- openacp api config set <key> <value> # Update config
4418
+ openacp config # Edit config (interactive)
4419
+ openacp config set <key> <value> # Update config value
4420
+ openacp api config # Show config (deprecated)
4421
+ openacp api config set <key> <value> # Update config (deprecated)
4403
4422
  openacp api adapters # List adapters
4404
4423
  openacp api tunnel # Tunnel status
4405
4424
  openacp api notify "message" # Send notification
@@ -4432,7 +4451,7 @@ function redirectToAssistant(chatId, assistantTopicId) {
4432
4451
  }
4433
4452
 
4434
4453
  // src/adapters/telegram/activity.ts
4435
- var log13 = createChildLogger({ module: "telegram:activity" });
4454
+ var log16 = createChildLogger({ module: "telegram:activity" });
4436
4455
  var THINKING_REFRESH_MS = 15e3;
4437
4456
  var THINKING_MAX_MS = 3 * 60 * 1e3;
4438
4457
  var ThinkingIndicator = class {
@@ -4464,7 +4483,7 @@ var ThinkingIndicator = class {
4464
4483
  this.startRefreshTimer();
4465
4484
  }
4466
4485
  } catch (err) {
4467
- log13.warn({ err }, "ThinkingIndicator.show() failed");
4486
+ log16.warn({ err }, "ThinkingIndicator.show() failed");
4468
4487
  } finally {
4469
4488
  this.sending = false;
4470
4489
  }
@@ -4537,7 +4556,7 @@ var UsageMessage = class {
4537
4556
  if (result) this.msgId = result.message_id;
4538
4557
  }
4539
4558
  } catch (err) {
4540
- log13.warn({ err }, "UsageMessage.send() failed");
4559
+ log16.warn({ err }, "UsageMessage.send() failed");
4541
4560
  }
4542
4561
  }
4543
4562
  getMsgId() {
@@ -4550,7 +4569,7 @@ var UsageMessage = class {
4550
4569
  try {
4551
4570
  await this.sendQueue.enqueue(() => this.api.deleteMessage(this.chatId, id));
4552
4571
  } catch (err) {
4553
- log13.warn({ err }, "UsageMessage.delete() failed");
4572
+ log16.warn({ err }, "UsageMessage.delete() failed");
4554
4573
  }
4555
4574
  }
4556
4575
  };
@@ -4636,7 +4655,7 @@ var PlanCard = class {
4636
4655
  if (result) this.msgId = result.message_id;
4637
4656
  }
4638
4657
  } catch (err) {
4639
- log13.warn({ err }, "PlanCard flush failed");
4658
+ log16.warn({ err }, "PlanCard flush failed");
4640
4659
  }
4641
4660
  }
4642
4661
  };
@@ -4699,7 +4718,7 @@ var ActivityTracker = class {
4699
4718
  })
4700
4719
  );
4701
4720
  } catch (err) {
4702
- log13.warn({ err }, "ActivityTracker.onComplete() Done send failed");
4721
+ log16.warn({ err }, "ActivityTracker.onComplete() Done send failed");
4703
4722
  }
4704
4723
  }
4705
4724
  }
@@ -4782,7 +4801,7 @@ var TelegramSendQueue = class {
4782
4801
 
4783
4802
  // src/adapters/telegram/action-detect.ts
4784
4803
  import { nanoid as nanoid3 } from "nanoid";
4785
- import { InlineKeyboard as InlineKeyboard7 } from "grammy";
4804
+ import { InlineKeyboard as InlineKeyboard8 } from "grammy";
4786
4805
  var CMD_NEW_RE = /\/new(?:\s+([^\s\u0080-\uFFFF]+)(?:\s+([^\s\u0080-\uFFFF]+))?)?/;
4787
4806
  var CMD_CANCEL_RE = /\/cancel\b/;
4788
4807
  var KW_NEW_RE = /(?:create|new)\s+session/i;
@@ -4829,7 +4848,7 @@ function removeAction(id) {
4829
4848
  actionMap.delete(id);
4830
4849
  }
4831
4850
  function buildActionKeyboard(actionId, action) {
4832
- const keyboard = new InlineKeyboard7();
4851
+ const keyboard = new InlineKeyboard8();
4833
4852
  if (action.action === "new_session") {
4834
4853
  keyboard.text("\u2705 Create session", `a:${actionId}`);
4835
4854
  keyboard.text("\u274C Cancel", `a:dismiss:${actionId}`);
@@ -4936,129 +4955,582 @@ function setupActionCallbacks(bot, core, chatId, getAssistantSessionId) {
4936
4955
  });
4937
4956
  }
4938
4957
 
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 };
4958
+ // src/adapters/telegram/tool-call-tracker.ts
4959
+ var log17 = createChildLogger({ module: "tool-call-tracker" });
4960
+ var ToolCallTracker = class {
4961
+ constructor(bot, chatId, sendQueue) {
4962
+ this.bot = bot;
4963
+ this.chatId = chatId;
4964
+ this.sendQueue = sendQueue;
4951
4965
  }
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
4978
- );
4979
- this.sessionTrackers.set(sessionId, tracker);
4966
+ sessions = /* @__PURE__ */ new Map();
4967
+ async trackNewCall(sessionId, threadId, meta) {
4968
+ if (!this.sessions.has(sessionId)) {
4969
+ this.sessions.set(sessionId, /* @__PURE__ */ new Map());
4980
4970
  }
4981
- return tracker;
4982
- }
4983
- constructor(core, config) {
4984
- super(core, config);
4985
- this.telegramConfig = config;
4986
- }
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);
5012
- });
5013
- this.bot.api.config.use((prev, method, payload, signal) => {
5014
- if (method === "getUpdates") {
5015
- const p = payload;
5016
- p.allowed_updates = p.allowed_updates ?? [
5017
- "message",
5018
- "callback_query"
5019
- ];
5020
- }
5021
- return prev(method, payload, signal);
5022
- });
5023
- await this.bot.api.setMyCommands(STATIC_COMMANDS, {
5024
- scope: { type: "chat", chat_id: this.telegramConfig.chatId }
4971
+ let resolveReady;
4972
+ const ready = new Promise((r) => {
4973
+ resolveReady = r;
5025
4974
  });
5026
- this.bot.use((ctx, next) => {
5027
- const chatId = ctx.chat?.id ?? ctx.callbackQuery?.message?.chat?.id;
5028
- if (chatId !== this.telegramConfig.chatId) return;
5029
- return next();
4975
+ this.sessions.get(sessionId).set(meta.id, {
4976
+ msgId: 0,
4977
+ name: meta.name,
4978
+ kind: meta.kind,
4979
+ viewerLinks: meta.viewerLinks,
4980
+ viewerFilePath: meta.viewerFilePath,
4981
+ ready
5030
4982
  });
5031
- const topics = await ensureTopics(
5032
- this.bot,
5033
- this.telegramConfig.chatId,
5034
- this.telegramConfig,
5035
- async (updates) => {
5036
- await this.core.configManager.save({
5037
- channels: { telegram: updates }
5038
- });
5039
- }
5040
- );
5041
- this.notificationTopicId = topics.notificationTopicId;
5042
- this.assistantTopicId = topics.assistantTopicId;
5043
- this.permissionHandler = new PermissionHandler(
5044
- this.bot,
5045
- this.telegramConfig.chatId,
5046
- (sessionId) => this.core.sessionManager.getSession(sessionId),
5047
- (notification) => this.sendNotification(notification)
4983
+ const msg = await this.sendQueue.enqueue(
4984
+ () => this.bot.api.sendMessage(
4985
+ this.chatId,
4986
+ formatToolCall(meta),
4987
+ {
4988
+ message_thread_id: threadId,
4989
+ parse_mode: "HTML",
4990
+ disable_notification: true
4991
+ }
4992
+ )
5048
4993
  );
5049
- setupDangerousModeCallbacks(this.bot, this.core);
5050
- setupActionCallbacks(
5051
- this.bot,
5052
- this.core,
5053
- this.telegramConfig.chatId,
5054
- () => this.assistantSession?.id
4994
+ const toolEntry = this.sessions.get(sessionId).get(meta.id);
4995
+ toolEntry.msgId = msg.message_id;
4996
+ resolveReady();
4997
+ }
4998
+ async updateCall(sessionId, meta) {
4999
+ const toolState = this.sessions.get(sessionId)?.get(meta.id);
5000
+ if (!toolState) return;
5001
+ if (meta.viewerLinks) {
5002
+ toolState.viewerLinks = meta.viewerLinks;
5003
+ log17.debug({ toolId: meta.id, viewerLinks: meta.viewerLinks }, "Accumulated viewerLinks");
5004
+ }
5005
+ if (meta.viewerFilePath) toolState.viewerFilePath = meta.viewerFilePath;
5006
+ if (meta.name) toolState.name = meta.name;
5007
+ if (meta.kind) toolState.kind = meta.kind;
5008
+ const isTerminal = meta.status === "completed" || meta.status === "failed";
5009
+ if (!isTerminal) return;
5010
+ await toolState.ready;
5011
+ log17.debug(
5012
+ {
5013
+ toolId: meta.id,
5014
+ status: meta.status,
5015
+ hasViewerLinks: !!toolState.viewerLinks,
5016
+ viewerLinks: toolState.viewerLinks,
5017
+ name: toolState.name,
5018
+ msgId: toolState.msgId
5019
+ },
5020
+ "Tool completed, preparing edit"
5055
5021
  );
5056
- setupIntegrateCallbacks(this.bot, this.core);
5057
- setupAllCallbacks(
5058
- this.bot,
5059
- this.core,
5060
- this.telegramConfig.chatId,
5061
- { notificationTopicId: this.notificationTopicId, assistantTopicId: this.assistantTopicId }
5022
+ const merged = {
5023
+ ...meta,
5024
+ name: toolState.name,
5025
+ kind: toolState.kind,
5026
+ viewerLinks: toolState.viewerLinks,
5027
+ viewerFilePath: toolState.viewerFilePath
5028
+ };
5029
+ const formattedText = formatToolUpdate(merged);
5030
+ try {
5031
+ await this.sendQueue.enqueue(
5032
+ () => this.bot.api.editMessageText(
5033
+ this.chatId,
5034
+ toolState.msgId,
5035
+ formattedText,
5036
+ { parse_mode: "HTML" }
5037
+ )
5038
+ );
5039
+ } catch (err) {
5040
+ log17.warn(
5041
+ {
5042
+ err,
5043
+ msgId: toolState.msgId,
5044
+ textLen: formattedText.length,
5045
+ hasViewerLinks: !!merged.viewerLinks
5046
+ },
5047
+ "Tool update edit failed"
5048
+ );
5049
+ }
5050
+ }
5051
+ cleanup(sessionId) {
5052
+ this.sessions.delete(sessionId);
5053
+ }
5054
+ };
5055
+
5056
+ // src/adapters/telegram/streaming.ts
5057
+ var FLUSH_INTERVAL = 5e3;
5058
+ var MessageDraft = class {
5059
+ constructor(bot, chatId, threadId, sendQueue, sessionId) {
5060
+ this.bot = bot;
5061
+ this.chatId = chatId;
5062
+ this.threadId = threadId;
5063
+ this.sendQueue = sendQueue;
5064
+ this.sessionId = sessionId;
5065
+ }
5066
+ buffer = "";
5067
+ messageId;
5068
+ firstFlushPending = false;
5069
+ flushTimer;
5070
+ flushPromise = Promise.resolve();
5071
+ lastSentBuffer = "";
5072
+ displayTruncated = false;
5073
+ append(text) {
5074
+ if (!text) return;
5075
+ this.buffer += text;
5076
+ this.scheduleFlush();
5077
+ }
5078
+ scheduleFlush() {
5079
+ if (this.flushTimer) return;
5080
+ this.flushTimer = setTimeout(() => {
5081
+ this.flushTimer = void 0;
5082
+ this.flushPromise = this.flushPromise.then(() => this.flush()).catch(() => {
5083
+ });
5084
+ }, FLUSH_INTERVAL);
5085
+ }
5086
+ async flush() {
5087
+ if (!this.buffer) return;
5088
+ if (this.firstFlushPending) return;
5089
+ const snapshot = this.buffer;
5090
+ let html = markdownToTelegramHtml(snapshot);
5091
+ if (!html) return;
5092
+ let truncated = false;
5093
+ if (html.length > 4096) {
5094
+ const ratio = 4e3 / html.length;
5095
+ const targetLen = Math.floor(snapshot.length * ratio);
5096
+ let cutAt = snapshot.lastIndexOf("\n", targetLen);
5097
+ if (cutAt < targetLen * 0.5) cutAt = targetLen;
5098
+ html = markdownToTelegramHtml(snapshot.slice(0, cutAt) + "\n\u2026");
5099
+ truncated = true;
5100
+ if (html.length > 4096) {
5101
+ html = html.slice(0, 4090) + "\n\u2026";
5102
+ }
5103
+ }
5104
+ if (!this.messageId) {
5105
+ this.firstFlushPending = true;
5106
+ try {
5107
+ const result = await this.sendQueue.enqueue(
5108
+ () => this.bot.api.sendMessage(this.chatId, html, {
5109
+ message_thread_id: this.threadId,
5110
+ parse_mode: "HTML",
5111
+ disable_notification: true
5112
+ }),
5113
+ { type: "other" }
5114
+ );
5115
+ if (result) {
5116
+ this.messageId = result.message_id;
5117
+ if (!truncated) {
5118
+ this.lastSentBuffer = snapshot;
5119
+ this.displayTruncated = false;
5120
+ } else {
5121
+ this.displayTruncated = true;
5122
+ }
5123
+ }
5124
+ } catch {
5125
+ } finally {
5126
+ this.firstFlushPending = false;
5127
+ }
5128
+ } else {
5129
+ try {
5130
+ const result = await this.sendQueue.enqueue(
5131
+ () => this.bot.api.editMessageText(this.chatId, this.messageId, html, {
5132
+ parse_mode: "HTML"
5133
+ }),
5134
+ { type: "text", key: this.sessionId }
5135
+ );
5136
+ if (result !== void 0) {
5137
+ if (!truncated) {
5138
+ this.lastSentBuffer = snapshot;
5139
+ this.displayTruncated = false;
5140
+ } else {
5141
+ this.displayTruncated = true;
5142
+ }
5143
+ }
5144
+ } catch {
5145
+ }
5146
+ }
5147
+ }
5148
+ async finalize() {
5149
+ if (this.flushTimer) {
5150
+ clearTimeout(this.flushTimer);
5151
+ this.flushTimer = void 0;
5152
+ }
5153
+ await this.flushPromise;
5154
+ if (!this.buffer) return this.messageId;
5155
+ if (this.messageId && this.buffer === this.lastSentBuffer && !this.displayTruncated) {
5156
+ return this.messageId;
5157
+ }
5158
+ const fullHtml = markdownToTelegramHtml(this.buffer);
5159
+ if (fullHtml.length <= 4096) {
5160
+ try {
5161
+ if (this.messageId) {
5162
+ await this.sendQueue.enqueue(
5163
+ () => this.bot.api.editMessageText(this.chatId, this.messageId, fullHtml, {
5164
+ parse_mode: "HTML"
5165
+ }),
5166
+ { type: "other" }
5167
+ );
5168
+ } else {
5169
+ const msg = await this.sendQueue.enqueue(
5170
+ () => this.bot.api.sendMessage(this.chatId, fullHtml, {
5171
+ message_thread_id: this.threadId,
5172
+ parse_mode: "HTML",
5173
+ disable_notification: true
5174
+ }),
5175
+ { type: "other" }
5176
+ );
5177
+ if (msg) this.messageId = msg.message_id;
5178
+ }
5179
+ return this.messageId;
5180
+ } catch {
5181
+ }
5182
+ }
5183
+ const mdChunks = splitMessage(this.buffer);
5184
+ for (let i = 0; i < mdChunks.length; i++) {
5185
+ const html = markdownToTelegramHtml(mdChunks[i]);
5186
+ try {
5187
+ if (i === 0 && this.messageId) {
5188
+ await this.sendQueue.enqueue(
5189
+ () => this.bot.api.editMessageText(this.chatId, this.messageId, html, {
5190
+ parse_mode: "HTML"
5191
+ }),
5192
+ { type: "other" }
5193
+ );
5194
+ } else {
5195
+ const msg = await this.sendQueue.enqueue(
5196
+ () => this.bot.api.sendMessage(this.chatId, html, {
5197
+ message_thread_id: this.threadId,
5198
+ parse_mode: "HTML",
5199
+ disable_notification: true
5200
+ }),
5201
+ { type: "other" }
5202
+ );
5203
+ if (msg) {
5204
+ this.messageId = msg.message_id;
5205
+ }
5206
+ }
5207
+ } catch {
5208
+ try {
5209
+ if (i === 0 && this.messageId) {
5210
+ await this.sendQueue.enqueue(
5211
+ () => this.bot.api.editMessageText(this.chatId, this.messageId, mdChunks[i].slice(0, 4096)),
5212
+ { type: "other" }
5213
+ );
5214
+ } else {
5215
+ const msg = await this.sendQueue.enqueue(
5216
+ () => this.bot.api.sendMessage(this.chatId, mdChunks[i].slice(0, 4096), {
5217
+ message_thread_id: this.threadId,
5218
+ disable_notification: true
5219
+ }),
5220
+ { type: "other" }
5221
+ );
5222
+ if (msg) {
5223
+ this.messageId = msg.message_id;
5224
+ }
5225
+ }
5226
+ } catch {
5227
+ }
5228
+ }
5229
+ }
5230
+ return this.messageId;
5231
+ }
5232
+ getMessageId() {
5233
+ return this.messageId;
5234
+ }
5235
+ };
5236
+
5237
+ // src/adapters/telegram/draft-manager.ts
5238
+ var DraftManager = class {
5239
+ constructor(bot, chatId, sendQueue) {
5240
+ this.bot = bot;
5241
+ this.chatId = chatId;
5242
+ this.sendQueue = sendQueue;
5243
+ }
5244
+ drafts = /* @__PURE__ */ new Map();
5245
+ textBuffers = /* @__PURE__ */ new Map();
5246
+ getOrCreate(sessionId, threadId) {
5247
+ let draft = this.drafts.get(sessionId);
5248
+ if (!draft) {
5249
+ draft = new MessageDraft(
5250
+ this.bot,
5251
+ this.chatId,
5252
+ threadId,
5253
+ this.sendQueue,
5254
+ sessionId
5255
+ );
5256
+ this.drafts.set(sessionId, draft);
5257
+ }
5258
+ return draft;
5259
+ }
5260
+ hasDraft(sessionId) {
5261
+ return this.drafts.has(sessionId);
5262
+ }
5263
+ appendText(sessionId, text) {
5264
+ this.textBuffers.set(
5265
+ sessionId,
5266
+ (this.textBuffers.get(sessionId) ?? "") + text
5267
+ );
5268
+ }
5269
+ /**
5270
+ * Finalize the current draft and return the message ID.
5271
+ * Optionally detects actions in assistant responses.
5272
+ */
5273
+ async finalize(sessionId, assistantSessionId) {
5274
+ const draft = this.drafts.get(sessionId);
5275
+ if (!draft) return;
5276
+ this.drafts.delete(sessionId);
5277
+ const finalMsgId = await draft.finalize();
5278
+ if (assistantSessionId && sessionId === assistantSessionId) {
5279
+ const fullText = this.textBuffers.get(sessionId);
5280
+ this.textBuffers.delete(sessionId);
5281
+ if (fullText && finalMsgId) {
5282
+ const detected = detectAction(fullText);
5283
+ if (detected) {
5284
+ const actionId = storeAction(detected);
5285
+ const keyboard = buildActionKeyboard(actionId, detected);
5286
+ try {
5287
+ await this.bot.api.editMessageReplyMarkup(
5288
+ this.chatId,
5289
+ finalMsgId,
5290
+ { reply_markup: keyboard }
5291
+ );
5292
+ } catch {
5293
+ }
5294
+ }
5295
+ }
5296
+ } else {
5297
+ this.textBuffers.delete(sessionId);
5298
+ }
5299
+ }
5300
+ cleanup(sessionId) {
5301
+ this.drafts.delete(sessionId);
5302
+ this.textBuffers.delete(sessionId);
5303
+ }
5304
+ };
5305
+
5306
+ // src/adapters/telegram/skill-command-manager.ts
5307
+ var log18 = createChildLogger({ module: "skill-commands" });
5308
+ var SkillCommandManager = class {
5309
+ // sessionId → pinned msgId
5310
+ constructor(bot, chatId, sendQueue, sessionManager) {
5311
+ this.bot = bot;
5312
+ this.chatId = chatId;
5313
+ this.sendQueue = sendQueue;
5314
+ this.sessionManager = sessionManager;
5315
+ }
5316
+ messages = /* @__PURE__ */ new Map();
5317
+ async send(sessionId, threadId, commands) {
5318
+ if (!this.messages.has(sessionId)) {
5319
+ const record = this.sessionManager.getSessionRecord(sessionId);
5320
+ const platform = record?.platform;
5321
+ if (platform?.skillMsgId) {
5322
+ this.messages.set(sessionId, platform.skillMsgId);
5323
+ }
5324
+ }
5325
+ if (commands.length === 0) {
5326
+ await this.cleanup(sessionId);
5327
+ return;
5328
+ }
5329
+ const messages = buildSkillMessages(commands);
5330
+ const existingMsgId = this.messages.get(sessionId);
5331
+ if (existingMsgId) {
5332
+ try {
5333
+ await this.bot.api.editMessageText(
5334
+ this.chatId,
5335
+ existingMsgId,
5336
+ messages[0],
5337
+ { parse_mode: "HTML" }
5338
+ );
5339
+ return;
5340
+ } catch (err) {
5341
+ const msg = err instanceof Error ? err.message : "";
5342
+ if (msg.includes("message is not modified")) return;
5343
+ try {
5344
+ await this.bot.api.deleteMessage(this.chatId, existingMsgId);
5345
+ } catch {
5346
+ }
5347
+ this.messages.delete(sessionId);
5348
+ }
5349
+ }
5350
+ try {
5351
+ let firstMsgId;
5352
+ for (const text of messages) {
5353
+ const msg = await this.sendQueue.enqueue(
5354
+ () => this.bot.api.sendMessage(this.chatId, text, {
5355
+ message_thread_id: threadId,
5356
+ parse_mode: "HTML",
5357
+ disable_notification: true
5358
+ })
5359
+ );
5360
+ if (!firstMsgId) firstMsgId = msg.message_id;
5361
+ }
5362
+ this.messages.set(sessionId, firstMsgId);
5363
+ const record = this.sessionManager.getSessionRecord(sessionId);
5364
+ if (record) {
5365
+ await this.sessionManager.patchRecord(sessionId, {
5366
+ platform: { ...record.platform, skillMsgId: firstMsgId }
5367
+ });
5368
+ }
5369
+ await this.bot.api.pinChatMessage(this.chatId, firstMsgId, {
5370
+ disable_notification: true
5371
+ });
5372
+ } catch (err) {
5373
+ log18.error({ err, sessionId }, "Failed to send skill commands");
5374
+ }
5375
+ }
5376
+ async cleanup(sessionId) {
5377
+ const msgId = this.messages.get(sessionId);
5378
+ if (!msgId) return;
5379
+ try {
5380
+ await this.bot.api.editMessageText(
5381
+ this.chatId,
5382
+ msgId,
5383
+ "\u{1F6E0} <i>Session ended</i>",
5384
+ { parse_mode: "HTML" }
5385
+ );
5386
+ await this.bot.api.unpinChatMessage(this.chatId, msgId);
5387
+ } catch {
5388
+ }
5389
+ this.messages.delete(sessionId);
5390
+ const record = this.sessionManager.getSessionRecord(sessionId);
5391
+ if (record) {
5392
+ const { skillMsgId: _removed, ...rest } = record.platform;
5393
+ await this.sessionManager.patchRecord(sessionId, { platform: rest });
5394
+ }
5395
+ }
5396
+ };
5397
+
5398
+ // src/adapters/telegram/adapter.ts
5399
+ var log19 = createChildLogger({ module: "telegram" });
5400
+ function patchedFetch(input, init) {
5401
+ if (init?.signal && !(init.signal instanceof AbortSignal)) {
5402
+ const nativeController = new AbortController();
5403
+ const polyfillSignal = init.signal;
5404
+ if (polyfillSignal.aborted) {
5405
+ nativeController.abort();
5406
+ } else {
5407
+ polyfillSignal.addEventListener("abort", () => nativeController.abort());
5408
+ }
5409
+ init = { ...init, signal: nativeController.signal };
5410
+ }
5411
+ return fetch(input, init);
5412
+ }
5413
+ var TelegramAdapter = class extends ChannelAdapter {
5414
+ bot;
5415
+ telegramConfig;
5416
+ permissionHandler;
5417
+ assistantSession = null;
5418
+ assistantInitializing = false;
5419
+ notificationTopicId;
5420
+ assistantTopicId;
5421
+ sendQueue = new TelegramSendQueue(3e3);
5422
+ // Extracted managers
5423
+ toolTracker;
5424
+ draftManager;
5425
+ skillManager;
5426
+ sessionTrackers = /* @__PURE__ */ new Map();
5427
+ getOrCreateTracker(sessionId, threadId) {
5428
+ let tracker = this.sessionTrackers.get(sessionId);
5429
+ if (!tracker) {
5430
+ tracker = new ActivityTracker(
5431
+ this.bot.api,
5432
+ this.telegramConfig.chatId,
5433
+ threadId,
5434
+ this.sendQueue
5435
+ );
5436
+ this.sessionTrackers.set(sessionId, tracker);
5437
+ }
5438
+ return tracker;
5439
+ }
5440
+ constructor(core, config) {
5441
+ super(core, config);
5442
+ this.telegramConfig = config;
5443
+ }
5444
+ async start() {
5445
+ this.bot = new Bot(this.telegramConfig.botToken, { client: { fetch: patchedFetch } });
5446
+ this.toolTracker = new ToolCallTracker(this.bot, this.telegramConfig.chatId, this.sendQueue);
5447
+ this.draftManager = new DraftManager(this.bot, this.telegramConfig.chatId, this.sendQueue);
5448
+ this.skillManager = new SkillCommandManager(
5449
+ this.bot,
5450
+ this.telegramConfig.chatId,
5451
+ this.sendQueue,
5452
+ this.core.sessionManager
5453
+ );
5454
+ this.bot.catch((err) => {
5455
+ const rootCause = err.error instanceof Error ? err.error : err;
5456
+ log19.error({ err: rootCause }, "Telegram bot error");
5457
+ });
5458
+ this.bot.api.config.use(async (prev, method, payload, signal) => {
5459
+ const maxRetries = 3;
5460
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
5461
+ const result = await prev(method, payload, signal);
5462
+ if (result.ok || result.error_code !== 429 || attempt === maxRetries) {
5463
+ return result;
5464
+ }
5465
+ const retryAfter = (result.parameters?.retry_after ?? 5) + 1;
5466
+ const rateLimitedMethods = ["sendMessage", "editMessageText", "editMessageReplyMarkup"];
5467
+ if (rateLimitedMethods.includes(method)) {
5468
+ this.sendQueue.onRateLimited();
5469
+ }
5470
+ log19.warn(
5471
+ { method, retryAfter, attempt: attempt + 1 },
5472
+ "Rate limited by Telegram, retrying"
5473
+ );
5474
+ await new Promise((r) => setTimeout(r, retryAfter * 1e3));
5475
+ }
5476
+ return prev(method, payload, signal);
5477
+ });
5478
+ this.bot.api.config.use((prev, method, payload, signal) => {
5479
+ if (method === "getUpdates") {
5480
+ const p = payload;
5481
+ p.allowed_updates = p.allowed_updates ?? [
5482
+ "message",
5483
+ "callback_query"
5484
+ ];
5485
+ }
5486
+ return prev(method, payload, signal);
5487
+ });
5488
+ await this.bot.api.setMyCommands(STATIC_COMMANDS, {
5489
+ scope: { type: "chat", chat_id: this.telegramConfig.chatId }
5490
+ });
5491
+ this.bot.use((ctx, next) => {
5492
+ const chatId = ctx.chat?.id ?? ctx.callbackQuery?.message?.chat?.id;
5493
+ if (chatId !== this.telegramConfig.chatId) return;
5494
+ return next();
5495
+ });
5496
+ const topics = await ensureTopics(
5497
+ this.bot,
5498
+ this.telegramConfig.chatId,
5499
+ this.telegramConfig,
5500
+ async (updates) => {
5501
+ await this.core.configManager.save({
5502
+ channels: { telegram: updates }
5503
+ });
5504
+ }
5505
+ );
5506
+ this.notificationTopicId = topics.notificationTopicId;
5507
+ this.assistantTopicId = topics.assistantTopicId;
5508
+ this.permissionHandler = new PermissionHandler(
5509
+ this.bot,
5510
+ this.telegramConfig.chatId,
5511
+ (sessionId) => this.core.sessionManager.getSession(sessionId),
5512
+ (notification) => this.sendNotification(notification)
5513
+ );
5514
+ setupDangerousModeCallbacks(this.bot, this.core);
5515
+ setupActionCallbacks(
5516
+ this.bot,
5517
+ this.core,
5518
+ this.telegramConfig.chatId,
5519
+ () => this.assistantSession?.id
5520
+ );
5521
+ setupIntegrateCallbacks(this.bot, this.core);
5522
+ setupAllCallbacks(
5523
+ this.bot,
5524
+ this.core,
5525
+ this.telegramConfig.chatId,
5526
+ { notificationTopicId: this.notificationTopicId, assistantTopicId: this.assistantTopicId },
5527
+ () => {
5528
+ if (!this.assistantSession) return void 0;
5529
+ return {
5530
+ topicId: this.assistantTopicId,
5531
+ enqueuePrompt: (p) => this.assistantSession.enqueuePrompt(p)
5532
+ };
5533
+ }
5062
5534
  );
5063
5535
  setupCommands(
5064
5536
  this.bot,
@@ -5127,7 +5599,7 @@ var TelegramAdapter = class extends ChannelAdapter {
5127
5599
  this.setupRoutes();
5128
5600
  this.bot.start({
5129
5601
  allowed_updates: ["message", "callback_query"],
5130
- onStart: () => log14.info(
5602
+ onStart: () => log19.info(
5131
5603
  { chatId: this.telegramConfig.chatId },
5132
5604
  "Telegram bot started"
5133
5605
  )
@@ -5149,10 +5621,10 @@ var TelegramAdapter = class extends ChannelAdapter {
5149
5621
  reply_markup: buildMenuKeyboard()
5150
5622
  });
5151
5623
  } catch (err) {
5152
- log14.warn({ err }, "Failed to send welcome message");
5624
+ log19.warn({ err }, "Failed to send welcome message");
5153
5625
  }
5154
5626
  try {
5155
- log14.info("Spawning assistant session...");
5627
+ log19.info("Spawning assistant session...");
5156
5628
  const { session, ready } = await spawnAssistant(
5157
5629
  this.core,
5158
5630
  this,
@@ -5160,13 +5632,13 @@ var TelegramAdapter = class extends ChannelAdapter {
5160
5632
  );
5161
5633
  this.assistantSession = session;
5162
5634
  this.assistantInitializing = true;
5163
- log14.info({ sessionId: session.id }, "Assistant session ready, system prompt running in background");
5635
+ log19.info({ sessionId: session.id }, "Assistant session ready, system prompt running in background");
5164
5636
  ready.then(() => {
5165
5637
  this.assistantInitializing = false;
5166
- log14.info({ sessionId: session.id }, "Assistant ready for user messages");
5638
+ log19.info({ sessionId: session.id }, "Assistant ready for user messages");
5167
5639
  });
5168
5640
  } catch (err) {
5169
- log14.error({ err }, "Failed to spawn assistant");
5641
+ log19.error({ err }, "Failed to spawn assistant");
5170
5642
  this.bot.api.sendMessage(
5171
5643
  this.telegramConfig.chatId,
5172
5644
  `\u26A0\uFE0F <b>Failed to start assistant session.</b>
@@ -5182,7 +5654,7 @@ var TelegramAdapter = class extends ChannelAdapter {
5182
5654
  await this.assistantSession.destroy();
5183
5655
  }
5184
5656
  await this.bot.stop();
5185
- log14.info("Telegram bot stopped");
5657
+ log19.info("Telegram bot stopped");
5186
5658
  }
5187
5659
  setupRoutes() {
5188
5660
  this.bot.on("message:text", async (ctx) => {
@@ -5204,16 +5676,16 @@ var TelegramAdapter = class extends ChannelAdapter {
5204
5676
  await ctx.reply("\u26A0\uFE0F Assistant is not available yet. Please try again shortly.", { parse_mode: "HTML" });
5205
5677
  return;
5206
5678
  }
5207
- await this.finalizeDraft(this.assistantSession.id);
5679
+ await this.draftManager.finalize(this.assistantSession.id, this.assistantSession.id);
5208
5680
  ctx.replyWithChatAction("typing").catch(() => {
5209
5681
  });
5210
5682
  handleAssistantMessage(this.assistantSession, ctx.message.text).catch(
5211
- (err) => log14.error({ err }, "Assistant error")
5683
+ (err) => log19.error({ err }, "Assistant error")
5212
5684
  );
5213
5685
  return;
5214
5686
  }
5215
5687
  const sessionId = this.core.sessionManager.getSessionByThread("telegram", String(threadId))?.id;
5216
- if (sessionId) await this.finalizeDraft(sessionId);
5688
+ if (sessionId) await this.draftManager.finalize(sessionId, this.assistantSession?.id);
5217
5689
  if (sessionId) {
5218
5690
  const tracker = this.sessionTrackers.get(sessionId);
5219
5691
  if (tracker) await tracker.onNewPrompt();
@@ -5225,19 +5697,17 @@ var TelegramAdapter = class extends ChannelAdapter {
5225
5697
  threadId: String(threadId),
5226
5698
  userId: String(ctx.from.id),
5227
5699
  text: ctx.message.text
5228
- }).catch((err) => log14.error({ err }, "handleMessage error"));
5700
+ }).catch((err) => log19.error({ err }, "handleMessage error"));
5229
5701
  });
5230
5702
  }
5231
5703
  // --- ChannelAdapter implementations ---
5232
5704
  async sendMessage(sessionId, content) {
5233
5705
  if (this.assistantInitializing && sessionId === this.assistantSession?.id) return;
5234
- const session = this.core.sessionManager.getSession(
5235
- sessionId
5236
- );
5706
+ const session = this.core.sessionManager.getSession(sessionId);
5237
5707
  if (!session) return;
5238
5708
  const threadId = Number(session.threadId);
5239
5709
  if (!threadId || isNaN(threadId)) {
5240
- log14.warn({ sessionId, threadId: session.threadId }, "Session has no valid threadId, skipping message");
5710
+ log19.warn({ sessionId, threadId: session.threadId }, "Session has no valid threadId, skipping message");
5241
5711
  return;
5242
5712
  }
5243
5713
  switch (content.type) {
@@ -5247,105 +5717,32 @@ var TelegramAdapter = class extends ChannelAdapter {
5247
5717
  break;
5248
5718
  }
5249
5719
  case "text": {
5250
- let draft = this.sessionDrafts.get(sessionId);
5251
- if (!draft) {
5720
+ if (!this.draftManager.hasDraft(sessionId)) {
5252
5721
  const tracker = this.getOrCreateTracker(sessionId, threadId);
5253
5722
  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
5723
  }
5724
+ const draft = this.draftManager.getOrCreate(sessionId, threadId);
5263
5725
  draft.append(content.text);
5264
- this.sessionTextBuffers.set(
5265
- sessionId,
5266
- (this.sessionTextBuffers.get(sessionId) ?? "") + content.text
5267
- );
5726
+ this.draftManager.appendText(sessionId, content.text);
5268
5727
  break;
5269
5728
  }
5270
5729
  case "tool_call": {
5271
5730
  const tracker = this.getOrCreateTracker(sessionId, threadId);
5272
5731
  await tracker.onToolCall();
5273
- await this.finalizeDraft(sessionId);
5732
+ await this.draftManager.finalize(sessionId, this.assistantSession?.id);
5274
5733
  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
5734
+ await this.toolTracker.trackNewCall(sessionId, threadId, {
5735
+ ...meta,
5736
+ viewerFilePath: content.metadata?.viewerFilePath
5289
5737
  });
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
5738
  break;
5305
5739
  }
5306
5740
  case "tool_update": {
5307
5741
  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
- }
5742
+ await this.toolTracker.updateCall(sessionId, {
5743
+ ...meta,
5744
+ viewerFilePath: content.metadata?.viewerFilePath
5745
+ });
5349
5746
  break;
5350
5747
  }
5351
5748
  case "plan": {
@@ -5362,7 +5759,7 @@ var TelegramAdapter = class extends ChannelAdapter {
5362
5759
  }
5363
5760
  case "usage": {
5364
5761
  const meta = content.metadata;
5365
- await this.finalizeDraft(sessionId);
5762
+ await this.draftManager.finalize(sessionId, this.assistantSession?.id);
5366
5763
  const tracker = this.getOrCreateTracker(sessionId, threadId);
5367
5764
  await tracker.sendUsage(meta);
5368
5765
  if (this.notificationTopicId && sessionId !== this.assistantSession?.id) {
@@ -5388,10 +5785,10 @@ Task completed.
5388
5785
  break;
5389
5786
  }
5390
5787
  case "session_end": {
5391
- await this.finalizeDraft(sessionId);
5392
- this.sessionDrafts.delete(sessionId);
5393
- this.toolCallMessages.delete(sessionId);
5394
- await this.cleanupSkillCommands(sessionId);
5788
+ await this.draftManager.finalize(sessionId, this.assistantSession?.id);
5789
+ this.draftManager.cleanup(sessionId);
5790
+ this.toolTracker.cleanup(sessionId);
5791
+ await this.skillManager.cleanup(sessionId);
5395
5792
  const tracker = this.sessionTrackers.get(sessionId);
5396
5793
  if (tracker) {
5397
5794
  await tracker.onComplete();
@@ -5413,7 +5810,7 @@ Task completed.
5413
5810
  break;
5414
5811
  }
5415
5812
  case "error": {
5416
- await this.finalizeDraft(sessionId);
5813
+ await this.draftManager.finalize(sessionId, this.assistantSession?.id);
5417
5814
  const tracker = this.sessionTrackers.get(sessionId);
5418
5815
  if (tracker) {
5419
5816
  tracker.destroy();
@@ -5435,15 +5832,13 @@ Task completed.
5435
5832
  }
5436
5833
  }
5437
5834
  async sendPermissionRequest(sessionId, request) {
5438
- log14.info({ sessionId, requestId: request.id }, "Permission request sent");
5439
- const session = this.core.sessionManager.getSession(
5440
- sessionId
5441
- );
5835
+ log19.info({ sessionId, requestId: request.id }, "Permission request sent");
5836
+ const session = this.core.sessionManager.getSession(sessionId);
5442
5837
  if (!session) return;
5443
5838
  if (request.description.includes("openacp")) {
5444
5839
  const allowOption = request.options.find((o) => o.isAllow);
5445
5840
  if (allowOption && session.permissionGate.requestId === request.id) {
5446
- log14.info({ sessionId, requestId: request.id }, "Auto-approving openacp command");
5841
+ log19.info({ sessionId, requestId: request.id }, "Auto-approving openacp command");
5447
5842
  session.permissionGate.resolve(allowOption.id);
5448
5843
  }
5449
5844
  return;
@@ -5451,7 +5846,7 @@ Task completed.
5451
5846
  if (session.dangerousMode) {
5452
5847
  const allowOption = request.options.find((o) => o.isAllow);
5453
5848
  if (allowOption && session.permissionGate.requestId === request.id) {
5454
- log14.info({ sessionId, requestId: request.id, optionId: allowOption.id }, "Dangerous mode: auto-approving permission");
5849
+ log19.info({ sessionId, requestId: request.id, optionId: allowOption.id }, "Dangerous mode: auto-approving permission");
5455
5850
  session.permissionGate.resolve(allowOption.id);
5456
5851
  }
5457
5852
  return;
@@ -5462,7 +5857,7 @@ Task completed.
5462
5857
  }
5463
5858
  async sendNotification(notification) {
5464
5859
  if (notification.sessionId === this.assistantSession?.id) return;
5465
- log14.info(
5860
+ log19.info(
5466
5861
  { sessionId: notification.sessionId, type: notification.type },
5467
5862
  "Notification sent"
5468
5863
  );
@@ -5498,15 +5893,13 @@ Task completed.
5498
5893
  );
5499
5894
  }
5500
5895
  async createSessionThread(sessionId, name) {
5501
- log14.info({ sessionId, name }, "Session topic created");
5896
+ log19.info({ sessionId, name }, "Session topic created");
5502
5897
  return String(
5503
5898
  await createSessionTopic(this.bot, this.telegramConfig.chatId, name)
5504
5899
  );
5505
5900
  }
5506
5901
  async renameSessionThread(sessionId, newName) {
5507
- const session = this.core.sessionManager.getSession(
5508
- sessionId
5509
- );
5902
+ const session = this.core.sessionManager.getSession(sessionId);
5510
5903
  if (!session) return;
5511
5904
  await renameSessionTopic(
5512
5905
  this.bot,
@@ -5514,10 +5907,7 @@ Task completed.
5514
5907
  Number(session.threadId),
5515
5908
  newName
5516
5909
  );
5517
- await this.core.sessionManager.updateSessionName(
5518
- sessionId,
5519
- newName
5520
- );
5910
+ await this.core.sessionManager.patchRecord(sessionId, { name: newName });
5521
5911
  }
5522
5912
  async deleteSessionThread(sessionId) {
5523
5913
  const record = this.core.sessionManager.getSessionRecord(sessionId);
@@ -5527,7 +5917,7 @@ Task completed.
5527
5917
  try {
5528
5918
  await this.bot.api.deleteForumTopic(this.telegramConfig.chatId, topicId);
5529
5919
  } catch (err) {
5530
- log14.warn({ err, sessionId, topicId }, "Failed to delete forum topic (may already be deleted)");
5920
+ log19.warn({ err, sessionId, topicId }, "Failed to delete forum topic (may already be deleted)");
5531
5921
  }
5532
5922
  }
5533
5923
  async sendSkillCommands(sessionId, commands) {
@@ -5536,119 +5926,10 @@ Task completed.
5536
5926
  if (!session) return;
5537
5927
  const threadId = Number(session.threadId);
5538
5928
  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
- }
5929
+ await this.skillManager.send(sessionId, threadId, commands);
5605
5930
  }
5606
5931
  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
- }
5932
+ await this.skillManager.cleanup(sessionId);
5652
5933
  }
5653
5934
  };
5654
5935
 
@@ -5663,6 +5944,7 @@ export {
5663
5944
  PermissionGate,
5664
5945
  Session,
5665
5946
  SessionManager,
5947
+ SessionBridge,
5666
5948
  NotificationManager,
5667
5949
  MessageTransformer,
5668
5950
  OpenACPCore,
@@ -5671,4 +5953,4 @@ export {
5671
5953
  TopicManager,
5672
5954
  TelegramAdapter
5673
5955
  };
5674
- //# sourceMappingURL=chunk-KPI4HGJC.js.map
5956
+ //# sourceMappingURL=chunk-66RVSUAR.js.map