@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.
- package/CHANGELOG.md +4 -0
- package/README.md +2 -2
- package/node_modules/@llblab/pi-grow-loop/AGENTS.md +1 -1
- package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +6 -0
- package/node_modules/@llblab/pi-grow-loop/README.md +2 -1
- package/node_modules/@llblab/pi-grow-loop/index.ts +11 -2
- package/node_modules/@llblab/pi-grow-loop/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/AGENTS.md +4 -4
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +0 -1
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +25 -0
- package/node_modules/@llblab/pi-telegram/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +5 -5
- package/node_modules/@llblab/pi-telegram/docs/ui-style.md +22 -8
- package/node_modules/@llblab/pi-telegram/index.ts +18 -7
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +49 -13
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +15 -0
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +17 -4
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +56 -2
- package/node_modules/@llblab/pi-telegram/lib/command-templates.ts +65 -4
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +122 -28
- package/node_modules/@llblab/pi-telegram/lib/journal.ts +2 -9
- package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +20 -18
- package/node_modules/@llblab/pi-telegram/lib/locks.ts +6 -1
- package/node_modules/@llblab/pi-telegram/lib/menu-queue.ts +15 -4
- package/node_modules/@llblab/pi-telegram/lib/menu.ts +10 -7
- package/node_modules/@llblab/pi-telegram/lib/prompts.ts +2 -2
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +49 -12
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +5 -2
- package/node_modules/@llblab/pi-telegram/lib/status.ts +4 -2
- package/node_modules/@llblab/pi-telegram/lib/sync.ts +17 -0
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +91 -45
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +5 -0
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +17 -9
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- 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 (
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
190
|
-
|
|
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: (
|
|
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: (
|
|
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
|
-
|
|
925
|
-
|
|
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(
|
|
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(
|
|
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: (
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
1371
|
-
|
|
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
|
-
|
|
1380
|
-
|
|
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
|
-
|
|
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.
|
|
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 =
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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" : "
|
|
196
|
+
text: isPriority ? "⚫️ Normal" : "🔵 Normal",
|
|
186
197
|
callback_data: `queue:prio-set:${chatId}:${replyToMessageId}:normal`,
|
|
187
198
|
},
|
|
188
199
|
],
|