@llblab/pi-kit 0.1.9 → 0.1.10

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.
Files changed (35) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +2 -2
  3. package/node_modules/@llblab/pi-grow-loop/AGENTS.md +1 -1
  4. package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +6 -0
  5. package/node_modules/@llblab/pi-grow-loop/README.md +2 -1
  6. package/node_modules/@llblab/pi-grow-loop/index.ts +11 -2
  7. package/node_modules/@llblab/pi-grow-loop/package.json +1 -1
  8. package/node_modules/@llblab/pi-telegram/AGENTS.md +4 -4
  9. package/node_modules/@llblab/pi-telegram/BACKLOG.md +0 -1
  10. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +25 -0
  11. package/node_modules/@llblab/pi-telegram/README.md +1 -1
  12. package/node_modules/@llblab/pi-telegram/docs/architecture.md +5 -5
  13. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +22 -8
  14. package/node_modules/@llblab/pi-telegram/index.ts +18 -7
  15. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +49 -13
  16. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +15 -0
  17. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +17 -4
  18. package/node_modules/@llblab/pi-telegram/lib/bus.ts +56 -2
  19. package/node_modules/@llblab/pi-telegram/lib/command-templates.ts +65 -4
  20. package/node_modules/@llblab/pi-telegram/lib/commands.ts +122 -28
  21. package/node_modules/@llblab/pi-telegram/lib/journal.ts +2 -9
  22. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +20 -18
  23. package/node_modules/@llblab/pi-telegram/lib/locks.ts +6 -1
  24. package/node_modules/@llblab/pi-telegram/lib/menu-queue.ts +15 -4
  25. package/node_modules/@llblab/pi-telegram/lib/menu.ts +10 -7
  26. package/node_modules/@llblab/pi-telegram/lib/prompts.ts +2 -2
  27. package/node_modules/@llblab/pi-telegram/lib/queue.ts +49 -12
  28. package/node_modules/@llblab/pi-telegram/lib/routing.ts +5 -2
  29. package/node_modules/@llblab/pi-telegram/lib/status.ts +4 -2
  30. package/node_modules/@llblab/pi-telegram/lib/sync.ts +17 -0
  31. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +91 -45
  32. package/node_modules/@llblab/pi-telegram/lib/threads.ts +5 -0
  33. package/node_modules/@llblab/pi-telegram/lib/updates.ts +17 -9
  34. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  35. package/package.json +3 -3
@@ -816,8 +816,11 @@ export type TelegramBusEnvelope = (
816
816
  | "commit-unknown"
817
817
  | "request-id-collision"
818
818
  | "ledger-overloaded"
819
- | "incompatible-protocol";
819
+ | "incompatible-protocol"
820
+ | "stale-target";
820
821
  method?: string;
822
+ chatId?: number;
823
+ threadId?: number;
821
824
  };
822
825
  }
823
826
  ) & { auth?: string };
@@ -947,6 +950,18 @@ export interface TelegramBusLocalServer {
947
950
  ensureEndpoint: () => Promise<boolean>;
948
951
  }
949
952
 
953
+ const TELEGRAM_ACTIVE_LOCAL_SERVERS = Symbol.for(
954
+ "@llblab/pi-telegram/active-local-servers",
955
+ );
956
+ type TelegramBusServerGlobal = typeof globalThis & {
957
+ [TELEGRAM_ACTIVE_LOCAL_SERVERS]?: Map<string, TelegramBusLocalServer>;
958
+ };
959
+
960
+ function getActiveTelegramBusLocalServers(): Map<string, TelegramBusLocalServer> {
961
+ const root = globalThis as TelegramBusServerGlobal;
962
+ return (root[TELEGRAM_ACTIVE_LOCAL_SERVERS] ??= new Map());
963
+ }
964
+
950
965
  export type TelegramBusSocketPathSource = string | (() => string);
951
966
 
952
967
  const TELEGRAM_BUS_MAX_DIRECT_UNIX_ENDPOINT_BYTES = 80;
@@ -1560,6 +1575,11 @@ export function createTelegramBusLocalServer(
1560
1575
  start: async () => {
1561
1576
  if (server) return;
1562
1577
  const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
1578
+ const activeServers = getActiveTelegramBusLocalServers();
1579
+ const replacedServer = activeServers.get(socketPath);
1580
+ if (replacedServer && replacedServer !== runtime) {
1581
+ await replacedServer.stop();
1582
+ }
1563
1583
  const usesWindowsPipe = isTelegramBusPipePath(socketPath);
1564
1584
  const endpointGeneration = randomBytes(8).toString("hex");
1565
1585
  const listenPath = usesWindowsPipe
@@ -1598,6 +1618,19 @@ export function createTelegramBusLocalServer(
1598
1618
  await delayTelegramBusTransportRetry(25);
1599
1619
  }
1600
1620
  }
1621
+ if (usesWindowsPipe) {
1622
+ await deps.beforeEndpointPublication?.();
1623
+ const committed = deps.commitEndpointPublication
1624
+ ? deps.commitEndpointPublication(() => {})
1625
+ : true;
1626
+ if (!committed) {
1627
+ activeSocketPath = undefined;
1628
+ activeListenPath = undefined;
1629
+ throw new Error(
1630
+ "Telegram bus endpoint publication lost transport ownership.",
1631
+ );
1632
+ }
1633
+ }
1601
1634
  server = createServer((socket) => {
1602
1635
  sockets.add(socket);
1603
1636
  let buffer = "";
@@ -1631,6 +1664,7 @@ export function createTelegramBusLocalServer(
1631
1664
  server?.once("error", reject);
1632
1665
  server?.listen(listenPath, resolve);
1633
1666
  });
1667
+ activeServers.set(socketPath, runtime);
1634
1668
  deps.recordTransportEvent?.(
1635
1669
  "server-started",
1636
1670
  getTelegramBusEndpointDiagnostics(socketPath),
@@ -1671,6 +1705,9 @@ export function createTelegramBusLocalServer(
1671
1705
  server = undefined;
1672
1706
  activeSocketPath = undefined;
1673
1707
  activeListenPath = undefined;
1708
+ if (activeServers.get(socketPath) === runtime) {
1709
+ activeServers.delete(socketPath);
1710
+ }
1674
1711
  if (failedServer) {
1675
1712
  await new Promise<void>((resolve) =>
1676
1713
  failedServer.close(() => resolve()),
@@ -1686,6 +1723,12 @@ export function createTelegramBusLocalServer(
1686
1723
  const activeServer = server;
1687
1724
  const socketPath = activeSocketPath;
1688
1725
  const listenPath = activeListenPath;
1726
+ if (
1727
+ socketPath &&
1728
+ getActiveTelegramBusLocalServers().get(socketPath) === runtime
1729
+ ) {
1730
+ getActiveTelegramBusLocalServers().delete(socketPath);
1731
+ }
1689
1732
  server = undefined;
1690
1733
  activeSocketPath = undefined;
1691
1734
  activeListenPath = undefined;
@@ -2527,13 +2570,24 @@ function parseAckEnvelope(
2527
2570
  code === "commit-unknown" ||
2528
2571
  code === "request-id-collision" ||
2529
2572
  code === "ledger-overloaded" ||
2530
- code === "incompatible-protocol"
2573
+ code === "incompatible-protocol" ||
2574
+ code === "stale-target"
2531
2575
  ) {
2576
+ const chatId = value.error.chatId;
2577
+ const threadId = value.error.threadId;
2578
+ if (
2579
+ code === "stale-target" &&
2580
+ (!Number.isSafeInteger(chatId) || !Number.isSafeInteger(threadId))
2581
+ ) {
2582
+ return undefined;
2583
+ }
2532
2584
  envelope.error = {
2533
2585
  code,
2534
2586
  ...(typeof value.error.method === "string"
2535
2587
  ? { method: value.error.method }
2536
2588
  : {}),
2589
+ ...(typeof chatId === "number" ? { chatId } : {}),
2590
+ ...(typeof threadId === "number" ? { threadId } : {}),
2537
2591
  };
2538
2592
  }
2539
2593
  }
@@ -6,7 +6,7 @@
6
6
 
7
7
  import { spawn } from "node:child_process";
8
8
  import { homedir } from "node:os";
9
- import { isAbsolute, resolve } from "node:path";
9
+ import { extname, isAbsolute, normalize, resolve } from "node:path";
10
10
 
11
11
  export type CommandTemplateFailureScope = "continue" | "branch" | "root";
12
12
 
@@ -541,7 +541,8 @@ export function splitCommandTemplate(input: string): string[] {
541
541
  let quote: "'" | '"' | undefined;
542
542
  let escaped = false;
543
543
  let active = false;
544
- for (const char of input) {
544
+ for (let index = 0; index < input.length; index += 1) {
545
+ const char = input[index] ?? "";
545
546
  if (escaped) {
546
547
  current += char;
547
548
  escaped = false;
@@ -549,7 +550,16 @@ export function splitCommandTemplate(input: string): string[] {
549
550
  continue;
550
551
  }
551
552
  if (char === "\\" && quote !== "'") {
552
- escaped = true;
553
+ const next = input[index + 1];
554
+ const escapesNext = quote === '"'
555
+ ? next === '"' || next === "\\"
556
+ : next !== undefined &&
557
+ (/\s/u.test(next) || next === "'" || next === '"' || next === "\\");
558
+ if (escapesNext) {
559
+ escaped = true;
560
+ } else {
561
+ current += "\\";
562
+ }
553
563
  active = true;
554
564
  continue;
555
565
  }
@@ -844,15 +854,66 @@ export async function execCommandTemplate(
844
854
  return lastResult;
845
855
  }
846
856
 
857
+ const WINDOWS_COMMAND_META_CHARS = /([()\][%!^"`<>&|;, *?])/g;
858
+
859
+ function escapeWindowsCommand(value: string): string {
860
+ return value.replace(WINDOWS_COMMAND_META_CHARS, "^$1");
861
+ }
862
+
863
+ function escapeWindowsCommandArgument(
864
+ value: string,
865
+ doubleEscapeMetaChars: boolean,
866
+ ): string {
867
+ let escaped = value
868
+ .replace(/(?=(\\+?)?)\1"/g, "$1$1\\\"")
869
+ .replace(/(?=(\\+?)?)\1$/g, "$1$1");
870
+ escaped = `"${escaped}"`.replace(WINDOWS_COMMAND_META_CHARS, "^$1");
871
+ return doubleEscapeMetaChars
872
+ ? escaped.replace(WINDOWS_COMMAND_META_CHARS, "^$1")
873
+ : escaped;
874
+ }
875
+
876
+ function resolveCommandTemplateSpawn(
877
+ command: string,
878
+ args: string[],
879
+ ): {
880
+ command: string;
881
+ args: string[];
882
+ windowsVerbatimArguments?: boolean;
883
+ } {
884
+ if (
885
+ process.platform !== "win32" ||
886
+ ![".bat", ".cmd"].includes(extname(command).toLowerCase())
887
+ ) {
888
+ return { command, args };
889
+ }
890
+ const normalizedCommand = normalize(command);
891
+ const isNodeModulesShim = /[\\/]node_modules[\\/]\.bin[\\/][^\\/]+\.cmd$/iu
892
+ .test(normalizedCommand);
893
+ const shellCommand = [
894
+ escapeWindowsCommand(normalizedCommand),
895
+ ...args.map((arg) =>
896
+ escapeWindowsCommandArgument(arg, isNodeModulesShim)
897
+ ),
898
+ ].join(" ");
899
+ return {
900
+ command: process.env.ComSpec ?? process.env.COMSPEC ?? "cmd.exe",
901
+ args: ["/d", "/s", "/c", `"${shellCommand}"`],
902
+ windowsVerbatimArguments: true,
903
+ };
904
+ }
905
+
847
906
  function execCommandTemplateOnce(
848
907
  command: string,
849
908
  args: string[],
850
909
  options: CommandTemplateExecOptions = {},
851
910
  ): Promise<CommandTemplateExecResult> {
852
911
  return new Promise((resolve) => {
853
- const proc = spawn(command, args, {
912
+ const invocation = resolveCommandTemplateSpawn(command, args);
913
+ const proc = spawn(invocation.command, invocation.args, {
854
914
  cwd: options.cwd,
855
915
  shell: false,
916
+ windowsVerbatimArguments: invocation.windowsVerbatimArguments,
856
917
  stdio: [options.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"],
857
918
  });
858
919
  let stdout = "";
@@ -9,6 +9,7 @@ import {
9
9
  TELEGRAM_DEFAULT_PROFILE_NAME,
10
10
  } from "./config.ts";
11
11
  import type { ExtensionAPI, ExtensionCommandContext } from "./pi.ts";
12
+ import { escapeHtml } from "./rendering.ts";
12
13
  import type { TelegramBridgeStatusLineOptions } from "./status.ts";
13
14
  import {
14
15
  createTelegramControlItemBuilder,
@@ -185,9 +186,24 @@ export function formatTelegramCommandEmojiPrefix(
185
186
  return `${getTelegramCommandEmoji(command)} `;
186
187
  }
187
188
 
189
+ export function formatTelegramInformationHeading(
190
+ emoji: string,
191
+ text: string,
192
+ ): string {
193
+ return `<b>${escapeHtml(emoji)} ${escapeHtml(text)}</b>`;
194
+ }
195
+
188
196
  export const TELEGRAM_COMPACTION_STARTED_TEXT =
189
- `${formatTelegramCommandEmojiPrefix("compact")}Compaction started.`;
190
- export const TELEGRAM_COMPACTION_COMPLETED_TEXT = "✅ Compaction completed.";
197
+ formatTelegramInformationHeading(
198
+ getTelegramCommandEmoji("compact"),
199
+ "Compaction started.",
200
+ );
201
+ export const TELEGRAM_COMPACTION_COMPLETED_TEXT =
202
+ formatTelegramInformationHeading("✅", "Compaction completed.");
203
+ export const TELEGRAM_COMPACTION_STARTED_MARKDOWN =
204
+ `**${formatTelegramCommandEmojiPrefix("compact")}Compaction started.**`;
205
+ export const TELEGRAM_COMPACTION_COMPLETED_MARKDOWN =
206
+ "**✅ Compaction completed.**";
191
207
 
192
208
  function formatTelegramBotCommandDescription(
193
209
  command: TelegramCommandEmojiName,
@@ -585,7 +601,10 @@ export interface TelegramStopCommandDeps {
585
601
  setFoldQueuedPromptsIntoHistory: (fold: boolean) => void;
586
602
  abortCurrentTurn: () => void;
587
603
  updateStatus: () => void;
588
- sendTextReply: (text: string) => Promise<void>;
604
+ sendTextReply: (
605
+ text: string,
606
+ options?: { parseMode?: "HTML" },
607
+ ) => Promise<void>;
589
608
  }
590
609
 
591
610
  export interface TelegramRuntimeEventRecorderPort {
@@ -619,7 +638,10 @@ export interface TelegramCompactCommandDeps extends TelegramRuntimeEventRecorder
619
638
  onComplete: () => void;
620
639
  onError: (error: unknown) => void;
621
640
  }) => void;
622
- sendTextReply: (text: string) => Promise<void>;
641
+ sendTextReply: (
642
+ text: string,
643
+ options?: { parseMode?: "HTML" },
644
+ ) => Promise<void>;
623
645
  suppressStartNotice?: boolean;
624
646
  }
625
647
 
@@ -921,8 +943,11 @@ export function createTelegramCommandTargetRuntime<
921
943
  await deps.sendTextReply(
922
944
  target.chatId,
923
945
  target.replyToMessageId,
924
- "Settings menu is unavailable.",
925
- { target },
946
+ formatTelegramInformationHeading(
947
+ "🚫",
948
+ "Settings menu is unavailable.",
949
+ ),
950
+ { target, parseMode: "HTML" },
926
951
  );
927
952
  return;
928
953
  }
@@ -1108,6 +1133,25 @@ function getTelegramCommandErrorMessage(error: unknown): string {
1108
1133
  return error instanceof Error ? error.message : String(error);
1109
1134
  }
1110
1135
 
1136
+ function formatTelegramCompactionFailure(error: unknown): string {
1137
+ let message = getTelegramCommandErrorMessage(error).trim();
1138
+ const redundantPrefixes = [
1139
+ "Compaction failed: ",
1140
+ "Turn prefix summarization failed: ",
1141
+ ];
1142
+ let stripped = true;
1143
+ while (stripped) {
1144
+ stripped = false;
1145
+ for (const prefix of redundantPrefixes) {
1146
+ if (!message.startsWith(prefix)) continue;
1147
+ message = message.slice(prefix.length).trim();
1148
+ stripped = true;
1149
+ }
1150
+ }
1151
+ const sentence = /[.!?]$/u.test(message) ? message : `${message}.`;
1152
+ return `Compaction failed! ${sentence}`;
1153
+ }
1154
+
1111
1155
  export function parseTelegramCommand(
1112
1156
  text: string,
1113
1157
  ): ParsedTelegramCommand | undefined {
@@ -1165,7 +1209,10 @@ export async function handleTelegramStopCommand(
1165
1209
  ? ` Cleared ${formatTelegramQueuedTurnCount(clearedCount)}.`
1166
1210
  : "";
1167
1211
  if (clearedCount > 0) deps.updateStatus();
1168
- await deps.sendTextReply(`No active turn.${clearedSuffix}`);
1212
+ await deps.sendTextReply(
1213
+ formatTelegramInformationHeading("💤", `No active turn.${clearedSuffix}`),
1214
+ { parseMode: "HTML" },
1215
+ );
1169
1216
  return;
1170
1217
  }
1171
1218
  deps.abortCurrentTurn();
@@ -1174,7 +1221,13 @@ export async function handleTelegramStopCommand(
1174
1221
  clearedCount > 0
1175
1222
  ? ` Cleared ${formatTelegramQueuedTurnCount(clearedCount)}.`
1176
1223
  : "";
1177
- await deps.sendTextReply(`Aborted current turn.${clearedSuffix}`);
1224
+ await deps.sendTextReply(
1225
+ formatTelegramInformationHeading(
1226
+ "⏹️",
1227
+ `Aborted current turn.${clearedSuffix}`,
1228
+ ),
1229
+ { parseMode: "HTML" },
1230
+ );
1178
1231
  }
1179
1232
 
1180
1233
  export async function handleTelegramAbortCommand(deps: {
@@ -1184,17 +1237,26 @@ export async function handleTelegramAbortCommand(deps: {
1184
1237
  abortCurrentTurn: () => void;
1185
1238
  setFoldQueuedPromptsIntoHistory: (fold: boolean) => void;
1186
1239
  updateStatus: () => void;
1187
- sendTextReply: (text: string) => Promise<void>;
1240
+ sendTextReply: (
1241
+ text: string,
1242
+ options?: { parseMode?: "HTML" },
1243
+ ) => Promise<void>;
1188
1244
  }): Promise<void> {
1189
1245
  deps.clearPendingModelSwitch();
1190
1246
  if (!deps.hasAbortHandler()) {
1191
- await deps.sendTextReply("No active turn.");
1247
+ await deps.sendTextReply(
1248
+ formatTelegramInformationHeading("💤", "No active turn."),
1249
+ { parseMode: "HTML" },
1250
+ );
1192
1251
  return;
1193
1252
  }
1194
1253
  deps.setFoldQueuedPromptsIntoHistory(deps.hasActiveTelegramTurn());
1195
1254
  deps.abortCurrentTurn();
1196
1255
  deps.updateStatus();
1197
- await deps.sendTextReply("Aborted current turn.");
1256
+ await deps.sendTextReply(
1257
+ formatTelegramInformationHeading("⏹️", "Aborted current turn."),
1258
+ { parseMode: "HTML" },
1259
+ );
1198
1260
  }
1199
1261
 
1200
1262
  export async function handleTelegramNextCommand(deps: {
@@ -1213,7 +1275,10 @@ export async function handleTelegramNextCommand(deps: {
1213
1275
  }): Promise<void> {
1214
1276
  deps.clearPendingModelSwitch();
1215
1277
  if (!deps.hasQueuedItems()) {
1216
- await deps.sendTextReply("<b>Queue is empty.</b>", { parseMode: "HTML" });
1278
+ await deps.sendTextReply(
1279
+ formatTelegramInformationHeading("⌛", "Queue is empty."),
1280
+ { parseMode: "HTML" },
1281
+ );
1217
1282
  return;
1218
1283
  }
1219
1284
  if (!deps.isIdle() && deps.hasAbortHandler()) {
@@ -1221,17 +1286,30 @@ export async function handleTelegramNextCommand(deps: {
1221
1286
  deps.abortCurrentTurn();
1222
1287
  deps.updateStatus();
1223
1288
  await deps.sendTextReply(
1224
- "Aborted current turn. Dispatching next queued turn.",
1289
+ formatTelegramInformationHeading(
1290
+ "⏩",
1291
+ "Aborted! Dispatching next queued turn.",
1292
+ ),
1293
+ { parseMode: "HTML" },
1225
1294
  );
1226
1295
  return;
1227
1296
  }
1228
1297
  if (!deps.isIdle()) {
1229
- await deps.sendTextReply("Pi is busy. Send /abort or /stop first.");
1298
+ await deps.sendTextReply(
1299
+ formatTelegramInformationHeading(
1300
+ "⏳",
1301
+ "Pi is busy. Send /abort or /stop first.",
1302
+ ),
1303
+ { parseMode: "HTML" },
1304
+ );
1230
1305
  return;
1231
1306
  }
1232
1307
  deps.dispatchNextQueuedTurn();
1233
1308
  deps.updateStatus();
1234
- await deps.sendTextReply("Dispatching next queued turn.");
1309
+ await deps.sendTextReply(
1310
+ formatTelegramInformationHeading("▶️", "Dispatching next queued turn."),
1311
+ { parseMode: "HTML" },
1312
+ );
1235
1313
  }
1236
1314
 
1237
1315
  export async function handleTelegramContinueCommand<TMessage, TContext>(
@@ -1301,15 +1379,15 @@ export async function handleTelegramCompactConfirmationCallback<TContext>(
1301
1379
  const chatId = callbackMessage?.chat?.id;
1302
1380
  const messageId = callbackMessage?.message_id;
1303
1381
  if (typeof chatId !== "number" || typeof messageId !== "number") {
1304
- await deps.answerCallbackQuery(query.id, "Interactive message expired.");
1382
+ await deps.answerCallbackQuery(query.id, "Interactive message expired.");
1305
1383
  return true;
1306
1384
  }
1307
1385
  if (query.data === "compact:cancel") {
1308
1386
  await deps.editInteractiveMessage(
1309
1387
  chatId,
1310
1388
  messageId,
1311
- "Compaction cancelled.",
1312
- "plain",
1389
+ "<b>🚫 Compaction cancelled.</b>",
1390
+ "html",
1313
1391
  { inline_keyboard: [] },
1314
1392
  );
1315
1393
  await deps.answerCallbackQuery(query.id);
@@ -1319,7 +1397,7 @@ export async function handleTelegramCompactConfirmationCallback<TContext>(
1319
1397
  chatId,
1320
1398
  messageId,
1321
1399
  TELEGRAM_COMPACTION_STARTED_TEXT,
1322
- "plain",
1400
+ "html",
1323
1401
  { inline_keyboard: [] },
1324
1402
  );
1325
1403
  await deps.answerCallbackQuery(query.id);
@@ -1345,7 +1423,11 @@ export async function handleTelegramCompactCommand(
1345
1423
  deps.isCompactionInProgress()
1346
1424
  ) {
1347
1425
  await deps.sendTextReply(
1348
- "Cannot compact while Pi or the Telegram queue is busy. Wait for queued turns to finish or send /abort first.",
1426
+ formatTelegramInformationHeading(
1427
+ "⏳",
1428
+ "Cannot compact while Pi or the Telegram queue is busy. Wait for queued turns to finish or send /abort first.",
1429
+ ),
1430
+ { parseMode: "HTML" },
1349
1431
  );
1350
1432
  return;
1351
1433
  }
@@ -1359,7 +1441,9 @@ export async function handleTelegramCompactCommand(
1359
1441
  deps.setCompactionInProgress(false);
1360
1442
  deps.updateStatus();
1361
1443
  dispatchNextQueuedTelegramTurnAfterCompact(deps);
1362
- void deps.sendTextReply(TELEGRAM_COMPACTION_COMPLETED_TEXT);
1444
+ void deps.sendTextReply(TELEGRAM_COMPACTION_COMPLETED_TEXT, {
1445
+ parseMode: "HTML",
1446
+ });
1363
1447
  },
1364
1448
  onError: (error) => {
1365
1449
  deps.stopTypingLoop?.();
@@ -1367,8 +1451,13 @@ export async function handleTelegramCompactCommand(
1367
1451
  deps.updateStatus();
1368
1452
  dispatchNextQueuedTelegramTurnAfterCompact(deps);
1369
1453
  deps.recordRuntimeEvent?.("compact", error);
1370
- const errorMessage = getTelegramCommandErrorMessage(error);
1371
- void deps.sendTextReply(`Compaction failed: ${errorMessage}`);
1454
+ void deps.sendTextReply(
1455
+ formatTelegramInformationHeading(
1456
+ "⚠️",
1457
+ formatTelegramCompactionFailure(error),
1458
+ ),
1459
+ { parseMode: "HTML" },
1460
+ );
1372
1461
  },
1373
1462
  });
1374
1463
  } catch (error) {
@@ -1376,14 +1465,19 @@ export async function handleTelegramCompactCommand(
1376
1465
  deps.setCompactionInProgress(false);
1377
1466
  deps.updateStatus();
1378
1467
  deps.recordRuntimeEvent?.("compact", error);
1379
- const errorMessage = getTelegramCommandErrorMessage(error);
1380
- await deps.sendTextReply(`Compaction failed: ${errorMessage}`);
1468
+ await deps.sendTextReply(
1469
+ formatTelegramInformationHeading(
1470
+ "⚠️",
1471
+ formatTelegramCompactionFailure(error),
1472
+ ),
1473
+ { parseMode: "HTML" },
1474
+ );
1381
1475
  return;
1382
1476
  }
1383
1477
  if (!deps.suppressStartNotice) {
1384
- await deps.sendTextReply(
1385
- TELEGRAM_COMPACTION_STARTED_TEXT,
1386
- );
1478
+ await deps.sendTextReply(TELEGRAM_COMPACTION_STARTED_TEXT, {
1479
+ parseMode: "HTML",
1480
+ });
1387
1481
  }
1388
1482
  }
1389
1483
 
@@ -3381,15 +3381,8 @@ export function createTelegramUpdateJournalStore(
3381
3381
  const recoveredUpdateIds = [...requestedIds].sort((a, b) => a - b);
3382
3382
  const published = publishMutation(
3383
3383
  current,
3384
- current.file.entries.map((entry) =>
3385
- requestedIds.has(entry.updateId)
3386
- ? {
3387
- updateId: entry.updateId,
3388
- update: entry.update,
3389
- admittedAtMs: entry.admittedAtMs,
3390
- state: "pending" as const,
3391
- }
3392
- : entry,
3384
+ current.file.entries.filter(
3385
+ (entry) => !requestedIds.has(entry.updateId),
3393
3386
  ),
3394
3387
  true,
3395
3388
  );
@@ -345,25 +345,27 @@ export function createTelegramBridgeSessionLifecycleAssembly<
345
345
  stopPolling: suspendForReplacement,
346
346
  clearPendingMediaGroups: deps.services.suspendGroupedInput,
347
347
  });
348
- const servicesLifecycle = appendTelegramLifecycleHooks(
349
- queueLifecycle,
350
- {
351
- async onSessionStart(event, ctx) {
352
- deps.services.resumeGroupedInput(ctx);
353
- await deps.services.delivery.onSessionStart();
354
- await deps.services.polling.onSessionStart(event, ctx);
355
- deps.services.capabilityMonitor.start(ctx);
356
- deps.services.queueWatchdog.start(ctx);
357
- },
358
- async onSessionShutdown() {
359
- await deps.services.delivery.onSessionShutdown();
360
- deps.services.queueWatchdog.stop();
361
- deps.services.capabilityMonitor.stop();
362
- await deps.services.inboundWorker.onSessionShutdown();
363
- },
348
+ const servicesLifecycle: TelegramSessionLifecycleHooks = {
349
+ async onSessionStart(event, ctx) {
350
+ await queueLifecycle.onSessionStart(event, ctx);
351
+ if (!isSessionActive(ctx)) return;
352
+ deps.services.resumeGroupedInput(ctx);
353
+ await deps.services.delivery.onSessionStart();
354
+ await deps.services.polling.onSessionStart(event, ctx);
355
+ deps.services.capabilityMonitor.start(ctx);
356
+ deps.services.queueWatchdog.start(ctx);
364
357
  },
365
- isSessionActive,
366
- );
358
+ async onSessionShutdown(event, ctx) {
359
+ if (!isSessionActive(ctx)) return;
360
+ await deps.services.delivery.onSessionShutdown();
361
+ if (!isSessionActive(ctx)) return;
362
+ deps.services.queueWatchdog.stop();
363
+ deps.services.capabilityMonitor.stop();
364
+ await queueLifecycle.onSessionShutdown(event, ctx);
365
+ if (!isSessionActive(ctx)) return;
366
+ await deps.services.inboundWorker.onSessionShutdown();
367
+ },
368
+ };
367
369
  const followerLifecycle = appendTelegramLifecycleHooks(
368
370
  servicesLifecycle,
369
371
  {
@@ -423,7 +423,12 @@ function reclaimAbandonedDirectoryGuard(
423
423
  // Claim inside the still-occupied guard before making its stable path free.
424
424
  renameRecovery(sourcePath, reclaimPath);
425
425
  } catch (error) {
426
- if ((error as { code?: unknown })?.code === "ENOENT") return false;
426
+ const code = (error as { code?: unknown })?.code;
427
+ if (code === "ENOENT") return false;
428
+ // macOS may report EINVAL instead of ENOENT when another process wins
429
+ // the same source rename. Only classify it as contention once the
430
+ // observed source is actually gone; preserve unrelated EINVAL failures.
431
+ if (code === "EINVAL" && !existsSync(sourcePath)) return false;
427
432
  throw error;
428
433
  }
429
434
 
@@ -73,6 +73,10 @@ function toTelegramQueueMenuItems<Context>(
73
73
  });
74
74
  }
75
75
 
76
+ function formatSkippedTelegramQueuePosition(position: number): string {
77
+ return Array.from(String(position), (char) => `${char}\u0335`).join("");
78
+ }
79
+
76
80
  function buildTelegramQueueMenuReplyMarkup(
77
81
  items: readonly TelegramQueueMenuItem[],
78
82
  emptyRefreshIndex = 0,
@@ -86,7 +90,7 @@ function buildTelegramQueueMenuReplyMarkup(
86
90
  : "queue:refresh";
87
91
  const refreshRow = [{ text: "🌀 Refresh", callback_data: refreshData }];
88
92
  if (items.length === 0) return { inline_keyboard: [backRow, refreshRow] };
89
- const rows = items.map((item, index) => {
93
+ const rows = items.map((item) => {
90
94
  const prefix = item.reactionSuppressionEmoji
91
95
  ? `${item.reactionSuppressionEmoji} `
92
96
  : item.isPriority
@@ -94,7 +98,11 @@ function buildTelegramQueueMenuReplyMarkup(
94
98
  : item.hasAttachments
95
99
  ? "📎 "
96
100
  : "";
97
- const label = `${index + 1}. ${prefix}${item.statusSummary}`;
101
+ const position = item.reactionSuppressionEmoji
102
+ ? formatSkippedTelegramQueuePosition(item.queuePosition)
103
+ : String(item.queuePosition);
104
+ const ordinalSeparator = item.reactionSuppressionEmoji ? "\u200A" : "";
105
+ const label = `${position}${ordinalSeparator}. ${prefix}${item.statusSummary}`;
98
106
  return [
99
107
  {
100
108
  text: label,
@@ -162,7 +170,10 @@ function getTelegramQueueMenuItemText(item: TelegramQueueMenuItem): string {
162
170
  : item.isPriority
163
171
  ? ` ${item.priorityEmoji ?? "⚡"}`
164
172
  : "";
165
- const heading = `<b>${item.queuePosition}.</b>${badge}`;
173
+ const position = item.reactionSuppressionEmoji
174
+ ? `<s>${item.queuePosition}</s>.`
175
+ : `<b>${item.queuePosition}.</b>`;
176
+ const heading = `${position}${badge}`;
166
177
  const preview = `<pre>${escapeTelegramQueueMenuHtmlPreview(item.promptText)}</pre>`;
167
178
  return `${heading}\n${preview}`;
168
179
  }
@@ -182,7 +193,7 @@ function buildTelegramQueueItemSubmenuReplyMarkup(
182
193
  callback_data: `queue:prio-set:${chatId}:${replyToMessageId}:priority`,
183
194
  },
184
195
  {
185
- text: isPriority ? "⚫️ Normal" : "🟣 Normal",
196
+ text: isPriority ? "⚫️ Normal" : "🔵 Normal",
186
197
  callback_data: `queue:prio-set:${chatId}:${replyToMessageId}:normal`,
187
198
  },
188
199
  ],