appback-remoteagent 0.16.0 → 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/README.md +3 -0
- package/dist/bot.js +139 -8
- package/dist/index.js +4 -37
- package/dist/telegram-bot-identity.js +36 -0
- package/dist/telegram-command-menu.js +1 -0
- package/docs/OPERATIONS.md +4 -0
- package/docs/RELEASING.md +23 -0
- package/package.json +1 -1
- package/scripts/selftest-telegram-update.mjs +110 -0
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
|
|
package/dist/bot.js
CHANGED
|
@@ -24,6 +24,7 @@ const HELP_TEXT = [
|
|
|
24
24
|
"/attach codex <thread_id>",
|
|
25
25
|
"/attach claude <session_id>",
|
|
26
26
|
"/model [name]",
|
|
27
|
+
"/queue [remove <id>|del]",
|
|
27
28
|
"/stop",
|
|
28
29
|
"/sandbox codex <read-only|workspace-write|danger-full-access>",
|
|
29
30
|
"/status",
|
|
@@ -80,6 +81,7 @@ const RECOGNIZED_COMMANDS = new Set([
|
|
|
80
81
|
"batch",
|
|
81
82
|
"attach",
|
|
82
83
|
"model",
|
|
84
|
+
"queue",
|
|
83
85
|
"stop",
|
|
84
86
|
"sandbox",
|
|
85
87
|
"status",
|
|
@@ -100,6 +102,8 @@ const TELEGRAM_STALE_UPDATE_GRACE_SECONDS = 10;
|
|
|
100
102
|
const TELEGRAM_PROCESS_STARTED_AT_SECONDS = Math.floor(Date.now() / 1000);
|
|
101
103
|
const workLoopTails = new Map();
|
|
102
104
|
const workLoopGenerations = new Map();
|
|
105
|
+
const queuedWorkLoops = new Map();
|
|
106
|
+
let nextQueuedWorkSequence = 1;
|
|
103
107
|
const REPORT_CONTINUE_PROMPT = [
|
|
104
108
|
"Continue the same task now.",
|
|
105
109
|
"Do more concrete work before replying again.",
|
|
@@ -453,17 +457,47 @@ ${bridge.formatStatus(mapping)}`);
|
|
|
453
457
|
const mapping = await bridge.setModel(botId, chatId, model);
|
|
454
458
|
await reply(ctx, `Set ${mapping.session.mode} model to ${model}.\n\n${bridge.formatStatus(mapping)}`);
|
|
455
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
|
+
});
|
|
456
490
|
bot.command("stop", async (ctx) => {
|
|
457
491
|
const botId = getBotId();
|
|
458
492
|
const chatId = String(ctx.chat.id);
|
|
459
493
|
const mapping = await bridge.status(botId, chatId);
|
|
460
494
|
const sessionId = mapping?.session.sessionId;
|
|
461
495
|
autoContinue.requestStop(botId, chatId, sessionId);
|
|
462
|
-
cancelQueuedWorkLoops(botId, chatId, sessionId);
|
|
496
|
+
const queuedWorkCount = cancelQueuedWorkLoops(botId, chatId, sessionId);
|
|
463
497
|
const pendingBatch = messageBatcher.cancelPending(botId, chatId);
|
|
464
498
|
const manualBatch = messageBatcher.cancelManual(botId, chatId);
|
|
465
499
|
if (!autoContinue.beginStop(botId, chatId, sessionId)) {
|
|
466
|
-
const batchCount = pendingBatch.count + manualBatch.count;
|
|
500
|
+
const batchCount = queuedWorkCount + pendingBatch.count + manualBatch.count;
|
|
467
501
|
if (batchCount > 0) {
|
|
468
502
|
await bridge.logSystem(botId, chatId, `Duplicate stop discarded ${batchCount} queued message(s).`);
|
|
469
503
|
}
|
|
@@ -472,7 +506,7 @@ ${bridge.formatStatus(mapping)}`);
|
|
|
472
506
|
try {
|
|
473
507
|
const result = await bridge.stopActiveRun(botId, chatId);
|
|
474
508
|
await bridge.logSystem(botId, chatId, "Stop requested for auto-continue.");
|
|
475
|
-
const batchCount = pendingBatch.count + manualBatch.count;
|
|
509
|
+
const batchCount = queuedWorkCount + pendingBatch.count + manualBatch.count;
|
|
476
510
|
await reply(ctx, result.stopped
|
|
477
511
|
? `Stop requested. Active work for ${result.sessionPublicId ?? "this session"} was interrupted, further automatic continuation will stop, and ${batchCount} queued message(s) were discarded.`
|
|
478
512
|
: `Stop requested. No active provider process was running, but further automatic continuation will stop, and ${batchCount} queued message(s) were discarded.`);
|
|
@@ -1228,12 +1262,36 @@ async function routeTelegramWorkLoop(bridge, botId, chatId, message, label, botM
|
|
|
1228
1262
|
.catch(() => undefined)
|
|
1229
1263
|
.then(() => gate);
|
|
1230
1264
|
workLoopTails.set(activeKey, currentTail);
|
|
1265
|
+
let queuedEntry;
|
|
1231
1266
|
if (previousTail) {
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
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}.`);
|
|
1237
1295
|
release();
|
|
1238
1296
|
if (workLoopTails.get(activeKey) === currentTail) {
|
|
1239
1297
|
workLoopTails.delete(activeKey);
|
|
@@ -1460,9 +1518,82 @@ function cancelQueuedWorkLoops(botId, chatId, sessionId) {
|
|
|
1460
1518
|
if (sessionId) {
|
|
1461
1519
|
keys.push(workLoopKey(botId, chatId));
|
|
1462
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
|
+
}
|
|
1463
1532
|
for (const key of keys) {
|
|
1464
1533
|
workLoopGenerations.set(key, (workLoopGenerations.get(key) ?? 0) + 1);
|
|
1465
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");
|
|
1466
1597
|
}
|
|
1467
1598
|
class SilentTelegramAbort extends Error {
|
|
1468
1599
|
constructor(message) {
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,7 @@ import { ProviderRecoveryService } from "./services/provider-recovery-service.js
|
|
|
20
20
|
import { computePolicyPollIntervalMs, computeRecentMessageRanks } from "./services/polling-policy.js";
|
|
21
21
|
import { terminateAllSpawnedExecutions } from "./adapters/windows-shell.js";
|
|
22
22
|
import { setTelegramCommandMenu } from "./telegram-command-menu.js";
|
|
23
|
+
import { buildBotInfoFromIdentity, buildFallbackBotInfo } from "./telegram-bot-identity.js";
|
|
23
24
|
const execFileAsync = promisify(execFile);
|
|
24
25
|
const TELEGRAM_GET_UPDATES_HTTP_TIMEOUT_SECONDS = 30;
|
|
25
26
|
const TELEGRAM_GET_UPDATES_CURL_TIMEOUT_SECONDS = 60;
|
|
@@ -68,7 +69,7 @@ async function main() {
|
|
|
68
69
|
console.error("Local UI failed to start:", error);
|
|
69
70
|
});
|
|
70
71
|
}
|
|
71
|
-
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])));
|
|
72
73
|
const bots = config.telegramBotTokens.map((token, index) => createBot(token, bridge, botManagement, botInfos[index]));
|
|
73
74
|
if (config.telegramCommandMenuEnabled) {
|
|
74
75
|
for (const bot of bots) {
|
|
@@ -484,7 +485,7 @@ class AsyncSemaphore {
|
|
|
484
485
|
}
|
|
485
486
|
}
|
|
486
487
|
}
|
|
487
|
-
async function resolveBotInfo(token, index) {
|
|
488
|
+
async function resolveBotInfo(token, index, configuredUsername) {
|
|
488
489
|
try {
|
|
489
490
|
const { stdout } = await execFileAsync("curl", [
|
|
490
491
|
"-sS",
|
|
@@ -502,32 +503,7 @@ async function resolveBotInfo(token, index) {
|
|
|
502
503
|
catch (error) {
|
|
503
504
|
console.warn(`Telegram getMe failed for bot ${tokenIdLabel(token)}: ${summarizeTelegramIdentityError(error)}`);
|
|
504
505
|
}
|
|
505
|
-
return buildFallbackBotInfo(token, index);
|
|
506
|
-
}
|
|
507
|
-
function buildBotInfoFromIdentity(id, username, firstName) {
|
|
508
|
-
return {
|
|
509
|
-
id,
|
|
510
|
-
is_bot: true,
|
|
511
|
-
first_name: firstName || username,
|
|
512
|
-
username,
|
|
513
|
-
can_join_groups: false,
|
|
514
|
-
can_read_all_group_messages: false,
|
|
515
|
-
supports_inline_queries: false,
|
|
516
|
-
};
|
|
517
|
-
}
|
|
518
|
-
function buildFallbackBotInfo(token, index) {
|
|
519
|
-
const id = Number.parseInt(token.split(":", 1)[0] ?? "", 10);
|
|
520
|
-
const fallbackUsername = knownBotUsername(id);
|
|
521
|
-
const username = fallbackUsername || `bot_${Number.isFinite(id) ? id : index + 1}`;
|
|
522
|
-
return {
|
|
523
|
-
id: Number.isFinite(id) ? id : index + 1,
|
|
524
|
-
is_bot: true,
|
|
525
|
-
first_name: username,
|
|
526
|
-
username,
|
|
527
|
-
can_join_groups: false,
|
|
528
|
-
can_read_all_group_messages: false,
|
|
529
|
-
supports_inline_queries: false,
|
|
530
|
-
};
|
|
506
|
+
return buildFallbackBotInfo(token, index, configuredUsername);
|
|
531
507
|
}
|
|
532
508
|
function tokenIdLabel(token) {
|
|
533
509
|
return token.split(":", 1)[0] || "unknown";
|
|
@@ -543,15 +519,6 @@ function summarizeTelegramIdentityError(error) {
|
|
|
543
519
|
.filter(Boolean)
|
|
544
520
|
.join(" ");
|
|
545
521
|
}
|
|
546
|
-
function knownBotUsername(id) {
|
|
547
|
-
if (id === 8369496408) {
|
|
548
|
-
return "codex_remoteagent_bot";
|
|
549
|
-
}
|
|
550
|
-
if (id === 8429712341) {
|
|
551
|
-
return "sqream_bot";
|
|
552
|
-
}
|
|
553
|
-
return undefined;
|
|
554
|
-
}
|
|
555
522
|
function commandExists(command) {
|
|
556
523
|
const trimmed = command.trim();
|
|
557
524
|
if (!trimmed) {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export function buildBotInfoFromIdentity(id, username, firstName) {
|
|
2
|
+
return {
|
|
3
|
+
id,
|
|
4
|
+
is_bot: true,
|
|
5
|
+
first_name: firstName || username,
|
|
6
|
+
username,
|
|
7
|
+
can_join_groups: false,
|
|
8
|
+
can_read_all_group_messages: false,
|
|
9
|
+
supports_inline_queries: false,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export function buildFallbackBotInfo(token, index, configuredUsername) {
|
|
13
|
+
const id = Number.parseInt(token.split(":", 1)[0] ?? "", 10);
|
|
14
|
+
const persistedUsername = configuredUsername?.trim().replace(/^@/, "");
|
|
15
|
+
const username = persistedUsername
|
|
16
|
+
|| knownBotUsername(id)
|
|
17
|
+
|| `bot_${Number.isFinite(id) ? id : index + 1}`;
|
|
18
|
+
return {
|
|
19
|
+
id: Number.isFinite(id) ? id : index + 1,
|
|
20
|
+
is_bot: true,
|
|
21
|
+
first_name: username,
|
|
22
|
+
username,
|
|
23
|
+
can_join_groups: false,
|
|
24
|
+
can_read_all_group_messages: false,
|
|
25
|
+
supports_inline_queries: false,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function knownBotUsername(id) {
|
|
29
|
+
if (id === 8369496408) {
|
|
30
|
+
return "codex_remoteagent_bot";
|
|
31
|
+
}
|
|
32
|
+
if (id === 8429712341) {
|
|
33
|
+
return "sqream_bot";
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
@@ -15,6 +15,7 @@ export const TELEGRAM_COMMAND_MENU = [
|
|
|
15
15
|
{ command: "docs", description: "Pin or find session documents" },
|
|
16
16
|
{ command: "macro", description: "Save or run reusable instructions" },
|
|
17
17
|
{ command: "model", description: "Show or change provider model" },
|
|
18
|
+
{ command: "queue", description: "List or remove queued instructions" },
|
|
18
19
|
{ command: "stop", description: "Stop active work and clear queued messages" },
|
|
19
20
|
{ command: "sandbox", description: "Set Codex sandbox mode" },
|
|
20
21
|
{ command: "batch", description: "Collect and send a multi-message batch" },
|
package/docs/OPERATIONS.md
CHANGED
|
@@ -44,6 +44,10 @@ Current production bot ownership is intentionally split:
|
|
|
44
44
|
Assign each Telegram bot token to one runtime at a time.
|
|
45
45
|
Bot polling conflicts are treated as incidents, not harmless warnings.
|
|
46
46
|
|
|
47
|
+
`TELEGRAM_BOT_TOKENS` and `TELEGRAM_BOT_USERNAMES` are parallel persisted lists.
|
|
48
|
+
At startup, Telegram `getMe` is the preferred identity source, but a temporary DNS or Telegram failure must fall back to the persisted username at the same list position.
|
|
49
|
+
Using a generated `bot_<numeric-id>` identity when a persisted username exists breaks the existing `<username>:<chat-id>` session binding.
|
|
50
|
+
|
|
47
51
|
When a runtime has several configured Telegram bots, polling pressure can become operationally visible.
|
|
48
52
|
RemoteAgent reduces that pressure with rank-based polling intervals instead of deep sleep or a special main bot.
|
|
49
53
|
See [BOT_POLLING_POLICY.md](./BOT_POLLING_POLICY.md).
|
package/docs/RELEASING.md
CHANGED
|
@@ -174,3 +174,26 @@ Runtime targets:
|
|
|
174
174
|
server 30: 0.15.5 active
|
|
175
175
|
server 26: 0.15.5 running
|
|
176
176
|
```
|
|
177
|
+
|
|
178
|
+
## Release 0.17.0
|
|
179
|
+
|
|
180
|
+
Date: 2026-07-29
|
|
181
|
+
|
|
182
|
+
Changes:
|
|
183
|
+
|
|
184
|
+
- Queued instructions receive runtime-unique `Q001`-style ids.
|
|
185
|
+
- `/queue` lists instructions waiting behind the current session work.
|
|
186
|
+
- `/queue remove <id>` removes one selected waiting instruction.
|
|
187
|
+
- `/queue del` removes the most recently queued instruction.
|
|
188
|
+
- `/stop` reports and clears queued work-loop instructions as well as pending message batches.
|
|
189
|
+
- Telegram startup preserves the configured bot username when `getMe` temporarily fails, preventing existing chat/session bindings from being bypassed by a generated numeric bot identity.
|
|
190
|
+
|
|
191
|
+
Validated:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
npm run check
|
|
195
|
+
npm run build
|
|
196
|
+
npm run selftest:telegram
|
|
197
|
+
npm run release:publish
|
|
198
|
+
npm run release:deploy -- 0.17.0 all
|
|
199
|
+
```
|
package/package.json
CHANGED
|
@@ -69,6 +69,7 @@ const [
|
|
|
69
69
|
{ FileStore },
|
|
70
70
|
{ AgentMemoryService },
|
|
71
71
|
{ WorkspaceCleanupService },
|
|
72
|
+
{ buildFallbackBotInfo },
|
|
72
73
|
] = await Promise.all([
|
|
73
74
|
import(path.join(root, "dist", "bot.js")),
|
|
74
75
|
import(path.join(root, "dist", "services", "bridge-service.js")),
|
|
@@ -76,12 +77,26 @@ const [
|
|
|
76
77
|
import(path.join(root, "dist", "store", "file-store.js")),
|
|
77
78
|
import(path.join(root, "dist", "services", "agent-memory-service.js")),
|
|
78
79
|
import(path.join(root, "dist", "services", "workspace-cleanup-service.js")),
|
|
80
|
+
import(path.join(root, "dist", "telegram-bot-identity.js")),
|
|
79
81
|
]);
|
|
80
82
|
|
|
83
|
+
const persistedBotIdentity = buildFallbackBotInfo(
|
|
84
|
+
"8966593034:test-token",
|
|
85
|
+
0,
|
|
86
|
+
"@appbackadmin_bot",
|
|
87
|
+
);
|
|
88
|
+
if (persistedBotIdentity.username !== "appbackadmin_bot") {
|
|
89
|
+
throw new Error(`Configured bot username was not preserved during getMe fallback: ${persistedBotIdentity.username}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
81
92
|
const providerCalls = [];
|
|
82
93
|
let providerMode = "success";
|
|
83
94
|
let untaggedIntentCalls = 0;
|
|
84
95
|
let missingEvidenceCalls = 0;
|
|
96
|
+
let queueHoldStartedResolve;
|
|
97
|
+
let queueHoldReleaseResolve;
|
|
98
|
+
let queueHoldStartedPromise = Promise.resolve();
|
|
99
|
+
let queueHoldReleasePromise = Promise.resolve();
|
|
85
100
|
const provider = {
|
|
86
101
|
async send(request) {
|
|
87
102
|
providerCalls.push(request);
|
|
@@ -112,6 +127,17 @@ const provider = {
|
|
|
112
127
|
: "REPORT:result\n수정 완료했습니다.\n\n근거:\n- 변경 파일: `src/example.ts`\n- 검증: `npm run check` 통과",
|
|
113
128
|
};
|
|
114
129
|
}
|
|
130
|
+
if (providerMode === "queue-hold") {
|
|
131
|
+
queueHoldStartedResolve?.();
|
|
132
|
+
await queueHoldReleasePromise;
|
|
133
|
+
return {
|
|
134
|
+
provider: "codex",
|
|
135
|
+
sessionId: request.sessionId || "mock-thread",
|
|
136
|
+
publicSessionId: request.publicSessionId,
|
|
137
|
+
cwd: request.cwd,
|
|
138
|
+
output: "REPORT:result\nactive queue test completed",
|
|
139
|
+
};
|
|
140
|
+
}
|
|
115
141
|
return {
|
|
116
142
|
provider: "codex",
|
|
117
143
|
sessionId: request.sessionId || "mock-thread",
|
|
@@ -169,6 +195,34 @@ async function send(text) {
|
|
|
169
195
|
await injectedBot.handleUpdates([update(text)]);
|
|
170
196
|
}
|
|
171
197
|
|
|
198
|
+
async function readTelegramCalls() {
|
|
199
|
+
return (await fs.readFile(telegramCalls, "utf8"))
|
|
200
|
+
.trim()
|
|
201
|
+
.split("\n")
|
|
202
|
+
.filter(Boolean)
|
|
203
|
+
.map((line) => {
|
|
204
|
+
const [method, chatId, textB64 = ""] = line.split("\t");
|
|
205
|
+
return {
|
|
206
|
+
method,
|
|
207
|
+
chat_id: chatId,
|
|
208
|
+
text: Buffer.from(textB64, "base64").toString("utf8"),
|
|
209
|
+
};
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function waitForTelegramCall(predicate, timeoutMs = 3000) {
|
|
214
|
+
const deadline = Date.now() + timeoutMs;
|
|
215
|
+
while (Date.now() < deadline) {
|
|
216
|
+
const calls = await readTelegramCalls();
|
|
217
|
+
const match = [...calls].reverse().find(predicate);
|
|
218
|
+
if (match) {
|
|
219
|
+
return match;
|
|
220
|
+
}
|
|
221
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
222
|
+
}
|
|
223
|
+
throw new Error("Timed out waiting for Telegram self-test call.");
|
|
224
|
+
}
|
|
225
|
+
|
|
172
226
|
await send("/start codex");
|
|
173
227
|
await send("/option retry 6");
|
|
174
228
|
await send("/option timeout 600");
|
|
@@ -407,6 +461,60 @@ if (evidenceCalls.some((call) => call.method === "sendMessage" && /^수정 완
|
|
|
407
461
|
throw new Error(`Evidence-free completion leaked as final Telegram message. Calls: ${JSON.stringify(evidenceCalls, null, 2)}`);
|
|
408
462
|
}
|
|
409
463
|
|
|
464
|
+
const queueProviderCallsBefore = providerCalls.length;
|
|
465
|
+
providerMode = "queue-hold";
|
|
466
|
+
queueHoldStartedPromise = new Promise((resolve) => {
|
|
467
|
+
queueHoldStartedResolve = resolve;
|
|
468
|
+
});
|
|
469
|
+
queueHoldReleasePromise = new Promise((resolve) => {
|
|
470
|
+
queueHoldReleaseResolve = resolve;
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
await send("/batch start");
|
|
474
|
+
await send("active queue regression test");
|
|
475
|
+
const activeQueueSend = send("/batch send");
|
|
476
|
+
await queueHoldStartedPromise;
|
|
477
|
+
|
|
478
|
+
await send("/batch start");
|
|
479
|
+
await send("first queued instruction");
|
|
480
|
+
const firstQueuedSend = send("/batch send");
|
|
481
|
+
const firstQueueNotice = await waitForTelegramCall((call) => /Queued instruction Q\d+/.test(call.text));
|
|
482
|
+
const firstQueueId = firstQueueNotice.text.match(/Queued instruction (Q\d+)/)?.[1];
|
|
483
|
+
if (!firstQueueId) {
|
|
484
|
+
throw new Error(`First queued instruction did not receive an id: ${firstQueueNotice.text}`);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
await send("/batch start");
|
|
488
|
+
await send("second queued instruction");
|
|
489
|
+
const secondQueuedSend = send("/batch send");
|
|
490
|
+
const secondQueueNotice = await waitForTelegramCall((call) => {
|
|
491
|
+
const queueId = call.text.match(/Queued instruction (Q\d+)/)?.[1];
|
|
492
|
+
return Boolean(queueId && queueId !== firstQueueId);
|
|
493
|
+
});
|
|
494
|
+
const secondQueueId = secondQueueNotice.text.match(/Queued instruction (Q\d+)/)?.[1];
|
|
495
|
+
if (!secondQueueId) {
|
|
496
|
+
throw new Error(`Second queued instruction did not receive an id: ${secondQueueNotice.text}`);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
await send("/queue");
|
|
500
|
+
const queueListCall = await waitForTelegramCall((call) =>
|
|
501
|
+
call.text.includes(firstQueueId) && call.text.includes(secondQueueId)
|
|
502
|
+
);
|
|
503
|
+
if (!/Queued instructions for S001 \(2\)/.test(queueListCall.text)) {
|
|
504
|
+
throw new Error(`Queue list did not report both entries: ${queueListCall.text}`);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
await send(`/queue remove ${firstQueueId}`);
|
|
508
|
+
await waitForTelegramCall((call) => call.text.includes(`Removed queued instruction ${firstQueueId}`));
|
|
509
|
+
await send("/queue del");
|
|
510
|
+
await waitForTelegramCall((call) => call.text.includes(`Removed queued instruction ${secondQueueId}`));
|
|
511
|
+
|
|
512
|
+
queueHoldReleaseResolve?.();
|
|
513
|
+
await Promise.all([activeQueueSend, firstQueuedSend, secondQueuedSend]);
|
|
514
|
+
if (providerCalls.length !== queueProviderCallsBefore + 1) {
|
|
515
|
+
throw new Error(`Removed queued instructions reached the provider: ${providerCalls.length - queueProviderCallsBefore} calls`);
|
|
516
|
+
}
|
|
517
|
+
|
|
410
518
|
console.log(JSON.stringify({
|
|
411
519
|
ok: true,
|
|
412
520
|
dataDir,
|
|
@@ -419,6 +527,8 @@ console.log(JSON.stringify({
|
|
|
419
527
|
providerCalls: providerCalls.length,
|
|
420
528
|
untaggedIntentCalls,
|
|
421
529
|
missingEvidenceCalls,
|
|
530
|
+
queueRemoveById: firstQueueId,
|
|
531
|
+
queueRemoveLatest: secondQueueId,
|
|
422
532
|
timeoutFinalMessage: true,
|
|
423
533
|
telegramSendMessages: evidenceCalls.filter((call) => call.method === "sendMessage").length,
|
|
424
534
|
}, null, 2));
|