@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 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
- - [ ] If Windows live smoke exposes pipe-specific behavior, add a minimized regression at the bus transport boundary before changing higher-level Threaded Mode logic.
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
  },
@@ -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
- response = await sendTelegramBusLocalEnvelope({
652
- socketPath: leaderSocketPath,
653
- timeoutMs: registrationTimeoutMs,
654
- envelope: {
655
- kind: "follower.register",
656
- requestId: deps.createRequestId(),
657
- auth: activeAuthSecret,
658
- registration: {
659
- instanceId: deps.instanceId,
660
- profileKey:
661
- deps.getProfileKey?.(ctx) ??
662
- (ctx.cwd ? `cwd:${ctx.cwd}` : undefined),
663
- threadName:
664
- deps.getThreadName?.(ctx) ??
665
- (ctx.cwd ? basename(ctx.cwd) : undefined),
666
- cwd: ctx.cwd,
667
- pid: getPid(),
668
- busSocketPath: deps.followerBusSocketPath,
669
- connectedAtMs: getNowMs(),
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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.18.3",
3
+ "version": "0.18.4",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"