@llblab/pi-kit 0.5.1 → 0.6.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 (41) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +3 -3
  3. package/node_modules/@llblab/pi-grow-loop/AGENTS.md +2 -2
  4. package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +4 -0
  5. package/node_modules/@llblab/pi-grow-loop/README.md +6 -6
  6. package/node_modules/@llblab/pi-grow-loop/index.ts +6 -3
  7. package/node_modules/@llblab/pi-grow-loop/package.json +1 -1
  8. package/node_modules/@llblab/pi-telegram/AGENTS.md +1 -1
  9. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +20 -0
  10. package/node_modules/@llblab/pi-telegram/README.md +1 -1
  11. package/node_modules/@llblab/pi-telegram/docs/architecture.md +2 -2
  12. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +4 -0
  13. package/node_modules/@llblab/pi-telegram/docs/outbound.md +27 -5
  14. package/node_modules/@llblab/pi-telegram/docs/public-api.md +1 -0
  15. package/node_modules/@llblab/pi-telegram/index.ts +24 -19
  16. package/node_modules/@llblab/pi-telegram/lib/activity-verbosity.ts +43 -25
  17. package/node_modules/@llblab/pi-telegram/lib/activity.ts +79 -6
  18. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +110 -91
  19. package/node_modules/@llblab/pi-telegram/lib/config.ts +1 -1
  20. package/node_modules/@llblab/pi-telegram/lib/delivery.ts +18 -18
  21. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +14 -3
  22. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +2 -2
  23. package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +41 -37
  24. package/node_modules/@llblab/pi-telegram/lib/outbound-voice.ts +39 -42
  25. package/node_modules/@llblab/pi-telegram/lib/outbound.ts +28 -17
  26. package/node_modules/@llblab/pi-telegram/lib/preview.ts +134 -73
  27. package/node_modules/@llblab/pi-telegram/lib/queue.ts +113 -72
  28. package/node_modules/@llblab/pi-telegram/lib/replies.ts +46 -38
  29. package/node_modules/@llblab/pi-telegram/lib/routing.ts +192 -58
  30. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +36 -3
  31. package/node_modules/@llblab/pi-telegram/lib/updates.ts +27 -35
  32. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  33. package/node_modules/@llblab/skills/abcd-context/AGENTS.md +1 -0
  34. package/node_modules/@llblab/skills/abcd-context/CHANGELOG.md +6 -2
  35. package/node_modules/@llblab/skills/abcd-context/SKILL.md +1 -1
  36. package/node_modules/@llblab/skills/abcd-context/docs/validation-design.md +11 -5
  37. package/node_modules/@llblab/skills/abcd-context/scripts/_self-test.mjs +61 -0
  38. package/node_modules/@llblab/skills/abcd-context/scripts/validate-context.mjs +67 -0
  39. package/node_modules/@llblab/skills/package.json +1 -1
  40. package/node_modules/@llblab/skills/release-flow/SKILL.md +2 -4
  41. package/package.json +4 -4
@@ -5,7 +5,8 @@
5
5
  */
6
6
 
7
7
  import { normalizeTelegramNativeMarkdown } from "./replies.ts";
8
- import { stripTelegramCommentMarkupForPreview } from "./outbound.ts";
8
+ import type { TelegramAssistantOutputPreparation } from "./activity.ts";
9
+ import { stripTelegramCommentMarkupForPreview } from "./outbound-markup.ts";
9
10
  import {
10
11
  getTelegramTargetThreadParams,
11
12
  type TelegramTarget,
@@ -14,6 +15,7 @@ import { shouldSuppressPreviewForVoice } from "./voice.ts";
14
15
 
15
16
  const TELEGRAM_DRAFT_ID_MAX = 2_147_483_647;
16
17
  const TELEGRAM_DRAFT_PREVIEW_MAX_CHARS = 4096;
18
+ const TELEGRAM_DRAFT_INTERVAL_MS = 2_000;
17
19
 
18
20
  export type TelegramDraftSupport = "unknown" | "supported";
19
21
 
@@ -27,6 +29,11 @@ export interface TelegramPreviewState {
27
29
  export interface TelegramPreviewRuntimeState extends TelegramPreviewState {
28
30
  flushPromise?: Promise<void>;
29
31
  flushRequested?: boolean;
32
+ precedingFlush?: Promise<void>;
33
+ publicationPromise?: Promise<void>;
34
+ sealed?: boolean;
35
+ nextDraftAt?: number;
36
+ flushTimer?: ReturnType<typeof setTimeout>;
30
37
  }
31
38
 
32
39
  export type TelegramPreviewReplyMarkup = unknown;
@@ -35,6 +42,7 @@ export interface TelegramPreviewRuntimeDeps {
35
42
  getState: () => TelegramPreviewRuntimeState | undefined;
36
43
  setState: (state: TelegramPreviewRuntimeState | undefined) => void;
37
44
  maxMessageLength: number;
45
+ minDraftIntervalMs?: number;
38
46
  getDraftSupport: () => TelegramDraftSupport;
39
47
  setDraftSupport: (support: TelegramDraftSupport) => void;
40
48
  allocateDraftId: () => number;
@@ -64,23 +72,13 @@ export interface TelegramPreviewActiveTurn {
64
72
  voiceReplyRequired?: boolean;
65
73
  }
66
74
 
67
- export interface TelegramAssistantMessagePreviewStartDeps<
68
- TMessage,
69
- TReplyMarkup = TelegramPreviewReplyMarkup,
70
- > {
75
+ export interface TelegramAssistantMessagePreviewStartDeps<TMessage> {
71
76
  getActiveTurn: () => TelegramPreviewActiveTurn | undefined;
72
77
  isAssistantMessage: (message: TMessage) => boolean;
73
78
  getState: () => TelegramPreviewRuntimeState | undefined;
74
79
  setState: (state: TelegramPreviewRuntimeState | undefined) => void;
75
80
  createPreviewState: () => TelegramPreviewRuntimeState;
76
81
  canSend?: () => boolean;
77
- finalizePreview: (chatId: number) => Promise<boolean>;
78
- finalizeMarkdownPreview: (
79
- chatId: number,
80
- markdown: string,
81
- replyToMessageId?: number,
82
- options?: { replyMarkup?: TReplyMarkup; target?: TelegramTarget },
83
- ) => Promise<boolean>;
84
82
  }
85
83
 
86
84
  export interface TelegramAssistantMessagePreviewUpdateDeps<TMessage> {
@@ -97,10 +95,7 @@ export interface TelegramAssistantMessagePreviewUpdateDeps<TMessage> {
97
95
  ) => void;
98
96
  }
99
97
 
100
- export type TelegramAssistantMessagePreviewHookDeps<
101
- TMessage,
102
- TReplyMarkup = TelegramPreviewReplyMarkup,
103
- > = TelegramAssistantMessagePreviewStartDeps<TMessage, TReplyMarkup> &
98
+ export type TelegramAssistantMessagePreviewHookDeps<TMessage> = TelegramAssistantMessagePreviewStartDeps<TMessage> &
104
99
  TelegramAssistantMessagePreviewUpdateDeps<TMessage>;
105
100
 
106
101
  export interface TelegramAssistantMessagePreviewHookEvent<TMessage> {
@@ -140,6 +135,12 @@ export interface TelegramPreviewControllerDeps {
140
135
  }
141
136
 
142
137
  export interface TelegramPreviewController {
138
+ seal: () => void;
139
+ preparePublication: () => TelegramAssistantOutputPreparation | undefined;
140
+ prepareClear: (
141
+ chatId: number,
142
+ options?: { target?: TelegramTarget; isDeliveryActive?: () => boolean },
143
+ ) => () => Promise<void>;
143
144
  getState: () => TelegramPreviewRuntimeState | undefined;
144
145
  setState: (state: TelegramPreviewRuntimeState | undefined) => void;
145
146
  setPendingText: (text: string) => void;
@@ -197,11 +198,18 @@ export interface TelegramAssistantPreviewRuntimeDeps<
197
198
  ) => Promise<number | undefined>;
198
199
  }
199
200
 
201
+ export interface TelegramPreparedPreviewDelivery<TReplyMarkup = unknown> {
202
+ clearPreview: TelegramPreviewController["clear"];
203
+ setPreviewPendingText: TelegramPreviewController["setPendingText"];
204
+ finalizeMarkdownPreview: ReturnType<typeof createTelegramNativeMarkdownPreviewFinalizer<TReplyMarkup>>;
205
+ }
206
+
200
207
  export type TelegramAssistantPreviewRuntime<
201
208
  TMessage,
202
209
  TReplyMarkup = TelegramPreviewReplyMarkup,
203
210
  > = TelegramPreviewController &
204
211
  TelegramAssistantMessagePreviewHooks<TMessage> & {
212
+ prepareDelivery: (isDeliveryActive: () => boolean) => TelegramPreparedPreviewDelivery<TReplyMarkup>;
205
213
  finalizeMarkdown: (
206
214
  chatId: number,
207
215
  markdown: string,
@@ -210,6 +218,14 @@ export type TelegramAssistantPreviewRuntime<
210
218
  ) => Promise<boolean>;
211
219
  };
212
220
 
221
+ function sealTelegramPreviewState(state: TelegramPreviewRuntimeState | undefined): void {
222
+ if (!state) return;
223
+ state.sealed = true;
224
+ state.flushRequested = false;
225
+ if (state.flushTimer) clearTimeout(state.flushTimer);
226
+ state.flushTimer = undefined;
227
+ }
228
+
213
229
  export function createTelegramNativeMarkdownPreviewFinalizer<
214
230
  TReplyMarkup,
215
231
  >(deps: {
@@ -219,6 +235,7 @@ export function createTelegramNativeMarkdownPreviewFinalizer<
219
235
  options?: { awaitFlush?: boolean; target?: TelegramTarget },
220
236
  ) => Promise<void>;
221
237
  discard?: () => void;
238
+ isDeliveryActive?: () => boolean;
222
239
  sendMarkdownReply: (
223
240
  chatId: number,
224
241
  replyToMessageId: number | undefined,
@@ -231,14 +248,21 @@ export function createTelegramNativeMarkdownPreviewFinalizer<
231
248
  replyToMessageId?: number,
232
249
  options?: { replyMarkup?: TReplyMarkup; target?: TelegramTarget },
233
250
  ) => Promise<boolean> {
251
+ return (...args) => prepareTelegramNativeMarkdownPreviewFinalizer(deps)(...args);
252
+ }
253
+
254
+ function prepareTelegramNativeMarkdownPreviewFinalizer<TReplyMarkup>(
255
+ deps: Parameters<typeof createTelegramNativeMarkdownPreviewFinalizer<TReplyMarkup>>[0],
256
+ ): ReturnType<typeof createTelegramNativeMarkdownPreviewFinalizer<TReplyMarkup>> {
257
+ const state = deps.getState();
258
+ sealTelegramPreviewState(state);
259
+ const inFlight = state?.flushPromise ?? state?.precedingFlush;
234
260
  return async (chatId, markdown, replyToMessageId, options) => {
235
- const state = deps.getState();
236
- if (state?.flushPromise) {
237
- await state.flushPromise.catch(() => {});
238
- if (deps.getState() !== state) return false;
239
- }
261
+ if (deps.isDeliveryActive?.() === false) return false;
262
+ await inFlight?.catch(() => {});
263
+ if (deps.getState() !== state || deps.isDeliveryActive?.() === false) return false;
240
264
  await deps.sendMarkdownReply(chatId, replyToMessageId, markdown, options);
241
- if (deps.getState() === state) deps.discard?.();
265
+ if (deps.getState() === state && deps.isDeliveryActive?.() !== false) deps.discard?.();
242
266
  return true;
243
267
  };
244
268
  }
@@ -250,15 +274,28 @@ export function createTelegramAssistantPreviewRuntime<
250
274
  deps: TelegramAssistantPreviewRuntimeDeps<TMessage, TReplyMarkup>,
251
275
  ): TelegramAssistantPreviewRuntime<TMessage, TReplyMarkup> {
252
276
  const controller = createTelegramPreviewControllerRuntime(deps);
253
- const finalizeMarkdownPreview = createTelegramNativeMarkdownPreviewFinalizer({
277
+ const finalizerDeps = {
254
278
  getState: controller.getState,
255
279
  clear: controller.clear,
256
280
  discard: () => controller.setState(undefined),
257
281
  sendMarkdownReply: deps.sendMarkdownReply,
258
- });
282
+ };
259
283
  return {
260
284
  ...controller,
261
- finalizeMarkdown: finalizeMarkdownPreview,
285
+ finalizeMarkdown: createTelegramNativeMarkdownPreviewFinalizer(finalizerDeps),
286
+ prepareDelivery(isDeliveryActive) {
287
+ const state = controller.getState();
288
+ return {
289
+ setPreviewPendingText(text) {
290
+ if (controller.getState() === state && isDeliveryActive()) controller.setPendingText(text);
291
+ },
292
+ clearPreview: async (chatId, options) => {
293
+ if (controller.getState() !== state || !isDeliveryActive()) return;
294
+ await controller.prepareClear(chatId, { ...options, isDeliveryActive })();
295
+ },
296
+ finalizeMarkdownPreview: prepareTelegramNativeMarkdownPreviewFinalizer({ ...finalizerDeps, isDeliveryActive }),
297
+ };
298
+ },
262
299
  ...createTelegramAssistantMessagePreviewHooks({
263
300
  getActiveTurn: deps.getActiveTurn,
264
301
  isAssistantMessage: deps.isAssistantMessage,
@@ -266,8 +303,6 @@ export function createTelegramAssistantPreviewRuntime<
266
303
  setState: controller.setState,
267
304
  createPreviewState: controller.createState,
268
305
  canSend: deps.canSend,
269
- finalizePreview: controller.finalize,
270
- finalizeMarkdownPreview,
271
306
  getMessageText: deps.getMessageText,
272
307
  schedulePreviewFlush: controller.scheduleFlush,
273
308
  }),
@@ -284,14 +319,17 @@ export function createTelegramPreviewController(
284
319
  deps.maxMessageLength ?? TELEGRAM_DRAFT_PREVIEW_MAX_CHARS;
285
320
  let draftSupport = deps.initialDraftSupport ?? "unknown";
286
321
  let nextDraftId = 0;
322
+ const setState = (nextState: TelegramPreviewRuntimeState | undefined): void => {
323
+ if (state !== nextState) sealTelegramPreviewState(state);
324
+ state = nextState;
325
+ };
287
326
  const getRuntimeDeps = (
288
327
  operationGeneration = generation,
289
328
  ): TelegramPreviewRuntimeDeps => ({
290
329
  getState: () => state,
291
- setState: (nextState) => {
292
- state = nextState;
293
- },
330
+ setState,
294
331
  maxMessageLength,
332
+ minDraftIntervalMs: TELEGRAM_DRAFT_INTERVAL_MS,
295
333
  getDraftSupport: () => draftSupport,
296
334
  setDraftSupport: (support) => {
297
335
  draftSupport = support;
@@ -307,20 +345,38 @@ export function createTelegramPreviewController(
307
345
  });
308
346
  return {
309
347
  getState: () => state,
310
- setState: (nextState) => {
311
- state = nextState;
312
- },
348
+ setState,
313
349
  setPendingText: (text) => {
314
350
  if (state) state.pendingText = text;
315
351
  },
316
352
  createState: () => createTelegramPreviewRuntimeState(),
317
353
  resetState: () => {
318
354
  generation += 1;
319
- state = createTelegramPreviewRuntimeState();
355
+ setState({ ...createTelegramPreviewRuntimeState(), nextDraftAt: state?.nextDraftAt });
320
356
  },
321
357
  invalidate: () => {
322
358
  generation += 1;
323
- state = undefined;
359
+ setState(undefined);
360
+ },
361
+ seal: () => sealTelegramPreviewState(state),
362
+ preparePublication: () => {
363
+ if (!state) return undefined;
364
+ sealTelegramPreviewState(state);
365
+ const prior = state.publicationPromise ?? state.flushPromise ?? state.precedingFlush;
366
+ let settle!: () => void;
367
+ state.publicationPromise = new Promise<void>((resolve) => { settle = resolve; });
368
+ return { wait: async () => { await prior?.catch(() => {}); }, settle };
369
+ },
370
+ prepareClear: (chatId, options) => {
371
+ const admittedState = state;
372
+ const runtime = getRuntimeDeps();
373
+ return async () => {
374
+ if (state !== admittedState) return;
375
+ await clearTelegramPreview(chatId, runtime, {
376
+ ...options,
377
+ isDeliveryActive: () => runtime.canSend?.() !== false && options?.isDeliveryActive?.() !== false,
378
+ });
379
+ };
324
380
  },
325
381
  clear: (chatId, options) =>
326
382
  clearTelegramPreview(chatId, getRuntimeDeps(), options),
@@ -335,11 +391,8 @@ export function createTelegramPreviewController(
335
391
  };
336
392
  }
337
393
 
338
- export function createTelegramAssistantMessagePreviewHooks<
339
- TMessage,
340
- TReplyMarkup = TelegramPreviewReplyMarkup,
341
- >(
342
- deps: TelegramAssistantMessagePreviewHookDeps<TMessage, TReplyMarkup>,
394
+ export function createTelegramAssistantMessagePreviewHooks<TMessage>(
395
+ deps: TelegramAssistantMessagePreviewHookDeps<TMessage>,
343
396
  ): TelegramAssistantMessagePreviewHooks<TMessage> {
344
397
  return {
345
398
  onMessageStart: async (
@@ -355,12 +408,9 @@ export function createTelegramAssistantMessagePreviewHooks<
355
408
  };
356
409
  }
357
410
 
358
- export async function handleTelegramAssistantMessagePreviewStart<
359
- TMessage,
360
- TReplyMarkup = TelegramPreviewReplyMarkup,
361
- >(
411
+ export async function handleTelegramAssistantMessagePreviewStart<TMessage>(
362
412
  message: TMessage,
363
- deps: TelegramAssistantMessagePreviewStartDeps<TMessage, TReplyMarkup>,
413
+ deps: TelegramAssistantMessagePreviewStartDeps<TMessage>,
364
414
  ): Promise<void> {
365
415
  const turn = deps.getActiveTurn();
366
416
  if (!turn || !deps.isAssistantMessage(message)) return;
@@ -373,26 +423,13 @@ export async function handleTelegramAssistantMessagePreviewStart<
373
423
  return;
374
424
  }
375
425
  const state = deps.getState();
376
- if (
377
- state &&
378
- (state.pendingText.trim().length > 0 ||
379
- state.lastSentText.trim().length > 0)
380
- ) {
381
- const previousText = state.pendingText.trim();
382
- if (previousText.length > 0) {
383
- await deps.finalizeMarkdownPreview(
384
- turn.chatId,
385
- previousText,
386
- turn.replyToMessageId,
387
- {
388
- target: turn.target,
389
- },
390
- );
391
- } else {
392
- await deps.finalizePreview(turn.chatId);
393
- }
394
- }
395
- deps.setState(deps.createPreviewState());
426
+ sealTelegramPreviewState(state);
427
+ const next = deps.createPreviewState();
428
+ // Carry the previous delivery boundary; permanent text remains with its publication owner.
429
+ next.draftId = state?.draftId;
430
+ next.nextDraftAt = state?.nextDraftAt;
431
+ next.precedingFlush = state?.publicationPromise ?? state?.flushPromise ?? state?.precedingFlush;
432
+ deps.setState(next);
396
433
  }
397
434
 
398
435
  export async function handleTelegramAssistantMessagePreviewUpdate<TMessage>(
@@ -411,6 +448,7 @@ export async function handleTelegramAssistantMessagePreviewUpdate<TMessage>(
411
448
  state = deps.createPreviewState();
412
449
  deps.setState(state);
413
450
  }
451
+ if (state.sealed) return;
414
452
  state.pendingText = stripTelegramCommentMarkupForPreview(
415
453
  deps.getMessageText(message),
416
454
  );
@@ -454,17 +492,20 @@ export function shouldUseTelegramDraftPreview(_options: {
454
492
  export async function clearTelegramPreview(
455
493
  chatId: number,
456
494
  deps: TelegramPreviewRuntimeDeps,
457
- options: { awaitFlush?: boolean; target?: TelegramTarget } = {},
495
+ options: { awaitFlush?: boolean; target?: TelegramTarget; isDeliveryActive?: () => boolean } = {},
458
496
  ): Promise<void> {
459
497
  const state = deps.getState();
460
- if (!state) return;
461
- if (state.flushPromise && options.awaitFlush !== false) {
498
+ if (!state || options.isDeliveryActive?.() === false) return;
499
+ sealTelegramPreviewState(state);
500
+ const inFlight = state.flushPromise ?? state.precedingFlush;
501
+ if (inFlight && options.awaitFlush !== false) {
462
502
  state.flushRequested = false;
463
- await state.flushPromise.catch(() => {});
503
+ await inFlight.catch(() => {});
464
504
  if (deps.getState() !== state) return;
465
505
  }
506
+ if (options.isDeliveryActive?.() === false) return;
466
507
  deps.setState(undefined);
467
- if (state.mode === "draft" && state.draftId !== undefined) {
508
+ if (state.mode === "draft" && state.draftId !== undefined && deps.canSend?.() !== false) {
468
509
  try {
469
510
  await deps.sendDraft(chatId, state.draftId, undefined, {
470
511
  ...getTelegramTargetThreadParams(options.target ?? { chatId }),
@@ -766,13 +807,15 @@ async function performTelegramPreviewFlush(
766
807
  ) {
767
808
  const draftId = state.draftId ?? deps.allocateDraftId();
768
809
  state.draftId = draftId;
810
+ state.nextDraftAt = Date.now() + (deps.minDraftIntervalMs ?? 0);
769
811
  try {
770
- await deps.sendDraft(
812
+ const delivered = await deps.sendDraft(
771
813
  chatId,
772
814
  draftId,
773
815
  normalizeTelegramNativeMarkdown(snapshot.text),
774
816
  { ...getTelegramTargetThreadParams(options.target ?? { chatId }) },
775
817
  );
818
+ if (delivered === false || deps.getState() !== state || deps.canSend?.() === false) return;
776
819
  deps.setDraftSupport("supported");
777
820
  state.mode = "draft";
778
821
  state.lastSentText = snapshot.text;
@@ -794,15 +837,33 @@ export async function flushTelegramPreview(
794
837
  options: { target?: TelegramTarget } = {},
795
838
  ): Promise<void> {
796
839
  const state = deps.getState();
797
- if (!state) return;
840
+ if (!state || state.sealed) return;
798
841
  if (state.flushPromise) {
799
842
  state.flushRequested = true;
800
843
  await state.flushPromise;
801
844
  return;
802
845
  }
803
846
  state.flushPromise = (async () => {
847
+ if (state.precedingFlush) {
848
+ await state.precedingFlush.catch(() => {});
849
+ state.precedingFlush = undefined;
850
+ if (deps.getState() !== state || state.sealed) return;
851
+ }
804
852
  do {
805
853
  state.flushRequested = false;
854
+ const delay = (state.nextDraftAt ?? 0) - Date.now();
855
+ if (delay > 0) {
856
+ if (!state.flushTimer) {
857
+ state.flushTimer = setTimeout(() => {
858
+ state.flushTimer = undefined;
859
+ if (deps.getState() === state && !state.sealed) void flushTelegramPreview(chatId, deps, options);
860
+ }, delay);
861
+ state.flushTimer.unref?.();
862
+ }
863
+ break;
864
+ }
865
+ if (state.flushTimer) clearTimeout(state.flushTimer);
866
+ state.flushTimer = undefined;
806
867
  try {
807
868
  await performTelegramPreviewFlush(chatId, state, deps, options);
808
869
  } catch (error) {
@@ -813,7 +874,7 @@ export async function flushTelegramPreview(
813
874
  });
814
875
  break;
815
876
  }
816
- } while (deps.getState() === state && state.flushRequested);
877
+ } while (deps.getState() === state && !state.sealed && state.flushRequested);
817
878
  })();
818
879
  try {
819
880
  await state.flushPromise;