@llblab/pi-telegram 0.16.3 → 0.16.4

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/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.16.4: Follow-Up And Runtime Mode Hotfix
4
+
5
+ - `[Runtime]` Feature-detect Pi `ctx.mode` and keep `print`/`json` runs passive by blocking polling start/resume in those modes. Impact: CLI/headless sessions can finish local work without inheriting Telegram polling, while `tui`/`rpc` and older Pi runtimes keep existing behavior.
6
+ - `[Queue]` Forward queued Telegram prompts and unknown callback fallbacks to Pi with explicit `followUp` delivery semantics. Impact: Telegram input keeps the existing non-steering queue contract even when Pi's native streaming-message API requires an explicit busy-run policy.
7
+
3
8
  ## 0.16.3: Ownership And Shutdown Hotfix
4
9
 
5
10
  - `[Ownership]` Lock-gated proactive local/headless final-result push so only the current `/telegram-connect` owner can send non-Telegram agent-end replies to the paired chat, while accepted Telegram turns and queued work still finalize session-locally after polling ownership moves away. Impact: child/headless/non-owner instances no longer leak unrelated local results into Telegram.
@@ -88,6 +88,7 @@ Telegram configuration lives in `~/.pi/agent/telegram.json`. Polling ownership l
88
88
  - `/telegram-connect` acquires or moves singleton polling ownership before polling starts.
89
89
  - `/telegram-disconnect` stops polling and releases ownership.
90
90
  - Session start resumes polling only when the existing lock already points at the current `pid`/`cwd`, or when a stale same-`cwd` lock can be safely replaced after process restart.
91
+ - Pi `print`/`json` run modes stay passive: they do not start or resume Telegram polling even if a lock is present. Older Pi runtimes without `ctx.mode` keep the previous compatibility behavior.
91
92
  - Inherited child sessions that see the same `telegram.json` but do not own the `pid`/`cwd` lock must not auto-start polling or call `getUpdates` unless the operator force-takes ownership.
92
93
  - Session replacement suspends polling/watchers without releasing ownership so the next session-start hook in the same process can resume.
93
94
  - Live polling owners require explicit takeover confirmation.
@@ -140,7 +141,7 @@ Dispatch requires:
140
141
 
141
142
  A dispatched prompt remains queued until `agent_start` consumes it. This keeps the active Telegram turn bound for previews, attachments, aborts, and final replies.
142
143
 
143
- Post-agent-end queue dispatch uses a session-bound deferred dispatcher. It is activated on session start, clears timers on shutdown, and skips callbacks from older generations before touching `ExtensionContext`. Dispatch stays session-bound after polling ownership moves elsewhere.
144
+ Post-agent-end queue dispatch uses a session-bound deferred dispatcher. It is activated on session start, clears timers on shutdown, and skips callbacks from older generations before touching `ExtensionContext`. Dispatch stays session-bound after polling ownership moves elsewhere. When a queued Telegram prompt is forwarded into Pi, it uses Pi's explicit `followUp` delivery option so Telegram input preserves the existing non-steering queue contract even if Pi is still settling active work.
144
145
 
145
146
  ### Controls And Menus
146
147
 
package/index.ts CHANGED
@@ -386,6 +386,8 @@ export default function (pi: Pi.ExtensionAPI) {
386
386
  const lockedPollingRuntime = Locks.createTelegramLockedPollingRuntime({
387
387
  lock: lockRuntime,
388
388
  hasBotToken: configStore.hasBotToken,
389
+ canStartPolling: Pi.canStartPollingInExtensionContext,
390
+ formatStartBlockedMessage: Pi.formatPollingStartBlockedByRunMode,
389
391
  startPolling: pollingRuntime.start,
390
392
  stopPolling: pollingRuntime.stop,
391
393
  updateStatus,
package/lib/locks.ts CHANGED
@@ -101,6 +101,8 @@ export interface TelegramLockedPollingRuntimeDeps<
101
101
  > {
102
102
  lock: TelegramLockRuntime<TContext>;
103
103
  hasBotToken: () => boolean;
104
+ canStartPolling?: (ctx: TContext) => boolean;
105
+ formatStartBlockedMessage?: (ctx: TContext) => string;
104
106
  startPolling: (ctx: TContext) => void | Promise<void>;
105
107
  stopPolling: () => Promise<void>;
106
108
  updateStatus: (ctx: TContext) => void;
@@ -309,10 +311,18 @@ export function createTelegramLockedPollingRuntime<
309
311
  }, ownershipCheckMs);
310
312
  ownershipInterval.unref?.();
311
313
  };
314
+ const canStartPolling = (ctx: TContext): boolean =>
315
+ deps.canStartPolling?.(ctx) ?? true;
316
+ const formatStartBlockedMessage = (ctx: TContext): string =>
317
+ deps.formatStartBlockedMessage?.(ctx) ??
318
+ "Telegram polling is unavailable in this π run mode.";
312
319
  return {
313
320
  start: async (ctx, options = {}) => {
314
321
  if (!deps.hasBotToken())
315
322
  return { ok: false, message: "Telegram bot is not configured." };
323
+ if (!canStartPolling(ctx)) {
324
+ return { ok: false, message: formatStartBlockedMessage(ctx) };
325
+ }
316
326
  const acquired = deps.lock.acquire(ctx, options);
317
327
  if (!acquired.ok) {
318
328
  return {
@@ -341,6 +351,7 @@ export function createTelegramLockedPollingRuntime<
341
351
  suspend: suspendPolling,
342
352
  onSessionStart: async (_event, ctx) => {
343
353
  if (!deps.hasBotToken()) return;
354
+ if (!canStartPolling(ctx)) return;
344
355
  const ownsCurrentLock = deps.lock.owns(ctx);
345
356
  const state = ownsCurrentLock ? undefined : deps.lock.getState();
346
357
  const canResumeStaleSameCwd =
package/lib/pi.ts CHANGED
@@ -41,6 +41,44 @@ export interface PiSettingsManager {
41
41
  }
42
42
 
43
43
  export type PiSlashCommandInfo = SlashCommandInfo;
44
+ export type PiRunMode = "tui" | "rpc" | "json" | "print";
45
+
46
+ function isPiRunMode(value: unknown): value is PiRunMode {
47
+ return (
48
+ value === "tui" ||
49
+ value === "rpc" ||
50
+ value === "json" ||
51
+ value === "print"
52
+ );
53
+ }
54
+
55
+ export function getExtensionContextMode(ctx: unknown): PiRunMode | undefined {
56
+ const mode =
57
+ typeof ctx === "object" && ctx !== null
58
+ ? (ctx as { mode?: unknown }).mode
59
+ : undefined;
60
+ return isPiRunMode(mode) ? mode : undefined;
61
+ }
62
+
63
+ export function isExtensionContextPassiveRunMode(ctx: unknown): boolean {
64
+ const mode = getExtensionContextMode(ctx);
65
+ return mode === "print" || mode === "json";
66
+ }
67
+
68
+ export function canStartPollingInExtensionContext(ctx: unknown): boolean {
69
+ return !isExtensionContextPassiveRunMode(ctx);
70
+ }
71
+
72
+ export function formatPollingStartBlockedByRunMode(ctx: unknown): string {
73
+ const mode = getExtensionContextMode(ctx);
74
+ return mode
75
+ ? `Telegram polling is unavailable in π ${mode} mode. Use /telegram-connect from a long-lived π session.`
76
+ : "Telegram polling is unavailable in this π run mode.";
77
+ }
78
+
79
+ export type PiSendUserMessageOptions = NonNullable<
80
+ Parameters<ExtensionAPI["sendUserMessage"]>[1]
81
+ >;
44
82
 
45
83
  export interface PiExtensionApiRuntimePorts {
46
84
  sendUserMessage: ExtensionAPI["sendUserMessage"];
@@ -63,7 +101,7 @@ export function createExtensionApiRuntimePorts(
63
101
  >,
64
102
  ): PiExtensionApiRuntimePorts {
65
103
  return {
66
- sendUserMessage: (content) => api.sendUserMessage(content),
104
+ sendUserMessage: (content, options) => api.sendUserMessage(content, options),
67
105
  exec: (command, args, options) => api.exec(command, args, options),
68
106
  getCommands: () => api.getCommands(),
69
107
  getThinkingLevel: () => api.getThinkingLevel(),
package/lib/queue.ts CHANGED
@@ -1820,6 +1820,14 @@ export function createTelegramDeferredQueueDispatchRuntime<TContext = unknown>(
1820
1820
 
1821
1821
  // --- Dispatch Runtime ---
1822
1822
 
1823
+ export interface TelegramPromptDeliveryOptions {
1824
+ deliverAs: "followUp";
1825
+ }
1826
+
1827
+ export const TELEGRAM_PROMPT_FOLLOW_UP_DELIVERY = {
1828
+ deliverAs: "followUp",
1829
+ } as const satisfies TelegramPromptDeliveryOptions;
1830
+
1823
1831
  export interface TelegramDispatchRuntimeDeps<TContext = unknown> {
1824
1832
  executeControlItem: (
1825
1833
  item: Extract<
@@ -1833,6 +1841,7 @@ export interface TelegramDispatchRuntimeDeps<TContext = unknown> {
1833
1841
  TelegramQueueDispatchAction,
1834
1842
  { kind: "prompt" }
1835
1843
  >["item"]["content"],
1844
+ options?: TelegramPromptDeliveryOptions,
1836
1845
  ) => void;
1837
1846
  onPromptDispatchFailure: (message: string) => void;
1838
1847
  onIdle: () => void;
@@ -1870,7 +1879,7 @@ export function executeTelegramQueueDispatchPlan<TContext = unknown>(
1870
1879
  }
1871
1880
  deps.onPromptDispatchStart(plan.item.chatId);
1872
1881
  try {
1873
- deps.sendUserMessage(plan.item.content);
1882
+ deps.sendUserMessage(plan.item.content, TELEGRAM_PROMPT_FOLLOW_UP_DELIVERY);
1874
1883
  } catch (error) {
1875
1884
  const message = getTelegramQueueErrorMessage(error);
1876
1885
  deps.onPromptDispatchFailure(message);
package/lib/routing.ts CHANGED
@@ -122,7 +122,10 @@ export interface TelegramInboundRouteRuntimeDeps<
122
122
  ctx: TContext,
123
123
  ) => Promise<void>;
124
124
  setModel: (model: TModel) => Promise<boolean>;
125
- sendUserMessage?: (message: string) => void;
125
+ sendUserMessage?: (
126
+ message: string,
127
+ options?: Queue.TelegramPromptDeliveryOptions,
128
+ ) => void;
126
129
  isIdle: (ctx: TContext) => boolean;
127
130
  hasPendingMessages: (ctx: TContext) => boolean;
128
131
  compact: (
@@ -315,7 +318,10 @@ export function createTelegramInboundRouteRuntime<
315
318
  callbackData &&
316
319
  !isTelegramOwnedCallbackData(callbackData)
317
320
  ) {
318
- deps.sendUserMessage(`[callback] ${callbackData}`);
321
+ deps.sendUserMessage(
322
+ `[callback] ${callbackData}`,
323
+ Queue.TELEGRAM_PROMPT_FOLLOW_UP_DELIVERY,
324
+ );
319
325
  await deps.answerCallbackQuery(query.id);
320
326
  return;
321
327
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.16.3",
3
+ "version": "0.16.4",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"