appback-remoteagent 0.15.7 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +2 -0
- package/README.md +5 -0
- package/dist/bot.js +367 -19
- package/dist/config.js +2 -0
- package/dist/index.js +30 -37
- package/dist/services/agent-memory-service.js +66 -0
- package/dist/services/workspace-cleanup-service.js +150 -0
- package/dist/telegram-bot-identity.js +36 -0
- package/dist/telegram-command-menu.js +3 -0
- package/docs/OPERATIONS.md +73 -0
- package/docs/RELEASING.md +23 -0
- package/package.json +3 -2
- package/scripts/disk-maintenance.sh +216 -0
- package/scripts/selftest-telegram-update.mjs +183 -2
package/.env.example
CHANGED
|
@@ -18,6 +18,8 @@ TELEGRAM_RECOVERY_CHECK_INTERVAL_MS=60000
|
|
|
18
18
|
ARTIFACT_CLEANUP_ENABLED=true
|
|
19
19
|
ARTIFACT_RETENTION_DAYS=30
|
|
20
20
|
ARTIFACT_CLEANUP_INTERVAL_MS=86400000
|
|
21
|
+
WORKSPACE_CLEANUP_ENABLED=true
|
|
22
|
+
WORKSPACE_CLEANUP_INTERVAL_MS=86400000
|
|
21
23
|
REMOTEAGENT_WATCHDOG_ENABLED=true
|
|
22
24
|
REMOTEAGENT_WATCHDOG_INTERVAL_MS=300000
|
|
23
25
|
REMOTEAGENT_WATCHDOG_CPU_PERCENT=90
|
package/README.md
CHANGED
|
@@ -123,6 +123,9 @@ Current command surface implemented in `src/bot.ts`:
|
|
|
123
123
|
| `/batch done` | Alias for `/batch send` |
|
|
124
124
|
| `/batch cancel` | Discards the current batch |
|
|
125
125
|
| `/batch status` | Shows current batch state |
|
|
126
|
+
| `/queue` | Lists instructions waiting behind the active session work |
|
|
127
|
+
| `/queue remove <id>` | Removes one waiting instruction by its `Q001`-style id |
|
|
128
|
+
| `/queue del` | Removes the most recently queued instruction |
|
|
126
129
|
|
|
127
130
|
Multi-bot polling is tiered by recent activity and active provider work. See [docs/BOT_POLLING_POLICY.md](./docs/BOT_POLLING_POLICY.md).
|
|
128
131
|
|
|
@@ -182,6 +185,8 @@ Telegram attachments are written into the local runtime under `~/.remoteagent/up
|
|
|
182
185
|
|
|
183
186
|
Attachments can be inspected with `/artifacts list` and cleaned manually with `/artifacts cleanup <days>`. RemoteAgent also runs periodic artifact cleanup when `ARTIFACT_CLEANUP_ENABLED=true`; by default it keeps 30 days and also removes old unindexed files under `uploads/telegram`.
|
|
184
187
|
|
|
188
|
+
Managed workspaces are cleaned separately. RemoteAgent periodically removes only orphan workspace directories that are no longer referenced by `state.json` when `WORKSPACE_CLEANUP_ENABLED=true`. Use `/cleanup` to clear the current session workspace; RemoteAgent keeps the session todo/state/history under `~/.remoteagent/managed/sessions/<session>` and refuses non-RemoteAgent-managed workspaces.
|
|
189
|
+
|
|
185
190
|
Current supported attachment classes:
|
|
186
191
|
|
|
187
192
|
- photos
|
package/dist/bot.js
CHANGED
|
@@ -9,6 +9,7 @@ import { config } from "./config.js";
|
|
|
9
9
|
import { ProviderSetupService } from "./services/provider-setup-service.js";
|
|
10
10
|
import { RemoteShellService } from "./services/remote-shell-service.js";
|
|
11
11
|
import { AgentMemoryService } from "./services/agent-memory-service.js";
|
|
12
|
+
import { WorkspaceCleanupService } from "./services/workspace-cleanup-service.js";
|
|
12
13
|
import { deleteTelegramCommandMenu, setTelegramCommandMenu } from "./telegram-command-menu.js";
|
|
13
14
|
const execFileAsync = promisify(execFile);
|
|
14
15
|
const HELP_TEXT = [
|
|
@@ -23,14 +24,18 @@ const HELP_TEXT = [
|
|
|
23
24
|
"/attach codex <thread_id>",
|
|
24
25
|
"/attach claude <session_id>",
|
|
25
26
|
"/model [name]",
|
|
27
|
+
"/queue [remove <id>|del]",
|
|
26
28
|
"/stop",
|
|
27
29
|
"/sandbox codex <read-only|workspace-write|danger-full-access>",
|
|
28
30
|
"/status",
|
|
29
31
|
"/option [retry <count>|timeout <seconds>|intent <count>|command-menu <on|off|refresh>]",
|
|
30
32
|
"/state [clear|note <text>]",
|
|
31
33
|
"/artifacts list|cleanup <days>",
|
|
34
|
+
"/cleanup",
|
|
32
35
|
"/secret set|list|remove",
|
|
33
36
|
"/docs pin|find|list|remove|reinforce",
|
|
37
|
+
"/macro set|list|remove|<alias|number>",
|
|
38
|
+
"/매크로 set|list|remove|<alias|number>",
|
|
34
39
|
"/보강 <count>",
|
|
35
40
|
"/bots",
|
|
36
41
|
"/bot add <token>",
|
|
@@ -76,14 +81,17 @@ const RECOGNIZED_COMMANDS = new Set([
|
|
|
76
81
|
"batch",
|
|
77
82
|
"attach",
|
|
78
83
|
"model",
|
|
84
|
+
"queue",
|
|
79
85
|
"stop",
|
|
80
86
|
"sandbox",
|
|
81
87
|
"status",
|
|
82
88
|
"option",
|
|
83
89
|
"state",
|
|
84
90
|
"artifacts",
|
|
91
|
+
"cleanup",
|
|
85
92
|
"secret",
|
|
86
93
|
"docs",
|
|
94
|
+
"macro",
|
|
87
95
|
"bots",
|
|
88
96
|
"bot",
|
|
89
97
|
"install",
|
|
@@ -92,7 +100,10 @@ const RECOGNIZED_COMMANDS = new Set([
|
|
|
92
100
|
]);
|
|
93
101
|
const TELEGRAM_STALE_UPDATE_GRACE_SECONDS = 10;
|
|
94
102
|
const TELEGRAM_PROCESS_STARTED_AT_SECONDS = Math.floor(Date.now() / 1000);
|
|
95
|
-
const
|
|
103
|
+
const workLoopTails = new Map();
|
|
104
|
+
const workLoopGenerations = new Map();
|
|
105
|
+
const queuedWorkLoops = new Map();
|
|
106
|
+
let nextQueuedWorkSequence = 1;
|
|
96
107
|
const REPORT_CONTINUE_PROMPT = [
|
|
97
108
|
"Continue the same task now.",
|
|
98
109
|
"Do more concrete work before replying again.",
|
|
@@ -177,6 +188,7 @@ export function createBot(token, bridge, botManagement, botInfo) {
|
|
|
177
188
|
const autoContinue = new AutoContinueController(path.join(config.dataDir, "stop-gates.json"));
|
|
178
189
|
const shellService = new RemoteShellService(config.commandTimeoutMs);
|
|
179
190
|
const memoryService = new AgentMemoryService(config.dataDir);
|
|
191
|
+
const workspaceCleanupService = new WorkspaceCleanupService(config.dataDir, config.workspaceRoot);
|
|
180
192
|
const sourceBotToken = token;
|
|
181
193
|
const setupService = new ProviderSetupService(config.setupCommandTimeoutMs, (provider) => bridge.listAvailableProviders().includes(provider), {
|
|
182
194
|
codex: config.codexInstallCommand,
|
|
@@ -196,6 +208,7 @@ export function createBot(token, bridge, botManagement, botInfo) {
|
|
|
196
208
|
if (!previous) {
|
|
197
209
|
return;
|
|
198
210
|
}
|
|
211
|
+
cancelQueuedWorkLoops(botId, chatId, previous.session.sessionId);
|
|
199
212
|
autoContinue.requestSessionStop(previous.session.sessionId);
|
|
200
213
|
messageBatcher.cancelPending(botId, chatId);
|
|
201
214
|
messageBatcher.cancelManual(botId, chatId);
|
|
@@ -350,6 +363,7 @@ ${bridge.formatStatus(mapping)}`);
|
|
|
350
363
|
const mapping = await bridge.switchSession(botId, chatId, sessionId);
|
|
351
364
|
if (previous && previous.session.sessionId !== mapping.session.sessionId) {
|
|
352
365
|
autoContinue.requestSessionStop(previous.session.sessionId);
|
|
366
|
+
cancelQueuedWorkLoops(botId, chatId, previous.session.sessionId);
|
|
353
367
|
messageBatcher.cancelPending(botId, chatId);
|
|
354
368
|
messageBatcher.cancelManual(botId, chatId);
|
|
355
369
|
await bridge.stopSessionRun(previous.session.sessionId, botId, chatId, "Chat switched to another session; previous session execution was stopped.");
|
|
@@ -443,16 +457,47 @@ ${bridge.formatStatus(mapping)}`);
|
|
|
443
457
|
const mapping = await bridge.setModel(botId, chatId, model);
|
|
444
458
|
await reply(ctx, `Set ${mapping.session.mode} model to ${model}.\n\n${bridge.formatStatus(mapping)}`);
|
|
445
459
|
});
|
|
460
|
+
bot.command("queue", async (ctx) => {
|
|
461
|
+
const botId = getBotId();
|
|
462
|
+
const chatId = String(ctx.chat.id);
|
|
463
|
+
const mapping = await bridge.status(botId, chatId);
|
|
464
|
+
const activeKey = workLoopKey(botId, chatId, mapping?.session.sessionId);
|
|
465
|
+
const { args, rest } = parseCommand(ctx.message?.text, 2);
|
|
466
|
+
const action = args[0]?.toLowerCase();
|
|
467
|
+
const selector = args[1];
|
|
468
|
+
if (rest?.trim() || (action && action !== "list" && action !== "remove" && action !== "rm" && action !== "del")) {
|
|
469
|
+
await reply(ctx, "Usage: `/queue`, `/queue remove <id>`, or `/queue del`", { parse_mode: "Markdown" });
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
if (!action || action === "list") {
|
|
473
|
+
await reply(ctx, formatQueuedWorkLoops(activeKey, mapping?.session.publicId));
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if ((action === "remove" || action === "rm") && !selector) {
|
|
477
|
+
await reply(ctx, "Usage: `/queue remove <id>`", { parse_mode: "Markdown" });
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
const removed = removeQueuedWorkLoop(activeKey, selector);
|
|
481
|
+
if (!removed) {
|
|
482
|
+
const target = selector ? normalizeQueueId(selector) : "the latest queued instruction";
|
|
483
|
+
await reply(ctx, `Queued instruction was not found: ${target}`);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
await bridge.logSystem(botId, chatId, `Removed queued instruction ${removed.id} for ${activeKey}.`);
|
|
487
|
+
await reply(ctx, `Removed queued instruction ${removed.id} from ${removed.publicSessionId ?? "this session"}.\n`
|
|
488
|
+
+ `Remaining queued instructions: ${listQueuedWorkLoops(activeKey).length}`);
|
|
489
|
+
});
|
|
446
490
|
bot.command("stop", async (ctx) => {
|
|
447
491
|
const botId = getBotId();
|
|
448
492
|
const chatId = String(ctx.chat.id);
|
|
449
493
|
const mapping = await bridge.status(botId, chatId);
|
|
450
494
|
const sessionId = mapping?.session.sessionId;
|
|
451
495
|
autoContinue.requestStop(botId, chatId, sessionId);
|
|
496
|
+
const queuedWorkCount = cancelQueuedWorkLoops(botId, chatId, sessionId);
|
|
452
497
|
const pendingBatch = messageBatcher.cancelPending(botId, chatId);
|
|
453
498
|
const manualBatch = messageBatcher.cancelManual(botId, chatId);
|
|
454
499
|
if (!autoContinue.beginStop(botId, chatId, sessionId)) {
|
|
455
|
-
const batchCount = pendingBatch.count + manualBatch.count;
|
|
500
|
+
const batchCount = queuedWorkCount + pendingBatch.count + manualBatch.count;
|
|
456
501
|
if (batchCount > 0) {
|
|
457
502
|
await bridge.logSystem(botId, chatId, `Duplicate stop discarded ${batchCount} queued message(s).`);
|
|
458
503
|
}
|
|
@@ -461,7 +506,7 @@ ${bridge.formatStatus(mapping)}`);
|
|
|
461
506
|
try {
|
|
462
507
|
const result = await bridge.stopActiveRun(botId, chatId);
|
|
463
508
|
await bridge.logSystem(botId, chatId, "Stop requested for auto-continue.");
|
|
464
|
-
const batchCount = pendingBatch.count + manualBatch.count;
|
|
509
|
+
const batchCount = queuedWorkCount + pendingBatch.count + manualBatch.count;
|
|
465
510
|
await reply(ctx, result.stopped
|
|
466
511
|
? `Stop requested. Active work for ${result.sessionPublicId ?? "this session"} was interrupted, further automatic continuation will stop, and ${batchCount} queued message(s) were discarded.`
|
|
467
512
|
: `Stop requested. No active provider process was running, but further automatic continuation will stop, and ${batchCount} queued message(s) were discarded.`);
|
|
@@ -620,6 +665,19 @@ ${bridge.formatStatus(mapping)}`);
|
|
|
620
665
|
}
|
|
621
666
|
await reply(ctx, "Usage: `/artifacts list` or `/artifacts cleanup <days>`", { parse_mode: "Markdown" });
|
|
622
667
|
});
|
|
668
|
+
bot.command("cleanup", async (ctx) => {
|
|
669
|
+
await ensureOwnerControlAccess(ctx);
|
|
670
|
+
const botId = getBotId();
|
|
671
|
+
const chatId = String(ctx.chat.id);
|
|
672
|
+
const mapping = await bridge.status(botId, chatId);
|
|
673
|
+
if (!mapping) {
|
|
674
|
+
await reply(ctx, "No paired session for this chat yet.");
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
const result = await workspaceCleanupService.cleanupSessionWorkspace(mapping.session);
|
|
678
|
+
await bridge.logSystem(botId, chatId, `Workspace cleanup requested for ${mapping.session.publicId}.`);
|
|
679
|
+
await reply(ctx, result);
|
|
680
|
+
});
|
|
623
681
|
bot.command("secret", async (ctx) => {
|
|
624
682
|
await ensureOwnerControlAccess(ctx);
|
|
625
683
|
const { args, rest } = parseCommand(ctx.message?.text, 2);
|
|
@@ -700,6 +758,40 @@ ${bridge.formatStatus(mapping)}`);
|
|
|
700
758
|
}
|
|
701
759
|
await reply(ctx, "Usage: `/docs list`, `/docs find <keyword>`, `/docs pin <keyword> <path>`, `/docs remove <keyword>`, or `/docs reinforce <1-10>`", { parse_mode: "Markdown" });
|
|
702
760
|
});
|
|
761
|
+
const handleMacroCommand = async (ctx, text) => {
|
|
762
|
+
if (!ctx.chat) {
|
|
763
|
+
throw new Error("Telegram chat context is missing.");
|
|
764
|
+
}
|
|
765
|
+
const botId = getBotId();
|
|
766
|
+
const chatId = String(ctx.chat.id);
|
|
767
|
+
const parsed = parseMacroCommandText(text, botId);
|
|
768
|
+
if (parsed.kind === "help") {
|
|
769
|
+
await reply(ctx, await formatMacroHelp(memoryService));
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
if (parsed.kind === "set") {
|
|
773
|
+
await ensureOwnerControlAccess(ctx);
|
|
774
|
+
const macro = await memoryService.setMacro(parsed.alias, parsed.prompt);
|
|
775
|
+
await reply(ctx, `Saved macro '${macro.alias}'.`);
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
if (parsed.kind === "remove") {
|
|
779
|
+
await ensureOwnerControlAccess(ctx);
|
|
780
|
+
const removed = await memoryService.removeMacro(parsed.alias);
|
|
781
|
+
await reply(ctx, removed ? `Removed macro '${parsed.alias}'.` : `Macro was not found: ${parsed.alias}`);
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
const macro = await memoryService.getMacro(parsed.target);
|
|
785
|
+
if (!macro) {
|
|
786
|
+
await reply(ctx, `Macro was not found: ${parsed.target}\n\n${await memoryService.listMacros()}`);
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
await bridge.logSystem(botId, chatId, `Macro executed: ${macro.alias}`);
|
|
790
|
+
await messageBatcher.enqueue({ botToken: token, telegramChatId: ctx.chat.id }, botId, chatId, macro.prompt);
|
|
791
|
+
};
|
|
792
|
+
bot.command("macro", async (ctx) => {
|
|
793
|
+
await handleMacroCommand(ctx, ctx.message?.text ?? "/macro");
|
|
794
|
+
});
|
|
703
795
|
bot.command("bots", async (ctx) => {
|
|
704
796
|
await ensureOwnerControlAccess(ctx);
|
|
705
797
|
const pendingNotice = await botManagement.getPendingOperationNotice();
|
|
@@ -825,6 +917,11 @@ ${bridge.formatStatus(mapping)}`);
|
|
|
825
917
|
await reply(ctx, "Usage: `/보강 <1-10>`", { parse_mode: "Markdown" });
|
|
826
918
|
return;
|
|
827
919
|
}
|
|
920
|
+
const koreanMacro = parseKoreanMacroCommand(text, botId);
|
|
921
|
+
if (koreanMacro) {
|
|
922
|
+
await handleMacroCommand(ctx, text ?? "/매크로");
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
828
925
|
if (text && isRecognizedSlashCommand(text, botId)) {
|
|
829
926
|
return;
|
|
830
927
|
}
|
|
@@ -1155,22 +1252,70 @@ async function routeTelegramWorkLoop(bridge, botId, chatId, message, label, botM
|
|
|
1155
1252
|
const currentSession = await bridge.status(botId, chatId);
|
|
1156
1253
|
const sessionId = currentSession?.session.sessionId;
|
|
1157
1254
|
const activeKey = workLoopKey(botId, chatId, sessionId);
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1255
|
+
const previousTail = workLoopTails.get(activeKey);
|
|
1256
|
+
const generation = workLoopGenerations.get(activeKey) ?? 0;
|
|
1257
|
+
let release;
|
|
1258
|
+
const gate = new Promise((resolve) => {
|
|
1259
|
+
release = resolve;
|
|
1260
|
+
});
|
|
1261
|
+
const currentTail = (previousTail ?? Promise.resolve())
|
|
1262
|
+
.catch(() => undefined)
|
|
1263
|
+
.then(() => gate);
|
|
1264
|
+
workLoopTails.set(activeKey, currentTail);
|
|
1265
|
+
let queuedEntry;
|
|
1266
|
+
if (previousTail) {
|
|
1267
|
+
queuedEntry = registerQueuedWorkLoop({
|
|
1268
|
+
activeKey,
|
|
1269
|
+
botId,
|
|
1270
|
+
chatId,
|
|
1271
|
+
sessionId,
|
|
1272
|
+
publicSessionId: currentSession?.session.publicId,
|
|
1273
|
+
message,
|
|
1274
|
+
release,
|
|
1275
|
+
});
|
|
1276
|
+
try {
|
|
1277
|
+
await bridge.logSystem(botId, chatId, `Queued overlapping Telegram work loop ${queuedEntry.id} for ${activeKey}.`);
|
|
1278
|
+
await helpers.reportProgress([
|
|
1279
|
+
`Queued instruction ${queuedEntry.id} for ${currentSession?.session.publicId ?? "this session"}. It will run after the active work finishes.`,
|
|
1280
|
+
`Remove it with \`/queue remove ${queuedEntry.id}\`, or remove the latest queued instruction with \`/queue del\`.`,
|
|
1281
|
+
]);
|
|
1282
|
+
await previousTail.catch(() => undefined);
|
|
1283
|
+
}
|
|
1284
|
+
catch (error) {
|
|
1285
|
+
queuedWorkLoops.delete(queuedEntry.id);
|
|
1286
|
+
release();
|
|
1287
|
+
if (workLoopTails.get(activeKey) === currentTail) {
|
|
1288
|
+
workLoopTails.delete(activeKey);
|
|
1289
|
+
}
|
|
1290
|
+
throw error;
|
|
1291
|
+
}
|
|
1292
|
+
queuedWorkLoops.delete(queuedEntry.id);
|
|
1293
|
+
if (queuedEntry.canceled || (workLoopGenerations.get(activeKey) ?? 0) !== generation || autoContinue.isStopRequested(botId, chatId, sessionId)) {
|
|
1294
|
+
await bridge.logSystem(botId, chatId, `Discarded queued Telegram work loop ${queuedEntry.id} for ${activeKey}.`);
|
|
1295
|
+
release();
|
|
1296
|
+
if (workLoopTails.get(activeKey) === currentTail) {
|
|
1297
|
+
workLoopTails.delete(activeKey);
|
|
1298
|
+
}
|
|
1299
|
+
throw new SilentTelegramAbort("Queued work was discarded.");
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
let managedContext = "";
|
|
1303
|
+
try {
|
|
1304
|
+
if (currentSession) {
|
|
1305
|
+
await memoryService.recordInstruction(currentSession.session, message);
|
|
1306
|
+
}
|
|
1307
|
+
managedContext = currentSession
|
|
1308
|
+
? await memoryService.formatProviderContext(currentSession.session)
|
|
1309
|
+
: "";
|
|
1310
|
+
autoContinue.clear(botId, chatId, sessionId);
|
|
1311
|
+
}
|
|
1312
|
+
catch (error) {
|
|
1313
|
+
release();
|
|
1314
|
+
if (workLoopTails.get(activeKey) === currentTail) {
|
|
1315
|
+
workLoopTails.delete(activeKey);
|
|
1316
|
+
}
|
|
1317
|
+
throw error;
|
|
1169
1318
|
}
|
|
1170
|
-
const managedContext = currentSession
|
|
1171
|
-
? await memoryService.formatProviderContext(currentSession.session)
|
|
1172
|
-
: "";
|
|
1173
|
-
autoContinue.clear(botId, chatId, sessionId);
|
|
1174
1319
|
let prompt = appendManagedContext(appendReportProtocol(message), managedContext);
|
|
1175
1320
|
const maxTurns = options.maxTurns ?? config.telegramAutoProgressMaxTurns;
|
|
1176
1321
|
const emptyResponseRetries = config.telegramEmptyResponseRetries;
|
|
@@ -1351,7 +1496,10 @@ async function routeTelegramWorkLoop(bridge, botId, chatId, message, label, botM
|
|
|
1351
1496
|
}
|
|
1352
1497
|
}
|
|
1353
1498
|
finally {
|
|
1354
|
-
|
|
1499
|
+
release();
|
|
1500
|
+
if (workLoopTails.get(activeKey) === currentTail) {
|
|
1501
|
+
workLoopTails.delete(activeKey);
|
|
1502
|
+
}
|
|
1355
1503
|
if (currentSession) {
|
|
1356
1504
|
if (providerCompleted) {
|
|
1357
1505
|
await botManagement.markProviderCompleted(botId, sessionId);
|
|
@@ -1365,6 +1513,88 @@ async function routeTelegramWorkLoop(bridge, botId, chatId, message, label, botM
|
|
|
1365
1513
|
function workLoopKey(botId, chatId, sessionId) {
|
|
1366
1514
|
return sessionId ? `session:${sessionId}` : `chat:${botId}:${chatId}`;
|
|
1367
1515
|
}
|
|
1516
|
+
function cancelQueuedWorkLoops(botId, chatId, sessionId) {
|
|
1517
|
+
const keys = [workLoopKey(botId, chatId, sessionId)];
|
|
1518
|
+
if (sessionId) {
|
|
1519
|
+
keys.push(workLoopKey(botId, chatId));
|
|
1520
|
+
}
|
|
1521
|
+
const keySet = new Set(keys);
|
|
1522
|
+
let removed = 0;
|
|
1523
|
+
for (const entry of queuedWorkLoops.values()) {
|
|
1524
|
+
if (!keySet.has(entry.activeKey)) {
|
|
1525
|
+
continue;
|
|
1526
|
+
}
|
|
1527
|
+
entry.canceled = true;
|
|
1528
|
+
entry.release();
|
|
1529
|
+
queuedWorkLoops.delete(entry.id);
|
|
1530
|
+
removed += 1;
|
|
1531
|
+
}
|
|
1532
|
+
for (const key of keys) {
|
|
1533
|
+
workLoopGenerations.set(key, (workLoopGenerations.get(key) ?? 0) + 1);
|
|
1534
|
+
}
|
|
1535
|
+
return removed;
|
|
1536
|
+
}
|
|
1537
|
+
function registerQueuedWorkLoop(input) {
|
|
1538
|
+
const sequence = nextQueuedWorkSequence;
|
|
1539
|
+
nextQueuedWorkSequence += 1;
|
|
1540
|
+
const entry = {
|
|
1541
|
+
id: `Q${String(sequence).padStart(3, "0")}`,
|
|
1542
|
+
sequence,
|
|
1543
|
+
activeKey: input.activeKey,
|
|
1544
|
+
botId: input.botId,
|
|
1545
|
+
chatId: input.chatId,
|
|
1546
|
+
sessionId: input.sessionId,
|
|
1547
|
+
publicSessionId: input.publicSessionId,
|
|
1548
|
+
messagePreview: summarizeQueuedInstruction(input.message),
|
|
1549
|
+
createdAt: new Date().toISOString(),
|
|
1550
|
+
canceled: false,
|
|
1551
|
+
release: input.release,
|
|
1552
|
+
};
|
|
1553
|
+
queuedWorkLoops.set(entry.id, entry);
|
|
1554
|
+
return entry;
|
|
1555
|
+
}
|
|
1556
|
+
function listQueuedWorkLoops(activeKey) {
|
|
1557
|
+
return [...queuedWorkLoops.values()]
|
|
1558
|
+
.filter((entry) => entry.activeKey === activeKey && !entry.canceled)
|
|
1559
|
+
.sort((left, right) => left.sequence - right.sequence);
|
|
1560
|
+
}
|
|
1561
|
+
function removeQueuedWorkLoop(activeKey, selector) {
|
|
1562
|
+
const entries = listQueuedWorkLoops(activeKey);
|
|
1563
|
+
const target = selector
|
|
1564
|
+
? queuedWorkLoops.get(normalizeQueueId(selector))
|
|
1565
|
+
: entries.at(-1);
|
|
1566
|
+
if (!target || target.activeKey !== activeKey || target.canceled) {
|
|
1567
|
+
return undefined;
|
|
1568
|
+
}
|
|
1569
|
+
target.canceled = true;
|
|
1570
|
+
target.release();
|
|
1571
|
+
queuedWorkLoops.delete(target.id);
|
|
1572
|
+
return target;
|
|
1573
|
+
}
|
|
1574
|
+
function normalizeQueueId(value) {
|
|
1575
|
+
const normalized = value.trim().toUpperCase();
|
|
1576
|
+
if (/^\d+$/.test(normalized)) {
|
|
1577
|
+
return `Q${normalized.padStart(3, "0")}`;
|
|
1578
|
+
}
|
|
1579
|
+
return normalized;
|
|
1580
|
+
}
|
|
1581
|
+
function summarizeQueuedInstruction(message) {
|
|
1582
|
+
const normalized = message.replace(/\s+/g, " ").trim();
|
|
1583
|
+
return normalized.length > 100 ? `${normalized.slice(0, 97)}...` : normalized;
|
|
1584
|
+
}
|
|
1585
|
+
function formatQueuedWorkLoops(activeKey, publicSessionId) {
|
|
1586
|
+
const entries = listQueuedWorkLoops(activeKey);
|
|
1587
|
+
if (entries.length === 0) {
|
|
1588
|
+
return `No queued instructions for ${publicSessionId ?? "this chat"}.`;
|
|
1589
|
+
}
|
|
1590
|
+
return [
|
|
1591
|
+
`Queued instructions for ${publicSessionId ?? "this chat"} (${entries.length})`,
|
|
1592
|
+
...entries.map((entry) => `${entry.id}: ${entry.messagePreview || "(empty instruction)"}`),
|
|
1593
|
+
"",
|
|
1594
|
+
`Remove one: /queue remove ${entries[0].id}`,
|
|
1595
|
+
"Remove latest: /queue del",
|
|
1596
|
+
].join("\n");
|
|
1597
|
+
}
|
|
1368
1598
|
class SilentTelegramAbort extends Error {
|
|
1369
1599
|
constructor(message) {
|
|
1370
1600
|
super(message);
|
|
@@ -1711,6 +1941,124 @@ function parseCommand(text, headCount) {
|
|
|
1711
1941
|
rest: remaining || undefined,
|
|
1712
1942
|
};
|
|
1713
1943
|
}
|
|
1944
|
+
function parseMacroCommandText(text, botId) {
|
|
1945
|
+
const body = stripSlashCommand(text, ["macro", "매크로"], botId).trim();
|
|
1946
|
+
if (!body) {
|
|
1947
|
+
return { kind: "help" };
|
|
1948
|
+
}
|
|
1949
|
+
const tokens = tokenizeCommandBody(body);
|
|
1950
|
+
const action = tokens[0]?.toLowerCase();
|
|
1951
|
+
if (action === "list") {
|
|
1952
|
+
return { kind: "help" };
|
|
1953
|
+
}
|
|
1954
|
+
if (action === "set") {
|
|
1955
|
+
const alias = tokens[1]?.trim();
|
|
1956
|
+
const prompt = tokens.length >= 3
|
|
1957
|
+
? tokens.slice(2).join(" ").trim()
|
|
1958
|
+
: "";
|
|
1959
|
+
if (!alias || !prompt) {
|
|
1960
|
+
throw new Error("Usage: /macro set <alias> <instruction>");
|
|
1961
|
+
}
|
|
1962
|
+
return { kind: "set", alias, prompt };
|
|
1963
|
+
}
|
|
1964
|
+
if (action === "remove" || action === "rm" || action === "delete" || action === "del" || action === "clear") {
|
|
1965
|
+
const alias = tokens[1]?.trim();
|
|
1966
|
+
if (!alias || tokens.length > 2) {
|
|
1967
|
+
throw new Error("Usage: /macro remove <alias>");
|
|
1968
|
+
}
|
|
1969
|
+
return { kind: "remove", alias };
|
|
1970
|
+
}
|
|
1971
|
+
return { kind: "run", target: tokens.length === 1 ? tokens[0] : body };
|
|
1972
|
+
}
|
|
1973
|
+
function parseKoreanMacroCommand(text, botId) {
|
|
1974
|
+
return Boolean(text && stripSlashCommand(text, ["매크로"], botId) !== text);
|
|
1975
|
+
}
|
|
1976
|
+
function stripSlashCommand(text, names, botId) {
|
|
1977
|
+
const trimmed = text?.trim() ?? "";
|
|
1978
|
+
for (const name of names) {
|
|
1979
|
+
const escaped = escapeRegExp(name);
|
|
1980
|
+
const username = botId ? `(?:@${escapeRegExp(botId.replace(/^@/, ""))})?` : "(?:@\\w+)?";
|
|
1981
|
+
const pattern = new RegExp(`^/${escaped}${username}(?:\\s+|$)`, "i");
|
|
1982
|
+
if (pattern.test(trimmed)) {
|
|
1983
|
+
return trimmed.replace(pattern, "");
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
return trimmed;
|
|
1987
|
+
}
|
|
1988
|
+
function tokenizeCommandBody(input) {
|
|
1989
|
+
const tokens = [];
|
|
1990
|
+
let current = "";
|
|
1991
|
+
let quote;
|
|
1992
|
+
let escaped = false;
|
|
1993
|
+
for (const char of input) {
|
|
1994
|
+
if (escaped) {
|
|
1995
|
+
current += char;
|
|
1996
|
+
escaped = false;
|
|
1997
|
+
continue;
|
|
1998
|
+
}
|
|
1999
|
+
if (char === "\\") {
|
|
2000
|
+
escaped = true;
|
|
2001
|
+
continue;
|
|
2002
|
+
}
|
|
2003
|
+
if (quote) {
|
|
2004
|
+
if (char === quote) {
|
|
2005
|
+
quote = undefined;
|
|
2006
|
+
}
|
|
2007
|
+
else {
|
|
2008
|
+
current += char;
|
|
2009
|
+
}
|
|
2010
|
+
continue;
|
|
2011
|
+
}
|
|
2012
|
+
if (char === "'" || char === "\"") {
|
|
2013
|
+
quote = char;
|
|
2014
|
+
continue;
|
|
2015
|
+
}
|
|
2016
|
+
if (/\s/.test(char)) {
|
|
2017
|
+
if (current) {
|
|
2018
|
+
tokens.push(current);
|
|
2019
|
+
current = "";
|
|
2020
|
+
}
|
|
2021
|
+
continue;
|
|
2022
|
+
}
|
|
2023
|
+
current += char;
|
|
2024
|
+
}
|
|
2025
|
+
if (escaped) {
|
|
2026
|
+
current += "\\";
|
|
2027
|
+
}
|
|
2028
|
+
if (quote) {
|
|
2029
|
+
throw new Error("Unclosed quote in command.");
|
|
2030
|
+
}
|
|
2031
|
+
if (current) {
|
|
2032
|
+
tokens.push(current);
|
|
2033
|
+
}
|
|
2034
|
+
return tokens;
|
|
2035
|
+
}
|
|
2036
|
+
function escapeRegExp(value) {
|
|
2037
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2038
|
+
}
|
|
2039
|
+
async function formatMacroHelp(memoryService) {
|
|
2040
|
+
return [
|
|
2041
|
+
"Macro command guide",
|
|
2042
|
+
"",
|
|
2043
|
+
"Commands:",
|
|
2044
|
+
"```text",
|
|
2045
|
+
"/macro",
|
|
2046
|
+
"/macro set 초기설정 너의 역할은 ...",
|
|
2047
|
+
"/macro set '초기설정' '너의 역할은 ...'",
|
|
2048
|
+
"/macro 초기설정",
|
|
2049
|
+
"/macro 1",
|
|
2050
|
+
"/macro remove 초기설정",
|
|
2051
|
+
"/매크로",
|
|
2052
|
+
"/매크로 초기설정",
|
|
2053
|
+
"```",
|
|
2054
|
+
"",
|
|
2055
|
+
"Rules:",
|
|
2056
|
+
"- Macro aliases can use Korean, letters, numbers, dot, underscore, and dash.",
|
|
2057
|
+
"- Numeric-only aliases are reserved for list selection and cannot be registered.",
|
|
2058
|
+
"",
|
|
2059
|
+
await memoryService.listMacros(),
|
|
2060
|
+
].join("\n");
|
|
2061
|
+
}
|
|
1714
2062
|
function parsePlanReinforcementCount(raw) {
|
|
1715
2063
|
const value = raw?.trim();
|
|
1716
2064
|
if (!value) {
|
package/dist/config.js
CHANGED
|
@@ -152,6 +152,8 @@ export const config = {
|
|
|
152
152
|
artifactCleanupEnabled: readBoolean("ARTIFACT_CLEANUP_ENABLED", true),
|
|
153
153
|
artifactRetentionDays: readTimeout("ARTIFACT_RETENTION_DAYS", 30),
|
|
154
154
|
artifactCleanupIntervalMs: readTimeout("ARTIFACT_CLEANUP_INTERVAL_MS", 86_400_000),
|
|
155
|
+
workspaceCleanupEnabled: readBoolean("WORKSPACE_CLEANUP_ENABLED", true),
|
|
156
|
+
workspaceCleanupIntervalMs: readTimeout("WORKSPACE_CLEANUP_INTERVAL_MS", 86_400_000),
|
|
155
157
|
dataDir: defaultDataDir,
|
|
156
158
|
defaultMode: readMode("DEFAULT_MODE", "codex"),
|
|
157
159
|
defaultWorkspace: path.resolve(process.env.DEFAULT_WORKSPACE?.trim() || os.homedir()),
|
package/dist/index.js
CHANGED
|
@@ -14,11 +14,13 @@ import { BridgeService } from "./services/bridge-service.js";
|
|
|
14
14
|
import { BotManagementService } from "./services/bot-management-service.js";
|
|
15
15
|
import { LocalUiService } from "./services/local-ui-service.js";
|
|
16
16
|
import { AgentMemoryService } from "./services/agent-memory-service.js";
|
|
17
|
+
import { WorkspaceCleanupService } from "./services/workspace-cleanup-service.js";
|
|
17
18
|
import { BotPollingStateService } from "./services/bot-polling-state-service.js";
|
|
18
19
|
import { ProviderRecoveryService } from "./services/provider-recovery-service.js";
|
|
19
20
|
import { computePolicyPollIntervalMs, computeRecentMessageRanks } from "./services/polling-policy.js";
|
|
20
21
|
import { terminateAllSpawnedExecutions } from "./adapters/windows-shell.js";
|
|
21
22
|
import { setTelegramCommandMenu } from "./telegram-command-menu.js";
|
|
23
|
+
import { buildBotInfoFromIdentity, buildFallbackBotInfo } from "./telegram-bot-identity.js";
|
|
22
24
|
const execFileAsync = promisify(execFile);
|
|
23
25
|
const TELEGRAM_GET_UPDATES_HTTP_TIMEOUT_SECONDS = 30;
|
|
24
26
|
const TELEGRAM_GET_UPDATES_CURL_TIMEOUT_SECONDS = 60;
|
|
@@ -56,6 +58,7 @@ async function main() {
|
|
|
56
58
|
const bridge = new BridgeService(store, adapters, config.defaultWorkspace, config.workspaceRoot, isProviderInstalled, config.defaultMode, config.codexSandboxMode);
|
|
57
59
|
const botManagement = new BotManagementService(config.dataDir, config.botRestartServiceName, config.botRestartHelperPath, botPollingState);
|
|
58
60
|
startArtifactCleanupSchedule(new AgentMemoryService(config.dataDir));
|
|
61
|
+
startWorkspaceCleanupSchedule(new WorkspaceCleanupService(config.dataDir, config.workspaceRoot));
|
|
59
62
|
if (config.localUiEnabled) {
|
|
60
63
|
const localUi = new LocalUiService(bridge, config.localUiHost, config.localUiPort);
|
|
61
64
|
await localUi.start()
|
|
@@ -66,7 +69,7 @@ async function main() {
|
|
|
66
69
|
console.error("Local UI failed to start:", error);
|
|
67
70
|
});
|
|
68
71
|
}
|
|
69
|
-
const botInfos = await Promise.all(config.telegramBotTokens.map((token, index) => resolveBotInfo(token, index)));
|
|
72
|
+
const botInfos = await Promise.all(config.telegramBotTokens.map((token, index) => resolveBotInfo(token, index, config.telegramBotUsernames[index])));
|
|
70
73
|
const bots = config.telegramBotTokens.map((token, index) => createBot(token, bridge, botManagement, botInfos[index]));
|
|
71
74
|
if (config.telegramCommandMenuEnabled) {
|
|
72
75
|
for (const bot of bots) {
|
|
@@ -88,6 +91,30 @@ async function main() {
|
|
|
88
91
|
});
|
|
89
92
|
await startManualPollingScheduler(bots);
|
|
90
93
|
}
|
|
94
|
+
function startWorkspaceCleanupSchedule(workspaceCleanupService) {
|
|
95
|
+
if (!config.workspaceCleanupEnabled) {
|
|
96
|
+
console.log("Workspace cleanup schedule is disabled.");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const run = async () => {
|
|
100
|
+
try {
|
|
101
|
+
const result = await workspaceCleanupService.cleanupOrphanWorkspaces();
|
|
102
|
+
console.log(`[workspace-cleanup] ${result}`);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
console.error("[workspace-cleanup] failed:", error);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
console.log(`Workspace cleanup schedule enabled: interval=${config.workspaceCleanupIntervalMs}ms`);
|
|
109
|
+
const initial = setTimeout(() => {
|
|
110
|
+
void run();
|
|
111
|
+
}, 120_000);
|
|
112
|
+
initial.unref();
|
|
113
|
+
const interval = setInterval(() => {
|
|
114
|
+
void run();
|
|
115
|
+
}, config.workspaceCleanupIntervalMs);
|
|
116
|
+
interval.unref();
|
|
117
|
+
}
|
|
91
118
|
function startArtifactCleanupSchedule(memoryService) {
|
|
92
119
|
if (!config.artifactCleanupEnabled) {
|
|
93
120
|
console.log("Artifact cleanup schedule is disabled.");
|
|
@@ -458,7 +485,7 @@ class AsyncSemaphore {
|
|
|
458
485
|
}
|
|
459
486
|
}
|
|
460
487
|
}
|
|
461
|
-
async function resolveBotInfo(token, index) {
|
|
488
|
+
async function resolveBotInfo(token, index, configuredUsername) {
|
|
462
489
|
try {
|
|
463
490
|
const { stdout } = await execFileAsync("curl", [
|
|
464
491
|
"-sS",
|
|
@@ -476,32 +503,7 @@ async function resolveBotInfo(token, index) {
|
|
|
476
503
|
catch (error) {
|
|
477
504
|
console.warn(`Telegram getMe failed for bot ${tokenIdLabel(token)}: ${summarizeTelegramIdentityError(error)}`);
|
|
478
505
|
}
|
|
479
|
-
return buildFallbackBotInfo(token, index);
|
|
480
|
-
}
|
|
481
|
-
function buildBotInfoFromIdentity(id, username, firstName) {
|
|
482
|
-
return {
|
|
483
|
-
id,
|
|
484
|
-
is_bot: true,
|
|
485
|
-
first_name: firstName || username,
|
|
486
|
-
username,
|
|
487
|
-
can_join_groups: false,
|
|
488
|
-
can_read_all_group_messages: false,
|
|
489
|
-
supports_inline_queries: false,
|
|
490
|
-
};
|
|
491
|
-
}
|
|
492
|
-
function buildFallbackBotInfo(token, index) {
|
|
493
|
-
const id = Number.parseInt(token.split(":", 1)[0] ?? "", 10);
|
|
494
|
-
const fallbackUsername = knownBotUsername(id);
|
|
495
|
-
const username = fallbackUsername || `bot_${Number.isFinite(id) ? id : index + 1}`;
|
|
496
|
-
return {
|
|
497
|
-
id: Number.isFinite(id) ? id : index + 1,
|
|
498
|
-
is_bot: true,
|
|
499
|
-
first_name: username,
|
|
500
|
-
username,
|
|
501
|
-
can_join_groups: false,
|
|
502
|
-
can_read_all_group_messages: false,
|
|
503
|
-
supports_inline_queries: false,
|
|
504
|
-
};
|
|
506
|
+
return buildFallbackBotInfo(token, index, configuredUsername);
|
|
505
507
|
}
|
|
506
508
|
function tokenIdLabel(token) {
|
|
507
509
|
return token.split(":", 1)[0] || "unknown";
|
|
@@ -517,15 +519,6 @@ function summarizeTelegramIdentityError(error) {
|
|
|
517
519
|
.filter(Boolean)
|
|
518
520
|
.join(" ");
|
|
519
521
|
}
|
|
520
|
-
function knownBotUsername(id) {
|
|
521
|
-
if (id === 8369496408) {
|
|
522
|
-
return "codex_remoteagent_bot";
|
|
523
|
-
}
|
|
524
|
-
if (id === 8429712341) {
|
|
525
|
-
return "sqream_bot";
|
|
526
|
-
}
|
|
527
|
-
return undefined;
|
|
528
|
-
}
|
|
529
522
|
function commandExists(command) {
|
|
530
523
|
const trimmed = command.trim();
|
|
531
524
|
if (!trimmed) {
|