@llblab/pi-telegram 0.50.1 → 0.51.1

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 (73) hide show
  1. package/AGENTS.md +10 -8
  2. package/BACKLOG.md +13 -32
  3. package/CHANGELOG.md +16 -0
  4. package/README.md +4 -3
  5. package/dist/lib/bindings.d.ts +4 -2
  6. package/dist/lib/bindings.js +5 -5
  7. package/dist/lib/bus-follower.d.ts +4 -2
  8. package/dist/lib/bus-follower.js +99 -10
  9. package/dist/lib/bus-leader.d.ts +9 -3
  10. package/dist/lib/bus-leader.js +231 -71
  11. package/dist/lib/bus.d.ts +4 -0
  12. package/dist/lib/bus.js +34 -7
  13. package/dist/lib/commands.d.ts +5 -0
  14. package/dist/lib/commands.js +9 -0
  15. package/dist/lib/extension.js +68 -3
  16. package/dist/lib/journal.d.ts +3 -0
  17. package/dist/lib/journal.js +13 -0
  18. package/dist/lib/lifecycle.d.ts +2 -0
  19. package/dist/lib/lifecycle.js +25 -4
  20. package/dist/lib/locks.d.ts +1 -0
  21. package/dist/lib/locks.js +32 -12
  22. package/dist/lib/paths.d.ts +10 -0
  23. package/dist/lib/paths.js +22 -1
  24. package/dist/lib/polling.js +2 -2
  25. package/dist/lib/queue.d.ts +9 -1
  26. package/dist/lib/queue.js +64 -13
  27. package/dist/lib/replies.d.ts +4 -0
  28. package/dist/lib/replies.js +14 -0
  29. package/dist/lib/routing.d.ts +1 -0
  30. package/dist/lib/routing.js +5 -0
  31. package/dist/lib/sync.d.ts +9 -0
  32. package/dist/lib/sync.js +41 -1
  33. package/dist/lib/telegram-api.d.ts +9 -1
  34. package/dist/lib/telegram-api.js +23 -3
  35. package/dist/lib/thread-cleanup-manager.js +7 -2
  36. package/dist/lib/threads.d.ts +2 -0
  37. package/dist/lib/threads.js +68 -37
  38. package/dist/lib/updates.d.ts +6 -4
  39. package/dist/lib/updates.js +134 -61
  40. package/dist/lib/workspace-admission.d.ts +9 -0
  41. package/dist/lib/workspace-admission.js +49 -3
  42. package/dist/lib/workspace-retirement.d.ts +77 -6
  43. package/dist/lib/workspace-retirement.js +384 -8
  44. package/dist/lib/workspace-slots.d.ts +3 -0
  45. package/dist/lib/workspace-slots.js +6 -0
  46. package/dist/package.json +1 -1
  47. package/docs/architecture.md +22 -15
  48. package/docs/multi-instance-bus.md +32 -11
  49. package/docs/public-api.md +1 -1
  50. package/docs/updates.md +2 -0
  51. package/lib/bindings.ts +12 -7
  52. package/lib/bus-follower.ts +82 -14
  53. package/lib/bus-leader.ts +236 -94
  54. package/lib/bus.ts +39 -9
  55. package/lib/commands.ts +19 -0
  56. package/lib/extension.ts +78 -2
  57. package/lib/journal.ts +14 -0
  58. package/lib/lifecycle.ts +23 -4
  59. package/lib/locks.ts +32 -13
  60. package/lib/paths.ts +29 -1
  61. package/lib/polling.ts +2 -2
  62. package/lib/queue.ts +77 -19
  63. package/lib/replies.ts +18 -0
  64. package/lib/routing.ts +7 -0
  65. package/lib/sync.ts +45 -1
  66. package/lib/telegram-api.ts +30 -2
  67. package/lib/thread-cleanup-manager.ts +11 -3
  68. package/lib/threads.ts +73 -36
  69. package/lib/updates.ts +145 -78
  70. package/lib/workspace-admission.ts +67 -4
  71. package/lib/workspace-retirement.ts +465 -7
  72. package/lib/workspace-slots.ts +7 -0
  73. package/package.json +1 -1
package/lib/bindings.ts CHANGED
@@ -52,6 +52,7 @@ export interface TelegramQueueBindingRuntime<TContext> {
52
52
  mutation: Queue.TelegramQueueMutationController<TContext>;
53
53
  dispatchNext: (ctx: TContext) => void;
54
54
  requestNextDispatchAnnouncement: () => void;
55
+ cancelNextDispatchAnnouncement: () => void;
55
56
  watchdog: Queue.TelegramQueueDispatchWatchdogRuntime<TContext>;
56
57
  }
57
58
 
@@ -69,12 +70,12 @@ export function createTelegramQueueBindingRuntime<TContext>(deps: {
69
70
  onItemsDiscarded: (
70
71
  items: readonly Queue.TelegramQueueItem<TContext>[],
71
72
  ctx: TContext,
72
- ) => void;
73
+ ) => boolean;
73
74
  isItemReady: (item: Queue.TelegramQueueItem<TContext>) => boolean;
74
75
  onPromptHandedOff?: (
75
76
  item: Queue.PendingTelegramTurn,
76
77
  ctx: TContext,
77
- ) => void;
78
+ ) => boolean;
78
79
  onControlSettled: (
79
80
  item: Queue.PendingTelegramControlItem<TContext>,
80
81
  ctx: TContext,
@@ -96,6 +97,9 @@ export function createTelegramQueueBindingRuntime<TContext>(deps: {
96
97
  updateStatus: (ctx: TContext, error?: string) => void;
97
98
  sendTextReply: Queue.TelegramQueueDispatchRuntimeDeps<TContext>["sendTextReply"];
98
99
  sendUserMessage: Queue.TelegramQueueDispatchRuntimeDeps<TContext>["sendUserMessage"];
100
+ reconcileNextDispatchAnnouncementReplyOwnership?: (
101
+ item: Queue.PendingTelegramTurn,
102
+ ) => void;
99
103
  recordRuntimeEvent?: TelegramRuntimeEventRecorder;
100
104
  }): TelegramQueueBindingRuntime<TContext> {
101
105
  const settleDiscardedItems = (
@@ -108,8 +112,7 @@ export function createTelegramQueueBindingRuntime<TContext>(deps: {
108
112
  if (durableItems.length === 0) return true;
109
113
  const settlement = deps.admission.getSettlement();
110
114
  if (!settlement) return false;
111
- settlement.onItemsDiscarded(durableItems, ctx);
112
- return durableItems.every((item) => !settlement.isItemReady(item));
115
+ return settlement.onItemsDiscarded(durableItems, ctx) === true;
113
116
  };
114
117
  const mutation = Queue.createTelegramQueueMutationController({
115
118
  ...deps.store,
@@ -150,8 +153,7 @@ export function createTelegramQueueBindingRuntime<TContext>(deps: {
150
153
  if ((item.admissionReceipts?.length ?? 0) === 0) return true;
151
154
  const settlement = deps.admission.getSettlement();
152
155
  if (!settlement?.onPromptHandedOff) return false;
153
- settlement.onPromptHandedOff(item, ctx);
154
- return !settlement.isItemReady(item);
156
+ return settlement.onPromptHandedOff(item, ctx) === true;
155
157
  },
156
158
  onControlSettled(item, ctx) {
157
159
  deps.admission.getSettlement()?.onControlSettled(item, ctx);
@@ -161,6 +163,8 @@ export function createTelegramQueueBindingRuntime<TContext>(deps: {
161
163
  },
162
164
  updateStatus: deps.updateStatus,
163
165
  sendTextReply: deps.sendTextReply,
166
+ reconcileNextDispatchAnnouncementReplyOwnership:
167
+ deps.reconcileNextDispatchAnnouncementReplyOwnership,
164
168
  recordRuntimeEvent: deps.recordRuntimeEvent,
165
169
  ...deps.promptDispatch,
166
170
  sendUserMessage: deps.sendUserMessage,
@@ -169,6 +173,7 @@ export function createTelegramQueueBindingRuntime<TContext>(deps: {
169
173
  mutation,
170
174
  dispatchNext: dispatch.dispatchNext,
171
175
  requestNextDispatchAnnouncement: dispatch.requestNextDispatchAnnouncement,
176
+ cancelNextDispatchAnnouncement: dispatch.cancelNextDispatchAnnouncement,
172
177
  watchdog: Queue.createTelegramQueueDispatchWatchdogRuntime({
173
178
  hasQueuedItems: deps.store.hasQueuedItems,
174
179
  dispatchNextQueuedTelegramTurn: dispatch.dispatchNext,
@@ -1143,7 +1148,7 @@ export function registerTelegramLifecycleRuntimeHooks({
1143
1148
  getActiveTurn: activeTurnRuntime.get,
1144
1149
  loadConfig: configStore.load,
1145
1150
  extractAssistant: Replies.extractRunAssistantMessage,
1146
- isRecoveredAssistantAlreadyPublished: (assistant) =>
1151
+ isAssistantAlreadyPublished: (assistant) =>
1147
1152
  !!assistant.text && assistantOutputRuntime.hasAdmittedTelegramIntermediate(assistant.text),
1148
1153
  getFoldQueuedPromptsIntoHistory:
1149
1154
  lifecycle.shouldFoldQueuedPromptsIntoHistory,
@@ -11,7 +11,11 @@ import { basename } from "node:path";
11
11
  import * as Sync from "./sync.ts";
12
12
  import * as Threads from "./threads.ts";
13
13
  import { parseTelegramUpdateJournalQueueOwner, type TelegramUpdateJournalStoreOptions } from "./journal.ts";
14
- import type { TelegramLockEntry, TelegramLockState } from "./locks.ts";
14
+ import {
15
+ TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS,
16
+ type TelegramLockEntry,
17
+ type TelegramLockState,
18
+ } from "./locks.ts";
15
19
  import type {
16
20
  TelegramQueueHandoffPayload,
17
21
  TelegramQueueHandoffStageResult,
@@ -60,6 +64,8 @@ export const TELEGRAM_BUS_FOLLOWER_PROMOTION_GRACE_MS = 2_500;
60
64
  export const TELEGRAM_FOLLOWER_SESSION_HANDOFF_TTL_MS = 30_000;
61
65
  export const TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS = 30_000;
62
66
  export const TELEGRAM_BUS_FOLLOWER_REGISTRATION_WAIT_MS = 30_000;
67
+ export const TELEGRAM_BUS_FOLLOWER_HEARTBEAT_TIMEOUT_MS =
68
+ TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS;
63
69
  export const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_ATTEMPTS =
64
70
  TELEGRAM_BUS_REGISTRATION_RETRY.attempts;
65
71
  export const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_DELAY_MS =
@@ -126,7 +132,7 @@ export interface TelegramBusFollowerRegistrationRuntime<TContext> {
126
132
  restoreWorkspace?: boolean;
127
133
  },
128
134
  ) => Promise<boolean>;
129
- setContext: (ctx: TContext) => void;
135
+ setContext: (ctx: TContext) => void | Promise<void>;
130
136
  disconnectFromLeader?: () => Promise<boolean>;
131
137
  renameThread?: (
132
138
  target: TelegramTarget & { threadId: number },
@@ -301,6 +307,7 @@ export interface TelegramBusFollowerRegistrationRuntimeDeps<
301
307
  registrationRetryAttempts?: number;
302
308
  registrationRetryDelayMs?: number;
303
309
  heartbeatMs?: number;
310
+ heartbeatTimeoutMs?: number;
304
311
  recordRuntimeEvent?: (
305
312
  category: string,
306
313
  error: unknown,
@@ -623,6 +630,22 @@ export function createTelegramBusFollowerRuntimeAssembly<
623
630
  >(
624
631
  ports: TelegramBusFollowerRuntimeAssemblyPorts<TContext>,
625
632
  ): TelegramBusFollowerRuntimeAssembly<TContext> {
633
+ // Binding startup needs registration identity before it can grant inbound authority.
634
+ let readyContext: { generation: string; ctx: TContext; sessionGeneration: number | undefined } | undefined;
635
+ const isReadyContext = (ctx?: TContext): boolean => Boolean(readyContext &&
636
+ (ctx === undefined || readyContext.ctx === ctx) &&
637
+ ports.registrationState.getGeneration() === readyContext.generation &&
638
+ ports.registration.getSessionGeneration?.() === readyContext.sessionGeneration &&
639
+ ports.registration.isContextActive?.(readyContext.ctx) !== false);
640
+ const prepareContext = async (ctx: TContext): Promise<void> => {
641
+ const generation = ports.registrationState.getGeneration();
642
+ const sessionGeneration = ports.registration.getSessionGeneration?.();
643
+ readyContext = undefined;
644
+ await ports.registration.onRegistered?.(ctx);
645
+ if (generation && ports.registrationState.getGeneration() === generation &&
646
+ ports.registration.getSessionGeneration?.() === sessionGeneration &&
647
+ ports.registration.isContextActive?.(ctx) !== false) readyContext = { generation, ctx, sessionGeneration };
648
+ };
626
649
  const sharedRuntimeDeps = {
627
650
  instanceId: ports.instanceId,
628
651
  registrationState: ports.registrationState,
@@ -632,7 +655,9 @@ export function createTelegramBusFollowerRuntimeAssembly<
632
655
  ...ports.receiver,
633
656
  instanceId: ports.instanceId,
634
657
  recordRuntimeEvent: ports.recordRuntimeEvent,
635
- getRegistrationGeneration: ports.registrationState.getGeneration,
658
+ getRegistrationGeneration() {
659
+ return isReadyContext() ? readyContext?.generation : undefined;
660
+ },
636
661
  handleReplaceTarget: createTelegramBusFollowerTargetReplacementHandler({
637
662
  ...ports.targetReplacement,
638
663
  ...sharedRuntimeDeps,
@@ -650,8 +675,21 @@ export function createTelegramBusFollowerRuntimeAssembly<
650
675
  ...sharedRuntimeDeps,
651
676
  startReceiving: receiver.start,
652
677
  stopReceiving: receiver.stop,
678
+ onRegistered: prepareContext,
653
679
  onHeartbeatFailure: recovery,
654
680
  });
681
+ const baseRegistration = registration;
682
+ registration = {
683
+ ...baseRegistration,
684
+ async setContext(ctx) {
685
+ const sessionGeneration = ports.registration.getSessionGeneration?.();
686
+ if (ports.registration.isContextActive?.(ctx) === false) return;
687
+ await baseRegistration.setContext(ctx);
688
+ if (ports.registration.getSessionGeneration?.() !== sessionGeneration ||
689
+ ports.registration.isContextActive?.(ctx) === false) return;
690
+ if (!isReadyContext(ctx)) await prepareContext(ctx);
691
+ },
692
+ };
655
693
  return { receiver, registration };
656
694
  }
657
695
 
@@ -1194,7 +1232,13 @@ export function createTelegramBusFollowerSessionRefreshHook<TContext>(
1194
1232
  }
1195
1233
  if (!deps.registrationState.isRegistered()) return;
1196
1234
  if (deps.isSessionActive && !deps.isSessionActive(ctx)) return;
1197
- deps.registrationRuntime.setContext(ctx);
1235
+ try {
1236
+ await deps.registrationRuntime.setContext(ctx);
1237
+ } catch (error) {
1238
+ deps.recordRuntimeEvent("bus", error, { phase: "follower-session-refresh" });
1239
+ return;
1240
+ }
1241
+ if (deps.isSessionActive && !deps.isSessionActive(ctx)) return;
1198
1242
  deps.updateStatus(ctx);
1199
1243
  deps.recordRuntimeEvent(
1200
1244
  "bus",
@@ -1582,6 +1626,10 @@ export function createTelegramBusFollowerRegistrationRuntime<
1582
1626
  const getNowMs = deps.getNowMs ?? Date.now;
1583
1627
  const getPid = deps.getPid ?? (() => process.pid);
1584
1628
  const heartbeatMs = deps.heartbeatMs ?? 1000;
1629
+ const heartbeatTimeoutMs =
1630
+ deps.heartbeatTimeoutMs ??
1631
+ deps.timeoutMs ??
1632
+ TELEGRAM_BUS_FOLLOWER_HEARTBEAT_TIMEOUT_MS;
1585
1633
  const registrationTimeoutMs =
1586
1634
  deps.registrationTimeoutMs ?? deps.timeoutMs ?? 30000;
1587
1635
  const registrationRetryAttempts =
@@ -1596,6 +1644,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
1596
1644
  let activeLeaderSocketPath: string | undefined;
1597
1645
  let activeAuthSecret: string | undefined;
1598
1646
  let activeRegistrationGeneration: string | undefined;
1647
+ let registrationAttempt: { ctx: TContext; sessionGeneration: number | undefined } | undefined;
1599
1648
  let activeContext: TContext | undefined;
1600
1649
  let lastKnownTarget: TelegramTarget | undefined;
1601
1650
  let lastKnownSlot: string | undefined;
@@ -1606,6 +1655,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
1606
1655
  heartbeatInterval = undefined;
1607
1656
  };
1608
1657
  const stop = () => {
1658
+ registrationAttempt = undefined;
1609
1659
  stopHeartbeat();
1610
1660
  activeAuthSecret = undefined;
1611
1661
  activeRegistrationGeneration = undefined;
@@ -1640,7 +1690,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
1640
1690
  try {
1641
1691
  const response = await sendTelegramBusLocalEnvelope({
1642
1692
  socketPath: leaderSocketPath,
1643
- timeoutMs: deps.timeoutMs,
1693
+ timeoutMs: heartbeatTimeoutMs,
1644
1694
  retry: getTelegramBusTransportRetryPolicy({
1645
1695
  endpoint: leaderSocketPath,
1646
1696
  operation: "operation",
@@ -1729,6 +1779,24 @@ export function createTelegramBusFollowerRegistrationRuntime<
1729
1779
  };
1730
1780
  return {
1731
1781
  registerWithLeader: async (ctx, leader, options) => {
1782
+ if (deps.isContextActive?.(ctx) === false) return false;
1783
+ const sessionGeneration = deps.getSessionGeneration?.();
1784
+ const attempt = { ctx, sessionGeneration };
1785
+ registrationAttempt = attempt;
1786
+ const isCurrentRequest = () => registrationAttempt === attempt &&
1787
+ deps.getSessionGeneration?.() === sessionGeneration && deps.isContextActive?.(ctx) !== false;
1788
+ const abandonOwnedRequest = async (): Promise<void> => {
1789
+ if (registrationAttempt !== attempt) return;
1790
+ registrationAttempt = undefined;
1791
+ stopHeartbeat();
1792
+ activeLeaderSocketPath = undefined;
1793
+ activeAuthSecret = undefined;
1794
+ activeRegistrationGeneration = undefined;
1795
+ activeContext = undefined;
1796
+ deps.registrationState?.setRegistered(false);
1797
+ deps.setActiveAuthSecret?.(undefined);
1798
+ await deps.stopReceiving?.();
1799
+ };
1732
1800
  const pendingHandoff = options
1733
1801
  ? undefined
1734
1802
  : getTelegramFollowerSessionHandoff();
@@ -1750,6 +1818,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
1750
1818
  deps.getLeaderSocketPath?.() ??
1751
1819
  getTelegramBusSocketPath();
1752
1820
  await deps.startReceiving?.();
1821
+ if (!isCurrentRequest()) { await abandonOwnedRequest(); return false; }
1753
1822
  activeAuthSecret = deps.getLeaderAuthSecret
1754
1823
  ? deps.getLeaderAuthSecret(leader)
1755
1824
  : leader?.busSecret;
@@ -1781,7 +1850,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
1781
1850
  sessionId: deps.getSessionId?.(ctx),
1782
1851
  pid: getPid(),
1783
1852
  processBirthId: deps.getProcessBirthId?.(),
1784
- sessionGeneration: deps.getSessionGeneration?.(),
1853
+ sessionGeneration,
1785
1854
  target:
1786
1855
  registrationOptions?.target ??
1787
1856
  deps.registrationState?.getTarget() ??
@@ -1815,6 +1884,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
1815
1884
  },
1816
1885
  });
1817
1886
  } catch (error) {
1887
+ if (!isCurrentRequest()) { await abandonOwnedRequest(); return false; }
1818
1888
  stopHeartbeat();
1819
1889
  activeLeaderSocketPath = undefined;
1820
1890
  activeAuthSecret = undefined;
@@ -1823,14 +1893,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
1823
1893
  await deps.stopReceiving?.();
1824
1894
  throw error;
1825
1895
  }
1826
- if (deps.isContextActive && !deps.isContextActive(ctx)) {
1827
- stopHeartbeat();
1828
- activeLeaderSocketPath = undefined;
1829
- activeAuthSecret = undefined;
1830
- deps.setActiveAuthSecret?.(undefined);
1831
- await deps.stopReceiving?.();
1832
- return false;
1833
- }
1896
+ if (!isCurrentRequest()) { await abandonOwnedRequest(); return false; }
1834
1897
  const compatibility = getTelegramBusProtocolCompatibility({
1835
1898
  local: deps.protocolIdentity,
1836
1899
  remote: response?.kind === "bus.ack" ? response.protocol : undefined,
@@ -1880,6 +1943,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
1880
1943
  try {
1881
1944
  await deps.onRegistered?.(ctx);
1882
1945
  } catch (error) {
1946
+ if (!isCurrentRequest()) { await abandonOwnedRequest(); return false; }
1883
1947
  stopHeartbeat();
1884
1948
  activeLeaderSocketPath = undefined;
1885
1949
  activeAuthSecret = undefined;
@@ -1889,7 +1953,9 @@ export function createTelegramBusFollowerRegistrationRuntime<
1889
1953
  await deps.stopReceiving?.();
1890
1954
  throw error;
1891
1955
  }
1956
+ if (!isCurrentRequest()) { await abandonOwnedRequest(); return false; }
1892
1957
  await requestHeartbeat();
1958
+ if (!isCurrentRequest()) { await abandonOwnedRequest(); return false; }
1893
1959
  startHeartbeat(leaderSocketPath);
1894
1960
  if (
1895
1961
  pendingHandoffOptions &&
@@ -1909,6 +1975,8 @@ export function createTelegramBusFollowerRegistrationRuntime<
1909
1975
  return false;
1910
1976
  },
1911
1977
  setContext(ctx) {
1978
+ if (registrationAttempt && (registrationAttempt.ctx !== ctx ||
1979
+ registrationAttempt.sessionGeneration !== deps.getSessionGeneration?.())) registrationAttempt = undefined;
1912
1980
  activeContext = ctx;
1913
1981
  },
1914
1982
  async setThreadDisplayMode(mode) {