appback-remoteagent 0.17.0 → 0.18.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/README.md +5 -0
- package/dist/adapters/codex-adapter.js +40 -3
- package/dist/adapters/windows-shell.js +27 -3
- package/dist/bot.js +351 -74
- package/dist/services/agent-memory-service.js +6 -4
- package/dist/services/bot-management-service.js +3 -0
- package/dist/services/bridge-service.js +44 -12
- package/docs/RELEASING.md +45 -0
- package/package.json +2 -1
- package/scripts/selftest-codex-stream.mjs +70 -0
- package/scripts/selftest-telegram-update.mjs +192 -5
package/dist/bot.js
CHANGED
|
@@ -104,6 +104,41 @@ const workLoopTails = new Map();
|
|
|
104
104
|
const workLoopGenerations = new Map();
|
|
105
105
|
const queuedWorkLoops = new Map();
|
|
106
106
|
let nextQueuedWorkSequence = 1;
|
|
107
|
+
class InlineActionRegistry {
|
|
108
|
+
records = new Map();
|
|
109
|
+
ttlMs = 6 * 60 * 60 * 1000;
|
|
110
|
+
maxRecords = 1_000;
|
|
111
|
+
register(action, chatId, userId) {
|
|
112
|
+
this.prune();
|
|
113
|
+
while (this.records.size >= this.maxRecords) {
|
|
114
|
+
const oldest = this.records.keys().next().value;
|
|
115
|
+
if (!oldest) {
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
this.records.delete(oldest);
|
|
119
|
+
}
|
|
120
|
+
const id = randomUUID().replace(/-/g, "").slice(0, 20);
|
|
121
|
+
this.records.set(id, { action, chatId, userId, createdAt: Date.now() });
|
|
122
|
+
return `remoteagent:action:${id}`;
|
|
123
|
+
}
|
|
124
|
+
resolve(data, chatId, userId) {
|
|
125
|
+
this.prune();
|
|
126
|
+
const match = /^remoteagent:action:([a-f0-9]{20})$/i.exec(data);
|
|
127
|
+
const record = match ? this.records.get(match[1]) : undefined;
|
|
128
|
+
if (!record || record.chatId !== chatId || record.userId !== userId) {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
return record.action;
|
|
132
|
+
}
|
|
133
|
+
prune() {
|
|
134
|
+
const expiresBefore = Date.now() - this.ttlMs;
|
|
135
|
+
for (const [id, record] of this.records) {
|
|
136
|
+
if (record.createdAt < expiresBefore) {
|
|
137
|
+
this.records.delete(id);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
107
142
|
const REPORT_CONTINUE_PROMPT = [
|
|
108
143
|
"Continue the same task now.",
|
|
109
144
|
"Do more concrete work before replying again.",
|
|
@@ -185,6 +220,7 @@ class AutoContinueController {
|
|
|
185
220
|
}
|
|
186
221
|
export function createBot(token, bridge, botManagement, botInfo) {
|
|
187
222
|
const bot = new Bot(token, { botInfo });
|
|
223
|
+
const inlineActions = new InlineActionRegistry();
|
|
188
224
|
const autoContinue = new AutoContinueController(path.join(config.dataDir, "stop-gates.json"));
|
|
189
225
|
const shellService = new RemoteShellService(config.commandTimeoutMs);
|
|
190
226
|
const memoryService = new AgentMemoryService(config.dataDir);
|
|
@@ -229,6 +265,32 @@ export function createBot(token, bridge, botManagement, botInfo) {
|
|
|
229
265
|
throw error;
|
|
230
266
|
}
|
|
231
267
|
};
|
|
268
|
+
const actionButton = (ctx, text, action) => {
|
|
269
|
+
if (!ctx.chat || !ctx.from) {
|
|
270
|
+
throw new Error("Telegram action button requires chat and user context.");
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
text,
|
|
274
|
+
callback_data: inlineActions.register(action, String(ctx.chat.id), String(ctx.from.id)),
|
|
275
|
+
};
|
|
276
|
+
};
|
|
277
|
+
const keyboardOptions = (rows) => {
|
|
278
|
+
return rows.length > 0
|
|
279
|
+
? { reply_markup: JSON.stringify({ inline_keyboard: rows }) }
|
|
280
|
+
: undefined;
|
|
281
|
+
};
|
|
282
|
+
const removeQueuedInstruction = async (botId, chatId, selector) => {
|
|
283
|
+
const mapping = await bridge.status(botId, chatId);
|
|
284
|
+
const activeKey = workLoopKey(botId, chatId, mapping?.session.sessionId);
|
|
285
|
+
const removed = removeQueuedWorkLoop(activeKey, selector);
|
|
286
|
+
if (!removed) {
|
|
287
|
+
const target = selector ? normalizeQueueId(selector) : "the latest queued instruction";
|
|
288
|
+
return `Queued instruction was not found: ${target}`;
|
|
289
|
+
}
|
|
290
|
+
await bridge.logSystem(botId, chatId, `Removed queued instruction ${removed.id} for ${activeKey}.`);
|
|
291
|
+
return `Removed queued instruction ${removed.id} from ${removed.publicSessionId ?? "this session"}.\n`
|
|
292
|
+
+ `Remaining queued instructions: ${listQueuedWorkLoops(activeKey).length}`;
|
|
293
|
+
};
|
|
232
294
|
const runPlanDocumentReinforcement = async (ctx, count) => {
|
|
233
295
|
if (!ctx.chat) {
|
|
234
296
|
throw new Error("Telegram chat context is missing.");
|
|
@@ -317,14 +379,14 @@ ${bridge.formatStatus(mapping)}`);
|
|
|
317
379
|
bot.command("help", async (ctx) => {
|
|
318
380
|
await reply(ctx, HELP_TEXT);
|
|
319
381
|
});
|
|
320
|
-
const replySessionList = async (ctx) => {
|
|
382
|
+
const replySessionList = async (ctx, requestedShowAll) => {
|
|
321
383
|
if (!ctx.chat) {
|
|
322
384
|
throw new Error("Telegram chat context is missing.");
|
|
323
385
|
}
|
|
324
386
|
const botId = getBotId();
|
|
325
387
|
const chatId = String(ctx.chat.id);
|
|
326
388
|
const { args } = parseCommand(ctx.message?.text, 1);
|
|
327
|
-
const showAll = args[0] === "-a" || args[0] === "--all";
|
|
389
|
+
const showAll = requestedShowAll ?? (args[0] === "-a" || args[0] === "--all");
|
|
328
390
|
const [mapping, sessions] = await Promise.all([
|
|
329
391
|
bridge.status(botId, chatId),
|
|
330
392
|
bridge.listSessions(),
|
|
@@ -333,9 +395,33 @@ ${bridge.formatStatus(mapping)}`);
|
|
|
333
395
|
const sessionList = showAll
|
|
334
396
|
? await bridge.formatSessionListDetailed(sessions, mapping?.session.sessionId, await bridge.listActiveSessionIds())
|
|
335
397
|
: bridge.formatSessionList(sessions, mapping?.session.sessionId);
|
|
336
|
-
|
|
337
|
-
|
|
398
|
+
const sessionButtons = sessions.slice(0, showAll ? 20 : 10).map((session) => [
|
|
399
|
+
actionButton(ctx, truncateButtonLabel(`${session.sessionId === mapping?.session.sessionId ? "✓ " : ""}${session.publicId} · ${workspaceLeaf(session.workspace)}`), { kind: "session.switch", selector: session.publicId }),
|
|
400
|
+
]);
|
|
401
|
+
if (!showAll && sessions.length > 10) {
|
|
402
|
+
sessionButtons.push([actionButton(ctx, `Show all ${sessions.length} sessions`, { kind: "session.list", showAll: true })]);
|
|
403
|
+
}
|
|
404
|
+
const chunks = flattenChunks([`${sessionList}\n\n${botSummary}`], 3900);
|
|
405
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
406
|
+
await reply(ctx, chunk, index === chunks.length - 1 ? keyboardOptions(sessionButtons) : undefined);
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
const switchChatSession = async (ctx, selector) => {
|
|
410
|
+
if (!ctx.chat) {
|
|
411
|
+
throw new Error("Telegram chat context is missing.");
|
|
412
|
+
}
|
|
413
|
+
const botId = getBotId();
|
|
414
|
+
const chatId = String(ctx.chat.id);
|
|
415
|
+
const previous = await bridge.status(botId, chatId).catch(() => undefined);
|
|
416
|
+
const mapping = await bridge.switchSession(botId, chatId, selector);
|
|
417
|
+
if (previous && previous.session.sessionId !== mapping.session.sessionId) {
|
|
418
|
+
autoContinue.requestSessionStop(previous.session.sessionId);
|
|
419
|
+
cancelQueuedWorkLoops(botId, chatId, previous.session.sessionId);
|
|
420
|
+
messageBatcher.cancelPending(botId, chatId);
|
|
421
|
+
messageBatcher.cancelManual(botId, chatId);
|
|
422
|
+
await bridge.stopSessionRun(previous.session.sessionId, botId, chatId, "Chat switched to another session; previous session execution was stopped.");
|
|
338
423
|
}
|
|
424
|
+
return `Switched this chat to session ${mapping.session.publicId}.\n\n${bridge.formatCurrentSession(mapping)}`;
|
|
339
425
|
};
|
|
340
426
|
bot.command("list", async (ctx) => {
|
|
341
427
|
await replySessionList(ctx);
|
|
@@ -349,8 +435,6 @@ ${bridge.formatStatus(mapping)}`);
|
|
|
349
435
|
await reply(ctx, `Created and bound a new ${mapping.session.mode} session.\n\n${bridge.formatCurrentSession(mapping)}`);
|
|
350
436
|
});
|
|
351
437
|
bot.command("switch", async (ctx) => {
|
|
352
|
-
const botId = getBotId();
|
|
353
|
-
const chatId = String(ctx.chat.id);
|
|
354
438
|
const { args } = parseCommand(ctx.message?.text, 1);
|
|
355
439
|
const sessionId = args[0];
|
|
356
440
|
if (!sessionId) {
|
|
@@ -359,16 +443,7 @@ ${bridge.formatStatus(mapping)}`);
|
|
|
359
443
|
});
|
|
360
444
|
return;
|
|
361
445
|
}
|
|
362
|
-
|
|
363
|
-
const mapping = await bridge.switchSession(botId, chatId, sessionId);
|
|
364
|
-
if (previous && previous.session.sessionId !== mapping.session.sessionId) {
|
|
365
|
-
autoContinue.requestSessionStop(previous.session.sessionId);
|
|
366
|
-
cancelQueuedWorkLoops(botId, chatId, previous.session.sessionId);
|
|
367
|
-
messageBatcher.cancelPending(botId, chatId);
|
|
368
|
-
messageBatcher.cancelManual(botId, chatId);
|
|
369
|
-
await bridge.stopSessionRun(previous.session.sessionId, botId, chatId, "Chat switched to another session; previous session execution was stopped.");
|
|
370
|
-
}
|
|
371
|
-
await reply(ctx, `Switched this chat to session ${sessionId}.\n\n${bridge.formatCurrentSession(mapping)}`);
|
|
446
|
+
await reply(ctx, await switchChatSession(ctx, sessionId));
|
|
372
447
|
});
|
|
373
448
|
bot.command("plan", async (ctx) => {
|
|
374
449
|
const { args, rest } = parseCommand(ctx.message?.text, 1);
|
|
@@ -437,6 +512,14 @@ ${bridge.formatStatus(mapping)}`);
|
|
|
437
512
|
|
|
438
513
|
${bridge.formatStatus(mapping)}`);
|
|
439
514
|
});
|
|
515
|
+
const setChatModel = async (ctx, model) => {
|
|
516
|
+
if (!ctx.chat) {
|
|
517
|
+
throw new Error("Telegram chat context is missing.");
|
|
518
|
+
}
|
|
519
|
+
const mapping = await bridge.setModel(getBotId(), String(ctx.chat.id), model);
|
|
520
|
+
const selectedModel = mapping.session[mapping.session.mode]?.model ?? model;
|
|
521
|
+
return `Set ${mapping.session.mode} model to ${selectedModel}.\n\n${bridge.formatStatus(mapping)}`;
|
|
522
|
+
};
|
|
440
523
|
bot.command("model", async (ctx) => {
|
|
441
524
|
const botId = getBotId();
|
|
442
525
|
const chatId = String(ctx.chat.id);
|
|
@@ -449,13 +532,17 @@ ${bridge.formatStatus(mapping)}`);
|
|
|
449
532
|
return;
|
|
450
533
|
}
|
|
451
534
|
if (!model) {
|
|
535
|
+
const selection = await bridge.getModelSelection(botId, chatId);
|
|
536
|
+
const rows = selection.presets.map((preset) => [
|
|
537
|
+
actionButton(ctx, `${preset === selection.currentModel ? "✓ " : ""}${preset}`, { kind: "model.set", model: preset }),
|
|
538
|
+
]);
|
|
452
539
|
await reply(ctx, await bridge.formatModelSelection(botId, chatId), {
|
|
453
540
|
parse_mode: "Markdown",
|
|
541
|
+
...(keyboardOptions(rows) ?? {}),
|
|
454
542
|
});
|
|
455
543
|
return;
|
|
456
544
|
}
|
|
457
|
-
|
|
458
|
-
await reply(ctx, `Set ${mapping.session.mode} model to ${model}.\n\n${bridge.formatStatus(mapping)}`);
|
|
545
|
+
await reply(ctx, await setChatModel(ctx, model));
|
|
459
546
|
});
|
|
460
547
|
bot.command("queue", async (ctx) => {
|
|
461
548
|
const botId = getBotId();
|
|
@@ -470,22 +557,22 @@ ${bridge.formatStatus(mapping)}`);
|
|
|
470
557
|
return;
|
|
471
558
|
}
|
|
472
559
|
if (!action || action === "list") {
|
|
473
|
-
|
|
560
|
+
const queued = listQueuedWorkLoops(activeKey);
|
|
561
|
+
const rows = queued.map((entry) => [{
|
|
562
|
+
text: `Remove ${entry.id}`,
|
|
563
|
+
callback_data: `remoteagent:queue:remove:${entry.id}`,
|
|
564
|
+
}]);
|
|
565
|
+
if (queued.length > 0) {
|
|
566
|
+
rows.push([{ text: "Remove latest", callback_data: "remoteagent:queue:del" }]);
|
|
567
|
+
}
|
|
568
|
+
await reply(ctx, formatQueuedWorkLoops(activeKey, mapping?.session.publicId), keyboardOptions(rows));
|
|
474
569
|
return;
|
|
475
570
|
}
|
|
476
571
|
if ((action === "remove" || action === "rm") && !selector) {
|
|
477
572
|
await reply(ctx, "Usage: `/queue remove <id>`", { parse_mode: "Markdown" });
|
|
478
573
|
return;
|
|
479
574
|
}
|
|
480
|
-
|
|
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}`);
|
|
575
|
+
await reply(ctx, await removeQueuedInstruction(botId, chatId, selector));
|
|
489
576
|
});
|
|
490
577
|
bot.command("stop", async (ctx) => {
|
|
491
578
|
const botId = getBotId();
|
|
@@ -529,7 +616,16 @@ ${bridge.formatStatus(mapping)}`);
|
|
|
529
616
|
const option = args[0]?.toLowerCase();
|
|
530
617
|
const value = args[1];
|
|
531
618
|
if (!option) {
|
|
532
|
-
await reply(ctx, formatRuntimeOptions()
|
|
619
|
+
await reply(ctx, formatRuntimeOptions(), keyboardOptions([
|
|
620
|
+
[
|
|
621
|
+
actionButton(ctx, "Retry", { kind: "option.show", option: "retry" }),
|
|
622
|
+
actionButton(ctx, "Timeout", { kind: "option.show", option: "timeout" }),
|
|
623
|
+
],
|
|
624
|
+
[
|
|
625
|
+
actionButton(ctx, "Intent", { kind: "option.show", option: "intent" }),
|
|
626
|
+
actionButton(ctx, "Command menu", { kind: "option.show", option: "command-menu" }),
|
|
627
|
+
],
|
|
628
|
+
]));
|
|
533
629
|
return;
|
|
534
630
|
}
|
|
535
631
|
if (option !== "retry" && option !== "timeout" && option !== "intent" && option !== "command-menu") {
|
|
@@ -539,14 +635,7 @@ ${bridge.formatStatus(mapping)}`);
|
|
|
539
635
|
return;
|
|
540
636
|
}
|
|
541
637
|
if (!value) {
|
|
542
|
-
|
|
543
|
-
? `Current automatic continuation retry limit: ${formatRetryLimit(config.telegramAutoProgressMaxTurns)}\n\nUsage: \`/option retry <count>\``
|
|
544
|
-
: option === "intent"
|
|
545
|
-
? `Current untagged intent retry limit: ${formatRetryLimit(config.telegramUntaggedIntentRetries)}\n\nUsage: \`/option intent <count>\``
|
|
546
|
-
: option === "command-menu"
|
|
547
|
-
? `Current Telegram command menu: ${config.telegramCommandMenuEnabled ? "on" : "off"}\n\nUsage: \`/option command-menu on\`, \`/option command-menu off\`, or \`/option command-menu refresh\``
|
|
548
|
-
: `Current provider execution timeout: ${formatTimeoutSeconds(config.commandTimeoutMs)}\n\nUsage: \`/option timeout <seconds>\``;
|
|
549
|
-
await reply(ctx, current, {
|
|
638
|
+
await reply(ctx, formatRuntimeOptionDetail(option), {
|
|
550
639
|
parse_mode: "Markdown",
|
|
551
640
|
});
|
|
552
641
|
return;
|
|
@@ -758,15 +847,29 @@ ${bridge.formatStatus(mapping)}`);
|
|
|
758
847
|
}
|
|
759
848
|
await reply(ctx, "Usage: `/docs list`, `/docs find <keyword>`, `/docs pin <keyword> <path>`, `/docs remove <keyword>`, or `/docs reinforce <1-10>`", { parse_mode: "Markdown" });
|
|
760
849
|
});
|
|
761
|
-
const
|
|
850
|
+
const runMacro = async (ctx, target) => {
|
|
762
851
|
if (!ctx.chat) {
|
|
763
852
|
throw new Error("Telegram chat context is missing.");
|
|
764
853
|
}
|
|
765
854
|
const botId = getBotId();
|
|
766
855
|
const chatId = String(ctx.chat.id);
|
|
856
|
+
const macro = await memoryService.getMacro(target);
|
|
857
|
+
if (!macro) {
|
|
858
|
+
return `Macro was not found: ${target}\n\n${await memoryService.listMacros()}`;
|
|
859
|
+
}
|
|
860
|
+
await bridge.logSystem(botId, chatId, `Macro executed: ${macro.alias}`);
|
|
861
|
+
await messageBatcher.enqueue({ botToken: token, telegramChatId: ctx.chat.id }, botId, chatId, macro.prompt);
|
|
862
|
+
return undefined;
|
|
863
|
+
};
|
|
864
|
+
const handleMacroCommand = async (ctx, text) => {
|
|
865
|
+
const botId = getBotId();
|
|
767
866
|
const parsed = parseMacroCommandText(text, botId);
|
|
768
867
|
if (parsed.kind === "help") {
|
|
769
|
-
|
|
868
|
+
const macros = await memoryService.getMacros();
|
|
869
|
+
const rows = macros.slice(0, 20).map((macro) => [
|
|
870
|
+
actionButton(ctx, truncateButtonLabel(macro.alias), { kind: "macro.run", alias: macro.alias }),
|
|
871
|
+
]);
|
|
872
|
+
await reply(ctx, await formatMacroHelp(memoryService), keyboardOptions(rows));
|
|
770
873
|
return;
|
|
771
874
|
}
|
|
772
875
|
if (parsed.kind === "set") {
|
|
@@ -781,25 +884,34 @@ ${bridge.formatStatus(mapping)}`);
|
|
|
781
884
|
await reply(ctx, removed ? `Removed macro '${parsed.alias}'.` : `Macro was not found: ${parsed.alias}`);
|
|
782
885
|
return;
|
|
783
886
|
}
|
|
784
|
-
const
|
|
785
|
-
if (
|
|
786
|
-
await reply(ctx,
|
|
787
|
-
return;
|
|
887
|
+
const result = await runMacro(ctx, parsed.target);
|
|
888
|
+
if (result) {
|
|
889
|
+
await reply(ctx, result);
|
|
788
890
|
}
|
|
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
891
|
};
|
|
792
892
|
bot.command("macro", async (ctx) => {
|
|
793
893
|
await handleMacroCommand(ctx, ctx.message?.text ?? "/macro");
|
|
794
894
|
});
|
|
795
|
-
|
|
895
|
+
const replyBotList = async (ctx) => {
|
|
796
896
|
await ensureOwnerControlAccess(ctx);
|
|
797
897
|
const pendingNotice = await botManagement.getPendingOperationNotice();
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
898
|
+
const [botList, choices] = await Promise.all([
|
|
899
|
+
botManagement.listBots(),
|
|
900
|
+
botManagement.listBotChoices(),
|
|
901
|
+
]);
|
|
902
|
+
const rows = [];
|
|
903
|
+
for (let index = 0; index < choices.length; index += 2) {
|
|
904
|
+
rows.push(choices.slice(index, index + 2).map((choice) => ({
|
|
905
|
+
text: `@${choice.username}`,
|
|
906
|
+
url: `https://t.me/${choice.username}`,
|
|
907
|
+
})));
|
|
908
|
+
}
|
|
909
|
+
rows.push([actionButton(ctx, "Refresh", { kind: "bots.refresh" })]);
|
|
910
|
+
const message = pendingNotice?.pending ? `${pendingNotice.message}\n\n${botList}` : botList;
|
|
911
|
+
await reply(ctx, message, keyboardOptions(rows));
|
|
912
|
+
};
|
|
913
|
+
bot.command("bots", async (ctx) => {
|
|
914
|
+
await replyBotList(ctx);
|
|
803
915
|
});
|
|
804
916
|
bot.command("bot", async (ctx) => {
|
|
805
917
|
await ensureOwnerControlAccess(ctx);
|
|
@@ -878,20 +990,39 @@ ${bridge.formatStatus(mapping)}`);
|
|
|
878
990
|
return { chunks: flattenChunks([output], 3900) };
|
|
879
991
|
});
|
|
880
992
|
});
|
|
993
|
+
const setChatSandbox = async (ctx, sandboxMode) => {
|
|
994
|
+
if (!ctx.chat) {
|
|
995
|
+
throw new Error("Telegram chat context is missing.");
|
|
996
|
+
}
|
|
997
|
+
const mapping = await bridge.setCodexSandboxMode(getBotId(), String(ctx.chat.id), sandboxMode);
|
|
998
|
+
return `Set Codex sandbox to ${sandboxMode}.\n\n${bridge.formatStatus(mapping)}`;
|
|
999
|
+
};
|
|
881
1000
|
bot.command("sandbox", async (ctx) => {
|
|
882
1001
|
const botId = getBotId();
|
|
883
1002
|
const chatId = String(ctx.chat.id);
|
|
884
1003
|
const { args } = parseCommand(ctx.message?.text, 2);
|
|
885
1004
|
const provider = args[0]?.toLowerCase();
|
|
886
1005
|
const sandboxMode = args[1]?.toLowerCase();
|
|
1006
|
+
if (!provider && !sandboxMode) {
|
|
1007
|
+
const mapping = await bridge.status(botId, chatId);
|
|
1008
|
+
if (!mapping?.session.codex) {
|
|
1009
|
+
await reply(ctx, "No Codex session is paired with this chat.");
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
const current = mapping.session.codex.sandboxMode ?? config.codexSandboxMode;
|
|
1013
|
+
const modes = ["read-only", "workspace-write", "danger-full-access"];
|
|
1014
|
+
await reply(ctx, `Codex sandbox\ncurrent: ${current}\n\nChoose a mode:`, keyboardOptions(modes.map((mode) => [
|
|
1015
|
+
actionButton(ctx, `${mode === current ? "✓ " : ""}${mode}`, { kind: "sandbox.set", mode }),
|
|
1016
|
+
])));
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
887
1019
|
if (provider !== "codex" || !sandboxMode || !isCodexSandboxMode(sandboxMode)) {
|
|
888
1020
|
await reply(ctx, "Usage: `/sandbox codex <read-only|workspace-write|danger-full-access>`", {
|
|
889
1021
|
parse_mode: "Markdown",
|
|
890
1022
|
});
|
|
891
1023
|
return;
|
|
892
1024
|
}
|
|
893
|
-
|
|
894
|
-
await reply(ctx, `Set Codex sandbox to ${sandboxMode}.\n\n${bridge.formatStatus(mapping)}`);
|
|
1025
|
+
await reply(ctx, await setChatSandbox(ctx, sandboxMode));
|
|
895
1026
|
});
|
|
896
1027
|
bot.command("reset", async (ctx) => {
|
|
897
1028
|
const botId = getBotId();
|
|
@@ -900,6 +1031,88 @@ ${bridge.formatStatus(mapping)}`);
|
|
|
900
1031
|
await bridge.reset(botId, chatId);
|
|
901
1032
|
await reply(ctx, "Cleared all pairings for this chat.");
|
|
902
1033
|
});
|
|
1034
|
+
bot.on("callback_query:data", async (ctx) => {
|
|
1035
|
+
const data = ctx.callbackQuery.data;
|
|
1036
|
+
const callbackChat = ctx.callbackQuery.message?.chat;
|
|
1037
|
+
if (!callbackChat) {
|
|
1038
|
+
await callTelegramApi(token, "answerCallbackQuery", {
|
|
1039
|
+
callback_query_id: ctx.callbackQuery.id,
|
|
1040
|
+
text: "This action is no longer available.",
|
|
1041
|
+
});
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
const botId = getBotId();
|
|
1045
|
+
const chatId = String(callbackChat.id);
|
|
1046
|
+
const queueMatch = /^remoteagent:queue:(remove:(Q\d+)|del)$/i.exec(data);
|
|
1047
|
+
if (queueMatch) {
|
|
1048
|
+
const result = await removeQueuedInstruction(botId, chatId, queueMatch[2]);
|
|
1049
|
+
await callTelegramApi(token, "answerCallbackQuery", {
|
|
1050
|
+
callback_query_id: ctx.callbackQuery.id,
|
|
1051
|
+
text: result.split("\n", 1)[0],
|
|
1052
|
+
});
|
|
1053
|
+
await sendTelegramMessage(token, callbackChat.id, result);
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
if (!data.startsWith("remoteagent:action:")) {
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
const action = inlineActions.resolve(data, chatId, String(ctx.from.id));
|
|
1060
|
+
if (!action) {
|
|
1061
|
+
await callTelegramApi(token, "answerCallbackQuery", {
|
|
1062
|
+
callback_query_id: ctx.callbackQuery.id,
|
|
1063
|
+
text: "This button expired. Open the command list again.",
|
|
1064
|
+
});
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
await callTelegramApi(token, "answerCallbackQuery", {
|
|
1068
|
+
callback_query_id: ctx.callbackQuery.id,
|
|
1069
|
+
text: action.kind === "macro.run" ? "Macro selected." : "Applying...",
|
|
1070
|
+
});
|
|
1071
|
+
try {
|
|
1072
|
+
if (action.kind === "session.switch") {
|
|
1073
|
+
await reply(ctx, await switchChatSession(ctx, action.selector));
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
if (action.kind === "session.list") {
|
|
1077
|
+
await replySessionList(ctx, action.showAll);
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
if (action.kind === "model.set") {
|
|
1081
|
+
await reply(ctx, await setChatModel(ctx, action.model));
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
if (action.kind === "macro.run") {
|
|
1085
|
+
const result = await runMacro(ctx, action.alias);
|
|
1086
|
+
if (result) {
|
|
1087
|
+
await reply(ctx, result);
|
|
1088
|
+
}
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
if (action.kind === "option.show") {
|
|
1092
|
+
await ensureOwnerControlAccess(ctx);
|
|
1093
|
+
await reply(ctx, formatRuntimeOptionDetail(action.option), { parse_mode: "Markdown" });
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
if (action.kind === "bots.refresh") {
|
|
1097
|
+
await replyBotList(ctx);
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
if (action.mode === "danger-full-access" && !action.confirmed) {
|
|
1101
|
+
await reply(ctx, "Confirm Codex sandbox change to danger-full-access.", keyboardOptions([[
|
|
1102
|
+
actionButton(ctx, "Confirm danger-full-access", {
|
|
1103
|
+
kind: "sandbox.set",
|
|
1104
|
+
mode: "danger-full-access",
|
|
1105
|
+
confirmed: true,
|
|
1106
|
+
}),
|
|
1107
|
+
]]));
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
await reply(ctx, await setChatSandbox(ctx, action.mode));
|
|
1111
|
+
}
|
|
1112
|
+
catch (error) {
|
|
1113
|
+
await reply(ctx, error instanceof Error ? error.message : String(error));
|
|
1114
|
+
}
|
|
1115
|
+
});
|
|
903
1116
|
bot.on("message", async (ctx) => {
|
|
904
1117
|
const botId = getBotId();
|
|
905
1118
|
const chatId = String(ctx.chat.id);
|
|
@@ -1197,14 +1410,17 @@ async function runWithPendingAnimation(botToken, chatId, task) {
|
|
|
1197
1410
|
pulseTyping();
|
|
1198
1411
|
try {
|
|
1199
1412
|
const helpers = {
|
|
1200
|
-
reportProgress: async (chunks, parseMode) => {
|
|
1413
|
+
reportProgress: async (chunks, parseMode, messageOptions) => {
|
|
1201
1414
|
const normalized = await normalizeTelegramDelivery(chunks);
|
|
1202
1415
|
const progressChunks = flattenChunks(normalized.chunks, 3900);
|
|
1203
1416
|
if (progressChunks.length === 0 && normalized.documents.length === 0) {
|
|
1204
1417
|
return;
|
|
1205
1418
|
}
|
|
1206
1419
|
const rendered = formatProviderTelegramChunks(progressChunks, parseMode);
|
|
1207
|
-
const extra =
|
|
1420
|
+
const extra = {
|
|
1421
|
+
...messageOptions,
|
|
1422
|
+
...(rendered.parseMode ? { parse_mode: rendered.parseMode } : {}),
|
|
1423
|
+
};
|
|
1208
1424
|
for (const chunk of rendered.chunks) {
|
|
1209
1425
|
await sendTelegramMessage(botToken, chatId, chunk, extra).catch((error) => {
|
|
1210
1426
|
console.warn(`[telegram-progress-delivery] chat=${chatId} dropped progress message: ${formatTelegramDeliveryError(error)}`);
|
|
@@ -1275,10 +1491,24 @@ async function routeTelegramWorkLoop(bridge, botId, chatId, message, label, botM
|
|
|
1275
1491
|
});
|
|
1276
1492
|
try {
|
|
1277
1493
|
await bridge.logSystem(botId, chatId, `Queued overlapping Telegram work loop ${queuedEntry.id} for ${activeKey}.`);
|
|
1278
|
-
await helpers.reportProgress([
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1494
|
+
await helpers.reportProgress([[
|
|
1495
|
+
`Queued instruction ${queuedEntry.id} for ${currentSession?.session.publicId ?? "this session"}. It will run after the active work finishes.`,
|
|
1496
|
+
"",
|
|
1497
|
+
`Remove it with \`/queue remove ${queuedEntry.id}\`, or remove the latest queued instruction with \`/queue del\`.`,
|
|
1498
|
+
].join("\n")], undefined, {
|
|
1499
|
+
reply_markup: JSON.stringify({
|
|
1500
|
+
inline_keyboard: [[
|
|
1501
|
+
{
|
|
1502
|
+
text: `/queue remove ${queuedEntry.id}`,
|
|
1503
|
+
callback_data: `remoteagent:queue:remove:${queuedEntry.id}`,
|
|
1504
|
+
},
|
|
1505
|
+
{
|
|
1506
|
+
text: "/queue del",
|
|
1507
|
+
callback_data: "remoteagent:queue:del",
|
|
1508
|
+
},
|
|
1509
|
+
]],
|
|
1510
|
+
}),
|
|
1511
|
+
});
|
|
1282
1512
|
await previousTail.catch(() => undefined);
|
|
1283
1513
|
}
|
|
1284
1514
|
catch (error) {
|
|
@@ -1328,6 +1558,7 @@ async function routeTelegramWorkLoop(bridge, botId, chatId, message, label, botM
|
|
|
1328
1558
|
let missingEvidenceRetryCount = 0;
|
|
1329
1559
|
let deliveredProgressCount = 0;
|
|
1330
1560
|
let providerCompleted = false;
|
|
1561
|
+
const streamedProgressKeys = new Set();
|
|
1331
1562
|
const ensureStillBound = async (phase) => {
|
|
1332
1563
|
if (!sessionId) {
|
|
1333
1564
|
return;
|
|
@@ -1339,6 +1570,23 @@ async function routeTelegramWorkLoop(bridge, botId, chatId, message, label, botM
|
|
|
1339
1570
|
await bridge.stopSessionRun(sessionId, botId, chatId, `Telegram work loop stopped during ${phase} because the chat is now bound to another session.`);
|
|
1340
1571
|
throw new SilentTelegramAbort(`Session ${currentSession?.session.publicId ?? sessionId} is no longer bound to this chat.`);
|
|
1341
1572
|
};
|
|
1573
|
+
const deliverStreamedProgress = async (response) => {
|
|
1574
|
+
const parsed = parseReportResponses(bridge.formatResponses([response]), transform);
|
|
1575
|
+
if (parsed.kind !== "progress" || parsed.chunks.length === 0) {
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
const key = progressDeliveryKey(parsed.chunks);
|
|
1579
|
+
if (streamedProgressKeys.has(key)) {
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
streamedProgressKeys.add(key);
|
|
1583
|
+
deliveredProgressCount += 1;
|
|
1584
|
+
await ensureStillBound(`${label} streamed progress delivery`);
|
|
1585
|
+
if (currentSession) {
|
|
1586
|
+
await memoryService.recordProgress(currentSession.session, parsed.chunks.join("\n"));
|
|
1587
|
+
}
|
|
1588
|
+
await helpers.reportProgress(parsed.chunks);
|
|
1589
|
+
};
|
|
1342
1590
|
if (currentSession) {
|
|
1343
1591
|
await botManagement.markProviderRunning(botId, sessionId);
|
|
1344
1592
|
}
|
|
@@ -1361,8 +1609,8 @@ async function routeTelegramWorkLoop(bridge, botId, chatId, message, label, botM
|
|
|
1361
1609
|
await bridge.logSystem(botId, chatId, `${turnLabel} started.`);
|
|
1362
1610
|
try {
|
|
1363
1611
|
const responses = sessionId
|
|
1364
|
-
? await bridge.routeSessionMessageForChat(sessionId, botId, chatId, prompt)
|
|
1365
|
-
: await bridge.routeMessage(botId, chatId, prompt);
|
|
1612
|
+
? await bridge.routeSessionMessageForChat(sessionId, botId, chatId, prompt, deliverStreamedProgress)
|
|
1613
|
+
: await bridge.routeMessage(botId, chatId, prompt, deliverStreamedProgress);
|
|
1366
1614
|
await ensureStillBound(`${turnLabel} response`);
|
|
1367
1615
|
const parsed = parseReportResponses(bridge.formatResponses(responses), transform);
|
|
1368
1616
|
await bridge.logSystem(botId, chatId, `${turnLabel} returned ${parsed.kind}.`);
|
|
@@ -1371,21 +1619,25 @@ async function routeTelegramWorkLoop(bridge, botId, chatId, message, label, botM
|
|
|
1371
1619
|
if (parsed.kind === "progress") {
|
|
1372
1620
|
untaggedIntentRetryCount = 0;
|
|
1373
1621
|
missingEvidenceRetryCount = 0;
|
|
1374
|
-
|
|
1375
|
-
if (
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1622
|
+
const key = progressDeliveryKey(parsed.chunks);
|
|
1623
|
+
if (!streamedProgressKeys.has(key)) {
|
|
1624
|
+
streamedProgressKeys.add(key);
|
|
1625
|
+
deliveredProgressCount += 1;
|
|
1626
|
+
if (currentSession) {
|
|
1627
|
+
const progress = await memoryService.recordProgress(currentSession.session, parsed.chunks.join("\n"));
|
|
1628
|
+
if (progress.repeated) {
|
|
1629
|
+
const repeatedMessage = [
|
|
1630
|
+
"Repeated progress detected. The same work pattern has appeared 3 or more times.",
|
|
1631
|
+
"Automatic continuation stopped so the task can be inspected instead of looping.",
|
|
1632
|
+
].join("\n");
|
|
1633
|
+
await bridge.logSystem(botId, chatId, repeatedMessage);
|
|
1634
|
+
autoContinue.clear(botId, chatId, sessionId);
|
|
1635
|
+
return [repeatedMessage];
|
|
1636
|
+
}
|
|
1385
1637
|
}
|
|
1638
|
+
await ensureStillBound(`${turnLabel} progress delivery`);
|
|
1639
|
+
await helpers.reportProgress(parsed.chunks);
|
|
1386
1640
|
}
|
|
1387
|
-
await ensureStillBound(`${turnLabel} progress delivery`);
|
|
1388
|
-
await helpers.reportProgress(parsed.chunks);
|
|
1389
1641
|
if (autoContinue.isStopRequested(botId, chatId, sessionId)) {
|
|
1390
1642
|
const stopMessage = "Automatic continuation stopped after the latest progress report.";
|
|
1391
1643
|
await bridge.logSystem(botId, chatId, stopMessage);
|
|
@@ -1640,6 +1892,9 @@ function parseReportResponses(formattedBlocks, transform) {
|
|
|
1640
1892
|
const chunks = transform(parsedBlocks.map((item) => item.text));
|
|
1641
1893
|
return { kind, chunks };
|
|
1642
1894
|
}
|
|
1895
|
+
function progressDeliveryKey(chunks) {
|
|
1896
|
+
return chunks.join("\n").replace(/\s+/g, " ").trim();
|
|
1897
|
+
}
|
|
1643
1898
|
function formatProviderTelegramChunks(chunks, explicitParseMode) {
|
|
1644
1899
|
if (explicitParseMode) {
|
|
1645
1900
|
return { chunks, parseMode: explicitParseMode };
|
|
@@ -2199,6 +2454,26 @@ function formatRuntimeOptions() {
|
|
|
2199
2454
|
"`command-menu refresh` reapplies Telegram slash-command autocomplete without changing the saved option.",
|
|
2200
2455
|
].join("\n");
|
|
2201
2456
|
}
|
|
2457
|
+
function formatRuntimeOptionDetail(option) {
|
|
2458
|
+
if (option === "retry") {
|
|
2459
|
+
return `Current automatic continuation retry limit: ${formatRetryLimit(config.telegramAutoProgressMaxTurns)}\n\nUsage: \`/option retry <count>\``;
|
|
2460
|
+
}
|
|
2461
|
+
if (option === "intent") {
|
|
2462
|
+
return `Current untagged intent retry limit: ${formatRetryLimit(config.telegramUntaggedIntentRetries)}\n\nUsage: \`/option intent <count>\``;
|
|
2463
|
+
}
|
|
2464
|
+
if (option === "command-menu") {
|
|
2465
|
+
return `Current Telegram command menu: ${config.telegramCommandMenuEnabled ? "on" : "off"}\n\nUsage: \`/option command-menu on\`, \`/option command-menu off\`, or \`/option command-menu refresh\``;
|
|
2466
|
+
}
|
|
2467
|
+
return `Current provider execution timeout: ${formatTimeoutSeconds(config.commandTimeoutMs)}\n\nUsage: \`/option timeout <seconds>\``;
|
|
2468
|
+
}
|
|
2469
|
+
function truncateButtonLabel(value, maxLength = 48) {
|
|
2470
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
2471
|
+
return normalized.length <= maxLength ? normalized : `${normalized.slice(0, maxLength - 3)}...`;
|
|
2472
|
+
}
|
|
2473
|
+
function workspaceLeaf(workspace) {
|
|
2474
|
+
const normalized = workspace.replace(/[\\/]+$/, "");
|
|
2475
|
+
return normalized.split(/[\\/]/).at(-1) || workspace;
|
|
2476
|
+
}
|
|
2202
2477
|
function formatRetryLimit(value) {
|
|
2203
2478
|
return value === 0 ? "unlimited" : `${value}`;
|
|
2204
2479
|
}
|
|
@@ -2745,6 +3020,7 @@ async function sendTelegramMessage(botToken, chatId, text, extra) {
|
|
|
2745
3020
|
chat_id: String(chatId),
|
|
2746
3021
|
text,
|
|
2747
3022
|
parse_mode: extra?.parse_mode,
|
|
3023
|
+
reply_markup: extra?.reply_markup,
|
|
2748
3024
|
});
|
|
2749
3025
|
}
|
|
2750
3026
|
catch (error) {
|
|
@@ -2755,6 +3031,7 @@ async function sendTelegramMessage(botToken, chatId, text, extra) {
|
|
|
2755
3031
|
return await callTelegramApi(botToken, "sendMessage", {
|
|
2756
3032
|
chat_id: String(chatId),
|
|
2757
3033
|
text: stripTelegramHtml(text),
|
|
3034
|
+
reply_markup: extra?.reply_markup,
|
|
2758
3035
|
});
|
|
2759
3036
|
}
|
|
2760
3037
|
}
|