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 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
- await bridge.logSystem(botId, chatId, `Queued overlapping Telegram work loop for ${activeKey}.`);
1233
- await helpers.reportProgress([`Queued instruction for ${currentSession?.session.publicId ?? "this session"}. It will run after the active work finishes.`]);
1234
- await previousTail.catch(() => undefined);
1235
- if ((workLoopGenerations.get(activeKey) ?? 0) !== generation || autoContinue.isStopRequested(botId, chatId, sessionId)) {
1236
- await bridge.logSystem(botId, chatId, `Discarded queued Telegram work loop for ${activeKey}.`);
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" },
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appback-remoteagent",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Personal installable session server for continuing local AI work across PC and Telegram",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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));