@llblab/pi-telegram 0.51.0 → 0.51.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/AGENTS.md +4 -3
- package/BACKLOG.md +3 -8
- package/CHANGELOG.md +11 -0
- package/README.md +1 -1
- package/dist/lib/bindings.d.ts +2 -0
- package/dist/lib/bindings.js +3 -4
- package/dist/lib/bus-api.d.ts +0 -1
- package/dist/lib/bus-api.js +0 -3
- package/dist/lib/bus-follower.d.ts +3 -1
- package/dist/lib/bus-follower.js +6 -1
- package/dist/lib/commands.d.ts +5 -0
- package/dist/lib/commands.js +9 -0
- package/dist/lib/extension.js +6 -3
- package/dist/lib/menu-model.d.ts +3 -3
- package/dist/lib/menu-model.js +17 -3
- package/dist/lib/menu.d.ts +5 -7
- package/dist/lib/menu.js +4 -5
- package/dist/lib/model.d.ts +12 -12
- package/dist/lib/model.js +49 -35
- package/dist/lib/queue.d.ts +8 -2
- package/dist/lib/queue.js +64 -19
- package/dist/lib/replies.d.ts +4 -0
- package/dist/lib/replies.js +14 -0
- package/dist/lib/routing.d.ts +1 -0
- package/dist/lib/routing.js +5 -1
- package/dist/lib/runtime.d.ts +3 -6
- package/dist/lib/runtime.js +4 -55
- package/dist/lib/telegram-api.js +36 -10
- package/dist/package.json +1 -1
- package/docs/architecture.md +9 -9
- package/docs/multi-instance-bus.md +3 -3
- package/docs/public-api.md +1 -1
- package/lib/bindings.ts +14 -4
- package/lib/bus-api.ts +0 -7
- package/lib/bus-follower.ts +13 -2
- package/lib/commands.ts +19 -0
- package/lib/extension.ts +10 -3
- package/lib/menu-model.ts +25 -4
- package/lib/menu.ts +14 -9
- package/lib/model.ts +77 -44
- package/lib/queue.ts +76 -24
- package/lib/replies.ts +18 -0
- package/lib/routing.ts +7 -1
- package/lib/runtime.ts +7 -58
- package/lib/telegram-api.ts +43 -17
- package/package.json +1 -1
package/dist/lib/telegram-api.js
CHANGED
|
@@ -900,7 +900,8 @@ export function createTelegramBridgeApiRuntime(deps) {
|
|
|
900
900
|
const chatActionMinIntervalMs = Math.max(0, deps.chatActionMinIntervalMs ?? 2_000);
|
|
901
901
|
const chatActionMaxGates = Math.max(1, deps.chatActionMaxGates ?? 256);
|
|
902
902
|
const chatActionGates = new Map();
|
|
903
|
-
const
|
|
903
|
+
const chatActionChatGates = new Map();
|
|
904
|
+
const getChatActionKeys = (method, body) => {
|
|
904
905
|
if (method !== "sendChatAction")
|
|
905
906
|
return undefined;
|
|
906
907
|
const chatId = body.chat_id;
|
|
@@ -909,32 +910,52 @@ export function createTelegramBridgeApiRuntime(deps) {
|
|
|
909
910
|
typeof action !== "string") {
|
|
910
911
|
return undefined;
|
|
911
912
|
}
|
|
913
|
+
const chat = String(chatId);
|
|
912
914
|
const threadId = body.message_thread_id;
|
|
913
|
-
return
|
|
914
|
-
|
|
915
|
-
|
|
915
|
+
return {
|
|
916
|
+
action: `${chat}:${typeof threadId === "number" || typeof threadId === "string"
|
|
917
|
+
? String(threadId)
|
|
918
|
+
: "all"}:${action}`,
|
|
919
|
+
chat,
|
|
920
|
+
};
|
|
916
921
|
};
|
|
917
922
|
const callRecorded = async (method, body, options) => {
|
|
918
923
|
const recoverError = deps.captureRequestErrorHandler?.(body);
|
|
919
|
-
const
|
|
920
|
-
if (
|
|
924
|
+
const chatActionKeys = getChatActionKeys(method, body);
|
|
925
|
+
if (chatActionKeys) {
|
|
921
926
|
const nowMs = now();
|
|
922
927
|
for (const [key, candidate] of chatActionGates) {
|
|
923
928
|
if (!candidate.inFlight && nowMs >= candidate.notBeforeMs) {
|
|
924
929
|
chatActionGates.delete(key);
|
|
925
930
|
}
|
|
926
931
|
}
|
|
927
|
-
|
|
932
|
+
for (const [key, candidate] of chatActionChatGates) {
|
|
933
|
+
if (!candidate.inFlight && nowMs >= candidate.notBeforeMs) {
|
|
934
|
+
chatActionChatGates.delete(key);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
let gate = chatActionGates.get(chatActionKeys.action);
|
|
928
938
|
if (!gate) {
|
|
929
939
|
if (chatActionGates.size >= chatActionMaxGates)
|
|
930
940
|
return true;
|
|
931
941
|
gate = { notBeforeMs: 0 };
|
|
932
|
-
chatActionGates.set(
|
|
942
|
+
chatActionGates.set(chatActionKeys.action, gate);
|
|
943
|
+
}
|
|
944
|
+
let chatGate = chatActionChatGates.get(chatActionKeys.chat);
|
|
945
|
+
if (!chatGate) {
|
|
946
|
+
if (chatActionChatGates.size >= chatActionMaxGates) {
|
|
947
|
+
return true;
|
|
948
|
+
}
|
|
949
|
+
chatGate = { notBeforeMs: 0 };
|
|
950
|
+
chatActionChatGates.set(chatActionKeys.chat, chatGate);
|
|
933
951
|
}
|
|
934
952
|
if (gate.inFlight)
|
|
935
953
|
return (await gate.inFlight);
|
|
936
|
-
if (
|
|
954
|
+
if (chatGate.inFlight)
|
|
937
955
|
return true;
|
|
956
|
+
if (nowMs < gate.notBeforeMs || nowMs < chatGate.notBeforeMs) {
|
|
957
|
+
return true;
|
|
958
|
+
}
|
|
938
959
|
let request;
|
|
939
960
|
request = Promise.resolve()
|
|
940
961
|
.then(() => deps.client.call(method, body, {
|
|
@@ -949,7 +970,9 @@ export function createTelegramBridgeApiRuntime(deps) {
|
|
|
949
970
|
await recoverRequestError(recoverError, error);
|
|
950
971
|
if (error instanceof TelegramApiHttpError && error.status === 429) {
|
|
951
972
|
const retryAfterMs = Math.max(chatActionMinIntervalMs, (error.retryAfterSeconds ?? 0) * 1_000);
|
|
952
|
-
|
|
973
|
+
const notBeforeMs = now() + retryAfterMs;
|
|
974
|
+
gate.notBeforeMs = notBeforeMs;
|
|
975
|
+
chatGate.notBeforeMs = Math.max(chatGate.notBeforeMs, notBeforeMs);
|
|
953
976
|
deps.recordRuntimeEvent("api", error, withTelegramTransportDiagnostics(error, {
|
|
954
977
|
method,
|
|
955
978
|
rateLimited: true,
|
|
@@ -963,8 +986,11 @@ export function createTelegramBridgeApiRuntime(deps) {
|
|
|
963
986
|
.finally(() => {
|
|
964
987
|
if (gate.inFlight === request)
|
|
965
988
|
gate.inFlight = undefined;
|
|
989
|
+
if (chatGate.inFlight === request)
|
|
990
|
+
chatGate.inFlight = undefined;
|
|
966
991
|
});
|
|
967
992
|
gate.inFlight = request;
|
|
993
|
+
chatGate.inFlight = request;
|
|
968
994
|
return request;
|
|
969
995
|
}
|
|
970
996
|
try {
|
package/dist/package.json
CHANGED
package/docs/architecture.md
CHANGED
|
@@ -518,7 +518,7 @@ Immediate controls:
|
|
|
518
518
|
- `/start` opens the main inline application menu.
|
|
519
519
|
- `/model`, `/thinking`, `/queue`, and `/settings` are hidden shortcuts to menu sections.
|
|
520
520
|
- `/compact` opens an inline confirmation dialog and then runs compaction when the bridge is idle.
|
|
521
|
-
- `/next` dispatches the next queued turn, aborting Pi first when needed. Active-turn settlement
|
|
521
|
+
- `/next` dispatches the next queued turn, aborting Pi first when needed. Target-bound command composition must forward the announcement request, exact-turn marker, superseding cancellation, and deferred-dispatch ports rather than silently dropping them. Active-turn settlement attempts its abort notice against the interrupted prompt before queue dispatch continues; definite or ambiguous notice failure is recorded but cannot block the selected turn. The queue owner then emits `Dispatching next queued turn.` against the exact selected prompt's chat/thread/reply id before its one model dispatch; skipped, inactive, pending-mutation, and admission-blocked candidates never receive it. A successful dispatch notice carries its reply-dedup anchor through the following agent-start reset, so later messages in that turn do not repeat the queued-prompt reply header. A later `/abort` or `/stop` cancels both pending `/next` notices before taking ownership, preventing duplicate abort results and stale future dispatch announcements. The `/next` command itself is never a lifecycle-notice reply target. Aborted pending assistant text is not projected as a second reply, while already completed intermediate output remains visible.
|
|
522
522
|
- `/abort` aborts active work while preserving queued items. Abort-history preservation is enabled only for Telegram-owned active turns; later local/non-Telegram agent starts clear stale abort-history mode so the next Telegram prompt appends instead of absorbing old queued turns as history.
|
|
523
523
|
- `/stop` aborts and clears waiting Telegram queue items.
|
|
524
524
|
|
|
@@ -526,7 +526,7 @@ Queued controls:
|
|
|
526
526
|
|
|
527
527
|
- `/continue` creates a priority Telegram-owned `continue` prompt.
|
|
528
528
|
- Prompt-template commands expand Telegram-safe Pi template aliases before entering the prompt queue.
|
|
529
|
-
- Model-switch continuation uses the control lane when
|
|
529
|
+
- Model-switch continuation uses the control lane when any interruptible in-flight agent run in the current Pi session must be stopped and resumed. A Telegram-owned run retains its prompt target; otherwise the exact model-menu chat/Thread/message supplies continuation and reply ownership.
|
|
530
530
|
|
|
531
531
|
Queue and menu mutations are reachable through Telegram updates handled by the current polling owner. After ownership moves, the old instance keeps processing its accepted local queue, but it no longer receives new menu callbacks or control updates for remote mutation. UI label, navigation, tab, toggle, card, and dialog rules are defined in [UI Style](./ui-style.md). Callback prefix ownership is defined in [Callback Namespaces](./callback-namespaces.md).
|
|
532
532
|
|
|
@@ -539,10 +539,10 @@ Native typing during compaction follows connected-instance activity rather than
|
|
|
539
539
|
- Confirmed manual `/compact` starts a native `typing` keepalive in the command target and stops it on completion/failure.
|
|
540
540
|
- Automatic/session compaction with an active Telegram turn reuses that turn's target.
|
|
541
541
|
- Automatic/session compaction without an active Telegram turn uses the connected instance's assigned target; an unconnected instance sends nothing.
|
|
542
|
-
- Thread-targeted typing is sent to the concrete thread
|
|
542
|
+
- Thread-targeted typing is sent only to the concrete thread. Aggregate `All` mirroring is intentionally omitted because duplicating every keepalive multiplies shared-chat flood pressure. Compaction completion, failure, or timeout stops only a loop actually started by the compaction observer; a pre-existing agent-owned loop remains active. Authority loss and shutdown still stop the keyed loop.
|
|
543
543
|
- Pi `ui_prompt_start` pauses typing while an extension-owned local prompt waits for the operator; `ui_prompt_end` emits the matching Activity boundary and resumes typing whenever agent or compaction work remains unsettled.
|
|
544
544
|
|
|
545
|
-
At every connected instance `agent_start`, the lifecycle binding starts Telegram's native `…typing` indicator in that instance's assigned target, whether the run came from Telegram, the local TUI, or an autonomous continuation such as Grow Loop. Terminal `Active` remains Telegram-turn-specific; the native indicator answers the separate question of whether the instance is doing agent work. Each loop keeps one action in flight
|
|
545
|
+
At every connected instance `agent_start`, the lifecycle binding starts Telegram's native `…typing` indicator in that instance's assigned target, whether the run came from Telegram, the local TUI, or an autonomous continuation such as Grow Loop. Terminal `Active` remains Telegram-turn-specific; the native indicator answers the separate question of whether the instance is doing agent work. Each loop refreshes its exact target every three seconds and keeps one action in flight. The leader API runtime coalesces identical chat/thread/action calls for two seconds, permits at most one concurrent chat action per chat, and shares a Telegram 429 `retry_after` fence across every Thread key in that chat; expired gates prune opportunistically and each gate family retains at most 256 active keys. Suppression never schedules a delayed retry. A failed typing action remains a structured diagnostic but cannot project `error` onto an otherwise healthy connected/leader/follower status. Assistant message start/update hooks still re-arm typing during Telegram-owned turns so transient provider/model errors do not leave a continuing run without activity feedback, and agent/session completion stops it.
|
|
546
546
|
|
|
547
547
|
### Rendering And Delivery
|
|
548
548
|
|
|
@@ -625,14 +625,14 @@ Telegram prompt guidance is context- and authority-aware. The package and source
|
|
|
625
625
|
|
|
626
626
|
## In-Flight Model Switching
|
|
627
627
|
|
|
628
|
-
When `/model` is used during an active
|
|
628
|
+
When `/model` is used during an interruptible active agent run in the current Pi session, the bridge emulates Pi's interactive stop/switch/continue workflow:
|
|
629
629
|
|
|
630
630
|
1. Apply the selected model immediately.
|
|
631
|
-
2. Queue or stage a synthetic Telegram continuation turn.
|
|
632
|
-
3. Abort
|
|
633
|
-
4. Dispatch the continuation
|
|
631
|
+
2. Queue or stage a synthetic Telegram continuation turn before aborting.
|
|
632
|
+
3. Abort immediately, or wait for every active tool execution to finish before aborting.
|
|
633
|
+
4. Dispatch the continuation in the same session context under the selected model.
|
|
634
634
|
|
|
635
|
-
|
|
635
|
+
A Telegram-originated run retains its active prompt target and reply anchor. For local/TUI or autonomous work without an active Telegram turn, the exact authorized model-menu chat, Thread, and message become the continuation target and reply anchor. Merely busy non-agent lifecycle work remains ineligible because no active agent abort handler exists. Pending selection and fallback-target state clear together on cancellation, new agent start, settlement, or session replacement.
|
|
636
636
|
|
|
637
637
|
## Shutdown And Timer Lifecycle
|
|
638
638
|
|
|
@@ -336,7 +336,7 @@ In Telegram private-chat Threaded Mode:
|
|
|
336
336
|
- Prompts typed in a thread route to the owning instance.
|
|
337
337
|
- Replies, previews, files, voice, and buttons stay in that thread.
|
|
338
338
|
- Queue controls and reactions affect only that instance target.
|
|
339
|
-
- Telegram's native `…typing` indicator for real agent work is
|
|
339
|
+
- Telegram's native `…typing` indicator for real agent work is refreshed only in that instance's exact Thread. Aggregate `All` mirroring is intentionally omitted because it duplicates every keepalive against the shared chat and amplifies Telegram flood control. Telegram turns, local prompts, and autonomous continuations all use the exact instance target. Terminal `Active` remains Telegram-turn-specific. Startup/connect/reload/recovery must not send activity by themselves.
|
|
340
340
|
- Generic heartbeat pruning remains silent and preserves the thread as a restart hint. With cleanup enabled, a later exact-PID death confirmation may delete it without posting an `Instance offline` notice. With cleanup disabled, a fenced owner-detachment commit records inactivity but retains the Thread and its Workspace slot for restoration or fully guarded capacity rotation. Confirmed deletion removes live routing authority but retains the Workspace binding's friendly name and uppercase ordering slot; a later authenticated reopen probes the old target, replaces it only on exact stale/deleted evidence, and carries that name and slot onto the new Thread.
|
|
341
341
|
- If the same binding identity returns, authenticated registration can reclaim the thread after the required visibility proof.
|
|
342
342
|
|
|
@@ -406,7 +406,7 @@ Threaded Mode should make follower threads behave like normal Telegram instance
|
|
|
406
406
|
| Replies/finals | Final replies land in the same thread | Follower finals go through leader transport into follower thread | Outbound calls carry target and inject `message_thread_id` | Reply delivery and bus API tests |
|
|
407
407
|
| Previews/Rich Drafts | Draft previews use the active thread target | Follower previews use the same native draft lifecycle through the leader | Preview transport preserves target and draft id | Preview thread-target tests |
|
|
408
408
|
| Attachments/voice | Files and voice upload in the instance thread | Follower uploads route through leader multipart transport | Multipart calls are target-scoped and follower-authorized | Bus allowlist and outbound delivery tests |
|
|
409
|
-
| Native activity status | `sendChatAction(typing)`
|
|
409
|
+
| Native activity status | `sendChatAction(typing)` refreshes Telegram's native `…typing` indicator only in the assigned Thread for every agent run, including local and autonomous work; aggregate `All` is not mirrored, terminal identity remains `connected`, and active work stays included in the green Queue count | Followers route the exact Thread action through leader transport while retaining their stable terminal `follower` identity and green activity count; the leader admits one concurrent action per chat and shares Telegram 429 cooldown across that chat's Thread keys | Agent lifecycle targets the active Telegram turn when present and otherwise the instance binding; one keyed loop avoids duplicate aggregate sends/rate-limit pressure | Agent-start binding, typing-loop, target-routing, and terminal-status regressions |
|
|
410
410
|
| Leader election / promotion | Current leader keeps its thread across reload | A promoted follower keeps its existing thread, slot, and name when elected and after later reload | Promotion converts the current follower binding into the leader profile before forced lock acquisition, so leader startup reuses it instead of provisioning a new thread | Follower heartbeat recovery passes binding snapshot into promotion; own-topic provisioner reuses promoted bindings |
|
|
411
411
|
| `/start` command/menu bootstrap | Registers visible bot commands and opens the menu | Follower `/start` can refresh the bot command menu through the leader and open its local menu without warnings | Bot command registration is a validated global Bot API call allowed through trusted follower bus transport | Bus allowlist regression for `setMyCommands` |
|
|
412
412
|
| Follower reconnect | Existing leader binding is reused only when still usable | Same-process `/new` or `/reload` suspends the old follower socket/context and automatically re-registers the new session to the exact prior target; explicit reconnect to a genuinely closed/stale Telegram tab still recreates a visible thread before success | A short-lived handoff carries the assigned target across session replacement, and the leader transfers that binding to the new runtime instance id by stable manual-follower identity; stale Bot API errors remain the proof for fresh provisioning | Session handoff/refresh, leader binding-transfer, persisted leader-reload reuse, and stale-target replacement regressions |
|
|
@@ -485,7 +485,7 @@ All files containing routing, chat ids, thread ids, or process details use priva
|
|
|
485
485
|
|
|
486
486
|
### Follower heartbeat is missed
|
|
487
487
|
|
|
488
|
-
- Leader tolerates up to 15 seconds without a follower heartbeat before pruning it from the live registry, so short local event-loop or IPC stalls do not create false routing gaps.
|
|
488
|
+
- Leader tolerates up to 15 seconds without a follower heartbeat before pruning it from the live registry, so short local event-loop or IPC stalls do not create false routing gaps. Follower heartbeat requests use a dedicated eight-second response deadline aligned with leader stale-liveness policy rather than the generic one-second local-RPC default. The deadline also defers its final timeout through one socket poll phase: an acknowledgement already buffered while the Pi/TUI event loop was blocked wins, while a genuinely silent peer still fails in the same event-loop turn. Heartbeat pruning remains liveness bookkeeping. One leader generation owns at most one prune operation; stop makes late endpoint, policy, and cleanup settlement inert, while durable-profile mutation serialization prevents replacement registration from crossing confirmed-dead cleanup.
|
|
489
489
|
- A missed heartbeat does not delete, close, mark offline, or send a disconnected notice for the follower's Telegram thread binding because the common cause may be leader reload, IPC handoff, or transient reconnect rather than a dead follower.
|
|
490
490
|
- After removal from live routing, the current leader retains at most 26 volatile preservation observations with exact registered PID, generation, target, profile and leader epoch. The existing prune loop rechecks alive/unverifiable PIDs and retries non-destructive publication only after fresh explicit absence and a known disabled-cleanup policy. Missing identity or overflow stays protected. Observations never answer heartbeat/API/forwarding requests, launch processes, or authorize deletion. Enabling cleanup cancels deferred preservation rather than promoting it into destructive work; the existing first-prune cleanup path is not retried by this mechanism.
|
|
491
491
|
- Registry registration invalidates overlapping instance/profile-owner/target observations, even if that replacement is removed before the next prune. Admitted same-instance provisioning cancels the old observation before its store writes can precede live registration. Profile/epoch drift, registry clear and leader stop invalidate it; late old completion cannot erase a replacement runtime's observation. No observation survives leader restart, reconstructs a PID from an opaque instance ID, or invents legacy inactivity.
|
package/docs/public-api.md
CHANGED
|
@@ -53,7 +53,7 @@ Stable commands inside the paired Telegram DM:
|
|
|
53
53
|
- `/start` — pair when needed and open the main application menu.
|
|
54
54
|
- `/new` — after idle and empty-queue checks, request a new Pi session in the current classic chat or Thread. The bridge acknowledges the callback, deletes its confirmation, completes and removes the exact durable update, then dispatches one runtime-armed typed action through the internal `/telegram-internal` Pi gateway using `pi.sendUserMessage(..., { expandPromptTemplates: true })`. Manual invocation reports that the gateway cannot be run manually; the armed handler receives a real `ExtensionCommandContext` and calls `ctx.newSession()`; one discriminated durable intent preserves either exact classic Profile/CWD/session/chat continuity or the Thread binding with slot/name re-key. The successor CAS-claims that intent before sending one terminal result. Busy, identity-mismatch, and unavailable-host paths fail closed.
|
|
55
55
|
- `/compact` — open confirmation and compact when idle.
|
|
56
|
-
- `/next` — abort active work first when needed,
|
|
56
|
+
- `/next` — abort active work first when needed, attempt the interrupted prompt's abort notice, then reply `Dispatching next queued turn.` to the exact queued prompt selected for the next model turn. That dispatch notice owns the turn's one reply header; later answer messages do not reply to the same prompt again. Abort-notice failure is diagnostic and cannot block dispatch. A later `/abort` or `/stop` cancels both pending transition notices before taking ownership. The `/next` command itself is never the lifecycle-notice reply target, and aborted pending assistant text is suppressed.
|
|
57
57
|
- `/continue` — enqueue a priority `continue` prompt.
|
|
58
58
|
- `/abort` — abort active work and keep the queue; abort-history is scoped to Telegram-owned active turns.
|
|
59
59
|
- `/stop` — abort active Telegram-owned work and clear waiting Telegram queue items.
|
package/lib/bindings.ts
CHANGED
|
@@ -52,6 +52,7 @@ export interface TelegramQueueBindingRuntime<TContext> {
|
|
|
52
52
|
mutation: Queue.TelegramQueueMutationController<TContext>;
|
|
53
53
|
dispatchNext: (ctx: TContext) => void;
|
|
54
54
|
requestNextDispatchAnnouncement: () => void;
|
|
55
|
+
cancelNextDispatchAnnouncement: () => void;
|
|
55
56
|
watchdog: Queue.TelegramQueueDispatchWatchdogRuntime<TContext>;
|
|
56
57
|
}
|
|
57
58
|
|
|
@@ -96,6 +97,9 @@ export function createTelegramQueueBindingRuntime<TContext>(deps: {
|
|
|
96
97
|
updateStatus: (ctx: TContext, error?: string) => void;
|
|
97
98
|
sendTextReply: Queue.TelegramQueueDispatchRuntimeDeps<TContext>["sendTextReply"];
|
|
98
99
|
sendUserMessage: Queue.TelegramQueueDispatchRuntimeDeps<TContext>["sendUserMessage"];
|
|
100
|
+
reconcileNextDispatchAnnouncementReplyOwnership?: (
|
|
101
|
+
item: Queue.PendingTelegramTurn,
|
|
102
|
+
) => void;
|
|
99
103
|
recordRuntimeEvent?: TelegramRuntimeEventRecorder;
|
|
100
104
|
}): TelegramQueueBindingRuntime<TContext> {
|
|
101
105
|
const settleDiscardedItems = (
|
|
@@ -159,6 +163,8 @@ export function createTelegramQueueBindingRuntime<TContext>(deps: {
|
|
|
159
163
|
},
|
|
160
164
|
updateStatus: deps.updateStatus,
|
|
161
165
|
sendTextReply: deps.sendTextReply,
|
|
166
|
+
reconcileNextDispatchAnnouncementReplyOwnership:
|
|
167
|
+
deps.reconcileNextDispatchAnnouncementReplyOwnership,
|
|
162
168
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
163
169
|
...deps.promptDispatch,
|
|
164
170
|
sendUserMessage: deps.sendUserMessage,
|
|
@@ -167,6 +173,7 @@ export function createTelegramQueueBindingRuntime<TContext>(deps: {
|
|
|
167
173
|
mutation,
|
|
168
174
|
dispatchNext: dispatch.dispatchNext,
|
|
169
175
|
requestNextDispatchAnnouncement: dispatch.requestNextDispatchAnnouncement,
|
|
176
|
+
cancelNextDispatchAnnouncement: dispatch.cancelNextDispatchAnnouncement,
|
|
170
177
|
watchdog: Queue.createTelegramQueueDispatchWatchdogRuntime({
|
|
171
178
|
hasQueuedItems: deps.store.hasQueuedItems,
|
|
172
179
|
dispatchNextQueuedTelegramTurn: dispatch.dispatchNext,
|
|
@@ -1196,10 +1203,13 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
1196
1203
|
if (uiPromptActive || !canSendAgentActivity(ctx)) return false;
|
|
1197
1204
|
const turn = activeTurnRuntime.get();
|
|
1198
1205
|
const target = turn?.target ?? proactivePushTargetGetter();
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1206
|
+
return (
|
|
1207
|
+
promptDispatchRuntime.startTypingLoop(
|
|
1208
|
+
ctx,
|
|
1209
|
+
turn?.chatId ?? target?.chatId,
|
|
1210
|
+
{ target },
|
|
1211
|
+
) !== false
|
|
1212
|
+
);
|
|
1203
1213
|
};
|
|
1204
1214
|
const startActiveTurnTypingLoop = (ctx: Pi.ExtensionContext): void => {
|
|
1205
1215
|
if (uiPromptActive) return;
|
package/lib/bus-api.ts
CHANGED
|
@@ -82,13 +82,6 @@ function rejectTelegramDirectOwnership(method: string): Promise<never> {
|
|
|
82
82
|
);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
export function createTelegramAggregateTypingActionSender(
|
|
86
|
-
runtime: Pick<TelegramBridgeApiRuntime, "call">,
|
|
87
|
-
): (chatId: number) => Promise<unknown> {
|
|
88
|
-
return (chatId) =>
|
|
89
|
-
runtime.call("sendChatAction", { chat_id: chatId, action: "typing" });
|
|
90
|
-
}
|
|
91
|
-
|
|
92
85
|
export function createTelegramBusAwareApiRuntime(
|
|
93
86
|
deps: TelegramBusAwareApiRuntimeDeps,
|
|
94
87
|
): TelegramBridgeApiRuntime {
|
package/lib/bus-follower.ts
CHANGED
|
@@ -11,7 +11,11 @@ import { basename } from "node:path";
|
|
|
11
11
|
import * as Sync from "./sync.ts";
|
|
12
12
|
import * as Threads from "./threads.ts";
|
|
13
13
|
import { parseTelegramUpdateJournalQueueOwner, type TelegramUpdateJournalStoreOptions } from "./journal.ts";
|
|
14
|
-
import
|
|
14
|
+
import {
|
|
15
|
+
TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS,
|
|
16
|
+
type TelegramLockEntry,
|
|
17
|
+
type TelegramLockState,
|
|
18
|
+
} from "./locks.ts";
|
|
15
19
|
import type {
|
|
16
20
|
TelegramQueueHandoffPayload,
|
|
17
21
|
TelegramQueueHandoffStageResult,
|
|
@@ -60,6 +64,8 @@ export const TELEGRAM_BUS_FOLLOWER_PROMOTION_GRACE_MS = 2_500;
|
|
|
60
64
|
export const TELEGRAM_FOLLOWER_SESSION_HANDOFF_TTL_MS = 30_000;
|
|
61
65
|
export const TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS = 30_000;
|
|
62
66
|
export const TELEGRAM_BUS_FOLLOWER_REGISTRATION_WAIT_MS = 30_000;
|
|
67
|
+
export const TELEGRAM_BUS_FOLLOWER_HEARTBEAT_TIMEOUT_MS =
|
|
68
|
+
TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS;
|
|
63
69
|
export const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_ATTEMPTS =
|
|
64
70
|
TELEGRAM_BUS_REGISTRATION_RETRY.attempts;
|
|
65
71
|
export const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_DELAY_MS =
|
|
@@ -301,6 +307,7 @@ export interface TelegramBusFollowerRegistrationRuntimeDeps<
|
|
|
301
307
|
registrationRetryAttempts?: number;
|
|
302
308
|
registrationRetryDelayMs?: number;
|
|
303
309
|
heartbeatMs?: number;
|
|
310
|
+
heartbeatTimeoutMs?: number;
|
|
304
311
|
recordRuntimeEvent?: (
|
|
305
312
|
category: string,
|
|
306
313
|
error: unknown,
|
|
@@ -1619,6 +1626,10 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
1619
1626
|
const getNowMs = deps.getNowMs ?? Date.now;
|
|
1620
1627
|
const getPid = deps.getPid ?? (() => process.pid);
|
|
1621
1628
|
const heartbeatMs = deps.heartbeatMs ?? 1000;
|
|
1629
|
+
const heartbeatTimeoutMs =
|
|
1630
|
+
deps.heartbeatTimeoutMs ??
|
|
1631
|
+
deps.timeoutMs ??
|
|
1632
|
+
TELEGRAM_BUS_FOLLOWER_HEARTBEAT_TIMEOUT_MS;
|
|
1622
1633
|
const registrationTimeoutMs =
|
|
1623
1634
|
deps.registrationTimeoutMs ?? deps.timeoutMs ?? 30000;
|
|
1624
1635
|
const registrationRetryAttempts =
|
|
@@ -1679,7 +1690,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
1679
1690
|
try {
|
|
1680
1691
|
const response = await sendTelegramBusLocalEnvelope({
|
|
1681
1692
|
socketPath: leaderSocketPath,
|
|
1682
|
-
timeoutMs:
|
|
1693
|
+
timeoutMs: heartbeatTimeoutMs,
|
|
1683
1694
|
retry: getTelegramBusTransportRetryPolicy({
|
|
1684
1695
|
endpoint: leaderSocketPath,
|
|
1685
1696
|
operation: "operation",
|
package/lib/commands.ts
CHANGED
|
@@ -711,6 +711,7 @@ export interface TelegramCommandActionDeps<TMessage, TContext> {
|
|
|
711
711
|
export interface TelegramStopCommandDeps {
|
|
712
712
|
hasAbortHandler: () => boolean;
|
|
713
713
|
clearPendingModelSwitch: () => void;
|
|
714
|
+
cancelNextTransitionAnnouncements?: () => void;
|
|
714
715
|
clearQueuedTelegramItems: () => number;
|
|
715
716
|
setFoldQueuedPromptsIntoHistory: (fold: boolean) => void;
|
|
716
717
|
abortCurrentTurn: () => void;
|
|
@@ -1146,6 +1147,8 @@ export interface TelegramCommandRuntimeDeps<
|
|
|
1146
1147
|
isContextActive?: (ctx: TContext) => boolean;
|
|
1147
1148
|
dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
|
|
1148
1149
|
requestNextDispatchAnnouncement?: () => void;
|
|
1150
|
+
markActiveTurnNextAbortAnnouncement?: () => boolean;
|
|
1151
|
+
cancelNextTransitionAnnouncements?: () => void;
|
|
1149
1152
|
requestDeferredDispatchNextQueuedTelegramTurn?: (
|
|
1150
1153
|
dispatch: (ctx: TContext) => void,
|
|
1151
1154
|
) => void;
|
|
@@ -1352,6 +1355,7 @@ export async function handleTelegramStopCommand(
|
|
|
1352
1355
|
deps: TelegramStopCommandDeps,
|
|
1353
1356
|
): Promise<void> {
|
|
1354
1357
|
deps.clearPendingModelSwitch();
|
|
1358
|
+
deps.cancelNextTransitionAnnouncements?.();
|
|
1355
1359
|
const clearedCount = deps.clearQueuedTelegramItems();
|
|
1356
1360
|
deps.setFoldQueuedPromptsIntoHistory(false);
|
|
1357
1361
|
if (!deps.hasAbortHandler()) {
|
|
@@ -1385,6 +1389,7 @@ export async function handleTelegramAbortCommand(deps: {
|
|
|
1385
1389
|
hasAbortHandler: () => boolean;
|
|
1386
1390
|
hasActiveTelegramTurn: () => boolean;
|
|
1387
1391
|
clearPendingModelSwitch: () => void;
|
|
1392
|
+
cancelNextTransitionAnnouncements?: () => void;
|
|
1388
1393
|
abortCurrentTurn: () => void;
|
|
1389
1394
|
setFoldQueuedPromptsIntoHistory: (fold: boolean) => void;
|
|
1390
1395
|
updateStatus: () => void;
|
|
@@ -1394,6 +1399,7 @@ export async function handleTelegramAbortCommand(deps: {
|
|
|
1394
1399
|
) => Promise<void>;
|
|
1395
1400
|
}): Promise<void> {
|
|
1396
1401
|
deps.clearPendingModelSwitch();
|
|
1402
|
+
deps.cancelNextTransitionAnnouncements?.();
|
|
1397
1403
|
if (!deps.hasAbortHandler()) {
|
|
1398
1404
|
await deps.sendTextReply(
|
|
1399
1405
|
formatTelegramInformationHeading("💤", "No active turn."),
|
|
@@ -1418,6 +1424,7 @@ export async function handleTelegramNextCommand(deps: {
|
|
|
1418
1424
|
abortCurrentTurn: () => void;
|
|
1419
1425
|
dispatchNextQueuedTurn: () => void;
|
|
1420
1426
|
requestNextDispatchAnnouncement?: () => void;
|
|
1427
|
+
markActiveTurnNextAbortAnnouncement?: () => boolean;
|
|
1421
1428
|
clearFoldForDispatch: () => void;
|
|
1422
1429
|
updateStatus: () => void;
|
|
1423
1430
|
sendTextReply: (
|
|
@@ -1439,6 +1446,7 @@ export async function handleTelegramNextCommand(deps: {
|
|
|
1439
1446
|
if (!deps.isIdle() && deps.hasAbortHandler()) {
|
|
1440
1447
|
deps.clearFoldForDispatch();
|
|
1441
1448
|
deps.requestNextDispatchAnnouncement?.();
|
|
1449
|
+
deps.markActiveTurnNextAbortAnnouncement?.();
|
|
1442
1450
|
deps.abortCurrentTurn();
|
|
1443
1451
|
deps.updateStatus();
|
|
1444
1452
|
return;
|
|
@@ -1880,6 +1888,11 @@ export function createTelegramCommandHandlerTargetRuntime<
|
|
|
1880
1888
|
updateStatus: deps.updateStatus,
|
|
1881
1889
|
isContextActive: deps.isContextActive,
|
|
1882
1890
|
dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
|
|
1891
|
+
requestNextDispatchAnnouncement: deps.requestNextDispatchAnnouncement,
|
|
1892
|
+
markActiveTurnNextAbortAnnouncement:
|
|
1893
|
+
deps.markActiveTurnNextAbortAnnouncement,
|
|
1894
|
+
cancelNextTransitionAnnouncements:
|
|
1895
|
+
deps.cancelNextTransitionAnnouncements,
|
|
1883
1896
|
startTypingLoop: deps.startTypingLoop,
|
|
1884
1897
|
stopTypingLoop: deps.stopTypingLoop,
|
|
1885
1898
|
enqueueContinueTurn: deps.enqueueContinueTurn,
|
|
@@ -2037,6 +2050,8 @@ async function handleTelegramCommandRuntime<
|
|
|
2037
2050
|
await handleTelegramStopCommand({
|
|
2038
2051
|
hasAbortHandler: deps.hasAbortHandler,
|
|
2039
2052
|
clearPendingModelSwitch: deps.clearPendingModelSwitch,
|
|
2053
|
+
cancelNextTransitionAnnouncements:
|
|
2054
|
+
deps.cancelNextTransitionAnnouncements,
|
|
2040
2055
|
clearQueuedTelegramItems: () =>
|
|
2041
2056
|
deps.clearQueuedTelegramItems(commandCtx),
|
|
2042
2057
|
setFoldQueuedPromptsIntoHistory: deps.setFoldQueuedPromptsIntoHistory,
|
|
@@ -2113,6 +2128,8 @@ async function handleTelegramCommandRuntime<
|
|
|
2113
2128
|
hasAbortHandler: deps.hasAbortHandler,
|
|
2114
2129
|
hasActiveTelegramTurn: deps.hasActiveTelegramTurn,
|
|
2115
2130
|
clearPendingModelSwitch: deps.clearPendingModelSwitch,
|
|
2131
|
+
cancelNextTransitionAnnouncements:
|
|
2132
|
+
deps.cancelNextTransitionAnnouncements,
|
|
2116
2133
|
abortCurrentTurn: deps.abortCurrentTurn,
|
|
2117
2134
|
setFoldQueuedPromptsIntoHistory: deps.setFoldQueuedPromptsIntoHistory,
|
|
2118
2135
|
updateStatus: updateStatusFor(commandCtx),
|
|
@@ -2129,6 +2146,8 @@ async function handleTelegramCommandRuntime<
|
|
|
2129
2146
|
dispatchNextQueuedTurn: () =>
|
|
2130
2147
|
deps.dispatchNextQueuedTelegramTurn(commandCtx),
|
|
2131
2148
|
requestNextDispatchAnnouncement: deps.requestNextDispatchAnnouncement,
|
|
2149
|
+
markActiveTurnNextAbortAnnouncement:
|
|
2150
|
+
deps.markActiveTurnNextAbortAnnouncement,
|
|
2132
2151
|
clearFoldForDispatch: () =>
|
|
2133
2152
|
deps.setFoldQueuedPromptsIntoHistory(false),
|
|
2134
2153
|
updateStatus: updateStatusFor(commandCtx),
|
package/lib/extension.ts
CHANGED
|
@@ -667,8 +667,6 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
667
667
|
typing,
|
|
668
668
|
getDefaultChatId: proactivePushChatIdGetter,
|
|
669
669
|
sendTypingAction,
|
|
670
|
-
sendAggregateTypingAction:
|
|
671
|
-
BusApi.createTelegramAggregateTypingActionSender(telegramApiRuntime),
|
|
672
670
|
updateStatus,
|
|
673
671
|
isContextActive: telegramSessionContextStore.isCurrent,
|
|
674
672
|
getTransportAuthority() {
|
|
@@ -820,6 +818,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
820
818
|
mutation: queueMutationRuntime,
|
|
821
819
|
dispatchNext: dispatchNextQueuedTelegramTurn,
|
|
822
820
|
requestNextDispatchAnnouncement,
|
|
821
|
+
cancelNextDispatchAnnouncement,
|
|
823
822
|
watchdog: queueDispatchWatchdogRuntime,
|
|
824
823
|
} = Bindings.createTelegramQueueBindingRuntime({
|
|
825
824
|
store: telegramQueueStore,
|
|
@@ -835,6 +834,13 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
835
834
|
updateStatus,
|
|
836
835
|
sendTextReply,
|
|
837
836
|
sendUserMessage,
|
|
837
|
+
reconcileNextDispatchAnnouncementReplyOwnership(item) {
|
|
838
|
+
Replies.preserveTransportReplyDedupOnNextReset(
|
|
839
|
+
item.chatId,
|
|
840
|
+
item.replyToMessageId,
|
|
841
|
+
item.target,
|
|
842
|
+
);
|
|
843
|
+
},
|
|
838
844
|
recordRuntimeEvent,
|
|
839
845
|
});
|
|
840
846
|
const { finalizeMarkdownPreview, preparePreviewDelivery } =
|
|
@@ -1093,6 +1099,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1093
1099
|
isContextActive: telegramSessionContextStore.isCurrent,
|
|
1094
1100
|
dispatchNextQueuedTelegramTurn,
|
|
1095
1101
|
requestNextDispatchAnnouncement,
|
|
1102
|
+
cancelNextDispatchAnnouncement,
|
|
1096
1103
|
requestDeferredDispatchNextQueuedTelegramTurn:
|
|
1097
1104
|
deferredQueueDispatchRuntime.request,
|
|
1098
1105
|
hasDeferredDispatchContext: deferredQueueDispatchRuntime.isBound,
|
|
@@ -1619,7 +1626,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1619
1626
|
loadConfig: configStore.load,
|
|
1620
1627
|
setQueuedItems: telegramQueueStore.setQueuedItems,
|
|
1621
1628
|
setCurrentModel: currentModelRuntime.set,
|
|
1622
|
-
setPendingModelSwitch:
|
|
1629
|
+
setPendingModelSwitch: modelSwitchController.clearPendingSwitch,
|
|
1623
1630
|
syncCounters: queue.syncCounters,
|
|
1624
1631
|
syncFlags: lifecycle.syncFlags,
|
|
1625
1632
|
bindDeferredDispatchContext: deferredQueueDispatchRuntime.bind,
|
package/lib/menu-model.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
resolveScopedModelPatterns,
|
|
14
14
|
type ScopedTelegramModel,
|
|
15
15
|
sortScopedModels,
|
|
16
|
+
type TelegramModelSwitchContinuationSource,
|
|
16
17
|
type ThinkingLevel,
|
|
17
18
|
} from "./model.ts";
|
|
18
19
|
|
|
@@ -157,9 +158,13 @@ export type TelegramModelMenuCallbackDeps<
|
|
|
157
158
|
setModel: (model: TModel) => Promise<boolean>;
|
|
158
159
|
setCurrentModel: (model: TModel) => void;
|
|
159
160
|
setThinkingLevel: (level: ThinkingLevel) => void;
|
|
160
|
-
stagePendingModelSwitch: (
|
|
161
|
+
stagePendingModelSwitch: (
|
|
162
|
+
selection: ScopedTelegramModel<TModel>,
|
|
163
|
+
continuationTurn: TelegramModelSwitchContinuationSource,
|
|
164
|
+
) => void;
|
|
161
165
|
restartInterruptedTelegramTurn: (
|
|
162
166
|
selection: ScopedTelegramModel<TModel>,
|
|
167
|
+
continuationTurn: TelegramModelSwitchContinuationSource,
|
|
163
168
|
) => Promise<boolean> | boolean;
|
|
164
169
|
};
|
|
165
170
|
|
|
@@ -961,14 +966,28 @@ export async function handleTelegramModelMenuCallbackAction<
|
|
|
961
966
|
if (plan.selection.thinkingLevel) {
|
|
962
967
|
deps.setThinkingLevel(plan.selection.thinkingLevel);
|
|
963
968
|
}
|
|
964
|
-
|
|
969
|
+
const continuationTurn: TelegramModelSwitchContinuationSource = {
|
|
970
|
+
chatId: params.state.chatId,
|
|
971
|
+
replyToMessageId: params.state.messageId,
|
|
972
|
+
target: {
|
|
973
|
+
chatId: params.state.chatId,
|
|
974
|
+
...(params.state.threadId === undefined
|
|
975
|
+
? {}
|
|
976
|
+
: { threadId: params.state.threadId }),
|
|
977
|
+
},
|
|
978
|
+
};
|
|
965
979
|
if (plan.mode === "restart-after-tool") {
|
|
966
|
-
deps.stagePendingModelSwitch(plan.selection);
|
|
980
|
+
deps.stagePendingModelSwitch(plan.selection, continuationTurn);
|
|
981
|
+
await deps.updateModelMenuMessage();
|
|
967
982
|
await deps.answerCallbackQuery(callbackQueryId, plan.callbackText);
|
|
968
983
|
return true;
|
|
969
984
|
}
|
|
970
985
|
if (plan.mode === "restart-now") {
|
|
971
|
-
const restarted = await deps.restartInterruptedTelegramTurn(
|
|
986
|
+
const restarted = await deps.restartInterruptedTelegramTurn(
|
|
987
|
+
plan.selection,
|
|
988
|
+
continuationTurn,
|
|
989
|
+
);
|
|
990
|
+
await deps.updateModelMenuMessage();
|
|
972
991
|
if (!restarted) {
|
|
973
992
|
await deps.answerCallbackQuery(
|
|
974
993
|
callbackQueryId,
|
|
@@ -976,6 +995,8 @@ export async function handleTelegramModelMenuCallbackAction<
|
|
|
976
995
|
);
|
|
977
996
|
return true;
|
|
978
997
|
}
|
|
998
|
+
} else {
|
|
999
|
+
await deps.updateModelMenuMessage();
|
|
979
1000
|
}
|
|
980
1001
|
await deps.answerCallbackQuery(callbackQueryId, plan.callbackText);
|
|
981
1002
|
return true;
|
package/lib/menu.ts
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
import {
|
|
31
31
|
type MenuModel,
|
|
32
32
|
type ScopedTelegramModel,
|
|
33
|
+
type TelegramModelSwitchContinuationSource,
|
|
33
34
|
type ThinkingLevel,
|
|
34
35
|
} from "./model.ts";
|
|
35
36
|
import type { TelegramInputRichMessage } from "./telegram-api.ts";
|
|
@@ -194,7 +195,6 @@ export interface TelegramMenuCallbackRuntimeDeps<
|
|
|
194
195
|
text?: string,
|
|
195
196
|
) => Promise<void>;
|
|
196
197
|
isIdle: (ctx: TContext) => boolean;
|
|
197
|
-
hasActiveTelegramTurn: () => boolean;
|
|
198
198
|
hasAbortHandler: () => boolean;
|
|
199
199
|
hasActiveToolExecutions: () => boolean;
|
|
200
200
|
persistScopedModelPatterns?: (
|
|
@@ -206,10 +206,12 @@ export interface TelegramMenuCallbackRuntimeDeps<
|
|
|
206
206
|
stagePendingModelSwitch: (
|
|
207
207
|
selection: ScopedTelegramModel<TModel>,
|
|
208
208
|
ctx: TContext,
|
|
209
|
+
continuationTurn: TelegramModelSwitchContinuationSource,
|
|
209
210
|
) => void;
|
|
210
211
|
restartInterruptedTelegramTurn: (
|
|
211
212
|
selection: ScopedTelegramModel<TModel>,
|
|
212
213
|
ctx: TContext,
|
|
214
|
+
continuationTurn: TelegramModelSwitchContinuationSource,
|
|
213
215
|
) => Promise<boolean> | boolean;
|
|
214
216
|
sectionRegistry?: TelegramSectionRegistry;
|
|
215
217
|
editInteractiveMessage?: (
|
|
@@ -452,7 +454,6 @@ export interface TelegramMenuCallbackRuntimeAdapterDeps<
|
|
|
452
454
|
text?: string,
|
|
453
455
|
) => Promise<void>;
|
|
454
456
|
isIdle: (ctx: TContext) => boolean;
|
|
455
|
-
hasActiveTelegramTurn: () => boolean;
|
|
456
457
|
hasAbortHandler: () => boolean;
|
|
457
458
|
getActiveToolExecutions: () => number;
|
|
458
459
|
persistScopedModelPatterns?: (
|
|
@@ -464,10 +465,12 @@ export interface TelegramMenuCallbackRuntimeAdapterDeps<
|
|
|
464
465
|
stagePendingModelSwitch: (
|
|
465
466
|
selection: ScopedTelegramModel<TModel>,
|
|
466
467
|
ctx: TContext,
|
|
468
|
+
continuationTurn: TelegramModelSwitchContinuationSource,
|
|
467
469
|
) => void;
|
|
468
470
|
restartInterruptedTelegramTurn: (
|
|
469
471
|
selection: ScopedTelegramModel<TModel>,
|
|
470
472
|
ctx: TContext,
|
|
473
|
+
continuationTurn: TelegramModelSwitchContinuationSource,
|
|
471
474
|
) => Promise<boolean> | boolean;
|
|
472
475
|
sectionRegistry?: TelegramSectionRegistry;
|
|
473
476
|
editInteractiveMessage?: (
|
|
@@ -527,7 +530,6 @@ export function createTelegramMenuCallbackHandlerForContext<
|
|
|
527
530
|
updateSettingsMenuMessage: deps.updateSettingsMenuMessage,
|
|
528
531
|
answerCallbackQuery: deps.answerCallbackQuery,
|
|
529
532
|
isIdle: deps.isIdle,
|
|
530
|
-
hasActiveTelegramTurn: deps.hasActiveTelegramTurn,
|
|
531
533
|
hasAbortHandler: deps.hasAbortHandler,
|
|
532
534
|
hasActiveToolExecutions: () => deps.getActiveToolExecutions() > 0,
|
|
533
535
|
persistScopedModelPatterns: deps.persistScopedModelPatterns,
|
|
@@ -715,8 +717,7 @@ export async function handleTelegramMenuCallbackRuntime<
|
|
|
715
717
|
activeModel: deps.getActiveModel(ctx),
|
|
716
718
|
currentThinkingLevel: deps.getThinkingLevel(),
|
|
717
719
|
isIdle: deps.isIdle(ctx),
|
|
718
|
-
canRestartBusyRun:
|
|
719
|
-
deps.hasActiveTelegramTurn() && deps.hasAbortHandler(),
|
|
720
|
+
canRestartBusyRun: deps.hasAbortHandler(),
|
|
720
721
|
hasActiveToolExecutions: deps.hasActiveToolExecutions(),
|
|
721
722
|
},
|
|
722
723
|
{
|
|
@@ -733,11 +734,15 @@ export async function handleTelegramMenuCallbackRuntime<
|
|
|
733
734
|
deps.setThinkingLevel(level);
|
|
734
735
|
deps.updateStatus(ctx);
|
|
735
736
|
},
|
|
736
|
-
stagePendingModelSwitch: (selection) => {
|
|
737
|
-
deps.stagePendingModelSwitch(selection, ctx);
|
|
737
|
+
stagePendingModelSwitch: (selection, continuationTurn) => {
|
|
738
|
+
deps.stagePendingModelSwitch(selection, ctx, continuationTurn);
|
|
738
739
|
},
|
|
739
|
-
restartInterruptedTelegramTurn: (selection) =>
|
|
740
|
-
deps.restartInterruptedTelegramTurn(
|
|
740
|
+
restartInterruptedTelegramTurn: (selection, continuationTurn) =>
|
|
741
|
+
deps.restartInterruptedTelegramTurn(
|
|
742
|
+
selection,
|
|
743
|
+
ctx,
|
|
744
|
+
continuationTurn,
|
|
745
|
+
),
|
|
741
746
|
},
|
|
742
747
|
);
|
|
743
748
|
} catch (error) {
|