@openacp/cli 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -2
- package/dist/{chunk-MHFCZGRW.js → chunk-2KJC3ILH.js} +13 -3
- package/dist/{chunk-MHFCZGRW.js.map → chunk-2KJC3ILH.js.map} +1 -1
- package/dist/{chunk-DWQKUECJ.js → chunk-4LFDEW22.js} +42 -4
- package/dist/chunk-4LFDEW22.js.map +1 -0
- package/dist/{chunk-Z46LGZ7R.js → chunk-4TR5Y3MP.js} +18 -1
- package/dist/chunk-4TR5Y3MP.js.map +1 -0
- package/dist/{chunk-IURZ4QHG.js → chunk-7QJS2XBD.js} +2 -1
- package/dist/chunk-7QJS2XBD.js.map +1 -0
- package/dist/{chunk-3WPG7GXA.js → chunk-GINCOFNW.js} +2 -2
- package/dist/{chunk-437NLISU.js → chunk-IMILOCR5.js} +2 -2
- package/dist/{chunk-5NBWM7P6.js → chunk-LGQYTK55.js} +5 -1
- package/dist/chunk-LGQYTK55.js.map +1 -0
- package/dist/{chunk-V2V767XI.js → chunk-R3UJUOXI.js} +842 -162
- package/dist/chunk-R3UJUOXI.js.map +1 -0
- package/dist/{chunk-YYQXWA62.js → chunk-TOZQ3JFN.js} +2 -2
- package/dist/{chunk-6Q7PZWCL.js → chunk-UB7XUO7C.js} +3 -3
- package/dist/{chunk-SPX7CKWV.js → chunk-ZCHNAM3B.js} +2 -2
- package/dist/cli.js +19 -19
- package/dist/{config-KF2MQWAP.js → config-AK2W3E67.js} +2 -2
- package/dist/{config-editor-OTODXUF7.js → config-editor-VIA7A72X.js} +4 -4
- package/dist/{config-registry-SNKA2EH2.js → config-registry-QQOJ2GQP.js} +2 -2
- package/dist/{daemon-U6UC7OM4.js → daemon-G27YZUWB.js} +3 -3
- package/dist/{discord-SLLKRUP7.js → discord-2DKRH45T.js} +16 -6
- package/dist/discord-2DKRH45T.js.map +1 -0
- package/dist/doctor-AN6AZ3PF.js +9 -0
- package/dist/{doctor-DB5PRQ6D.js → doctor-CHCYUTV5.js} +4 -4
- package/dist/index.d.ts +320 -3
- package/dist/index.js +18 -10
- package/dist/{main-M6RH3SS5.js → main-56SPFYW4.js} +16 -16
- package/dist/{menu-J5YVH665.js → menu-XR2GET2B.js} +2 -2
- package/dist/{setup-LI5CKYDK.js → setup-IPWJCIJM.js} +3 -3
- package/package.json +1 -1
- package/dist/chunk-5NBWM7P6.js.map +0 -1
- package/dist/chunk-DWQKUECJ.js.map +0 -1
- package/dist/chunk-IURZ4QHG.js.map +0 -1
- package/dist/chunk-V2V767XI.js.map +0 -1
- package/dist/chunk-Z46LGZ7R.js.map +0 -1
- package/dist/discord-SLLKRUP7.js.map +0 -1
- package/dist/doctor-SYWNJFYK.js +0 -9
- /package/dist/{chunk-3WPG7GXA.js.map → chunk-GINCOFNW.js.map} +0 -0
- /package/dist/{chunk-437NLISU.js.map → chunk-IMILOCR5.js.map} +0 -0
- /package/dist/{chunk-YYQXWA62.js.map → chunk-TOZQ3JFN.js.map} +0 -0
- /package/dist/{chunk-6Q7PZWCL.js.map → chunk-UB7XUO7C.js.map} +0 -0
- /package/dist/{chunk-SPX7CKWV.js.map → chunk-ZCHNAM3B.js.map} +0 -0
- /package/dist/{config-KF2MQWAP.js.map → config-AK2W3E67.js.map} +0 -0
- /package/dist/{config-editor-OTODXUF7.js.map → config-editor-VIA7A72X.js.map} +0 -0
- /package/dist/{config-registry-SNKA2EH2.js.map → config-registry-QQOJ2GQP.js.map} +0 -0
- /package/dist/{daemon-U6UC7OM4.js.map → daemon-G27YZUWB.js.map} +0 -0
- /package/dist/{doctor-DB5PRQ6D.js.map → doctor-AN6AZ3PF.js.map} +0 -0
- /package/dist/{doctor-SYWNJFYK.js.map → doctor-CHCYUTV5.js.map} +0 -0
- /package/dist/{main-M6RH3SS5.js.map → main-56SPFYW4.js.map} +0 -0
- /package/dist/{menu-J5YVH665.js.map → menu-XR2GET2B.js.map} +0 -0
- /package/dist/{setup-LI5CKYDK.js.map → setup-IPWJCIJM.js.map} +0 -0
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ChannelAdapter,
|
|
3
3
|
PRODUCT_GUIDE
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-LGQYTK55.js";
|
|
5
5
|
import {
|
|
6
6
|
DoctorEngine
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-ZCHNAM3B.js";
|
|
8
8
|
import {
|
|
9
9
|
buildMenuKeyboard,
|
|
10
10
|
buildSkillMessages,
|
|
11
11
|
handleClear,
|
|
12
12
|
handleHelp,
|
|
13
13
|
handleMenu
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-7QJS2XBD.js";
|
|
15
15
|
import {
|
|
16
16
|
AgentCatalog
|
|
17
17
|
} from "./chunk-J6X5SW6O.js";
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
getSafeFields,
|
|
24
24
|
isHotReloadable,
|
|
25
25
|
resolveOptions
|
|
26
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-4TR5Y3MP.js";
|
|
27
27
|
import {
|
|
28
28
|
createChildLogger,
|
|
29
29
|
createSessionLogger
|
|
@@ -761,6 +761,7 @@ var PermissionGate = class {
|
|
|
761
761
|
|
|
762
762
|
// src/core/session.ts
|
|
763
763
|
import { nanoid } from "nanoid";
|
|
764
|
+
import * as fs2 from "fs";
|
|
764
765
|
var moduleLog = createChildLogger({ module: "session" });
|
|
765
766
|
var VALID_TRANSITIONS = {
|
|
766
767
|
initializing: /* @__PURE__ */ new Set(["active", "error"]),
|
|
@@ -781,9 +782,11 @@ var Session = class extends TypedEmitter {
|
|
|
781
782
|
name;
|
|
782
783
|
createdAt = /* @__PURE__ */ new Date();
|
|
783
784
|
dangerousMode = false;
|
|
785
|
+
archiving = false;
|
|
784
786
|
log;
|
|
785
787
|
permissionGate = new PermissionGate();
|
|
786
788
|
queue;
|
|
789
|
+
speechService;
|
|
787
790
|
constructor(opts) {
|
|
788
791
|
super();
|
|
789
792
|
this.id = opts.id || nanoid(12);
|
|
@@ -791,6 +794,7 @@ var Session = class extends TypedEmitter {
|
|
|
791
794
|
this.agentName = opts.agentName;
|
|
792
795
|
this.workingDirectory = opts.workingDirectory;
|
|
793
796
|
this.agentInstance = opts.agentInstance;
|
|
797
|
+
this.speechService = opts.speechService;
|
|
794
798
|
this.log = createSessionLogger(this.id, moduleLog);
|
|
795
799
|
this.log.info({ agentName: this.agentName }, "Session created");
|
|
796
800
|
this.queue = new PromptQueue(
|
|
@@ -856,7 +860,8 @@ var Session = class extends TypedEmitter {
|
|
|
856
860
|
}
|
|
857
861
|
const promptStart = Date.now();
|
|
858
862
|
this.log.debug("Prompt execution started");
|
|
859
|
-
await this.
|
|
863
|
+
const processed = await this.maybeTranscribeAudio(text, attachments);
|
|
864
|
+
await this.agentInstance.prompt(processed.text, processed.attachments);
|
|
860
865
|
this.log.info(
|
|
861
866
|
{ durationMs: Date.now() - promptStart },
|
|
862
867
|
"Prompt execution completed"
|
|
@@ -865,6 +870,51 @@ var Session = class extends TypedEmitter {
|
|
|
865
870
|
await this.autoName();
|
|
866
871
|
}
|
|
867
872
|
}
|
|
873
|
+
async maybeTranscribeAudio(text, attachments) {
|
|
874
|
+
if (!attachments?.length || !this.speechService) {
|
|
875
|
+
return { text, attachments };
|
|
876
|
+
}
|
|
877
|
+
const hasAudioCapability = this.agentInstance.promptCapabilities?.audio === true;
|
|
878
|
+
if (hasAudioCapability) {
|
|
879
|
+
return { text, attachments };
|
|
880
|
+
}
|
|
881
|
+
if (!this.speechService.isSTTAvailable()) {
|
|
882
|
+
return { text, attachments };
|
|
883
|
+
}
|
|
884
|
+
let transcribedText = text;
|
|
885
|
+
const remainingAttachments = [];
|
|
886
|
+
for (const att of attachments) {
|
|
887
|
+
if (att.type !== "audio") {
|
|
888
|
+
remainingAttachments.push(att);
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
891
|
+
try {
|
|
892
|
+
const audioPath = att.originalFilePath || att.filePath;
|
|
893
|
+
const audioMime = att.originalFilePath ? "audio/ogg" : att.mimeType;
|
|
894
|
+
const audioBuffer = await fs2.promises.readFile(audioPath);
|
|
895
|
+
const result = await this.speechService.transcribe(audioBuffer, audioMime);
|
|
896
|
+
this.log.info({ provider: "stt", duration: result.duration }, "Voice transcribed");
|
|
897
|
+
this.emit("agent_event", {
|
|
898
|
+
type: "system_message",
|
|
899
|
+
message: `\u{1F3A4} You said: ${result.text}`
|
|
900
|
+
});
|
|
901
|
+
transcribedText = transcribedText.replace(/\[Audio:\s*[^\]]*\]\s*/g, "").trim();
|
|
902
|
+
transcribedText = transcribedText ? `${transcribedText}
|
|
903
|
+
${result.text}` : result.text;
|
|
904
|
+
} catch (err) {
|
|
905
|
+
this.log.warn({ err }, "STT transcription failed, keeping audio attachment");
|
|
906
|
+
this.emit("agent_event", {
|
|
907
|
+
type: "error",
|
|
908
|
+
message: `Voice transcription failed: ${err.message}`
|
|
909
|
+
});
|
|
910
|
+
remainingAttachments.push(att);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
return {
|
|
914
|
+
text: transcribedText,
|
|
915
|
+
attachments: remainingAttachments.length > 0 ? remainingAttachments : void 0
|
|
916
|
+
};
|
|
917
|
+
}
|
|
868
918
|
// NOTE: This injects a summary prompt into the agent's conversation history.
|
|
869
919
|
async autoName() {
|
|
870
920
|
let title = "";
|
|
@@ -1042,7 +1092,7 @@ var SessionManager = class {
|
|
|
1042
1092
|
};
|
|
1043
1093
|
|
|
1044
1094
|
// src/core/file-service.ts
|
|
1045
|
-
import
|
|
1095
|
+
import fs3 from "fs";
|
|
1046
1096
|
import path2 from "path";
|
|
1047
1097
|
import { OggOpusDecoder } from "ogg-opus-decoder";
|
|
1048
1098
|
import wav from "node-wav";
|
|
@@ -1089,10 +1139,10 @@ var FileService = class {
|
|
|
1089
1139
|
}
|
|
1090
1140
|
async saveFile(sessionId, fileName, data, mimeType) {
|
|
1091
1141
|
const sessionDir = path2.join(this.baseDir, sessionId);
|
|
1092
|
-
await
|
|
1142
|
+
await fs3.promises.mkdir(sessionDir, { recursive: true });
|
|
1093
1143
|
const safeName = `${Date.now()}-${fileName.replace(/[^a-zA-Z0-9._-]/g, "_")}`;
|
|
1094
1144
|
const filePath = path2.join(sessionDir, safeName);
|
|
1095
|
-
await
|
|
1145
|
+
await fs3.promises.writeFile(filePath, data);
|
|
1096
1146
|
return {
|
|
1097
1147
|
type: classifyMime(mimeType),
|
|
1098
1148
|
filePath,
|
|
@@ -1103,7 +1153,7 @@ var FileService = class {
|
|
|
1103
1153
|
}
|
|
1104
1154
|
async resolveFile(filePath) {
|
|
1105
1155
|
try {
|
|
1106
|
-
const stat = await
|
|
1156
|
+
const stat = await fs3.promises.stat(filePath);
|
|
1107
1157
|
if (!stat.isFile()) return null;
|
|
1108
1158
|
const ext = path2.extname(filePath).toLowerCase();
|
|
1109
1159
|
const mimeType = EXT_TO_MIME[ext] || "application/octet-stream";
|
|
@@ -1232,12 +1282,12 @@ var SessionBridge = class {
|
|
|
1232
1282
|
break;
|
|
1233
1283
|
case "image_content": {
|
|
1234
1284
|
if (this.deps.fileService) {
|
|
1235
|
-
const
|
|
1285
|
+
const fs7 = this.deps.fileService;
|
|
1236
1286
|
const sid = this.session.id;
|
|
1237
1287
|
const { data, mimeType } = event;
|
|
1238
1288
|
const buffer = Buffer.from(data, "base64");
|
|
1239
1289
|
const ext = FileService.extensionFromMime(mimeType);
|
|
1240
|
-
|
|
1290
|
+
fs7.saveFile(sid, `agent-image${ext}`, buffer, mimeType).then((att) => {
|
|
1241
1291
|
this.adapter.sendMessage(sid, { type: "attachment", text: "", attachment: att });
|
|
1242
1292
|
}).catch((err) => log2.error({ err }, "Failed to save agent image"));
|
|
1243
1293
|
}
|
|
@@ -1245,12 +1295,12 @@ var SessionBridge = class {
|
|
|
1245
1295
|
}
|
|
1246
1296
|
case "audio_content": {
|
|
1247
1297
|
if (this.deps.fileService) {
|
|
1248
|
-
const
|
|
1298
|
+
const fs7 = this.deps.fileService;
|
|
1249
1299
|
const sid = this.session.id;
|
|
1250
1300
|
const { data, mimeType } = event;
|
|
1251
1301
|
const buffer = Buffer.from(data, "base64");
|
|
1252
1302
|
const ext = FileService.extensionFromMime(mimeType);
|
|
1253
|
-
|
|
1303
|
+
fs7.saveFile(sid, `agent-audio${ext}`, buffer, mimeType).then((att) => {
|
|
1254
1304
|
this.adapter.sendMessage(sid, { type: "attachment", text: "", attachment: att });
|
|
1255
1305
|
}).catch((err) => log2.error({ err }, "Failed to save agent audio"));
|
|
1256
1306
|
}
|
|
@@ -1260,6 +1310,12 @@ var SessionBridge = class {
|
|
|
1260
1310
|
log2.debug({ commands: event.commands }, "Commands available");
|
|
1261
1311
|
this.adapter.sendSkillCommands(this.session.id, event.commands);
|
|
1262
1312
|
break;
|
|
1313
|
+
case "system_message":
|
|
1314
|
+
this.adapter.sendMessage(
|
|
1315
|
+
this.session.id,
|
|
1316
|
+
this.deps.messageTransformer.transform(event)
|
|
1317
|
+
);
|
|
1318
|
+
break;
|
|
1263
1319
|
}
|
|
1264
1320
|
};
|
|
1265
1321
|
this.session.on("agent_event", this.agentEventHandler);
|
|
@@ -1454,6 +1510,8 @@ var MessageTransformer = class {
|
|
|
1454
1510
|
return { type: "session_end", text: `Done (${event.reason})` };
|
|
1455
1511
|
case "error":
|
|
1456
1512
|
return { type: "error", text: event.message };
|
|
1513
|
+
case "system_message":
|
|
1514
|
+
return { type: "system_message", text: event.message };
|
|
1457
1515
|
default:
|
|
1458
1516
|
return { type: "text", text: "" };
|
|
1459
1517
|
}
|
|
@@ -1509,15 +1567,331 @@ var MessageTransformer = class {
|
|
|
1509
1567
|
}
|
|
1510
1568
|
};
|
|
1511
1569
|
|
|
1570
|
+
// src/core/usage-store.ts
|
|
1571
|
+
import fs4 from "fs";
|
|
1572
|
+
import path3 from "path";
|
|
1573
|
+
var log4 = createChildLogger({ module: "usage-store" });
|
|
1574
|
+
var DEBOUNCE_MS = 2e3;
|
|
1575
|
+
var UsageStore = class {
|
|
1576
|
+
constructor(filePath, retentionDays) {
|
|
1577
|
+
this.filePath = filePath;
|
|
1578
|
+
this.retentionDays = retentionDays;
|
|
1579
|
+
this.load();
|
|
1580
|
+
this.cleanup();
|
|
1581
|
+
this.cleanupInterval = setInterval(
|
|
1582
|
+
() => this.cleanup(),
|
|
1583
|
+
24 * 60 * 60 * 1e3
|
|
1584
|
+
);
|
|
1585
|
+
this.flushHandler = () => {
|
|
1586
|
+
try {
|
|
1587
|
+
this.flushSync();
|
|
1588
|
+
} catch {
|
|
1589
|
+
}
|
|
1590
|
+
};
|
|
1591
|
+
process.on("SIGTERM", this.flushHandler);
|
|
1592
|
+
process.on("SIGINT", this.flushHandler);
|
|
1593
|
+
process.on("exit", this.flushHandler);
|
|
1594
|
+
}
|
|
1595
|
+
records = [];
|
|
1596
|
+
debounceTimer = null;
|
|
1597
|
+
cleanupInterval = null;
|
|
1598
|
+
flushHandler = null;
|
|
1599
|
+
append(record) {
|
|
1600
|
+
this.records.push(record);
|
|
1601
|
+
this.scheduleDiskWrite();
|
|
1602
|
+
}
|
|
1603
|
+
query(period) {
|
|
1604
|
+
const cutoff = this.getCutoff(period);
|
|
1605
|
+
const filtered = cutoff ? this.records.filter((r) => new Date(r.timestamp).getTime() >= cutoff) : this.records;
|
|
1606
|
+
const totalTokens = filtered.reduce((sum, r) => sum + r.tokensUsed, 0);
|
|
1607
|
+
const totalCost = filtered.reduce(
|
|
1608
|
+
(sum, r) => sum + (r.cost?.amount ?? 0),
|
|
1609
|
+
0
|
|
1610
|
+
);
|
|
1611
|
+
const sessionIds = new Set(filtered.map((r) => r.sessionId));
|
|
1612
|
+
const currency = filtered.find((r) => r.cost?.currency)?.cost?.currency ?? "USD";
|
|
1613
|
+
return {
|
|
1614
|
+
period,
|
|
1615
|
+
totalTokens,
|
|
1616
|
+
totalCost,
|
|
1617
|
+
currency,
|
|
1618
|
+
sessionCount: sessionIds.size,
|
|
1619
|
+
recordCount: filtered.length
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1622
|
+
getMonthlyTotal() {
|
|
1623
|
+
const now = /* @__PURE__ */ new Date();
|
|
1624
|
+
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
1625
|
+
const cutoff = startOfMonth.getTime();
|
|
1626
|
+
const filtered = this.records.filter(
|
|
1627
|
+
(r) => new Date(r.timestamp).getTime() >= cutoff
|
|
1628
|
+
);
|
|
1629
|
+
const totalCost = filtered.reduce(
|
|
1630
|
+
(sum, r) => sum + (r.cost?.amount ?? 0),
|
|
1631
|
+
0
|
|
1632
|
+
);
|
|
1633
|
+
const currency = filtered.find((r) => r.cost?.currency)?.cost?.currency ?? "USD";
|
|
1634
|
+
return { totalCost, currency };
|
|
1635
|
+
}
|
|
1636
|
+
cleanup() {
|
|
1637
|
+
const cutoff = Date.now() - this.retentionDays * 24 * 60 * 60 * 1e3;
|
|
1638
|
+
const before = this.records.length;
|
|
1639
|
+
this.records = this.records.filter(
|
|
1640
|
+
(r) => new Date(r.timestamp).getTime() >= cutoff
|
|
1641
|
+
);
|
|
1642
|
+
const removed = before - this.records.length;
|
|
1643
|
+
if (removed > 0) {
|
|
1644
|
+
log4.info({ removed }, "Cleaned up expired usage records");
|
|
1645
|
+
this.scheduleDiskWrite();
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
flushSync() {
|
|
1649
|
+
if (this.debounceTimer) {
|
|
1650
|
+
clearTimeout(this.debounceTimer);
|
|
1651
|
+
this.debounceTimer = null;
|
|
1652
|
+
}
|
|
1653
|
+
const data = { version: 1, records: this.records };
|
|
1654
|
+
const dir = path3.dirname(this.filePath);
|
|
1655
|
+
if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
|
|
1656
|
+
fs4.writeFileSync(this.filePath, JSON.stringify(data, null, 2));
|
|
1657
|
+
}
|
|
1658
|
+
destroy() {
|
|
1659
|
+
if (this.debounceTimer) this.flushSync();
|
|
1660
|
+
if (this.cleanupInterval) clearInterval(this.cleanupInterval);
|
|
1661
|
+
if (this.flushHandler) {
|
|
1662
|
+
process.removeListener("SIGTERM", this.flushHandler);
|
|
1663
|
+
process.removeListener("SIGINT", this.flushHandler);
|
|
1664
|
+
process.removeListener("exit", this.flushHandler);
|
|
1665
|
+
this.flushHandler = null;
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
load() {
|
|
1669
|
+
if (!fs4.existsSync(this.filePath)) return;
|
|
1670
|
+
try {
|
|
1671
|
+
const raw = JSON.parse(
|
|
1672
|
+
fs4.readFileSync(this.filePath, "utf-8")
|
|
1673
|
+
);
|
|
1674
|
+
if (raw.version !== 1) {
|
|
1675
|
+
log4.warn(
|
|
1676
|
+
{ version: raw.version },
|
|
1677
|
+
"Unknown usage store version, skipping load"
|
|
1678
|
+
);
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1681
|
+
this.records = raw.records || [];
|
|
1682
|
+
log4.info({ count: this.records.length }, "Loaded usage records");
|
|
1683
|
+
} catch (err) {
|
|
1684
|
+
log4.error({ err }, "Failed to load usage store, backing up corrupt file");
|
|
1685
|
+
try {
|
|
1686
|
+
fs4.copyFileSync(this.filePath, this.filePath + ".bak");
|
|
1687
|
+
} catch {
|
|
1688
|
+
}
|
|
1689
|
+
this.records = [];
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
getCutoff(period) {
|
|
1693
|
+
const now = /* @__PURE__ */ new Date();
|
|
1694
|
+
switch (period) {
|
|
1695
|
+
case "today": {
|
|
1696
|
+
const start = new Date(now);
|
|
1697
|
+
start.setHours(0, 0, 0, 0);
|
|
1698
|
+
return start.getTime();
|
|
1699
|
+
}
|
|
1700
|
+
case "week":
|
|
1701
|
+
return Date.now() - 7 * 24 * 60 * 60 * 1e3;
|
|
1702
|
+
case "month": {
|
|
1703
|
+
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
1704
|
+
return startOfMonth.getTime();
|
|
1705
|
+
}
|
|
1706
|
+
case "all":
|
|
1707
|
+
return null;
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
scheduleDiskWrite() {
|
|
1711
|
+
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
1712
|
+
this.debounceTimer = setTimeout(() => {
|
|
1713
|
+
this.flushSync();
|
|
1714
|
+
}, DEBOUNCE_MS);
|
|
1715
|
+
}
|
|
1716
|
+
};
|
|
1717
|
+
|
|
1718
|
+
// src/core/usage-budget.ts
|
|
1719
|
+
var UsageBudget = class {
|
|
1720
|
+
constructor(store, config, now = () => /* @__PURE__ */ new Date()) {
|
|
1721
|
+
this.store = store;
|
|
1722
|
+
this.config = config;
|
|
1723
|
+
this.now = now;
|
|
1724
|
+
this.lastNotifiedMonth = this.now().getMonth();
|
|
1725
|
+
}
|
|
1726
|
+
lastNotifiedStatus = "ok";
|
|
1727
|
+
lastNotifiedMonth;
|
|
1728
|
+
check() {
|
|
1729
|
+
if (!this.config.monthlyBudget) {
|
|
1730
|
+
return { status: "ok" };
|
|
1731
|
+
}
|
|
1732
|
+
const currentMonth = this.now().getMonth();
|
|
1733
|
+
if (currentMonth !== this.lastNotifiedMonth) {
|
|
1734
|
+
this.lastNotifiedStatus = "ok";
|
|
1735
|
+
this.lastNotifiedMonth = currentMonth;
|
|
1736
|
+
}
|
|
1737
|
+
const { totalCost } = this.store.getMonthlyTotal();
|
|
1738
|
+
const budget = this.config.monthlyBudget;
|
|
1739
|
+
const threshold = this.config.warningThreshold;
|
|
1740
|
+
let status;
|
|
1741
|
+
if (totalCost >= budget) {
|
|
1742
|
+
status = "exceeded";
|
|
1743
|
+
} else if (totalCost >= threshold * budget) {
|
|
1744
|
+
status = "warning";
|
|
1745
|
+
} else {
|
|
1746
|
+
status = "ok";
|
|
1747
|
+
}
|
|
1748
|
+
let message;
|
|
1749
|
+
if (status !== "ok" && status !== this.lastNotifiedStatus) {
|
|
1750
|
+
const pct = Math.round(totalCost / budget * 100);
|
|
1751
|
+
const filled = Math.round(Math.min(totalCost / budget, 1) * 10);
|
|
1752
|
+
const bar = "\u2593".repeat(filled) + "\u2591".repeat(10 - filled);
|
|
1753
|
+
if (status === "warning") {
|
|
1754
|
+
message = `\u26A0\uFE0F <b>Budget Warning</b>
|
|
1755
|
+
Monthly usage: $${totalCost.toFixed(2)} / $${budget.toFixed(2)} (${pct}%)
|
|
1756
|
+
${bar} ${pct}%`;
|
|
1757
|
+
} else {
|
|
1758
|
+
message = `\u{1F6A8} <b>Budget Exceeded</b>
|
|
1759
|
+
Monthly usage: $${totalCost.toFixed(2)} / $${budget.toFixed(2)} (${pct}%)
|
|
1760
|
+
${bar} ${pct}%
|
|
1761
|
+
Sessions are NOT blocked \u2014 this is a warning only.`;
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
this.lastNotifiedStatus = status;
|
|
1765
|
+
return { status, message };
|
|
1766
|
+
}
|
|
1767
|
+
getStatus() {
|
|
1768
|
+
const { totalCost } = this.store.getMonthlyTotal();
|
|
1769
|
+
const budget = this.config.monthlyBudget ?? 0;
|
|
1770
|
+
let status = "ok";
|
|
1771
|
+
if (budget > 0) {
|
|
1772
|
+
if (totalCost >= budget) {
|
|
1773
|
+
status = "exceeded";
|
|
1774
|
+
} else if (totalCost >= this.config.warningThreshold * budget) {
|
|
1775
|
+
status = "warning";
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
const percent = budget > 0 ? Math.round(totalCost / budget * 100) : 0;
|
|
1779
|
+
return { status, used: totalCost, budget, percent };
|
|
1780
|
+
}
|
|
1781
|
+
};
|
|
1782
|
+
|
|
1783
|
+
// src/core/speech/speech-service.ts
|
|
1784
|
+
var SpeechService = class {
|
|
1785
|
+
constructor(config) {
|
|
1786
|
+
this.config = config;
|
|
1787
|
+
}
|
|
1788
|
+
sttProviders = /* @__PURE__ */ new Map();
|
|
1789
|
+
ttsProviders = /* @__PURE__ */ new Map();
|
|
1790
|
+
registerSTTProvider(name, provider) {
|
|
1791
|
+
this.sttProviders.set(name, provider);
|
|
1792
|
+
}
|
|
1793
|
+
registerTTSProvider(name, provider) {
|
|
1794
|
+
this.ttsProviders.set(name, provider);
|
|
1795
|
+
}
|
|
1796
|
+
isSTTAvailable() {
|
|
1797
|
+
const { provider, providers } = this.config.stt;
|
|
1798
|
+
return provider !== null && providers[provider]?.apiKey !== void 0;
|
|
1799
|
+
}
|
|
1800
|
+
isTTSAvailable() {
|
|
1801
|
+
const { provider, providers } = this.config.tts;
|
|
1802
|
+
return provider !== null && providers[provider]?.apiKey !== void 0;
|
|
1803
|
+
}
|
|
1804
|
+
async transcribe(audioBuffer, mimeType, options) {
|
|
1805
|
+
const providerName = this.config.stt.provider;
|
|
1806
|
+
if (!providerName || !this.config.stt.providers[providerName]?.apiKey) {
|
|
1807
|
+
throw new Error("STT not configured. Set speech.stt.provider and API key in config.");
|
|
1808
|
+
}
|
|
1809
|
+
const provider = this.sttProviders.get(providerName);
|
|
1810
|
+
if (!provider) {
|
|
1811
|
+
throw new Error(`STT provider "${providerName}" not registered. Available: ${[...this.sttProviders.keys()].join(", ") || "none"}`);
|
|
1812
|
+
}
|
|
1813
|
+
return provider.transcribe(audioBuffer, mimeType, options);
|
|
1814
|
+
}
|
|
1815
|
+
async synthesize(text, options) {
|
|
1816
|
+
const providerName = this.config.tts.provider;
|
|
1817
|
+
if (!providerName || !this.config.tts.providers[providerName]?.apiKey) {
|
|
1818
|
+
throw new Error("TTS not configured. Set speech.tts.provider and API key in config.");
|
|
1819
|
+
}
|
|
1820
|
+
const provider = this.ttsProviders.get(providerName);
|
|
1821
|
+
if (!provider) {
|
|
1822
|
+
throw new Error(`TTS provider "${providerName}" not registered. Available: ${[...this.ttsProviders.keys()].join(", ") || "none"}`);
|
|
1823
|
+
}
|
|
1824
|
+
return provider.synthesize(text, options);
|
|
1825
|
+
}
|
|
1826
|
+
updateConfig(config) {
|
|
1827
|
+
this.config = config;
|
|
1828
|
+
}
|
|
1829
|
+
};
|
|
1830
|
+
|
|
1831
|
+
// src/core/speech/providers/groq.ts
|
|
1832
|
+
var GROQ_API_URL = "https://api.groq.com/openai/v1/audio/transcriptions";
|
|
1833
|
+
var GroqSTT = class {
|
|
1834
|
+
constructor(apiKey, defaultModel = "whisper-large-v3-turbo") {
|
|
1835
|
+
this.apiKey = apiKey;
|
|
1836
|
+
this.defaultModel = defaultModel;
|
|
1837
|
+
}
|
|
1838
|
+
name = "groq";
|
|
1839
|
+
async transcribe(audioBuffer, mimeType, options) {
|
|
1840
|
+
const ext = mimeToExt(mimeType);
|
|
1841
|
+
const form = new FormData();
|
|
1842
|
+
form.append("file", new Blob([new Uint8Array(audioBuffer)], { type: mimeType }), `audio${ext}`);
|
|
1843
|
+
form.append("model", options?.model || this.defaultModel);
|
|
1844
|
+
form.append("response_format", "verbose_json");
|
|
1845
|
+
if (options?.language) {
|
|
1846
|
+
form.append("language", options.language);
|
|
1847
|
+
}
|
|
1848
|
+
const resp = await fetch(GROQ_API_URL, {
|
|
1849
|
+
method: "POST",
|
|
1850
|
+
headers: { Authorization: `Bearer ${this.apiKey}` },
|
|
1851
|
+
body: form
|
|
1852
|
+
});
|
|
1853
|
+
if (!resp.ok) {
|
|
1854
|
+
const body = await resp.text();
|
|
1855
|
+
if (resp.status === 401) {
|
|
1856
|
+
throw new Error("Invalid Groq API key. Check your key at console.groq.com.");
|
|
1857
|
+
}
|
|
1858
|
+
if (resp.status === 413) {
|
|
1859
|
+
throw new Error("Audio file too large for Groq API (max 25MB).");
|
|
1860
|
+
}
|
|
1861
|
+
if (resp.status === 429) {
|
|
1862
|
+
throw new Error("Groq rate limit exceeded. Free tier: 28,800 seconds/day. Try again later.");
|
|
1863
|
+
}
|
|
1864
|
+
throw new Error(`Groq STT error (${resp.status}): ${body}`);
|
|
1865
|
+
}
|
|
1866
|
+
const data = await resp.json();
|
|
1867
|
+
return {
|
|
1868
|
+
text: data.text,
|
|
1869
|
+
language: data.language,
|
|
1870
|
+
duration: data.duration
|
|
1871
|
+
};
|
|
1872
|
+
}
|
|
1873
|
+
};
|
|
1874
|
+
function mimeToExt(mimeType) {
|
|
1875
|
+
const map = {
|
|
1876
|
+
"audio/ogg": ".ogg",
|
|
1877
|
+
"audio/wav": ".wav",
|
|
1878
|
+
"audio/mpeg": ".mp3",
|
|
1879
|
+
"audio/mp4": ".m4a",
|
|
1880
|
+
"audio/webm": ".webm",
|
|
1881
|
+
"audio/flac": ".flac"
|
|
1882
|
+
};
|
|
1883
|
+
return map[mimeType] || ".bin";
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1512
1886
|
// src/core/core.ts
|
|
1513
|
-
import
|
|
1887
|
+
import path5 from "path";
|
|
1514
1888
|
import os from "os";
|
|
1515
1889
|
|
|
1516
1890
|
// src/core/session-store.ts
|
|
1517
|
-
import
|
|
1518
|
-
import
|
|
1519
|
-
var
|
|
1520
|
-
var
|
|
1891
|
+
import fs5 from "fs";
|
|
1892
|
+
import path4 from "path";
|
|
1893
|
+
var log5 = createChildLogger({ module: "session-store" });
|
|
1894
|
+
var DEBOUNCE_MS2 = 2e3;
|
|
1521
1895
|
var JsonFileSessionStore = class {
|
|
1522
1896
|
records = /* @__PURE__ */ new Map();
|
|
1523
1897
|
filePath;
|
|
@@ -1577,9 +1951,9 @@ var JsonFileSessionStore = class {
|
|
|
1577
1951
|
version: 1,
|
|
1578
1952
|
sessions: Object.fromEntries(this.records)
|
|
1579
1953
|
};
|
|
1580
|
-
const dir =
|
|
1581
|
-
if (!
|
|
1582
|
-
|
|
1954
|
+
const dir = path4.dirname(this.filePath);
|
|
1955
|
+
if (!fs5.existsSync(dir)) fs5.mkdirSync(dir, { recursive: true });
|
|
1956
|
+
fs5.writeFileSync(this.filePath, JSON.stringify(data, null, 2));
|
|
1583
1957
|
}
|
|
1584
1958
|
destroy() {
|
|
1585
1959
|
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
@@ -1592,13 +1966,13 @@ var JsonFileSessionStore = class {
|
|
|
1592
1966
|
}
|
|
1593
1967
|
}
|
|
1594
1968
|
load() {
|
|
1595
|
-
if (!
|
|
1969
|
+
if (!fs5.existsSync(this.filePath)) return;
|
|
1596
1970
|
try {
|
|
1597
1971
|
const raw = JSON.parse(
|
|
1598
|
-
|
|
1972
|
+
fs5.readFileSync(this.filePath, "utf-8")
|
|
1599
1973
|
);
|
|
1600
1974
|
if (raw.version !== 1) {
|
|
1601
|
-
|
|
1975
|
+
log5.warn(
|
|
1602
1976
|
{ version: raw.version },
|
|
1603
1977
|
"Unknown session store version, skipping load"
|
|
1604
1978
|
);
|
|
@@ -1607,9 +1981,9 @@ var JsonFileSessionStore = class {
|
|
|
1607
1981
|
for (const [id, record] of Object.entries(raw.sessions)) {
|
|
1608
1982
|
this.records.set(id, record);
|
|
1609
1983
|
}
|
|
1610
|
-
|
|
1984
|
+
log5.info({ count: this.records.size }, "Loaded session records");
|
|
1611
1985
|
} catch (err) {
|
|
1612
|
-
|
|
1986
|
+
log5.error({ err }, "Failed to load session store");
|
|
1613
1987
|
}
|
|
1614
1988
|
}
|
|
1615
1989
|
cleanup() {
|
|
@@ -1625,7 +1999,7 @@ var JsonFileSessionStore = class {
|
|
|
1625
1999
|
}
|
|
1626
2000
|
}
|
|
1627
2001
|
if (removed > 0) {
|
|
1628
|
-
|
|
2002
|
+
log5.info({ removed }, "Cleaned up expired session records");
|
|
1629
2003
|
this.scheduleDiskWrite();
|
|
1630
2004
|
}
|
|
1631
2005
|
}
|
|
@@ -1633,12 +2007,13 @@ var JsonFileSessionStore = class {
|
|
|
1633
2007
|
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
1634
2008
|
this.debounceTimer = setTimeout(() => {
|
|
1635
2009
|
this.flushSync();
|
|
1636
|
-
},
|
|
2010
|
+
}, DEBOUNCE_MS2);
|
|
1637
2011
|
}
|
|
1638
2012
|
};
|
|
1639
2013
|
|
|
1640
2014
|
// src/core/core.ts
|
|
1641
|
-
|
|
2015
|
+
import { nanoid as nanoid2 } from "nanoid";
|
|
2016
|
+
var log6 = createChildLogger({ module: "core" });
|
|
1642
2017
|
var OpenACPCore = class {
|
|
1643
2018
|
configManager;
|
|
1644
2019
|
agentCatalog;
|
|
@@ -1647,32 +2022,57 @@ var OpenACPCore = class {
|
|
|
1647
2022
|
notificationManager;
|
|
1648
2023
|
messageTransformer;
|
|
1649
2024
|
fileService;
|
|
2025
|
+
speechService;
|
|
1650
2026
|
adapters = /* @__PURE__ */ new Map();
|
|
1651
2027
|
/** Set by main.ts — triggers graceful shutdown with restart exit code */
|
|
1652
2028
|
requestRestart = null;
|
|
1653
2029
|
_tunnelService;
|
|
1654
2030
|
sessionStore = null;
|
|
1655
2031
|
resumeLocks = /* @__PURE__ */ new Map();
|
|
2032
|
+
usageStore = null;
|
|
2033
|
+
usageBudget = null;
|
|
1656
2034
|
constructor(configManager) {
|
|
1657
2035
|
this.configManager = configManager;
|
|
1658
2036
|
const config = configManager.get();
|
|
1659
2037
|
this.agentCatalog = new AgentCatalog();
|
|
1660
2038
|
this.agentCatalog.load();
|
|
1661
2039
|
this.agentManager = new AgentManager(this.agentCatalog);
|
|
1662
|
-
const storePath =
|
|
2040
|
+
const storePath = path5.join(os.homedir(), ".openacp", "sessions.json");
|
|
1663
2041
|
this.sessionStore = new JsonFileSessionStore(
|
|
1664
2042
|
storePath,
|
|
1665
2043
|
config.sessionStore.ttlDays
|
|
1666
2044
|
);
|
|
1667
2045
|
this.sessionManager = new SessionManager(this.sessionStore);
|
|
1668
2046
|
this.notificationManager = new NotificationManager(this.adapters);
|
|
2047
|
+
const usageConfig = config.usage;
|
|
2048
|
+
if (usageConfig.enabled) {
|
|
2049
|
+
const usagePath = path5.join(os.homedir(), ".openacp", "usage.json");
|
|
2050
|
+
this.usageStore = new UsageStore(usagePath, usageConfig.retentionDays);
|
|
2051
|
+
this.usageBudget = new UsageBudget(this.usageStore, usageConfig);
|
|
2052
|
+
}
|
|
1669
2053
|
this.messageTransformer = new MessageTransformer();
|
|
1670
|
-
this.fileService = new FileService(
|
|
2054
|
+
this.fileService = new FileService(path5.join(os.homedir(), ".openacp", "files"));
|
|
2055
|
+
const speechConfig = config.speech ?? { stt: { provider: null, providers: {} }, tts: { provider: null, providers: {} } };
|
|
2056
|
+
this.speechService = new SpeechService(speechConfig);
|
|
2057
|
+
const groqConfig = speechConfig.stt?.providers?.groq;
|
|
2058
|
+
if (groqConfig?.apiKey) {
|
|
2059
|
+
this.speechService.registerSTTProvider("groq", new GroqSTT(groqConfig.apiKey, groqConfig.model));
|
|
2060
|
+
}
|
|
1671
2061
|
this.configManager.on("config:changed", async ({ path: configPath, value }) => {
|
|
1672
2062
|
if (configPath === "logging.level" && typeof value === "string") {
|
|
1673
2063
|
const { setLogLevel: setLogLevel2 } = await import("./log-SPS2S6FO.js");
|
|
1674
2064
|
setLogLevel2(value);
|
|
1675
|
-
|
|
2065
|
+
log6.info({ level: value }, "Log level changed at runtime");
|
|
2066
|
+
}
|
|
2067
|
+
if (configPath.startsWith("speech.")) {
|
|
2068
|
+
const newConfig = this.configManager.get();
|
|
2069
|
+
const newSpeechConfig = newConfig.speech ?? { stt: { provider: null, providers: {} }, tts: { provider: null, providers: {} } };
|
|
2070
|
+
this.speechService.updateConfig(newSpeechConfig);
|
|
2071
|
+
const groqCfg = newSpeechConfig.stt?.providers?.groq;
|
|
2072
|
+
if (groqCfg?.apiKey) {
|
|
2073
|
+
this.speechService.registerSTTProvider("groq", new GroqSTT(groqCfg.apiKey, groqCfg.model));
|
|
2074
|
+
}
|
|
2075
|
+
log6.info("Speech service config updated at runtime");
|
|
1676
2076
|
}
|
|
1677
2077
|
});
|
|
1678
2078
|
}
|
|
@@ -1688,7 +2088,7 @@ var OpenACPCore = class {
|
|
|
1688
2088
|
}
|
|
1689
2089
|
async start() {
|
|
1690
2090
|
this.agentCatalog.refreshRegistryIfStale().catch((err) => {
|
|
1691
|
-
|
|
2091
|
+
log6.warn({ err }, "Background registry refresh failed");
|
|
1692
2092
|
});
|
|
1693
2093
|
for (const adapter of this.adapters.values()) {
|
|
1694
2094
|
await adapter.start();
|
|
@@ -1707,11 +2107,30 @@ var OpenACPCore = class {
|
|
|
1707
2107
|
for (const adapter of this.adapters.values()) {
|
|
1708
2108
|
await adapter.stop();
|
|
1709
2109
|
}
|
|
2110
|
+
if (this.usageStore) {
|
|
2111
|
+
this.usageStore.destroy();
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
// --- Archive ---
|
|
2115
|
+
async archiveSession(sessionId) {
|
|
2116
|
+
const session = this.sessionManager.getSession(sessionId);
|
|
2117
|
+
if (!session) return { ok: false, error: "Session not found" };
|
|
2118
|
+
if (session.status === "initializing") return { ok: false, error: "Session is still initializing" };
|
|
2119
|
+
if (session.status !== "active") return { ok: false, error: `Session is ${session.status}` };
|
|
2120
|
+
const adapter = this.adapters.get(session.channelId);
|
|
2121
|
+
if (!adapter) return { ok: false, error: "Adapter not found for session" };
|
|
2122
|
+
try {
|
|
2123
|
+
const result = await adapter.archiveSessionTopic(session.id);
|
|
2124
|
+
if (!result) return { ok: false, error: "Adapter does not support archiving" };
|
|
2125
|
+
return { ok: true, newThreadId: result.newThreadId };
|
|
2126
|
+
} catch (err) {
|
|
2127
|
+
return { ok: false, error: err.message };
|
|
2128
|
+
}
|
|
1710
2129
|
}
|
|
1711
2130
|
// --- Message Routing ---
|
|
1712
2131
|
async handleMessage(message) {
|
|
1713
2132
|
const config = this.configManager.get();
|
|
1714
|
-
|
|
2133
|
+
log6.debug(
|
|
1715
2134
|
{
|
|
1716
2135
|
channelId: message.channelId,
|
|
1717
2136
|
threadId: message.threadId,
|
|
@@ -1722,7 +2141,7 @@ var OpenACPCore = class {
|
|
|
1722
2141
|
if (config.security.allowedUserIds.length > 0) {
|
|
1723
2142
|
const userId = String(message.userId);
|
|
1724
2143
|
if (!config.security.allowedUserIds.includes(userId)) {
|
|
1725
|
-
|
|
2144
|
+
log6.warn(
|
|
1726
2145
|
{ userId },
|
|
1727
2146
|
"Rejected message from unauthorized user"
|
|
1728
2147
|
);
|
|
@@ -1731,7 +2150,7 @@ var OpenACPCore = class {
|
|
|
1731
2150
|
}
|
|
1732
2151
|
const activeSessions = this.sessionManager.listSessions().filter((s) => s.status === "active" || s.status === "initializing");
|
|
1733
2152
|
if (activeSessions.length >= config.security.maxConcurrentSessions) {
|
|
1734
|
-
|
|
2153
|
+
log6.warn(
|
|
1735
2154
|
{
|
|
1736
2155
|
userId: message.userId,
|
|
1737
2156
|
currentCount: activeSessions.length,
|
|
@@ -1756,7 +2175,7 @@ var OpenACPCore = class {
|
|
|
1756
2175
|
session = await this.lazyResume(message) ?? void 0;
|
|
1757
2176
|
}
|
|
1758
2177
|
if (!session) {
|
|
1759
|
-
|
|
2178
|
+
log6.warn(
|
|
1760
2179
|
{ channelId: message.channelId, threadId: message.threadId },
|
|
1761
2180
|
"No session found for thread (in-memory miss + lazy resume returned null)"
|
|
1762
2181
|
);
|
|
@@ -1780,7 +2199,8 @@ var OpenACPCore = class {
|
|
|
1780
2199
|
channelId: params.channelId,
|
|
1781
2200
|
agentName: params.agentName,
|
|
1782
2201
|
workingDirectory: params.workingDirectory,
|
|
1783
|
-
agentInstance
|
|
2202
|
+
agentInstance,
|
|
2203
|
+
speechService: this.speechService
|
|
1784
2204
|
});
|
|
1785
2205
|
session.agentSessionId = agentInstance.sessionId;
|
|
1786
2206
|
if (params.initialName) {
|
|
@@ -1799,6 +2219,32 @@ var OpenACPCore = class {
|
|
|
1799
2219
|
const bridge = this.createBridge(session, adapter);
|
|
1800
2220
|
bridge.connect();
|
|
1801
2221
|
}
|
|
2222
|
+
if (this.usageStore) {
|
|
2223
|
+
session.on("agent_event", (event) => {
|
|
2224
|
+
if (event.type !== "usage") return;
|
|
2225
|
+
const record = {
|
|
2226
|
+
id: nanoid2(),
|
|
2227
|
+
sessionId: session.id,
|
|
2228
|
+
agentName: session.agentName,
|
|
2229
|
+
tokensUsed: event.tokensUsed ?? 0,
|
|
2230
|
+
contextSize: event.contextSize ?? 0,
|
|
2231
|
+
cost: event.cost,
|
|
2232
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2233
|
+
};
|
|
2234
|
+
this.usageStore.append(record);
|
|
2235
|
+
if (this.usageBudget) {
|
|
2236
|
+
const result = this.usageBudget.check();
|
|
2237
|
+
if (result.message) {
|
|
2238
|
+
this.notificationManager.notifyAll({
|
|
2239
|
+
sessionId: session.id,
|
|
2240
|
+
sessionName: session.name,
|
|
2241
|
+
type: "budget_warning",
|
|
2242
|
+
summary: result.message
|
|
2243
|
+
});
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
});
|
|
2247
|
+
}
|
|
1802
2248
|
session.on("status_change", (_from, to) => {
|
|
1803
2249
|
if ((to === "finished" || to === "cancelled") && this._tunnelService) {
|
|
1804
2250
|
this._tunnelService.stopBySession(session.id).then((stopped) => {
|
|
@@ -1838,7 +2284,7 @@ var OpenACPCore = class {
|
|
|
1838
2284
|
name: session.name,
|
|
1839
2285
|
platform
|
|
1840
2286
|
});
|
|
1841
|
-
|
|
2287
|
+
log6.info(
|
|
1842
2288
|
{ sessionId: session.id, agentName: params.agentName },
|
|
1843
2289
|
"Session created via pipeline"
|
|
1844
2290
|
);
|
|
@@ -1847,7 +2293,7 @@ var OpenACPCore = class {
|
|
|
1847
2293
|
async handleNewSession(channelId, agentName, workspacePath) {
|
|
1848
2294
|
const config = this.configManager.get();
|
|
1849
2295
|
const resolvedAgent = agentName || config.defaultAgent;
|
|
1850
|
-
|
|
2296
|
+
log6.info({ channelId, agentName: resolvedAgent }, "New session request");
|
|
1851
2297
|
const agentDef = this.agentCatalog.resolve(resolvedAgent);
|
|
1852
2298
|
const resolvedWorkspace = this.configManager.resolveWorkspace(
|
|
1853
2299
|
workspacePath || agentDef?.workingDirectory
|
|
@@ -1962,20 +2408,20 @@ var OpenACPCore = class {
|
|
|
1962
2408
|
(p) => String(p.topicId) === message.threadId
|
|
1963
2409
|
);
|
|
1964
2410
|
if (!record) {
|
|
1965
|
-
|
|
2411
|
+
log6.debug(
|
|
1966
2412
|
{ threadId: message.threadId, channelId: message.channelId },
|
|
1967
2413
|
"No session record found for thread"
|
|
1968
2414
|
);
|
|
1969
2415
|
return null;
|
|
1970
2416
|
}
|
|
1971
2417
|
if (record.status === "error") {
|
|
1972
|
-
|
|
2418
|
+
log6.debug(
|
|
1973
2419
|
{ threadId: message.threadId, sessionId: record.sessionId, status: record.status },
|
|
1974
2420
|
"Skipping resume of error session"
|
|
1975
2421
|
);
|
|
1976
2422
|
return null;
|
|
1977
2423
|
}
|
|
1978
|
-
|
|
2424
|
+
log6.info(
|
|
1979
2425
|
{ threadId: message.threadId, sessionId: record.sessionId, status: record.status },
|
|
1980
2426
|
"Lazy resume: found record, attempting resume"
|
|
1981
2427
|
);
|
|
@@ -1992,13 +2438,13 @@ var OpenACPCore = class {
|
|
|
1992
2438
|
session.threadId = message.threadId;
|
|
1993
2439
|
session.activate();
|
|
1994
2440
|
session.dangerousMode = record.dangerousMode ?? false;
|
|
1995
|
-
|
|
2441
|
+
log6.info(
|
|
1996
2442
|
{ sessionId: session.id, threadId: message.threadId },
|
|
1997
2443
|
"Lazy resume successful"
|
|
1998
2444
|
);
|
|
1999
2445
|
return session;
|
|
2000
2446
|
} catch (err) {
|
|
2001
|
-
|
|
2447
|
+
log6.error({ err, record }, "Lazy resume failed");
|
|
2002
2448
|
const adapter = this.adapters.get(message.channelId);
|
|
2003
2449
|
if (adapter) {
|
|
2004
2450
|
try {
|
|
@@ -2031,20 +2477,20 @@ var OpenACPCore = class {
|
|
|
2031
2477
|
|
|
2032
2478
|
// src/core/api-server.ts
|
|
2033
2479
|
import * as http from "http";
|
|
2034
|
-
import * as
|
|
2035
|
-
import * as
|
|
2480
|
+
import * as fs6 from "fs";
|
|
2481
|
+
import * as path6 from "path";
|
|
2036
2482
|
import * as os2 from "os";
|
|
2037
2483
|
import * as crypto from "crypto";
|
|
2038
2484
|
import { fileURLToPath } from "url";
|
|
2039
|
-
var
|
|
2040
|
-
var DEFAULT_PORT_FILE =
|
|
2485
|
+
var log7 = createChildLogger({ module: "api-server" });
|
|
2486
|
+
var DEFAULT_PORT_FILE = path6.join(os2.homedir(), ".openacp", "api.port");
|
|
2041
2487
|
var cachedVersion;
|
|
2042
2488
|
function getVersion() {
|
|
2043
2489
|
if (cachedVersion) return cachedVersion;
|
|
2044
2490
|
try {
|
|
2045
2491
|
const __filename = fileURLToPath(import.meta.url);
|
|
2046
|
-
const pkgPath =
|
|
2047
|
-
const pkg = JSON.parse(
|
|
2492
|
+
const pkgPath = path6.resolve(path6.dirname(__filename), "../../package.json");
|
|
2493
|
+
const pkg = JSON.parse(fs6.readFileSync(pkgPath, "utf-8"));
|
|
2048
2494
|
cachedVersion = pkg.version ?? "0.0.0-dev";
|
|
2049
2495
|
} catch {
|
|
2050
2496
|
cachedVersion = "0.0.0-dev";
|
|
@@ -2072,7 +2518,7 @@ var ApiServer = class {
|
|
|
2072
2518
|
this.config = config;
|
|
2073
2519
|
this.topicManager = topicManager;
|
|
2074
2520
|
this.portFilePath = portFilePath ?? DEFAULT_PORT_FILE;
|
|
2075
|
-
this.secretFilePath = secretFilePath ??
|
|
2521
|
+
this.secretFilePath = secretFilePath ?? path6.join(os2.homedir(), ".openacp", "api-secret");
|
|
2076
2522
|
}
|
|
2077
2523
|
server = null;
|
|
2078
2524
|
actualPort = 0;
|
|
@@ -2086,7 +2532,7 @@ var ApiServer = class {
|
|
|
2086
2532
|
await new Promise((resolve2, reject) => {
|
|
2087
2533
|
this.server.on("error", (err) => {
|
|
2088
2534
|
if (err.code === "EADDRINUSE") {
|
|
2089
|
-
|
|
2535
|
+
log7.warn({ port: this.config.port }, "API port in use, continuing without API server");
|
|
2090
2536
|
this.server = null;
|
|
2091
2537
|
resolve2();
|
|
2092
2538
|
} else {
|
|
@@ -2099,7 +2545,7 @@ var ApiServer = class {
|
|
|
2099
2545
|
this.actualPort = addr.port;
|
|
2100
2546
|
}
|
|
2101
2547
|
this.writePortFile();
|
|
2102
|
-
|
|
2548
|
+
log7.info({ host: this.config.host, port: this.actualPort }, "API server listening");
|
|
2103
2549
|
resolve2();
|
|
2104
2550
|
});
|
|
2105
2551
|
});
|
|
@@ -2117,27 +2563,27 @@ var ApiServer = class {
|
|
|
2117
2563
|
return this.actualPort;
|
|
2118
2564
|
}
|
|
2119
2565
|
writePortFile() {
|
|
2120
|
-
const dir =
|
|
2121
|
-
|
|
2122
|
-
|
|
2566
|
+
const dir = path6.dirname(this.portFilePath);
|
|
2567
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
2568
|
+
fs6.writeFileSync(this.portFilePath, String(this.actualPort));
|
|
2123
2569
|
}
|
|
2124
2570
|
removePortFile() {
|
|
2125
2571
|
try {
|
|
2126
|
-
|
|
2572
|
+
fs6.unlinkSync(this.portFilePath);
|
|
2127
2573
|
} catch {
|
|
2128
2574
|
}
|
|
2129
2575
|
}
|
|
2130
2576
|
loadOrCreateSecret() {
|
|
2131
|
-
const dir =
|
|
2132
|
-
|
|
2577
|
+
const dir = path6.dirname(this.secretFilePath);
|
|
2578
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
2133
2579
|
try {
|
|
2134
|
-
this.secret =
|
|
2580
|
+
this.secret = fs6.readFileSync(this.secretFilePath, "utf-8").trim();
|
|
2135
2581
|
if (this.secret) {
|
|
2136
2582
|
try {
|
|
2137
|
-
const stat =
|
|
2583
|
+
const stat = fs6.statSync(this.secretFilePath);
|
|
2138
2584
|
const mode = stat.mode & 511;
|
|
2139
2585
|
if (mode & 63) {
|
|
2140
|
-
|
|
2586
|
+
log7.warn({ path: this.secretFilePath, mode: "0" + mode.toString(8) }, "API secret file has insecure permissions (should be 0600). Run: chmod 600 %s", this.secretFilePath);
|
|
2141
2587
|
}
|
|
2142
2588
|
} catch {
|
|
2143
2589
|
}
|
|
@@ -2146,7 +2592,7 @@ var ApiServer = class {
|
|
|
2146
2592
|
} catch {
|
|
2147
2593
|
}
|
|
2148
2594
|
this.secret = crypto.randomBytes(32).toString("hex");
|
|
2149
|
-
|
|
2595
|
+
fs6.writeFileSync(this.secretFilePath, this.secret, { mode: 384 });
|
|
2150
2596
|
}
|
|
2151
2597
|
authenticate(req) {
|
|
2152
2598
|
const authHeader = req.headers.authorization;
|
|
@@ -2177,6 +2623,9 @@ var ApiServer = class {
|
|
|
2177
2623
|
} else if (method === "GET" && url.match(/^\/api\/sessions\/([^/]+)$/)) {
|
|
2178
2624
|
const sessionId = decodeURIComponent(url.match(/^\/api\/sessions\/([^/]+)$/)[1]);
|
|
2179
2625
|
await this.handleGetSession(sessionId, res);
|
|
2626
|
+
} else if (method === "POST" && url.match(/^\/api\/sessions\/([^/]+)\/archive$/)) {
|
|
2627
|
+
const sessionId = decodeURIComponent(url.match(/^\/api\/sessions\/([^/]+)\/archive$/)[1]);
|
|
2628
|
+
await this.handleArchiveSession(sessionId, res);
|
|
2180
2629
|
} else if (method === "DELETE" && url.match(/^\/api\/sessions\/([^/]+)$/)) {
|
|
2181
2630
|
const sessionId = decodeURIComponent(url.match(/^\/api\/sessions\/([^/]+)$/)[1]);
|
|
2182
2631
|
await this.handleCancelSession(sessionId, res);
|
|
@@ -2222,7 +2671,7 @@ var ApiServer = class {
|
|
|
2222
2671
|
this.sendJson(res, 404, { error: "Not found" });
|
|
2223
2672
|
}
|
|
2224
2673
|
} catch (err) {
|
|
2225
|
-
|
|
2674
|
+
log7.error({ err }, "API request error");
|
|
2226
2675
|
this.sendJson(res, 500, { error: "Internal server error" });
|
|
2227
2676
|
}
|
|
2228
2677
|
}
|
|
@@ -2264,11 +2713,11 @@ var ApiServer = class {
|
|
|
2264
2713
|
if (!adapter) {
|
|
2265
2714
|
session.agentInstance.onPermissionRequest = async (request) => {
|
|
2266
2715
|
const allowOption = request.options.find((o) => o.isAllow);
|
|
2267
|
-
|
|
2716
|
+
log7.debug({ sessionId: session.id, permissionId: request.id, option: allowOption?.id }, "Auto-approving permission for API session");
|
|
2268
2717
|
return allowOption?.id ?? request.options[0]?.id ?? "";
|
|
2269
2718
|
};
|
|
2270
2719
|
}
|
|
2271
|
-
session.warmup().catch((err) =>
|
|
2720
|
+
session.warmup().catch((err) => log7.warn({ err, sessionId: session.id }, "API session warmup failed"));
|
|
2272
2721
|
this.sendJson(res, 200, {
|
|
2273
2722
|
sessionId: session.id,
|
|
2274
2723
|
agent: session.agentName,
|
|
@@ -2379,7 +2828,7 @@ var ApiServer = class {
|
|
|
2379
2828
|
this.sendJson(res, 200, { version: getVersion() });
|
|
2380
2829
|
}
|
|
2381
2830
|
async handleGetEditableConfig(res) {
|
|
2382
|
-
const { getSafeFields: getSafeFields2, resolveOptions: resolveOptions2, getConfigValue: getConfigValue2 } = await import("./config-registry-
|
|
2831
|
+
const { getSafeFields: getSafeFields2, resolveOptions: resolveOptions2, getConfigValue: getConfigValue2 } = await import("./config-registry-QQOJ2GQP.js");
|
|
2383
2832
|
const config = this.core.configManager.get();
|
|
2384
2833
|
const safeFields = getSafeFields2();
|
|
2385
2834
|
const fields = safeFields.map((def) => ({
|
|
@@ -2420,20 +2869,20 @@ var ApiServer = class {
|
|
|
2420
2869
|
const parts = configPath.split(".");
|
|
2421
2870
|
let target = cloned;
|
|
2422
2871
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
2423
|
-
|
|
2424
|
-
|
|
2872
|
+
const part = parts[i];
|
|
2873
|
+
if (target[part] && typeof target[part] === "object" && !Array.isArray(target[part])) {
|
|
2874
|
+
target = target[part];
|
|
2875
|
+
} else if (target[part] === void 0 || target[part] === null) {
|
|
2876
|
+
target[part] = {};
|
|
2877
|
+
target = target[part];
|
|
2425
2878
|
} else {
|
|
2426
2879
|
this.sendJson(res, 400, { error: "Invalid config path" });
|
|
2427
2880
|
return;
|
|
2428
2881
|
}
|
|
2429
2882
|
}
|
|
2430
2883
|
const lastKey = parts[parts.length - 1];
|
|
2431
|
-
if (!(lastKey in target)) {
|
|
2432
|
-
this.sendJson(res, 400, { error: "Invalid config path" });
|
|
2433
|
-
return;
|
|
2434
|
-
}
|
|
2435
2884
|
target[lastKey] = value;
|
|
2436
|
-
const { ConfigSchema } = await import("./config-
|
|
2885
|
+
const { ConfigSchema } = await import("./config-AK2W3E67.js");
|
|
2437
2886
|
const result = ConfigSchema.safeParse(cloned);
|
|
2438
2887
|
if (!result.success) {
|
|
2439
2888
|
this.sendJson(res, 400, {
|
|
@@ -2450,7 +2899,7 @@ var ApiServer = class {
|
|
|
2450
2899
|
}
|
|
2451
2900
|
updateTarget[lastKey] = value;
|
|
2452
2901
|
await this.core.configManager.save(updates, configPath);
|
|
2453
|
-
const { isHotReloadable: isHotReloadable2 } = await import("./config-registry-
|
|
2902
|
+
const { isHotReloadable: isHotReloadable2 } = await import("./config-registry-QQOJ2GQP.js");
|
|
2454
2903
|
const needsRestart = !isHotReloadable2(configPath);
|
|
2455
2904
|
this.sendJson(res, 200, {
|
|
2456
2905
|
ok: true,
|
|
@@ -2558,6 +3007,14 @@ var ApiServer = class {
|
|
|
2558
3007
|
this.sendJson(res, 200, { ok: true, message: "Restarting..." });
|
|
2559
3008
|
setImmediate(() => this.core.requestRestart());
|
|
2560
3009
|
}
|
|
3010
|
+
async handleArchiveSession(sessionId, res) {
|
|
3011
|
+
const result = await this.core.archiveSession(sessionId);
|
|
3012
|
+
if (result.ok) {
|
|
3013
|
+
this.sendJson(res, 200, result);
|
|
3014
|
+
} else {
|
|
3015
|
+
this.sendJson(res, 400, result);
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
2561
3018
|
async handleCancelSession(sessionId, res) {
|
|
2562
3019
|
const session = this.core.sessionManager.getSession(sessionId);
|
|
2563
3020
|
if (!session) {
|
|
@@ -2673,7 +3130,7 @@ var ApiServer = class {
|
|
|
2673
3130
|
};
|
|
2674
3131
|
|
|
2675
3132
|
// src/core/topic-manager.ts
|
|
2676
|
-
var
|
|
3133
|
+
var log8 = createChildLogger({ module: "topic-manager" });
|
|
2677
3134
|
var TopicManager = class {
|
|
2678
3135
|
constructor(sessionManager, adapter, systemTopicIds) {
|
|
2679
3136
|
this.sessionManager = sessionManager;
|
|
@@ -2712,7 +3169,7 @@ var TopicManager = class {
|
|
|
2712
3169
|
try {
|
|
2713
3170
|
await this.adapter.deleteSessionThread(sessionId);
|
|
2714
3171
|
} catch (err) {
|
|
2715
|
-
|
|
3172
|
+
log8.warn({ err, sessionId, topicId }, "Failed to delete platform thread, removing record anyway");
|
|
2716
3173
|
}
|
|
2717
3174
|
}
|
|
2718
3175
|
await this.sessionManager.removeRecord(sessionId);
|
|
@@ -2735,7 +3192,7 @@ var TopicManager = class {
|
|
|
2735
3192
|
try {
|
|
2736
3193
|
await this.adapter.deleteSessionThread(record.sessionId);
|
|
2737
3194
|
} catch (err) {
|
|
2738
|
-
|
|
3195
|
+
log8.warn({ err, sessionId: record.sessionId }, "Failed to delete platform thread during cleanup");
|
|
2739
3196
|
}
|
|
2740
3197
|
}
|
|
2741
3198
|
await this.sessionManager.removeRecord(record.sessionId);
|
|
@@ -2782,6 +3239,9 @@ async function renameSessionTopic(bot, chatId, threadId, name) {
|
|
|
2782
3239
|
} catch {
|
|
2783
3240
|
}
|
|
2784
3241
|
}
|
|
3242
|
+
async function deleteSessionTopic(bot, chatId, threadId) {
|
|
3243
|
+
await bot.api.deleteForumTopic(chatId, threadId);
|
|
3244
|
+
}
|
|
2785
3245
|
function buildDeepLink(chatId, messageId) {
|
|
2786
3246
|
const cleanId = String(chatId).replace("-100", "");
|
|
2787
3247
|
return `https://t.me/c/${cleanId}/${messageId}`;
|
|
@@ -2920,6 +3380,33 @@ function formatUsage(usage) {
|
|
|
2920
3380
|
return `${emoji} ${formatTokens(tokensUsed)} / ${formatTokens(contextSize)} tokens
|
|
2921
3381
|
${bar} ${pct}%`;
|
|
2922
3382
|
}
|
|
3383
|
+
var PERIOD_LABEL = {
|
|
3384
|
+
today: "Today",
|
|
3385
|
+
week: "This Week",
|
|
3386
|
+
month: "This Month",
|
|
3387
|
+
all: "All Time"
|
|
3388
|
+
};
|
|
3389
|
+
function formatUsageReport(summaries, budgetStatus) {
|
|
3390
|
+
const hasData = summaries.some((s) => s.recordCount > 0);
|
|
3391
|
+
if (!hasData) {
|
|
3392
|
+
return "\u{1F4CA} <b>Usage Report</b>\n\nNo usage data yet.";
|
|
3393
|
+
}
|
|
3394
|
+
const formatCost = (n) => `$${n.toFixed(2)}`;
|
|
3395
|
+
const lines = ["\u{1F4CA} <b>Usage Report</b>"];
|
|
3396
|
+
for (const summary of summaries) {
|
|
3397
|
+
lines.push("");
|
|
3398
|
+
lines.push(`\u2500\u2500 <b>${PERIOD_LABEL[summary.period] ?? summary.period}</b> \u2500\u2500`);
|
|
3399
|
+
lines.push(
|
|
3400
|
+
`\u{1F4B0} ${formatCost(summary.totalCost)} \xB7 \u{1F524} ${formatTokens(summary.totalTokens)} tokens \xB7 \u{1F4CB} ${summary.sessionCount} sessions`
|
|
3401
|
+
);
|
|
3402
|
+
if (summary.period === "month" && budgetStatus.budget > 0) {
|
|
3403
|
+
const bar = progressBar(budgetStatus.used / budgetStatus.budget);
|
|
3404
|
+
lines.push(`Budget: ${formatCost(budgetStatus.used)} / ${formatCost(budgetStatus.budget)} (${budgetStatus.percent}%)`);
|
|
3405
|
+
lines.push(`${bar} ${budgetStatus.percent}%`);
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
return lines.join("\n");
|
|
3409
|
+
}
|
|
2923
3410
|
function splitMessage(text, maxLength = 3800) {
|
|
2924
3411
|
if (text.length <= maxLength) return [text];
|
|
2925
3412
|
const chunks = [];
|
|
@@ -2955,7 +3442,7 @@ function splitMessage(text, maxLength = 3800) {
|
|
|
2955
3442
|
|
|
2956
3443
|
// src/adapters/telegram/commands/admin.ts
|
|
2957
3444
|
import { InlineKeyboard } from "grammy";
|
|
2958
|
-
var
|
|
3445
|
+
var log10 = createChildLogger({ module: "telegram-cmd-admin" });
|
|
2959
3446
|
function buildDangerousModeKeyboard(sessionId, enabled) {
|
|
2960
3447
|
return new InlineKeyboard().text(
|
|
2961
3448
|
enabled ? "\u{1F510} Disable Dangerous Mode" : "\u2620\uFE0F Enable Dangerous Mode",
|
|
@@ -2968,7 +3455,7 @@ function setupDangerousModeCallbacks(bot, core) {
|
|
|
2968
3455
|
const session = core.sessionManager.getSession(sessionId);
|
|
2969
3456
|
if (session) {
|
|
2970
3457
|
session.dangerousMode = !session.dangerousMode;
|
|
2971
|
-
|
|
3458
|
+
log10.info({ sessionId, dangerousMode: session.dangerousMode }, "Dangerous mode toggled via button");
|
|
2972
3459
|
core.sessionManager.patchRecord(sessionId, { dangerousMode: session.dangerousMode }).catch(() => {
|
|
2973
3460
|
});
|
|
2974
3461
|
const toastText2 = session.dangerousMode ? "\u2620\uFE0F Dangerous mode enabled \u2014 permissions auto-approved" : "\u{1F510} Dangerous mode disabled \u2014 permissions shown normally";
|
|
@@ -2995,7 +3482,7 @@ function setupDangerousModeCallbacks(bot, core) {
|
|
|
2995
3482
|
const newDangerousMode = !(record.dangerousMode ?? false);
|
|
2996
3483
|
core.sessionManager.patchRecord(sessionId, { dangerousMode: newDangerousMode }).catch(() => {
|
|
2997
3484
|
});
|
|
2998
|
-
|
|
3485
|
+
log10.info({ sessionId, dangerousMode: newDangerousMode }, "Dangerous mode toggled via button (store-only, session not in memory)");
|
|
2999
3486
|
const toastText = newDangerousMode ? "\u2620\uFE0F Dangerous mode enabled \u2014 permissions auto-approved" : "\u{1F510} Dangerous mode disabled \u2014 permissions shown normally";
|
|
3000
3487
|
try {
|
|
3001
3488
|
await ctx.answerCallbackQuery({ text: toastText });
|
|
@@ -3124,7 +3611,7 @@ async function handleRestart(ctx, core) {
|
|
|
3124
3611
|
}
|
|
3125
3612
|
|
|
3126
3613
|
// src/adapters/telegram/commands/new-session.ts
|
|
3127
|
-
var
|
|
3614
|
+
var log11 = createChildLogger({ module: "telegram-cmd-new-session" });
|
|
3128
3615
|
var pendingNewSessions = /* @__PURE__ */ new Map();
|
|
3129
3616
|
var PENDING_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
3130
3617
|
function cleanupPending(userId) {
|
|
@@ -3223,7 +3710,7 @@ async function startConfirmStep(ctx, chatId, userId, agentName, workspace) {
|
|
|
3223
3710
|
});
|
|
3224
3711
|
}
|
|
3225
3712
|
async function createSessionDirect(ctx, core, chatId, agentName, workspace) {
|
|
3226
|
-
|
|
3713
|
+
log11.info({ userId: ctx.from?.id, agentName, workspace }, "New session command (direct)");
|
|
3227
3714
|
let threadId;
|
|
3228
3715
|
try {
|
|
3229
3716
|
const topicName = `\u{1F504} New Session`;
|
|
@@ -3253,10 +3740,10 @@ This is your coding session \u2014 chat here to work with the agent.`,
|
|
|
3253
3740
|
reply_markup: buildDangerousModeKeyboard(session.id, false)
|
|
3254
3741
|
}
|
|
3255
3742
|
);
|
|
3256
|
-
session.warmup().catch((err) =>
|
|
3743
|
+
session.warmup().catch((err) => log11.error({ err }, "Warm-up error"));
|
|
3257
3744
|
return threadId ?? null;
|
|
3258
3745
|
} catch (err) {
|
|
3259
|
-
|
|
3746
|
+
log11.error({ err }, "Session creation failed");
|
|
3260
3747
|
if (threadId) {
|
|
3261
3748
|
try {
|
|
3262
3749
|
await ctx.api.deleteForumTopic(chatId, threadId);
|
|
@@ -3334,7 +3821,7 @@ async function handleNewChat(ctx, core, chatId) {
|
|
|
3334
3821
|
reply_markup: buildDangerousModeKeyboard(session.id, false)
|
|
3335
3822
|
}
|
|
3336
3823
|
);
|
|
3337
|
-
session.warmup().catch((err) =>
|
|
3824
|
+
session.warmup().catch((err) => log11.error({ err }, "Warm-up error"));
|
|
3338
3825
|
} catch (err) {
|
|
3339
3826
|
if (newThreadId) {
|
|
3340
3827
|
try {
|
|
@@ -3365,7 +3852,7 @@ async function executeNewSession(bot, core, chatId, agentName, workspace) {
|
|
|
3365
3852
|
} });
|
|
3366
3853
|
const finalName = `\u{1F504} ${session.agentName} \u2014 New Session`;
|
|
3367
3854
|
await renameSessionTopic(bot, chatId, threadId, finalName);
|
|
3368
|
-
session.warmup().catch((err) =>
|
|
3855
|
+
session.warmup().catch((err) => log11.error({ err }, "Warm-up error"));
|
|
3369
3856
|
return { session, threadId, firstMsgId };
|
|
3370
3857
|
} catch (err) {
|
|
3371
3858
|
try {
|
|
@@ -3513,7 +4000,7 @@ Or just the folder name like <code>my-project</code> (will use ${core.configMana
|
|
|
3513
4000
|
|
|
3514
4001
|
// src/adapters/telegram/commands/session.ts
|
|
3515
4002
|
import { InlineKeyboard as InlineKeyboard3 } from "grammy";
|
|
3516
|
-
var
|
|
4003
|
+
var log12 = createChildLogger({ module: "telegram-cmd-session" });
|
|
3517
4004
|
async function handleCancel(ctx, core, assistant) {
|
|
3518
4005
|
const threadId = ctx.message?.message_thread_id;
|
|
3519
4006
|
if (!threadId) return;
|
|
@@ -3531,14 +4018,14 @@ async function handleCancel(ctx, core, assistant) {
|
|
|
3531
4018
|
String(threadId)
|
|
3532
4019
|
);
|
|
3533
4020
|
if (session) {
|
|
3534
|
-
|
|
4021
|
+
log12.info({ sessionId: session.id }, "Abort prompt command");
|
|
3535
4022
|
await session.abortPrompt();
|
|
3536
4023
|
await ctx.reply("\u26D4 Prompt aborted. Session is still active \u2014 send a new message to continue.", { parse_mode: "HTML" });
|
|
3537
4024
|
return;
|
|
3538
4025
|
}
|
|
3539
4026
|
const record = core.sessionManager.getRecordByThread("telegram", String(threadId));
|
|
3540
4027
|
if (record && record.status !== "error") {
|
|
3541
|
-
|
|
4028
|
+
log12.info({ sessionId: record.sessionId, status: record.status }, "Cancel command \u2014 no active prompt to abort");
|
|
3542
4029
|
await ctx.reply("\u2139\uFE0F No active prompt to cancel. Send a new message to resume the session.", { parse_mode: "HTML" });
|
|
3543
4030
|
}
|
|
3544
4031
|
}
|
|
@@ -3642,7 +4129,7 @@ ${lines.join("\n")}${truncated}`,
|
|
|
3642
4129
|
{ parse_mode: "HTML", reply_markup: keyboard }
|
|
3643
4130
|
);
|
|
3644
4131
|
} catch (err) {
|
|
3645
|
-
|
|
4132
|
+
log12.error({ err }, "handleTopics error");
|
|
3646
4133
|
await ctx.reply("\u274C Failed to list sessions.", { parse_mode: "HTML" }).catch(() => {
|
|
3647
4134
|
});
|
|
3648
4135
|
}
|
|
@@ -3663,13 +4150,13 @@ async function handleCleanup(ctx, core, chatId, statuses) {
|
|
|
3663
4150
|
try {
|
|
3664
4151
|
await ctx.api.deleteForumTopic(chatId, topicId);
|
|
3665
4152
|
} catch (err) {
|
|
3666
|
-
|
|
4153
|
+
log12.warn({ err, sessionId: record.sessionId, topicId }, "Failed to delete forum topic during cleanup");
|
|
3667
4154
|
}
|
|
3668
4155
|
}
|
|
3669
4156
|
await core.sessionManager.removeRecord(record.sessionId);
|
|
3670
4157
|
deleted++;
|
|
3671
4158
|
} catch (err) {
|
|
3672
|
-
|
|
4159
|
+
log12.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
|
|
3673
4160
|
failed++;
|
|
3674
4161
|
}
|
|
3675
4162
|
}
|
|
@@ -3740,7 +4227,7 @@ async function handleCleanupEverythingConfirmed(ctx, core, chatId, systemTopicId
|
|
|
3740
4227
|
try {
|
|
3741
4228
|
await core.sessionManager.cancelSession(record.sessionId);
|
|
3742
4229
|
} catch (err) {
|
|
3743
|
-
|
|
4230
|
+
log12.warn({ err, sessionId: record.sessionId }, "Failed to cancel session during cleanup");
|
|
3744
4231
|
}
|
|
3745
4232
|
}
|
|
3746
4233
|
const topicId = record.platform?.topicId;
|
|
@@ -3748,13 +4235,13 @@ async function handleCleanupEverythingConfirmed(ctx, core, chatId, systemTopicId
|
|
|
3748
4235
|
try {
|
|
3749
4236
|
await ctx.api.deleteForumTopic(chatId, topicId);
|
|
3750
4237
|
} catch (err) {
|
|
3751
|
-
|
|
4238
|
+
log12.warn({ err, sessionId: record.sessionId, topicId }, "Failed to delete forum topic during cleanup");
|
|
3752
4239
|
}
|
|
3753
4240
|
}
|
|
3754
4241
|
await core.sessionManager.removeRecord(record.sessionId);
|
|
3755
4242
|
deleted++;
|
|
3756
4243
|
} catch (err) {
|
|
3757
|
-
|
|
4244
|
+
log12.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
|
|
3758
4245
|
failed++;
|
|
3759
4246
|
}
|
|
3760
4247
|
}
|
|
@@ -3796,6 +4283,86 @@ function setupSessionCallbacks(bot, core, chatId, systemTopicIds) {
|
|
|
3796
4283
|
}
|
|
3797
4284
|
});
|
|
3798
4285
|
}
|
|
4286
|
+
async function handleUsage(ctx, core) {
|
|
4287
|
+
if (!core.usageStore) {
|
|
4288
|
+
await ctx.reply("\u{1F4CA} Usage tracking is disabled.", { parse_mode: "HTML" });
|
|
4289
|
+
return;
|
|
4290
|
+
}
|
|
4291
|
+
const rawMatch = ctx.match;
|
|
4292
|
+
const period = typeof rawMatch === "string" ? rawMatch.trim().toLowerCase() : "";
|
|
4293
|
+
let summaries;
|
|
4294
|
+
if (period === "today" || period === "week" || period === "month") {
|
|
4295
|
+
summaries = [core.usageStore.query(period)];
|
|
4296
|
+
} else {
|
|
4297
|
+
summaries = [
|
|
4298
|
+
core.usageStore.query("month"),
|
|
4299
|
+
core.usageStore.query("week"),
|
|
4300
|
+
core.usageStore.query("today")
|
|
4301
|
+
];
|
|
4302
|
+
}
|
|
4303
|
+
const budgetStatus = core.usageBudget ? core.usageBudget.getStatus() : { status: "ok", used: 0, budget: 0, percent: 0 };
|
|
4304
|
+
const text = formatUsageReport(summaries, budgetStatus);
|
|
4305
|
+
await ctx.reply(text, { parse_mode: "HTML" });
|
|
4306
|
+
}
|
|
4307
|
+
async function handleArchive(ctx, core) {
|
|
4308
|
+
const threadId = ctx.message?.message_thread_id;
|
|
4309
|
+
if (!threadId) return;
|
|
4310
|
+
const session = core.sessionManager.getSessionByThread("telegram", String(threadId));
|
|
4311
|
+
if (!session) {
|
|
4312
|
+
await ctx.reply(
|
|
4313
|
+
"\u2139\uFE0F <b>/archive</b> works in session topics \u2014 it recreates the topic with a clean chat view while keeping your agent session alive.\n\nGo to the session topic you want to archive and type /archive there.",
|
|
4314
|
+
{ parse_mode: "HTML" }
|
|
4315
|
+
);
|
|
4316
|
+
return;
|
|
4317
|
+
}
|
|
4318
|
+
if (session.status === "initializing") {
|
|
4319
|
+
await ctx.reply("\u23F3 Please wait for session to be ready.", { parse_mode: "HTML" });
|
|
4320
|
+
return;
|
|
4321
|
+
}
|
|
4322
|
+
if (session.status !== "active") {
|
|
4323
|
+
await ctx.reply(`\u26A0\uFE0F Cannot archive \u2014 session is ${session.status}.`, { parse_mode: "HTML" });
|
|
4324
|
+
return;
|
|
4325
|
+
}
|
|
4326
|
+
await ctx.reply(
|
|
4327
|
+
"\u26A0\uFE0F <b>Archive this session topic?</b>\n\nThis will permanently delete all messages in this topic and create a fresh one.\nYour agent session will continue \u2014 only the chat view is reset.\n\n<i>Note: links to messages in this topic will stop working.</i>",
|
|
4328
|
+
{
|
|
4329
|
+
parse_mode: "HTML",
|
|
4330
|
+
reply_markup: new InlineKeyboard3().text("\u{1F5D1} Yes, archive", `ar:yes:${session.id}`).text("\u274C Cancel", `ar:no:${session.id}`)
|
|
4331
|
+
}
|
|
4332
|
+
);
|
|
4333
|
+
}
|
|
4334
|
+
async function handleArchiveConfirm(ctx, core, chatId) {
|
|
4335
|
+
const data = ctx.callbackQuery?.data;
|
|
4336
|
+
if (!data) return;
|
|
4337
|
+
try {
|
|
4338
|
+
await ctx.answerCallbackQuery();
|
|
4339
|
+
} catch {
|
|
4340
|
+
}
|
|
4341
|
+
const [, action, sessionId] = data.split(":");
|
|
4342
|
+
if (action === "no") {
|
|
4343
|
+
await ctx.editMessageText("Archive cancelled.", { parse_mode: "HTML" });
|
|
4344
|
+
return;
|
|
4345
|
+
}
|
|
4346
|
+
await ctx.editMessageText("\u{1F504} Archiving topic...", { parse_mode: "HTML" });
|
|
4347
|
+
const result = await core.archiveSession(sessionId);
|
|
4348
|
+
if (result.ok) {
|
|
4349
|
+
const newTopicId = Number(result.newThreadId);
|
|
4350
|
+
await ctx.api.sendMessage(chatId, "\u2705 Topic archived. Session continues.", {
|
|
4351
|
+
message_thread_id: newTopicId,
|
|
4352
|
+
parse_mode: "HTML"
|
|
4353
|
+
});
|
|
4354
|
+
} else {
|
|
4355
|
+
try {
|
|
4356
|
+
await ctx.editMessageText(`\u274C Failed to archive: <code>${escapeHtml(result.error)}</code>`, { parse_mode: "HTML" });
|
|
4357
|
+
} catch {
|
|
4358
|
+
core.notificationManager.notifyAll({
|
|
4359
|
+
sessionId,
|
|
4360
|
+
type: "error",
|
|
4361
|
+
summary: `Failed to recreate topic for session "${sessionId}": ${result.error}`
|
|
4362
|
+
});
|
|
4363
|
+
}
|
|
4364
|
+
}
|
|
4365
|
+
}
|
|
3799
4366
|
|
|
3800
4367
|
// src/adapters/telegram/commands/agents.ts
|
|
3801
4368
|
import { InlineKeyboard as InlineKeyboard4 } from "grammy";
|
|
@@ -4186,7 +4753,7 @@ ${resultText}`,
|
|
|
4186
4753
|
|
|
4187
4754
|
// src/adapters/telegram/commands/settings.ts
|
|
4188
4755
|
import { InlineKeyboard as InlineKeyboard6 } from "grammy";
|
|
4189
|
-
var
|
|
4756
|
+
var log13 = createChildLogger({ module: "telegram-settings" });
|
|
4190
4757
|
function buildSettingsKeyboard(core) {
|
|
4191
4758
|
const config = core.configManager.get();
|
|
4192
4759
|
const fields = getSafeFields();
|
|
@@ -4212,13 +4779,15 @@ function formatFieldLabel(field, value) {
|
|
|
4212
4779
|
tunnel: "\u{1F517}",
|
|
4213
4780
|
security: "\u{1F512}",
|
|
4214
4781
|
workspace: "\u{1F4C1}",
|
|
4215
|
-
storage: "\u{1F4BE}"
|
|
4782
|
+
storage: "\u{1F4BE}",
|
|
4783
|
+
speech: "\u{1F3A4}"
|
|
4216
4784
|
};
|
|
4217
4785
|
const icon = icons[field.group] ?? "\u2699\uFE0F";
|
|
4218
4786
|
if (field.type === "toggle") {
|
|
4219
4787
|
return `${icon} ${field.displayName}: ${value ? "ON" : "OFF"}`;
|
|
4220
4788
|
}
|
|
4221
|
-
|
|
4789
|
+
const displayValue = value === null || value === void 0 ? "Not set" : String(value);
|
|
4790
|
+
return `${icon} ${field.displayName}: ${displayValue}`;
|
|
4222
4791
|
}
|
|
4223
4792
|
async function handleSettings(ctx, core) {
|
|
4224
4793
|
const kb = buildSettingsKeyboard(core);
|
|
@@ -4247,7 +4816,7 @@ function setupSettingsCallbacks(bot, core, getAssistantSession) {
|
|
|
4247
4816
|
} catch {
|
|
4248
4817
|
}
|
|
4249
4818
|
} catch (err) {
|
|
4250
|
-
|
|
4819
|
+
log13.error({ err, fieldPath }, "Failed to toggle config");
|
|
4251
4820
|
try {
|
|
4252
4821
|
await ctx.answerCallbackQuery({ text: "\u274C Failed to update" });
|
|
4253
4822
|
} catch {
|
|
@@ -4285,6 +4854,27 @@ Select a value:`, {
|
|
|
4285
4854
|
const fieldPath = parts.slice(0, -1).join(":");
|
|
4286
4855
|
const newValue = parts[parts.length - 1];
|
|
4287
4856
|
try {
|
|
4857
|
+
if (fieldPath === "speech.stt.provider") {
|
|
4858
|
+
const config = core.configManager.get();
|
|
4859
|
+
const providerConfig = config.speech?.stt?.providers?.[newValue];
|
|
4860
|
+
if (!providerConfig?.apiKey) {
|
|
4861
|
+
const assistant = getAssistantSession();
|
|
4862
|
+
if (assistant) {
|
|
4863
|
+
try {
|
|
4864
|
+
await ctx.answerCallbackQuery({ text: `\u{1F511} API key needed \u2014 check Assistant topic` });
|
|
4865
|
+
} catch {
|
|
4866
|
+
}
|
|
4867
|
+
const prompt = `User wants to enable ${newValue} as Speech-to-Text provider, but no API key is configured yet. Guide them to get a ${newValue} API key and set it up. After they provide the key, run both commands: \`openacp config set speech.stt.providers.${newValue}.apiKey <key>\` and \`openacp config set speech.stt.provider ${newValue}\``;
|
|
4868
|
+
await assistant.enqueuePrompt(prompt);
|
|
4869
|
+
return;
|
|
4870
|
+
}
|
|
4871
|
+
try {
|
|
4872
|
+
await ctx.answerCallbackQuery({ text: `\u26A0\uFE0F Set API key first: openacp config set speech.stt.providers.${newValue}.apiKey <key>` });
|
|
4873
|
+
} catch {
|
|
4874
|
+
}
|
|
4875
|
+
return;
|
|
4876
|
+
}
|
|
4877
|
+
}
|
|
4288
4878
|
const updates = buildNestedUpdate(fieldPath, newValue);
|
|
4289
4879
|
await core.configManager.save(updates, fieldPath);
|
|
4290
4880
|
try {
|
|
@@ -4300,7 +4890,7 @@ Tap to change:`, {
|
|
|
4300
4890
|
} catch {
|
|
4301
4891
|
}
|
|
4302
4892
|
} catch (err) {
|
|
4303
|
-
|
|
4893
|
+
log13.error({ err, fieldPath }, "Failed to set config");
|
|
4304
4894
|
try {
|
|
4305
4895
|
await ctx.answerCallbackQuery({ text: "\u274C Failed to update" });
|
|
4306
4896
|
} catch {
|
|
@@ -4333,7 +4923,7 @@ Tap to change:`, {
|
|
|
4333
4923
|
await ctx.answerCallbackQuery();
|
|
4334
4924
|
} catch {
|
|
4335
4925
|
}
|
|
4336
|
-
const { buildMenuKeyboard: buildMenuKeyboard3 } = await import("./menu-
|
|
4926
|
+
const { buildMenuKeyboard: buildMenuKeyboard3 } = await import("./menu-XR2GET2B.js");
|
|
4337
4927
|
try {
|
|
4338
4928
|
await ctx.editMessageText(`<b>OpenACP Menu</b>
|
|
4339
4929
|
Choose an action:`, {
|
|
@@ -4372,7 +4962,7 @@ function buildNestedUpdate(dotPath, value) {
|
|
|
4372
4962
|
|
|
4373
4963
|
// src/adapters/telegram/commands/doctor.ts
|
|
4374
4964
|
import { InlineKeyboard as InlineKeyboard7 } from "grammy";
|
|
4375
|
-
var
|
|
4965
|
+
var log14 = createChildLogger({ module: "telegram-cmd-doctor" });
|
|
4376
4966
|
var pendingFixesStore = /* @__PURE__ */ new Map();
|
|
4377
4967
|
function renderReport(report) {
|
|
4378
4968
|
const icons = { pass: "\u2705", warn: "\u26A0\uFE0F", fail: "\u274C" };
|
|
@@ -4415,7 +5005,7 @@ async function handleDoctor(ctx) {
|
|
|
4415
5005
|
reply_markup: keyboard
|
|
4416
5006
|
});
|
|
4417
5007
|
} catch (err) {
|
|
4418
|
-
|
|
5008
|
+
log14.error({ err }, "Doctor command failed");
|
|
4419
5009
|
await ctx.api.editMessageText(
|
|
4420
5010
|
ctx.chat.id,
|
|
4421
5011
|
statusMsg.message_id,
|
|
@@ -4464,7 +5054,7 @@ function setupDoctorCallbacks(bot) {
|
|
|
4464
5054
|
}
|
|
4465
5055
|
}
|
|
4466
5056
|
} catch (err) {
|
|
4467
|
-
|
|
5057
|
+
log14.error({ err, index }, "Doctor fix callback failed");
|
|
4468
5058
|
}
|
|
4469
5059
|
});
|
|
4470
5060
|
bot.callbackQuery("m:doctor", async (ctx) => {
|
|
@@ -4478,7 +5068,7 @@ function setupDoctorCallbacks(bot) {
|
|
|
4478
5068
|
|
|
4479
5069
|
// src/adapters/telegram/commands/tunnel.ts
|
|
4480
5070
|
import { InlineKeyboard as InlineKeyboard8 } from "grammy";
|
|
4481
|
-
var
|
|
5071
|
+
var log15 = createChildLogger({ module: "telegram-cmd-tunnel" });
|
|
4482
5072
|
async function handleTunnel(ctx, core) {
|
|
4483
5073
|
if (!core.tunnelService) {
|
|
4484
5074
|
await ctx.reply("\u274C Tunnel service is not enabled.", { parse_mode: "HTML" });
|
|
@@ -4543,12 +5133,19 @@ async function handleTunnels(ctx, core) {
|
|
|
4543
5133
|
await ctx.reply("\u274C Tunnel service is not enabled.", { parse_mode: "HTML" });
|
|
4544
5134
|
return;
|
|
4545
5135
|
}
|
|
4546
|
-
const
|
|
5136
|
+
const threadId = ctx.message?.message_thread_id;
|
|
5137
|
+
let entries = core.tunnelService.listTunnels();
|
|
5138
|
+
let sessionScoped = false;
|
|
5139
|
+
if (threadId) {
|
|
5140
|
+
const session = core.sessionManager.getSessionByThread("telegram", String(threadId));
|
|
5141
|
+
if (session) {
|
|
5142
|
+
entries = entries.filter((e) => e.sessionId === session.id);
|
|
5143
|
+
sessionScoped = true;
|
|
5144
|
+
}
|
|
5145
|
+
}
|
|
4547
5146
|
if (entries.length === 0) {
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
{ parse_mode: "HTML" }
|
|
4551
|
-
);
|
|
5147
|
+
const hint = sessionScoped ? "No tunnels for this session.\n\nUse <code>/tunnel <port></code> to create one." : "No active tunnels.\n\nUse <code>/tunnel <port></code> to create one.";
|
|
5148
|
+
await ctx.reply(hint, { parse_mode: "HTML" });
|
|
4552
5149
|
return;
|
|
4553
5150
|
}
|
|
4554
5151
|
const lines = entries.map((e) => {
|
|
@@ -4643,8 +5240,10 @@ function setupCommands(bot, core, chatId, assistant) {
|
|
|
4643
5240
|
bot.command("integrate", (ctx) => handleIntegrate(ctx, core));
|
|
4644
5241
|
bot.command("clear", (ctx) => handleClear(ctx, assistant));
|
|
4645
5242
|
bot.command("doctor", (ctx) => handleDoctor(ctx));
|
|
5243
|
+
bot.command("usage", (ctx) => handleUsage(ctx, core));
|
|
4646
5244
|
bot.command("tunnel", (ctx) => handleTunnel(ctx, core));
|
|
4647
5245
|
bot.command("tunnels", (ctx) => handleTunnels(ctx, core));
|
|
5246
|
+
bot.command("archive", (ctx) => handleArchive(ctx, core));
|
|
4648
5247
|
}
|
|
4649
5248
|
function setupAllCallbacks(bot, core, chatId, systemTopicIds, getAssistantSession) {
|
|
4650
5249
|
setupNewSessionCallbacks(bot, core, chatId);
|
|
@@ -4658,6 +5257,7 @@ function setupAllCallbacks(bot, core, chatId, systemTopicIds, getAssistantSessio
|
|
|
4658
5257
|
await ctx.answerCallbackQuery();
|
|
4659
5258
|
await createSessionDirect(ctx, core, chatId, agentKey, core.configManager.get().workspace.baseDir);
|
|
4660
5259
|
});
|
|
5260
|
+
bot.callbackQuery(/^ar:/, (ctx) => handleArchiveConfirm(ctx, core, chatId));
|
|
4661
5261
|
bot.callbackQuery(/^m:/, async (ctx) => {
|
|
4662
5262
|
const data = ctx.callbackQuery.data;
|
|
4663
5263
|
try {
|
|
@@ -4713,14 +5313,16 @@ var STATIC_COMMANDS = [
|
|
|
4713
5313
|
{ command: "restart", description: "Restart OpenACP" },
|
|
4714
5314
|
{ command: "update", description: "Update to latest version and restart" },
|
|
4715
5315
|
{ command: "doctor", description: "Run system diagnostics" },
|
|
5316
|
+
{ command: "usage", description: "View token usage and cost report" },
|
|
4716
5317
|
{ command: "tunnel", description: "Create/stop tunnel for a local port" },
|
|
4717
|
-
{ command: "tunnels", description: "List active tunnels" }
|
|
5318
|
+
{ command: "tunnels", description: "List active tunnels" },
|
|
5319
|
+
{ command: "archive", description: "Archive session topic (recreate with clean history)" }
|
|
4718
5320
|
];
|
|
4719
5321
|
|
|
4720
5322
|
// src/adapters/telegram/permissions.ts
|
|
4721
5323
|
import { InlineKeyboard as InlineKeyboard9 } from "grammy";
|
|
4722
|
-
import { nanoid as
|
|
4723
|
-
var
|
|
5324
|
+
import { nanoid as nanoid3 } from "nanoid";
|
|
5325
|
+
var log16 = createChildLogger({ module: "telegram-permissions" });
|
|
4724
5326
|
var PermissionHandler = class {
|
|
4725
5327
|
constructor(bot, chatId, getSession, sendNotification) {
|
|
4726
5328
|
this.bot = bot;
|
|
@@ -4731,7 +5333,7 @@ var PermissionHandler = class {
|
|
|
4731
5333
|
pending = /* @__PURE__ */ new Map();
|
|
4732
5334
|
async sendPermissionRequest(session, request) {
|
|
4733
5335
|
const threadId = Number(session.threadId);
|
|
4734
|
-
const callbackKey =
|
|
5336
|
+
const callbackKey = nanoid3(8);
|
|
4735
5337
|
this.pending.set(callbackKey, {
|
|
4736
5338
|
sessionId: session.id,
|
|
4737
5339
|
requestId: request.id,
|
|
@@ -4780,7 +5382,7 @@ ${escapeHtml(request.description)}`,
|
|
|
4780
5382
|
}
|
|
4781
5383
|
const session = this.getSession(pending.sessionId);
|
|
4782
5384
|
const isAllow = pending.options.find((o) => o.id === optionId)?.isAllow ?? false;
|
|
4783
|
-
|
|
5385
|
+
log16.info({ requestId: pending.requestId, optionId, isAllow }, "Permission responded");
|
|
4784
5386
|
if (session?.permissionGate.requestId === pending.requestId) {
|
|
4785
5387
|
session.permissionGate.resolve(optionId);
|
|
4786
5388
|
}
|
|
@@ -4798,10 +5400,10 @@ ${escapeHtml(request.description)}`,
|
|
|
4798
5400
|
};
|
|
4799
5401
|
|
|
4800
5402
|
// src/adapters/telegram/assistant.ts
|
|
4801
|
-
var
|
|
5403
|
+
var log17 = createChildLogger({ module: "telegram-assistant" });
|
|
4802
5404
|
async function spawnAssistant(core, adapter, assistantTopicId) {
|
|
4803
5405
|
const config = core.configManager.get();
|
|
4804
|
-
|
|
5406
|
+
log17.info({ agent: config.defaultAgent }, "Creating assistant session...");
|
|
4805
5407
|
const session = await core.createSession({
|
|
4806
5408
|
channelId: "telegram",
|
|
4807
5409
|
agentName: config.defaultAgent,
|
|
@@ -4810,7 +5412,7 @@ async function spawnAssistant(core, adapter, assistantTopicId) {
|
|
|
4810
5412
|
// Prevent auto-naming from triggering after system prompt
|
|
4811
5413
|
});
|
|
4812
5414
|
session.threadId = String(assistantTopicId);
|
|
4813
|
-
|
|
5415
|
+
log17.info({ sessionId: session.id }, "Assistant agent spawned");
|
|
4814
5416
|
const allRecords = core.sessionManager.listRecords();
|
|
4815
5417
|
const activeCount = allRecords.filter((r) => r.status === "active" || r.status === "initializing").length;
|
|
4816
5418
|
const statusCounts = /* @__PURE__ */ new Map();
|
|
@@ -4831,9 +5433,9 @@ async function spawnAssistant(core, adapter, assistantTopicId) {
|
|
|
4831
5433
|
};
|
|
4832
5434
|
const systemPrompt = buildAssistantSystemPrompt(ctx);
|
|
4833
5435
|
const ready = session.enqueuePrompt(systemPrompt).then(() => {
|
|
4834
|
-
|
|
5436
|
+
log17.info({ sessionId: session.id }, "Assistant system prompt completed");
|
|
4835
5437
|
}).catch((err) => {
|
|
4836
|
-
|
|
5438
|
+
log17.warn({ err }, "Assistant system prompt failed");
|
|
4837
5439
|
});
|
|
4838
5440
|
return { session, ready };
|
|
4839
5441
|
}
|
|
@@ -4871,6 +5473,7 @@ function buildAssistantSystemPrompt(ctx) {
|
|
|
4871
5473
|
- Available in ACP Registry: ${availableAgentCount ?? "28+"} more agents (use /agents to browse)
|
|
4872
5474
|
- Default agent: ${config.defaultAgent}
|
|
4873
5475
|
- Workspace base directory: ${config.workspace.baseDir}
|
|
5476
|
+
- STT: ${config.speech?.stt?.provider ? `${config.speech.stt.provider} \u2705` : "Not configured"}
|
|
4874
5477
|
|
|
4875
5478
|
## Action Playbook
|
|
4876
5479
|
|
|
@@ -4918,6 +5521,16 @@ function buildAssistantSystemPrompt(ctx) {
|
|
|
4918
5521
|
- When user asks about "settings" or "config", use \`openacp config set\` directly
|
|
4919
5522
|
- When receiving a delegated request from the Settings menu, ask user for the new value, then apply with \`openacp config set <path> <value>\`
|
|
4920
5523
|
|
|
5524
|
+
### Voice / Speech-to-Text
|
|
5525
|
+
- OpenACP can transcribe voice messages to text using STT providers (Groq Whisper, OpenAI Whisper)
|
|
5526
|
+
- Current STT provider: ${config.speech?.stt?.provider ?? "Not configured"}
|
|
5527
|
+
- To enable: user needs an API key from the STT provider
|
|
5528
|
+
- Groq (recommended, free tier ~8h/day): Get key at console.groq.com \u2192 API Keys
|
|
5529
|
+
- Set via: \`openacp config set speech.stt.provider groq\` then \`openacp config set speech.stt.providers.groq.apiKey <key>\`
|
|
5530
|
+
- When STT is configured, voice messages are automatically transcribed before sending to agents that don't support audio
|
|
5531
|
+
- Agents with audio capability receive the audio directly (no transcription needed)
|
|
5532
|
+
- User can also configure via /settings \u2192 STT Provider
|
|
5533
|
+
|
|
4921
5534
|
### Restart / Update
|
|
4922
5535
|
- Always ask for confirmation \u2014 these are disruptive actions
|
|
4923
5536
|
- Guide user: "Tap \u{1F504} Restart button or type /restart"
|
|
@@ -4988,7 +5601,7 @@ function redirectToAssistant(chatId, assistantTopicId) {
|
|
|
4988
5601
|
}
|
|
4989
5602
|
|
|
4990
5603
|
// src/adapters/telegram/activity.ts
|
|
4991
|
-
var
|
|
5604
|
+
var log18 = createChildLogger({ module: "telegram:activity" });
|
|
4992
5605
|
var THINKING_REFRESH_MS = 15e3;
|
|
4993
5606
|
var THINKING_MAX_MS = 3 * 60 * 1e3;
|
|
4994
5607
|
var ThinkingIndicator = class {
|
|
@@ -5020,7 +5633,7 @@ var ThinkingIndicator = class {
|
|
|
5020
5633
|
this.startRefreshTimer();
|
|
5021
5634
|
}
|
|
5022
5635
|
} catch (err) {
|
|
5023
|
-
|
|
5636
|
+
log18.warn({ err }, "ThinkingIndicator.show() failed");
|
|
5024
5637
|
} finally {
|
|
5025
5638
|
this.sending = false;
|
|
5026
5639
|
}
|
|
@@ -5093,7 +5706,7 @@ var UsageMessage = class {
|
|
|
5093
5706
|
if (result) this.msgId = result.message_id;
|
|
5094
5707
|
}
|
|
5095
5708
|
} catch (err) {
|
|
5096
|
-
|
|
5709
|
+
log18.warn({ err }, "UsageMessage.send() failed");
|
|
5097
5710
|
}
|
|
5098
5711
|
}
|
|
5099
5712
|
getMsgId() {
|
|
@@ -5106,7 +5719,7 @@ var UsageMessage = class {
|
|
|
5106
5719
|
try {
|
|
5107
5720
|
await this.sendQueue.enqueue(() => this.api.deleteMessage(this.chatId, id));
|
|
5108
5721
|
} catch (err) {
|
|
5109
|
-
|
|
5722
|
+
log18.warn({ err }, "UsageMessage.delete() failed");
|
|
5110
5723
|
}
|
|
5111
5724
|
}
|
|
5112
5725
|
};
|
|
@@ -5192,7 +5805,7 @@ var PlanCard = class {
|
|
|
5192
5805
|
if (result) this.msgId = result.message_id;
|
|
5193
5806
|
}
|
|
5194
5807
|
} catch (err) {
|
|
5195
|
-
|
|
5808
|
+
log18.warn({ err }, "PlanCard flush failed");
|
|
5196
5809
|
}
|
|
5197
5810
|
}
|
|
5198
5811
|
};
|
|
@@ -5255,7 +5868,7 @@ var ActivityTracker = class {
|
|
|
5255
5868
|
})
|
|
5256
5869
|
);
|
|
5257
5870
|
} catch (err) {
|
|
5258
|
-
|
|
5871
|
+
log18.warn({ err }, "ActivityTracker.onComplete() Done send failed");
|
|
5259
5872
|
}
|
|
5260
5873
|
}
|
|
5261
5874
|
}
|
|
@@ -5337,7 +5950,7 @@ var TelegramSendQueue = class {
|
|
|
5337
5950
|
};
|
|
5338
5951
|
|
|
5339
5952
|
// src/adapters/telegram/action-detect.ts
|
|
5340
|
-
import { nanoid as
|
|
5953
|
+
import { nanoid as nanoid4 } from "nanoid";
|
|
5341
5954
|
import { InlineKeyboard as InlineKeyboard10 } from "grammy";
|
|
5342
5955
|
var CMD_NEW_RE = /\/new(?:\s+([^\s\u0080-\uFFFF]+)(?:\s+([^\s\u0080-\uFFFF]+))?)?/;
|
|
5343
5956
|
var CMD_CANCEL_RE = /\/cancel\b/;
|
|
@@ -5363,7 +5976,7 @@ function detectAction(text) {
|
|
|
5363
5976
|
var ACTION_TTL_MS = 5 * 60 * 1e3;
|
|
5364
5977
|
var actionMap = /* @__PURE__ */ new Map();
|
|
5365
5978
|
function storeAction(action) {
|
|
5366
|
-
const id =
|
|
5979
|
+
const id = nanoid4(10);
|
|
5367
5980
|
actionMap.set(id, { action, createdAt: Date.now() });
|
|
5368
5981
|
for (const [key, entry] of actionMap) {
|
|
5369
5982
|
if (Date.now() - entry.createdAt > ACTION_TTL_MS) {
|
|
@@ -5493,7 +6106,7 @@ function setupActionCallbacks(bot, core, chatId, getAssistantSessionId) {
|
|
|
5493
6106
|
}
|
|
5494
6107
|
|
|
5495
6108
|
// src/adapters/telegram/tool-call-tracker.ts
|
|
5496
|
-
var
|
|
6109
|
+
var log19 = createChildLogger({ module: "tool-call-tracker" });
|
|
5497
6110
|
var ToolCallTracker = class {
|
|
5498
6111
|
constructor(bot, chatId, sendQueue) {
|
|
5499
6112
|
this.bot = bot;
|
|
@@ -5537,7 +6150,7 @@ var ToolCallTracker = class {
|
|
|
5537
6150
|
if (!toolState) return;
|
|
5538
6151
|
if (meta.viewerLinks) {
|
|
5539
6152
|
toolState.viewerLinks = meta.viewerLinks;
|
|
5540
|
-
|
|
6153
|
+
log19.debug({ toolId: meta.id, viewerLinks: meta.viewerLinks }, "Accumulated viewerLinks");
|
|
5541
6154
|
}
|
|
5542
6155
|
if (meta.viewerFilePath) toolState.viewerFilePath = meta.viewerFilePath;
|
|
5543
6156
|
if (meta.name) toolState.name = meta.name;
|
|
@@ -5545,7 +6158,7 @@ var ToolCallTracker = class {
|
|
|
5545
6158
|
const isTerminal = meta.status === "completed" || meta.status === "failed";
|
|
5546
6159
|
if (!isTerminal) return;
|
|
5547
6160
|
await toolState.ready;
|
|
5548
|
-
|
|
6161
|
+
log19.debug(
|
|
5549
6162
|
{
|
|
5550
6163
|
toolId: meta.id,
|
|
5551
6164
|
status: meta.status,
|
|
@@ -5574,7 +6187,7 @@ var ToolCallTracker = class {
|
|
|
5574
6187
|
)
|
|
5575
6188
|
);
|
|
5576
6189
|
} catch (err) {
|
|
5577
|
-
|
|
6190
|
+
log19.warn(
|
|
5578
6191
|
{
|
|
5579
6192
|
err,
|
|
5580
6193
|
msgId: toolState.msgId,
|
|
@@ -5824,7 +6437,7 @@ var DraftManager = class {
|
|
|
5824
6437
|
};
|
|
5825
6438
|
|
|
5826
6439
|
// src/adapters/telegram/skill-command-manager.ts
|
|
5827
|
-
var
|
|
6440
|
+
var log20 = createChildLogger({ module: "skill-commands" });
|
|
5828
6441
|
var SkillCommandManager = class {
|
|
5829
6442
|
// sessionId → pinned msgId
|
|
5830
6443
|
constructor(bot, chatId, sendQueue, sessionManager) {
|
|
@@ -5890,7 +6503,7 @@ var SkillCommandManager = class {
|
|
|
5890
6503
|
disable_notification: true
|
|
5891
6504
|
});
|
|
5892
6505
|
} catch (err) {
|
|
5893
|
-
|
|
6506
|
+
log20.error({ err, sessionId }, "Failed to send skill commands");
|
|
5894
6507
|
}
|
|
5895
6508
|
}
|
|
5896
6509
|
async cleanup(sessionId) {
|
|
@@ -5916,7 +6529,7 @@ var SkillCommandManager = class {
|
|
|
5916
6529
|
};
|
|
5917
6530
|
|
|
5918
6531
|
// src/adapters/telegram/adapter.ts
|
|
5919
|
-
var
|
|
6532
|
+
var log21 = createChildLogger({ module: "telegram" });
|
|
5920
6533
|
function patchedFetch(input, init) {
|
|
5921
6534
|
if (init?.signal && !(init.signal instanceof AbortSignal)) {
|
|
5922
6535
|
const nativeController = new AbortController();
|
|
@@ -5975,7 +6588,7 @@ var TelegramAdapter = class extends ChannelAdapter {
|
|
|
5975
6588
|
);
|
|
5976
6589
|
this.bot.catch((err) => {
|
|
5977
6590
|
const rootCause = err.error instanceof Error ? err.error : err;
|
|
5978
|
-
|
|
6591
|
+
log21.error({ err: rootCause }, "Telegram bot error");
|
|
5979
6592
|
});
|
|
5980
6593
|
this.bot.api.config.use(async (prev, method, payload, signal) => {
|
|
5981
6594
|
const maxRetries = 3;
|
|
@@ -5989,7 +6602,7 @@ var TelegramAdapter = class extends ChannelAdapter {
|
|
|
5989
6602
|
if (rateLimitedMethods.includes(method)) {
|
|
5990
6603
|
this.sendQueue.onRateLimited();
|
|
5991
6604
|
}
|
|
5992
|
-
|
|
6605
|
+
log21.warn(
|
|
5993
6606
|
{ method, retryAfter, attempt: attempt + 1 },
|
|
5994
6607
|
"Rate limited by Telegram, retrying"
|
|
5995
6608
|
);
|
|
@@ -6121,7 +6734,7 @@ var TelegramAdapter = class extends ChannelAdapter {
|
|
|
6121
6734
|
this.setupRoutes();
|
|
6122
6735
|
this.bot.start({
|
|
6123
6736
|
allowed_updates: ["message", "callback_query"],
|
|
6124
|
-
onStart: () =>
|
|
6737
|
+
onStart: () => log21.info(
|
|
6125
6738
|
{ chatId: this.telegramConfig.chatId },
|
|
6126
6739
|
"Telegram bot started"
|
|
6127
6740
|
)
|
|
@@ -6143,10 +6756,10 @@ var TelegramAdapter = class extends ChannelAdapter {
|
|
|
6143
6756
|
reply_markup: buildMenuKeyboard()
|
|
6144
6757
|
});
|
|
6145
6758
|
} catch (err) {
|
|
6146
|
-
|
|
6759
|
+
log21.warn({ err }, "Failed to send welcome message");
|
|
6147
6760
|
}
|
|
6148
6761
|
try {
|
|
6149
|
-
|
|
6762
|
+
log21.info("Spawning assistant session...");
|
|
6150
6763
|
const { session, ready } = await spawnAssistant(
|
|
6151
6764
|
this.core,
|
|
6152
6765
|
this,
|
|
@@ -6154,13 +6767,13 @@ var TelegramAdapter = class extends ChannelAdapter {
|
|
|
6154
6767
|
);
|
|
6155
6768
|
this.assistantSession = session;
|
|
6156
6769
|
this.assistantInitializing = true;
|
|
6157
|
-
|
|
6770
|
+
log21.info({ sessionId: session.id }, "Assistant session ready, system prompt running in background");
|
|
6158
6771
|
ready.then(() => {
|
|
6159
6772
|
this.assistantInitializing = false;
|
|
6160
|
-
|
|
6773
|
+
log21.info({ sessionId: session.id }, "Assistant ready for user messages");
|
|
6161
6774
|
});
|
|
6162
6775
|
} catch (err) {
|
|
6163
|
-
|
|
6776
|
+
log21.error({ err }, "Failed to spawn assistant");
|
|
6164
6777
|
this.bot.api.sendMessage(
|
|
6165
6778
|
this.telegramConfig.chatId,
|
|
6166
6779
|
`\u26A0\uFE0F <b>Failed to start assistant session.</b>
|
|
@@ -6176,7 +6789,7 @@ var TelegramAdapter = class extends ChannelAdapter {
|
|
|
6176
6789
|
await this.assistantSession.destroy();
|
|
6177
6790
|
}
|
|
6178
6791
|
await this.bot.stop();
|
|
6179
|
-
|
|
6792
|
+
log21.info("Telegram bot stopped");
|
|
6180
6793
|
}
|
|
6181
6794
|
setupRoutes() {
|
|
6182
6795
|
this.bot.on("message:text", async (ctx) => {
|
|
@@ -6204,7 +6817,7 @@ var TelegramAdapter = class extends ChannelAdapter {
|
|
|
6204
6817
|
ctx.replyWithChatAction("typing").catch(() => {
|
|
6205
6818
|
});
|
|
6206
6819
|
handleAssistantMessage(this.assistantSession, forwardText).catch(
|
|
6207
|
-
(err) =>
|
|
6820
|
+
(err) => log21.error({ err }, "Assistant error")
|
|
6208
6821
|
);
|
|
6209
6822
|
return;
|
|
6210
6823
|
}
|
|
@@ -6221,7 +6834,7 @@ var TelegramAdapter = class extends ChannelAdapter {
|
|
|
6221
6834
|
threadId: String(threadId),
|
|
6222
6835
|
userId: String(ctx.from.id),
|
|
6223
6836
|
text: forwardText
|
|
6224
|
-
}).catch((err) =>
|
|
6837
|
+
}).catch((err) => log21.error({ err }, "handleMessage error"));
|
|
6225
6838
|
});
|
|
6226
6839
|
this.bot.on("message:photo", async (ctx) => {
|
|
6227
6840
|
const threadId = ctx.message.message_thread_id;
|
|
@@ -6296,9 +6909,10 @@ var TelegramAdapter = class extends ChannelAdapter {
|
|
|
6296
6909
|
if (this.assistantInitializing && sessionId === this.assistantSession?.id) return;
|
|
6297
6910
|
const session = this.core.sessionManager.getSession(sessionId);
|
|
6298
6911
|
if (!session) return;
|
|
6912
|
+
if (session.archiving) return;
|
|
6299
6913
|
const threadId = Number(session.threadId);
|
|
6300
6914
|
if (!threadId || isNaN(threadId)) {
|
|
6301
|
-
|
|
6915
|
+
log21.warn({ sessionId, threadId: session.threadId }, "Session has no valid threadId, skipping message");
|
|
6302
6916
|
return;
|
|
6303
6917
|
}
|
|
6304
6918
|
switch (content.type) {
|
|
@@ -6380,7 +6994,7 @@ Task completed.
|
|
|
6380
6994
|
if (!content.attachment) break;
|
|
6381
6995
|
const { attachment } = content;
|
|
6382
6996
|
if (attachment.size > 50 * 1024 * 1024) {
|
|
6383
|
-
|
|
6997
|
+
log21.warn({ sessionId, fileName: attachment.fileName, size: attachment.size }, "File too large for Telegram (>50MB)");
|
|
6384
6998
|
await this.sendQueue.enqueue(
|
|
6385
6999
|
() => this.bot.api.sendMessage(
|
|
6386
7000
|
this.telegramConfig.chatId,
|
|
@@ -6412,7 +7026,7 @@ Task completed.
|
|
|
6412
7026
|
);
|
|
6413
7027
|
}
|
|
6414
7028
|
} catch (err) {
|
|
6415
|
-
|
|
7029
|
+
log21.error({ err, sessionId, fileName: attachment.fileName }, "Failed to send attachment");
|
|
6416
7030
|
}
|
|
6417
7031
|
break;
|
|
6418
7032
|
}
|
|
@@ -6461,16 +7075,30 @@ Task completed.
|
|
|
6461
7075
|
);
|
|
6462
7076
|
break;
|
|
6463
7077
|
}
|
|
7078
|
+
case "system_message": {
|
|
7079
|
+
await this.sendQueue.enqueue(
|
|
7080
|
+
() => this.bot.api.sendMessage(
|
|
7081
|
+
this.telegramConfig.chatId,
|
|
7082
|
+
escapeHtml(content.text),
|
|
7083
|
+
{
|
|
7084
|
+
message_thread_id: threadId,
|
|
7085
|
+
parse_mode: "HTML",
|
|
7086
|
+
disable_notification: true
|
|
7087
|
+
}
|
|
7088
|
+
)
|
|
7089
|
+
);
|
|
7090
|
+
break;
|
|
7091
|
+
}
|
|
6464
7092
|
}
|
|
6465
7093
|
}
|
|
6466
7094
|
async sendPermissionRequest(sessionId, request) {
|
|
6467
|
-
|
|
7095
|
+
log21.info({ sessionId, requestId: request.id }, "Permission request sent");
|
|
6468
7096
|
const session = this.core.sessionManager.getSession(sessionId);
|
|
6469
7097
|
if (!session) return;
|
|
6470
7098
|
if (request.description.includes("openacp")) {
|
|
6471
7099
|
const allowOption = request.options.find((o) => o.isAllow);
|
|
6472
7100
|
if (allowOption && session.permissionGate.requestId === request.id) {
|
|
6473
|
-
|
|
7101
|
+
log21.info({ sessionId, requestId: request.id }, "Auto-approving openacp command");
|
|
6474
7102
|
session.permissionGate.resolve(allowOption.id);
|
|
6475
7103
|
}
|
|
6476
7104
|
return;
|
|
@@ -6478,7 +7106,7 @@ Task completed.
|
|
|
6478
7106
|
if (session.dangerousMode) {
|
|
6479
7107
|
const allowOption = request.options.find((o) => o.isAllow);
|
|
6480
7108
|
if (allowOption && session.permissionGate.requestId === request.id) {
|
|
6481
|
-
|
|
7109
|
+
log21.info({ sessionId, requestId: request.id, optionId: allowOption.id }, "Dangerous mode: auto-approving permission");
|
|
6482
7110
|
session.permissionGate.resolve(allowOption.id);
|
|
6483
7111
|
}
|
|
6484
7112
|
return;
|
|
@@ -6489,7 +7117,7 @@ Task completed.
|
|
|
6489
7117
|
}
|
|
6490
7118
|
async sendNotification(notification) {
|
|
6491
7119
|
if (notification.sessionId === this.assistantSession?.id) return;
|
|
6492
|
-
|
|
7120
|
+
log21.info(
|
|
6493
7121
|
{ sessionId: notification.sessionId, type: notification.type },
|
|
6494
7122
|
"Notification sent"
|
|
6495
7123
|
);
|
|
@@ -6525,7 +7153,7 @@ Task completed.
|
|
|
6525
7153
|
);
|
|
6526
7154
|
}
|
|
6527
7155
|
async createSessionThread(sessionId, name) {
|
|
6528
|
-
|
|
7156
|
+
log21.info({ sessionId, name }, "Session topic created");
|
|
6529
7157
|
return String(
|
|
6530
7158
|
await createSessionTopic(this.bot, this.telegramConfig.chatId, name)
|
|
6531
7159
|
);
|
|
@@ -6549,7 +7177,7 @@ Task completed.
|
|
|
6549
7177
|
try {
|
|
6550
7178
|
await this.bot.api.deleteForumTopic(this.telegramConfig.chatId, topicId);
|
|
6551
7179
|
} catch (err) {
|
|
6552
|
-
|
|
7180
|
+
log21.warn({ err, sessionId, topicId }, "Failed to delete forum topic (may already be deleted)");
|
|
6553
7181
|
}
|
|
6554
7182
|
}
|
|
6555
7183
|
async sendSkillCommands(sessionId, commands) {
|
|
@@ -6573,7 +7201,7 @@ Task completed.
|
|
|
6573
7201
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
6574
7202
|
return { buffer, filePath: file.file_path };
|
|
6575
7203
|
} catch (err) {
|
|
6576
|
-
|
|
7204
|
+
log21.error({ err }, "Failed to download file from Telegram");
|
|
6577
7205
|
return null;
|
|
6578
7206
|
}
|
|
6579
7207
|
}
|
|
@@ -6581,17 +7209,24 @@ Task completed.
|
|
|
6581
7209
|
const downloaded = await this.downloadTelegramFile(fileId);
|
|
6582
7210
|
if (!downloaded) return;
|
|
6583
7211
|
let buffer = downloaded.buffer;
|
|
7212
|
+
let originalFilePath;
|
|
7213
|
+
const sessionId = this.resolveSessionId(threadId) || "unknown";
|
|
6584
7214
|
if (convertOggToWav) {
|
|
7215
|
+
const oggAtt = await this.fileService.saveFile(sessionId, "voice.ogg", downloaded.buffer, "audio/ogg");
|
|
7216
|
+
originalFilePath = oggAtt.filePath;
|
|
6585
7217
|
try {
|
|
6586
7218
|
buffer = await this.fileService.convertOggToWav(buffer);
|
|
6587
7219
|
} catch (err) {
|
|
6588
|
-
|
|
7220
|
+
log21.warn({ err }, "OGG\u2192WAV conversion failed, saving original OGG");
|
|
6589
7221
|
fileName = "voice.ogg";
|
|
6590
7222
|
mimeType = "audio/ogg";
|
|
7223
|
+
originalFilePath = void 0;
|
|
6591
7224
|
}
|
|
6592
7225
|
}
|
|
6593
|
-
const sessionId = this.resolveSessionId(threadId) || "unknown";
|
|
6594
7226
|
const att = await this.fileService.saveFile(sessionId, fileName, buffer, mimeType);
|
|
7227
|
+
if (originalFilePath) {
|
|
7228
|
+
att.originalFilePath = originalFilePath;
|
|
7229
|
+
}
|
|
6595
7230
|
const rawText = caption || `[${att.type === "image" ? "Photo" : att.type === "audio" ? "Audio" : "File"}: ${att.fileName}]`;
|
|
6596
7231
|
const text = rawText.startsWith("/") ? rawText.slice(1) : rawText;
|
|
6597
7232
|
if (threadId === this.assistantTopicId) {
|
|
@@ -6608,11 +7243,52 @@ Task completed.
|
|
|
6608
7243
|
userId: String(userId),
|
|
6609
7244
|
text,
|
|
6610
7245
|
attachments: [att]
|
|
6611
|
-
}).catch((err) =>
|
|
7246
|
+
}).catch((err) => log21.error({ err }, "handleMessage error"));
|
|
6612
7247
|
}
|
|
6613
7248
|
async cleanupSkillCommands(sessionId) {
|
|
6614
7249
|
await this.skillManager.cleanup(sessionId);
|
|
6615
7250
|
}
|
|
7251
|
+
async archiveSessionTopic(sessionId) {
|
|
7252
|
+
const core = this.core;
|
|
7253
|
+
const session = core.sessionManager.getSession(sessionId);
|
|
7254
|
+
if (!session) return null;
|
|
7255
|
+
const chatId = this.telegramConfig.chatId;
|
|
7256
|
+
const oldTopicId = Number(session.threadId);
|
|
7257
|
+
const rawName = (session.name || `Session ${session.id.slice(0, 6)}`).replace(/^🔄\s*/, "");
|
|
7258
|
+
session.archiving = true;
|
|
7259
|
+
await this.draftManager.finalize(session.id, this.assistantSession?.id);
|
|
7260
|
+
this.draftManager.cleanup(session.id);
|
|
7261
|
+
this.toolTracker.cleanup(session.id);
|
|
7262
|
+
await this.skillManager.cleanup(session.id);
|
|
7263
|
+
const tracker = this.sessionTrackers.get(session.id);
|
|
7264
|
+
if (tracker) {
|
|
7265
|
+
tracker.destroy();
|
|
7266
|
+
this.sessionTrackers.delete(session.id);
|
|
7267
|
+
}
|
|
7268
|
+
await deleteSessionTopic(this.bot, chatId, oldTopicId);
|
|
7269
|
+
let newTopicId;
|
|
7270
|
+
try {
|
|
7271
|
+
newTopicId = await createSessionTopic(this.bot, chatId, `\u{1F504} ${rawName}`);
|
|
7272
|
+
} catch (createErr) {
|
|
7273
|
+
session.archiving = false;
|
|
7274
|
+
core.notificationManager.notifyAll({
|
|
7275
|
+
sessionId: session.id,
|
|
7276
|
+
sessionName: session.name,
|
|
7277
|
+
type: "error",
|
|
7278
|
+
summary: `Topic recreation failed for session "${rawName}". Session is orphaned. Error: ${createErr.message}`
|
|
7279
|
+
});
|
|
7280
|
+
throw createErr;
|
|
7281
|
+
}
|
|
7282
|
+
session.threadId = String(newTopicId);
|
|
7283
|
+
const existingRecord = core.sessionManager.getSessionRecord(session.id);
|
|
7284
|
+
const existingPlatform = { ...existingRecord?.platform ?? {} };
|
|
7285
|
+
delete existingPlatform.skillMsgId;
|
|
7286
|
+
await core.sessionManager.patchRecord(session.id, {
|
|
7287
|
+
platform: { ...existingPlatform, topicId: newTopicId }
|
|
7288
|
+
});
|
|
7289
|
+
session.archiving = false;
|
|
7290
|
+
return { newThreadId: String(newTopicId) };
|
|
7291
|
+
}
|
|
6616
7292
|
};
|
|
6617
7293
|
|
|
6618
7294
|
export {
|
|
@@ -6630,9 +7306,13 @@ export {
|
|
|
6630
7306
|
SessionBridge,
|
|
6631
7307
|
NotificationManager,
|
|
6632
7308
|
MessageTransformer,
|
|
7309
|
+
UsageStore,
|
|
7310
|
+
UsageBudget,
|
|
7311
|
+
SpeechService,
|
|
7312
|
+
GroqSTT,
|
|
6633
7313
|
OpenACPCore,
|
|
6634
7314
|
ApiServer,
|
|
6635
7315
|
TopicManager,
|
|
6636
7316
|
TelegramAdapter
|
|
6637
7317
|
};
|
|
6638
|
-
//# sourceMappingURL=chunk-
|
|
7318
|
+
//# sourceMappingURL=chunk-R3UJUOXI.js.map
|