@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 +5 -0
- package/docs/architecture.md +2 -1
- package/index.ts +2 -0
- package/lib/locks.ts +11 -0
- package/lib/pi.ts +39 -1
- package/lib/queue.ts +10 -1
- package/lib/routing.ts +8 -2
- package/package.json +1 -1
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.
|
package/docs/architecture.md
CHANGED
|
@@ -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?: (
|
|
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(
|
|
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
|
}
|