@llblab/pi-telegram 0.21.1 → 0.22.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.
package/lib/lifecycle.ts CHANGED
@@ -4,6 +4,8 @@
4
4
  * Binds prepared Telegram lifecycle runtimes to pi extension lifecycle events
5
5
  */
6
6
 
7
+ import * as BusFollower from "./bus-follower.ts";
8
+ import * as Queue from "./queue.ts";
7
9
  import type {
8
10
  AgentEndEvent,
9
11
  AgentSettledEvent,
@@ -50,6 +52,7 @@ type TelegramLifecycleModel = ExtensionContext["model"];
50
52
  type TelegramLifecycleMessage = AgentEndEvent["messages"][number];
51
53
 
52
54
  export interface TelegramLifecycleRegistrationDeps {
55
+ isSessionActive?: (ctx: ExtensionContext) => boolean;
53
56
  onInput?: (event: InputEvent, ctx: ExtensionContext) => Promise<void> | void;
54
57
  onSessionStart: (
55
58
  event: SessionStartEvent,
@@ -122,21 +125,73 @@ export interface TelegramSessionLifecycleHooks {
122
125
 
123
126
  export interface TelegramSessionContextStore<TContext> {
124
127
  get: () => TContext | undefined;
125
- set: (ctx: TContext) => void;
126
- clear: () => void;
128
+ getGeneration: () => number;
129
+ isCurrent: (ctx: TContext, generation?: number) => boolean;
130
+ set: (ctx: TContext) => number;
131
+ clear: (ctx?: TContext) => boolean;
127
132
  }
128
133
 
129
- export function createTelegramSessionContextStore<
130
- TContext,
131
- >(): TelegramSessionContextStore<TContext> {
134
+ export function createTelegramSessionContextStore<TContext>(
135
+ options: {
136
+ getIdentity?: (ctx: TContext) => unknown;
137
+ } = {},
138
+ ): TelegramSessionContextStore<TContext> {
132
139
  let currentContext: TContext | undefined;
140
+ let currentIdentity: unknown;
141
+ let generation = 0;
142
+ const objectIdentityGenerations = new WeakMap<object, number>();
143
+ const primitiveIdentityGenerations = new Map<unknown, number>();
144
+ const resolveIdentity = (ctx: TContext): unknown =>
145
+ options.getIdentity?.(ctx) ?? ctx;
146
+ const getIdentityGeneration = (identity: unknown): number | undefined =>
147
+ (typeof identity === "object" && identity !== null) ||
148
+ typeof identity === "function"
149
+ ? objectIdentityGenerations.get(identity as object)
150
+ : primitiveIdentityGenerations.get(identity);
151
+ const setIdentityGeneration = (identity: unknown, value: number): void => {
152
+ if (
153
+ (typeof identity === "object" && identity !== null) ||
154
+ typeof identity === "function"
155
+ ) {
156
+ objectIdentityGenerations.set(identity as object, value);
157
+ } else {
158
+ primitiveIdentityGenerations.set(identity, value);
159
+ }
160
+ };
133
161
  return {
134
162
  get: () => currentContext,
163
+ getGeneration: () => generation,
164
+ isCurrent: (ctx, expectedGeneration) => {
165
+ if (currentContext === undefined) return false;
166
+ const identity = resolveIdentity(ctx);
167
+ return (
168
+ identity === currentIdentity &&
169
+ getIdentityGeneration(identity) === generation &&
170
+ (expectedGeneration === undefined || generation === expectedGeneration)
171
+ );
172
+ },
135
173
  set: (ctx) => {
136
174
  currentContext = ctx;
175
+ currentIdentity = resolveIdentity(ctx);
176
+ generation += 1;
177
+ setIdentityGeneration(currentIdentity, generation);
178
+ return generation;
137
179
  },
138
- clear: () => {
180
+ clear: (ctx) => {
181
+ if (ctx !== undefined) {
182
+ const identity = resolveIdentity(ctx);
183
+ if (
184
+ identity !== currentIdentity ||
185
+ getIdentityGeneration(identity) !== generation
186
+ ) {
187
+ return false;
188
+ }
189
+ }
190
+ if (currentContext === undefined) return false;
139
191
  currentContext = undefined;
192
+ currentIdentity = undefined;
193
+ generation += 1;
194
+ return true;
140
195
  },
141
196
  };
142
197
  }
@@ -148,12 +203,127 @@ export function createTelegramSessionContextTracker(
148
203
  onSessionStart: async (_event, ctx) => {
149
204
  store.set(ctx);
150
205
  },
151
- onSessionShutdown: async () => {
152
- store.clear();
206
+ onSessionShutdown: async (_event, ctx) => {
207
+ store.clear(ctx);
208
+ },
209
+ };
210
+ }
211
+
212
+ export function createTelegramSessionGenerationFence(
213
+ store: TelegramSessionContextStore<ExtensionContext>,
214
+ hooks: TelegramSessionLifecycleHooks,
215
+ ): TelegramSessionLifecycleHooks {
216
+ return {
217
+ async onSessionStart(event, ctx) {
218
+ const generation = store.set(ctx);
219
+ await hooks.onSessionStart(event, ctx);
220
+ if (!store.isCurrent(ctx, generation)) return;
221
+ },
222
+ async onSessionShutdown(event, ctx) {
223
+ const generation = store.getGeneration();
224
+ if (!store.isCurrent(ctx, generation)) return;
225
+ await hooks.onSessionShutdown(event, ctx);
226
+ store.clear(ctx);
153
227
  },
154
228
  };
155
229
  }
156
230
 
231
+ export interface TelegramBridgeSessionServiceRuntime {
232
+ resumeGroupedInput(ctx: ExtensionContext): void;
233
+ suspendGroupedInput(): void;
234
+ delivery: {
235
+ onSessionStart(): Promise<void>;
236
+ onSessionShutdown(): Promise<void>;
237
+ };
238
+ polling: {
239
+ onSessionStart(
240
+ event: SessionStartEvent,
241
+ ctx: ExtensionContext,
242
+ ): Promise<void>;
243
+ };
244
+ capabilityMonitor: { start(ctx: ExtensionContext): void; stop(): void };
245
+ queueWatchdog: { start(ctx: ExtensionContext): void; stop(): void };
246
+ }
247
+
248
+ export interface TelegramBridgeSessionLifecycleAssemblyDeps<
249
+ TQueueItem,
250
+ TModel = unknown,
251
+ > {
252
+ contextStore: TelegramSessionContextStore<ExtensionContext>;
253
+ queue: Omit<
254
+ Queue.TelegramSessionLifecycleRuntimeDeps<
255
+ ExtensionContext,
256
+ TQueueItem,
257
+ TModel
258
+ >,
259
+ "isSessionActive" | "stopPolling" | "clearPendingMediaGroups"
260
+ >;
261
+ follower: Omit<
262
+ BusFollower.TelegramBusFollowerSessionRefreshHookDeps<ExtensionContext>,
263
+ "isSessionActive"
264
+ > &
265
+ BusFollower.TelegramBusFollowerSessionReplacementSuspenderDeps;
266
+ services: TelegramBridgeSessionServiceRuntime;
267
+ }
268
+
269
+ export function createTelegramBridgeSessionLifecycleAssembly<
270
+ TQueueItem,
271
+ TModel = unknown,
272
+ >(
273
+ deps: TelegramBridgeSessionLifecycleAssemblyDeps<TQueueItem, TModel>,
274
+ ): TelegramSessionLifecycleHooks {
275
+ const isSessionActive = deps.contextStore.isCurrent;
276
+ const suspendForReplacement =
277
+ BusFollower.createTelegramBusFollowerSessionReplacementSuspender({
278
+ registrationState: deps.follower.registrationState,
279
+ instanceId: deps.follower.instanceId,
280
+ suspendPolling: deps.follower.suspendPolling,
281
+ recordRuntimeEvent: deps.follower.recordRuntimeEvent,
282
+ });
283
+ const queueLifecycle = Queue.createTelegramSessionLifecycleRuntime({
284
+ ...deps.queue,
285
+ isSessionActive,
286
+ stopPolling: suspendForReplacement,
287
+ clearPendingMediaGroups: deps.services.suspendGroupedInput,
288
+ });
289
+ const servicesLifecycle = appendTelegramLifecycleHooks(
290
+ queueLifecycle,
291
+ {
292
+ async onSessionStart(event, ctx) {
293
+ deps.services.resumeGroupedInput(ctx);
294
+ await deps.services.delivery.onSessionStart();
295
+ await deps.services.polling.onSessionStart(event, ctx);
296
+ deps.services.capabilityMonitor.start(ctx);
297
+ deps.services.queueWatchdog.start(ctx);
298
+ },
299
+ async onSessionShutdown() {
300
+ await deps.services.delivery.onSessionShutdown();
301
+ deps.services.queueWatchdog.stop();
302
+ deps.services.capabilityMonitor.stop();
303
+ },
304
+ },
305
+ isSessionActive,
306
+ );
307
+ const followerLifecycle = appendTelegramLifecycleHooks(
308
+ servicesLifecycle,
309
+ {
310
+ onSessionStart: BusFollower.createTelegramBusFollowerSessionRefreshHook({
311
+ registrationState: deps.follower.registrationState,
312
+ registrationRuntime: deps.follower.registrationRuntime,
313
+ getLeaderState: deps.follower.getLeaderState,
314
+ isSessionActive,
315
+ updateStatus: deps.follower.updateStatus,
316
+ recordRuntimeEvent: deps.follower.recordRuntimeEvent,
317
+ }),
318
+ },
319
+ isSessionActive,
320
+ );
321
+ return createTelegramSessionGenerationFence(
322
+ deps.contextStore,
323
+ followerLifecycle,
324
+ );
325
+ }
326
+
157
327
  type TelegramLifecycleTimer = number | ReturnType<typeof setTimeout>;
158
328
 
159
329
  function unrefTelegramLifecycleTimer(timer: TelegramLifecycleTimer): void {
@@ -162,6 +332,7 @@ function unrefTelegramLifecycleTimer(timer: TelegramLifecycleTimer): void {
162
332
  }
163
333
 
164
334
  export interface TelegramCompactionObserverRuntimeDeps<TContext> {
335
+ isContextActive?: (ctx: TContext) => boolean;
165
336
  setCompactionInProgress: (inProgress: boolean) => void;
166
337
  updateStatus: (ctx: TContext) => void;
167
338
  startTypingLoop?: (ctx: TContext) => boolean | void;
@@ -206,6 +377,7 @@ export function createTelegramCompactionObserverRuntime<TContext>(
206
377
  };
207
378
  return {
208
379
  onSessionBeforeCompact: (_event, ctx) => {
380
+ if (deps.isContextActive && !deps.isContextActive(ctx)) return;
209
381
  deps.setCompactionInProgress(true);
210
382
  const typingStartResult = deps.startTypingLoop?.(ctx);
211
383
  typingStartedByObserver =
@@ -214,6 +386,7 @@ export function createTelegramCompactionObserverRuntime<TContext>(
214
386
  clearFallbackTimer();
215
387
  fallbackTimer = setTimer(() => {
216
388
  fallbackTimer = undefined;
389
+ if (deps.isContextActive && !deps.isContextActive(ctx)) return;
217
390
  deps.setCompactionInProgress(false);
218
391
  if (typingStartedByObserver) deps.stopTypingLoop?.();
219
392
  typingStartedByObserver = false;
@@ -229,6 +402,7 @@ export function createTelegramCompactionObserverRuntime<TContext>(
229
402
  },
230
403
  onSessionCompact: (_event, ctx) => {
231
404
  clearFallbackTimer();
405
+ if (deps.isContextActive && !deps.isContextActive(ctx)) return;
232
406
  deps.setCompactionInProgress(false);
233
407
  if (typingStartedByObserver) deps.stopTypingLoop?.();
234
408
  typingStartedByObserver = false;
@@ -314,14 +488,17 @@ export interface TelegramExtraLifecycleHooks {
314
488
  export function appendTelegramLifecycleHooks(
315
489
  base: TelegramSessionLifecycleHooks,
316
490
  extra: TelegramExtraLifecycleHooks,
491
+ isSessionActive?: (ctx: ExtensionContext) => boolean,
317
492
  ): TelegramSessionLifecycleHooks {
318
493
  return {
319
494
  onSessionStart: async (event, ctx) => {
320
495
  await base.onSessionStart(event, ctx);
496
+ if (isSessionActive?.(ctx) === false) return;
321
497
  await extra.onSessionStart?.(event, ctx);
322
498
  },
323
499
  onSessionShutdown: async (event, ctx) => {
324
500
  await base.onSessionShutdown(event, ctx);
501
+ if (isSessionActive?.(ctx) === false) return;
325
502
  await extra.onSessionShutdown?.(event, ctx);
326
503
  },
327
504
  };
@@ -331,7 +508,10 @@ export function registerTelegramLifecycleHooks(
331
508
  pi: ExtensionAPI,
332
509
  deps: TelegramLifecycleRegistrationDeps,
333
510
  ): void {
511
+ const isActive = (ctx: ExtensionContext): boolean =>
512
+ deps.isSessionActive?.(ctx) !== false;
334
513
  pi.on("input", async (event, ctx) => {
514
+ if (!isActive(ctx)) return;
335
515
  await deps.onInput?.(event, ctx);
336
516
  });
337
517
  pi.on("session_start", async (event, ctx) => {
@@ -341,39 +521,50 @@ export function registerTelegramLifecycleHooks(
341
521
  await deps.onSessionShutdown(event, ctx);
342
522
  });
343
523
  pi.on("session_before_compact", async (event, ctx) => {
524
+ if (!isActive(ctx)) return;
344
525
  await deps.onSessionBeforeCompact?.(event, ctx);
345
526
  });
346
527
  pi.on("session_compact", async (event, ctx) => {
528
+ if (!isActive(ctx)) return;
347
529
  await deps.onSessionCompact?.(event, ctx);
348
530
  });
349
531
  pi.on("before_agent_start", async (event, ctx) => {
350
532
  return deps.onBeforeAgentStart(event, ctx);
351
533
  });
352
534
  pi.on("model_select", async (event, ctx) => {
535
+ if (!isActive(ctx)) return;
353
536
  await deps.onModelSelect(event, ctx);
354
537
  });
355
538
  pi.on("agent_start", async (event, ctx) => {
539
+ if (!isActive(ctx)) return;
356
540
  await deps.onAgentStart(event, ctx);
357
541
  });
358
542
  pi.on("tool_execution_start", async (event, ctx) => {
543
+ if (!isActive(ctx)) return;
359
544
  await deps.onToolExecutionStart(event, ctx);
360
545
  });
361
546
  pi.on("tool_execution_update", async (event, ctx) => {
547
+ if (!isActive(ctx)) return;
362
548
  await deps.onToolExecutionUpdate?.(event, ctx);
363
549
  });
364
550
  pi.on("tool_execution_end", async (event, ctx) => {
551
+ if (!isActive(ctx)) return;
365
552
  await deps.onToolExecutionEnd(event, ctx);
366
553
  });
367
554
  pi.on("message_start", async (event, ctx) => {
555
+ if (!isActive(ctx)) return;
368
556
  await deps.onMessageStart(event, ctx);
369
557
  });
370
558
  pi.on("message_update", async (event, ctx) => {
559
+ if (!isActive(ctx)) return;
371
560
  await deps.onMessageUpdate(event, ctx);
372
561
  });
373
562
  pi.on("agent_end", async (event, ctx) => {
563
+ if (!isActive(ctx)) return;
374
564
  await deps.onAgentEnd(event, ctx);
375
565
  });
376
566
  pi.on("agent_settled", async (event, ctx) => {
567
+ if (!isActive(ctx)) return;
377
568
  await deps.onAgentSettled?.(event, ctx);
378
569
  });
379
570
  }