appback-remoteagent 0.17.1 → 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 +22 -4
- package/dist/bot.js +259 -52
- package/dist/services/agent-memory-service.js +6 -4
- package/dist/services/bot-management-service.js +3 -0
- package/dist/services/bridge-service.js +16 -7
- package/docs/RELEASING.md +22 -0
- package/package.json +1 -1
- package/scripts/selftest-codex-stream.mjs +9 -2
- package/scripts/selftest-telegram-update.mjs +85 -0
package/README.md
CHANGED
|
@@ -103,6 +103,8 @@ Current command surface implemented in `src/bot.ts`:
|
|
|
103
103
|
| `/new` | Creates and binds a new session using the saved default mode in a new managed workspace |
|
|
104
104
|
| `/switch <session>` | Rebinds this chat to an existing RemoteAgent session |
|
|
105
105
|
| `/status` | Shows current session, workspace, provider, and sandbox state |
|
|
106
|
+
| `/model [name]` | Lists selectable provider models or changes the current session model |
|
|
107
|
+
| `/sandbox [codex <mode>]` | Lists Codex sandbox choices or changes the current session sandbox |
|
|
106
108
|
| `/option retry <count>` | Sets the automatic continuation turn limit and persists it to `~/.remoteagent/.env` |
|
|
107
109
|
| `/option timeout <seconds>` | Sets the provider execution timeout and persists it to `~/.remoteagent/.env` |
|
|
108
110
|
| `/option intent <count>` | Sets retries for untagged intent-only provider replies and persists it to `~/.remoteagent/.env` |
|
|
@@ -110,6 +112,7 @@ Current command surface implemented in `src/bot.ts`:
|
|
|
110
112
|
| `/state clear` | Clears the current session ledger without deleting the session |
|
|
111
113
|
| `/state note <text>` | Adds an operator note to the session ledger |
|
|
112
114
|
| `/bots` | Lists the currently configured Telegram bots |
|
|
115
|
+
| `/macro [alias\|number]` | Lists stored macros or runs one against the current session |
|
|
113
116
|
| `/bot add <token>` | Adds a conversation bot, restarts the runtime, and confirms the result after restart |
|
|
114
117
|
| `/bot doctor` | Checks configured Telegram bots and removes bots that Telegram reports as permanently dead |
|
|
115
118
|
| `/bot remove <username\|id>` | Removes a configured Telegram bot, restarts the runtime, and confirms the result after restart |
|
|
@@ -127,6 +130,8 @@ Current command surface implemented in `src/bot.ts`:
|
|
|
127
130
|
| `/queue remove <id>` | Removes one waiting instruction by its `Q001`-style id |
|
|
128
131
|
| `/queue del` | Removes the most recently queued instruction |
|
|
129
132
|
|
|
133
|
+
Selection-oriented replies include Telegram inline buttons for sessions, models, macros, runtime option details, sandbox modes, queued instructions, and configured bot links. Text commands remain the canonical compatible interface. Callback actions use short-lived opaque IDs bound to the originating chat and user; they do not expose secrets or rely on visible list positions as internal identity. Selecting `danger-full-access` through a button requires a second confirmation.
|
|
134
|
+
|
|
130
135
|
Multi-bot polling is tiered by recent activity and active provider work. See [docs/BOT_POLLING_POLICY.md](./docs/BOT_POLLING_POLICY.md).
|
|
131
136
|
|
|
132
137
|
### 2. Terminal control
|
|
@@ -115,10 +115,7 @@ export class CodexAdapter {
|
|
|
115
115
|
}
|
|
116
116
|
let text;
|
|
117
117
|
try {
|
|
118
|
-
|
|
119
|
-
text = event.type === "item.completed" && event.item?.type === "agent_message"
|
|
120
|
-
? event.item.text?.trim()
|
|
121
|
-
: undefined;
|
|
118
|
+
text = this.extractEventAgentText(JSON.parse(line));
|
|
122
119
|
}
|
|
123
120
|
catch {
|
|
124
121
|
// Ignore non-JSON stdout and let the normal final-response parser handle it.
|
|
@@ -128,6 +125,27 @@ export class CodexAdapter {
|
|
|
128
125
|
await onProgress(text);
|
|
129
126
|
}
|
|
130
127
|
}
|
|
128
|
+
extractEventAgentText(event) {
|
|
129
|
+
if (!event || typeof event !== "object") {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
const record = event;
|
|
133
|
+
if (record.type === "item.completed" && record.item?.type === "agent_message") {
|
|
134
|
+
return record.item.text?.trim();
|
|
135
|
+
}
|
|
136
|
+
if (record.type === "event_msg" && record.payload?.type === "agent_message") {
|
|
137
|
+
return (record.payload.message ?? record.payload.text)?.trim();
|
|
138
|
+
}
|
|
139
|
+
if (record.type === "response_item" && record.payload?.type === "message") {
|
|
140
|
+
const text = record.payload.content
|
|
141
|
+
?.filter((item) => item.type === "output_text" && typeof item.text === "string")
|
|
142
|
+
.map((item) => item.text)
|
|
143
|
+
.join("\n")
|
|
144
|
+
.trim();
|
|
145
|
+
return text || record.payload.text?.trim();
|
|
146
|
+
}
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
131
149
|
extractThreadId(stdout) {
|
|
132
150
|
for (const line of stdout.split(/\r?\n/)) {
|
|
133
151
|
if (!line.startsWith("{")) {
|
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,20 @@ 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
|
+
};
|
|
232
282
|
const removeQueuedInstruction = async (botId, chatId, selector) => {
|
|
233
283
|
const mapping = await bridge.status(botId, chatId);
|
|
234
284
|
const activeKey = workLoopKey(botId, chatId, mapping?.session.sessionId);
|
|
@@ -329,14 +379,14 @@ ${bridge.formatStatus(mapping)}`);
|
|
|
329
379
|
bot.command("help", async (ctx) => {
|
|
330
380
|
await reply(ctx, HELP_TEXT);
|
|
331
381
|
});
|
|
332
|
-
const replySessionList = async (ctx) => {
|
|
382
|
+
const replySessionList = async (ctx, requestedShowAll) => {
|
|
333
383
|
if (!ctx.chat) {
|
|
334
384
|
throw new Error("Telegram chat context is missing.");
|
|
335
385
|
}
|
|
336
386
|
const botId = getBotId();
|
|
337
387
|
const chatId = String(ctx.chat.id);
|
|
338
388
|
const { args } = parseCommand(ctx.message?.text, 1);
|
|
339
|
-
const showAll = args[0] === "-a" || args[0] === "--all";
|
|
389
|
+
const showAll = requestedShowAll ?? (args[0] === "-a" || args[0] === "--all");
|
|
340
390
|
const [mapping, sessions] = await Promise.all([
|
|
341
391
|
bridge.status(botId, chatId),
|
|
342
392
|
bridge.listSessions(),
|
|
@@ -345,9 +395,33 @@ ${bridge.formatStatus(mapping)}`);
|
|
|
345
395
|
const sessionList = showAll
|
|
346
396
|
? await bridge.formatSessionListDetailed(sessions, mapping?.session.sessionId, await bridge.listActiveSessionIds())
|
|
347
397
|
: bridge.formatSessionList(sessions, mapping?.session.sessionId);
|
|
348
|
-
|
|
349
|
-
|
|
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.");
|
|
350
423
|
}
|
|
424
|
+
return `Switched this chat to session ${mapping.session.publicId}.\n\n${bridge.formatCurrentSession(mapping)}`;
|
|
351
425
|
};
|
|
352
426
|
bot.command("list", async (ctx) => {
|
|
353
427
|
await replySessionList(ctx);
|
|
@@ -361,8 +435,6 @@ ${bridge.formatStatus(mapping)}`);
|
|
|
361
435
|
await reply(ctx, `Created and bound a new ${mapping.session.mode} session.\n\n${bridge.formatCurrentSession(mapping)}`);
|
|
362
436
|
});
|
|
363
437
|
bot.command("switch", async (ctx) => {
|
|
364
|
-
const botId = getBotId();
|
|
365
|
-
const chatId = String(ctx.chat.id);
|
|
366
438
|
const { args } = parseCommand(ctx.message?.text, 1);
|
|
367
439
|
const sessionId = args[0];
|
|
368
440
|
if (!sessionId) {
|
|
@@ -371,16 +443,7 @@ ${bridge.formatStatus(mapping)}`);
|
|
|
371
443
|
});
|
|
372
444
|
return;
|
|
373
445
|
}
|
|
374
|
-
|
|
375
|
-
const mapping = await bridge.switchSession(botId, chatId, sessionId);
|
|
376
|
-
if (previous && previous.session.sessionId !== mapping.session.sessionId) {
|
|
377
|
-
autoContinue.requestSessionStop(previous.session.sessionId);
|
|
378
|
-
cancelQueuedWorkLoops(botId, chatId, previous.session.sessionId);
|
|
379
|
-
messageBatcher.cancelPending(botId, chatId);
|
|
380
|
-
messageBatcher.cancelManual(botId, chatId);
|
|
381
|
-
await bridge.stopSessionRun(previous.session.sessionId, botId, chatId, "Chat switched to another session; previous session execution was stopped.");
|
|
382
|
-
}
|
|
383
|
-
await reply(ctx, `Switched this chat to session ${sessionId}.\n\n${bridge.formatCurrentSession(mapping)}`);
|
|
446
|
+
await reply(ctx, await switchChatSession(ctx, sessionId));
|
|
384
447
|
});
|
|
385
448
|
bot.command("plan", async (ctx) => {
|
|
386
449
|
const { args, rest } = parseCommand(ctx.message?.text, 1);
|
|
@@ -449,6 +512,14 @@ ${bridge.formatStatus(mapping)}`);
|
|
|
449
512
|
|
|
450
513
|
${bridge.formatStatus(mapping)}`);
|
|
451
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
|
+
};
|
|
452
523
|
bot.command("model", async (ctx) => {
|
|
453
524
|
const botId = getBotId();
|
|
454
525
|
const chatId = String(ctx.chat.id);
|
|
@@ -461,13 +532,17 @@ ${bridge.formatStatus(mapping)}`);
|
|
|
461
532
|
return;
|
|
462
533
|
}
|
|
463
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
|
+
]);
|
|
464
539
|
await reply(ctx, await bridge.formatModelSelection(botId, chatId), {
|
|
465
540
|
parse_mode: "Markdown",
|
|
541
|
+
...(keyboardOptions(rows) ?? {}),
|
|
466
542
|
});
|
|
467
543
|
return;
|
|
468
544
|
}
|
|
469
|
-
|
|
470
|
-
await reply(ctx, `Set ${mapping.session.mode} model to ${model}.\n\n${bridge.formatStatus(mapping)}`);
|
|
545
|
+
await reply(ctx, await setChatModel(ctx, model));
|
|
471
546
|
});
|
|
472
547
|
bot.command("queue", async (ctx) => {
|
|
473
548
|
const botId = getBotId();
|
|
@@ -482,7 +557,15 @@ ${bridge.formatStatus(mapping)}`);
|
|
|
482
557
|
return;
|
|
483
558
|
}
|
|
484
559
|
if (!action || action === "list") {
|
|
485
|
-
|
|
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));
|
|
486
569
|
return;
|
|
487
570
|
}
|
|
488
571
|
if ((action === "remove" || action === "rm") && !selector) {
|
|
@@ -533,7 +616,16 @@ ${bridge.formatStatus(mapping)}`);
|
|
|
533
616
|
const option = args[0]?.toLowerCase();
|
|
534
617
|
const value = args[1];
|
|
535
618
|
if (!option) {
|
|
536
|
-
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
|
+
]));
|
|
537
629
|
return;
|
|
538
630
|
}
|
|
539
631
|
if (option !== "retry" && option !== "timeout" && option !== "intent" && option !== "command-menu") {
|
|
@@ -543,14 +635,7 @@ ${bridge.formatStatus(mapping)}`);
|
|
|
543
635
|
return;
|
|
544
636
|
}
|
|
545
637
|
if (!value) {
|
|
546
|
-
|
|
547
|
-
? `Current automatic continuation retry limit: ${formatRetryLimit(config.telegramAutoProgressMaxTurns)}\n\nUsage: \`/option retry <count>\``
|
|
548
|
-
: option === "intent"
|
|
549
|
-
? `Current untagged intent retry limit: ${formatRetryLimit(config.telegramUntaggedIntentRetries)}\n\nUsage: \`/option intent <count>\``
|
|
550
|
-
: option === "command-menu"
|
|
551
|
-
? `Current Telegram command menu: ${config.telegramCommandMenuEnabled ? "on" : "off"}\n\nUsage: \`/option command-menu on\`, \`/option command-menu off\`, or \`/option command-menu refresh\``
|
|
552
|
-
: `Current provider execution timeout: ${formatTimeoutSeconds(config.commandTimeoutMs)}\n\nUsage: \`/option timeout <seconds>\``;
|
|
553
|
-
await reply(ctx, current, {
|
|
638
|
+
await reply(ctx, formatRuntimeOptionDetail(option), {
|
|
554
639
|
parse_mode: "Markdown",
|
|
555
640
|
});
|
|
556
641
|
return;
|
|
@@ -762,15 +847,29 @@ ${bridge.formatStatus(mapping)}`);
|
|
|
762
847
|
}
|
|
763
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" });
|
|
764
849
|
});
|
|
765
|
-
const
|
|
850
|
+
const runMacro = async (ctx, target) => {
|
|
766
851
|
if (!ctx.chat) {
|
|
767
852
|
throw new Error("Telegram chat context is missing.");
|
|
768
853
|
}
|
|
769
854
|
const botId = getBotId();
|
|
770
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();
|
|
771
866
|
const parsed = parseMacroCommandText(text, botId);
|
|
772
867
|
if (parsed.kind === "help") {
|
|
773
|
-
|
|
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));
|
|
774
873
|
return;
|
|
775
874
|
}
|
|
776
875
|
if (parsed.kind === "set") {
|
|
@@ -785,25 +884,34 @@ ${bridge.formatStatus(mapping)}`);
|
|
|
785
884
|
await reply(ctx, removed ? `Removed macro '${parsed.alias}'.` : `Macro was not found: ${parsed.alias}`);
|
|
786
885
|
return;
|
|
787
886
|
}
|
|
788
|
-
const
|
|
789
|
-
if (
|
|
790
|
-
await reply(ctx,
|
|
791
|
-
return;
|
|
887
|
+
const result = await runMacro(ctx, parsed.target);
|
|
888
|
+
if (result) {
|
|
889
|
+
await reply(ctx, result);
|
|
792
890
|
}
|
|
793
|
-
await bridge.logSystem(botId, chatId, `Macro executed: ${macro.alias}`);
|
|
794
|
-
await messageBatcher.enqueue({ botToken: token, telegramChatId: ctx.chat.id }, botId, chatId, macro.prompt);
|
|
795
891
|
};
|
|
796
892
|
bot.command("macro", async (ctx) => {
|
|
797
893
|
await handleMacroCommand(ctx, ctx.message?.text ?? "/macro");
|
|
798
894
|
});
|
|
799
|
-
|
|
895
|
+
const replyBotList = async (ctx) => {
|
|
800
896
|
await ensureOwnerControlAccess(ctx);
|
|
801
897
|
const pendingNotice = await botManagement.getPendingOperationNotice();
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
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);
|
|
807
915
|
});
|
|
808
916
|
bot.command("bot", async (ctx) => {
|
|
809
917
|
await ensureOwnerControlAccess(ctx);
|
|
@@ -882,20 +990,39 @@ ${bridge.formatStatus(mapping)}`);
|
|
|
882
990
|
return { chunks: flattenChunks([output], 3900) };
|
|
883
991
|
});
|
|
884
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
|
+
};
|
|
885
1000
|
bot.command("sandbox", async (ctx) => {
|
|
886
1001
|
const botId = getBotId();
|
|
887
1002
|
const chatId = String(ctx.chat.id);
|
|
888
1003
|
const { args } = parseCommand(ctx.message?.text, 2);
|
|
889
1004
|
const provider = args[0]?.toLowerCase();
|
|
890
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
|
+
}
|
|
891
1019
|
if (provider !== "codex" || !sandboxMode || !isCodexSandboxMode(sandboxMode)) {
|
|
892
1020
|
await reply(ctx, "Usage: `/sandbox codex <read-only|workspace-write|danger-full-access>`", {
|
|
893
1021
|
parse_mode: "Markdown",
|
|
894
1022
|
});
|
|
895
1023
|
return;
|
|
896
1024
|
}
|
|
897
|
-
|
|
898
|
-
await reply(ctx, `Set Codex sandbox to ${sandboxMode}.\n\n${bridge.formatStatus(mapping)}`);
|
|
1025
|
+
await reply(ctx, await setChatSandbox(ctx, sandboxMode));
|
|
899
1026
|
});
|
|
900
1027
|
bot.command("reset", async (ctx) => {
|
|
901
1028
|
const botId = getBotId();
|
|
@@ -905,26 +1032,86 @@ ${bridge.formatStatus(mapping)}`);
|
|
|
905
1032
|
await reply(ctx, "Cleared all pairings for this chat.");
|
|
906
1033
|
});
|
|
907
1034
|
bot.on("callback_query:data", async (ctx) => {
|
|
908
|
-
const
|
|
909
|
-
if (!match) {
|
|
910
|
-
return;
|
|
911
|
-
}
|
|
1035
|
+
const data = ctx.callbackQuery.data;
|
|
912
1036
|
const callbackChat = ctx.callbackQuery.message?.chat;
|
|
913
1037
|
if (!callbackChat) {
|
|
914
1038
|
await callTelegramApi(token, "answerCallbackQuery", {
|
|
915
1039
|
callback_query_id: ctx.callbackQuery.id,
|
|
916
|
-
text: "This
|
|
1040
|
+
text: "This action is no longer available.",
|
|
917
1041
|
});
|
|
918
1042
|
return;
|
|
919
1043
|
}
|
|
920
1044
|
const botId = getBotId();
|
|
921
1045
|
const chatId = String(callbackChat.id);
|
|
922
|
-
const
|
|
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
|
+
}
|
|
923
1067
|
await callTelegramApi(token, "answerCallbackQuery", {
|
|
924
1068
|
callback_query_id: ctx.callbackQuery.id,
|
|
925
|
-
text:
|
|
1069
|
+
text: action.kind === "macro.run" ? "Macro selected." : "Applying...",
|
|
926
1070
|
});
|
|
927
|
-
|
|
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
|
+
}
|
|
928
1115
|
});
|
|
929
1116
|
bot.on("message", async (ctx) => {
|
|
930
1117
|
const botId = getBotId();
|
|
@@ -2267,6 +2454,26 @@ function formatRuntimeOptions() {
|
|
|
2267
2454
|
"`command-menu refresh` reapplies Telegram slash-command autocomplete without changing the saved option.",
|
|
2268
2455
|
].join("\n");
|
|
2269
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
|
+
}
|
|
2270
2477
|
function formatRetryLimit(value) {
|
|
2271
2478
|
return value === 0 ? "unlimited" : `${value}`;
|
|
2272
2479
|
}
|
|
@@ -267,8 +267,7 @@ export class AgentMemoryService {
|
|
|
267
267
|
return existed;
|
|
268
268
|
}
|
|
269
269
|
async getMacro(aliasOrIndex) {
|
|
270
|
-
const macros =
|
|
271
|
-
.sort((left, right) => left.alias.localeCompare(right.alias, "ko"));
|
|
270
|
+
const macros = await this.getMacros();
|
|
272
271
|
const trimmed = aliasOrIndex.trim();
|
|
273
272
|
if (/^[0-9]+$/.test(trimmed)) {
|
|
274
273
|
const index = Number.parseInt(trimmed, 10);
|
|
@@ -277,9 +276,12 @@ export class AgentMemoryService {
|
|
|
277
276
|
const normalizedAlias = this.normalizeMacroAlias(trimmed);
|
|
278
277
|
return macros.find((macro) => macro.alias === normalizedAlias);
|
|
279
278
|
}
|
|
280
|
-
async
|
|
281
|
-
|
|
279
|
+
async getMacros() {
|
|
280
|
+
return Object.values(await this.readMacros())
|
|
282
281
|
.sort((left, right) => left.alias.localeCompare(right.alias, "ko"));
|
|
282
|
+
}
|
|
283
|
+
async listMacros() {
|
|
284
|
+
const macros = await this.getMacros();
|
|
283
285
|
if (macros.length === 0) {
|
|
284
286
|
return "No macros are stored.";
|
|
285
287
|
}
|
|
@@ -31,6 +31,9 @@ export class BotManagementService {
|
|
|
31
31
|
}
|
|
32
32
|
return this.formatBots(bots, await this.pollingState.list());
|
|
33
33
|
}
|
|
34
|
+
async listBotChoices() {
|
|
35
|
+
return (await this.listConfiguredBots()).map(({ id, username }) => ({ id, username }));
|
|
36
|
+
}
|
|
34
37
|
async formatCurrentBotSummary(currentBotId) {
|
|
35
38
|
const env = await this.readEnvConfig();
|
|
36
39
|
const bots = this.zipBots(env.tokens, env.usernames);
|
|
@@ -181,15 +181,12 @@ export class BridgeService {
|
|
|
181
181
|
}, chatSession.session.workspace);
|
|
182
182
|
}
|
|
183
183
|
async formatModelSelection(botId, chatId) {
|
|
184
|
-
const
|
|
185
|
-
const provider =
|
|
186
|
-
this.ensurePaired(chatSession, provider);
|
|
187
|
-
const providerSession = chatSession.session[provider];
|
|
188
|
-
const presets = MODEL_PRESETS[provider] ?? [];
|
|
184
|
+
const selection = await this.getModelSelection(botId, chatId);
|
|
185
|
+
const { provider, currentModel, presets } = selection;
|
|
189
186
|
const lines = [
|
|
190
|
-
`session: ${
|
|
187
|
+
`session: ${selection.sessionPublicId}`,
|
|
191
188
|
`mode: ${provider}`,
|
|
192
|
-
`currentModel: ${
|
|
189
|
+
`currentModel: ${currentModel}`,
|
|
193
190
|
"availablePresets:",
|
|
194
191
|
...presets.map((item, index) => ` ${index + 1}. ${item}`),
|
|
195
192
|
"",
|
|
@@ -200,6 +197,18 @@ export class BridgeService {
|
|
|
200
197
|
}
|
|
201
198
|
return lines.join("\n");
|
|
202
199
|
}
|
|
200
|
+
async getModelSelection(botId, chatId) {
|
|
201
|
+
const chatSession = await this.requireChat(botId, chatId);
|
|
202
|
+
const provider = chatSession.session.mode;
|
|
203
|
+
this.ensurePaired(chatSession, provider);
|
|
204
|
+
const providerSession = chatSession.session[provider];
|
|
205
|
+
return {
|
|
206
|
+
sessionPublicId: chatSession.session.publicId,
|
|
207
|
+
provider,
|
|
208
|
+
currentModel: providerSession.model ?? this.defaultModelFor(provider),
|
|
209
|
+
presets: [...(MODEL_PRESETS[provider] ?? [])],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
203
212
|
async status(botId, chatId) {
|
|
204
213
|
return this.store.getChatSession(botId, chatId);
|
|
205
214
|
}
|
package/docs/RELEASING.md
CHANGED
|
@@ -220,3 +220,25 @@ npm run selftest:telegram
|
|
|
220
220
|
npm run release:publish
|
|
221
221
|
npm run release:deploy -- 0.17.1 30
|
|
222
222
|
```
|
|
223
|
+
|
|
224
|
+
## Release 0.18.0
|
|
225
|
+
|
|
226
|
+
Date: 2026-08-03
|
|
227
|
+
|
|
228
|
+
Changes:
|
|
229
|
+
|
|
230
|
+
- Telegram list and option responses provide inline command controls.
|
|
231
|
+
- Queue notices provide inline remove-latest and remove-by-id controls in one message.
|
|
232
|
+
- Codex progress streaming accepts both normalized CLI `item.completed` events and raw `event_msg`/`response_item` agent-message events.
|
|
233
|
+
- The Codex stream self-test covers every supported progress event shape and keeps final results out of the progress callback.
|
|
234
|
+
|
|
235
|
+
Validated:
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
npm run check
|
|
239
|
+
npm run build
|
|
240
|
+
npm run selftest:codex-stream
|
|
241
|
+
npm run selftest:telegram
|
|
242
|
+
npm run release:publish
|
|
243
|
+
npm run release:deploy -- 0.18.0 30
|
|
244
|
+
```
|
package/package.json
CHANGED
|
@@ -23,7 +23,9 @@ printf '%s\\n' '{"type":"thread.started","thread_id":"stream-thread"}'
|
|
|
23
23
|
printf '%s' '{"type":"item.completed","item":{"type":"agent_message","text":"REPORT:progress\\nphase one"}}'
|
|
24
24
|
printf '\\n'
|
|
25
25
|
sleep 0.05
|
|
26
|
-
printf '%s\\n' '{"type":"
|
|
26
|
+
printf '%s\\n' '{"type":"event_msg","payload":{"type":"agent_message","message":"REPORT:progress\\nphase two"}}'
|
|
27
|
+
sleep 0.05
|
|
28
|
+
printf '%s\\n' '{"type":"response_item","payload":{"type":"message","content":[{"type":"output_text","text":"REPORT:progress\\nphase three"}]}}'
|
|
27
29
|
sleep 0.05
|
|
28
30
|
printf '%s\\n' '{"type":"item.completed","item":{"type":"agent_message","text":"REPORT:result\\nfinished"}}'
|
|
29
31
|
printf '%s\\n' 'REPORT:result' 'finished' > "$output"
|
|
@@ -50,7 +52,12 @@ const responsePromise = adapter.send({
|
|
|
50
52
|
const response = await responsePromise;
|
|
51
53
|
settled = true;
|
|
52
54
|
|
|
53
|
-
if (
|
|
55
|
+
if (
|
|
56
|
+
progress.length !== 3
|
|
57
|
+
|| !progress[0]?.includes("phase one")
|
|
58
|
+
|| !progress[1]?.includes("phase two")
|
|
59
|
+
|| !progress[2]?.includes("phase three")
|
|
60
|
+
) {
|
|
54
61
|
throw new Error(`Unexpected streamed progress: ${JSON.stringify(progress)}`);
|
|
55
62
|
}
|
|
56
63
|
if (progress.some((item) => item.includes("REPORT:result"))) {
|
|
@@ -254,6 +254,14 @@ async function click(data) {
|
|
|
254
254
|
await injectedBot.handleUpdates([callbackUpdate(data)]);
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
function findInlineButton(call, label) {
|
|
258
|
+
if (!call?.reply_markup) {
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
const markup = JSON.parse(call.reply_markup);
|
|
262
|
+
return markup.inline_keyboard?.flat().find((button) => button.text === label);
|
|
263
|
+
}
|
|
264
|
+
|
|
257
265
|
async function readTelegramCalls() {
|
|
258
266
|
return (await fs.readFile(telegramCalls, "utf8"))
|
|
259
267
|
.trim()
|
|
@@ -440,6 +448,83 @@ if (calls.some((call) => /미완료 TODO|\/task|새 작업으로 접수/.test(ca
|
|
|
440
448
|
throw new Error(`Task gate language leaked to Telegram replies. Calls: ${JSON.stringify(calls, null, 2)}`);
|
|
441
449
|
}
|
|
442
450
|
|
|
451
|
+
await send("/new");
|
|
452
|
+
await send("/list");
|
|
453
|
+
const sessionListCall = await waitForTelegramCall((call) => call.text.includes("Sessions (2/2)"));
|
|
454
|
+
const firstSessionButton = findInlineButton(sessionListCall, `S001 · ${path.basename(session.workspace)}`);
|
|
455
|
+
if (!firstSessionButton?.callback_data?.startsWith("remoteagent:action:")) {
|
|
456
|
+
throw new Error(`Session switch button is missing: ${sessionListCall.reply_markup}`);
|
|
457
|
+
}
|
|
458
|
+
await click(firstSessionButton.callback_data);
|
|
459
|
+
await waitForTelegramCall((call) => call.text.includes("Switched this chat to session S001."));
|
|
460
|
+
|
|
461
|
+
await send("/model");
|
|
462
|
+
const modelListCall = await waitForTelegramCall((call) => call.text.includes("availablePresets:"));
|
|
463
|
+
const modelButton = findInlineButton(modelListCall, "gpt-5.6-terra");
|
|
464
|
+
if (!modelButton?.callback_data) {
|
|
465
|
+
throw new Error(`Model selection button is missing: ${modelListCall.reply_markup}`);
|
|
466
|
+
}
|
|
467
|
+
await click(modelButton.callback_data);
|
|
468
|
+
await waitForTelegramCall((call) => call.text.includes("Set codex model to gpt-5.6-terra."));
|
|
469
|
+
const modelState = JSON.parse(await fs.readFile(path.join(dataDir, "state.json"), "utf8"));
|
|
470
|
+
if (modelState.sessions[session.sessionId]?.codex?.model !== "gpt-5.6-terra") {
|
|
471
|
+
throw new Error("Model button did not update the bound session model");
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
await send("/option");
|
|
475
|
+
const optionListCall = await waitForTelegramCall((call) => call.text.startsWith("Runtime options"));
|
|
476
|
+
const timeoutButton = findInlineButton(optionListCall, "Timeout");
|
|
477
|
+
if (!timeoutButton?.callback_data) {
|
|
478
|
+
throw new Error(`Runtime option button is missing: ${optionListCall.reply_markup}`);
|
|
479
|
+
}
|
|
480
|
+
await click(timeoutButton.callback_data);
|
|
481
|
+
await waitForTelegramCall((call) => call.text.includes("Current provider execution timeout: 600s"));
|
|
482
|
+
|
|
483
|
+
await send("/sandbox");
|
|
484
|
+
const sandboxListCall = await waitForTelegramCall((call) => call.text.startsWith("Codex sandbox"));
|
|
485
|
+
const readOnlyButton = findInlineButton(sandboxListCall, "read-only");
|
|
486
|
+
const dangerButton = findInlineButton(sandboxListCall, "danger-full-access");
|
|
487
|
+
if (!readOnlyButton?.callback_data || !dangerButton?.callback_data) {
|
|
488
|
+
throw new Error(`Sandbox selection buttons are missing: ${sandboxListCall.reply_markup}`);
|
|
489
|
+
}
|
|
490
|
+
await click(readOnlyButton.callback_data);
|
|
491
|
+
await waitForTelegramCall((call) => call.text.includes("Set Codex sandbox to read-only."));
|
|
492
|
+
await click(dangerButton.callback_data);
|
|
493
|
+
const sandboxConfirmCall = await waitForTelegramCall((call) => call.text.includes("Confirm Codex sandbox change"));
|
|
494
|
+
if (!findInlineButton(sandboxConfirmCall, "Confirm danger-full-access")?.callback_data) {
|
|
495
|
+
throw new Error(`Danger sandbox confirmation button is missing: ${sandboxConfirmCall.reply_markup}`);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
await send("/macro set button-test inspect the callback path");
|
|
499
|
+
await send("/batch start");
|
|
500
|
+
await send("/macro");
|
|
501
|
+
const macroListCall = await waitForTelegramCall((call) => call.text.includes("Macros (1)"));
|
|
502
|
+
const macroButton = findInlineButton(macroListCall, "button-test");
|
|
503
|
+
if (!macroButton?.callback_data) {
|
|
504
|
+
throw new Error(`Macro execution button is missing: ${macroListCall.reply_markup}`);
|
|
505
|
+
}
|
|
506
|
+
await click(macroButton.callback_data);
|
|
507
|
+
await send("/batch send");
|
|
508
|
+
await waitForTelegramCall((call) => call.text.includes("mock provider completed"));
|
|
509
|
+
|
|
510
|
+
await fs.appendFile(path.join(dataDir, ".env"), [
|
|
511
|
+
"TELEGRAM_BOT_TOKENS=000000:test-token",
|
|
512
|
+
"TELEGRAM_BOT_USERNAMES=remoteagent_test_bot",
|
|
513
|
+
"",
|
|
514
|
+
].join("\n"), "utf8");
|
|
515
|
+
await send("/bots");
|
|
516
|
+
const botsCall = await waitForTelegramCall((call) => call.text.includes("Configured bots (1)"));
|
|
517
|
+
const botLink = findInlineButton(botsCall, "@bot_0");
|
|
518
|
+
const refreshButton = findInlineButton(botsCall, "Refresh");
|
|
519
|
+
if (botLink?.url !== "https://t.me/bot_0" || !refreshButton?.callback_data) {
|
|
520
|
+
throw new Error(`Bot link or refresh button is missing: ${botsCall.reply_markup}`);
|
|
521
|
+
}
|
|
522
|
+
await click(refreshButton.callback_data);
|
|
523
|
+
const refreshedBotsCalls = (await readTelegramCalls()).filter((call) => call.text.includes("Configured bots (1)"));
|
|
524
|
+
if (refreshedBotsCalls.length < 2) {
|
|
525
|
+
throw new Error("Bot refresh callback did not render the bot list again");
|
|
526
|
+
}
|
|
527
|
+
|
|
443
528
|
providerMode = "timeout";
|
|
444
529
|
await send("/batch start");
|
|
445
530
|
await send("timeout regression test");
|