@llblab/pi-telegram 0.18.3 → 0.18.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/BACKLOG.md +4 -1
- package/CHANGELOG.md +5 -0
- package/index.ts +8 -0
- package/lib/bus-follower.ts +69 -22
- package/lib/queue.ts +70 -0
- package/package.json +1 -1
package/BACKLOG.md
CHANGED
|
@@ -36,8 +36,11 @@ Open work:
|
|
|
36
36
|
|
|
37
37
|
- [ ] Live smoke Threaded Mode on native Windows without WSL.
|
|
38
38
|
- Scope: leader/follower `/telegram-connect`, follower heartbeat, forwarded Bot API calls, restore flows, lifecycle announcements, shutdown cleanup, and reconnect/reload behavior.
|
|
39
|
+
- Observed: a same-directory follower can see a live leader lock but fail registration with `connect ENOENT \\.\\pipe\\...`, leaving the follower disconnected during leader reload/hot activation timing.
|
|
40
|
+
- Observed: Windows/QEMU live polling/dispatch can lag until reload; inbound messages appear to increase the extension queue count, but the next queued item is not dispatched promptly.
|
|
39
41
|
- Baseline: deterministic path tests run everywhere, and a Windows-only named-pipe roundtrip regression runs when the suite executes on `win32`. Live Windows smoke remains unavailable in this environment.
|
|
40
|
-
- [
|
|
42
|
+
- [x] Add a minimized registration-boundary regression for transient leader endpoint startup races.
|
|
43
|
+
- [x] Add a session-bound queue dispatch watchdog so queued Telegram work can recover if a one-shot wakeup/timer is missed.
|
|
41
44
|
|
|
42
45
|
Done when: Threaded Mode leader/follower operation works on native Windows with the same safety guarantees as Unix-like systems, and unsupported transport assumptions are covered by tests/docs.
|
|
43
46
|
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.18.4: Windows Threaded Mode hotfix
|
|
6
|
+
|
|
7
|
+
- `[Windows IPC]` Follower registration now retries transient local bus connection failures while the leader named pipe/socket is still coming online. Impact: a same-directory Windows follower is less likely to fail `/telegram-connect` with `connect ENOENT \\.\\pipe\\...` during leader reload or hot Threaded Mode activation.
|
|
8
|
+
- `[Queue]` A session-bound queue dispatch watchdog now retries dispatch while Telegram work remains queued. Impact: if a platform drops the one-shot deferred dispatch wakeup, queued Telegram messages can resume without waiting for a manual `/reload`.
|
|
9
|
+
|
|
5
10
|
## 0.18.3: Threaded Mode live hotfix
|
|
6
11
|
|
|
7
12
|
- `[Threaded Mode]` Inbound Telegram prompts now request an immediate dispatch and a session-bound deferred retry. Impact: hosts where Pi is not yet dispatch-ready at update handling time no longer need a later `/reload` or command to process the queued prompt.
|
package/index.ts
CHANGED
|
@@ -454,6 +454,12 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
454
454
|
...promptDispatchRuntime,
|
|
455
455
|
sendUserMessage,
|
|
456
456
|
}).dispatchNext;
|
|
457
|
+
const queueDispatchWatchdogRuntime =
|
|
458
|
+
Queue.createTelegramQueueDispatchWatchdogRuntime({
|
|
459
|
+
hasQueuedItems: telegramQueueStore.hasQueuedItems,
|
|
460
|
+
dispatchNextQueuedTelegramTurn,
|
|
461
|
+
recordRuntimeEvent,
|
|
462
|
+
});
|
|
457
463
|
const nativeMarkdownDraftSender =
|
|
458
464
|
TelegramApi.createTelegramNativeMarkdownDraftSender({
|
|
459
465
|
sendMessageDraft,
|
|
@@ -1168,8 +1174,10 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1168
1174
|
async onSessionStart(event, ctx) {
|
|
1169
1175
|
await lockedPollingRuntime.onSessionStart(event, ctx);
|
|
1170
1176
|
telegramThreadCapabilityMonitor.start(ctx);
|
|
1177
|
+
queueDispatchWatchdogRuntime.start(ctx);
|
|
1171
1178
|
},
|
|
1172
1179
|
async onSessionShutdown() {
|
|
1180
|
+
queueDispatchWatchdogRuntime.stop();
|
|
1173
1181
|
telegramThreadCapabilityMonitor.stop();
|
|
1174
1182
|
},
|
|
1175
1183
|
},
|
package/lib/bus-follower.ts
CHANGED
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
|
|
23
23
|
export const TELEGRAM_BUS_FOLLOWER_PROMOTION_GRACE_MS = 2_500;
|
|
24
24
|
export const TELEGRAM_FOLLOWER_SESSION_HANDOFF_TTL_MS = 30_000;
|
|
25
|
+
export const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_ATTEMPTS = 10;
|
|
26
|
+
export const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_DELAY_MS = 150;
|
|
25
27
|
|
|
26
28
|
const TELEGRAM_FOLLOWER_SESSION_HANDOFF_KEY =
|
|
27
29
|
"__piTelegramFollowerSessionHandoff";
|
|
@@ -150,6 +152,8 @@ export interface TelegramBusFollowerRegistrationRuntimeDeps<
|
|
|
150
152
|
getPid?: () => number;
|
|
151
153
|
timeoutMs?: number;
|
|
152
154
|
registrationTimeoutMs?: number;
|
|
155
|
+
registrationRetryAttempts?: number;
|
|
156
|
+
registrationRetryDelayMs?: number;
|
|
153
157
|
heartbeatMs?: number;
|
|
154
158
|
recordRuntimeEvent?: (
|
|
155
159
|
category: string,
|
|
@@ -364,6 +368,19 @@ function isTelegramStaleContextError(error: unknown): boolean {
|
|
|
364
368
|
);
|
|
365
369
|
}
|
|
366
370
|
|
|
371
|
+
function isRetryableTelegramBusRegistrationError(error: unknown): boolean {
|
|
372
|
+
if (!(error instanceof Error)) return false;
|
|
373
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
374
|
+
return code === "ENOENT" || code === "ECONNREFUSED" || code === "EPIPE";
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function delayTelegramBusFollowerRegistration(ms: number): Promise<void> {
|
|
378
|
+
return new Promise((resolve) => {
|
|
379
|
+
const timer = setTimeout(resolve, ms);
|
|
380
|
+
timer.unref?.();
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
367
384
|
export function createTelegramBusFollowerSessionReplacementSuspender(
|
|
368
385
|
deps: TelegramBusFollowerSessionReplacementSuspenderDeps,
|
|
369
386
|
): () => Promise<void> {
|
|
@@ -588,6 +605,12 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
588
605
|
const heartbeatMs = deps.heartbeatMs ?? 1000;
|
|
589
606
|
const registrationTimeoutMs =
|
|
590
607
|
deps.registrationTimeoutMs ?? deps.timeoutMs ?? 30000;
|
|
608
|
+
const registrationRetryAttempts =
|
|
609
|
+
deps.registrationRetryAttempts ??
|
|
610
|
+
TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_ATTEMPTS;
|
|
611
|
+
const registrationRetryDelayMs =
|
|
612
|
+
deps.registrationRetryDelayMs ??
|
|
613
|
+
TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_DELAY_MS;
|
|
591
614
|
let heartbeatInterval: ReturnType<typeof setInterval> | undefined;
|
|
592
615
|
let activeLeaderSocketPath: string | undefined;
|
|
593
616
|
let activeAuthSecret: string | undefined;
|
|
@@ -646,30 +669,54 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
646
669
|
await deps.startReceiving?.();
|
|
647
670
|
activeAuthSecret = deps.getLeaderAuthSecret?.(leader);
|
|
648
671
|
deps.setActiveAuthSecret?.(activeAuthSecret);
|
|
672
|
+
const createRegistrationEnvelope = (): Extract<
|
|
673
|
+
TelegramBusEnvelope,
|
|
674
|
+
{ kind: "follower.register" }
|
|
675
|
+
> => ({
|
|
676
|
+
kind: "follower.register",
|
|
677
|
+
requestId: deps.createRequestId(),
|
|
678
|
+
auth: activeAuthSecret,
|
|
679
|
+
registration: {
|
|
680
|
+
instanceId: deps.instanceId,
|
|
681
|
+
profileKey:
|
|
682
|
+
deps.getProfileKey?.(ctx) ??
|
|
683
|
+
(ctx.cwd ? `cwd:${ctx.cwd}` : undefined),
|
|
684
|
+
threadName:
|
|
685
|
+
deps.getThreadName?.(ctx) ??
|
|
686
|
+
(ctx.cwd ? basename(ctx.cwd) : undefined),
|
|
687
|
+
cwd: ctx.cwd,
|
|
688
|
+
pid: getPid(),
|
|
689
|
+
busSocketPath: deps.followerBusSocketPath,
|
|
690
|
+
connectedAtMs: getNowMs(),
|
|
691
|
+
},
|
|
692
|
+
});
|
|
649
693
|
let response: TelegramBusEnvelope | undefined;
|
|
650
694
|
try {
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
|
|
695
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
696
|
+
try {
|
|
697
|
+
response = await sendTelegramBusLocalEnvelope({
|
|
698
|
+
socketPath: leaderSocketPath,
|
|
699
|
+
timeoutMs: registrationTimeoutMs,
|
|
700
|
+
envelope: createRegistrationEnvelope(),
|
|
701
|
+
});
|
|
702
|
+
break;
|
|
703
|
+
} catch (error) {
|
|
704
|
+
if (
|
|
705
|
+
attempt >= registrationRetryAttempts ||
|
|
706
|
+
!isRetryableTelegramBusRegistrationError(error)
|
|
707
|
+
) {
|
|
708
|
+
throw error;
|
|
709
|
+
}
|
|
710
|
+
deps.recordRuntimeEvent?.("bus", error, {
|
|
711
|
+
phase: "follower-register-retry",
|
|
712
|
+
attempt,
|
|
713
|
+
socketPath: leaderSocketPath,
|
|
714
|
+
});
|
|
715
|
+
await delayTelegramBusFollowerRegistration(
|
|
716
|
+
registrationRetryDelayMs,
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
673
720
|
} catch (error) {
|
|
674
721
|
stopHeartbeat();
|
|
675
722
|
activeLeaderSocketPath = undefined;
|
package/lib/queue.ts
CHANGED
|
@@ -1920,6 +1920,76 @@ export function createTelegramDeferredQueueDispatchRuntime<TContext = unknown>(
|
|
|
1920
1920
|
};
|
|
1921
1921
|
}
|
|
1922
1922
|
|
|
1923
|
+
// --- Dispatch Watchdog Runtime ---
|
|
1924
|
+
|
|
1925
|
+
export interface TelegramQueueDispatchWatchdogRuntime<TContext = unknown> {
|
|
1926
|
+
start: (ctx: TContext) => void;
|
|
1927
|
+
stop: () => void;
|
|
1928
|
+
poke: () => void;
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
export interface TelegramQueueDispatchWatchdogRuntimeDeps<
|
|
1932
|
+
TContext = unknown,
|
|
1933
|
+
> extends TelegramRuntimeEventRecorderPort {
|
|
1934
|
+
hasQueuedItems: () => boolean;
|
|
1935
|
+
dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
|
|
1936
|
+
intervalMs?: number;
|
|
1937
|
+
setInterval?: (
|
|
1938
|
+
callback: () => void,
|
|
1939
|
+
ms: number,
|
|
1940
|
+
) => ReturnType<typeof setInterval>;
|
|
1941
|
+
clearInterval?: (timer: ReturnType<typeof setInterval>) => void;
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
export function createTelegramQueueDispatchWatchdogRuntime<
|
|
1945
|
+
TContext = unknown,
|
|
1946
|
+
>(
|
|
1947
|
+
deps: TelegramQueueDispatchWatchdogRuntimeDeps<TContext>,
|
|
1948
|
+
): TelegramQueueDispatchWatchdogRuntime<TContext> {
|
|
1949
|
+
const intervalMs = deps.intervalMs ?? 1000;
|
|
1950
|
+
const setIntervalFn: NonNullable<
|
|
1951
|
+
TelegramQueueDispatchWatchdogRuntimeDeps<TContext>["setInterval"]
|
|
1952
|
+
> = deps.setInterval ?? ((callback, ms) => setInterval(callback, ms));
|
|
1953
|
+
const clearIntervalFn: NonNullable<
|
|
1954
|
+
TelegramQueueDispatchWatchdogRuntimeDeps<TContext>["clearInterval"]
|
|
1955
|
+
> = deps.clearInterval ?? ((timer) => clearInterval(timer));
|
|
1956
|
+
let ctx: TContext | undefined;
|
|
1957
|
+
let interval: ReturnType<typeof setInterval> | undefined;
|
|
1958
|
+
let dispatchInFlight = false;
|
|
1959
|
+
const tick = (): void => {
|
|
1960
|
+
if (ctx === undefined || dispatchInFlight || !deps.hasQueuedItems()) return;
|
|
1961
|
+
dispatchInFlight = true;
|
|
1962
|
+
try {
|
|
1963
|
+
deps.dispatchNextQueuedTelegramTurn(ctx);
|
|
1964
|
+
} catch (error) {
|
|
1965
|
+
deps.recordRuntimeEvent?.("dispatch", error, {
|
|
1966
|
+
phase: "queue-watchdog",
|
|
1967
|
+
});
|
|
1968
|
+
} finally {
|
|
1969
|
+
dispatchInFlight = false;
|
|
1970
|
+
}
|
|
1971
|
+
};
|
|
1972
|
+
const stop = (): void => {
|
|
1973
|
+
ctx = undefined;
|
|
1974
|
+
if (!interval) return;
|
|
1975
|
+
clearIntervalFn(interval);
|
|
1976
|
+
interval = undefined;
|
|
1977
|
+
};
|
|
1978
|
+
return {
|
|
1979
|
+
start: (nextCtx) => {
|
|
1980
|
+
ctx = nextCtx;
|
|
1981
|
+
if (!interval) {
|
|
1982
|
+
const nextInterval = setIntervalFn(tick, intervalMs);
|
|
1983
|
+
interval = nextInterval;
|
|
1984
|
+
nextInterval.unref?.();
|
|
1985
|
+
}
|
|
1986
|
+
tick();
|
|
1987
|
+
},
|
|
1988
|
+
stop,
|
|
1989
|
+
poke: tick,
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1923
1993
|
// --- Dispatch Runtime ---
|
|
1924
1994
|
|
|
1925
1995
|
export interface TelegramPromptDeliveryOptions {
|