@llblab/pi-telegram 0.54.3 → 0.55.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 (80) hide show
  1. package/AGENTS.md +4 -2
  2. package/BACKLOG.md +1 -1
  3. package/CHANGELOG.md +21 -0
  4. package/README.md +5 -3
  5. package/dist/lib/activity.d.ts +5 -1
  6. package/dist/lib/activity.js +31 -2
  7. package/dist/lib/bindings.d.ts +5 -1
  8. package/dist/lib/bindings.js +9 -2
  9. package/dist/lib/bus-api.d.ts +2 -2
  10. package/dist/lib/bus-api.js +7 -2
  11. package/dist/lib/bus-follower.d.ts +49 -3
  12. package/dist/lib/bus-follower.js +313 -146
  13. package/dist/lib/bus-leader.d.ts +3 -0
  14. package/dist/lib/bus-leader.js +99 -18
  15. package/dist/lib/bus.d.ts +5 -3
  16. package/dist/lib/bus.js +13 -10
  17. package/dist/lib/delivery.d.ts +4 -0
  18. package/dist/lib/delivery.js +48 -15
  19. package/dist/lib/extension.js +56 -6
  20. package/dist/lib/lifecycle.d.ts +68 -0
  21. package/dist/lib/lifecycle.js +178 -58
  22. package/dist/lib/machine.d.ts +23 -0
  23. package/dist/lib/machine.js +37 -0
  24. package/dist/lib/media.d.ts +114 -28
  25. package/dist/lib/media.js +292 -119
  26. package/dist/lib/model.d.ts +54 -15
  27. package/dist/lib/model.js +98 -53
  28. package/dist/lib/polling.js +2 -0
  29. package/dist/lib/preview.d.ts +128 -113
  30. package/dist/lib/preview.js +448 -366
  31. package/dist/lib/queue.d.ts +2 -2
  32. package/dist/lib/queue.js +3 -3
  33. package/dist/lib/routing.d.ts +14 -2
  34. package/dist/lib/routing.js +546 -207
  35. package/dist/lib/runtime.d.ts +77 -11
  36. package/dist/lib/runtime.js +174 -92
  37. package/dist/lib/sections.js +0 -1
  38. package/dist/lib/telegram-api.d.ts +11 -9
  39. package/dist/lib/telegram-api.js +53 -4
  40. package/dist/lib/text-groups.d.ts +0 -26
  41. package/dist/lib/text-groups.js +51 -98
  42. package/dist/lib/thread-display.js +14 -6
  43. package/dist/lib/threads.d.ts +20 -5
  44. package/dist/lib/threads.js +111 -32
  45. package/dist/lib/turns.d.ts +2 -0
  46. package/dist/lib/turns.js +6 -1
  47. package/dist/lib/updates.d.ts +11 -4
  48. package/dist/lib/updates.js +54 -19
  49. package/dist/package.json +1 -1
  50. package/docs/activity.md +1 -1
  51. package/docs/architecture.md +16 -15
  52. package/docs/delivery.md +7 -5
  53. package/docs/multi-instance-bus.md +18 -8
  54. package/docs/public-api.md +2 -2
  55. package/docs/ui-style.md +8 -6
  56. package/lib/activity.ts +29 -5
  57. package/lib/bindings.ts +13 -1
  58. package/lib/bus-api.ts +11 -3
  59. package/lib/bus-follower.ts +417 -203
  60. package/lib/bus-leader.ts +88 -14
  61. package/lib/bus.ts +17 -15
  62. package/lib/delivery.ts +47 -19
  63. package/lib/extension.ts +59 -9
  64. package/lib/lifecycle.ts +229 -55
  65. package/lib/machine.ts +60 -0
  66. package/lib/media.ts +438 -172
  67. package/lib/model.ts +169 -74
  68. package/lib/polling.ts +2 -0
  69. package/lib/preview.ts +715 -566
  70. package/lib/queue.ts +5 -5
  71. package/lib/routing.ts +644 -283
  72. package/lib/runtime.ts +260 -104
  73. package/lib/sections.ts +0 -2
  74. package/lib/telegram-api.ts +61 -8
  75. package/lib/text-groups.ts +75 -136
  76. package/lib/thread-display.ts +15 -4
  77. package/lib/threads.ts +130 -38
  78. package/lib/turns.ts +8 -1
  79. package/lib/updates.ts +58 -21
  80. package/package.json +1 -1
@@ -3,6 +3,7 @@
3
3
  * Zones: pi agent model control, telegram controls, queue continuation
4
4
  * Owns model identity, thinking levels, scoped resolution, current-model state, and in-flight model switching
5
5
  */
6
+ import * as Machine from "./machine.ts";
6
7
  import { type PendingTelegramTurn } from "./queue.ts";
7
8
  export interface MenuModel {
8
9
  provider: string;
@@ -44,17 +45,10 @@ export declare function parseTelegramScopedModelPatternList(value: string): stri
44
45
  export declare function parseTelegramCliScopedModelPatterns(args: string[]): string[] | undefined;
45
46
  export declare function resolveScopedModelPatterns<TModel extends MenuModel = MenuModel>(patterns: string[], availableModels: TModel[]): ScopedTelegramModel<TModel>[];
46
47
  export declare function sortScopedModels<TModel extends MenuModel = MenuModel>(models: ScopedTelegramModel<TModel>[], currentModel: TModel | undefined): ScopedTelegramModel<TModel>[];
47
- export interface PendingModelSwitchStore<TSelection> {
48
- get: () => TSelection | undefined;
49
- set: (selection: TSelection | undefined) => void;
50
- clear: () => void;
51
- has: () => boolean;
52
- }
53
48
  export interface TelegramInFlightModelSwitchState {
54
49
  isIdle: boolean;
55
50
  hasAbortHandler: boolean;
56
51
  }
57
- export declare function createPendingModelSwitchStore<TSelection>(): PendingModelSwitchStore<TSelection>;
58
52
  export declare function canRestartAgentRunForTelegramModelSwitch(state: TelegramInFlightModelSwitchState): boolean;
59
53
  export declare function shouldTriggerPendingTelegramModelSwitchAbort(state: {
60
54
  hasPendingModelSwitch: boolean;
@@ -62,12 +56,6 @@ export declare function shouldTriggerPendingTelegramModelSwitchAbort(state: {
62
56
  hasAbortHandler: boolean;
63
57
  activeToolExecutions: number;
64
58
  }): boolean;
65
- export declare function restartTelegramModelSwitchContinuation<TTurn, TSelection>(state: {
66
- activeTurn: TTurn | undefined;
67
- abort: (() => void) | undefined;
68
- selection: TSelection;
69
- queueContinuation: (turn: TTurn, selection: TSelection) => void;
70
- }): boolean;
71
59
  export declare function buildTelegramModelSwitchContinuationText<TModel extends MenuModel>(telegramPrefix: string, model: TModel, thinkingLevel?: ScopedTelegramModel<TModel>["thinkingLevel"]): string;
72
60
  export type TelegramModelSwitchContinuationSource = Pick<PendingTelegramTurn, "chatId" | "replyToMessageId" | "target">;
73
61
  export declare function buildTelegramModelSwitchContinuationTurn<TModel extends MenuModel>(options: {
@@ -100,8 +88,6 @@ export declare function createTelegramModelSwitchContinuationQueueRuntime<TConte
100
88
  }): (turn: TelegramModelSwitchContinuationSource, selection: TSelection, ctx: TContext) => void;
101
89
  export interface TelegramModelSwitchControllerDeps<TContext, TSelection> {
102
90
  isIdle: (ctx: TContext) => boolean;
103
- getPendingModelSwitch: () => TSelection | undefined;
104
- setPendingModelSwitch: (selection: TSelection | undefined) => void;
105
91
  getActiveTurn: () => PendingTelegramTurn | undefined;
106
92
  getAbortHandler: () => (() => void) | undefined;
107
93
  hasAbortHandler: () => boolean;
@@ -111,6 +97,8 @@ export interface TelegramModelSwitchControllerDeps<TContext, TSelection> {
111
97
  }
112
98
  export interface TelegramModelSwitchController<TContext, TSelection> {
113
99
  canOfferInFlightSwitch: (ctx: TContext) => boolean;
100
+ /** A staged switch waits for its abort; queue dispatch and live-work observation treat it as pending control. */
101
+ hasPendingSwitch: () => boolean;
114
102
  stagePendingSwitch: (selection: TSelection, ctx: TContext, continuationTurn?: TelegramModelSwitchContinuationSource) => void;
115
103
  clearPendingSwitch: () => void;
116
104
  queueContinuation: (turn: TelegramModelSwitchContinuationSource, selection: TSelection, ctx: TContext) => void;
@@ -124,4 +112,55 @@ export interface TelegramModelSwitchControllerRuntimeDeps<TContext, TSelection e
124
112
  appendQueuedItem: (item: PendingTelegramTurn, ctx: TContext) => void;
125
113
  }
126
114
  export declare function createTelegramModelSwitchControllerRuntime<TContext, TSelection extends ScopedTelegramModel>(deps: TelegramModelSwitchControllerRuntimeDeps<TContext, TSelection>): TelegramModelSwitchController<TContext, TSelection>;
115
+ /** The whole model-switch state; only `transitionTelegramModelSwitch` produces a new one. */
116
+ export interface TelegramModelSwitchMachineState<TSelection> {
117
+ /** One staged switch: the selection and the turn its continuation answers move and clear together. */
118
+ readonly pending: {
119
+ readonly selection: TSelection;
120
+ /** The active Telegram turn at staging, or the model menu's own target for a local run. */
121
+ readonly turn?: TelegramModelSwitchContinuationSource;
122
+ } | null;
123
+ }
124
+ /** Facts owned by other domains, sampled by the shell when the event is dispatched. */
125
+ interface TelegramModelSwitchRunFacts {
126
+ activeTurn?: TelegramModelSwitchContinuationSource;
127
+ hasAbortHandler: boolean;
128
+ activeToolExecutions: number;
129
+ }
130
+ export type TelegramModelSwitchEvent<TSelection> = {
131
+ type: "staged";
132
+ selection: TSelection;
133
+ turn?: TelegramModelSwitchContinuationSource;
134
+ } | ({
135
+ type: "abortRequested";
136
+ } & TelegramModelSwitchRunFacts) | {
137
+ type: "restartRequested";
138
+ selection: TSelection;
139
+ turn?: TelegramModelSwitchContinuationSource;
140
+ hasAbortHandler: boolean;
141
+ } | {
142
+ type: "cleared";
143
+ };
144
+ export type TelegramModelSwitchEffect<TSelection> = {
145
+ type: "queueContinuation";
146
+ turn: TelegramModelSwitchContinuationSource;
147
+ selection: TSelection;
148
+ } | {
149
+ type: "abort";
150
+ };
151
+ export interface TelegramModelSwitchStep<TSelection> extends Machine.TelegramMachineStep<TelegramModelSwitchMachineState<TSelection>, TelegramModelSwitchEffect<TSelection>> {
152
+ /** `abortRequested` / `restartRequested`: the run was stopped for a continuation. */
153
+ result: boolean;
154
+ }
155
+ export declare function createTelegramModelSwitchMachineState<TSelection>(): TelegramModelSwitchMachineState<TSelection>;
156
+ /**
157
+ * Pure model-switch transition. A staged switch fires only once no tool runs and an abort handler exists; any restart
158
+ * or clear consumes the staged switch too, so a stale selection can never abort the continuation run it started.
159
+ */
160
+ export declare function transitionTelegramModelSwitch<TSelection>(state: TelegramModelSwitchMachineState<TSelection>, event: TelegramModelSwitchEvent<TSelection>): TelegramModelSwitchStep<TSelection>;
161
+ /**
162
+ * The shell around `transitionTelegramModelSwitch`: it samples the active turn, abort handler and tool count into
163
+ * events, and runs the continuation and abort effects with the caller's context and the handler it sampled.
164
+ */
127
165
  export declare function createTelegramModelSwitchController<TContext, TSelection>(deps: TelegramModelSwitchControllerDeps<TContext, TSelection>): TelegramModelSwitchController<TContext, TSelection>;
166
+ export {};
package/dist/lib/model.js CHANGED
@@ -3,6 +3,7 @@
3
3
  * Zones: pi agent model control, telegram controls, queue continuation
4
4
  * Owns model identity, thinking levels, scoped resolution, current-model state, and in-flight model switching
5
5
  */
6
+ import * as Machine from "./machine.js";
6
7
  import { truncateTelegramQueueSummary, } from "./queue.js";
7
8
  import { TELEGRAM_PREFIX } from "./turns.js";
8
9
  export const THINKING_LEVELS = [
@@ -249,19 +250,6 @@ export function sortScopedModels(models, currentModel) {
249
250
  });
250
251
  return sorted;
251
252
  }
252
- export function createPendingModelSwitchStore() {
253
- let selection;
254
- return {
255
- get: () => selection,
256
- set: (nextSelection) => {
257
- selection = nextSelection;
258
- },
259
- clear: () => {
260
- selection = undefined;
261
- },
262
- has: () => selection !== undefined,
263
- };
264
- }
265
253
  export function canRestartAgentRunForTelegramModelSwitch(state) {
266
254
  return !state.isIdle && state.hasAbortHandler;
267
255
  }
@@ -271,13 +259,6 @@ export function shouldTriggerPendingTelegramModelSwitchAbort(state) {
271
259
  state.hasAbortHandler &&
272
260
  state.activeToolExecutions === 0);
273
261
  }
274
- export function restartTelegramModelSwitchContinuation(state) {
275
- if (!state.activeTurn || !state.abort)
276
- return false;
277
- state.queueContinuation(state.activeTurn, state.selection);
278
- state.abort();
279
- return true;
280
- }
281
262
  export function buildTelegramModelSwitchContinuationText(telegramPrefix, model, thinkingLevel) {
282
263
  const modelLabel = `${model.provider}/${model.id}`;
283
264
  const thinkingSuffix = thinkingLevel ? `; thinking: ${thinkingLevel}` : "";
@@ -332,8 +313,6 @@ export function createTelegramModelSwitchContinuationQueueRuntime(deps) {
332
313
  export function createTelegramModelSwitchControllerRuntime(deps) {
333
314
  return createTelegramModelSwitchController({
334
315
  isIdle: deps.isIdle,
335
- getPendingModelSwitch: deps.getPendingModelSwitch,
336
- setPendingModelSwitch: deps.setPendingModelSwitch,
337
316
  getActiveTurn: deps.getActiveTurn,
338
317
  getAbortHandler: deps.getAbortHandler,
339
318
  hasAbortHandler: deps.hasAbortHandler,
@@ -347,36 +326,106 @@ export function createTelegramModelSwitchControllerRuntime(deps) {
347
326
  updateStatus: deps.updateStatus,
348
327
  });
349
328
  }
329
+ export function createTelegramModelSwitchMachineState() {
330
+ return { pending: null };
331
+ }
332
+ /** The continuation is queued before the abort, so the stopped run always has its successor waiting. */
333
+ const restartFor = (turn, selection) => ({
334
+ state: { pending: null },
335
+ effects: [{ type: "queueContinuation", turn, selection }, { type: "abort" }],
336
+ result: true,
337
+ });
338
+ /**
339
+ * Pure model-switch transition. A staged switch fires only once no tool runs and an abort handler exists; any restart
340
+ * or clear consumes the staged switch too, so a stale selection can never abort the continuation run it started.
341
+ */
342
+ export function transitionTelegramModelSwitch(state, event) {
343
+ const unchanged = { state, effects: [], result: false };
344
+ switch (event.type) {
345
+ case "staged":
346
+ return {
347
+ state: {
348
+ pending: {
349
+ selection: event.selection,
350
+ ...(event.turn ? { turn: event.turn } : {}),
351
+ },
352
+ },
353
+ effects: [],
354
+ result: false,
355
+ };
356
+ case "abortRequested": {
357
+ const pending = state.pending;
358
+ const turn = pending?.turn ?? event.activeTurn;
359
+ if (!pending ||
360
+ !turn ||
361
+ !shouldTriggerPendingTelegramModelSwitchAbort({
362
+ hasPendingModelSwitch: true,
363
+ hasContinuationTurn: true,
364
+ hasAbortHandler: event.hasAbortHandler,
365
+ activeToolExecutions: event.activeToolExecutions,
366
+ }))
367
+ return unchanged;
368
+ return restartFor(turn, pending.selection);
369
+ }
370
+ case "restartRequested":
371
+ return event.turn && event.hasAbortHandler
372
+ ? restartFor(event.turn, event.selection)
373
+ : unchanged;
374
+ case "cleared":
375
+ return { state: { pending: null }, effects: [], result: false };
376
+ }
377
+ }
378
+ /**
379
+ * The shell around `transitionTelegramModelSwitch`: it samples the active turn, abort handler and tool count into
380
+ * events, and runs the continuation and abort effects with the caller's context and the handler it sampled.
381
+ */
350
382
  export function createTelegramModelSwitchController(deps) {
351
- let pendingContinuationTurn;
352
- const triggerPendingAbort = (ctx) => {
353
- const turn = pendingContinuationTurn ?? deps.getActiveTurn();
354
- if (!shouldTriggerPendingTelegramModelSwitchAbort({
355
- hasPendingModelSwitch: !!deps.getPendingModelSwitch(),
356
- hasContinuationTurn: !!turn,
357
- hasAbortHandler: deps.hasAbortHandler(),
358
- activeToolExecutions: deps.getActiveToolExecutions(),
359
- })) {
360
- return false;
383
+ // The context and abort handler of the call being dispatched; effects run synchronously inside that dispatch.
384
+ let call;
385
+ const machine = Machine.createTelegramMachineRunner({
386
+ initial: createTelegramModelSwitchMachineState(),
387
+ transition: transitionTelegramModelSwitch,
388
+ run(effect) {
389
+ if (!call)
390
+ return;
391
+ if (effect.type === "queueContinuation")
392
+ deps.queueContinuation(effect.turn, effect.selection, call.ctx);
393
+ else
394
+ call.abort?.();
395
+ },
396
+ });
397
+ const dispatchFor = (ctx, abort, event) => {
398
+ const preceding = call;
399
+ call = { ctx, abort };
400
+ try {
401
+ return machine.dispatch(event).result;
402
+ }
403
+ finally {
404
+ call = preceding;
361
405
  }
362
- const selection = deps.getPendingModelSwitch();
406
+ };
407
+ const triggerPendingAbort = (ctx) => {
363
408
  const abort = deps.getAbortHandler();
364
- if (!selection || !turn || !abort)
365
- return false;
366
- pendingContinuationTurn = undefined;
367
- deps.setPendingModelSwitch(undefined);
368
- deps.queueContinuation(turn, selection, ctx);
369
- abort();
370
- return true;
409
+ return dispatchFor(ctx, abort, {
410
+ type: "abortRequested",
411
+ activeTurn: deps.getActiveTurn(),
412
+ hasAbortHandler: deps.hasAbortHandler() && !!abort,
413
+ activeToolExecutions: deps.getActiveToolExecutions(),
414
+ });
371
415
  };
372
416
  return {
373
417
  canOfferInFlightSwitch: (ctx) => canRestartAgentRunForTelegramModelSwitch({
374
418
  isIdle: deps.isIdle(ctx),
375
419
  hasAbortHandler: deps.hasAbortHandler(),
376
420
  }),
421
+ hasPendingSwitch: () => machine.state.pending !== null,
377
422
  stagePendingSwitch: (selection, ctx, continuationTurn) => {
378
- pendingContinuationTurn = deps.getActiveTurn() ?? continuationTurn;
379
- deps.setPendingModelSwitch(selection);
423
+ const turn = deps.getActiveTurn() ?? continuationTurn;
424
+ machine.dispatch({
425
+ type: "staged",
426
+ selection,
427
+ ...(turn ? { turn } : {}),
428
+ });
380
429
  try {
381
430
  deps.updateStatus(ctx);
382
431
  }
@@ -385,23 +434,19 @@ export function createTelegramModelSwitchController(deps) {
385
434
  }
386
435
  },
387
436
  clearPendingSwitch: () => {
388
- pendingContinuationTurn = undefined;
389
- deps.setPendingModelSwitch(undefined);
437
+ machine.dispatch({ type: "cleared" });
390
438
  },
391
439
  queueContinuation: deps.queueContinuation,
392
440
  triggerPendingAbort,
393
441
  restartInterruptedTurn: (selection, ctx, continuationTurn) => {
394
- const restarted = restartTelegramModelSwitchContinuation({
395
- activeTurn: deps.getActiveTurn() ?? continuationTurn,
396
- abort: deps.getAbortHandler(),
442
+ const abort = deps.getAbortHandler();
443
+ const turn = deps.getActiveTurn() ?? continuationTurn;
444
+ return dispatchFor(ctx, abort, {
445
+ type: "restartRequested",
397
446
  selection,
398
- queueContinuation: (turn, nextSelection) => {
399
- deps.queueContinuation(turn, nextSelection, ctx);
400
- },
447
+ ...(turn ? { turn } : {}),
448
+ hasAbortHandler: !!abort,
401
449
  });
402
- if (restarted)
403
- pendingContinuationTurn = undefined;
404
- return restarted;
405
450
  },
406
451
  };
407
452
  }
@@ -893,6 +893,8 @@ export function createTelegramThreadCapabilityMonitor(deps) {
893
893
  return {
894
894
  start(ctx) {
895
895
  stop();
896
+ // Disabled-mode confirmation counts consecutive probes of one run, never observations from before a restart.
897
+ consecutiveDisabledProbes = 0;
896
898
  const expectedGeneration = generation;
897
899
  interval = setInterval(() => {
898
900
  if (generation !== expectedGeneration)
@@ -2,42 +2,28 @@
2
2
  * Telegram preview streaming helpers
3
3
  * Zones: telegram outbound, native rich markdown drafts
4
4
  * Owns safe draft preview selection, runtime updates, and preview finalization
5
+ * The streaming state machine is a pure transition function driven by one imperative shell per runtime.
5
6
  */
6
7
  import type { TelegramAssistantOutputPreparation } from "./activity.ts";
7
8
  import { type TelegramTarget } from "./target.ts";
8
- export type TelegramDraftSupport = "unknown" | "supported";
9
9
  export interface TelegramPreviewState {
10
10
  mode: "draft";
11
11
  draftId?: number;
12
12
  pendingText: string;
13
13
  lastSentText: string;
14
14
  }
15
- export interface TelegramPreviewRuntimeState extends TelegramPreviewState {
16
- flushPromise?: Promise<void>;
17
- flushRequested?: boolean;
18
- precedingFlush?: Promise<void>;
19
- publicationPromise?: Promise<void>;
20
- sealed?: boolean;
21
- nextDraftAt?: number;
22
- flushTimer?: ReturnType<typeof setTimeout>;
15
+ /** One immutable preview segment; a replacement segment gets a new id, so late results for an old one are dropped. */
16
+ export interface TelegramPreviewSegment extends Readonly<TelegramPreviewState> {
17
+ readonly id: number;
18
+ readonly sealed: boolean;
23
19
  }
24
20
  export type TelegramPreviewReplyMarkup = unknown;
25
- export interface TelegramPreviewRuntimeDeps {
26
- getState: () => TelegramPreviewRuntimeState | undefined;
27
- setState: (state: TelegramPreviewRuntimeState | undefined) => void;
28
- maxMessageLength: number;
29
- minDraftIntervalMs?: number;
30
- getDraftSupport: () => TelegramDraftSupport;
31
- setDraftSupport: (support: TelegramDraftSupport) => void;
32
- allocateDraftId: () => number;
33
- sendDraft: (chatId: number, draftId: number, text?: string, options?: {
34
- parse_mode?: string;
35
- entities?: unknown[];
36
- message_thread_id?: number;
37
- }) => Promise<unknown>;
38
- canSend?: () => boolean;
39
- recordRuntimeEvent?: (category: string, error: unknown, details?: Record<string, unknown>) => void;
40
- }
21
+ type TelegramDraftSender = (chatId: number, draftId: number, text?: string, options?: {
22
+ parse_mode?: string;
23
+ entities?: unknown[];
24
+ message_thread_id?: number;
25
+ }) => Promise<unknown>;
26
+ type TelegramPreviewEventRecorder = (category: string, error: unknown, details?: Record<string, unknown>) => void;
41
27
  export interface TelegramPreviewActiveTurn {
42
28
  chatId: number;
43
29
  replyToMessageId?: number;
@@ -46,28 +32,6 @@ export interface TelegramPreviewActiveTurn {
46
32
  voiceReplyRequired?: boolean;
47
33
  guestQueryId?: string;
48
34
  }
49
- export interface TelegramAssistantMessagePreviewStartDeps<TMessage> {
50
- getActiveTurn: () => TelegramPreviewActiveTurn | undefined;
51
- isAssistantMessage: (message: TMessage) => boolean;
52
- getState: () => TelegramPreviewRuntimeState | undefined;
53
- setState: (state: TelegramPreviewRuntimeState | undefined) => void;
54
- createPreviewState: () => TelegramPreviewRuntimeState;
55
- canSend?: () => boolean;
56
- }
57
- export interface TelegramAssistantMessagePreviewUpdateDeps<TMessage> {
58
- getActiveTurn: () => TelegramPreviewActiveTurn | undefined;
59
- isAssistantMessage: (message: TMessage) => boolean;
60
- getState: () => TelegramPreviewRuntimeState | undefined;
61
- setState: (state: TelegramPreviewRuntimeState | undefined) => void;
62
- createPreviewState: () => TelegramPreviewRuntimeState;
63
- canSend?: () => boolean;
64
- getMessageText: (message: TMessage) => string;
65
- minDraftIntervalMs?: number;
66
- schedulePreviewFlush: (chatId: number, options?: {
67
- target?: TelegramTarget;
68
- }) => void;
69
- }
70
- export type TelegramAssistantMessagePreviewHookDeps<TMessage> = TelegramAssistantMessagePreviewStartDeps<TMessage> & TelegramAssistantMessagePreviewUpdateDeps<TMessage>;
71
35
  export interface TelegramAssistantMessagePreviewHookEvent<TMessage> {
72
36
  message: TMessage;
73
37
  }
@@ -78,46 +42,37 @@ export interface TelegramAssistantMessagePreviewHooks<TMessage> {
78
42
  export interface TelegramPreviewControllerDeps {
79
43
  getDefaultReplyToMessageId?: () => number | undefined;
80
44
  maxMessageLength?: number;
81
- initialDraftSupport?: TelegramDraftSupport;
82
- sendDraft: (chatId: number, draftId: number, text?: string, options?: {
83
- parse_mode?: string;
84
- entities?: unknown[];
85
- message_thread_id?: number;
86
- }) => Promise<unknown>;
45
+ sendDraft: TelegramDraftSender;
87
46
  canSend?: () => boolean;
88
47
  maxDraftId?: number;
89
- recordRuntimeEvent?: (category: string, error: unknown, details?: Record<string, unknown>) => void;
48
+ /** Throttle between draft frames; defaults to the native draft cadence. */
49
+ minDraftIntervalMs?: number;
50
+ recordRuntimeEvent?: TelegramPreviewEventRecorder;
90
51
  }
52
+ type TelegramPreviewTargetOptions = {
53
+ target?: TelegramTarget;
54
+ };
91
55
  export interface TelegramPreviewController {
56
+ /** Read-only view of the current segment; it never grants mutation. */
57
+ getState: () => TelegramPreviewSegment | undefined;
58
+ /** True while a draft request is in flight or a pending request waits behind a publication. */
59
+ isFlushing: () => boolean;
92
60
  seal: () => void;
93
61
  preparePublication: () => TelegramAssistantOutputPreparation | undefined;
94
- prepareClear: (chatId: number, options?: {
95
- target?: TelegramTarget;
62
+ prepareClear: (chatId: number, options?: TelegramPreviewTargetOptions & {
96
63
  isDeliveryActive?: () => boolean;
97
64
  }) => () => Promise<void>;
98
- getState: () => TelegramPreviewRuntimeState | undefined;
99
- setState: (state: TelegramPreviewRuntimeState | undefined) => void;
100
65
  setPendingText: (text: string) => void;
101
- createState: () => TelegramPreviewRuntimeState;
102
66
  resetState: () => void;
103
67
  invalidate: () => void;
104
- clear: (chatId: number, options?: {
68
+ clear: (chatId: number, options?: TelegramPreviewTargetOptions & {
105
69
  awaitFlush?: boolean;
106
- target?: TelegramTarget;
107
- }) => Promise<void>;
108
- flush: (chatId: number, options?: {
109
- target?: TelegramTarget;
110
70
  }) => Promise<void>;
111
- scheduleFlush: (chatId: number, options?: {
112
- target?: TelegramTarget;
113
- }) => void;
114
- finalize: (chatId: number, replyToMessageId?: number, options?: {
115
- target?: TelegramTarget;
116
- }) => Promise<boolean>;
71
+ flush: (chatId: number, options?: TelegramPreviewTargetOptions) => Promise<void>;
72
+ scheduleFlush: (chatId: number, options?: TelegramPreviewTargetOptions) => void;
73
+ finalize: (chatId: number, replyToMessageId?: number, options?: TelegramPreviewTargetOptions) => Promise<boolean>;
117
74
  }
118
- export type TelegramPreviewControllerRuntimeDeps = TelegramPreviewControllerDeps;
119
- export declare function createTelegramPreviewControllerRuntime(deps: TelegramPreviewControllerRuntimeDeps): TelegramPreviewController;
120
- export interface TelegramAssistantPreviewRuntimeDeps<TMessage, TReplyMarkup = TelegramPreviewReplyMarkup> extends TelegramPreviewControllerRuntimeDeps {
75
+ export interface TelegramAssistantPreviewRuntimeDeps<TMessage, TReplyMarkup = TelegramPreviewReplyMarkup> extends TelegramPreviewControllerDeps {
121
76
  getActiveTurn: () => TelegramPreviewActiveTurn | undefined;
122
77
  isAssistantMessage: (message: TMessage) => boolean;
123
78
  getMessageText: (message: TMessage) => string;
@@ -126,35 +81,113 @@ export interface TelegramAssistantPreviewRuntimeDeps<TMessage, TReplyMarkup = Te
126
81
  target?: TelegramTarget;
127
82
  }) => Promise<number | undefined>;
128
83
  }
84
+ type TelegramMarkdownFinalizer<TReplyMarkup> = (chatId: number, markdown: string, replyToMessageId?: number, options?: {
85
+ replyMarkup?: TReplyMarkup;
86
+ target?: TelegramTarget;
87
+ }) => Promise<boolean>;
129
88
  export interface TelegramPreparedPreviewDelivery<TReplyMarkup = unknown> {
130
89
  clearPreview: TelegramPreviewController["clear"];
131
90
  setPreviewPendingText: TelegramPreviewController["setPendingText"];
132
- finalizeMarkdownPreview: ReturnType<typeof createTelegramNativeMarkdownPreviewFinalizer<TReplyMarkup>>;
91
+ finalizeMarkdownPreview: TelegramMarkdownFinalizer<TReplyMarkup>;
133
92
  }
134
93
  export type TelegramAssistantPreviewRuntime<TMessage, TReplyMarkup = TelegramPreviewReplyMarkup> = TelegramPreviewController & TelegramAssistantMessagePreviewHooks<TMessage> & {
135
94
  prepareDelivery: (isDeliveryActive: () => boolean) => TelegramPreparedPreviewDelivery<TReplyMarkup>;
136
- finalizeMarkdown: (chatId: number, markdown: string, replyToMessageId?: number, options?: {
137
- replyMarkup?: TReplyMarkup;
138
- target?: TelegramTarget;
139
- }) => Promise<boolean>;
95
+ finalizeMarkdown: TelegramMarkdownFinalizer<TReplyMarkup>;
140
96
  };
141
- export declare function createTelegramNativeMarkdownPreviewFinalizer<TReplyMarkup>(deps: {
142
- getState: () => TelegramPreviewRuntimeState | undefined;
143
- clear: (chatId: number, options?: {
144
- awaitFlush?: boolean;
145
- target?: TelegramTarget;
146
- }) => Promise<void>;
147
- discard?: () => void;
148
- isDeliveryActive?: () => boolean;
149
- sendMarkdownReply: (chatId: number, replyToMessageId: number | undefined, markdown: string, options?: {
150
- replyMarkup?: TReplyMarkup;
151
- target?: TelegramTarget;
152
- }) => Promise<number | undefined>;
153
- }): (chatId: number, markdown: string, replyToMessageId?: number, options?: {
154
- replyMarkup?: TReplyMarkup;
97
+ interface TelegramPreviewFlushRequest {
98
+ chatId: number;
155
99
  target?: TelegramTarget;
156
- }) => Promise<boolean>;
157
- export declare function createTelegramAssistantPreviewRuntime<TMessage, TReplyMarkup = TelegramPreviewReplyMarkup>(deps: TelegramAssistantPreviewRuntimeDeps<TMessage, TReplyMarkup>): TelegramAssistantPreviewRuntime<TMessage, TReplyMarkup>;
100
+ }
101
+ interface TelegramPreviewMachineConfig {
102
+ maxDraftId: number;
103
+ maxMessageLength: number;
104
+ minDraftIntervalMs: number;
105
+ }
106
+ /** The whole preview state; only `transitionTelegramPreview` produces a new one. */
107
+ export interface TelegramPreviewMachineState {
108
+ readonly segment: TelegramPreviewSegment | null;
109
+ /** Bumped by reset and invalidation; a result issued under an older generation is inert. */
110
+ readonly generation: number;
111
+ readonly nextSegmentId: number;
112
+ readonly nextDraftId: number;
113
+ readonly nextDraftAt: number;
114
+ /** Segment that owns the current generation's issued draft; old-generation requests do not block it. */
115
+ readonly inFlight: number | null;
116
+ /** The one coalesced flush request waiting behind an in-flight draft or an open publication. */
117
+ readonly pending: TelegramPreviewFlushRequest | null;
118
+ readonly publications: readonly number[];
119
+ /** The segment the throttle timer is armed for; the timer belongs to that segment alone. */
120
+ readonly timerSegmentId: number | null;
121
+ }
122
+ /** Impure inputs the runner samples at dispatch time, so transitions stay pure. */
123
+ interface TelegramPreviewClock {
124
+ now: number;
125
+ canSend: boolean;
126
+ }
127
+ export type TelegramPreviewEvent = {
128
+ type: "reset";
129
+ } | {
130
+ type: "invalidated";
131
+ } | {
132
+ type: "segmentStarted";
133
+ } | {
134
+ type: "segmentEnsured";
135
+ } | {
136
+ type: "sealed";
137
+ } | {
138
+ type: "discarded";
139
+ } | {
140
+ type: "textSet";
141
+ text: string;
142
+ } | {
143
+ type: "draftWindowOpened";
144
+ at: number;
145
+ } | ({
146
+ type: "flushRequested";
147
+ request: TelegramPreviewFlushRequest;
148
+ } & TelegramPreviewClock) | ({
149
+ type: "timerFired";
150
+ request: TelegramPreviewFlushRequest;
151
+ segmentId: number;
152
+ } & TelegramPreviewClock) | ({
153
+ type: "draftSettled";
154
+ segmentId: number;
155
+ generation: number;
156
+ /** Present only when Telegram accepted the draft. */
157
+ text?: string;
158
+ } & TelegramPreviewClock) | {
159
+ type: "publicationOpened";
160
+ id: number;
161
+ } | ({
162
+ type: "publicationSettled";
163
+ id: number;
164
+ } & TelegramPreviewClock);
165
+ export type TelegramPreviewEffect = {
166
+ type: "sendDraft";
167
+ request: TelegramPreviewFlushRequest;
168
+ draftId: number;
169
+ text: string;
170
+ segmentId: number;
171
+ generation: number;
172
+ } | {
173
+ type: "armTimer";
174
+ delay: number;
175
+ request: TelegramPreviewFlushRequest;
176
+ segmentId: number;
177
+ } | {
178
+ type: "cancelTimer";
179
+ } | {
180
+ type: "clearClosedGate";
181
+ request: TelegramPreviewFlushRequest;
182
+ };
183
+ export interface TelegramPreviewStep {
184
+ state: TelegramPreviewMachineState;
185
+ effects: readonly TelegramPreviewEffect[];
186
+ }
187
+ export declare function createTelegramPreviewMachineState(): TelegramPreviewMachineState;
188
+ export declare function transitionTelegramPreview(state: TelegramPreviewMachineState, event: TelegramPreviewEvent, config: TelegramPreviewMachineConfig): TelegramPreviewStep;
189
+ /** True while a draft request is in flight, or, when publications block, a pending request waits behind one. */
190
+ export declare function isTelegramPreviewBusy(state: TelegramPreviewMachineState, blockedByPublication?: boolean): boolean;
158
191
  export declare function createTelegramPreviewController(deps: TelegramPreviewControllerDeps): TelegramPreviewController;
159
192
  /**
160
193
  * Returns true when the active turn is a Telegram Guest Mode query. A guest
@@ -164,26 +197,8 @@ export declare function createTelegramPreviewController(deps: TelegramPreviewCon
164
197
  export declare function shouldSuppressPreviewForGuestTurn(turn: {
165
198
  guestQueryId?: string;
166
199
  } | null | undefined): boolean;
200
+ export declare function createTelegramAssistantPreviewRuntime<TMessage, TReplyMarkup = TelegramPreviewReplyMarkup>(deps: TelegramAssistantPreviewRuntimeDeps<TMessage, TReplyMarkup>): TelegramAssistantPreviewRuntime<TMessage, TReplyMarkup>;
167
201
  export declare function buildTelegramPreviewFinalText(state: TelegramPreviewState): string | undefined;
168
- export declare function createTelegramPreviewRuntimeState(): TelegramPreviewRuntimeState;
169
202
  export declare function allocateTelegramDraftId(currentDraftId: number, maxDraftId: number): number;
170
- interface TelegramNativeMarkdownPreviewSnapshot {
171
- text: string;
172
- }
173
- export declare function shouldUseTelegramDraftPreview(_options: {
174
- draftSupport: TelegramDraftSupport;
175
- snapshot?: TelegramNativeMarkdownPreviewSnapshot;
176
- }): boolean;
177
- export declare function clearTelegramPreview(chatId: number, deps: TelegramPreviewRuntimeDeps, options?: {
178
- awaitFlush?: boolean;
179
- target?: TelegramTarget;
180
- isDeliveryActive?: () => boolean;
181
- }): Promise<void>;
182
203
  export declare function getSafeTelegramRichMarkdownDraftPrefix(markdown: string, maxMessageLength: number): string | undefined;
183
- export declare function flushTelegramPreview(chatId: number, deps: TelegramPreviewRuntimeDeps, options?: {
184
- target?: TelegramTarget;
185
- }): Promise<void>;
186
- export declare function finalizeTelegramPreview(chatId: number, deps: TelegramPreviewRuntimeDeps, options?: {
187
- target?: TelegramTarget;
188
- }): Promise<boolean>;
189
204
  export {};