@llblab/pi-telegram 0.13.2 → 0.15.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/bindings.ts CHANGED
@@ -41,6 +41,16 @@ interface TelegramCommandsAndToolsBindingDeps {
41
41
  activeTurnRuntime: Queue.TelegramActiveTurnStore<Queue.PendingTelegramTurn>;
42
42
  lockedPollingRuntime: Locks.TelegramLockedPollingRuntime<Pi.ExtensionContext>;
43
43
  getStatusLines: () => string[];
44
+ buttonActionStore: OutboundHandlers.TelegramButtonActionStore;
45
+ sendMarkdownReply: (
46
+ chatId: number,
47
+ replyToMessageId: number | undefined,
48
+ markdown: string,
49
+ options?: { replyMarkup?: unknown },
50
+ ) => Promise<number | undefined>;
51
+ callMultipart: OutboundHandlers.TelegramVoiceReplySenderDeps["sendMultipart"];
52
+ getDefaultChatId: () => number | undefined;
53
+ canSendDirect: () => boolean;
44
54
  updateStatus: TelegramBridgeStatusUpdater;
45
55
  recordRuntimeEvent: TelegramRuntimeEventRecorder;
46
56
  }
@@ -52,11 +62,29 @@ export function registerTelegramCommandsAndTools({
52
62
  activeTurnRuntime,
53
63
  lockedPollingRuntime,
54
64
  getStatusLines,
65
+ buttonActionStore,
66
+ sendMarkdownReply,
67
+ callMultipart,
68
+ getDefaultChatId,
69
+ canSendDirect,
55
70
  updateStatus,
56
71
  recordRuntimeEvent,
57
72
  }: TelegramCommandsAndToolsBindingDeps): void {
58
73
  OutboundAttachments.registerTelegramOutboundAttachmentTool(pi, {
59
74
  getActiveTurn: activeTurnRuntime.get,
75
+ getDefaultChatId,
76
+ canSendDirect,
77
+ sendMultipart: callMultipart,
78
+ recordRuntimeEvent,
79
+ });
80
+ OutboundAttachments.registerTelegramOutboundMessageTool(pi, {
81
+ getDefaultChatId,
82
+ canSendDirect,
83
+ planMessage: OutboundHandlers.createTelegramOutboundReplyPlanner(
84
+ buttonActionStore,
85
+ ),
86
+ sendMarkdownMessage: (chatId, markdown, options) =>
87
+ sendMarkdownReply(chatId, undefined, markdown, options),
60
88
  recordRuntimeEvent,
61
89
  });
62
90
  Commands.registerTelegramBridgeCommands(pi, {
@@ -229,15 +257,17 @@ export function registerTelegramLifecycleRuntimeHooks({
229
257
  resetPendingModelSwitch: modelSwitchController.clearPendingSwitch,
230
258
  setQueuedItems: telegramQueueStore.setQueuedItems,
231
259
  clearDispatchPending: lifecycle.clearDispatchPending,
260
+ setFoldQueuedPromptsIntoHistory: lifecycle.setFoldQueuedPromptsIntoHistory,
232
261
  setActiveTurn: activeTurnRuntime.set,
233
262
  createPreviewState: previewRuntime.resetState,
234
263
  startTypingLoop: promptDispatchRuntime.startTypingLoop,
235
264
  updateStatus,
236
265
  getActiveTurn: activeTurnRuntime.get,
237
266
  extractAssistant: Replies.extractLatestAssistantMessageText,
238
- getPreserveQueuedTurnsAsHistory:
239
- lifecycle.shouldPreserveQueuedTurnsAsHistory,
267
+ getFoldQueuedPromptsIntoHistory:
268
+ lifecycle.shouldFoldQueuedPromptsIntoHistory,
240
269
  resetRuntimeState: agentEndResetter,
270
+ waitForTypingIdle: typing.waitForIdle,
241
271
  dispatchNextQueuedTelegramTurn,
242
272
  requestDeferredDispatchNextQueuedTelegramTurn:
243
273
  deferredQueueDispatchRuntime.request,
@@ -251,7 +281,6 @@ export function registerTelegramLifecycleRuntimeHooks({
251
281
  sendGuestReply,
252
282
  planOutboundReply: outboundReplyPlanner,
253
283
  sendOutboundReplyArtifacts: outboundReplyArtifactSender,
254
- isCurrentOwner: lockOwnershipGuard.ownsContext,
255
284
  getDefaultChatId: proactivePushChatIdGetter,
256
285
  isProactivePushEnabled,
257
286
  recordRuntimeEvent,
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Command-template standard helpers
3
- * Zones: shared utils, local process execution, automation standard
4
- * Owns shell-free command-template splitting, placeholder defaults, composition expansion, executable path expansion, and direct execution
2
+ * Command-template execution standard.
3
+ * Zones: shell-free command parsing, placeholder expansion, local process execution, composition semantics
4
+ * Owns portable command-template parsing, expansion, risk checks, retries, timeouts, and direct execution.
5
5
  */
6
6
 
7
7
  import { spawn } from "node:child_process";
@@ -10,7 +10,16 @@ import { isAbsolute, resolve } from "node:path";
10
10
 
11
11
  export type CommandTemplateFailureScope = "continue" | "branch" | "root";
12
12
 
13
+ export interface CommandTemplateActorRecipeContext {
14
+ alias?: string;
15
+ file?: string;
16
+ name?: string;
17
+ path?: string;
18
+ role?: string;
19
+ }
20
+
13
21
  export interface CommandTemplateObjectConfig {
22
+ actorRecipeContext?: CommandTemplateActorRecipeContext;
14
23
  label?: string;
15
24
  parallel?: boolean;
16
25
  when?: boolean | string;
@@ -58,6 +67,29 @@ export interface CommandTemplateExecResult {
58
67
  killed: boolean;
59
68
  }
60
69
 
70
+ export type CommandTemplateRiskLabel =
71
+ | "risk.shell"
72
+ | "risk.eval"
73
+ | "risk.broad_fs_write"
74
+ | "risk.destructive_fs"
75
+ | "risk.network"
76
+ | "risk.external_side_effect"
77
+ | "risk.long_running"
78
+ | "risk.platform_specific"
79
+ | "risk.secret_touching";
80
+
81
+ const COMMAND_TEMPLATE_RISK_LABEL_ORDER: CommandTemplateRiskLabel[] = [
82
+ "risk.shell",
83
+ "risk.eval",
84
+ "risk.destructive_fs",
85
+ "risk.broad_fs_write",
86
+ "risk.external_side_effect",
87
+ "risk.secret_touching",
88
+ "risk.network",
89
+ "risk.long_running",
90
+ "risk.platform_specific",
91
+ ];
92
+
61
93
  export type CommandTemplateExecCommand = (
62
94
  command: string,
63
95
  args: string[],
@@ -148,8 +180,15 @@ function getExecutableName(command: string | undefined): string {
148
180
  return command.split(/[\\/]/).pop()?.toLowerCase() ?? "";
149
181
  }
150
182
 
183
+ function matchesFlag(arg: string, flag: string): boolean {
184
+ if (arg === flag) return true;
185
+ if (/^-[A-Za-z]$/.test(flag) && /^-[A-Za-z]+$/.test(arg))
186
+ return arg.slice(1).includes(flag.slice(1));
187
+ return false;
188
+ }
189
+
151
190
  function hasAnyFlag(args: string[], flags: string[]): boolean {
152
- return args.some((arg) => flags.includes(arg));
191
+ return args.some((arg) => flags.some((flag) => matchesFlag(arg, flag)));
153
192
  }
154
193
 
155
194
  function hasRiskyPathArg(args: string[]): boolean {
@@ -165,6 +204,97 @@ function hasRiskyPathArg(args: string[]): boolean {
165
204
  );
166
205
  }
167
206
 
207
+ function sortRiskLabels(
208
+ labels: Iterable<CommandTemplateRiskLabel>,
209
+ ): CommandTemplateRiskLabel[] {
210
+ const unique = new Set(labels);
211
+ return COMMAND_TEMPLATE_RISK_LABEL_ORDER.filter((label) => unique.has(label));
212
+ }
213
+
214
+ function hasAnyArg(args: string[], values: string[]): boolean {
215
+ return args.some((arg) => values.includes(arg.toLowerCase()));
216
+ }
217
+
218
+ function hasSecretTouchingText(parts: string[]): boolean {
219
+ return parts.some((part) =>
220
+ /(^|[{}._\-\s/])(?:secret|token|password|passwd|credential|api[_-]?key|private[_-]?key|\.env|ssh[_-]?key)(?:[{}._\-\s/]|$)/i.test(
221
+ part,
222
+ ),
223
+ );
224
+ }
225
+
226
+ function getLeafCommandTemplateRiskLabels(
227
+ config: CommandTemplateLeafConfig,
228
+ ): CommandTemplateRiskLabel[] {
229
+ const parts = splitCommandTemplate(config.template);
230
+ const command = getExecutableName(parts[0]);
231
+ const args = parts.slice(1);
232
+ const labels = new Set<CommandTemplateRiskLabel>();
233
+ if (["bash", "sh", "zsh", "fish"].includes(command)) {
234
+ labels.add("risk.shell");
235
+ if (hasAnyFlag(args, ["-c"])) labels.add("risk.eval");
236
+ }
237
+ if (
238
+ ["node", "deno", "bun"].includes(command) &&
239
+ hasAnyFlag(args, ["-e", "--eval"])
240
+ ) {
241
+ labels.add("risk.eval");
242
+ }
243
+ if (
244
+ ["python", "python3", "perl", "ruby"].includes(command) &&
245
+ hasAnyFlag(args, ["-c", "-e"])
246
+ ) {
247
+ labels.add("risk.eval");
248
+ }
249
+ if (
250
+ command === "rm" &&
251
+ (args.some((arg) => /^-[^-]*r/.test(arg) || /^-[^-]*f/.test(arg)) ||
252
+ hasRiskyPathArg(args))
253
+ ) {
254
+ labels.add("risk.destructive_fs");
255
+ }
256
+ if (["mv", "cp", "rsync"].includes(command) && hasRiskyPathArg(args)) {
257
+ labels.add("risk.broad_fs_write");
258
+ }
259
+ if (
260
+ ["curl", "wget", "ssh", "scp", "sftp", "rsync", "nc", "ncat", "telnet", "ftp"].includes(
261
+ command,
262
+ ) ||
263
+ (command === "git" &&
264
+ hasAnyArg(args, ["clone", "fetch", "pull", "push", "ls-remote"])) ||
265
+ ["npm", "pnpm", "yarn", "pip", "cargo"].includes(command)
266
+ ) {
267
+ labels.add("risk.network");
268
+ }
269
+ if (
270
+ ["gh", "glab", "hub", "kubectl", "terraform"].includes(command) ||
271
+ (command === "git" && hasAnyArg(args, ["push"])) ||
272
+ (["npm", "pnpm", "yarn"].includes(command) &&
273
+ hasAnyArg(args, ["publish", "login", "logout", "deprecate"]))
274
+ ) {
275
+ labels.add("risk.external_side_effect");
276
+ }
277
+ if (
278
+ command === "sleep" ||
279
+ command === "watch" ||
280
+ (command === "tail" && hasAnyFlag(args, ["-f"])) ||
281
+ hasAnyArg(args, ["--watch", "--serve", "serve"])
282
+ ) {
283
+ labels.add("risk.long_running");
284
+ }
285
+ if (
286
+ ["systemctl", "launchctl", "osascript", "open", "xdg-open", "powershell", "pwsh", "cmd.exe", "apt", "apt-get", "dnf", "yum", "brew", "pacman", "apk", "xclip", "wl-copy"].includes(
287
+ command,
288
+ )
289
+ ) {
290
+ labels.add("risk.platform_specific");
291
+ }
292
+ if (["pass", "gpg", "ssh-add"].includes(command) || hasSecretTouchingText(parts)) {
293
+ labels.add("risk.secret_touching");
294
+ }
295
+ return sortRiskLabels(labels);
296
+ }
297
+
168
298
  function getLeafCommandTemplateWarnings(
169
299
  config: CommandTemplateLeafConfig,
170
300
  ): string[] {
@@ -177,7 +307,7 @@ function getLeafCommandTemplateWarnings(
177
307
  ? "shell command strings"
178
308
  : "shell scripts";
179
309
  warnings.push(
180
- `${config.label ?? command}: invokes ${command}; ${shellContent} are trusted executable content and are not sandboxed by command-template argv splitting.`,
310
+ `${config.label ?? command}: invokes ${command}; ${shellContent} are trusted executable content and are not sandboxed by command-template argv splitting. Mitigation: keep scripts local, reviewed, and parameterized with explicit placeholders.`,
181
311
  );
182
312
  }
183
313
  if (
@@ -185,7 +315,7 @@ function getLeafCommandTemplateWarnings(
185
315
  hasAnyFlag(args, ["-e", "--eval"])
186
316
  ) {
187
317
  warnings.push(
188
- `${config.label ?? command}: invokes ${command} eval mode; code strings are trusted executable content and are not sandboxed.`,
318
+ `${config.label ?? command}: invokes ${command} eval mode; code strings are trusted executable content and are not sandboxed. Mitigation: prefer a checked-in script file or keep eval input fixed and reviewed.`,
189
319
  );
190
320
  }
191
321
  if (
@@ -193,7 +323,7 @@ function getLeafCommandTemplateWarnings(
193
323
  hasAnyFlag(args, ["-c", "-e"])
194
324
  ) {
195
325
  warnings.push(
196
- `${config.label ?? command}: invokes ${command} code-eval mode; code strings are trusted executable content and are not sandboxed.`,
326
+ `${config.label ?? command}: invokes ${command} code-eval mode; code strings are trusted executable content and are not sandboxed. Mitigation: prefer a checked-in script file or keep eval input fixed and reviewed.`,
197
327
  );
198
328
  }
199
329
  if (
@@ -202,12 +332,12 @@ function getLeafCommandTemplateWarnings(
202
332
  hasRiskyPathArg(args))
203
333
  ) {
204
334
  warnings.push(
205
- `${config.label ?? command}: removes filesystem paths; verify placeholders and paths before running trusted destructive commands.`,
335
+ `${config.label ?? command}: removes filesystem paths; verify placeholders and paths before running trusted destructive commands. Mitigation: constrain path placeholders and consider dry-run or explicit confirmation.`,
206
336
  );
207
337
  }
208
338
  if (["mv", "cp", "rsync"].includes(command) && hasRiskyPathArg(args)) {
209
339
  warnings.push(
210
- `${config.label ?? command}: mutates broad filesystem paths; verify placeholders and paths before running trusted commands.`,
340
+ `${config.label ?? command}: mutates broad filesystem paths; verify placeholders and paths before running trusted commands. Mitigation: constrain path placeholders and prefer narrow source/destination paths.`,
211
341
  );
212
342
  }
213
343
  return warnings;
@@ -331,6 +461,16 @@ export function getCommandTemplateWarnings(
331
461
  ];
332
462
  }
333
463
 
464
+ export function getCommandTemplateRiskLabels(
465
+ config: CommandTemplateConfig,
466
+ ): CommandTemplateRiskLabel[] {
467
+ return sortRiskLabels(
468
+ expandCommandTemplateConfigs(config).flatMap((leaf) =>
469
+ getLeafCommandTemplateRiskLabels(leaf),
470
+ ),
471
+ );
472
+ }
473
+
334
474
  function parseCommandTemplateArgToken(value: string): {
335
475
  name: string;
336
476
  defaultValue?: string;
@@ -541,7 +681,12 @@ function shouldResolveEmbeddedCommandTemplateToken(
541
681
  function isFalsyCommandTemplateValue(value: unknown): boolean {
542
682
  if (value === undefined || value === null || value === false) return true;
543
683
  const normalized = String(value).trim().toLowerCase();
544
- return normalized === "" || normalized === "0" || normalized === "false" || normalized === "no";
684
+ return (
685
+ normalized === "" ||
686
+ normalized === "0" ||
687
+ normalized === "false" ||
688
+ normalized === "no"
689
+ );
545
690
  }
546
691
 
547
692
  function resolveCommandTemplateCondition(
package/lib/commands.ts CHANGED
@@ -293,7 +293,7 @@ export interface TelegramStopCommandDeps {
293
293
  hasAbortHandler: () => boolean;
294
294
  clearPendingModelSwitch: () => void;
295
295
  clearQueuedTelegramItems: () => number;
296
- setPreserveQueuedTurnsAsHistory: (preserve: boolean) => void;
296
+ setFoldQueuedPromptsIntoHistory: (fold: boolean) => void;
297
297
  abortCurrentTurn: () => void;
298
298
  updateStatus: () => void;
299
299
  sendTextReply: (text: string) => Promise<void>;
@@ -582,7 +582,7 @@ export interface TelegramCommandRuntimeDeps<
582
582
  clearPendingModelSwitch: () => void;
583
583
  hasQueuedTelegramItems: () => boolean;
584
584
  clearQueuedTelegramItems: (ctx: TContext) => number;
585
- setPreserveQueuedTurnsAsHistory: (preserve: boolean) => void;
585
+ setFoldQueuedPromptsIntoHistory: (fold: boolean) => void;
586
586
  abortCurrentTurn: () => void;
587
587
  isIdle: (ctx: TContext) => boolean;
588
588
  hasPendingMessages: (ctx: TContext) => boolean;
@@ -727,7 +727,7 @@ export async function handleTelegramStopCommand(
727
727
  ): Promise<void> {
728
728
  deps.clearPendingModelSwitch();
729
729
  const clearedCount = deps.clearQueuedTelegramItems();
730
- deps.setPreserveQueuedTurnsAsHistory(false);
730
+ deps.setFoldQueuedPromptsIntoHistory(false);
731
731
  if (!deps.hasAbortHandler()) {
732
732
  const clearedSuffix =
733
733
  clearedCount > 0
@@ -748,9 +748,10 @@ export async function handleTelegramStopCommand(
748
748
 
749
749
  export async function handleTelegramAbortCommand(deps: {
750
750
  hasAbortHandler: () => boolean;
751
+ hasActiveTelegramTurn: () => boolean;
751
752
  clearPendingModelSwitch: () => void;
752
753
  abortCurrentTurn: () => void;
753
- setPreserveForIdle: () => void;
754
+ setFoldQueuedPromptsIntoHistory: (fold: boolean) => void;
754
755
  updateStatus: () => void;
755
756
  sendTextReply: (text: string) => Promise<void>;
756
757
  }): Promise<void> {
@@ -759,7 +760,7 @@ export async function handleTelegramAbortCommand(deps: {
759
760
  await deps.sendTextReply("No active turn.");
760
761
  return;
761
762
  }
762
- deps.setPreserveForIdle();
763
+ deps.setFoldQueuedPromptsIntoHistory(deps.hasActiveTelegramTurn());
763
764
  deps.abortCurrentTurn();
764
765
  deps.updateStatus();
765
766
  await deps.sendTextReply("Aborted current turn.");
@@ -772,7 +773,7 @@ export async function handleTelegramNextCommand(deps: {
772
773
  clearPendingModelSwitch: () => void;
773
774
  abortCurrentTurn: () => void;
774
775
  dispatchNextQueuedTurn: () => void;
775
- setPreserveForDispatch: () => void;
776
+ clearFoldForDispatch: () => void;
776
777
  updateStatus: () => void;
777
778
  sendTextReply: (text: string) => Promise<void>;
778
779
  }): Promise<void> {
@@ -782,7 +783,7 @@ export async function handleTelegramNextCommand(deps: {
782
783
  return;
783
784
  }
784
785
  if (!deps.isIdle() && deps.hasAbortHandler()) {
785
- deps.setPreserveForDispatch();
786
+ deps.clearFoldForDispatch();
786
787
  deps.abortCurrentTurn();
787
788
  deps.updateStatus();
788
789
  await deps.sendTextReply(
@@ -1070,7 +1071,7 @@ export function createTelegramCommandHandlerTargetRuntime<
1070
1071
  clearPendingModelSwitch: deps.clearPendingModelSwitch,
1071
1072
  hasQueuedTelegramItems: deps.hasQueuedTelegramItems,
1072
1073
  clearQueuedTelegramItems: deps.clearQueuedTelegramItems,
1073
- setPreserveQueuedTurnsAsHistory: deps.setPreserveQueuedTurnsAsHistory,
1074
+ setFoldQueuedPromptsIntoHistory: deps.setFoldQueuedPromptsIntoHistory,
1074
1075
  abortCurrentTurn: deps.abortCurrentTurn,
1075
1076
  isIdle: deps.isIdle,
1076
1077
  hasPendingMessages: deps.hasPendingMessages,
@@ -1177,7 +1178,7 @@ async function handleTelegramCommandRuntime<
1177
1178
  clearPendingModelSwitch: deps.clearPendingModelSwitch,
1178
1179
  clearQueuedTelegramItems: () =>
1179
1180
  deps.clearQueuedTelegramItems(commandCtx),
1180
- setPreserveQueuedTurnsAsHistory: deps.setPreserveQueuedTurnsAsHistory,
1181
+ setFoldQueuedPromptsIntoHistory: deps.setFoldQueuedPromptsIntoHistory,
1181
1182
  abortCurrentTurn: deps.abortCurrentTurn,
1182
1183
  updateStatus: updateStatusFor(commandCtx),
1183
1184
  sendTextReply: sendReplyFor(nextMessage),
@@ -1186,9 +1187,10 @@ async function handleTelegramCommandRuntime<
1186
1187
  handleAbort: async (nextMessage, commandCtx) => {
1187
1188
  await handleTelegramAbortCommand({
1188
1189
  hasAbortHandler: deps.hasAbortHandler,
1190
+ hasActiveTelegramTurn: deps.hasActiveTelegramTurn,
1189
1191
  clearPendingModelSwitch: deps.clearPendingModelSwitch,
1190
1192
  abortCurrentTurn: deps.abortCurrentTurn,
1191
- setPreserveForIdle: () => deps.setPreserveQueuedTurnsAsHistory(true),
1193
+ setFoldQueuedPromptsIntoHistory: deps.setFoldQueuedPromptsIntoHistory,
1192
1194
  updateStatus: updateStatusFor(commandCtx),
1193
1195
  sendTextReply: sendReplyFor(nextMessage),
1194
1196
  });
@@ -1202,8 +1204,8 @@ async function handleTelegramCommandRuntime<
1202
1204
  abortCurrentTurn: deps.abortCurrentTurn,
1203
1205
  dispatchNextQueuedTurn: () =>
1204
1206
  deps.dispatchNextQueuedTelegramTurn(commandCtx),
1205
- setPreserveForDispatch: () =>
1206
- deps.setPreserveQueuedTurnsAsHistory(false),
1207
+ clearFoldForDispatch: () =>
1208
+ deps.setFoldQueuedPromptsIntoHistory(false),
1207
1209
  updateStatus: updateStatusFor(commandCtx),
1208
1210
  sendTextReply: sendReplyFor(nextMessage),
1209
1211
  });
package/lib/lifecycle.ts CHANGED
@@ -102,6 +102,40 @@ export interface TelegramSessionLifecycleHooks {
102
102
  ) => Promise<void>;
103
103
  }
104
104
 
105
+ export interface TelegramSessionContextStore<TContext> {
106
+ get: () => TContext | undefined;
107
+ set: (ctx: TContext) => void;
108
+ clear: () => void;
109
+ }
110
+
111
+ export function createTelegramSessionContextStore<
112
+ TContext,
113
+ >(): TelegramSessionContextStore<TContext> {
114
+ let currentContext: TContext | undefined;
115
+ return {
116
+ get: () => currentContext,
117
+ set: (ctx) => {
118
+ currentContext = ctx;
119
+ },
120
+ clear: () => {
121
+ currentContext = undefined;
122
+ },
123
+ };
124
+ }
125
+
126
+ export function createTelegramSessionContextTracker(
127
+ store: Pick<TelegramSessionContextStore<ExtensionContext>, "set" | "clear">,
128
+ ): TelegramSessionLifecycleHooks {
129
+ return {
130
+ onSessionStart: async (_event, ctx) => {
131
+ store.set(ctx);
132
+ },
133
+ onSessionShutdown: async () => {
134
+ store.clear();
135
+ },
136
+ };
137
+ }
138
+
105
139
  type TelegramLifecycleTimer = number | ReturnType<typeof setTimeout>;
106
140
 
107
141
  export interface TelegramCompactionObserverRuntimeDeps<TContext> {
package/lib/locks.ts CHANGED
@@ -62,10 +62,13 @@ export interface TelegramLockRuntime<TContext extends TelegramLockContext> {
62
62
  }
63
63
 
64
64
  export interface TelegramLockOwnershipGuard<TContext extends TelegramLockContext> {
65
- ownsCurrentProcess: () => boolean;
66
65
  ownsContext: (ctx: TContext) => boolean;
67
66
  }
68
67
 
68
+ export interface TelegramLockContextStore<TContext extends TelegramLockContext> {
69
+ get: () => TContext | undefined;
70
+ }
71
+
69
72
  export interface TelegramLockRuntimeOptions {
70
73
  key?: string;
71
74
  locksPath?: string;
@@ -248,11 +251,22 @@ export function createTelegramLockOwnershipGuard<
248
251
  lock: TelegramLockRuntime<TContext>,
249
252
  ): TelegramLockOwnershipGuard<TContext> {
250
253
  return {
251
- ownsCurrentProcess: () => lock.owns(),
252
254
  ownsContext: (ctx) => lock.owns(ctx),
253
255
  };
254
256
  }
255
257
 
258
+ export function createTelegramDirectDeliveryOwnershipChecker<
259
+ TContext extends TelegramLockContext,
260
+ >(deps: {
261
+ lock: TelegramLockRuntime<TContext>;
262
+ contextStore: TelegramLockContextStore<TContext>;
263
+ }): () => boolean {
264
+ return () => {
265
+ const ctx = deps.contextStore.get();
266
+ return ctx ? deps.lock.owns(ctx) : false;
267
+ };
268
+ }
269
+
256
270
  export function createTelegramLockedPollingRuntime<
257
271
  TContext extends TelegramLockContext,
258
272
  >(