@llblab/pi-kit 0.5.2 → 0.7.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 (141) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +5 -5
  3. package/node_modules/@llblab/pi-actors/AGENTS.md +1 -1
  4. package/node_modules/@llblab/pi-actors/CHANGELOG.md +6 -0
  5. package/node_modules/@llblab/pi-actors/README.md +1 -1
  6. package/node_modules/@llblab/pi-actors/dist/lib/async-runs.d.ts +3 -0
  7. package/node_modules/@llblab/pi-actors/dist/lib/async-runs.js +14 -1
  8. package/node_modules/@llblab/pi-actors/dist/lib/command-templates.js +45 -3
  9. package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.js +1 -1
  10. package/node_modules/@llblab/pi-actors/dist/lib/observability.d.ts +16 -3
  11. package/node_modules/@llblab/pi-actors/dist/lib/observability.js +92 -7
  12. package/node_modules/@llblab/pi-actors/dist/lib/pi.d.ts +0 -1
  13. package/node_modules/@llblab/pi-actors/dist/lib/pi.js +15 -24
  14. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery-lineage.d.ts +17 -0
  15. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery-lineage.js +44 -0
  16. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.d.ts +4 -0
  17. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.js +102 -4
  18. package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.js +58 -39
  19. package/node_modules/@llblab/pi-actors/dist/lib/runtime.js +14 -6
  20. package/node_modules/@llblab/pi-actors/dist/skills/actors/SKILL.md +1 -1
  21. package/node_modules/@llblab/pi-actors/docs/async-runs.md +1 -1
  22. package/node_modules/@llblab/pi-actors/docs/coordinator-delivery.md +18 -23
  23. package/node_modules/@llblab/pi-actors/lib/async-runs.ts +18 -1
  24. package/node_modules/@llblab/pi-actors/lib/command-templates.ts +41 -3
  25. package/node_modules/@llblab/pi-actors/lib/extension-runtime.ts +1 -1
  26. package/node_modules/@llblab/pi-actors/lib/observability.ts +119 -5
  27. package/node_modules/@llblab/pi-actors/lib/pi.ts +15 -28
  28. package/node_modules/@llblab/pi-actors/lib/run-delivery-lineage.ts +68 -0
  29. package/node_modules/@llblab/pi-actors/lib/run-delivery.ts +120 -4
  30. package/node_modules/@llblab/pi-actors/lib/run-ui-runtime.ts +69 -44
  31. package/node_modules/@llblab/pi-actors/lib/runtime.ts +17 -6
  32. package/node_modules/@llblab/pi-actors/package.json +1 -1
  33. package/node_modules/@llblab/pi-actors/skills/actors/SKILL.md +1 -1
  34. package/node_modules/@llblab/pi-grow-loop/AGENTS.md +2 -2
  35. package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +4 -0
  36. package/node_modules/@llblab/pi-grow-loop/README.md +6 -6
  37. package/node_modules/@llblab/pi-grow-loop/index.ts +6 -3
  38. package/node_modules/@llblab/pi-grow-loop/package.json +1 -1
  39. package/node_modules/@llblab/pi-state-flow/AGENTS.md +33 -12
  40. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +125 -2
  41. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +87 -45
  42. package/node_modules/@llblab/pi-state-flow/README.md +203 -107
  43. package/node_modules/@llblab/pi-state-flow/docs/README.md +4 -0
  44. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +188 -0
  45. package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +42 -0
  46. package/node_modules/@llblab/pi-state-flow/index.ts +164 -2
  47. package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +138 -0
  48. package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +273 -0
  49. package/node_modules/@llblab/pi-state-flow/lib/config.ts +48 -0
  50. package/node_modules/@llblab/pi-state-flow/lib/context.ts +18 -5
  51. package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +268 -0
  52. package/node_modules/@llblab/pi-state-flow/lib/discovery.ts +117 -0
  53. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +562 -0
  54. package/node_modules/@llblab/pi-state-flow/lib/episode.ts +24 -12
  55. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +606 -70
  56. package/node_modules/@llblab/pi-state-flow/lib/git.ts +666 -0
  57. package/node_modules/@llblab/pi-state-flow/lib/history.ts +95 -0
  58. package/node_modules/@llblab/pi-state-flow/lib/json.ts +24 -0
  59. package/node_modules/@llblab/pi-state-flow/lib/maintenance.ts +141 -0
  60. package/node_modules/@llblab/pi-state-flow/lib/memory.ts +52 -0
  61. package/node_modules/@llblab/pi-state-flow/lib/migration.ts +88 -0
  62. package/node_modules/@llblab/pi-state-flow/lib/publication.ts +296 -0
  63. package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +23 -7
  64. package/node_modules/@llblab/pi-state-flow/lib/rehydration.ts +79 -0
  65. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +264 -0
  66. package/node_modules/@llblab/pi-state-flow/lib/session.ts +6 -0
  67. package/node_modules/@llblab/pi-state-flow/lib/skills.ts +99 -7
  68. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +305 -48
  69. package/node_modules/@llblab/pi-state-flow/lib/state.ts +74 -7
  70. package/node_modules/@llblab/pi-state-flow/lib/status.ts +125 -6
  71. package/node_modules/@llblab/pi-state-flow/lib/storage.ts +196 -0
  72. package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +233 -0
  73. package/node_modules/@llblab/pi-state-flow/lib/terminal.ts +70 -24
  74. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +254 -29
  75. package/node_modules/@llblab/pi-state-flow/package.json +8 -2
  76. package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +128 -0
  77. package/node_modules/@llblab/pi-telegram/AGENTS.md +14 -9
  78. package/node_modules/@llblab/pi-telegram/BACKLOG.md +20 -4
  79. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +22 -5
  80. package/node_modules/@llblab/pi-telegram/README.md +13 -9
  81. package/node_modules/@llblab/pi-telegram/docs/README.md +1 -0
  82. package/node_modules/@llblab/pi-telegram/docs/architecture.md +220 -18
  83. package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +1 -1
  84. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +70 -19
  85. package/node_modules/@llblab/pi-telegram/docs/outbound.md +13 -7
  86. package/node_modules/@llblab/pi-telegram/docs/public-api.md +13 -5
  87. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +3 -1
  88. package/node_modules/@llblab/pi-telegram/index.ts +4 -1415
  89. package/node_modules/@llblab/pi-telegram/lib/activity.ts +19 -5
  90. package/node_modules/@llblab/pi-telegram/lib/agent-messages.ts +6 -3
  91. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +37 -2
  92. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +600 -135
  93. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +962 -55
  94. package/node_modules/@llblab/pi-telegram/lib/bus.ts +350 -26
  95. package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +544 -0
  96. package/node_modules/@llblab/pi-telegram/lib/commands.ts +234 -11
  97. package/node_modules/@llblab/pi-telegram/lib/config.ts +178 -25
  98. package/node_modules/@llblab/pi-telegram/lib/delivery.ts +18 -18
  99. package/node_modules/@llblab/pi-telegram/lib/extension.ts +1792 -0
  100. package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +20 -2
  101. package/node_modules/@llblab/pi-telegram/lib/journal.ts +2184 -126
  102. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +7 -1
  103. package/node_modules/@llblab/pi-telegram/lib/locks.ts +38 -1
  104. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +154 -15
  105. package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +74 -40
  106. package/node_modules/@llblab/pi-telegram/lib/outbound-voice.ts +28 -42
  107. package/node_modules/@llblab/pi-telegram/lib/outbound.ts +18 -14
  108. package/node_modules/@llblab/pi-telegram/lib/paths.ts +29 -0
  109. package/node_modules/@llblab/pi-telegram/lib/polling.ts +85 -17
  110. package/node_modules/@llblab/pi-telegram/lib/preview.ts +115 -70
  111. package/node_modules/@llblab/pi-telegram/lib/prompts.ts +5 -2
  112. package/node_modules/@llblab/pi-telegram/lib/queue.ts +66 -22
  113. package/node_modules/@llblab/pi-telegram/lib/replies.ts +47 -39
  114. package/node_modules/@llblab/pi-telegram/lib/routing.ts +305 -112
  115. package/node_modules/@llblab/pi-telegram/lib/status.ts +10 -0
  116. package/node_modules/@llblab/pi-telegram/lib/sync.ts +308 -39
  117. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +315 -7
  118. package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +664 -0
  119. package/node_modules/@llblab/pi-telegram/lib/thread-display.ts +226 -0
  120. package/node_modules/@llblab/pi-telegram/lib/thread-naming.ts +118 -0
  121. package/node_modules/@llblab/pi-telegram/lib/threads.ts +1686 -129
  122. package/node_modules/@llblab/pi-telegram/lib/updates.ts +1319 -97
  123. package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +1643 -0
  124. package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +968 -0
  125. package/node_modules/@llblab/pi-telegram/lib/workspace-slots.ts +84 -0
  126. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  127. package/node_modules/@llblab/pi-telegram/screenshot.png +0 -0
  128. package/node_modules/@llblab/pi-telegram/scripts/measure-bus.mjs +83 -0
  129. package/node_modules/@llblab/pi-telegram/scripts/measure-workspace.mjs +101 -0
  130. package/node_modules/@llblab/{skills → pi-telegram/skills}/show-me/SKILL.md +28 -6
  131. package/node_modules/@llblab/pi-telegram/skills/show-me/references/telegram-surfaces.md +43 -0
  132. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/delivery-and-threads.md +1 -1
  133. package/node_modules/@llblab/skills/abcd-context/AGENTS.md +1 -0
  134. package/node_modules/@llblab/skills/abcd-context/CHANGELOG.md +6 -2
  135. package/node_modules/@llblab/skills/abcd-context/SKILL.md +1 -1
  136. package/node_modules/@llblab/skills/abcd-context/docs/validation-design.md +11 -5
  137. package/node_modules/@llblab/skills/abcd-context/scripts/_self-test.mjs +61 -0
  138. package/node_modules/@llblab/skills/abcd-context/scripts/validate-context.mjs +67 -0
  139. package/node_modules/@llblab/skills/package.json +2 -3
  140. package/node_modules/@llblab/skills/release-flow/SKILL.md +2 -4
  141. package/package.json +7 -6
@@ -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,8 @@ export interface TelegramPreviewControllerDeps {
140
135
  }
141
136
 
142
137
  export interface TelegramPreviewController {
138
+ seal: () => void;
139
+ preparePublication: () => TelegramAssistantOutputPreparation | undefined;
143
140
  prepareClear: (
144
141
  chatId: number,
145
142
  options?: { target?: TelegramTarget; isDeliveryActive?: () => boolean },
@@ -201,11 +198,18 @@ export interface TelegramAssistantPreviewRuntimeDeps<
201
198
  ) => Promise<number | undefined>;
202
199
  }
203
200
 
201
+ export interface TelegramPreparedPreviewDelivery<TReplyMarkup = unknown> {
202
+ clearPreview: TelegramPreviewController["clear"];
203
+ setPreviewPendingText: TelegramPreviewController["setPendingText"];
204
+ finalizeMarkdownPreview: ReturnType<typeof createTelegramNativeMarkdownPreviewFinalizer<TReplyMarkup>>;
205
+ }
206
+
204
207
  export type TelegramAssistantPreviewRuntime<
205
208
  TMessage,
206
209
  TReplyMarkup = TelegramPreviewReplyMarkup,
207
210
  > = TelegramPreviewController &
208
211
  TelegramAssistantMessagePreviewHooks<TMessage> & {
212
+ prepareDelivery: (isDeliveryActive: () => boolean) => TelegramPreparedPreviewDelivery<TReplyMarkup>;
209
213
  finalizeMarkdown: (
210
214
  chatId: number,
211
215
  markdown: string,
@@ -214,6 +218,14 @@ export type TelegramAssistantPreviewRuntime<
214
218
  ) => Promise<boolean>;
215
219
  };
216
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
+
217
229
  export function createTelegramNativeMarkdownPreviewFinalizer<
218
230
  TReplyMarkup,
219
231
  >(deps: {
@@ -223,6 +235,7 @@ export function createTelegramNativeMarkdownPreviewFinalizer<
223
235
  options?: { awaitFlush?: boolean; target?: TelegramTarget },
224
236
  ) => Promise<void>;
225
237
  discard?: () => void;
238
+ isDeliveryActive?: () => boolean;
226
239
  sendMarkdownReply: (
227
240
  chatId: number,
228
241
  replyToMessageId: number | undefined,
@@ -235,14 +248,21 @@ export function createTelegramNativeMarkdownPreviewFinalizer<
235
248
  replyToMessageId?: number,
236
249
  options?: { replyMarkup?: TReplyMarkup; target?: TelegramTarget },
237
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;
238
260
  return async (chatId, markdown, replyToMessageId, options) => {
239
- const state = deps.getState();
240
- if (state?.flushPromise) {
241
- await state.flushPromise.catch(() => {});
242
- if (deps.getState() !== state) return false;
243
- }
261
+ if (deps.isDeliveryActive?.() === false) return false;
262
+ await inFlight?.catch(() => {});
263
+ if (deps.getState() !== state || deps.isDeliveryActive?.() === false) return false;
244
264
  await deps.sendMarkdownReply(chatId, replyToMessageId, markdown, options);
245
- if (deps.getState() === state) deps.discard?.();
265
+ if (deps.getState() === state && deps.isDeliveryActive?.() !== false) deps.discard?.();
246
266
  return true;
247
267
  };
248
268
  }
@@ -254,15 +274,28 @@ export function createTelegramAssistantPreviewRuntime<
254
274
  deps: TelegramAssistantPreviewRuntimeDeps<TMessage, TReplyMarkup>,
255
275
  ): TelegramAssistantPreviewRuntime<TMessage, TReplyMarkup> {
256
276
  const controller = createTelegramPreviewControllerRuntime(deps);
257
- const finalizeMarkdownPreview = createTelegramNativeMarkdownPreviewFinalizer({
277
+ const finalizerDeps = {
258
278
  getState: controller.getState,
259
279
  clear: controller.clear,
260
280
  discard: () => controller.setState(undefined),
261
281
  sendMarkdownReply: deps.sendMarkdownReply,
262
- });
282
+ };
263
283
  return {
264
284
  ...controller,
265
- 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
+ },
266
299
  ...createTelegramAssistantMessagePreviewHooks({
267
300
  getActiveTurn: deps.getActiveTurn,
268
301
  isAssistantMessage: deps.isAssistantMessage,
@@ -270,8 +303,6 @@ export function createTelegramAssistantPreviewRuntime<
270
303
  setState: controller.setState,
271
304
  createPreviewState: controller.createState,
272
305
  canSend: deps.canSend,
273
- finalizePreview: controller.finalize,
274
- finalizeMarkdownPreview,
275
306
  getMessageText: deps.getMessageText,
276
307
  schedulePreviewFlush: controller.scheduleFlush,
277
308
  }),
@@ -288,14 +319,17 @@ export function createTelegramPreviewController(
288
319
  deps.maxMessageLength ?? TELEGRAM_DRAFT_PREVIEW_MAX_CHARS;
289
320
  let draftSupport = deps.initialDraftSupport ?? "unknown";
290
321
  let nextDraftId = 0;
322
+ const setState = (nextState: TelegramPreviewRuntimeState | undefined): void => {
323
+ if (state !== nextState) sealTelegramPreviewState(state);
324
+ state = nextState;
325
+ };
291
326
  const getRuntimeDeps = (
292
327
  operationGeneration = generation,
293
328
  ): TelegramPreviewRuntimeDeps => ({
294
329
  getState: () => state,
295
- setState: (nextState) => {
296
- state = nextState;
297
- },
330
+ setState,
298
331
  maxMessageLength,
332
+ minDraftIntervalMs: TELEGRAM_DRAFT_INTERVAL_MS,
299
333
  getDraftSupport: () => draftSupport,
300
334
  setDraftSupport: (support) => {
301
335
  draftSupport = support;
@@ -311,20 +345,27 @@ export function createTelegramPreviewController(
311
345
  });
312
346
  return {
313
347
  getState: () => state,
314
- setState: (nextState) => {
315
- state = nextState;
316
- },
348
+ setState,
317
349
  setPendingText: (text) => {
318
350
  if (state) state.pendingText = text;
319
351
  },
320
352
  createState: () => createTelegramPreviewRuntimeState(),
321
353
  resetState: () => {
322
354
  generation += 1;
323
- state = createTelegramPreviewRuntimeState();
355
+ setState({ ...createTelegramPreviewRuntimeState(), nextDraftAt: state?.nextDraftAt });
324
356
  },
325
357
  invalidate: () => {
326
358
  generation += 1;
327
- 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 };
328
369
  },
329
370
  prepareClear: (chatId, options) => {
330
371
  const admittedState = state;
@@ -350,11 +391,8 @@ export function createTelegramPreviewController(
350
391
  };
351
392
  }
352
393
 
353
- export function createTelegramAssistantMessagePreviewHooks<
354
- TMessage,
355
- TReplyMarkup = TelegramPreviewReplyMarkup,
356
- >(
357
- deps: TelegramAssistantMessagePreviewHookDeps<TMessage, TReplyMarkup>,
394
+ export function createTelegramAssistantMessagePreviewHooks<TMessage>(
395
+ deps: TelegramAssistantMessagePreviewHookDeps<TMessage>,
358
396
  ): TelegramAssistantMessagePreviewHooks<TMessage> {
359
397
  return {
360
398
  onMessageStart: async (
@@ -370,12 +408,9 @@ export function createTelegramAssistantMessagePreviewHooks<
370
408
  };
371
409
  }
372
410
 
373
- export async function handleTelegramAssistantMessagePreviewStart<
374
- TMessage,
375
- TReplyMarkup = TelegramPreviewReplyMarkup,
376
- >(
411
+ export async function handleTelegramAssistantMessagePreviewStart<TMessage>(
377
412
  message: TMessage,
378
- deps: TelegramAssistantMessagePreviewStartDeps<TMessage, TReplyMarkup>,
413
+ deps: TelegramAssistantMessagePreviewStartDeps<TMessage>,
379
414
  ): Promise<void> {
380
415
  const turn = deps.getActiveTurn();
381
416
  if (!turn || !deps.isAssistantMessage(message)) return;
@@ -388,26 +423,13 @@ export async function handleTelegramAssistantMessagePreviewStart<
388
423
  return;
389
424
  }
390
425
  const state = deps.getState();
391
- if (
392
- state &&
393
- (state.pendingText.trim().length > 0 ||
394
- state.lastSentText.trim().length > 0)
395
- ) {
396
- const previousText = state.pendingText.trim();
397
- if (previousText.length > 0) {
398
- await deps.finalizeMarkdownPreview(
399
- turn.chatId,
400
- previousText,
401
- turn.replyToMessageId,
402
- {
403
- target: turn.target,
404
- },
405
- );
406
- } else {
407
- await deps.finalizePreview(turn.chatId);
408
- }
409
- }
410
- 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);
411
433
  }
412
434
 
413
435
  export async function handleTelegramAssistantMessagePreviewUpdate<TMessage>(
@@ -426,6 +448,7 @@ export async function handleTelegramAssistantMessagePreviewUpdate<TMessage>(
426
448
  state = deps.createPreviewState();
427
449
  deps.setState(state);
428
450
  }
451
+ if (state.sealed) return;
429
452
  state.pendingText = stripTelegramCommentMarkupForPreview(
430
453
  deps.getMessageText(message),
431
454
  );
@@ -473,9 +496,11 @@ export async function clearTelegramPreview(
473
496
  ): Promise<void> {
474
497
  const state = deps.getState();
475
498
  if (!state || options.isDeliveryActive?.() === false) return;
476
- if (state.flushPromise && options.awaitFlush !== false) {
499
+ sealTelegramPreviewState(state);
500
+ const inFlight = state.flushPromise ?? state.precedingFlush;
501
+ if (inFlight && options.awaitFlush !== false) {
477
502
  state.flushRequested = false;
478
- await state.flushPromise.catch(() => {});
503
+ await inFlight.catch(() => {});
479
504
  if (deps.getState() !== state) return;
480
505
  }
481
506
  if (options.isDeliveryActive?.() === false) return;
@@ -782,13 +807,15 @@ async function performTelegramPreviewFlush(
782
807
  ) {
783
808
  const draftId = state.draftId ?? deps.allocateDraftId();
784
809
  state.draftId = draftId;
810
+ state.nextDraftAt = Date.now() + (deps.minDraftIntervalMs ?? 0);
785
811
  try {
786
- await deps.sendDraft(
812
+ const delivered = await deps.sendDraft(
787
813
  chatId,
788
814
  draftId,
789
815
  normalizeTelegramNativeMarkdown(snapshot.text),
790
816
  { ...getTelegramTargetThreadParams(options.target ?? { chatId }) },
791
817
  );
818
+ if (delivered === false || deps.getState() !== state || deps.canSend?.() === false) return;
792
819
  deps.setDraftSupport("supported");
793
820
  state.mode = "draft";
794
821
  state.lastSentText = snapshot.text;
@@ -810,15 +837,33 @@ export async function flushTelegramPreview(
810
837
  options: { target?: TelegramTarget } = {},
811
838
  ): Promise<void> {
812
839
  const state = deps.getState();
813
- if (!state) return;
840
+ if (!state || state.sealed) return;
814
841
  if (state.flushPromise) {
815
842
  state.flushRequested = true;
816
843
  await state.flushPromise;
817
844
  return;
818
845
  }
819
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
+ }
820
852
  do {
821
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;
822
867
  try {
823
868
  await performTelegramPreviewFlush(chatId, state, deps, options);
824
869
  } catch (error) {
@@ -829,7 +874,7 @@ export async function flushTelegramPreview(
829
874
  });
830
875
  break;
831
876
  }
832
- } while (deps.getState() === state && state.flushRequested);
877
+ } while (deps.getState() === state && !state.sealed && state.flushRequested);
833
878
  })();
834
879
  try {
835
880
  await state.flushPromise;
@@ -14,7 +14,7 @@ export const TELEGRAM_DISCONNECTED_CONTEXT_MESSAGE =
14
14
 
15
15
  const LOCAL_SYSTEM_PROMPT_SUFFIX = `
16
16
 
17
- ${TELEGRAM_CONNECTED_CONTEXT_MESSAGE} For Telegram work, consult bundled Skills in routing order: \`telegram-bridge\` for the transport and turn protocol, \`generated-control-surface\` when contextual controls materially shorten feedback, then \`generative-apps\` when the interaction warrants a reusable deterministic app. Load a Skill only if its instructions are not already present in the current context. Do not use Telegram-specific features from unrelated local/TUI prompts.`;
17
+ ${TELEGRAM_CONNECTED_CONTEXT_MESSAGE} For Telegram work, consult bundled Skills in routing order: \`telegram-bridge\` for the transport and turn protocol, \`show-me\` when a user needs a truthful visual explanation of work or behavior, \`generated-control-surface\` when contextual controls materially shorten feedback, then \`generative-apps\` when the interaction warrants a reusable deterministic app. Load a Skill only if its instructions are not already present in the current context. Do not use Telegram-specific features from unrelated local/TUI prompts.`;
18
18
 
19
19
  const TELEGRAM_TURN_SYSTEM_PROMPT_SUFFIX = `
20
20
 
@@ -28,9 +28,10 @@ export const TELEGRAM_ATTACH_PROMPT_GUIDELINES = [
28
28
  "For an explicit thread target, provide chat_id plus thread_id; registered multi-instance followers default to their assigned thread target.",
29
29
  ] as const;
30
30
  export const TELEGRAM_MESSAGE_PROMPT_SNIPPET =
31
- "Send direct Telegram Markdown text when the user explicitly asks for Telegram delivery outside the normal reply flow.";
31
+ "Send direct Telegram Markdown text when the user explicitly asks for Telegram delivery to a concrete chat, channel, or live Pi Thread outside the normal reply flow.";
32
32
  export const TELEGRAM_MESSAGE_PROMPT_GUIDELINES = [
33
33
  "Use telegram_message only when the user explicitly asks to send a message to Telegram from the local/TUI side, or names a concrete Telegram delivery target.",
34
+ "For an explicitly requested channel post, pass its exact numeric id or public @username as chat_id; no local channel registry is required, and Telegram remains the authority on the bot's posting permission.",
34
35
  "For a live Pi thread target, provide thread as its case-insensitive name or numeric id; the bridge sends visibly and admits one attributed turn to that live instance. Unknown, ambiguous, same, or offline targets fail before sending.",
35
36
  "Add buttons by embedding the same top-level telegram_button HTML comments used in normal Telegram replies; Telegram does not support standalone buttons.",
36
37
  "During an active Telegram turn, omit telegram_message for the current target and answer normally; use thread only when the user requests delivery to a different live Pi thread.",
@@ -48,6 +49,8 @@ const TELEGRAM_TOOL_METADATA_LINES = Object.fromEntries(
48
49
  const TELEGRAM_MODEL_CONTEXT_TOOL_NAMES = new Set([
49
50
  "telegram_attach",
50
51
  "telegram_bind",
52
+ "telegram_channel_post",
53
+ "telegram_channel_posts",
51
54
  "telegram_message",
52
55
  ]);
53
56
  const TELEGRAM_MODEL_CONTEXT_MEMORY_KEY = Symbol.for(
@@ -1498,6 +1498,10 @@ export interface TelegramAgentEndRuntimeDeps<
1498
1498
  updateStatus: () => void;
1499
1499
  dispatchNextQueuedTelegramTurn: () => void;
1500
1500
  scheduleActiveTurnDelivery?: (task: () => Promise<void>) => void;
1501
+ preparePreviewDelivery?: (isDeliveryActive: () => boolean) => Pick<
1502
+ TelegramAgentEndRuntimeDeps<TTurn, TReplyMarkup>,
1503
+ "clearPreview" | "setPreviewPendingText" | "finalizeMarkdownPreview"
1504
+ > | undefined;
1501
1505
  preparePreviewClear?: (
1502
1506
  chatId: number,
1503
1507
  options?: { target?: TelegramQueueTarget; isDeliveryActive?: () => boolean },
@@ -1523,7 +1527,7 @@ export interface TelegramAgentEndRuntimeDeps<
1523
1527
  chatId: number,
1524
1528
  replyToMessageId: number,
1525
1529
  text: string,
1526
- options?: { target?: TelegramQueueTarget },
1530
+ options?: { target?: TelegramQueueTarget; parseMode?: "HTML" },
1527
1531
  ) => Promise<unknown>;
1528
1532
  sendQueuedAttachments: (
1529
1533
  turn: TTurn,
@@ -1595,6 +1599,7 @@ export interface TelegramAgentEndHookRuntimeDeps<
1595
1599
  schedule: (task: () => Promise<void>) => void;
1596
1600
  cancel: () => void;
1597
1601
  };
1602
+ preparePreviewDelivery?: TelegramAgentEndRuntimeDeps<TTurn, TReplyMarkup>["preparePreviewDelivery"];
1598
1603
  preparePreviewClear?: TelegramAgentEndRuntimeDeps<TTurn, TReplyMarkup>["preparePreviewClear"];
1599
1604
  clearPreview: TelegramAgentEndRuntimeDeps<
1600
1605
  TTurn,
@@ -1746,6 +1751,7 @@ export function createTelegramAgentEndHook<
1746
1751
  await task();
1747
1752
  })
1748
1753
  : undefined,
1754
+ preparePreviewDelivery: deps.preparePreviewDelivery,
1749
1755
  preparePreviewClear: deps.preparePreviewClear,
1750
1756
  clearPreview: deps.clearPreview,
1751
1757
  setPreviewPendingText: deps.setPreviewPendingText,
@@ -1807,9 +1813,13 @@ export async function handleTelegramAgentEndRuntime<
1807
1813
  const isDeliveryActive = (): boolean =>
1808
1814
  deps.isSessionActive?.() !== false &&
1809
1815
  (!turn || deps.isTurnTransportActive?.(turn) !== false);
1816
+ const preview = turn && !turn.guestQueryId ? deps.preparePreviewDelivery?.(isDeliveryActive) : undefined;
1817
+ const setPreviewPendingText = preview?.setPreviewPendingText ?? deps.setPreviewPendingText;
1818
+ const finalizeMarkdownPreview = preview?.finalizeMarkdownPreview ?? deps.finalizeMarkdownPreview;
1810
1819
  const clearPreview = turn
1811
- ? deps.preparePreviewClear?.(turn.chatId, { target: turn.target, isDeliveryActive })
1812
- ?? (() => deps.clearPreview(turn.chatId, { target: turn.target }))
1820
+ ? preview ? () => preview.clearPreview(turn.chatId, { target: turn.target })
1821
+ : deps.preparePreviewClear?.(turn.chatId, { target: turn.target, isDeliveryActive })
1822
+ ?? (() => deps.clearPreview(turn.chatId, { target: turn.target }))
1813
1823
  : undefined;
1814
1824
  const updateStatusIgnoringStaleContext = (): void => {
1815
1825
  try {
@@ -1898,23 +1908,37 @@ export async function handleTelegramAgentEndRuntime<
1898
1908
  const deliverActiveTurn = async () => {
1899
1909
  await deps.waitForActivityIdle?.();
1900
1910
  if (!isDeliveryActive()) return;
1901
- if (endPlan.shouldClearPreview || (!finalText && hasOutboundArtifacts)) {
1911
+ let previewCleared = false;
1912
+ const clearTurnPreview = async () => {
1913
+ if (previewCleared) return;
1902
1914
  await clearPreview?.();
1915
+ previewCleared = true;
1916
+ };
1917
+ if (endPlan.shouldClearPreview || (!finalText && hasOutboundArtifacts)) {
1918
+ await clearTurnPreview();
1903
1919
  if (!isDeliveryActive()) return;
1904
1920
  }
1905
1921
  if (endPlan.shouldSendErrorMessage) {
1922
+ const errorMessage = assistant.errorMessage ||
1923
+ "Telegram bridge: Pi failed while processing the request.";
1924
+ const isOperationAborted = errorMessage.trim().replace(/\.$/, "") ===
1925
+ "This operation was aborted";
1906
1926
  await deps.sendTextReply(
1907
1927
  turn.chatId,
1908
1928
  turn.replyToMessageId,
1909
- assistant.errorMessage ||
1910
- "Telegram bridge: Pi failed while processing the request.",
1911
- { target: turn.target },
1929
+ isOperationAborted
1930
+ ? "<b>⏹️ This operation was aborted.</b>"
1931
+ : errorMessage,
1932
+ {
1933
+ target: turn.target,
1934
+ ...(isOperationAborted ? { parseMode: "HTML" as const } : {}),
1935
+ },
1912
1936
  );
1913
1937
  if (!isDeliveryActive()) return;
1914
1938
  if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
1915
1939
  return;
1916
1940
  }
1917
- if (finalText) deps.setPreviewPendingText(finalText);
1941
+ if (finalText && turn.queuedAttachments.length === 0) setPreviewPendingText(finalText);
1918
1942
 
1919
1943
  if (!isDeliveryActive()) return;
1920
1944
  let richAttachmentDelivered = false;
@@ -1932,9 +1956,9 @@ export async function handleTelegramAgentEndRuntime<
1932
1956
  );
1933
1957
  if (!isDeliveryActive()) return;
1934
1958
  if (richAttachmentDelivered) {
1935
- await deps.clearPreview(turn.chatId, { target: turn.target });
1959
+ await clearTurnPreview();
1936
1960
  if (!isDeliveryActive()) return;
1937
- deps.setPreviewPendingText("");
1961
+ setPreviewPendingText("");
1938
1962
  }
1939
1963
  } catch (error) {
1940
1964
  if (!isDeliveryActive()) return;
@@ -1947,27 +1971,45 @@ export async function handleTelegramAgentEndRuntime<
1947
1971
  }
1948
1972
  }
1949
1973
  if (!isDeliveryActive()) return;
1974
+ let queuedAttachmentsDelivered = false;
1975
+ if (!richAttachmentDelivered && turn.queuedAttachments.length > 0) {
1976
+ await clearTurnPreview();
1977
+ if (!isDeliveryActive()) return;
1978
+ setPreviewPendingText("");
1979
+ await deps.sendQueuedAttachments(turn, { isDeliveryActive });
1980
+ if (!isDeliveryActive()) return;
1981
+ queuedAttachmentsDelivered = true;
1982
+ }
1950
1983
  if (!richAttachmentDelivered && endPlan.kind === "text" && finalText) {
1951
1984
  try {
1952
- const finalized = await deps.finalizeMarkdownPreview(
1953
- turn.chatId,
1954
- finalText,
1955
- turn.replyToMessageId,
1956
- { replyMarkup, target: turn.target },
1957
- );
1958
- if (!isDeliveryActive()) return;
1959
- if (!finalized) {
1960
- await deps.clearPreview(turn.chatId, { target: turn.target });
1961
- if (!isDeliveryActive()) return;
1985
+ if (queuedAttachmentsDelivered) {
1962
1986
  await deps.sendMarkdownReply(
1963
1987
  turn.chatId,
1964
1988
  turn.replyToMessageId,
1965
1989
  finalText,
1966
1990
  { replyMarkup, target: turn.target },
1967
1991
  );
1992
+ } else {
1993
+ const finalized = await finalizeMarkdownPreview(
1994
+ turn.chatId,
1995
+ finalText,
1996
+ turn.replyToMessageId,
1997
+ { replyMarkup, target: turn.target },
1998
+ );
1999
+ if (!isDeliveryActive()) return;
2000
+ if (!finalized) {
2001
+ await clearTurnPreview();
2002
+ if (!isDeliveryActive()) return;
2003
+ await deps.sendMarkdownReply(
2004
+ turn.chatId,
2005
+ turn.replyToMessageId,
2006
+ finalText,
2007
+ { replyMarkup, target: turn.target },
2008
+ );
2009
+ }
1968
2010
  }
1969
2011
  if (!isDeliveryActive()) return;
1970
- deps.setPreviewPendingText("");
2012
+ setPreviewPendingText("");
1971
2013
  } catch (error) {
1972
2014
  deps.recordRuntimeEvent?.("delivery", error, {
1973
2015
  phase: "final-text",
@@ -2031,7 +2073,9 @@ export async function handleTelegramAgentEndRuntime<
2031
2073
  );
2032
2074
  }
2033
2075
  if (!isDeliveryActive()) return;
2034
- if (!richAttachmentDelivered) await deps.sendQueuedAttachments(turn, { isDeliveryActive });
2076
+ if (!richAttachmentDelivered && !queuedAttachmentsDelivered) {
2077
+ await deps.sendQueuedAttachments(turn, { isDeliveryActive });
2078
+ }
2035
2079
  if (!isDeliveryActive()) return;
2036
2080
  if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
2037
2081
  };