@llblab/pi-kit 0.18.1 → 0.18.2
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 +6 -0
- package/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +7 -0
- package/node_modules/@llblab/pi-telegram/README.md +1 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +1 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +1 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +1 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +13 -33
- package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +1 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-queue.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-queue.js +72 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +7 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +27 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +3 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +17 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +8 -1
- package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +2 -0
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +1 -1
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +0 -10
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +18 -41
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +1 -8
- package/node_modules/@llblab/pi-telegram/lib/menu-queue.ts +126 -1
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +46 -0
- package/node_modules/@llblab/pi-telegram/lib/replies.ts +19 -0
- package/node_modules/@llblab/pi-telegram/lib/sync.ts +8 -1
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@llblab/pi-kit` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.18.2 - 2026-09-22
|
|
6
|
+
|
|
7
|
+
- `Telegram Operational Hotfixes`: Advances the exact Telegram pin to `0.50.1`, restoring follower registration when canonical Workspace binding deduplication changes a provisional slot and letting the busy leader permanently skip queued Guest prompts while visually clearing their inline placeholders.
|
|
8
|
+
- `Lifecycle And Command Safety`: Adds one-use authorization to armed internal lifecycle actions, prevents Thread naming from Pi commands, and aligns concise autocomplete hints with Pi's argument-first style.
|
|
9
|
+
- `Package Cohort`: Keeps every other bundled package at its current exact version; the package set, resource inventory, and explicit load order remain unchanged.
|
|
10
|
+
|
|
5
11
|
## 0.18.1 - 2026-09-20
|
|
6
12
|
|
|
7
13
|
- `Passive State Flow Guidance`: Advances the exact State Flow pin to `0.16.3`, requiring finalization barriers only in active episodes while preventing gratuitous final-only patches during passive memory use.
|
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ Package links lead to the owning repositories for usage, documentation, issues,
|
|
|
15
15
|
| [`@llblab/pi-codex-usage`](https://github.com/llblab/pi-codex-usage) | `0.10.0` | Compact Codex/Spark subscription-limit and Business credit-usage status |
|
|
16
16
|
| [`@llblab/pi-grow-loop`](https://github.com/llblab/pi-grow-loop) | `0.8.1` | Visible continuation scheduling and bounded worker Skills |
|
|
17
17
|
| [`@llblab/pi-state-flow`](https://github.com/llblab/pi-state-flow) | `0.16.3` | Incremental scoped state/context/memory compiler with mode-aware passive guidance, proportional operational guidance, intentional agency, reactive dangling-reference diagnostics, safe compaction, and exact publication |
|
|
18
|
-
| [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.50.
|
|
18
|
+
| [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.50.1` | Telegram companion with guarded lifecycle actions, recoverable Workspace slots, immediate Guest queue Skip, concise Pi command hints, adaptive Thread continuity, exact queues, files, voice, controls, and Generative Apps guidance |
|
|
19
19
|
| [`@llblab/skills`](https://github.com/llblab/skills) | `1.15.0` | Portable workflows for engineering, review, design, context maintenance, and other focused tasks |
|
|
20
20
|
|
|
21
21
|
Versions are exact by design. An upstream release does not change an installed kit until this repository explicitly advances the dependency and publishes a new kit version. Runtime defects and package-specific feature requests belong in the linked repository; package selection and kit installation issues belong here.
|
|
@@ -4,6 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.50.1: Queue, lifecycle, and Workspace hotfixes
|
|
8
|
+
|
|
9
|
+
- `Workspace slot recovery`: Uses the canonical binding returned by target-deduplicating Workspace commit for the live leader record and connection result, preventing a provisional recovery slot from consuming a second letter and falsely blocking follower registration at A–Z capacity.
|
|
10
|
+
- `Guest queue Skip`: Lets the leader discard a waiting Guest Mode prompt directly from the queue menu while other work is active. Guest callbacks use stable queue identity, settle durable admission before removal, stop placeholder rotation, and visually clear the inline globe placeholder without starting a model turn.
|
|
11
|
+
- `Internal lifecycle safety`: Binds each armed `/telegram-internal` lifecycle action to an unpredictable one-use token, so manual, mismatched, and replayed calls cannot consume or execute it.
|
|
12
|
+
- `Command surface`: Uses Pi-style argument-first autocomplete hints, keeps descriptions concise and punctuation-consistent, uses `profile` consistently, describes `/telegram-internal` as a manual-use warning, and removes Workspace Thread naming from `/telegram-connect`; Thread names remain configurable from Telegram.
|
|
13
|
+
|
|
7
14
|
## 0.50.0: Internal lifecycle gateway and thinking cadence
|
|
8
15
|
|
|
9
16
|
- `Internal lifecycle gateway`: Replaces the session-specific technical command with one `/telegram-internal` gateway for runtime-armed typed actions. Manual invocation performs no action and explains that the command cannot be run manually; settled `/new` replacement retains the same lifecycle and terminal-result guarantees.
|
|
@@ -55,7 +55,7 @@ Paste the bot token. If `~/.pi/agent/telegram.json` already contains a saved tok
|
|
|
55
55
|
/telegram-connect
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
The connected Pi instance owns Telegram polling. Use `/telegram-connect <profile>` to activate a named profile
|
|
58
|
+
The connected Pi instance owns Telegram polling. Use `/telegram-connect <profile>` to activate a named profile. Each profile is a parallel bot runtime with isolated polling, diagnostics, Threaded Mode state, and local bus transport; the `default` profile keeps unsuffixed runtime paths. In classic mode each profile uses a singleton lock. When Telegram private-chat Threaded Mode is available, one live instance becomes the profile's leader and later visible Pi instances register as followers. Reopening or resuming the same Pi session restores its remembered Thread at session startup; a distinct session in the same directory has its own binding and still requires explicit `/telegram-connect` when no remembered binding exists.
|
|
59
59
|
|
|
60
60
|
After an unclean computer shutdown, `/telegram-connect` detects truncated or structurally invalid temporary ownership/routing files, quarantines only the damaged files under `tmp/telegram/recovery/`, and retries once. A journal snapshot removed by older broad temp cleanup is rebuilt when its complete segment history proves an empty result, while a revisionless snapshot is repaired from the first surviving segment's exact predecessor when the reconstructed tail validates. Otherwise the snapshot and segments are quarantined as recovery evidence, a fresh journal is published, and startup continues with an informational diagnostic instead of requiring manual JSON repair. Unsupported journal versions block recovery without rewriting or quarantining the retained files; use a compatible runtime rather than deleting journals. Saved `telegram.json` configuration and runtime diagnostics remain intact. Recovery never replaces a verifiable live owner; if safe automatic recovery cannot complete, the command gives one explicit Pi-restart instruction instead of requiring deletion of the whole `tmp/` directory.
|
|
61
61
|
|
|
@@ -183,7 +183,6 @@ Run these inside Pi.
|
|
|
183
183
|
| `/telegram-setup <profile>` | Save or update a named-profile bot token |
|
|
184
184
|
| `/telegram-connect` / `/telegram-connect default` | Activate `profiles.default` and acquire its transport ownership |
|
|
185
185
|
| `/telegram-connect <profile>` | Activate a named profile and acquire its transport ownership |
|
|
186
|
-
| `/telegram-connect [profile] as=Name` | Give a fresh Workspace Thread one unique capitalized Latin-word identity while connecting |
|
|
187
186
|
| `/telegram-disconnect` | Confirm, then stop polling, release ownership, and delete this instance's Threaded Mode tab; graceful Pi quit always preserves restart ownership and independently deletes the tab only when automatic cleanup is enabled |
|
|
188
187
|
| `/telegram-status` | Inspect connection, mode, separate polling/worker progress, journal depth, queue, transport, automatic retry state, and recent diagnostics |
|
|
189
188
|
|
|
@@ -165,8 +165,6 @@ interface TelegramCommandsAndToolsBindingDeps {
|
|
|
165
165
|
stopPolling?: () => Promise<void | string>;
|
|
166
166
|
recoverPollingStart?: Commands.TelegramBridgeCommandRegistrationDeps["recoverPollingStart"];
|
|
167
167
|
getDisconnectThreadName?: () => string | undefined;
|
|
168
|
-
setRequestedThreadNameForPollingStart?: (threadName: string | undefined) => void;
|
|
169
|
-
validateThreadName?: Commands.TelegramBridgeCommandRegistrationDeps["validateThreadName"];
|
|
170
168
|
onTransportChanged?: () => Promise<void> | void;
|
|
171
169
|
getStatusLines: (options?: Status.TelegramBridgeStatusLineOptions) => string[];
|
|
172
170
|
buttonActionStore: OutboundHandlers.TelegramButtonActionStore;
|
|
@@ -196,7 +194,7 @@ interface TelegramCommandsAndToolsBindingDeps {
|
|
|
196
194
|
updateStatus: TelegramBridgeStatusUpdater;
|
|
197
195
|
recordRuntimeEvent: TelegramRuntimeEventRecorder;
|
|
198
196
|
}
|
|
199
|
-
export declare function registerTelegramCommandsAndTools({ pi, agentDir, configStore, persistConfig, setup, activeTurnRuntime, lockedPollingRuntime, stopPolling, recoverPollingStart, getDisconnectThreadName,
|
|
197
|
+
export declare function registerTelegramCommandsAndTools({ pi, agentDir, configStore, persistConfig, setup, activeTurnRuntime, lockedPollingRuntime, stopPolling, recoverPollingStart, getDisconnectThreadName, onTransportChanged, getStatusLines, buttonActionStore, sendMarkdownReply, sendChannelMarkdownMessage, sendChannelMediaMessage, listChannelPosts, mutateChannelPost, callMultipart, getDefaultChatId, getDefaultTarget, resolveAgentTarget, routeAgentMessage, canSendDirect, setGenerativeAppLiveSurfaceRuntime, recordRuntimeEvent, updateStatus, }: TelegramCommandsAndToolsBindingDeps): void;
|
|
200
198
|
interface TelegramLifecycleBindingDeps {
|
|
201
199
|
pi: Pi.ExtensionAPI;
|
|
202
200
|
publicationRuntime: TelegramBridgePublicationRuntime;
|
|
@@ -323,7 +323,7 @@ export function createTelegramActivityBindingRuntime(deps) {
|
|
|
323
323
|
},
|
|
324
324
|
};
|
|
325
325
|
}
|
|
326
|
-
export function registerTelegramCommandsAndTools({ pi, agentDir, configStore, persistConfig, setup, activeTurnRuntime, lockedPollingRuntime, stopPolling, recoverPollingStart, getDisconnectThreadName,
|
|
326
|
+
export function registerTelegramCommandsAndTools({ pi, agentDir, configStore, persistConfig, setup, activeTurnRuntime, lockedPollingRuntime, stopPolling, recoverPollingStart, getDisconnectThreadName, onTransportChanged, getStatusLines, buttonActionStore, sendMarkdownReply, sendChannelMarkdownMessage, sendChannelMediaMessage, listChannelPosts, mutateChannelPost, callMultipart, getDefaultChatId, getDefaultTarget, resolveAgentTarget, routeAgentMessage, canSendDirect, setGenerativeAppLiveSurfaceRuntime, recordRuntimeEvent, updateStatus, }) {
|
|
327
327
|
GenerativeApps.registerTelegramBindTool(pi, {
|
|
328
328
|
agentDir,
|
|
329
329
|
getActiveProfileName: configStore.getActiveProfileName,
|
|
@@ -453,7 +453,6 @@ export function registerTelegramCommandsAndTools({ pi, agentDir, configStore, pe
|
|
|
453
453
|
hasBotToken: configStore.hasBotToken,
|
|
454
454
|
getBotTokenDiagnostic: configStore.getBotTokenDiagnostic,
|
|
455
455
|
startPolling: async (ctx, options) => {
|
|
456
|
-
setRequestedThreadNameForPollingStart?.(options?.requestedThreadName);
|
|
457
456
|
try {
|
|
458
457
|
return await lockedPollingRuntime.start(ctx, options);
|
|
459
458
|
}
|
|
@@ -461,14 +460,10 @@ export function registerTelegramCommandsAndTools({ pi, agentDir, configStore, pe
|
|
|
461
460
|
recordRuntimeEvent("recovery", error, { phase: "polling-start" });
|
|
462
461
|
throw error;
|
|
463
462
|
}
|
|
464
|
-
finally {
|
|
465
|
-
setRequestedThreadNameForPollingStart?.(undefined);
|
|
466
|
-
}
|
|
467
463
|
},
|
|
468
464
|
stopPolling: stopPolling ?? lockedPollingRuntime.stop,
|
|
469
465
|
recoverPollingStart,
|
|
470
466
|
getDisconnectThreadName,
|
|
471
|
-
validateThreadName,
|
|
472
467
|
queueAgentConnectionContext,
|
|
473
468
|
updateStatus,
|
|
474
469
|
getProfileNames: () => Config.getTelegramProfileNames(configStore.getStoredConfig()),
|
|
@@ -87,7 +87,6 @@ export declare function createTelegramBotCommandRegistrar(deps: TelegramBotComma
|
|
|
87
87
|
export interface TelegramBridgeCommandStartPollingOptions {
|
|
88
88
|
force?: boolean;
|
|
89
89
|
forceFreshLeaderThread?: boolean;
|
|
90
|
-
requestedThreadName?: string;
|
|
91
90
|
}
|
|
92
91
|
export interface TelegramBridgeCommandStartPollingResult {
|
|
93
92
|
ok: boolean;
|
|
@@ -119,7 +118,6 @@ export interface TelegramBridgeCommandRegistrationDeps {
|
|
|
119
118
|
getProfileNames?: () => string[];
|
|
120
119
|
activateDefaultProfileConfig?: (ctx: ExtensionCommandContext) => Promise<void>;
|
|
121
120
|
activateProfileConfig?: (ctx: ExtensionCommandContext, profileName: string) => Promise<boolean>;
|
|
122
|
-
validateThreadName?: (threadName: string) => string | undefined;
|
|
123
121
|
}
|
|
124
122
|
export type TelegramThreadDisplayNameRenamePort = (target: {
|
|
125
123
|
chatId: number;
|
|
@@ -145,7 +143,6 @@ export declare function createTelegramThreadDisplayNameRenameBinding(): {
|
|
|
145
143
|
bind: (rename: TelegramThreadDisplayNameRenamePort) => void;
|
|
146
144
|
rename: TelegramThreadDisplayNameRenamePort;
|
|
147
145
|
};
|
|
148
|
-
export declare function parseTelegramRequestedThreadName(args: string): string | undefined;
|
|
149
146
|
export declare function registerTelegramBridgeCommands(pi: ExtensionAPI, deps: TelegramBridgeCommandRegistrationDeps): void;
|
|
150
147
|
export declare const TELEGRAM_RESERVED_COMMAND_NAMES: readonly ["stop", "name", "new", "abort", "next", "continue", "status", "queue", "compact", "model", "thinking", "settings", "help", "start"];
|
|
151
148
|
export type TelegramReservedCommandName = (typeof TELEGRAM_RESERVED_COMMAND_NAMES)[number];
|
|
@@ -575,7 +572,7 @@ export declare function createTelegramCommandOrPromptRuntime<TMessage, TContext>
|
|
|
575
572
|
dispatchMessages: (messages: TMessage[], ctx: TContext) => Promise<void>;
|
|
576
573
|
};
|
|
577
574
|
export declare const TELEGRAM_INTERNAL_COMMAND_NAME = "telegram-internal";
|
|
578
|
-
export declare const TELEGRAM_INTERNAL_COMMAND_DESCRIPTION = "
|
|
575
|
+
export declare const TELEGRAM_INTERNAL_COMMAND_DESCRIPTION = "Internal Telegram command cannot be run manually";
|
|
579
576
|
export declare const TELEGRAM_INTERNAL_MANUAL_USE_MESSAGE = "This internal Telegram command cannot be run manually.";
|
|
580
577
|
export declare function delayTelegramSessionAction(delayMs: number): Promise<void>;
|
|
581
578
|
export interface TelegramSessionActionRuntimeDeps {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Zones: telegram controls, pi agent commands, queue controls
|
|
4
4
|
* Owns Telegram slash-command normalization, bot command metadata, pi-side command registration, and command-initiated session replacement orchestration behind runtime ports
|
|
5
5
|
*/
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
6
7
|
import { pairTelegramUserIfNeeded, TELEGRAM_DEFAULT_PROFILE_NAME, } from "./config.js";
|
|
7
8
|
import { escapeHtml } from "./rendering.js";
|
|
8
9
|
import { createTelegramControlItemBuilder, createTelegramControlQueueController, createTelegramQueueAdmissionReceipt, } from "./queue.js";
|
|
@@ -241,14 +242,6 @@ function parseTelegramProfileArg(args) {
|
|
|
241
242
|
return undefined;
|
|
242
243
|
return word === TELEGRAM_DEFAULT_PROFILE_NAME ? undefined : word;
|
|
243
244
|
}
|
|
244
|
-
export function parseTelegramRequestedThreadName(args) {
|
|
245
|
-
const token = args
|
|
246
|
-
.trim()
|
|
247
|
-
.split(/\s+/)
|
|
248
|
-
.find((word) => /^as=/i.test(word));
|
|
249
|
-
const value = token?.slice(3).trim();
|
|
250
|
-
return value || undefined;
|
|
251
|
-
}
|
|
252
245
|
function formatTelegramTakeoverTitle(ctx) {
|
|
253
246
|
return ctx.ui.theme.fg("accent", "pi-telegram");
|
|
254
247
|
}
|
|
@@ -262,7 +255,7 @@ function formatTelegramTakeoverPrompt(ctx, owner) {
|
|
|
262
255
|
}
|
|
263
256
|
export function registerTelegramBridgeCommands(pi, deps) {
|
|
264
257
|
pi.registerCommand("telegram-setup", {
|
|
265
|
-
description: "Configure Telegram bot token
|
|
258
|
+
description: "<profile> — Configure Telegram bot token",
|
|
266
259
|
handler: async (args, ctx) => {
|
|
267
260
|
await deps.promptForConfig(ctx, parseTelegramProfileArg(args));
|
|
268
261
|
},
|
|
@@ -275,26 +268,14 @@ export function registerTelegramBridgeCommands(pi, deps) {
|
|
|
275
268
|
},
|
|
276
269
|
});
|
|
277
270
|
pi.registerCommand("telegram-connect", {
|
|
278
|
-
description: "
|
|
271
|
+
description: "<profile> — Start Telegram bridge",
|
|
279
272
|
handler: async (args, ctx) => {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
.trim()
|
|
283
|
-
.split(/\s+/)
|
|
284
|
-
.filter((word) => /^as=/i.test(word));
|
|
285
|
-
const requestedThreadName = parseTelegramRequestedThreadName(args);
|
|
286
|
-
const requestedNameError = requestedNameTokens.length > 1
|
|
287
|
-
? "Specify at most one as=Name Workspace Thread name."
|
|
288
|
-
: requestedNameTokens.length === 1 && !requestedThreadName
|
|
289
|
-
? "Usage: /telegram-connect [profile] as=Flightprice"
|
|
290
|
-
: requestedThreadName
|
|
291
|
-
? deps.validateThreadName?.(requestedThreadName)
|
|
292
|
-
: undefined;
|
|
293
|
-
if (requestedNameError) {
|
|
294
|
-
ctx.ui.notify(requestedNameError, "warning");
|
|
273
|
+
if (args.trim().split(/\s+/).some((word) => /^as=/i.test(word))) {
|
|
274
|
+
ctx.ui.notify("Thread names are configured from Telegram, not from Pi commands.", "warning");
|
|
295
275
|
deps.updateStatus(ctx);
|
|
296
276
|
return;
|
|
297
277
|
}
|
|
278
|
+
const profileName = parseTelegramProfileArg(args);
|
|
298
279
|
if (profileName && deps.activateProfileConfig) {
|
|
299
280
|
const ok = await deps.activateProfileConfig(ctx, profileName);
|
|
300
281
|
if (!ok) {
|
|
@@ -357,7 +338,6 @@ export function registerTelegramBridgeCommands(pi, deps) {
|
|
|
357
338
|
};
|
|
358
339
|
let result = await startWithRecovery({
|
|
359
340
|
forceFreshLeaderThread: true,
|
|
360
|
-
...(requestedThreadName ? { requestedThreadName } : {}),
|
|
361
341
|
});
|
|
362
342
|
if (result && !result.ok && result.canTakeover) {
|
|
363
343
|
const confirmed = await ctx.ui.confirm(formatTelegramTakeoverTitle(ctx), formatTelegramTakeoverPrompt(ctx, result.owner));
|
|
@@ -369,7 +349,6 @@ export function registerTelegramBridgeCommands(pi, deps) {
|
|
|
369
349
|
result = await startWithRecovery({
|
|
370
350
|
force: true,
|
|
371
351
|
forceFreshLeaderThread: true,
|
|
372
|
-
...(requestedThreadName ? { requestedThreadName } : {}),
|
|
373
352
|
});
|
|
374
353
|
}
|
|
375
354
|
if (result?.message) {
|
|
@@ -382,7 +361,7 @@ export function registerTelegramBridgeCommands(pi, deps) {
|
|
|
382
361
|
},
|
|
383
362
|
});
|
|
384
363
|
pi.registerCommand("telegram-disconnect", {
|
|
385
|
-
description: "Stop Telegram
|
|
364
|
+
description: "Stop Telegram and delete current thread in Threaded Mode",
|
|
386
365
|
handler: async (_args, ctx) => {
|
|
387
366
|
const threadName = deps.getDisconnectThreadName?.();
|
|
388
367
|
if (threadName) {
|
|
@@ -1233,7 +1212,7 @@ async function handleTelegramCommandRuntime(commandName, message, ctx, deps, com
|
|
|
1233
1212
|
}, commandArgs);
|
|
1234
1213
|
}
|
|
1235
1214
|
export const TELEGRAM_INTERNAL_COMMAND_NAME = "telegram-internal";
|
|
1236
|
-
export const TELEGRAM_INTERNAL_COMMAND_DESCRIPTION = "
|
|
1215
|
+
export const TELEGRAM_INTERNAL_COMMAND_DESCRIPTION = "Internal Telegram command cannot be run manually";
|
|
1237
1216
|
export const TELEGRAM_INTERNAL_MANUAL_USE_MESSAGE = "This internal Telegram command cannot be run manually.";
|
|
1238
1217
|
export function delayTelegramSessionAction(delayMs) {
|
|
1239
1218
|
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
@@ -1382,9 +1361,9 @@ export function createTelegramSessionActionRuntime(deps) {
|
|
|
1382
1361
|
registered = true;
|
|
1383
1362
|
deps.registerCommand(TELEGRAM_INTERNAL_COMMAND_NAME, {
|
|
1384
1363
|
description: TELEGRAM_INTERNAL_COMMAND_DESCRIPTION,
|
|
1385
|
-
handler: async (
|
|
1364
|
+
handler: async (args, ctx) => {
|
|
1386
1365
|
const action = pendingAction;
|
|
1387
|
-
if (!action) {
|
|
1366
|
+
if (!action || args.trim() !== action.token) {
|
|
1388
1367
|
ctx.ui.notify(TELEGRAM_INTERNAL_MANUAL_USE_MESSAGE, "warning");
|
|
1389
1368
|
return;
|
|
1390
1369
|
}
|
|
@@ -1421,9 +1400,10 @@ export function createTelegramSessionActionRuntime(deps) {
|
|
|
1421
1400
|
pendingTarget = undefined;
|
|
1422
1401
|
if (!target)
|
|
1423
1402
|
return;
|
|
1424
|
-
|
|
1403
|
+
const token = randomUUID();
|
|
1404
|
+
pendingAction = { kind: "replace-session", token, updateId, target };
|
|
1425
1405
|
void Promise.resolve()
|
|
1426
|
-
.then(() => deps.sendUserMessage(`/${TELEGRAM_INTERNAL_COMMAND_NAME}`, {
|
|
1406
|
+
.then(() => deps.sendUserMessage(`/${TELEGRAM_INTERNAL_COMMAND_NAME} ${token}`, {
|
|
1427
1407
|
expandPromptTemplates: true,
|
|
1428
1408
|
}))
|
|
1429
1409
|
.catch((error) => {
|
|
@@ -689,6 +689,7 @@ export default function (pi) {
|
|
|
689
689
|
storeModelMenuState: modelMenuRuntime.storeState,
|
|
690
690
|
updateStatusMessage: menuActions.updateStatusMessage,
|
|
691
691
|
updateStatus,
|
|
692
|
+
dismissGuestPlaceholder: guestPlaceholderRuntime.dismiss,
|
|
692
693
|
});
|
|
693
694
|
const threadDisplaySettingsRuntime = ThreadDisplay.createTelegramThreadDisplaySettingsRuntime({
|
|
694
695
|
getTarget() {
|
|
@@ -1449,10 +1450,6 @@ export default function (pi) {
|
|
|
1449
1450
|
return undefined;
|
|
1450
1451
|
return record.threadName ?? "current Telegram thread";
|
|
1451
1452
|
},
|
|
1452
|
-
setRequestedThreadNameForPollingStart: telegramThreadCapabilityState.setRequestedThreadName,
|
|
1453
|
-
validateThreadName(threadName) {
|
|
1454
|
-
return Threads.getTelegramTopicThreadNameValidationError(threadName, undefined);
|
|
1455
|
-
},
|
|
1456
1453
|
onTransportChanged() {
|
|
1457
1454
|
deliveryLifecycleRuntime.onSessionStart();
|
|
1458
1455
|
activityVerbosityRuntime.reset();
|
|
@@ -33,5 +33,6 @@ export declare function createTelegramQueueMenuRuntime<Context, TModel extends M
|
|
|
33
33
|
storeModelMenuState: (state: TelegramModelMenuState<TModel>) => void;
|
|
34
34
|
updateStatusMessage: (state: TelegramModelMenuState<TModel>, ctx: Context) => Promise<void>;
|
|
35
35
|
updateStatus: (ctx: Context) => void;
|
|
36
|
+
dismissGuestPlaceholder?: (inlineMessageId: string) => Promise<void>;
|
|
36
37
|
}): TelegramQueueMenuRuntime<Context>;
|
|
37
38
|
export {};
|
|
@@ -34,6 +34,8 @@ function toTelegramQueueMenuItems(items) {
|
|
|
34
34
|
return {
|
|
35
35
|
chatId: item.chatId,
|
|
36
36
|
replyToMessageId: item.replyToMessageId,
|
|
37
|
+
queueOrder: item.queueOrder,
|
|
38
|
+
isGuest: item.kind === "prompt" && item.guestQueryId !== undefined,
|
|
37
39
|
queuePosition: index + 1,
|
|
38
40
|
isPriority: item.queueLane === "priority",
|
|
39
41
|
priorityEmoji: item.kind === "prompt" ? item.priorityEmoji : undefined,
|
|
@@ -72,7 +74,9 @@ function buildTelegramQueueMenuReplyMarkup(items, emptyRefreshIndex = 0) {
|
|
|
72
74
|
return [
|
|
73
75
|
{
|
|
74
76
|
text: label,
|
|
75
|
-
callback_data:
|
|
77
|
+
callback_data: item.isGuest
|
|
78
|
+
? `queue:guest-pick:${item.queueOrder}`
|
|
79
|
+
: `queue:pick:${item.chatId}:${item.replyToMessageId}`,
|
|
76
80
|
},
|
|
77
81
|
];
|
|
78
82
|
});
|
|
@@ -128,6 +132,19 @@ function getTelegramQueueMenuItemText(item) {
|
|
|
128
132
|
const preview = `<pre>${escapeTelegramQueueMenuHtmlPreview(item.promptText)}</pre>`;
|
|
129
133
|
return `${heading}\n${preview}`;
|
|
130
134
|
}
|
|
135
|
+
function buildTelegramGuestQueueItemSubmenuReplyMarkup(queueOrder) {
|
|
136
|
+
return {
|
|
137
|
+
inline_keyboard: [
|
|
138
|
+
[{ text: "⬆️ Back", callback_data: "queue:list" }],
|
|
139
|
+
[
|
|
140
|
+
{
|
|
141
|
+
text: "🔴 Skip",
|
|
142
|
+
callback_data: `queue:guest-skip:${queueOrder}`,
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
131
148
|
function buildTelegramQueueItemSubmenuReplyMarkup(chatId, replyToMessageId, isPriority, isSkipped) {
|
|
132
149
|
return {
|
|
133
150
|
inline_keyboard: [
|
|
@@ -171,6 +188,21 @@ async function handleTelegramQueueMenuCallback(callbackQueryId, data, replyChatI
|
|
|
171
188
|
await updateTelegramQueueMenuList(callbackQueryId, replyChatId, replyMessageId, deps, undefined, refreshMatch[1] === undefined ? 0 : Number(refreshMatch[1]));
|
|
172
189
|
return true;
|
|
173
190
|
}
|
|
191
|
+
const guestPickMatch = data.match(/^queue:guest-pick:(\d+)$/);
|
|
192
|
+
if (guestPickMatch) {
|
|
193
|
+
await handleTelegramGuestQueueMenuPick(callbackQueryId, replyChatId, replyMessageId, Number(guestPickMatch[1]), deps);
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
const guestSkipMatch = data.match(/^queue:guest-skip:(\d+)$/);
|
|
197
|
+
if (guestSkipMatch) {
|
|
198
|
+
const skipped = await deps.skipGuest(Number(guestSkipMatch[1]), ctx);
|
|
199
|
+
if (!skipped) {
|
|
200
|
+
await refreshStaleTelegramQueueMenuItem(callbackQueryId, replyChatId, replyMessageId, deps);
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
await updateTelegramQueueMenuList(callbackQueryId, replyChatId, replyMessageId, deps, "Guest prompt skipped.");
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
174
206
|
const pickMatch = data.match(/^queue:pick:(\d+):(\d+)$/);
|
|
175
207
|
if (pickMatch) {
|
|
176
208
|
await handleTelegramQueueMenuPick(callbackQueryId, replyChatId, replyMessageId, Number(pickMatch[1]), Number(pickMatch[2]), deps);
|
|
@@ -208,6 +240,14 @@ async function updateTelegramQueueMenuList(callbackQueryId, replyChatId, replyMe
|
|
|
208
240
|
async function refreshStaleTelegramQueueMenuItem(callbackQueryId, replyChatId, replyMessageId, deps) {
|
|
209
241
|
await updateTelegramQueueMenuList(callbackQueryId, replyChatId, replyMessageId, deps, "Item no longer in queue.");
|
|
210
242
|
}
|
|
243
|
+
async function handleTelegramGuestQueueMenuPick(callbackQueryId, replyChatId, replyMessageId, queueOrder, deps) {
|
|
244
|
+
const item = deps.findGuestItem(queueOrder);
|
|
245
|
+
if (!item) {
|
|
246
|
+
return refreshStaleTelegramQueueMenuItem(callbackQueryId, replyChatId, replyMessageId, deps);
|
|
247
|
+
}
|
|
248
|
+
await deps.updateQueueMessage(replyChatId, replyMessageId, getTelegramQueueMenuItemText(item), buildTelegramGuestQueueItemSubmenuReplyMarkup(queueOrder));
|
|
249
|
+
await deps.answerCallbackQuery(callbackQueryId);
|
|
250
|
+
}
|
|
211
251
|
async function handleTelegramQueueMenuPick(callbackQueryId, replyChatId, replyMessageId, chatId, msgId, deps) {
|
|
212
252
|
const item = deps.findItem(chatId, msgId);
|
|
213
253
|
if (!item) {
|
|
@@ -272,6 +312,7 @@ export function createTelegramQueueMenuRuntime(deps) {
|
|
|
272
312
|
updateStatusMessage: deps.updateStatusMessage,
|
|
273
313
|
answerCallbackQuery: deps.answerCallbackQuery,
|
|
274
314
|
updateStatus: deps.updateStatus,
|
|
315
|
+
dismissGuestPlaceholder: deps.dismissGuestPlaceholder,
|
|
275
316
|
}),
|
|
276
317
|
};
|
|
277
318
|
}
|
|
@@ -318,9 +359,22 @@ function createQueueMenuCallbackHandler(deps) {
|
|
|
318
359
|
const findItem = (cId, rId) => {
|
|
319
360
|
return findTelegramQueueMenuItem(toMenuItems(), cId, rId);
|
|
320
361
|
};
|
|
362
|
+
const findGuestItem = (queueOrder) => {
|
|
363
|
+
return toMenuItems().find((item) => {
|
|
364
|
+
return item.isGuest && item.queueOrder === queueOrder;
|
|
365
|
+
});
|
|
366
|
+
};
|
|
321
367
|
return handleTelegramQueueMenuCallback(query.id, data, chatId, messageId, ctx, {
|
|
322
368
|
getQueuedItems: toMenuItems,
|
|
323
369
|
findItem,
|
|
370
|
+
findGuestItem,
|
|
371
|
+
skipGuest: (queueOrder, callbackContext) => {
|
|
372
|
+
return skipQueuedTelegramGuestPrompt(queueOrder, callbackContext, {
|
|
373
|
+
getQueueSnapshot,
|
|
374
|
+
queueMutationRuntime: deps.queueMutationRuntime,
|
|
375
|
+
dismissGuestPlaceholder: deps.dismissGuestPlaceholder,
|
|
376
|
+
});
|
|
377
|
+
},
|
|
324
378
|
togglePriority: (cId, rId) => {
|
|
325
379
|
return toggleQueuedTelegramPromptPriority(cId, rId, ctx, {
|
|
326
380
|
getQueueSnapshot,
|
|
@@ -345,6 +399,23 @@ function createQueueMenuCallbackHandler(deps) {
|
|
|
345
399
|
});
|
|
346
400
|
};
|
|
347
401
|
}
|
|
402
|
+
async function skipQueuedTelegramGuestPrompt(queueOrder, ctx, deps) {
|
|
403
|
+
const item = deps.getQueueSnapshot().find((candidate) => {
|
|
404
|
+
return candidate.kind === "prompt" &&
|
|
405
|
+
candidate.guestQueryId !== undefined &&
|
|
406
|
+
candidate.queueOrder === queueOrder;
|
|
407
|
+
});
|
|
408
|
+
if (!item || !deps.queueMutationRuntime.removeGuestPromptByQueueOrder) {
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
const removed = deps.queueMutationRuntime.removeGuestPromptByQueueOrder(queueOrder, ctx);
|
|
412
|
+
if (!removed)
|
|
413
|
+
return false;
|
|
414
|
+
if (item.guestInlineMessageId && deps.dismissGuestPlaceholder) {
|
|
415
|
+
await deps.dismissGuestPlaceholder(item.guestInlineMessageId);
|
|
416
|
+
}
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
348
419
|
function getQueueMenuReactionDisposition(item, priority, skipped) {
|
|
349
420
|
if (priority && skipped) {
|
|
350
421
|
return {
|
|
@@ -242,6 +242,11 @@ export declare function removeTelegramQueueItemsByMessageIds<TContext = unknown>
|
|
|
242
242
|
removedItems: TelegramQueueItem<TContext>[];
|
|
243
243
|
removedCount: number;
|
|
244
244
|
};
|
|
245
|
+
export declare function removeTelegramQueuedGuestPromptByOrder<TContext = unknown>(items: TelegramQueueItem<TContext>[], queueOrder: number): {
|
|
246
|
+
items: TelegramQueueItem<TContext>[];
|
|
247
|
+
removedItems: PendingTelegramTurn[];
|
|
248
|
+
removedCount: number;
|
|
249
|
+
};
|
|
245
250
|
export declare function applyTelegramQueuePromptReactionDisposition<TContext = unknown>(items: TelegramQueueItem<TContext>[], messageId: number, disposition: TelegramQueueReactionDisposition, destinationLaneOrder?: number, scope?: TelegramQueueMessageScope): {
|
|
246
251
|
items: TelegramQueueItem<TContext>[];
|
|
247
252
|
changed: boolean;
|
|
@@ -624,6 +629,7 @@ export interface TelegramQueueMutationController<TContext> {
|
|
|
624
629
|
reorder: (ctx: TContext) => void;
|
|
625
630
|
clear: (ctx: TContext) => number;
|
|
626
631
|
removeByMessageIds: (messageIds: number[], ctx: TContext, scope?: TelegramQueueMessageScope) => number;
|
|
632
|
+
removeGuestPromptByQueueOrder?: (queueOrder: number, ctx: TContext) => boolean;
|
|
627
633
|
applyReactionByMessageId: (messageId: number, disposition: TelegramQueueReactionDisposition, ctx: TContext, scope?: TelegramQueueMessageScope) => boolean;
|
|
628
634
|
}
|
|
629
635
|
export interface TelegramControlQueueControllerDeps<TContext> {
|
|
@@ -673,6 +679,7 @@ export declare function createTelegramQueueMutationController<TContext>(deps: Te
|
|
|
673
679
|
export declare function reorderTelegramQueueItemsRuntime<TContext>(deps: TelegramQueueMutationRuntimeDeps<TContext>): void;
|
|
674
680
|
export declare function clearTelegramQueueItemsRuntime<TContext>(deps: TelegramQueueMutationRuntimeDeps<TContext>): number;
|
|
675
681
|
export declare function removeTelegramQueueItemsByMessageIdsRuntime<TContext>(messageIds: number[], deps: TelegramQueueMutationRuntimeDeps<TContext>, scope?: TelegramQueueMessageScope): number;
|
|
682
|
+
export declare function removeTelegramQueuedGuestPromptByOrderRuntime<TContext>(queueOrder: number, deps: TelegramQueueMutationRuntimeDeps<TContext>): boolean;
|
|
676
683
|
export declare function applyTelegramQueuePromptReactionDispositionRuntime<TContext>(messageId: number, disposition: TelegramQueueReactionDisposition, deps: TelegramQueueMutationRuntimeDeps<TContext>, scope?: TelegramQueueMessageScope): boolean;
|
|
677
684
|
export declare function enqueueTelegramPromptTurnRuntime<TMessage, TContext = unknown>(messages: TMessage[], deps: TelegramPromptEnqueueRuntimeDeps<TMessage, TContext>): Promise<PendingTelegramTurn>;
|
|
678
685
|
export declare function createTelegramPromptEnqueueController<TMessage, TContext = unknown>(deps: TelegramPromptEnqueueControllerDeps<TMessage, TContext>): TelegramPromptEnqueueController<TMessage, TContext>;
|
|
@@ -491,6 +491,21 @@ export function removeTelegramQueueItemsByMessageIds(items, messageIds, scope) {
|
|
|
491
491
|
removedCount: removedItems.length,
|
|
492
492
|
};
|
|
493
493
|
}
|
|
494
|
+
export function removeTelegramQueuedGuestPromptByOrder(items, queueOrder) {
|
|
495
|
+
const index = items.findIndex((item) => {
|
|
496
|
+
return isPendingTelegramTurn(item) &&
|
|
497
|
+
item.guestQueryId !== undefined &&
|
|
498
|
+
item.queueOrder === queueOrder;
|
|
499
|
+
});
|
|
500
|
+
if (index < 0)
|
|
501
|
+
return { items, removedItems: [], removedCount: 0 };
|
|
502
|
+
const removedItem = items[index];
|
|
503
|
+
return {
|
|
504
|
+
items: [...items.slice(0, index), ...items.slice(index + 1)],
|
|
505
|
+
removedItems: [removedItem],
|
|
506
|
+
removedCount: 1,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
494
509
|
export function applyTelegramQueuePromptReactionDisposition(items, messageId, disposition, destinationLaneOrder, scope) {
|
|
495
510
|
let nextItems = items;
|
|
496
511
|
for (const [index, item] of items.entries()) {
|
|
@@ -1440,6 +1455,7 @@ export function createTelegramQueueMutationController(deps) {
|
|
|
1440
1455
|
reorder: (ctx) => reorderTelegramQueueItemsRuntime(buildRuntimeDeps(ctx)),
|
|
1441
1456
|
clear: (ctx) => clearTelegramQueueItemsRuntime(buildRuntimeDeps(ctx)),
|
|
1442
1457
|
removeByMessageIds: (messageIds, ctx, scope) => removeTelegramQueueItemsByMessageIdsRuntime(messageIds, buildRuntimeDeps(ctx), scope),
|
|
1458
|
+
removeGuestPromptByQueueOrder: (queueOrder, ctx) => removeTelegramQueuedGuestPromptByOrderRuntime(queueOrder, buildRuntimeDeps(ctx)),
|
|
1443
1459
|
applyReactionByMessageId: (messageId, disposition, ctx, scope) => applyTelegramQueuePromptReactionDispositionRuntime(messageId, disposition, buildRuntimeDeps(ctx), scope),
|
|
1444
1460
|
};
|
|
1445
1461
|
}
|
|
@@ -1492,6 +1508,17 @@ export function removeTelegramQueueItemsByMessageIdsRuntime(messageIds, deps, sc
|
|
|
1492
1508
|
updateTelegramQueueStatusRuntime(deps);
|
|
1493
1509
|
return removedCount;
|
|
1494
1510
|
}
|
|
1511
|
+
export function removeTelegramQueuedGuestPromptByOrderRuntime(queueOrder, deps) {
|
|
1512
|
+
const { items, removedItems, removedCount } = removeTelegramQueuedGuestPromptByOrder(deps.getQueuedItems(), queueOrder);
|
|
1513
|
+
if (removedCount === 0)
|
|
1514
|
+
return false;
|
|
1515
|
+
// A menu Skip means permanent discard, so settle durable admission before
|
|
1516
|
+
// removing the live item. A failed settlement keeps the prompt retryable.
|
|
1517
|
+
deps.onItemsDiscarded?.(removedItems, deps.ctx);
|
|
1518
|
+
deps.setQueuedItems(items);
|
|
1519
|
+
updateTelegramQueueStatusRuntime(deps);
|
|
1520
|
+
return true;
|
|
1521
|
+
}
|
|
1495
1522
|
export function applyTelegramQueuePromptReactionDispositionRuntime(messageId, disposition, deps, scope) {
|
|
1496
1523
|
const queuedItems = deps.getQueuedItems();
|
|
1497
1524
|
const changesLane = queuedItems.some((item) => {
|
|
@@ -190,6 +190,7 @@ export declare const TELEGRAM_GUEST_PLACEHOLDER_FRAME_MS = 1000;
|
|
|
190
190
|
export declare const TELEGRAM_GUEST_PLACEHOLDER_MIN_MS = 20000;
|
|
191
191
|
export declare const TELEGRAM_GUEST_PLACEHOLDER_MAX_MS = 26000;
|
|
192
192
|
export declare const TELEGRAM_GUEST_PLACEHOLDER_FRAMES: readonly ["<b>🌎 Working on it.</b>", "<b>🌍 Working on it.</b>", "<b>🌏 Working on it..</b>", "<b>🌎 Working on it..</b>", "<b>🌍 Working on it...</b>", "<b>🌏 Working on it...</b>"];
|
|
193
|
+
export declare const TELEGRAM_DISMISSED_GUEST_PLACEHOLDER_TEXT = "\u2063";
|
|
193
194
|
export declare function buildTelegramGuestPlaceholderFrame(step: number): string;
|
|
194
195
|
export interface TelegramGuestPlaceholderRuntimeDeps {
|
|
195
196
|
editGuestInlineMessage: (inlineMessageId: string, content: {
|
|
@@ -211,5 +212,7 @@ export interface TelegramGuestPlaceholderRuntime {
|
|
|
211
212
|
stop: (inlineMessageId: string) => Promise<void>;
|
|
212
213
|
/** Cancels every loop without waiting for in-flight edits (session shutdown). */
|
|
213
214
|
stopAll: () => void;
|
|
215
|
+
/** Stops rotation and visually clears the inline placeholder. */
|
|
216
|
+
dismiss: (inlineMessageId: string) => Promise<void>;
|
|
214
217
|
}
|
|
215
218
|
export declare function createTelegramGuestPlaceholderRuntime(deps: TelegramGuestPlaceholderRuntimeDeps): TelegramGuestPlaceholderRuntime;
|
|
@@ -612,6 +612,9 @@ export const TELEGRAM_GUEST_PLACEHOLDER_FRAMES = [
|
|
|
612
612
|
"<b>🌍 Working on it...</b>",
|
|
613
613
|
"<b>🌏 Working on it...</b>",
|
|
614
614
|
];
|
|
615
|
+
// Telegram does not expose deletion for inline messages. This non-printing,
|
|
616
|
+
// non-empty edit visually clears a guest placeholder after its query is skipped.
|
|
617
|
+
export const TELEGRAM_DISMISSED_GUEST_PLACEHOLDER_TEXT = "\u2063";
|
|
615
618
|
export function buildTelegramGuestPlaceholderFrame(step) {
|
|
616
619
|
const frames = TELEGRAM_GUEST_PLACEHOLDER_FRAMES;
|
|
617
620
|
const index = ((step % frames.length) + frames.length) % frames.length;
|
|
@@ -734,5 +737,19 @@ export function createTelegramGuestPlaceholderRuntime(deps) {
|
|
|
734
737
|
}
|
|
735
738
|
sessions.clear();
|
|
736
739
|
},
|
|
740
|
+
async dismiss(inlineMessageId) {
|
|
741
|
+
await stop(inlineMessageId);
|
|
742
|
+
try {
|
|
743
|
+
await deps.editGuestInlineMessage(inlineMessageId, {
|
|
744
|
+
text: TELEGRAM_DISMISSED_GUEST_PLACEHOLDER_TEXT,
|
|
745
|
+
parseMode: "HTML",
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
catch (error) {
|
|
749
|
+
deps.recordRuntimeEvent?.("guest", error, {
|
|
750
|
+
phase: "guest-placeholder-dismiss",
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
},
|
|
737
754
|
};
|
|
738
755
|
}
|
|
@@ -381,7 +381,14 @@ export async function ensureTelegramLeaderThreadBinding(deps) {
|
|
|
381
381
|
assertLeaderEpoch("before-workspace-persist");
|
|
382
382
|
await deps.topicTargetStore.persist();
|
|
383
383
|
assertLeaderEpoch("after-workspace-persist");
|
|
384
|
-
|
|
384
|
+
const { target: _target, slot: _slot, threadName: _threadName, displayTitle: _displayTitle, ...rest } = result;
|
|
385
|
+
return {
|
|
386
|
+
...rest,
|
|
387
|
+
target: { ...committed.target },
|
|
388
|
+
slot: committed.slot ?? result.slot,
|
|
389
|
+
...(committed.threadName ? { threadName: committed.threadName } : {}),
|
|
390
|
+
...(committed.displayTitle ? { displayTitle: committed.displayTitle } : {}),
|
|
391
|
+
};
|
|
385
392
|
};
|
|
386
393
|
try {
|
|
387
394
|
const unavailableTargetKeys = new Set([
|
|
@@ -480,6 +480,8 @@ Priority and Skip may coexist, including `👍 + 💩`. The prompt remains at it
|
|
|
480
480
|
|
|
481
481
|
Menu and reaction controls share the same canonical queue state. A menu Keep can clear internal Skip without changing Priority or queue position, but Telegram's Bot API cannot remove a reaction created by the user; the visible user reaction can therefore remain until that user removes it. Once Pi has consumed or dropped a prompt, later reactions cannot retract or restore it.
|
|
482
482
|
|
|
483
|
+
Guest Mode is the deliberate exception to deferred Keep/Skip semantics. A guest prompt has no source message reaction surface and may share zero-valued chat/message placeholders with other guest prompts, so queue-menu callbacks address it by immutable `queueOrder` and expose an immediate `Skip`. The leader settles its durable admission, removes it while other agent work may continue, stops placeholder animation, and replaces the inline globe placeholder with non-printing content. Bot API inline messages have no delete method, so this visual clear is the supported deletion equivalent. The guest prompt never reaches a model turn.
|
|
484
|
+
|
|
483
485
|
Admission and planning validate lane contracts. Invalid lane/kind pairings fail predictably instead of being silently coerced.
|
|
484
486
|
|
|
485
487
|
Prompt preparation completes downloads, inbound handlers, and binary image reads before synchronous final assembly and queue commit. Enqueue captures only intended abort-history identities before preparation; afterward it reads the current queue, folds surviving selected prompts using their current text and receipts, allocates the new turn's order, and appends without another asynchronous boundary. The handed-off head remains in place until `agent_start` consumes it, even if preparation finishes first. Concurrent arrivals, removals, and lane/reaction edits remain intact; preparation failure or stale-generation completion leaves intervening queue changes untouched.
|
|
@@ -186,7 +186,7 @@ Status: locally implemented and validated for the next minor release; final comp
|
|
|
186
186
|
|
|
187
187
|
A durable Workspace binding is owned by the selected bot profile plus `{ cwd, sessionId? }`. `cwd` is the normalized exact full path. When present, `sessionId` is the exact non-empty bounded value returned by the public `ctx.sessionManager.getSessionId()` API. Session display names, selector positions, session file paths, process instance IDs, and lifecycle generations are metadata or transient fences, never substitutes for the stable session ID. Absence is a real identity value `{ cwd, ∅ }`, not a wildcard; malformed present values still fail closed.
|
|
188
188
|
|
|
189
|
-
The persisted binding stores the exact session ID and derives its new binding key from the existing collision-verified Workspace directory key plus a full SHA-256 digest of the session ID. Exact persisted values remain the collision check; the digest is an index component, not independent authority. Reopening, `/resume`, `/continue`/continue-recent, or process replacement with the same `{ cwd, sessionId }` reclaims the same retained target and profile-wide letter when current ownership, accepted-work, admission, and retirement evidence allow it. `/new` and `/fork` produce distinct session IDs and therefore distinct bindings even in the same directory. `/reload` changes neither component.
|
|
189
|
+
The persisted binding stores the exact session ID and derives its new binding key from the existing collision-verified Workspace directory key plus a full SHA-256 digest of the session ID. Exact persisted values remain the collision check; the digest is an index component, not independent authority. Reopening, `/resume`, `/continue`/continue-recent, or process replacement with the same `{ cwd, sessionId }` reclaims the same retained target and profile-wide letter when current ownership, accepted-work, admission, and retirement evidence allow it. When target deduplication resolves a provisional claim to an already retained binding, the committed binding's canonical target, slot, and generated name must drive the live record and reported result; provisional metadata must not consume a second letter. `/new` and `/fork` produce distinct session IDs and therefore distinct bindings even in the same directory. `/reload` changes neither component.
|
|
190
190
|
|
|
191
191
|
Legacy cwd-only bindings remain exact compatibility identities whose absent session component is itself a unique key. They are neither wildcard session bindings nor adoption candidates: session-aware claims never stamp, migrate, consume, or restore them, and instead allocate their own `{ cwd, sessionId }` binding subject to the same global slot and protection rules. `{ cwd, ∅ }`, `{ cwd, session-a }`, and `{ cwd, session-b }` may coexist as majestic degradation for older state and peers. The 0.46 runtime reads strict legacy records as inert independent `{ cwd, ∅ }` bindings but never migrates, adopts, or routes a session through them. Protocol v2 is session-native and intentionally incompatible with protocol-v1 0.45.x peers. Upgrade requires stopping all Telegram instances first, updating them together, and then starting them again; the first v2 process becomes leader with its own session-qualified binding.
|
|
192
192
|
|
|
@@ -40,7 +40,7 @@ import {
|
|
|
40
40
|
Stable commands inside Pi:
|
|
41
41
|
|
|
42
42
|
- `/telegram-setup` — configure/update the bot token.
|
|
43
|
-
- `/telegram-connect` — start polling here and acquire external Telegram control ownership
|
|
43
|
+
- `/telegram-connect` — start polling here and acquire external Telegram control ownership, optionally for a named profile. Thread names are configured from Telegram rather than Pi commands. Accepted queue/reply state stays local if ownership later moves elsewhere. A successful command queues a hidden connection-state note for delivery with the agent's next turn without triggering one; it says Telegram is connected and that connectivity alone is not user intent.
|
|
44
44
|
- `/telegram-disconnect` — after destructive confirmation, stop polling and release ownership without deleting or silencing accepted local queue state. A successful command queues the corresponding hidden, non-triggering disconnected context note; cancelled or failed disconnects do not publish a false state transition. In Threaded Mode it deletes this instance's current Telegram thread; a follower waits for its active leader to confirm generation-fenced cleanup before stopping. Graceful Pi `quit` performs the same teardown without prompting, while `reload`, `new`, `resume`, and `fork` preserve same-process handoff.
|
|
45
45
|
- `/telegram-status` — show connection, polling, execution, queue, and recent event diagnostics; debug output separates poller and worker progress, durable automatic-retry state, exact foreign queued-owner identity, and negotiated protocol/build/capabilities.
|
|
46
46
|
|
|
@@ -563,10 +563,6 @@ interface TelegramCommandsAndToolsBindingDeps {
|
|
|
563
563
|
stopPolling?: () => Promise<void | string>;
|
|
564
564
|
recoverPollingStart?: Commands.TelegramBridgeCommandRegistrationDeps["recoverPollingStart"];
|
|
565
565
|
getDisconnectThreadName?: () => string | undefined;
|
|
566
|
-
setRequestedThreadNameForPollingStart?: (
|
|
567
|
-
threadName: string | undefined,
|
|
568
|
-
) => void;
|
|
569
|
-
validateThreadName?: Commands.TelegramBridgeCommandRegistrationDeps["validateThreadName"];
|
|
570
566
|
onTransportChanged?: () => Promise<void> | void;
|
|
571
567
|
getStatusLines: (
|
|
572
568
|
options?: Status.TelegramBridgeStatusLineOptions,
|
|
@@ -615,8 +611,6 @@ export function registerTelegramCommandsAndTools({
|
|
|
615
611
|
stopPolling,
|
|
616
612
|
recoverPollingStart,
|
|
617
613
|
getDisconnectThreadName,
|
|
618
|
-
setRequestedThreadNameForPollingStart,
|
|
619
|
-
validateThreadName,
|
|
620
614
|
onTransportChanged,
|
|
621
615
|
getStatusLines,
|
|
622
616
|
buttonActionStore,
|
|
@@ -785,20 +779,16 @@ export function registerTelegramCommandsAndTools({
|
|
|
785
779
|
hasBotToken: configStore.hasBotToken,
|
|
786
780
|
getBotTokenDiagnostic: configStore.getBotTokenDiagnostic,
|
|
787
781
|
startPolling: async (ctx, options) => {
|
|
788
|
-
setRequestedThreadNameForPollingStart?.(options?.requestedThreadName);
|
|
789
782
|
try {
|
|
790
783
|
return await lockedPollingRuntime.start(ctx, options);
|
|
791
784
|
} catch (error) {
|
|
792
785
|
recordRuntimeEvent("recovery", error, { phase: "polling-start" });
|
|
793
786
|
throw error;
|
|
794
|
-
} finally {
|
|
795
|
-
setRequestedThreadNameForPollingStart?.(undefined);
|
|
796
787
|
}
|
|
797
788
|
},
|
|
798
789
|
stopPolling: stopPolling ?? lockedPollingRuntime.stop,
|
|
799
790
|
recoverPollingStart,
|
|
800
791
|
getDisconnectThreadName,
|
|
801
|
-
validateThreadName,
|
|
802
792
|
queueAgentConnectionContext,
|
|
803
793
|
updateStatus,
|
|
804
794
|
getProfileNames: () =>
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Owns Telegram slash-command normalization, bot command metadata, pi-side command registration, and command-initiated session replacement orchestration behind runtime ports
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
|
|
7
9
|
import {
|
|
8
10
|
pairTelegramUserIfNeeded,
|
|
9
11
|
type TelegramConfigStore,
|
|
@@ -366,7 +368,6 @@ export function createTelegramBotCommandRegistrar(
|
|
|
366
368
|
export interface TelegramBridgeCommandStartPollingOptions {
|
|
367
369
|
force?: boolean;
|
|
368
370
|
forceFreshLeaderThread?: boolean;
|
|
369
|
-
requestedThreadName?: string;
|
|
370
371
|
}
|
|
371
372
|
|
|
372
373
|
export interface TelegramBridgeCommandStartPollingResult {
|
|
@@ -407,7 +408,6 @@ export interface TelegramBridgeCommandRegistrationDeps {
|
|
|
407
408
|
ctx: ExtensionCommandContext,
|
|
408
409
|
profileName: string,
|
|
409
410
|
) => Promise<boolean>;
|
|
410
|
-
validateThreadName?: (threadName: string) => string | undefined;
|
|
411
411
|
}
|
|
412
412
|
|
|
413
413
|
export type TelegramThreadDisplayNameRenamePort = (
|
|
@@ -466,17 +466,6 @@ function parseTelegramProfileArg(args: string): string | undefined {
|
|
|
466
466
|
return word === TELEGRAM_DEFAULT_PROFILE_NAME ? undefined : word;
|
|
467
467
|
}
|
|
468
468
|
|
|
469
|
-
export function parseTelegramRequestedThreadName(
|
|
470
|
-
args: string,
|
|
471
|
-
): string | undefined {
|
|
472
|
-
const token = args
|
|
473
|
-
.trim()
|
|
474
|
-
.split(/\s+/)
|
|
475
|
-
.find((word) => /^as=/i.test(word));
|
|
476
|
-
const value = token?.slice(3).trim();
|
|
477
|
-
return value || undefined;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
469
|
function formatTelegramTakeoverTitle(ctx: ExtensionCommandContext): string {
|
|
481
470
|
return ctx.ui.theme.fg("accent", "pi-telegram");
|
|
482
471
|
}
|
|
@@ -498,7 +487,7 @@ export function registerTelegramBridgeCommands(
|
|
|
498
487
|
deps: TelegramBridgeCommandRegistrationDeps,
|
|
499
488
|
): void {
|
|
500
489
|
pi.registerCommand("telegram-setup", {
|
|
501
|
-
description: "Configure Telegram bot token
|
|
490
|
+
description: "<profile> — Configure Telegram bot token",
|
|
502
491
|
handler: async (args, ctx) => {
|
|
503
492
|
await deps.promptForConfig(ctx, parseTelegramProfileArg(args));
|
|
504
493
|
},
|
|
@@ -513,28 +502,17 @@ export function registerTelegramBridgeCommands(
|
|
|
513
502
|
},
|
|
514
503
|
});
|
|
515
504
|
pi.registerCommand("telegram-connect", {
|
|
516
|
-
description:
|
|
517
|
-
"Start the Telegram bridge. Use /telegram-connect <profile> and optional as=Name for a fresh Workspace Thread.",
|
|
505
|
+
description: "<profile> — Start Telegram bridge",
|
|
518
506
|
handler: async (args, ctx) => {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
const requestedThreadName = parseTelegramRequestedThreadName(args);
|
|
525
|
-
const requestedNameError =
|
|
526
|
-
requestedNameTokens.length > 1
|
|
527
|
-
? "Specify at most one as=Name Workspace Thread name."
|
|
528
|
-
: requestedNameTokens.length === 1 && !requestedThreadName
|
|
529
|
-
? "Usage: /telegram-connect [profile] as=Flightprice"
|
|
530
|
-
: requestedThreadName
|
|
531
|
-
? deps.validateThreadName?.(requestedThreadName)
|
|
532
|
-
: undefined;
|
|
533
|
-
if (requestedNameError) {
|
|
534
|
-
ctx.ui.notify(requestedNameError, "warning");
|
|
507
|
+
if (args.trim().split(/\s+/).some((word) => /^as=/i.test(word))) {
|
|
508
|
+
ctx.ui.notify(
|
|
509
|
+
"Thread names are configured from Telegram, not from Pi commands.",
|
|
510
|
+
"warning",
|
|
511
|
+
);
|
|
535
512
|
deps.updateStatus(ctx);
|
|
536
513
|
return;
|
|
537
514
|
}
|
|
515
|
+
const profileName = parseTelegramProfileArg(args);
|
|
538
516
|
if (profileName && deps.activateProfileConfig) {
|
|
539
517
|
const ok = await deps.activateProfileConfig(ctx, profileName);
|
|
540
518
|
if (!ok) {
|
|
@@ -597,7 +575,6 @@ export function registerTelegramBridgeCommands(
|
|
|
597
575
|
};
|
|
598
576
|
let result = await startWithRecovery({
|
|
599
577
|
forceFreshLeaderThread: true,
|
|
600
|
-
...(requestedThreadName ? { requestedThreadName } : {}),
|
|
601
578
|
});
|
|
602
579
|
if (result && !result.ok && result.canTakeover) {
|
|
603
580
|
const confirmed = await ctx.ui.confirm(
|
|
@@ -612,7 +589,6 @@ export function registerTelegramBridgeCommands(
|
|
|
612
589
|
result = await startWithRecovery({
|
|
613
590
|
force: true,
|
|
614
591
|
forceFreshLeaderThread: true,
|
|
615
|
-
...(requestedThreadName ? { requestedThreadName } : {}),
|
|
616
592
|
});
|
|
617
593
|
}
|
|
618
594
|
if (result?.message) {
|
|
@@ -625,8 +601,7 @@ export function registerTelegramBridgeCommands(
|
|
|
625
601
|
},
|
|
626
602
|
});
|
|
627
603
|
pi.registerCommand("telegram-disconnect", {
|
|
628
|
-
description:
|
|
629
|
-
"Stop Telegram; in Threaded Mode, delete this instance's current thread",
|
|
604
|
+
description: "Stop Telegram and delete current thread in Threaded Mode",
|
|
630
605
|
handler: async (_args, ctx) => {
|
|
631
606
|
const threadName = deps.getDisconnectThreadName?.();
|
|
632
607
|
if (threadName) {
|
|
@@ -2337,7 +2312,7 @@ async function handleTelegramCommandRuntime<
|
|
|
2337
2312
|
|
|
2338
2313
|
export const TELEGRAM_INTERNAL_COMMAND_NAME = "telegram-internal";
|
|
2339
2314
|
export const TELEGRAM_INTERNAL_COMMAND_DESCRIPTION =
|
|
2340
|
-
"
|
|
2315
|
+
"Internal Telegram command cannot be run manually";
|
|
2341
2316
|
export const TELEGRAM_INTERNAL_MANUAL_USE_MESSAGE =
|
|
2342
2317
|
"This internal Telegram command cannot be run manually.";
|
|
2343
2318
|
|
|
@@ -2542,6 +2517,7 @@ export function createTelegramSessionActionAssembly(
|
|
|
2542
2517
|
|
|
2543
2518
|
type TelegramPendingInternalAction = {
|
|
2544
2519
|
kind: "replace-session";
|
|
2520
|
+
token: string;
|
|
2545
2521
|
updateId: number;
|
|
2546
2522
|
target: { chatId: number; threadId?: number; messageId: number };
|
|
2547
2523
|
};
|
|
@@ -2582,9 +2558,9 @@ export function createTelegramSessionActionRuntime(
|
|
|
2582
2558
|
registered = true;
|
|
2583
2559
|
deps.registerCommand(TELEGRAM_INTERNAL_COMMAND_NAME, {
|
|
2584
2560
|
description: TELEGRAM_INTERNAL_COMMAND_DESCRIPTION,
|
|
2585
|
-
handler: async (
|
|
2561
|
+
handler: async (args, ctx) => {
|
|
2586
2562
|
const action = pendingAction;
|
|
2587
|
-
if (!action) {
|
|
2563
|
+
if (!action || args.trim() !== action.token) {
|
|
2588
2564
|
ctx.ui.notify(TELEGRAM_INTERNAL_MANUAL_USE_MESSAGE, "warning");
|
|
2589
2565
|
return;
|
|
2590
2566
|
}
|
|
@@ -2616,10 +2592,11 @@ export function createTelegramSessionActionRuntime(
|
|
|
2616
2592
|
const target = pendingTarget;
|
|
2617
2593
|
pendingTarget = undefined;
|
|
2618
2594
|
if (!target) return;
|
|
2619
|
-
|
|
2595
|
+
const token = randomUUID();
|
|
2596
|
+
pendingAction = { kind: "replace-session", token, updateId, target };
|
|
2620
2597
|
void Promise.resolve()
|
|
2621
2598
|
.then(() =>
|
|
2622
|
-
deps.sendUserMessage(`/${TELEGRAM_INTERNAL_COMMAND_NAME}`, {
|
|
2599
|
+
deps.sendUserMessage(`/${TELEGRAM_INTERNAL_COMMAND_NAME} ${token}`, {
|
|
2623
2600
|
expandPromptTemplates: true,
|
|
2624
2601
|
}),
|
|
2625
2602
|
)
|
|
@@ -871,6 +871,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
871
871
|
storeModelMenuState: modelMenuRuntime.storeState,
|
|
872
872
|
updateStatusMessage: menuActions.updateStatusMessage,
|
|
873
873
|
updateStatus,
|
|
874
|
+
dismissGuestPlaceholder: guestPlaceholderRuntime.dismiss,
|
|
874
875
|
});
|
|
875
876
|
const threadDisplaySettingsRuntime =
|
|
876
877
|
ThreadDisplay.createTelegramThreadDisplaySettingsRuntime({
|
|
@@ -1731,14 +1732,6 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1731
1732
|
if (!record?.target.threadId) return undefined;
|
|
1732
1733
|
return record.threadName ?? "current Telegram thread";
|
|
1733
1734
|
},
|
|
1734
|
-
setRequestedThreadNameForPollingStart:
|
|
1735
|
-
telegramThreadCapabilityState.setRequestedThreadName,
|
|
1736
|
-
validateThreadName(threadName) {
|
|
1737
|
-
return Threads.getTelegramTopicThreadNameValidationError(
|
|
1738
|
-
threadName,
|
|
1739
|
-
undefined,
|
|
1740
|
-
);
|
|
1741
|
-
},
|
|
1742
1735
|
onTransportChanged() {
|
|
1743
1736
|
deliveryLifecycleRuntime.onSessionStart();
|
|
1744
1737
|
activityVerbosityRuntime.reset();
|
|
@@ -31,6 +31,8 @@ type TelegramQueueMenuReplyMarkup = TelegramInlineKeyboardMarkup;
|
|
|
31
31
|
interface TelegramQueueMenuItem {
|
|
32
32
|
chatId: number;
|
|
33
33
|
replyToMessageId: number;
|
|
34
|
+
queueOrder: number;
|
|
35
|
+
isGuest: boolean;
|
|
34
36
|
queuePosition: number;
|
|
35
37
|
isPriority: boolean;
|
|
36
38
|
priorityEmoji?: string;
|
|
@@ -60,6 +62,8 @@ function toTelegramQueueMenuItems<Context>(
|
|
|
60
62
|
return {
|
|
61
63
|
chatId: item.chatId,
|
|
62
64
|
replyToMessageId: item.replyToMessageId,
|
|
65
|
+
queueOrder: item.queueOrder,
|
|
66
|
+
isGuest: item.kind === "prompt" && item.guestQueryId !== undefined,
|
|
63
67
|
queuePosition: index + 1,
|
|
64
68
|
isPriority: item.queueLane === "priority",
|
|
65
69
|
priorityEmoji: item.kind === "prompt" ? item.priorityEmoji : undefined,
|
|
@@ -106,7 +110,9 @@ function buildTelegramQueueMenuReplyMarkup(
|
|
|
106
110
|
return [
|
|
107
111
|
{
|
|
108
112
|
text: label,
|
|
109
|
-
callback_data:
|
|
113
|
+
callback_data: item.isGuest
|
|
114
|
+
? `queue:guest-pick:${item.queueOrder}`
|
|
115
|
+
: `queue:pick:${item.chatId}:${item.replyToMessageId}`,
|
|
110
116
|
},
|
|
111
117
|
];
|
|
112
118
|
});
|
|
@@ -178,6 +184,22 @@ function getTelegramQueueMenuItemText(item: TelegramQueueMenuItem): string {
|
|
|
178
184
|
return `${heading}\n${preview}`;
|
|
179
185
|
}
|
|
180
186
|
|
|
187
|
+
function buildTelegramGuestQueueItemSubmenuReplyMarkup(
|
|
188
|
+
queueOrder: number,
|
|
189
|
+
): TelegramQueueMenuReplyMarkup {
|
|
190
|
+
return {
|
|
191
|
+
inline_keyboard: [
|
|
192
|
+
[{ text: "⬆️ Back", callback_data: "queue:list" }],
|
|
193
|
+
[
|
|
194
|
+
{
|
|
195
|
+
text: "🔴 Skip",
|
|
196
|
+
callback_data: `queue:guest-skip:${queueOrder}`,
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
],
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
181
203
|
function buildTelegramQueueItemSubmenuReplyMarkup(
|
|
182
204
|
chatId: number,
|
|
183
205
|
replyToMessageId: number,
|
|
@@ -217,6 +239,8 @@ interface TelegramQueueMenuCallbackDeps<Context = unknown> {
|
|
|
217
239
|
chatId: number,
|
|
218
240
|
replyToMessageId: number,
|
|
219
241
|
) => TelegramQueueMenuItem | undefined;
|
|
242
|
+
findGuestItem: (queueOrder: number) => TelegramQueueMenuItem | undefined;
|
|
243
|
+
skipGuest: (queueOrder: number, ctx: Context) => Promise<boolean>;
|
|
220
244
|
togglePriority: (chatId: number, replyToMessageId: number) => boolean;
|
|
221
245
|
setPriority: (
|
|
222
246
|
chatId: number,
|
|
@@ -275,6 +299,38 @@ async function handleTelegramQueueMenuCallback<Context>(
|
|
|
275
299
|
);
|
|
276
300
|
return true;
|
|
277
301
|
}
|
|
302
|
+
const guestPickMatch = data.match(/^queue:guest-pick:(\d+)$/);
|
|
303
|
+
if (guestPickMatch) {
|
|
304
|
+
await handleTelegramGuestQueueMenuPick(
|
|
305
|
+
callbackQueryId,
|
|
306
|
+
replyChatId,
|
|
307
|
+
replyMessageId,
|
|
308
|
+
Number(guestPickMatch[1]),
|
|
309
|
+
deps,
|
|
310
|
+
);
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
const guestSkipMatch = data.match(/^queue:guest-skip:(\d+)$/);
|
|
314
|
+
if (guestSkipMatch) {
|
|
315
|
+
const skipped = await deps.skipGuest(Number(guestSkipMatch[1]), ctx);
|
|
316
|
+
if (!skipped) {
|
|
317
|
+
await refreshStaleTelegramQueueMenuItem(
|
|
318
|
+
callbackQueryId,
|
|
319
|
+
replyChatId,
|
|
320
|
+
replyMessageId,
|
|
321
|
+
deps,
|
|
322
|
+
);
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
await updateTelegramQueueMenuList(
|
|
326
|
+
callbackQueryId,
|
|
327
|
+
replyChatId,
|
|
328
|
+
replyMessageId,
|
|
329
|
+
deps,
|
|
330
|
+
"Guest prompt skipped.",
|
|
331
|
+
);
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
278
334
|
const pickMatch = data.match(/^queue:pick:(\d+):(\d+)$/);
|
|
279
335
|
if (pickMatch) {
|
|
280
336
|
await handleTelegramQueueMenuPick(
|
|
@@ -379,6 +435,31 @@ async function refreshStaleTelegramQueueMenuItem<Context>(
|
|
|
379
435
|
);
|
|
380
436
|
}
|
|
381
437
|
|
|
438
|
+
async function handleTelegramGuestQueueMenuPick<Context>(
|
|
439
|
+
callbackQueryId: string,
|
|
440
|
+
replyChatId: number,
|
|
441
|
+
replyMessageId: number,
|
|
442
|
+
queueOrder: number,
|
|
443
|
+
deps: TelegramQueueMenuCallbackDeps<Context>,
|
|
444
|
+
): Promise<void> {
|
|
445
|
+
const item = deps.findGuestItem(queueOrder);
|
|
446
|
+
if (!item) {
|
|
447
|
+
return refreshStaleTelegramQueueMenuItem(
|
|
448
|
+
callbackQueryId,
|
|
449
|
+
replyChatId,
|
|
450
|
+
replyMessageId,
|
|
451
|
+
deps,
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
await deps.updateQueueMessage(
|
|
455
|
+
replyChatId,
|
|
456
|
+
replyMessageId,
|
|
457
|
+
getTelegramQueueMenuItemText(item),
|
|
458
|
+
buildTelegramGuestQueueItemSubmenuReplyMarkup(queueOrder),
|
|
459
|
+
);
|
|
460
|
+
await deps.answerCallbackQuery(callbackQueryId);
|
|
461
|
+
}
|
|
462
|
+
|
|
382
463
|
async function handleTelegramQueueMenuPick<Context>(
|
|
383
464
|
callbackQueryId: string,
|
|
384
465
|
replyChatId: number,
|
|
@@ -606,6 +687,7 @@ export function createTelegramQueueMenuRuntime<
|
|
|
606
687
|
ctx: Context,
|
|
607
688
|
) => Promise<void>;
|
|
608
689
|
updateStatus: (ctx: Context) => void;
|
|
690
|
+
dismissGuestPlaceholder?: (inlineMessageId: string) => Promise<void>;
|
|
609
691
|
}): TelegramQueueMenuRuntime<Context> {
|
|
610
692
|
const sendQueueMenuMessage = createQueueMenuSendMessageAdapter(
|
|
611
693
|
deps.sendInteractiveMessage,
|
|
@@ -628,6 +710,7 @@ export function createTelegramQueueMenuRuntime<
|
|
|
628
710
|
updateStatusMessage: deps.updateStatusMessage,
|
|
629
711
|
answerCallbackQuery: deps.answerCallbackQuery,
|
|
630
712
|
updateStatus: deps.updateStatus,
|
|
713
|
+
dismissGuestPlaceholder: deps.dismissGuestPlaceholder,
|
|
631
714
|
}),
|
|
632
715
|
};
|
|
633
716
|
}
|
|
@@ -695,6 +778,7 @@ function createQueueMenuCallbackHandler<
|
|
|
695
778
|
text?: string,
|
|
696
779
|
) => Promise<void>;
|
|
697
780
|
updateStatus: (ctx: Context) => void;
|
|
781
|
+
dismissGuestPlaceholder?: (inlineMessageId: string) => Promise<void>;
|
|
698
782
|
}) {
|
|
699
783
|
return async (
|
|
700
784
|
query: TelegramQueueMenuCallbackQuery,
|
|
@@ -737,6 +821,11 @@ function createQueueMenuCallbackHandler<
|
|
|
737
821
|
const findItem = (cId: number, rId: number) => {
|
|
738
822
|
return findTelegramQueueMenuItem(toMenuItems(), cId, rId);
|
|
739
823
|
};
|
|
824
|
+
const findGuestItem = (queueOrder: number) => {
|
|
825
|
+
return toMenuItems().find((item) => {
|
|
826
|
+
return item.isGuest && item.queueOrder === queueOrder;
|
|
827
|
+
});
|
|
828
|
+
};
|
|
740
829
|
return handleTelegramQueueMenuCallback(
|
|
741
830
|
query.id,
|
|
742
831
|
data,
|
|
@@ -746,6 +835,14 @@ function createQueueMenuCallbackHandler<
|
|
|
746
835
|
{
|
|
747
836
|
getQueuedItems: toMenuItems,
|
|
748
837
|
findItem,
|
|
838
|
+
findGuestItem,
|
|
839
|
+
skipGuest: (queueOrder, callbackContext) => {
|
|
840
|
+
return skipQueuedTelegramGuestPrompt(queueOrder, callbackContext, {
|
|
841
|
+
getQueueSnapshot,
|
|
842
|
+
queueMutationRuntime: deps.queueMutationRuntime,
|
|
843
|
+
dismissGuestPlaceholder: deps.dismissGuestPlaceholder,
|
|
844
|
+
});
|
|
845
|
+
},
|
|
749
846
|
togglePriority: (cId, rId) => {
|
|
750
847
|
return toggleQueuedTelegramPromptPriority(cId, rId, ctx, {
|
|
751
848
|
getQueueSnapshot,
|
|
@@ -772,6 +869,34 @@ function createQueueMenuCallbackHandler<
|
|
|
772
869
|
};
|
|
773
870
|
}
|
|
774
871
|
|
|
872
|
+
async function skipQueuedTelegramGuestPrompt<Context>(
|
|
873
|
+
queueOrder: number,
|
|
874
|
+
ctx: Context,
|
|
875
|
+
deps: {
|
|
876
|
+
getQueueSnapshot: () => Queue.TelegramQueueItem<Context>[];
|
|
877
|
+
queueMutationRuntime: Queue.TelegramQueueMutationController<Context>;
|
|
878
|
+
dismissGuestPlaceholder?: (inlineMessageId: string) => Promise<void>;
|
|
879
|
+
},
|
|
880
|
+
): Promise<boolean> {
|
|
881
|
+
const item = deps.getQueueSnapshot().find((candidate) => {
|
|
882
|
+
return candidate.kind === "prompt" &&
|
|
883
|
+
candidate.guestQueryId !== undefined &&
|
|
884
|
+
candidate.queueOrder === queueOrder;
|
|
885
|
+
});
|
|
886
|
+
if (!item || !deps.queueMutationRuntime.removeGuestPromptByQueueOrder) {
|
|
887
|
+
return false;
|
|
888
|
+
}
|
|
889
|
+
const removed = deps.queueMutationRuntime.removeGuestPromptByQueueOrder(
|
|
890
|
+
queueOrder,
|
|
891
|
+
ctx,
|
|
892
|
+
);
|
|
893
|
+
if (!removed) return false;
|
|
894
|
+
if (item.guestInlineMessageId && deps.dismissGuestPlaceholder) {
|
|
895
|
+
await deps.dismissGuestPlaceholder(item.guestInlineMessageId);
|
|
896
|
+
}
|
|
897
|
+
return true;
|
|
898
|
+
}
|
|
899
|
+
|
|
775
900
|
function getQueueMenuReactionDisposition<Context>(
|
|
776
901
|
item: Queue.TelegramQueueItem<Context>,
|
|
777
902
|
priority: boolean,
|
|
@@ -922,6 +922,28 @@ export function removeTelegramQueueItemsByMessageIds<TContext = unknown>(
|
|
|
922
922
|
};
|
|
923
923
|
}
|
|
924
924
|
|
|
925
|
+
export function removeTelegramQueuedGuestPromptByOrder<TContext = unknown>(
|
|
926
|
+
items: TelegramQueueItem<TContext>[],
|
|
927
|
+
queueOrder: number,
|
|
928
|
+
): {
|
|
929
|
+
items: TelegramQueueItem<TContext>[];
|
|
930
|
+
removedItems: PendingTelegramTurn[];
|
|
931
|
+
removedCount: number;
|
|
932
|
+
} {
|
|
933
|
+
const index = items.findIndex((item) => {
|
|
934
|
+
return isPendingTelegramTurn(item) &&
|
|
935
|
+
item.guestQueryId !== undefined &&
|
|
936
|
+
item.queueOrder === queueOrder;
|
|
937
|
+
});
|
|
938
|
+
if (index < 0) return { items, removedItems: [], removedCount: 0 };
|
|
939
|
+
const removedItem = items[index] as PendingTelegramTurn;
|
|
940
|
+
return {
|
|
941
|
+
items: [...items.slice(0, index), ...items.slice(index + 1)],
|
|
942
|
+
removedItems: [removedItem],
|
|
943
|
+
removedCount: 1,
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
|
|
925
947
|
export function applyTelegramQueuePromptReactionDisposition<
|
|
926
948
|
TContext = unknown,
|
|
927
949
|
>(
|
|
@@ -2331,6 +2353,10 @@ export interface TelegramQueueMutationController<TContext> {
|
|
|
2331
2353
|
ctx: TContext,
|
|
2332
2354
|
scope?: TelegramQueueMessageScope,
|
|
2333
2355
|
) => number;
|
|
2356
|
+
removeGuestPromptByQueueOrder?: (
|
|
2357
|
+
queueOrder: number,
|
|
2358
|
+
ctx: TContext,
|
|
2359
|
+
) => boolean;
|
|
2334
2360
|
applyReactionByMessageId: (
|
|
2335
2361
|
messageId: number,
|
|
2336
2362
|
disposition: TelegramQueueReactionDisposition,
|
|
@@ -2604,6 +2630,11 @@ export function createTelegramQueueMutationController<TContext>(
|
|
|
2604
2630
|
buildRuntimeDeps(ctx),
|
|
2605
2631
|
scope,
|
|
2606
2632
|
),
|
|
2633
|
+
removeGuestPromptByQueueOrder: (queueOrder, ctx) =>
|
|
2634
|
+
removeTelegramQueuedGuestPromptByOrderRuntime(
|
|
2635
|
+
queueOrder,
|
|
2636
|
+
buildRuntimeDeps(ctx),
|
|
2637
|
+
),
|
|
2607
2638
|
applyReactionByMessageId: (messageId, disposition, ctx, scope) =>
|
|
2608
2639
|
applyTelegramQueuePromptReactionDispositionRuntime(
|
|
2609
2640
|
messageId,
|
|
@@ -2684,6 +2715,21 @@ export function removeTelegramQueueItemsByMessageIdsRuntime<TContext>(
|
|
|
2684
2715
|
return removedCount;
|
|
2685
2716
|
}
|
|
2686
2717
|
|
|
2718
|
+
export function removeTelegramQueuedGuestPromptByOrderRuntime<TContext>(
|
|
2719
|
+
queueOrder: number,
|
|
2720
|
+
deps: TelegramQueueMutationRuntimeDeps<TContext>,
|
|
2721
|
+
): boolean {
|
|
2722
|
+
const { items, removedItems, removedCount } =
|
|
2723
|
+
removeTelegramQueuedGuestPromptByOrder(deps.getQueuedItems(), queueOrder);
|
|
2724
|
+
if (removedCount === 0) return false;
|
|
2725
|
+
// A menu Skip means permanent discard, so settle durable admission before
|
|
2726
|
+
// removing the live item. A failed settlement keeps the prompt retryable.
|
|
2727
|
+
deps.onItemsDiscarded?.(removedItems, deps.ctx);
|
|
2728
|
+
deps.setQueuedItems(items);
|
|
2729
|
+
updateTelegramQueueStatusRuntime(deps);
|
|
2730
|
+
return true;
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2687
2733
|
export function applyTelegramQueuePromptReactionDispositionRuntime<TContext>(
|
|
2688
2734
|
messageId: number,
|
|
2689
2735
|
disposition: TelegramQueueReactionDisposition,
|
|
@@ -1018,6 +1018,10 @@ export const TELEGRAM_GUEST_PLACEHOLDER_FRAMES = [
|
|
|
1018
1018
|
"<b>🌏 Working on it...</b>",
|
|
1019
1019
|
] as const;
|
|
1020
1020
|
|
|
1021
|
+
// Telegram does not expose deletion for inline messages. This non-printing,
|
|
1022
|
+
// non-empty edit visually clears a guest placeholder after its query is skipped.
|
|
1023
|
+
export const TELEGRAM_DISMISSED_GUEST_PLACEHOLDER_TEXT = "\u2063";
|
|
1024
|
+
|
|
1021
1025
|
export function buildTelegramGuestPlaceholderFrame(step: number): string {
|
|
1022
1026
|
const frames: readonly string[] = TELEGRAM_GUEST_PLACEHOLDER_FRAMES;
|
|
1023
1027
|
const index = ((step % frames.length) + frames.length) % frames.length;
|
|
@@ -1052,6 +1056,8 @@ export interface TelegramGuestPlaceholderRuntime {
|
|
|
1052
1056
|
stop: (inlineMessageId: string) => Promise<void>;
|
|
1053
1057
|
/** Cancels every loop without waiting for in-flight edits (session shutdown). */
|
|
1054
1058
|
stopAll: () => void;
|
|
1059
|
+
/** Stops rotation and visually clears the inline placeholder. */
|
|
1060
|
+
dismiss: (inlineMessageId: string) => Promise<void>;
|
|
1055
1061
|
}
|
|
1056
1062
|
|
|
1057
1063
|
interface TelegramGuestPlaceholderSession {
|
|
@@ -1202,5 +1208,18 @@ export function createTelegramGuestPlaceholderRuntime(
|
|
|
1202
1208
|
}
|
|
1203
1209
|
sessions.clear();
|
|
1204
1210
|
},
|
|
1211
|
+
async dismiss(inlineMessageId) {
|
|
1212
|
+
await stop(inlineMessageId);
|
|
1213
|
+
try {
|
|
1214
|
+
await deps.editGuestInlineMessage(inlineMessageId, {
|
|
1215
|
+
text: TELEGRAM_DISMISSED_GUEST_PLACEHOLDER_TEXT,
|
|
1216
|
+
parseMode: "HTML",
|
|
1217
|
+
});
|
|
1218
|
+
} catch (error) {
|
|
1219
|
+
deps.recordRuntimeEvent?.("guest", error, {
|
|
1220
|
+
phase: "guest-placeholder-dismiss",
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
},
|
|
1205
1224
|
};
|
|
1206
1225
|
}
|
|
@@ -684,7 +684,14 @@ export async function ensureTelegramLeaderThreadBinding(
|
|
|
684
684
|
assertLeaderEpoch("before-workspace-persist");
|
|
685
685
|
await deps.topicTargetStore.persist();
|
|
686
686
|
assertLeaderEpoch("after-workspace-persist");
|
|
687
|
-
|
|
687
|
+
const { target: _target, slot: _slot, threadName: _threadName, displayTitle: _displayTitle, ...rest } = result;
|
|
688
|
+
return {
|
|
689
|
+
...rest,
|
|
690
|
+
target: { ...committed.target },
|
|
691
|
+
slot: committed.slot ?? result.slot,
|
|
692
|
+
...(committed.threadName ? { threadName: committed.threadName } : {}),
|
|
693
|
+
...(committed.displayTitle ? { displayTitle: committed.displayTitle } : {}),
|
|
694
|
+
};
|
|
688
695
|
};
|
|
689
696
|
try {
|
|
690
697
|
const unavailableTargetKeys = new Set([
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-kit",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"@llblab/pi-codex-usage": "0.10.0",
|
|
46
46
|
"@llblab/pi-grow-loop": "0.8.1",
|
|
47
47
|
"@llblab/pi-state-flow": "0.16.3",
|
|
48
|
-
"@llblab/pi-telegram": "0.50.
|
|
48
|
+
"@llblab/pi-telegram": "0.50.1",
|
|
49
49
|
"@llblab/skills": "1.15.0"
|
|
50
50
|
},
|
|
51
51
|
"bundledDependencies": [
|