@llblab/pi-telegram 0.39.2 → 0.39.4
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 +11 -0
- package/lib/bus.ts +40 -0
- package/lib/command-templates.ts +65 -4
- package/lib/locks.ts +6 -1
- package/lib/status.ts +14 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
> Each release keeps at most 8 outcome records of at most 512 characters.
|
|
4
4
|
|
|
5
|
+
## 0.39.4: Headless Status Bar Hotfix
|
|
6
|
+
|
|
7
|
+
- `Headless Hosts`: Skips status-bar rendering when a print, RPC, ACP, or other non-interactive host has not initialized its theme, preventing repeated extension errors during lifecycle status refreshes while preserving normal interactive status updates and error propagation.
|
|
8
|
+
|
|
9
|
+
## 0.39.3: Windows IPC And Outbound Voice Hotfix
|
|
10
|
+
|
|
11
|
+
- `Windows IPC Replacement`: Replaces an older same-process Named Pipe server before a new session generation listens on the stable endpoint, preventing `EADDRINUSE` during reload while keeping a late stop from invalidating its successor.
|
|
12
|
+
- `Windows IPC Fencing`: Applies the ownership commit fence before publishing a Named Pipe, so a stale generation that loses authority cannot expose an endpoint or block its replacement.
|
|
13
|
+
- `Windows Voice Commands`: Executes trusted `.cmd` and `.bat` outbound-handler wrappers through escaped `%ComSpec%` adaptation while preserving direct shell-free execution for native executables, bounded process controls, npm command-shim argument isolation, and paths containing spaces.
|
|
14
|
+
- `Windows Template Paths`: Preserves backslashes in quoted and unquoted Windows executable and artifact paths without breaking intentional escaped whitespace or quotes, allowing configured TTS handlers to produce and upload OGG/Opus voice replies through direct and follower transport.
|
|
15
|
+
|
|
5
16
|
## 0.39.2: Deleted Thread Receipt Hotfix
|
|
6
17
|
|
|
7
18
|
- `Deleted Thread Receipt`: Terminally settles the currently executing durable update when Telegram returns exact HTTP 400 stale/deleted-thread evidence for its `{chatId, threadId}`, including leader-first shared-store invalidation followed by follower settlement. Transient, ambiguous, unrelated, and stale-looking HTTP 5xx failures retain indefinite retry authority; persisted follower records remain restart hints rather than speculative live registrations.
|
package/lib/bus.ts
CHANGED
|
@@ -950,6 +950,18 @@ export interface TelegramBusLocalServer {
|
|
|
950
950
|
ensureEndpoint: () => Promise<boolean>;
|
|
951
951
|
}
|
|
952
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
|
+
|
|
953
965
|
export type TelegramBusSocketPathSource = string | (() => string);
|
|
954
966
|
|
|
955
967
|
const TELEGRAM_BUS_MAX_DIRECT_UNIX_ENDPOINT_BYTES = 80;
|
|
@@ -1563,6 +1575,11 @@ export function createTelegramBusLocalServer(
|
|
|
1563
1575
|
start: async () => {
|
|
1564
1576
|
if (server) return;
|
|
1565
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
|
+
}
|
|
1566
1583
|
const usesWindowsPipe = isTelegramBusPipePath(socketPath);
|
|
1567
1584
|
const endpointGeneration = randomBytes(8).toString("hex");
|
|
1568
1585
|
const listenPath = usesWindowsPipe
|
|
@@ -1601,6 +1618,19 @@ export function createTelegramBusLocalServer(
|
|
|
1601
1618
|
await delayTelegramBusTransportRetry(25);
|
|
1602
1619
|
}
|
|
1603
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
|
+
}
|
|
1604
1634
|
server = createServer((socket) => {
|
|
1605
1635
|
sockets.add(socket);
|
|
1606
1636
|
let buffer = "";
|
|
@@ -1634,6 +1664,7 @@ export function createTelegramBusLocalServer(
|
|
|
1634
1664
|
server?.once("error", reject);
|
|
1635
1665
|
server?.listen(listenPath, resolve);
|
|
1636
1666
|
});
|
|
1667
|
+
activeServers.set(socketPath, runtime);
|
|
1637
1668
|
deps.recordTransportEvent?.(
|
|
1638
1669
|
"server-started",
|
|
1639
1670
|
getTelegramBusEndpointDiagnostics(socketPath),
|
|
@@ -1674,6 +1705,9 @@ export function createTelegramBusLocalServer(
|
|
|
1674
1705
|
server = undefined;
|
|
1675
1706
|
activeSocketPath = undefined;
|
|
1676
1707
|
activeListenPath = undefined;
|
|
1708
|
+
if (activeServers.get(socketPath) === runtime) {
|
|
1709
|
+
activeServers.delete(socketPath);
|
|
1710
|
+
}
|
|
1677
1711
|
if (failedServer) {
|
|
1678
1712
|
await new Promise<void>((resolve) =>
|
|
1679
1713
|
failedServer.close(() => resolve()),
|
|
@@ -1689,6 +1723,12 @@ export function createTelegramBusLocalServer(
|
|
|
1689
1723
|
const activeServer = server;
|
|
1690
1724
|
const socketPath = activeSocketPath;
|
|
1691
1725
|
const listenPath = activeListenPath;
|
|
1726
|
+
if (
|
|
1727
|
+
socketPath &&
|
|
1728
|
+
getActiveTelegramBusLocalServers().get(socketPath) === runtime
|
|
1729
|
+
) {
|
|
1730
|
+
getActiveTelegramBusLocalServers().delete(socketPath);
|
|
1731
|
+
}
|
|
1692
1732
|
server = undefined;
|
|
1693
1733
|
activeSocketPath = undefined;
|
|
1694
1734
|
activeListenPath = undefined;
|
package/lib/command-templates.ts
CHANGED
|
@@ -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 = "";
|
package/lib/locks.ts
CHANGED
|
@@ -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
|
|
package/lib/status.ts
CHANGED
|
@@ -628,18 +628,28 @@ export function createTelegramStatusHtmlBuilder<TContext>(deps: {
|
|
|
628
628
|
);
|
|
629
629
|
}
|
|
630
630
|
|
|
631
|
+
function resolveStatusBarTheme(
|
|
632
|
+
ctx: TelegramStatusRuntimeContext,
|
|
633
|
+
): TelegramStatusBarTheme | undefined {
|
|
634
|
+
try {
|
|
635
|
+
const theme = ctx.ui?.theme;
|
|
636
|
+
return typeof theme?.fg === "function" ? theme : undefined;
|
|
637
|
+
} catch {
|
|
638
|
+
return undefined;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
631
642
|
export function createTelegramStatusRuntime<
|
|
632
643
|
TContext extends TelegramStatusRuntimeContext,
|
|
633
644
|
>(deps: TelegramStatusRuntimeDeps<TContext>): TelegramStatusRuntime<TContext> {
|
|
634
645
|
const statusKey = deps.statusKey ?? "telegram";
|
|
635
646
|
return {
|
|
636
647
|
updateStatus: (ctx, error) => {
|
|
648
|
+
const theme = resolveStatusBarTheme(ctx);
|
|
649
|
+
if (!theme) return;
|
|
637
650
|
ctx.ui.setStatus(
|
|
638
651
|
statusKey,
|
|
639
|
-
buildTelegramStatusBarText(
|
|
640
|
-
ctx.ui.theme,
|
|
641
|
-
deps.getStatusBarState(ctx, error),
|
|
642
|
-
),
|
|
652
|
+
buildTelegramStatusBarText(theme, deps.getStatusBarState(ctx, error)),
|
|
643
653
|
);
|
|
644
654
|
},
|
|
645
655
|
getStatusLines: (options) =>
|