@llblab/pi-kit 0.19.2 → 0.20.0

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 (66) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +1 -1
  3. package/node_modules/@llblab/pi-telegram/AGENTS.md +9 -7
  4. package/node_modules/@llblab/pi-telegram/BACKLOG.md +13 -32
  5. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +11 -0
  6. package/node_modules/@llblab/pi-telegram/README.md +4 -3
  7. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +2 -2
  8. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +3 -5
  9. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +1 -1
  10. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +93 -9
  11. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.d.ts +9 -3
  12. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +231 -71
  13. package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +4 -0
  14. package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +34 -7
  15. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +63 -2
  16. package/node_modules/@llblab/pi-telegram/dist/lib/journal.d.ts +3 -0
  17. package/node_modules/@llblab/pi-telegram/dist/lib/journal.js +13 -0
  18. package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.d.ts +2 -0
  19. package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.js +25 -4
  20. package/node_modules/@llblab/pi-telegram/dist/lib/locks.d.ts +1 -0
  21. package/node_modules/@llblab/pi-telegram/dist/lib/locks.js +32 -12
  22. package/node_modules/@llblab/pi-telegram/dist/lib/paths.d.ts +10 -0
  23. package/node_modules/@llblab/pi-telegram/dist/lib/paths.js +22 -1
  24. package/node_modules/@llblab/pi-telegram/dist/lib/polling.js +2 -2
  25. package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +1 -1
  26. package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +1 -2
  27. package/node_modules/@llblab/pi-telegram/dist/lib/sync.d.ts +9 -0
  28. package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +41 -1
  29. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +9 -1
  30. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +23 -3
  31. package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.js +7 -2
  32. package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +2 -0
  33. package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +68 -37
  34. package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +6 -4
  35. package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +134 -61
  36. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.d.ts +9 -0
  37. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.js +49 -3
  38. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +77 -6
  39. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +384 -8
  40. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-slots.d.ts +3 -0
  41. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-slots.js +6 -0
  42. package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
  43. package/node_modules/@llblab/pi-telegram/docs/architecture.md +21 -14
  44. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +31 -10
  45. package/node_modules/@llblab/pi-telegram/docs/updates.md +2 -0
  46. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +5 -7
  47. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +69 -12
  48. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +236 -94
  49. package/node_modules/@llblab/pi-telegram/lib/bus.ts +39 -9
  50. package/node_modules/@llblab/pi-telegram/lib/extension.ts +69 -2
  51. package/node_modules/@llblab/pi-telegram/lib/journal.ts +14 -0
  52. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +23 -4
  53. package/node_modules/@llblab/pi-telegram/lib/locks.ts +32 -13
  54. package/node_modules/@llblab/pi-telegram/lib/paths.ts +29 -1
  55. package/node_modules/@llblab/pi-telegram/lib/polling.ts +2 -2
  56. package/node_modules/@llblab/pi-telegram/lib/queue.ts +2 -3
  57. package/node_modules/@llblab/pi-telegram/lib/sync.ts +45 -1
  58. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +30 -2
  59. package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +11 -3
  60. package/node_modules/@llblab/pi-telegram/lib/threads.ts +73 -36
  61. package/node_modules/@llblab/pi-telegram/lib/updates.ts +145 -78
  62. package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +67 -4
  63. package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +465 -7
  64. package/node_modules/@llblab/pi-telegram/lib/workspace-slots.ts +7 -0
  65. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  66. package/package.json +2 -2
@@ -139,6 +139,23 @@ export function createTelegramBusFollowerPromotionHandler(input) {
139
139
  });
140
140
  }
141
141
  export function createTelegramBusFollowerRuntimeAssembly(ports) {
142
+ // Binding startup needs registration identity before it can grant inbound authority.
143
+ let readyContext;
144
+ const isReadyContext = (ctx) => Boolean(readyContext &&
145
+ (ctx === undefined || readyContext.ctx === ctx) &&
146
+ ports.registrationState.getGeneration() === readyContext.generation &&
147
+ ports.registration.getSessionGeneration?.() === readyContext.sessionGeneration &&
148
+ ports.registration.isContextActive?.(readyContext.ctx) !== false);
149
+ const prepareContext = async (ctx) => {
150
+ const generation = ports.registrationState.getGeneration();
151
+ const sessionGeneration = ports.registration.getSessionGeneration?.();
152
+ readyContext = undefined;
153
+ await ports.registration.onRegistered?.(ctx);
154
+ if (generation && ports.registrationState.getGeneration() === generation &&
155
+ ports.registration.getSessionGeneration?.() === sessionGeneration &&
156
+ ports.registration.isContextActive?.(ctx) !== false)
157
+ readyContext = { generation, ctx, sessionGeneration };
158
+ };
142
159
  const sharedRuntimeDeps = {
143
160
  instanceId: ports.instanceId,
144
161
  registrationState: ports.registrationState,
@@ -148,7 +165,9 @@ export function createTelegramBusFollowerRuntimeAssembly(ports) {
148
165
  ...ports.receiver,
149
166
  instanceId: ports.instanceId,
150
167
  recordRuntimeEvent: ports.recordRuntimeEvent,
151
- getRegistrationGeneration: ports.registrationState.getGeneration,
168
+ getRegistrationGeneration() {
169
+ return isReadyContext() ? readyContext?.generation : undefined;
170
+ },
152
171
  handleReplaceTarget: createTelegramBusFollowerTargetReplacementHandler({
153
172
  ...ports.targetReplacement,
154
173
  ...sharedRuntimeDeps,
@@ -166,8 +185,24 @@ export function createTelegramBusFollowerRuntimeAssembly(ports) {
166
185
  ...sharedRuntimeDeps,
167
186
  startReceiving: receiver.start,
168
187
  stopReceiving: receiver.stop,
188
+ onRegistered: prepareContext,
169
189
  onHeartbeatFailure: recovery,
170
190
  });
191
+ const baseRegistration = registration;
192
+ registration = {
193
+ ...baseRegistration,
194
+ async setContext(ctx) {
195
+ const sessionGeneration = ports.registration.getSessionGeneration?.();
196
+ if (ports.registration.isContextActive?.(ctx) === false)
197
+ return;
198
+ await baseRegistration.setContext(ctx);
199
+ if (ports.registration.getSessionGeneration?.() !== sessionGeneration ||
200
+ ports.registration.isContextActive?.(ctx) === false)
201
+ return;
202
+ if (!isReadyContext(ctx))
203
+ await prepareContext(ctx);
204
+ },
205
+ };
171
206
  return { receiver, registration };
172
207
  }
173
208
  export function createTelegramBusFollowerTargetReplacementHandler(deps) {
@@ -575,7 +610,15 @@ export function createTelegramBusFollowerSessionRefreshHook(deps) {
575
610
  return;
576
611
  if (deps.isSessionActive && !deps.isSessionActive(ctx))
577
612
  return;
578
- deps.registrationRuntime.setContext(ctx);
613
+ try {
614
+ await deps.registrationRuntime.setContext(ctx);
615
+ }
616
+ catch (error) {
617
+ deps.recordRuntimeEvent("bus", error, { phase: "follower-session-refresh" });
618
+ return;
619
+ }
620
+ if (deps.isSessionActive && !deps.isSessionActive(ctx))
621
+ return;
579
622
  deps.updateStatus(ctx);
580
623
  deps.recordRuntimeEvent("bus", "Telegram follower session context refreshed", { phase: "follower-session-refresh" });
581
624
  };
@@ -888,6 +931,7 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
888
931
  let activeLeaderSocketPath;
889
932
  let activeAuthSecret;
890
933
  let activeRegistrationGeneration;
934
+ let registrationAttempt;
891
935
  let activeContext;
892
936
  let lastKnownTarget;
893
937
  let lastKnownSlot;
@@ -899,6 +943,7 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
899
943
  heartbeatInterval = undefined;
900
944
  };
901
945
  const stop = () => {
946
+ registrationAttempt = undefined;
902
947
  stopHeartbeat();
903
948
  activeAuthSecret = undefined;
904
949
  activeRegistrationGeneration = undefined;
@@ -1027,6 +1072,26 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
1027
1072
  };
1028
1073
  return {
1029
1074
  registerWithLeader: async (ctx, leader, options) => {
1075
+ if (deps.isContextActive?.(ctx) === false)
1076
+ return false;
1077
+ const sessionGeneration = deps.getSessionGeneration?.();
1078
+ const attempt = { ctx, sessionGeneration };
1079
+ registrationAttempt = attempt;
1080
+ const isCurrentRequest = () => registrationAttempt === attempt &&
1081
+ deps.getSessionGeneration?.() === sessionGeneration && deps.isContextActive?.(ctx) !== false;
1082
+ const abandonOwnedRequest = async () => {
1083
+ if (registrationAttempt !== attempt)
1084
+ return;
1085
+ registrationAttempt = undefined;
1086
+ stopHeartbeat();
1087
+ activeLeaderSocketPath = undefined;
1088
+ activeAuthSecret = undefined;
1089
+ activeRegistrationGeneration = undefined;
1090
+ activeContext = undefined;
1091
+ deps.registrationState?.setRegistered(false);
1092
+ deps.setActiveAuthSecret?.(undefined);
1093
+ await deps.stopReceiving?.();
1094
+ };
1030
1095
  const pendingHandoff = options
1031
1096
  ? undefined
1032
1097
  : getTelegramFollowerSessionHandoff();
@@ -1041,6 +1106,10 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
1041
1106
  deps.getLeaderSocketPath?.() ??
1042
1107
  getTelegramBusSocketPath();
1043
1108
  await deps.startReceiving?.();
1109
+ if (!isCurrentRequest()) {
1110
+ await abandonOwnedRequest();
1111
+ return false;
1112
+ }
1044
1113
  activeAuthSecret = deps.getLeaderAuthSecret
1045
1114
  ? deps.getLeaderAuthSecret(leader)
1046
1115
  : leader?.busSecret;
@@ -1070,7 +1139,7 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
1070
1139
  sessionId: deps.getSessionId?.(ctx),
1071
1140
  pid: getPid(),
1072
1141
  processBirthId: deps.getProcessBirthId?.(),
1073
- sessionGeneration: deps.getSessionGeneration?.(),
1142
+ sessionGeneration,
1074
1143
  target: registrationOptions?.target ??
1075
1144
  deps.registrationState?.getTarget() ??
1076
1145
  lastKnownTarget,
@@ -1103,6 +1172,10 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
1103
1172
  });
1104
1173
  }
1105
1174
  catch (error) {
1175
+ if (!isCurrentRequest()) {
1176
+ await abandonOwnedRequest();
1177
+ return false;
1178
+ }
1106
1179
  stopHeartbeat();
1107
1180
  activeLeaderSocketPath = undefined;
1108
1181
  activeAuthSecret = undefined;
@@ -1111,12 +1184,8 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
1111
1184
  await deps.stopReceiving?.();
1112
1185
  throw error;
1113
1186
  }
1114
- if (deps.isContextActive && !deps.isContextActive(ctx)) {
1115
- stopHeartbeat();
1116
- activeLeaderSocketPath = undefined;
1117
- activeAuthSecret = undefined;
1118
- deps.setActiveAuthSecret?.(undefined);
1119
- await deps.stopReceiving?.();
1187
+ if (!isCurrentRequest()) {
1188
+ await abandonOwnedRequest();
1120
1189
  return false;
1121
1190
  }
1122
1191
  const compatibility = getTelegramBusProtocolCompatibility({
@@ -1164,6 +1233,10 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
1164
1233
  await deps.onRegistered?.(ctx);
1165
1234
  }
1166
1235
  catch (error) {
1236
+ if (!isCurrentRequest()) {
1237
+ await abandonOwnedRequest();
1238
+ return false;
1239
+ }
1167
1240
  stopHeartbeat();
1168
1241
  activeLeaderSocketPath = undefined;
1169
1242
  activeAuthSecret = undefined;
@@ -1173,7 +1246,15 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
1173
1246
  await deps.stopReceiving?.();
1174
1247
  throw error;
1175
1248
  }
1249
+ if (!isCurrentRequest()) {
1250
+ await abandonOwnedRequest();
1251
+ return false;
1252
+ }
1176
1253
  await requestHeartbeat();
1254
+ if (!isCurrentRequest()) {
1255
+ await abandonOwnedRequest();
1256
+ return false;
1257
+ }
1177
1258
  startHeartbeat(leaderSocketPath);
1178
1259
  if (pendingHandoffOptions &&
1179
1260
  getTelegramFollowerSessionHandoff()?.instanceId ===
@@ -1191,6 +1272,9 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
1191
1272
  return false;
1192
1273
  },
1193
1274
  setContext(ctx) {
1275
+ if (registrationAttempt && (registrationAttempt.ctx !== ctx ||
1276
+ registrationAttempt.sessionGeneration !== deps.getSessionGeneration?.()))
1277
+ registrationAttempt = undefined;
1194
1278
  activeContext = ctx;
1195
1279
  },
1196
1280
  async setThreadDisplayMode(mode) {
@@ -12,7 +12,7 @@ import type { TelegramTarget } from "./target.ts";
12
12
  import * as Threads from "./threads.ts";
13
13
  import { type TelegramBusEnvelope, type TelegramBusFollowerRegistry, type TelegramBusFollowerView, type TelegramBusInstanceRegistration, type TelegramBusProtocolIdentity, type TelegramBusSocketPathSource } from "./bus.ts";
14
14
  import type { TelegramQueueHandoffPayload } from "./queue.ts";
15
- import { type TelegramWorkspaceOperationRunner } from "./workspace-retirement.ts";
15
+ import { type TelegramWorkspaceCapacityRunner, type TelegramWorkspaceSlotRotationPorts, type TelegramWorkspaceOperationRunner } from "./workspace-retirement.ts";
16
16
  import { type TelegramWorkspaceAdmissionLedger } from "./workspace-admission.ts";
17
17
  export declare const TELEGRAM_BUS_FOLLOWER_STALE_AFTER_MS = 15000;
18
18
  export type TelegramBusWorkspaceAdmissionRunner = TelegramWorkspaceOperationRunner;
@@ -90,7 +90,7 @@ export interface TelegramBusFollowerTargetProvisionerDeps {
90
90
  recordRuntimeEvent: (category: string, error: unknown, details?: Record<string, unknown>) => void;
91
91
  }
92
92
  export interface TelegramBusFollowerDisconnectHandlerDeps {
93
- topicTargetStore: Pick<Threads.TelegramTopicTargetStore, "list" | "markStaleByTarget" | "markWorkspaceBindingInactiveByTarget" | "persist" | "upsertPendingCleanup" | "removePendingCleanup">;
93
+ topicTargetStore: Pick<Threads.TelegramTopicTargetStore, "list" | "markStaleByTarget" | "persist" | "upsertPendingCleanup" | "removePendingCleanup">;
94
94
  callApi: <TResponse>(method: string, body: Record<string, unknown>) => Promise<TResponse>;
95
95
  getCurrentLeaderEpoch?: () => number | string | undefined;
96
96
  getSyncState: () => Sync.TelegramSyncState;
@@ -108,7 +108,7 @@ export interface TelegramBusLeaderRuntimeAssemblyDeps<TContext> {
108
108
  getThreadDisplayMode?: () => TelegramThreadDisplayMode;
109
109
  persistThreadDisplayMode?: (mode: TelegramThreadDisplayMode, isCurrent: () => boolean) => Promise<void>;
110
110
  onThreadDisplayChanged?: () => void;
111
- runtime: Omit<TelegramBusLeaderRuntimeDeps<TContext>, "callApi" | "onFollowerDisconnected" | "onFollowerConfirmedDead" | "provisionFollowerTarget" | "provisionLeaderTarget" | "recordRuntimeEvent">;
111
+ runtime: Omit<TelegramBusLeaderRuntimeDeps<TContext>, "callApi" | "onFollowerDisconnected" | "onFollowerConfirmedDead" | "onFollowerConfirmedDeadPreserved" | "getTelegramProfile" | "provisionFollowerTarget" | "provisionLeaderTarget" | "recordRuntimeEvent">;
112
112
  getAllowedUserId: () => number | undefined;
113
113
  instanceId: string;
114
114
  getCwd?: (ctx: TContext) => string | undefined;
@@ -133,6 +133,7 @@ export interface TelegramBusLeaderRuntimeAssemblyDeps<TContext> {
133
133
  captureWorkspaceExternalProtection?: (binding: Threads.TelegramWorkspaceThreadBinding) => Threads.TelegramWorkspaceExternalProtectionEvidence;
134
134
  getWorkspaceAdmission?: () => Pick<TelegramWorkspaceAdmissionLedger, "acquireAdmission" | "releaseAdmission"> | undefined;
135
135
  runWorkspaceOperation?: TelegramBusWorkspaceAdmissionRunner;
136
+ workspaceRotation?: TelegramWorkspaceSlotRotationPorts;
136
137
  }
137
138
  export declare function createTelegramBusLeaderRuntimeAssembly<TContext>(deps: TelegramBusLeaderRuntimeAssemblyDeps<TContext>): TelegramBusLeaderRuntime<TContext> & {
138
139
  renameLeaderThreadAdmitted: (threadName: string, expectedTarget?: Threads.TelegramTopicTargetRecord["target"]) => Promise<Threads.TelegramTopicTargetRecord>;
@@ -192,8 +193,10 @@ export interface TelegramBusLeaderRuntimeDeps<TContext> {
192
193
  applyThreadDisplayMode?: (mode: TelegramThreadDisplayMode, isCurrent: () => boolean) => Promise<void>;
193
194
  getThreadDisplayMode?: () => TelegramThreadDisplayMode;
194
195
  getCurrentLeaderEpoch?: () => number | string | undefined;
196
+ getTelegramProfile?: () => string | undefined;
195
197
  provisionLeaderTarget?: (ctx: TContext) => Promise<void> | void;
196
198
  runWorkspaceAdmission?: TelegramBusWorkspaceAdmissionRunner;
199
+ runWithWorkspaceCapacity?: TelegramWorkspaceCapacityRunner;
197
200
  getNowMs?: () => number;
198
201
  timeoutMs?: number;
199
202
  followerPruneIntervalMs?: number;
@@ -202,6 +205,8 @@ export interface TelegramBusLeaderRuntimeDeps<TContext> {
202
205
  shouldCleanupConfirmedDeadFollower?: () => Promise<boolean> | boolean;
203
206
  onFollowerDisconnected?: (follower: TelegramBusFollowerView) => Promise<void> | void;
204
207
  onFollowerConfirmedDead?: (follower: TelegramBusFollowerView) => Promise<void> | void;
208
+ /** True settles this observation; false needs fresh proof before another attempt. */
209
+ onFollowerConfirmedDeadPreserved?: (follower: TelegramBusFollowerView, isDetached: () => boolean, operationId: string) => Promise<boolean> | boolean;
205
210
  recordRuntimeEvent?: (category: string, error: unknown, details?: Record<string, unknown>) => void;
206
211
  }
207
212
  export declare function createTelegramBusInstanceLifecycleAnnouncement(input: {
@@ -275,6 +280,7 @@ export declare function createTelegramBusLeaderEnvelopeHandler(deps: {
275
280
  getCurrentLeaderEpoch?: () => number | string | undefined;
276
281
  runFollowerMutation?: TelegramBusFollowerMutationRunner;
277
282
  runWorkspaceAdmission?: TelegramBusWorkspaceAdmissionRunner;
283
+ runWithWorkspaceCapacity?: TelegramWorkspaceCapacityRunner;
278
284
  }): (envelope: TelegramBusEnvelope) => Promise<TelegramBusEnvelope> | TelegramBusEnvelope;
279
285
  export interface TelegramBusLeaderActivationSchedulerDeps<TContext> {
280
286
  isBusEnabled: () => boolean;