@llblab/pi-telegram 0.23.3 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/locks.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Telegram singleton lock helpers
3
- * Zones: shared singleton, filesystem, telegram runtime ownership
4
- * Owns shared locks.json access and Telegram bridge ownership semantics
3
+ * Zones: telegram ownership, filesystem, transport authority
4
+ * Owns extension-local owners.json access and Telegram bridge ownership semantics
5
5
  */
6
6
 
7
7
  import {
@@ -19,10 +19,11 @@ import {
19
19
  } from "node:fs";
20
20
  import { randomUUID } from "node:crypto";
21
21
  import { basename, dirname, join } from "node:path";
22
- import { resolveTelegramLocksPath } from "./paths.ts";
22
+ import { resolveTelegramOwnersPath } from "./paths.ts";
23
23
 
24
- export const TELEGRAM_LOCK_KEY = "@llblab/pi-telegram";
25
- export const TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS = 5_000;
24
+ export const TELEGRAM_LOCK_KEY = "default";
25
+ export const TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS = 8_000;
26
+ const TELEGRAM_OWNERSHIP_REFRESH_MS = 2_000;
26
27
  const TELEGRAM_LOCK_WRITE_RETRY_ATTEMPTS = 5;
27
28
  const TELEGRAM_LOCK_WRITE_RETRY_DELAY_MS = 25;
28
29
  const TELEGRAM_LOCK_TRANSACTION_ATTEMPTS = 80;
@@ -42,18 +43,17 @@ function allocateTelegramLockRuntimeGeneration(): number {
42
43
  return generation;
43
44
  }
44
45
 
45
- function getLocksPath(): string {
46
- return resolveTelegramLocksPath();
46
+ function getOwnersPath(): string {
47
+ return resolveTelegramOwnersPath();
47
48
  }
48
49
 
49
50
  /**
50
- * Resolve the scoped lock key for the active Telegram profile.
51
- * Default profile → @llblab/pi-telegram
52
- * Named profile → @llblab/pi-telegram:<name>
51
+ * Resolve the extension-local owner slot for the active Telegram profile.
52
+ * Default profile → default
53
+ * Named profile → the validated profile name
53
54
  */
54
55
  export function resolveTelegramLockKey(activeProfile?: string): string {
55
- if (activeProfile) return `${TELEGRAM_LOCK_KEY}:${activeProfile}`;
56
- return TELEGRAM_LOCK_KEY;
56
+ return activeProfile || TELEGRAM_LOCK_KEY;
57
57
  }
58
58
 
59
59
  export interface TelegramActiveProfileGetter {
@@ -139,7 +139,7 @@ export interface TelegramLockRuntimeOptions {
139
139
  staleHeartbeatMs?: number;
140
140
  }
141
141
 
142
- export function readLocks(path = getLocksPath()): Record<string, unknown> {
142
+ export function readLocks(path = getOwnersPath()): Record<string, unknown> {
143
143
  if (!existsSync(path)) return {};
144
144
  try {
145
145
  const value = JSON.parse(readFileSync(path, "utf8"));
@@ -161,7 +161,7 @@ function readLocksForTransaction(path: string): Record<string, unknown> {
161
161
  }
162
162
  const value: unknown = JSON.parse(source);
163
163
  if (!value || typeof value !== "object" || Array.isArray(value)) {
164
- throw new Error(`Invalid Telegram lock registry: ${path}`);
164
+ throw new Error(`Invalid Telegram owner store: ${path}`);
165
165
  }
166
166
  return value as Record<string, unknown>;
167
167
  }
@@ -182,8 +182,7 @@ interface TelegramLockTransactionOwner {
182
182
  generation: string;
183
183
  }
184
184
 
185
- const TELEGRAM_TRANSACTION_OWNER_PATTERN =
186
- /^owner\.([A-Za-z0-9-]+)\.json$/u;
185
+ const TELEGRAM_TRANSACTION_OWNER_PATTERN = /^owner\.([A-Za-z0-9-]+)\.json$/u;
187
186
 
188
187
  function getLockTransactionOwnerFile(generation: string): string {
189
188
  return `owner.${generation}.json`;
@@ -351,6 +350,8 @@ type TelegramTransactionGlobal = typeof globalThis & {
351
350
 
352
351
  export interface TelegramFileTransactionOptions {
353
352
  recoveryRename?: typeof renameSync;
353
+ attempts?: number;
354
+ retryDelayMs?: number;
354
355
  }
355
356
 
356
357
  function getActiveTransactionReclaims(): Set<string> {
@@ -367,7 +368,13 @@ function reclaimAbandonedDirectoryGuard(
367
368
  } catch {
368
369
  return false;
369
370
  }
370
- const entries = readdirSync(path);
371
+ let entries: string[];
372
+ try {
373
+ entries = readdirSync(path);
374
+ } catch {
375
+ // Another recovery candidate may remove the observed guard after lstat.
376
+ return false;
377
+ }
371
378
  if (entries.length !== 1) return false;
372
379
  const entry = entries[0];
373
380
  let observedPid: number;
@@ -383,10 +390,7 @@ function reclaimAbandonedDirectoryGuard(
383
390
  observedReclaimGeneration = match[2];
384
391
  }
385
392
  const activeReclaims = getActiveTransactionReclaims();
386
- if (
387
- observedPid === process.pid &&
388
- observedReclaimGeneration !== undefined
389
- ) {
393
+ if (observedPid === process.pid && observedReclaimGeneration !== undefined) {
390
394
  if (activeReclaims.has(observedReclaimGeneration)) return false;
391
395
  } else if (isProcessAlive(observedPid)) {
392
396
  return false;
@@ -565,10 +569,7 @@ function recoverAbandonedLockTransaction(
565
569
  }
566
570
 
567
571
  const recoveryGuardPath = `${path}.recovery`;
568
- const recoveryOwner = acquireLegacyRecoveryGuard(
569
- recoveryGuardPath,
570
- options,
571
- );
572
+ const recoveryOwner = acquireLegacyRecoveryGuard(recoveryGuardPath, options);
572
573
  if (!recoveryOwner) return undefined;
573
574
  let recoveredOwner: TelegramLockTransactionOwner | undefined;
574
575
  try {
@@ -607,24 +608,28 @@ function acquireLockTransaction(
607
608
  path: string,
608
609
  options: TelegramFileTransactionOptions = {},
609
610
  ): TelegramLockTransactionOwner {
611
+ const attempts = Math.max(
612
+ 1,
613
+ options.attempts ?? TELEGRAM_LOCK_TRANSACTION_ATTEMPTS,
614
+ );
615
+ const retryDelayMs = Math.max(
616
+ 0,
617
+ options.retryDelayMs ?? TELEGRAM_LOCK_TRANSACTION_RETRY_DELAY_MS,
618
+ );
610
619
  mkdirSync(dirname(path), { recursive: true });
611
- for (
612
- let attempt = 0;
613
- attempt < TELEGRAM_LOCK_TRANSACTION_ATTEMPTS;
614
- attempt += 1
615
- ) {
620
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
616
621
  try {
617
622
  return createLockTransactionGuard(path);
618
623
  } catch (error) {
619
624
  if (!isLockTransactionContentionError(error, path)) throw error;
620
625
  const recoveredOwner = recoverAbandonedLockTransaction(path, options);
621
626
  if (recoveredOwner !== undefined) return recoveredOwner;
622
- if (attempt === TELEGRAM_LOCK_TRANSACTION_ATTEMPTS - 1) {
627
+ if (attempt === attempts - 1) {
623
628
  throw new Error(
624
629
  `Timed out acquiring Telegram lock transaction: ${path}`,
625
630
  );
626
631
  }
627
- sleepSync(TELEGRAM_LOCK_TRANSACTION_RETRY_DELAY_MS);
632
+ sleepSync(retryDelayMs);
628
633
  }
629
634
  }
630
635
  throw new Error(`Failed to acquire Telegram lock transaction: ${path}`);
@@ -856,7 +861,7 @@ export function createTelegramLockRuntime<TContext extends TelegramLockContext>(
856
861
  options: TelegramLockRuntimeOptions = {},
857
862
  ): TelegramLockRuntime<TContext> {
858
863
  const key = options.key ?? TELEGRAM_LOCK_KEY;
859
- const locksPath = options.locksPath ?? getLocksPath();
864
+ const locksPath = options.locksPath ?? getOwnersPath();
860
865
  const pid = options.pid ?? process.pid;
861
866
  const isAlive = options.isProcessAlive ?? isProcessAlive;
862
867
  const getNowMs = options.getNowMs ?? Date.now;
@@ -1141,6 +1146,7 @@ export interface TelegramLockedPollingRuntimeDeps<
1141
1146
  details?: Record<string, unknown>,
1142
1147
  ) => void;
1143
1148
  ownershipCheckMs?: number;
1149
+ ownershipRefreshMs?: number;
1144
1150
  }
1145
1151
 
1146
1152
  function snapshotLockContext(ctx: TelegramLockContext): TelegramLockContext {
@@ -1152,16 +1158,20 @@ export function createTelegramLockedPollingRuntime<
1152
1158
  >(
1153
1159
  deps: TelegramLockedPollingRuntimeDeps<TContext>,
1154
1160
  ): TelegramLockedPollingRuntime<TContext> {
1155
- let ownershipInterval: ReturnType<typeof setInterval> | undefined;
1161
+ let ownershipCheckInterval: ReturnType<typeof setInterval> | undefined;
1162
+ let ownershipRefreshInterval: ReturnType<typeof setInterval> | undefined;
1156
1163
  let ownershipStop: Promise<void> | undefined;
1157
1164
  let takeoverCandidate: TelegramLockEntry | undefined;
1158
1165
  let sessionAutoStartRun: Promise<void> | undefined;
1159
1166
  let sessionAutoStartGeneration = 0;
1160
1167
  const ownershipCheckMs = deps.ownershipCheckMs ?? 1000;
1168
+ const ownershipRefreshMs =
1169
+ deps.ownershipRefreshMs ?? TELEGRAM_OWNERSHIP_REFRESH_MS;
1161
1170
  const stopOwnershipWatcher = () => {
1162
- if (!ownershipInterval) return;
1163
- clearInterval(ownershipInterval);
1164
- ownershipInterval = undefined;
1171
+ if (ownershipCheckInterval) clearInterval(ownershipCheckInterval);
1172
+ if (ownershipRefreshInterval) clearInterval(ownershipRefreshInterval);
1173
+ ownershipCheckInterval = undefined;
1174
+ ownershipRefreshInterval = undefined;
1165
1175
  };
1166
1176
  const suspendPolling = async () => {
1167
1177
  sessionAutoStartGeneration += 1;
@@ -1191,15 +1201,24 @@ export function createTelegramLockedPollingRuntime<
1191
1201
  const startOwnershipWatcher = (ctx: TContext) => {
1192
1202
  const owner = snapshotLockContext(ctx);
1193
1203
  stopOwnershipWatcher();
1194
- ownershipInterval = setInterval(() => {
1204
+ ownershipCheckInterval = setInterval(() => {
1205
+ try {
1206
+ if (deps.lock.owns(owner)) return;
1207
+ } catch (error) {
1208
+ deps.recordRuntimeEvent?.("lock", error, { phase: "check" });
1209
+ }
1210
+ stopAfterOwnershipLoss();
1211
+ }, ownershipCheckMs);
1212
+ ownershipRefreshInterval = setInterval(() => {
1195
1213
  try {
1196
1214
  if (deps.lock.refresh(owner)) return;
1197
1215
  } catch (error) {
1198
1216
  deps.recordRuntimeEvent?.("lock", error, { phase: "refresh" });
1199
1217
  }
1200
1218
  stopAfterOwnershipLoss();
1201
- }, ownershipCheckMs);
1202
- ownershipInterval.unref?.();
1219
+ }, ownershipRefreshMs);
1220
+ ownershipCheckInterval.unref?.();
1221
+ ownershipRefreshInterval.unref?.();
1203
1222
  };
1204
1223
  const runOwnedPollingStart = async (
1205
1224
  ctx: TContext,
package/lib/logs.ts CHANGED
@@ -102,6 +102,12 @@ export function createTelegramRuntimeJsonlLog(
102
102
  const getNowMs = options.getNowMs ?? Date.now;
103
103
  const scopeKeys = new Map<string, string | undefined>();
104
104
  let pending: Promise<void> = Promise.resolve();
105
+ let appendScheduled = false;
106
+ let queuedAppends: {
107
+ path: string;
108
+ previousPath: string;
109
+ line: string;
110
+ }[] = [];
105
111
 
106
112
  const ensureParent = (path: string) => {
107
113
  mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
@@ -156,27 +162,79 @@ export function createTelegramRuntimeJsonlLog(
156
162
  };
157
163
 
158
164
  const appendLine = (line: string) => {
159
- const path = resolvePath();
160
- const previousPath = resolvePreviousPath();
165
+ queuedAppends.push({
166
+ path: resolvePath(),
167
+ previousPath: resolvePreviousPath(),
168
+ line,
169
+ });
170
+ if (appendScheduled) return;
171
+ appendScheduled = true;
161
172
  pending = pending
162
173
  .then(() => {
163
- ensureParent(path);
164
- withTelegramFileTransaction(`${path}.transaction`, () => {
165
- if (
166
- existsSync(path) &&
167
- statSync(path).size > maxBytes &&
168
- (!options.canReset || options.canReset())
169
- ) {
170
- const rotate = () =>
171
- writeResetLocked(path, previousPath, "max-bytes", { maxBytes });
172
- if (options.commitReset) {
173
- options.commitReset(rotate);
174
- } else {
175
- rotate();
176
- }
174
+ appendScheduled = false;
175
+ const batch = queuedAppends;
176
+ queuedAppends = [];
177
+ const groups = new Map<
178
+ string,
179
+ { path: string; previousPath: string; lines: string[] }
180
+ >();
181
+ for (const entry of batch) {
182
+ const key = `${entry.path}\u0000${entry.previousPath}`;
183
+ const group = groups.get(key);
184
+ if (group) group.lines.push(entry.line);
185
+ else groups.set(key, { ...entry, lines: [entry.line] });
186
+ }
187
+ for (const group of groups.values()) {
188
+ try {
189
+ ensureParent(group.path);
190
+ withTelegramFileTransaction(`${group.path}.transaction`, () => {
191
+ let currentSize = existsSync(group.path)
192
+ ? statSync(group.path).size
193
+ : 0;
194
+ let chunk = "";
195
+ let chunkBytes = 0;
196
+ const flushChunk = () => {
197
+ if (!chunk) return;
198
+ appendFileSync(group.path, chunk, { mode: 0o600 });
199
+ currentSize += chunkBytes;
200
+ chunk = "";
201
+ chunkBytes = 0;
202
+ };
203
+ const rotate = (): boolean => {
204
+ if (options.canReset && !options.canReset()) return false;
205
+ let rotated = false;
206
+ const commit = () => {
207
+ writeResetLocked(
208
+ group.path,
209
+ group.previousPath,
210
+ "max-bytes",
211
+ { maxBytes },
212
+ );
213
+ rotated = true;
214
+ };
215
+ if (options.commitReset) options.commitReset(commit);
216
+ else commit();
217
+ if (rotated) currentSize = statSync(group.path).size;
218
+ return rotated;
219
+ };
220
+ for (const line of group.lines) {
221
+ const lineBytes = Buffer.byteLength(line);
222
+ if (
223
+ currentSize + chunkBytes > 0 &&
224
+ currentSize + chunkBytes + lineBytes > maxBytes
225
+ ) {
226
+ flushChunk();
227
+ rotate();
228
+ }
229
+ chunk += line;
230
+ chunkBytes += lineBytes;
231
+ }
232
+ flushChunk();
233
+ });
234
+ } catch {
235
+ // Diagnostics failures for one profile must not drop other groups.
177
236
  }
178
- appendFileSync(path, line, { mode: 0o600 });
179
- });
237
+ }
180
238
  })
181
239
  .catch(() => undefined);
182
240
  };
package/lib/media.ts CHANGED
@@ -390,15 +390,6 @@ export function appendTelegramReplyContext(
390
390
  return text ? `${text}\n\n${replyContext}` : `_\n\n${replyContext}`;
391
391
  }
392
392
 
393
- export function appendTelegramForwardContext(
394
- text: string,
395
- forwardContext: string,
396
- ): string {
397
- if (!forwardContext) return text;
398
- const forwardBlock = `[forward|${forwardContext.replace(/:\s+/g, ":")}]`;
399
- return text ? `\n\n${forwardBlock} ${text}` : `\n\n${forwardBlock}`;
400
- }
401
-
402
393
  export function extractTelegramMessagePromptText(
403
394
  message: TelegramMediaMessage,
404
395
  ): string {
package/lib/menu-model.ts CHANGED
@@ -484,7 +484,11 @@ export function createTelegramModelMenuStateBuilder<
484
484
  TelegramModelMenuStateBuilderContext<TModel>,
485
485
  >(
486
486
  deps: TelegramModelMenuStateBuilderDeps<TModel, TContext>,
487
- ): (chatId: number, ctx: TContext, threadId?: number) => Promise<TelegramModelMenuState<TModel>> {
487
+ ): (
488
+ chatId: number,
489
+ ctx: TContext,
490
+ threadId?: number,
491
+ ) => Promise<TelegramModelMenuState<TModel>> {
488
492
  return async (chatId, ctx, threadId) => {
489
493
  const settingsManager = deps.createSettingsManager(ctx.cwd);
490
494
  return deps.runtime.buildState({
@@ -745,17 +749,6 @@ export function setTelegramModelScope(
745
749
  return { patterns, enabled };
746
750
  }
747
751
 
748
- export function toggleTelegramModelScope(
749
- state: TelegramModelMenuState,
750
- model: MenuModel,
751
- ): { patterns: string[]; enabled: boolean } {
752
- return setTelegramModelScope(
753
- state,
754
- model,
755
- !isTelegramModelScoped(state, model),
756
- );
757
- }
758
-
759
752
  export function buildTelegramModelCallbackPlan<
760
753
  TModel extends MenuModel = MenuModel,
761
754
  >(
package/lib/menu.ts CHANGED
@@ -118,25 +118,6 @@ export type {
118
118
  TelegramThinkingMenuOpenDeps,
119
119
  } from "./menu-thinking.ts";
120
120
 
121
- export interface TelegramMenuEffectPort<TModel extends MenuModel = MenuModel> {
122
- answerCallbackQuery: (
123
- callbackQueryId: string,
124
- text?: string,
125
- ) => Promise<void>;
126
- updateModelMenuMessage: () => Promise<void>;
127
- updateThinkingMenuMessage: () => Promise<void>;
128
- updateStatusMessage: () => Promise<void>;
129
- persistScopedModelPatterns?: (patterns: string[]) => Promise<void>;
130
- setModel: (model: TModel) => Promise<boolean>;
131
- setCurrentModel: (model: TModel) => void;
132
- setThinkingLevel: (level: ThinkingLevel) => void;
133
- getCurrentThinkingLevel: () => ThinkingLevel;
134
- stagePendingModelSwitch: (selection: ScopedTelegramModel<TModel>) => void;
135
- restartInterruptedTelegramTurn: (
136
- selection: ScopedTelegramModel<TModel>,
137
- ) => Promise<boolean> | boolean;
138
- }
139
-
140
121
  export interface TelegramMenuCallbackEntryDeps {
141
122
  handleStatusAction: () => Promise<boolean>;
142
123
  handleThinkingAction: () => Promise<boolean>;
package/lib/outbound.ts CHANGED
@@ -197,12 +197,6 @@ export function registerTelegramOutboundHandler(
197
197
  };
198
198
  }
199
199
 
200
- export function hasTelegramOutboundHandler(kind: string): boolean {
201
- const registry = getOrCreateOutboundHandlerRegistry();
202
- const list = registry.handlers.get(kind);
203
- return !!list && list.length > 0;
204
- }
205
-
206
200
  export function getTelegramOutboundProgrammaticHandlers(
207
201
  kind: string,
208
202
  ): TelegramOutboundProgrammaticHandler[] {
@@ -953,15 +947,11 @@ export function createTelegramAssistantOutputSender<
953
947
  TReplyMarkup = unknown,
954
948
  >(deps: {
955
949
  recordOwnership?: Replies.TelegramReplyOwnershipRecorder["record"];
956
- sendMessage: (
957
- body: TelegramSendMessageBody,
958
- ) => Promise<TelegramSentMessage>;
950
+ sendMessage: (body: TelegramSendMessageBody) => Promise<TelegramSentMessage>;
959
951
  sendRichMessage: (
960
952
  body: TelegramSendRichMessageBody,
961
953
  ) => Promise<TelegramSentMessage>;
962
- editMessage: (
963
- body: TelegramEditMessageTextBody,
964
- ) => Promise<unknown>;
954
+ editMessage: (body: TelegramEditMessageTextBody) => Promise<unknown>;
965
955
  getAssistantRenderingMode: () => "rich" | "html";
966
956
  execCommand: TelegramOutboundTextReplyRuntimeDeps<TReplyMarkup>["execCommand"];
967
957
  getHandlers?: TelegramOutboundTextReplyRuntimeDeps<TReplyMarkup>["getHandlers"];
package/lib/paths.ts CHANGED
@@ -10,6 +10,8 @@
10
10
  import { homedir } from "node:os";
11
11
  import { join, resolve } from "node:path";
12
12
 
13
+ export const TELEGRAM_DEFAULT_PROFILE_NAME = "default";
14
+
13
15
  export interface TelegramAgentDirResolutionInput {
14
16
  env?: Partial<Pick<NodeJS.ProcessEnv, "PI_CODING_AGENT_DIR">>;
15
17
  execPath?: string;
@@ -45,18 +47,19 @@ export function resolveTelegramConfigPath(): string {
45
47
  return join(resolveAgentDir(), "telegram.json");
46
48
  }
47
49
 
48
- /** Telegram singleton lock file (<agentDir>/locks.json). */
49
- export function resolveTelegramLocksPath(): string {
50
- return join(resolveAgentDir(), "locks.json");
51
- }
52
-
53
50
  /** Telegram bridge temporary directory (<agentDir>/tmp/telegram). */
54
51
  export function resolveTelegramTempDir(agentDir = resolveAgentDir()): string {
55
52
  return join(agentDir, "tmp", "telegram");
56
53
  }
57
54
 
55
+ /** Telegram transport ownership store (<agentDir>/tmp/telegram/owners.json). */
56
+ export function resolveTelegramOwnersPath(): string {
57
+ return join(resolveTelegramTempDir(), "owners.json");
58
+ }
59
+
58
60
  export function getTelegramProfilePathSuffix(profileName?: string): string {
59
- return profileName ? `.${profileName.replace(/[^a-zA-Z0-9._-]+/g, "_")}` : "";
61
+ if (!profileName || profileName === TELEGRAM_DEFAULT_PROFILE_NAME) return "";
62
+ return `.${profileName.replace(/[^a-zA-Z0-9._-]+/g, "_")}`;
60
63
  }
61
64
 
62
65
  export function resolveTelegramProfileTempFilePath(
package/lib/pi.ts CHANGED
@@ -114,10 +114,6 @@ export function getSessionCompactionReason(
114
114
  : "unknown";
115
115
  }
116
116
 
117
- export type PiSendUserMessageOptions = NonNullable<
118
- Parameters<ExtensionAPI["sendUserMessage"]>[1]
119
- >;
120
-
121
117
  export interface PiExtensionApiRuntimePorts {
122
118
  sendUserMessage: ExtensionAPI["sendUserMessage"];
123
119
  exec: ExtensionAPI["exec"];
package/lib/queue.ts CHANGED
@@ -1764,7 +1764,6 @@ export async function shutdownTelegramSessionRuntime<TQueueItem>(
1764
1764
  deps.clearPreview(activeTurnChatId, target ? { target } : undefined),
1765
1765
  new Promise<void>((resolve) => {
1766
1766
  timeout = setTimeout(resolve, previewTimeoutMs);
1767
- timeout.unref?.();
1768
1767
  }),
1769
1768
  ]).finally(() => {
1770
1769
  if (timeout) clearTimeout(timeout);
@@ -2240,10 +2239,6 @@ export interface TelegramPromptDeliveryOptions {
2240
2239
  deliverAs: "followUp";
2241
2240
  }
2242
2241
 
2243
- export const TELEGRAM_PROMPT_FOLLOW_UP_DELIVERY = {
2244
- deliverAs: "followUp",
2245
- } as const satisfies TelegramPromptDeliveryOptions;
2246
-
2247
2242
  export interface TelegramDispatchRuntimeDeps<TContext = unknown> {
2248
2243
  executeControlItem: (
2249
2244
  item: Extract<
package/lib/replies.ts CHANGED
@@ -871,29 +871,6 @@ export function dedupSendTextReply(
871
871
  };
872
872
  }
873
873
 
874
- /** Wrap a sendMarkdownReply with reply dedup. */
875
- export function dedupSendMarkdownReply<TReplyMarkup = unknown>(
876
- dedup: ReplyDedupRuntime,
877
- inner: (
878
- chatId: number,
879
- replyToMessageId: number | undefined,
880
- markdown: string,
881
- options?: { replyMarkup?: TReplyMarkup },
882
- ) => Promise<number | undefined>,
883
- ): (
884
- chatId: number,
885
- replyToMessageId: number,
886
- markdown: string,
887
- options?: { replyMarkup?: TReplyMarkup },
888
- ) => Promise<number | undefined> {
889
- return async (chatId, replyToMessageId, markdown, options) => {
890
- const effectiveReplyTo = dedup.shouldReply(replyToMessageId)
891
- ? replyToMessageId
892
- : undefined;
893
- return inner(chatId, effectiveReplyTo, markdown, options);
894
- };
895
- }
896
-
897
874
  /**
898
875
  * Guest reply sender: answers guest queries with native Rich Markdown content.
899
876
  * Guest queries use InlineQueryResult input_message_content rather than chat
package/lib/runtime.ts CHANGED
@@ -508,8 +508,7 @@ export async function waitForTelegramTypingLoopIdle(
508
508
  await Promise.race([
509
509
  inFlight,
510
510
  new Promise<void>((resolve) => {
511
- const timer = setTimeout(resolve, timeoutMs);
512
- timer.unref?.();
511
+ setTimeout(resolve, timeoutMs);
513
512
  }),
514
513
  ]);
515
514
  }
package/lib/sections.ts CHANGED
@@ -358,7 +358,7 @@ export function createTelegramExtensionSectionRegistry(): TelegramSectionRegistr
358
358
  >();
359
359
  let nextToken = 0;
360
360
 
361
- const register = (section: TelegramSectionRegistration): () => void => {
361
+ const register = (section: TelegramSectionRegistration): (() => void) => {
362
362
  const duplicate = [...sections.values()].find((s) => s.id === section.id);
363
363
  if (duplicate) {
364
364
  throw new Error(`Telegram section id already registered: ${section.id}`);
@@ -708,50 +708,3 @@ export async function handleTelegramSectionSettingsOpen(
708
708
  return true;
709
709
  }
710
710
  }
711
-
712
- export async function handleTelegramSectionSettingsCallback(
713
- registry: TelegramSectionRegistry,
714
- token: TelegramSectionToken,
715
- action: string,
716
- payload: string,
717
- chatId: number,
718
- messageId: number,
719
- callbackQueryId: string,
720
- deps: TelegramSectionCallbackHandlerDeps,
721
- ): Promise<boolean> {
722
- const section = registry.getByToken(token);
723
- if (!section || !section.registration.settings?.handleCallback) {
724
- await deps.answerCallbackQuery(
725
- callbackQueryId,
726
- "This section is no longer available.",
727
- );
728
- return true;
729
- }
730
- try {
731
- const ctx = buildTelegramSectionCallbackContext(
732
- section.id,
733
- token,
734
- chatId,
735
- messageId,
736
- action,
737
- payload,
738
- callbackQueryId,
739
- deps,
740
- `settings:list`,
741
- );
742
- const result = await section.registration.settings.handleCallback(ctx);
743
- if (result === "pass") {
744
- await deps.answerCallbackQuery(callbackQueryId);
745
- }
746
- registry.clearError(token, "settings_callback");
747
- return true;
748
- } catch (error) {
749
- const message = sectionErrorMessage(error);
750
- registry.recordError(token, message, "settings_callback");
751
- await deps.answerCallbackQuery(
752
- callbackQueryId,
753
- `Section error: ${message}`,
754
- );
755
- return true;
756
- }
757
- }