@llblab/pi-telegram 0.21.0 → 0.22.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.
package/lib/config.ts CHANGED
@@ -4,12 +4,22 @@
4
4
  * Owns persisted bot/session pairing state, local config storage, live config controls, authorization policy, and first-user pairing side effects
5
5
  */
6
6
 
7
- import { existsSync } from "node:fs";
8
- import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
7
+ import { randomUUID } from "node:crypto";
8
+ import {
9
+ chmodSync,
10
+ existsSync,
11
+ mkdirSync,
12
+ readFileSync,
13
+ renameSync,
14
+ statSync,
15
+ writeFileSync,
16
+ } from "node:fs";
17
+ import { chmod, mkdir, rename, writeFile } from "node:fs/promises";
9
18
  import { resolveAgentDir, resolveTelegramConfigPath } from "./paths.ts";
10
19
 
11
20
  import type { TelegramInboundHandlerConfig } from "./inbound.ts";
12
21
  import type { CommandTemplateObjectConfig } from "./command-templates.ts";
22
+ import { withTelegramFileTransaction } from "./locks.ts";
13
23
 
14
24
  const CONFIG_RUNTIME_KEY = "__piTelegramConfigRuntime__";
15
25
 
@@ -136,9 +146,7 @@ export function resolveTelegramActiveProfile(
136
146
  }
137
147
 
138
148
  /** List defined profile names. */
139
- export function getTelegramProfileNames(
140
- config: TelegramConfig,
141
- ): string[] {
149
+ export function getTelegramProfileNames(config: TelegramConfig): string[] {
142
150
  return Object.keys(config.profiles ?? {}).sort();
143
151
  }
144
152
 
@@ -251,16 +259,31 @@ export async function readTelegramConfig(
251
259
  onInvalidConfig?: (recovery: TelegramInvalidConfigRecovery) => void;
252
260
  } = {},
253
261
  ): Promise<TelegramConfig> {
254
- if (!existsSync(configPath)) return {};
255
- const content = await readFile(configPath, "utf8");
256
- try {
257
- return JSON.parse(content) as TelegramConfig;
258
- } catch (error) {
259
- const recoveryPath = getInvalidTelegramConfigRecoveryPath(configPath);
260
- await rename(configPath, recoveryPath);
261
- options.onInvalidConfig?.({ configPath, recoveryPath, error });
262
- return {};
263
- }
262
+ return withTelegramFileTransaction(`${configPath}.transaction`, () => {
263
+ if (!existsSync(configPath)) return {};
264
+ const identity = statSync(configPath);
265
+ const content = readFileSync(configPath, "utf8");
266
+ try {
267
+ return JSON.parse(content) as TelegramConfig;
268
+ } catch (error) {
269
+ const currentIdentity = statSync(configPath);
270
+ if (
271
+ currentIdentity.dev !== identity.dev ||
272
+ currentIdentity.ino !== identity.ino ||
273
+ currentIdentity.size !== identity.size ||
274
+ currentIdentity.mtimeMs !== identity.mtimeMs
275
+ ) {
276
+ throw new Error(
277
+ `Telegram config changed while validating invalid content: ${configPath}`,
278
+ { cause: error },
279
+ );
280
+ }
281
+ const recoveryPath = getInvalidTelegramConfigRecoveryPath(configPath);
282
+ renameSync(configPath, recoveryPath);
283
+ options.onInvalidConfig?.({ configPath, recoveryPath, error });
284
+ return {};
285
+ }
286
+ });
264
287
  }
265
288
 
266
289
  export async function writeTelegramConfig(
@@ -279,7 +302,88 @@ export async function writeTelegramConfig(
279
302
  await chmod(configPath, 0o600);
280
303
  }
281
304
 
282
- function getTelegramProfileFields(config: TelegramConfig): TelegramBotProfile | undefined {
305
+ function isPlainConfigRecord(value: unknown): value is Record<string, unknown> {
306
+ return !!value && typeof value === "object" && !Array.isArray(value);
307
+ }
308
+
309
+ function cloneTelegramConfig<T>(value: T): T {
310
+ return structuredClone(value);
311
+ }
312
+
313
+ function configValuesEqual(left: unknown, right: unknown): boolean {
314
+ return JSON.stringify(left) === JSON.stringify(right);
315
+ }
316
+
317
+ function mergeTelegramConfigDelta(
318
+ base: Record<string, unknown>,
319
+ desired: Record<string, unknown>,
320
+ latest: Record<string, unknown>,
321
+ ): Record<string, unknown> {
322
+ const merged = cloneTelegramConfig(latest);
323
+ for (const key of new Set([...Object.keys(base), ...Object.keys(desired)])) {
324
+ const baseHas = Object.hasOwn(base, key);
325
+ const desiredHas = Object.hasOwn(desired, key);
326
+ const baseValue = base[key];
327
+ const desiredValue = desired[key];
328
+ if (baseHas === desiredHas && configValuesEqual(baseValue, desiredValue)) {
329
+ continue;
330
+ }
331
+ if (!desiredHas) {
332
+ if (key !== "lastUpdateId") delete merged[key];
333
+ continue;
334
+ }
335
+ if (
336
+ key === "lastUpdateId" &&
337
+ typeof desiredValue === "number" &&
338
+ typeof merged[key] === "number"
339
+ ) {
340
+ merged[key] = Math.max(desiredValue, merged[key] as number);
341
+ continue;
342
+ }
343
+ if (
344
+ isPlainConfigRecord(desiredValue) &&
345
+ (!baseHas || isPlainConfigRecord(baseValue))
346
+ ) {
347
+ merged[key] = mergeTelegramConfigDelta(
348
+ isPlainConfigRecord(baseValue) ? baseValue : {},
349
+ desiredValue,
350
+ isPlainConfigRecord(merged[key]) ? merged[key] : {},
351
+ );
352
+ continue;
353
+ }
354
+ merged[key] = cloneTelegramConfig(desiredValue);
355
+ }
356
+ return merged;
357
+ }
358
+
359
+ function readTelegramConfigForTransaction(configPath: string): TelegramConfig {
360
+ if (!existsSync(configPath)) return {};
361
+ const parsed: unknown = JSON.parse(readFileSync(configPath, "utf8"));
362
+ if (!isPlainConfigRecord(parsed)) {
363
+ throw new Error(`Invalid Telegram config object: ${configPath}`);
364
+ }
365
+ return parsed as TelegramConfig;
366
+ }
367
+
368
+ function writeTelegramConfigInTransaction(
369
+ agentDir: string,
370
+ configPath: string,
371
+ config: TelegramConfig,
372
+ ): void {
373
+ mkdirSync(agentDir, { recursive: true, mode: 0o700 });
374
+ const tempConfigPath = `${configPath}.tmp-${process.pid}-${randomUUID()}`;
375
+ writeFileSync(tempConfigPath, `${JSON.stringify(config, null, "\t")}\n`, {
376
+ encoding: "utf8",
377
+ mode: 0o600,
378
+ });
379
+ chmodSync(tempConfigPath, 0o600);
380
+ renameSync(tempConfigPath, configPath);
381
+ chmodSync(configPath, 0o600);
382
+ }
383
+
384
+ export function getTelegramProfileFields(
385
+ config: TelegramConfig,
386
+ ): TelegramBotProfile | undefined {
283
387
  const token = config.botToken?.trim();
284
388
  if (!token) return undefined;
285
389
  return {
@@ -332,13 +436,22 @@ function storeTelegramEffectiveConfig(
332
436
  export function createTelegramConfigStore(
333
437
  options: TelegramConfigStoreOptions = {},
334
438
  ): TelegramConfigStore {
335
- let config: TelegramConfig = options.initialConfig ?? {};
439
+ let config: TelegramConfig = cloneTelegramConfig(options.initialConfig ?? {});
440
+ let persistedConfig: TelegramConfig = {};
441
+ let mutationVersion = 0;
442
+ let persistQueue: Promise<void> = Promise.resolve();
336
443
  let activeProfileName: string | undefined;
337
444
  const agentDir = options.agentDir ?? resolveAgentDir();
338
445
  const configPath = options.configPath ?? getConfigPath();
339
- const getEffectiveConfig = () => applyTelegramProfile(config, activeProfileName);
446
+ const getEffectiveConfig = () =>
447
+ applyTelegramProfile(config, activeProfileName);
340
448
  const setEffectiveConfig = (nextConfig: TelegramConfig) => {
341
- config = storeTelegramEffectiveConfig(config, nextConfig, activeProfileName);
449
+ config = storeTelegramEffectiveConfig(
450
+ config,
451
+ nextConfig,
452
+ activeProfileName,
453
+ );
454
+ mutationVersion += 1;
342
455
  };
343
456
  return {
344
457
  get: getEffectiveConfig,
@@ -379,18 +492,45 @@ export function createTelegramConfigStore(
379
492
  });
380
493
  },
381
494
  });
382
- if (activeProfileName && !config.profiles?.[activeProfileName]) {
383
- activeProfileName = undefined;
384
- }
495
+ persistedConfig = cloneTelegramConfig(config);
496
+ mutationVersion += 1;
385
497
  },
386
- persist: async (nextConfig = getEffectiveConfig()) => {
387
- const storedConfig = storeTelegramEffectiveConfig(
498
+ persist: (nextConfig = getEffectiveConfig()) => {
499
+ const profileName = activeProfileName;
500
+ const desiredConfig = storeTelegramEffectiveConfig(
388
501
  config,
389
- nextConfig,
390
- activeProfileName,
502
+ cloneTelegramConfig(nextConfig),
503
+ profileName,
391
504
  );
392
- config = storedConfig;
393
- await writeTelegramConfig(agentDir, configPath, storedConfig);
505
+ const baseConfig = cloneTelegramConfig(persistedConfig);
506
+ const capturedMutationVersion = mutationVersion;
507
+ const persist = persistQueue.then(() => {
508
+ const mergedConfig = withTelegramFileTransaction(
509
+ `${configPath}.transaction`,
510
+ () => {
511
+ const latestConfig = readTelegramConfigForTransaction(configPath);
512
+ const merged = mergeTelegramConfigDelta(
513
+ baseConfig as Record<string, unknown>,
514
+ desiredConfig as Record<string, unknown>,
515
+ latestConfig as Record<string, unknown>,
516
+ ) as TelegramConfig;
517
+ writeTelegramConfigInTransaction(agentDir, configPath, merged);
518
+ return merged;
519
+ },
520
+ );
521
+ persistedConfig = cloneTelegramConfig(mergedConfig);
522
+ if (mutationVersion === capturedMutationVersion) {
523
+ config = cloneTelegramConfig(mergedConfig);
524
+ } else {
525
+ config = mergeTelegramConfigDelta(
526
+ baseConfig as Record<string, unknown>,
527
+ config as Record<string, unknown>,
528
+ mergedConfig as Record<string, unknown>,
529
+ ) as TelegramConfig;
530
+ }
531
+ });
532
+ persistQueue = persist.catch(() => undefined);
533
+ return persist;
394
534
  },
395
535
  };
396
536
  }
package/lib/delivery.ts CHANGED
@@ -17,10 +17,20 @@ import {
17
17
  getTelegramTargetThreadParams,
18
18
  type TelegramTarget,
19
19
  } from "./target.ts";
20
- import type { TelegramBridgeApiRuntime } from "./telegram-api.ts";
20
+ import {
21
+ isTelegramApiCommitUnknownError,
22
+ type TelegramBridgeApiRuntime,
23
+ } from "./telegram-api.ts";
21
24
 
22
25
  const TELEGRAM_DELIVERY_RUNTIME_KEY = "__piTelegramDeliveryRuntime__";
23
26
 
27
+ class TelegramDeliveryTransportGenerationError extends Error {
28
+ constructor() {
29
+ super("Telegram Delivery transport generation is no longer active.");
30
+ this.name = "TelegramDeliveryTransportGenerationError";
31
+ }
32
+ }
33
+
24
34
  export type TelegramDeliveryParseMode = "plain" | "html" | "markdown";
25
35
 
26
36
  export interface TelegramDeliveryView {
@@ -49,6 +59,7 @@ export type TelegramDeliveryFailureReason =
49
59
  | "target-unauthorized"
50
60
  | "stale-handle"
51
61
  | "invalid-view"
62
+ | "commit-unknown"
52
63
  | "transport-failed";
53
64
 
54
65
  export type TelegramDeliveryResult<T> =
@@ -67,10 +78,7 @@ export interface SendTelegramViewOptions {
67
78
  }
68
79
 
69
80
  export type TelegramDeliveryChatAction =
70
- | "typing"
71
- | "upload_document"
72
- | "upload_photo"
73
- | "record_voice";
81
+ "typing" | "upload_document" | "upload_photo" | "record_voice";
74
82
 
75
83
  /** @internal */
76
84
  export interface TelegramDeliveryRuntime {
@@ -114,8 +122,7 @@ export interface TelegramDeliveryTransportOptions {
114
122
  }
115
123
 
116
124
  /** @internal */
117
- export interface TelegramDeliveryRuntimeDeps
118
- extends TelegramDeliveryTargetResolverDeps {
125
+ export interface TelegramDeliveryRuntimeDeps extends TelegramDeliveryTargetResolverDeps {
119
126
  generation: string;
120
127
  renderView: (
121
128
  view: TelegramDeliveryView,
@@ -151,6 +158,7 @@ export interface TelegramBridgeDeliveryRuntimeDeps {
151
158
  generation: string;
152
159
  getTargetPolicyView: () => TelegramDeliveryTargetPolicyView;
153
160
  getActiveTurnTarget: () => TelegramDeliveryTarget | undefined;
161
+ isTransportActive?: () => boolean;
154
162
  api: Pick<
155
163
  TelegramBridgeApiRuntime,
156
164
  "sendMessage" | "editMessageText" | "deleteMessage" | "sendChatAction"
@@ -191,18 +199,28 @@ export function createTelegramDeliveryLifecycleHooks(
191
199
  }
192
200
 
193
201
  /** @internal */
194
- export function createTelegramBridgeDeliveryLifecycleHooks(
195
- deps: Omit<TelegramBridgeDeliveryRuntimeDeps, "generation"> & {
202
+ export function createTelegramBridgeDeliveryLifecycleHooks<TTransportStamp>(
203
+ deps: Omit<
204
+ TelegramBridgeDeliveryRuntimeDeps,
205
+ "generation" | "isTransportActive"
206
+ > & {
196
207
  generationSeed: string;
208
+ getTransportStamp?: () => TTransportStamp;
209
+ isTransportStampActive?: (stamp: TTransportStamp) => boolean;
197
210
  },
198
211
  ): ReturnType<typeof createTelegramDeliveryLifecycleHooks> {
199
212
  let generationSequence = 0;
200
- return createTelegramDeliveryLifecycleHooks(() =>
201
- createTelegramBridgeDeliveryRuntime({
213
+ return createTelegramDeliveryLifecycleHooks(() => {
214
+ const transportStamp = deps.getTransportStamp?.();
215
+ return createTelegramBridgeDeliveryRuntime({
202
216
  ...deps,
203
217
  generation: `${deps.generationSeed}:${++generationSequence}`,
204
- }),
205
- );
218
+ isTransportActive:
219
+ transportStamp !== undefined && deps.isTransportStampActive
220
+ ? () => deps.isTransportStampActive?.(transportStamp) ?? false
221
+ : undefined,
222
+ });
223
+ });
206
224
  }
207
225
 
208
226
  interface TelegramDeliveryRuntimeRegistry {
@@ -212,11 +230,7 @@ interface TelegramDeliveryRuntimeRegistry {
212
230
  function getTelegramDeliveryRuntimeRegistry(): TelegramDeliveryRuntimeRegistry {
213
231
  const globals = globalThis as Record<string, unknown>;
214
232
  const existing = globals[TELEGRAM_DELIVERY_RUNTIME_KEY];
215
- if (
216
- existing &&
217
- typeof existing === "object" &&
218
- "runtime" in existing
219
- ) {
233
+ if (existing && typeof existing === "object" && "runtime" in existing) {
220
234
  return existing as TelegramDeliveryRuntimeRegistry;
221
235
  }
222
236
  const registry: TelegramDeliveryRuntimeRegistry = {};
@@ -244,6 +258,43 @@ export interface TelegramDeliveryTargetPolicyView {
244
258
  liveTargets?: readonly TelegramDeliveryTarget[];
245
259
  }
246
260
 
261
+ /** @internal */
262
+ export interface TelegramDeliveryTargetPolicyRuntime {
263
+ getTargetPolicyView(): TelegramDeliveryTargetPolicyView;
264
+ getActiveTurnTarget(): TelegramDeliveryTarget | undefined;
265
+ }
266
+
267
+ /** @internal */
268
+ export function createTelegramDeliveryTargetPolicyRuntime(deps: {
269
+ ownsDirect(): boolean;
270
+ isFollowerRegistered(): boolean;
271
+ getAllowedChatId(): number | undefined;
272
+ getFollowerTarget(): TelegramDeliveryTarget | undefined;
273
+ getLeaderTarget(): TelegramDeliveryTarget | undefined;
274
+ listThreadRecords(): readonly { target: TelegramDeliveryTarget }[];
275
+ getActiveTurnTarget(): TelegramDeliveryTarget | undefined;
276
+ getActiveGuestQueryId(): string | undefined;
277
+ }): TelegramDeliveryTargetPolicyRuntime {
278
+ return {
279
+ getTargetPolicyView() {
280
+ const ownsDirect = deps.ownsDirect();
281
+ return {
282
+ canDeliver: ownsDirect || deps.isFollowerRegistered(),
283
+ ownsDirect,
284
+ allowedChatId: deps.getAllowedChatId(),
285
+ followerTarget: deps.getFollowerTarget(),
286
+ leaderTarget: deps.getLeaderTarget(),
287
+ liveTargets: deps.listThreadRecords().map((record) => record.target),
288
+ };
289
+ },
290
+ getActiveTurnTarget() {
291
+ return deps.getActiveGuestQueryId()
292
+ ? undefined
293
+ : deps.getActiveTurnTarget();
294
+ },
295
+ };
296
+ }
297
+
247
298
  /** @internal */
248
299
  export function resolveTelegramDeliveryInstanceTarget(
249
300
  view: TelegramDeliveryTargetPolicyView,
@@ -252,7 +303,9 @@ export function resolveTelegramDeliveryInstanceTarget(
252
303
  return (
253
304
  view.followerTarget ??
254
305
  view.leaderTarget ??
255
- (view.allowedChatId === undefined ? undefined : { chatId: view.allowedChatId })
306
+ (view.allowedChatId === undefined
307
+ ? undefined
308
+ : { chatId: view.allowedChatId })
256
309
  );
257
310
  }
258
311
 
@@ -338,19 +391,6 @@ function resolveTelegramDeliveryTarget(
338
391
  return { ok: true, value: cloneTarget(target) };
339
392
  }
340
393
 
341
- function isValidHandleForRuntime(
342
- handle: TelegramDeliveryHandle,
343
- generation: string,
344
- ): boolean {
345
- return (
346
- handle.generation === generation &&
347
- handle.messageIds.length > 0 &&
348
- handle.messageIds.every(
349
- (messageId) => Number.isInteger(messageId) && messageId > 0,
350
- )
351
- );
352
- }
353
-
354
394
  function createTelegramDeliveryTargetQueue() {
355
395
  const queues = new Map<string, Promise<void>>();
356
396
  return async function run<T>(
@@ -397,6 +437,10 @@ export function createTelegramDeliveryRuntime(
397
437
  deps: TelegramDeliveryRuntimeDeps,
398
438
  ): TelegramDeliveryRuntime {
399
439
  let active = true;
440
+ const handleBindings = new WeakMap<
441
+ TelegramDeliveryHandle,
442
+ { target: TelegramDeliveryTarget; messageIds: readonly number[] }
443
+ >();
400
444
  const runForTarget = createTelegramDeliveryTargetQueue();
401
445
  const render = (
402
446
  view: TelegramDeliveryView,
@@ -409,10 +453,18 @@ export function createTelegramDeliveryRuntime(
409
453
  typeof chunk.text !== "string" || chunk.text.trim().length === 0,
410
454
  )
411
455
  ) {
412
- return failure("invalid-view", "Telegram delivery view rendered no content.");
456
+ return failure(
457
+ "invalid-view",
458
+ "Telegram delivery view rendered no content.",
459
+ );
413
460
  }
414
461
  return { ok: true, value: chunks };
415
462
  };
463
+ const inactive = <T>(): TelegramDeliveryResult<T> =>
464
+ failure(
465
+ "runtime-unavailable",
466
+ "Telegram delivery runtime generation is inactive.",
467
+ );
416
468
  const transportFailure = <T>(
417
469
  operation: "send" | "edit" | "delete" | "chat-action",
418
470
  error: unknown,
@@ -420,39 +472,58 @@ export function createTelegramDeliveryRuntime(
420
472
  partial?: T,
421
473
  ): TelegramDeliveryResult<T> => {
422
474
  deps.recordFailure?.(operation, error, target);
475
+ if (error instanceof TelegramDeliveryTransportGenerationError) {
476
+ return inactive();
477
+ }
423
478
  return failure(
424
- "transport-failed",
425
- `Telegram delivery ${operation} failed.`,
479
+ isTelegramApiCommitUnknownError(error)
480
+ ? "commit-unknown"
481
+ : "transport-failed",
482
+ isTelegramApiCommitUnknownError(error)
483
+ ? `Telegram delivery ${operation} may have committed before transport failed.`
484
+ : `Telegram delivery ${operation} failed.`,
426
485
  partial,
427
486
  );
428
487
  };
429
- const inactive = <T>(): TelegramDeliveryResult<T> =>
430
- failure(
431
- "runtime-unavailable",
432
- "Telegram delivery runtime generation is inactive.",
433
- );
434
488
  const createHandle = (
435
489
  target: TelegramDeliveryTarget,
436
490
  messageIds: readonly number[],
437
- ): TelegramDeliveryHandle => ({
438
- target: cloneTarget(target),
439
- messageIds: [...messageIds],
440
- generation: deps.generation,
441
- });
442
- const resolveHandleTarget = (
491
+ ): TelegramDeliveryHandle => {
492
+ const canonicalTarget = Object.freeze(cloneTarget(target));
493
+ const canonicalMessageIds = Object.freeze([...messageIds]);
494
+ const handle = Object.freeze({
495
+ target: canonicalTarget,
496
+ messageIds: canonicalMessageIds,
497
+ generation: deps.generation,
498
+ });
499
+ handleBindings.set(handle, {
500
+ target: canonicalTarget,
501
+ messageIds: canonicalMessageIds,
502
+ });
503
+ return handle;
504
+ };
505
+ const resolveHandle = (
443
506
  handle: TelegramDeliveryHandle,
444
- ): TelegramDeliveryResult<TelegramDeliveryTarget> => {
507
+ ): TelegramDeliveryResult<{
508
+ target: TelegramDeliveryTarget;
509
+ messageIds: readonly number[];
510
+ }> => {
445
511
  if (!active) return inactive();
446
- if (!isValidHandleForRuntime(handle, deps.generation)) {
512
+ const binding = handleBindings.get(handle);
513
+ if (!binding || handle.generation !== deps.generation) {
447
514
  return failure(
448
515
  "stale-handle",
449
516
  "Telegram delivery handle belongs to an inactive runtime generation.",
450
517
  );
451
518
  }
452
- return resolveTelegramDeliveryTarget(
453
- { kind: "target", target: handle.target },
519
+ const authorized = resolveTelegramDeliveryTarget(
520
+ { kind: "target", target: binding.target },
454
521
  deps,
455
522
  );
523
+ if (!authorized.ok) {
524
+ return failure(authorized.reason, authorized.message);
525
+ }
526
+ return { ok: true, value: binding };
456
527
  };
457
528
  return {
458
529
  generation: deps.generation,
@@ -501,14 +572,14 @@ export function createTelegramDeliveryRuntime(
501
572
  });
502
573
  },
503
574
  async editView(handle, view) {
504
- const resolved = resolveHandleTarget(handle);
575
+ const resolved = resolveHandle(handle);
505
576
  if (!resolved.ok) return failure(resolved.reason, resolved.message);
506
577
  const rendered = render(view);
507
578
  if (!rendered.ok) return failure(rendered.reason, rendered.message);
508
- const target = resolved.value;
579
+ const target = resolved.value.target;
509
580
  return runForTarget(target, async () => {
510
581
  if (!active) return inactive();
511
- const visibleMessageIds = [...handle.messageIds];
582
+ const visibleMessageIds = [...resolved.value.messageIds];
512
583
  try {
513
584
  const sharedCount = Math.min(
514
585
  visibleMessageIds.length,
@@ -571,20 +642,22 @@ export function createTelegramDeliveryRuntime(
571
642
  });
572
643
  },
573
644
  async deleteView(handle) {
574
- const resolved = resolveHandleTarget(handle);
645
+ const resolved = resolveHandle(handle);
575
646
  if (!resolved.ok) return failure(resolved.reason, resolved.message);
576
- const target = resolved.value;
647
+ const target = resolved.value.target;
577
648
  return runForTarget(target, async () => {
578
649
  if (!active) return inactive();
579
650
  try {
580
- for (const messageId of handle.messageIds) {
651
+ for (const messageId of resolved.value.messageIds) {
581
652
  if (!active) return inactive();
582
653
  await deps.deleteMessage(target, messageId);
583
654
  if (!active) return inactive();
584
655
  }
585
656
  return { ok: true, value: undefined };
586
657
  } catch (error) {
587
- return active ? transportFailure("delete", error, target) : inactive();
658
+ return active
659
+ ? transportFailure("delete", error, target)
660
+ : inactive();
588
661
  }
589
662
  });
590
663
  },
@@ -614,6 +687,11 @@ export function createTelegramBridgeDeliveryRuntime(
614
687
  deps: TelegramBridgeDeliveryRuntimeDeps,
615
688
  ): TelegramDeliveryRuntime {
616
689
  const getPolicyView = deps.getTargetPolicyView;
690
+ const assertTransportActive = (): void => {
691
+ if (deps.isTransportActive?.() === false) {
692
+ throw new TelegramDeliveryTransportGenerationError();
693
+ }
694
+ };
617
695
  return createTelegramDeliveryRuntime({
618
696
  generation: deps.generation,
619
697
  getActiveTurnTarget: deps.getActiveTurnTarget,
@@ -624,7 +702,10 @@ export function createTelegramBridgeDeliveryRuntime(
624
702
  return resolveTelegramDeliveryAggregateTarget(getPolicyView());
625
703
  },
626
704
  isExplicitTargetAuthorized(target) {
627
- return isTelegramDeliveryExplicitTargetAuthorized(target, getPolicyView());
705
+ return isTelegramDeliveryExplicitTargetAuthorized(
706
+ target,
707
+ getPolicyView(),
708
+ );
628
709
  },
629
710
  renderView(view) {
630
711
  assertTelegramInlineKeyboardCallbackData(view.replyMarkup);
@@ -638,6 +719,7 @@ export function createTelegramBridgeDeliveryRuntime(
638
719
  });
639
720
  },
640
721
  async sendChunk(target, chunk, options) {
722
+ assertTransportActive();
641
723
  const replyParameters = buildTelegramReplyParameters(
642
724
  target.chatId,
643
725
  options.replyToMessageId,
@@ -656,6 +738,7 @@ export function createTelegramBridgeDeliveryRuntime(
656
738
  ? markTelegramBusAggregateDelivery(body)
657
739
  : body,
658
740
  );
741
+ assertTransportActive();
659
742
  deps.recordOwnership({
660
743
  chatId: target.chatId,
661
744
  messageId: sent.message_id,
@@ -664,6 +747,7 @@ export function createTelegramBridgeDeliveryRuntime(
664
747
  return sent.message_id;
665
748
  },
666
749
  async editChunk(target, messageId, chunk, options) {
750
+ assertTransportActive();
667
751
  await deps.api.editMessageText({
668
752
  chat_id: target.chatId,
669
753
  message_id: messageId,
@@ -676,9 +760,11 @@ export function createTelegramBridgeDeliveryRuntime(
676
760
  });
677
761
  },
678
762
  deleteMessage(target, messageId) {
763
+ assertTransportActive();
679
764
  return deps.api.deleteMessage(target.chatId, messageId);
680
765
  },
681
766
  async sendChatAction(target, action) {
767
+ assertTransportActive();
682
768
  await deps.api.sendChatAction(target.chatId, action, {
683
769
  message_thread_id: target.threadId,
684
770
  });
@@ -688,8 +774,7 @@ export function createTelegramBridgeDeliveryRuntime(
688
774
  }
689
775
 
690
776
  function getBoundTelegramDeliveryRuntime():
691
- | TelegramDeliveryRuntime
692
- | TelegramDeliveryResult<never> {
777
+ TelegramDeliveryRuntime | TelegramDeliveryResult<never> {
693
778
  const runtime = getTelegramDeliveryRuntimeRegistry().runtime;
694
779
  return (
695
780
  runtime ??
@@ -716,7 +801,9 @@ function validateView<T>(
716
801
  }
717
802
 
718
803
  async function runDeliveryOperation<T>(
719
- operation: (runtime: TelegramDeliveryRuntime) => Promise<TelegramDeliveryResult<T>>,
804
+ operation: (
805
+ runtime: TelegramDeliveryRuntime,
806
+ ) => Promise<TelegramDeliveryResult<T>>,
720
807
  ): Promise<TelegramDeliveryResult<T>> {
721
808
  const runtime = getBoundTelegramDeliveryRuntime();
722
809
  if (isFailure(runtime)) return runtime;