@llblab/pi-kit 0.21.0 → 0.21.1
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/AGENTS.md +2 -2
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +5 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +3 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +6 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +5 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +9 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +5 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +8 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +63 -11
- package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +4 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +14 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +5 -0
- package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +1 -1
- 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 +7 -0
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +13 -2
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +19 -0
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +9 -0
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +75 -16
- package/node_modules/@llblab/pi-telegram/lib/replies.ts +18 -0
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +7 -0
- 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.21.1 - 2026-09-23
|
|
6
|
+
|
|
7
|
+
- `Queue Transition Reliability`: Advances the exact Telegram pin to `0.51.1`. Busy `/next` now reports the interrupted turn and exact selected queued prompt in order, preserves one reply-header owner through agent start, keeps rapid ordinary messages separate, and lets `/abort` or `/stop` cancel stale notices without allowing notice failures to block dispatch.
|
|
8
|
+
- `Follower Heartbeat Stability`: Uses a dedicated eight-second follower heartbeat response deadline aligned with leader stale-liveness policy, preventing ordinary multi-second Pi/TUI event-loop stalls from destroying a healthy socket, producing leader `EPIPE` noise, or cycling registration status.
|
|
9
|
+
- `Package Cohort`: Keeps every other bundled package at its current exact version. Package membership, resource paths, load order, Pi minimum, and bundled Skill ownership remain unchanged.
|
|
10
|
+
|
|
5
11
|
## 0.21.0 - 2026-09-23
|
|
6
12
|
|
|
7
13
|
- `Independent Scope Revisions`: Advances the exact State Flow pin to `0.18.0`. Global, CWD, and Session now persist independent semantic revision counters, Effective displays the truthful `G#/C#/S#` vector, and Session exclusively owns response state while Global and CWD retain only their structural placeholders.
|
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.18.0` | Incremental scoped context/memory compiler with independent scope revisions, canonical file persistence, optional replicated Git backups, native working context within each run, and targeted historical reads |
|
|
18
|
-
| [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.51.
|
|
18
|
+
| [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.51.1` | Telegram companion with stable follower heartbeats, exact queue-transition notices, pressure-safe Workspace rotation, adaptive Thread continuity, files, voice, controls, and bundled Telegram interaction Skills |
|
|
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.
|
|
@@ -74,7 +74,7 @@ Use the relevant local skill before non-trivial work in its domain. Keep skill o
|
|
|
74
74
|
- Threaded Mode has exactly one live leader per bot profile. Followers are real operator-started Pi processes and must authenticate/register over local IPC; Telegram never spawns hidden Pi processes. A live but unreachable owner does not authorize split-brain polling.
|
|
75
75
|
- Local IPC is a trust boundary, not merely a private socket. Unknown, stale, mismatched-generation, or unauthorized requests must not inject prompts, callbacks, API sends, artifacts, liveness, or bindings. Follower registration identity may prepare a binding, but inbound generation authority is published only after successful preparation for the same generation and current context; pending or failed startup cannot append into a retained old journal. Readiness also binds the active Pi context and supplied session generation. Session refresh awaits binding preparation without re-registering; reusing a context object cannot carry readiness across a session-generation change. Registration requests capture session authority before asynchronous startup and fence every publication/finalization against the current attempt. Stop or supersession invalidates that attempt; obsolete cleanup cannot erase a newer registration or a refreshed context.
|
|
76
76
|
- Protocol compatibility is independent from package version. Registration negotiates protocol version, runtime build, and canonical capabilities before target provisioning or live publication. `durable-follower-admission-v1` gates source forwarding; `queue-handoff-v1` independently gates semantic queue transfer for every participant and is advertised only with exact source/recipient journal-binding composition. `follower.register` and capability-gated restore-only `follower.restoreWorkspace` are bootstrap requests; other requests require exact live-registry generation authority, and `bus.ack` is response-only. `thread-display-mode-v1` gates follower display-setting requests; the leader owns their serialized profile preference and title application.
|
|
77
|
-
- Long-lived timers, pollers, watchers, receivers, heartbeats, background delivery, and deferred dispatch are session-bound. Replacement stops stale activity and makes late work inert; teardown must recheck the captured session generation even when context identity is reused. Same-process handoff may preserve exact profile/target identity but never stale Pi context or cross-profile authority. Participating source observations must hold a reference for the actual read, including pending-mutation/count queries against a stopped worker's retained source; a scoped observation never restores receipt readiness or execution authority. A donor cancellation resumed after remote handoff awaits must also hold a source reference; only the existing exact journal CAS may cancel, never undo accepted recipient custody. Stable source keys do not certify captured callable lifetimes: snapshot prepared worker capabilities at construction and replace them on source-handle renewal without replaying unsettled input. Aborting a durable update generation does not release that `update_id`: replacement replay waits for its actual handler settlement, and effectful handlers use the shared execution fence immediately before commit and after awaited delegation. Admission also rechecks that fence after the default handler returns, before its outcome can settle custody; a stopped handler's ordinary return is not completion authority. Internal clones explicitly carry the hidden fence; reroute forwarding, thread-store mutation, cleanup, and Bot API boundaries retain the originating generation.
|
|
77
|
+
- Long-lived timers, pollers, watchers, receivers, heartbeats, background delivery, and deferred dispatch are session-bound. Replacement stops stale activity and makes late work inert; teardown must recheck the captured session generation even when context identity is reused. Same-process handoff may preserve exact profile/target identity but never stale Pi context or cross-profile authority. Follower heartbeat response deadlines align with the eight-second leader stale-liveness window rather than the generic one-second local-RPC default; an ordinary multi-second Pi/TUI event-loop stall is not registration loss. Participating source observations must hold a reference for the actual read, including pending-mutation/count queries against a stopped worker's retained source; a scoped observation never restores receipt readiness or execution authority. A donor cancellation resumed after remote handoff awaits must also hold a source reference; only the existing exact journal CAS may cancel, never undo accepted recipient custody. Stable source keys do not certify captured callable lifetimes: snapshot prepared worker capabilities at construction and replace them on source-handle renewal without replaying unsettled input. Aborting a durable update generation does not release that `update_id`: replacement replay waits for its actual handler settlement, and effectful handlers use the shared execution fence immediately before commit and after awaited delegation. Admission also rechecks that fence after the default handler returns, before its outcome can settle custody; a stopped handler's ordinary return is not completion authority. Internal clones explicitly carry the hidden fence; reroute forwarding, thread-store mutation, cleanup, and Bot API boundaries retain the originating generation.
|
|
78
78
|
- Runtime state is event-driven reconciliation of local assumptions against Telegram signals, not a complete bot read-model and not permission to query Telegram on every action. Destructive thread cleanup goes through `thread-reconciler` with current proof and leader fencing. Fresh Workspace Thread creation derives its initial Bot API title from the active display mode before issuance; the stable generated `threadName` remains separate from the acknowledged `displayTitle`.
|
|
79
79
|
|
|
80
80
|
### 4.3 Durable Admission And Settlement
|
|
@@ -95,7 +95,7 @@ Use the relevant local skill before non-trivial work in its domain. Keep skill o
|
|
|
95
95
|
### 4.4 Queue, Delivery, And User Surfaces
|
|
96
96
|
|
|
97
97
|
- Queue lane/kind admission is explicit. Dispatch waits for active-turn, pending-dispatch, control, compaction, `ctx.isIdle()`, and Pi pending-message guards; a dispatched prompt stays queued until `agent_start` consumes it. The terminal `+N` suffix is a yellow count of executable prompts still waiting, excludes the dispatched head immediately, and never counts current agent work from any source. Each prompt is one object with one active lane and no reserved return slot. Normal and Priority are separate FIFO lanes: crossing lanes removes it from the source and appends it at the destination tail, while Keep/Skip and same-category emoji changes preserve lane position. Complete reaction sets independently derive Priority from recognized positive emoji and Skip from recognized negative emoji; both may coexist, suppressed turns retain durable receipts while waiting, and Skip settles them only when the prompt reaches dispatch before dropping it without inference. Suppressed turns remain visible at a struck-through physical ordinal without contributing to executable queue counters, while graceful session shutdown discards all remaining queue authority before clearing memory.
|
|
98
|
-
- `/stop`, `/abort`, `/next`, and `/continue` respectively reset+abort, abort while preserving queue, force the next turn, and enqueue a control-lane continuation. Abort-history folding applies only to Telegram-owned active turns.
|
|
98
|
+
- `/stop`, `/abort`, `/next`, and `/continue` respectively reset+abort, abort while preserving queue, force the next turn, and enqueue a control-lane continuation. Abort-history folding applies only to Telegram-owned active turns. A busy `/next` marks only its exact active Telegram turn: terminal abort settlement attempts one explicit abort notice before the exact selected queued prompt receives its dispatch notice. Notice failure is diagnostic and cannot block dispatch; a later `/abort` or `/stop` cancels both pending transition notices before taking ownership. Target command adapters must forward these transition ports. A successful dispatch notice keeps the queued prompt's one reply-header claim across agent start, so later messages in that turn never repeat it.
|
|
99
99
|
- Telegram extension side effects must not hold Pi's core lifecycle hostage after semantic completion. Preserve ordering in extension-owned background work, record failures, and fence target/profile/transport/session authority.
|
|
100
100
|
- Complete assistant/guest model answers use Telegram-native Rich Markdown. Harness-owned menus, status, diagnostics, thinking, and tool evidence remain explicit HTML/plain or their documented native surface. Before Telegram preview or final delivery, strip every assistant-authored HTML comment regardless of Markdown position while keeping action activation top-level-only; a comment-only result sends no text message. Preserve literal code outside comments and structurally safe chunking; never split invalid markup.
|
|
101
101
|
- `preview` owns streaming lifecycle only, not assistant rendering. Finalization waits for active preview flushes and must not issue pre/post-final draft-clear calls that create transient Telegram draft UI. Turns that already answer as one atomic reply (voice replies, Guest Mode queries) never stream previews.
|
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.51.1: Queue transitions and follower heartbeat stability
|
|
8
|
+
|
|
9
|
+
- `/next lifecycle notices`: Target command composition now forwards request, exact-turn marker, and cancellation ports. Busy `/next` orders abort then selected-prompt dispatch notices; failures cannot block dispatch, and `/abort`/`/stop` cancel stale notices. A successful dispatch notice keeps first-reply ownership through agent start, so later answer messages do not repeat the queued-prompt reply. Rapid ordinary messages remain distinct turns.
|
|
10
|
+
- `Follower heartbeat stability`: Follower heartbeats now use a dedicated eight-second response deadline aligned with leader stale-liveness policy instead of the generic one-second local-RPC default. Ordinary multi-second TUI/event-loop stalls therefore retain registration and stable `follower` status rather than destroying the socket, producing leader EPIPE noise, and entering repeated reconnect cycles.
|
|
11
|
+
|
|
7
12
|
## 0.51.0: Safe Workspace rotation and follower readiness
|
|
8
13
|
|
|
9
14
|
- `Workspace slot rotation`: Fresh allocation reclaims the oldest proven inactive slot only at full A–Z pressure. Exact grouped receipts may be journal-CAS discarded only after complete source inspection proves every process-birth owner dead. Live/unknown owners, partial groups, handoffs and unrelated work stay protected. Fresh protection precedes Thread/history deletion and slot reuse. Pi sessions/files/memory/journals remain; restore-only never evicts; ambiguous deletion never replays.
|
|
@@ -34,6 +34,7 @@ export interface TelegramQueueBindingRuntime<TContext> {
|
|
|
34
34
|
mutation: Queue.TelegramQueueMutationController<TContext>;
|
|
35
35
|
dispatchNext: (ctx: TContext) => void;
|
|
36
36
|
requestNextDispatchAnnouncement: () => void;
|
|
37
|
+
cancelNextDispatchAnnouncement: () => void;
|
|
37
38
|
watchdog: Queue.TelegramQueueDispatchWatchdogRuntime<TContext>;
|
|
38
39
|
}
|
|
39
40
|
export declare function createTelegramQueueBindingRuntime<TContext>(deps: {
|
|
@@ -58,6 +59,7 @@ export declare function createTelegramQueueBindingRuntime<TContext>(deps: {
|
|
|
58
59
|
updateStatus: (ctx: TContext, error?: string) => void;
|
|
59
60
|
sendTextReply: Queue.TelegramQueueDispatchRuntimeDeps<TContext>["sendTextReply"];
|
|
60
61
|
sendUserMessage: Queue.TelegramQueueDispatchRuntimeDeps<TContext>["sendUserMessage"];
|
|
62
|
+
reconcileNextDispatchAnnouncementReplyOwnership?: (item: Queue.PendingTelegramTurn) => void;
|
|
61
63
|
recordRuntimeEvent?: TelegramRuntimeEventRecorder;
|
|
62
64
|
}): TelegramQueueBindingRuntime<TContext>;
|
|
63
65
|
export interface TelegramGenerativeAppLiveSurfaceBinding {
|
|
@@ -79,6 +79,7 @@ export function createTelegramQueueBindingRuntime(deps) {
|
|
|
79
79
|
},
|
|
80
80
|
updateStatus: deps.updateStatus,
|
|
81
81
|
sendTextReply: deps.sendTextReply,
|
|
82
|
+
reconcileNextDispatchAnnouncementReplyOwnership: deps.reconcileNextDispatchAnnouncementReplyOwnership,
|
|
82
83
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
83
84
|
...deps.promptDispatch,
|
|
84
85
|
sendUserMessage: deps.sendUserMessage,
|
|
@@ -87,6 +88,7 @@ export function createTelegramQueueBindingRuntime(deps) {
|
|
|
87
88
|
mutation,
|
|
88
89
|
dispatchNext: dispatch.dispatchNext,
|
|
89
90
|
requestNextDispatchAnnouncement: dispatch.requestNextDispatchAnnouncement,
|
|
91
|
+
cancelNextDispatchAnnouncement: dispatch.cancelNextDispatchAnnouncement,
|
|
90
92
|
watchdog: Queue.createTelegramQueueDispatchWatchdogRuntime({
|
|
91
93
|
hasQueuedItems: deps.store.hasQueuedItems,
|
|
92
94
|
dispatchNextQueuedTelegramTurn: dispatch.dispatchNext,
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import * as Sync from "./sync.ts";
|
|
9
9
|
import * as Threads from "./threads.ts";
|
|
10
10
|
import { type TelegramUpdateJournalStoreOptions } from "./journal.ts";
|
|
11
|
-
import type
|
|
11
|
+
import { type TelegramLockEntry, type TelegramLockState } from "./locks.ts";
|
|
12
12
|
import type { TelegramQueueHandoffPayload, TelegramQueueHandoffStageResult } from "./queue.ts";
|
|
13
13
|
import type { TelegramTarget } from "./target.ts";
|
|
14
14
|
import { type TelegramBusAgentMessage, type TelegramBusAgentTargetSelector, type TelegramBusEnvelope, type TelegramBusForwardOwnership, type TelegramBusProtocolIdentity, type TelegramBusSocketPathSource } from "./bus.ts";
|
|
@@ -18,6 +18,7 @@ export declare const TELEGRAM_BUS_FOLLOWER_PROMOTION_GRACE_MS = 2500;
|
|
|
18
18
|
export declare const TELEGRAM_FOLLOWER_SESSION_HANDOFF_TTL_MS = 30000;
|
|
19
19
|
export declare const TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS = 30000;
|
|
20
20
|
export declare const TELEGRAM_BUS_FOLLOWER_REGISTRATION_WAIT_MS = 30000;
|
|
21
|
+
export declare const TELEGRAM_BUS_FOLLOWER_HEARTBEAT_TIMEOUT_MS = 8000;
|
|
21
22
|
export declare const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_ATTEMPTS: number;
|
|
22
23
|
export declare const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_DELAY_MS: number;
|
|
23
24
|
export interface TelegramFollowerSessionHandoff {
|
|
@@ -170,6 +171,7 @@ export interface TelegramBusFollowerRegistrationRuntimeDeps<TContext extends {
|
|
|
170
171
|
registrationRetryAttempts?: number;
|
|
171
172
|
registrationRetryDelayMs?: number;
|
|
172
173
|
heartbeatMs?: number;
|
|
174
|
+
heartbeatTimeoutMs?: number;
|
|
173
175
|
recordRuntimeEvent?: (category: string, error: unknown, details?: Record<string, unknown>) => void;
|
|
174
176
|
onHeartbeatFailure?: (error: unknown, ctx: TContext) => Promise<void> | void;
|
|
175
177
|
onRegistered?: (ctx: TContext) => Promise<void> | void;
|
|
@@ -9,6 +9,7 @@ import { basename } from "node:path";
|
|
|
9
9
|
import * as Sync from "./sync.js";
|
|
10
10
|
import * as Threads from "./threads.js";
|
|
11
11
|
import { parseTelegramUpdateJournalQueueOwner } from "./journal.js";
|
|
12
|
+
import { TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS, } from "./locks.js";
|
|
12
13
|
import { isTelegramApiMethodRetrySafe, TelegramApiCommitUnknownError, TelegramApiStaleTargetError, } from "./telegram-api.js";
|
|
13
14
|
import { createTelegramBusFollowerDeliveryIdentity, createTelegramBusFollowerTargetController, createTelegramBusForeignOwnedUpdateForwarder, createTelegramBusLocalServer, createTelegramBusRequestIdFactory, createUnauthorizedBusAck, getTelegramBusProtocolCompatibility, getTelegramBusSocketPath, hasTelegramBusCapability, isTelegramBusEnvelopeAuthorized, resolveTelegramBusSocketPath, sendTelegramBusLocalEnvelope, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE, TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT, } from "./bus.js";
|
|
14
15
|
import { getTelegramBusTransportRetryPolicy, TELEGRAM_BUS_REGISTRATION_RETRY, } from "./bus-transport.js";
|
|
@@ -17,6 +18,7 @@ export const TELEGRAM_BUS_FOLLOWER_PROMOTION_GRACE_MS = 2_500;
|
|
|
17
18
|
export const TELEGRAM_FOLLOWER_SESSION_HANDOFF_TTL_MS = 30_000;
|
|
18
19
|
export const TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS = 30_000;
|
|
19
20
|
export const TELEGRAM_BUS_FOLLOWER_REGISTRATION_WAIT_MS = 30_000;
|
|
21
|
+
export const TELEGRAM_BUS_FOLLOWER_HEARTBEAT_TIMEOUT_MS = TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS;
|
|
20
22
|
export const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_ATTEMPTS = TELEGRAM_BUS_REGISTRATION_RETRY.attempts;
|
|
21
23
|
export const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_DELAY_MS = TELEGRAM_BUS_REGISTRATION_RETRY.delayMs;
|
|
22
24
|
const TELEGRAM_FOLLOWER_SESSION_HANDOFF_KEY = "__piTelegramFollowerSessionHandoff";
|
|
@@ -920,6 +922,9 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
|
|
|
920
922
|
const getNowMs = deps.getNowMs ?? Date.now;
|
|
921
923
|
const getPid = deps.getPid ?? (() => process.pid);
|
|
922
924
|
const heartbeatMs = deps.heartbeatMs ?? 1000;
|
|
925
|
+
const heartbeatTimeoutMs = deps.heartbeatTimeoutMs ??
|
|
926
|
+
deps.timeoutMs ??
|
|
927
|
+
TELEGRAM_BUS_FOLLOWER_HEARTBEAT_TIMEOUT_MS;
|
|
923
928
|
const registrationTimeoutMs = deps.registrationTimeoutMs ?? deps.timeoutMs ?? 30000;
|
|
924
929
|
const registrationRetryAttempts = deps.registrationRetryAttempts ??
|
|
925
930
|
TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_ATTEMPTS;
|
|
@@ -979,7 +984,7 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
|
|
|
979
984
|
try {
|
|
980
985
|
const response = await sendTelegramBusLocalEnvelope({
|
|
981
986
|
socketPath: leaderSocketPath,
|
|
982
|
-
timeoutMs:
|
|
987
|
+
timeoutMs: heartbeatTimeoutMs,
|
|
983
988
|
retry: getTelegramBusTransportRetryPolicy({
|
|
984
989
|
endpoint: leaderSocketPath,
|
|
985
990
|
operation: "operation",
|
|
@@ -210,6 +210,7 @@ export interface TelegramCommandActionDeps<TMessage, TContext> {
|
|
|
210
210
|
export interface TelegramStopCommandDeps {
|
|
211
211
|
hasAbortHandler: () => boolean;
|
|
212
212
|
clearPendingModelSwitch: () => void;
|
|
213
|
+
cancelNextTransitionAnnouncements?: () => void;
|
|
213
214
|
clearQueuedTelegramItems: () => number;
|
|
214
215
|
setFoldQueuedPromptsIntoHistory: (fold: boolean) => void;
|
|
215
216
|
abortCurrentTurn: () => void;
|
|
@@ -392,6 +393,8 @@ export interface TelegramCommandRuntimeDeps<TMessage extends TelegramCommandRunt
|
|
|
392
393
|
isContextActive?: (ctx: TContext) => boolean;
|
|
393
394
|
dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
|
|
394
395
|
requestNextDispatchAnnouncement?: () => void;
|
|
396
|
+
markActiveTurnNextAbortAnnouncement?: () => boolean;
|
|
397
|
+
cancelNextTransitionAnnouncements?: () => void;
|
|
395
398
|
requestDeferredDispatchNextQueuedTelegramTurn?: (dispatch: (ctx: TContext) => void) => void;
|
|
396
399
|
startTypingLoop?: (ctx: TContext, chatId?: number, options?: {
|
|
397
400
|
target?: {
|
|
@@ -504,6 +507,7 @@ export declare function handleTelegramAbortCommand(deps: {
|
|
|
504
507
|
hasAbortHandler: () => boolean;
|
|
505
508
|
hasActiveTelegramTurn: () => boolean;
|
|
506
509
|
clearPendingModelSwitch: () => void;
|
|
510
|
+
cancelNextTransitionAnnouncements?: () => void;
|
|
507
511
|
abortCurrentTurn: () => void;
|
|
508
512
|
setFoldQueuedPromptsIntoHistory: (fold: boolean) => void;
|
|
509
513
|
updateStatus: () => void;
|
|
@@ -519,6 +523,7 @@ export declare function handleTelegramNextCommand(deps: {
|
|
|
519
523
|
abortCurrentTurn: () => void;
|
|
520
524
|
dispatchNextQueuedTurn: () => void;
|
|
521
525
|
requestNextDispatchAnnouncement?: () => void;
|
|
526
|
+
markActiveTurnNextAbortAnnouncement?: () => boolean;
|
|
522
527
|
clearFoldForDispatch: () => void;
|
|
523
528
|
updateStatus: () => void;
|
|
524
529
|
sendTextReply: (text: string, options?: {
|
|
@@ -631,6 +631,7 @@ function formatTelegramQueuedTurnCount(count) {
|
|
|
631
631
|
}
|
|
632
632
|
export async function handleTelegramStopCommand(deps) {
|
|
633
633
|
deps.clearPendingModelSwitch();
|
|
634
|
+
deps.cancelNextTransitionAnnouncements?.();
|
|
634
635
|
const clearedCount = deps.clearQueuedTelegramItems();
|
|
635
636
|
deps.setFoldQueuedPromptsIntoHistory(false);
|
|
636
637
|
if (!deps.hasAbortHandler()) {
|
|
@@ -651,6 +652,7 @@ export async function handleTelegramStopCommand(deps) {
|
|
|
651
652
|
}
|
|
652
653
|
export async function handleTelegramAbortCommand(deps) {
|
|
653
654
|
deps.clearPendingModelSwitch();
|
|
655
|
+
deps.cancelNextTransitionAnnouncements?.();
|
|
654
656
|
if (!deps.hasAbortHandler()) {
|
|
655
657
|
await deps.sendTextReply(formatTelegramInformationHeading("💤", "No active turn."), { parseMode: "HTML" });
|
|
656
658
|
return;
|
|
@@ -669,6 +671,7 @@ export async function handleTelegramNextCommand(deps) {
|
|
|
669
671
|
if (!deps.isIdle() && deps.hasAbortHandler()) {
|
|
670
672
|
deps.clearFoldForDispatch();
|
|
671
673
|
deps.requestNextDispatchAnnouncement?.();
|
|
674
|
+
deps.markActiveTurnNextAbortAnnouncement?.();
|
|
672
675
|
deps.abortCurrentTurn();
|
|
673
676
|
deps.updateStatus();
|
|
674
677
|
return;
|
|
@@ -934,6 +937,9 @@ export function createTelegramCommandHandlerTargetRuntime(deps) {
|
|
|
934
937
|
updateStatus: deps.updateStatus,
|
|
935
938
|
isContextActive: deps.isContextActive,
|
|
936
939
|
dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
|
|
940
|
+
requestNextDispatchAnnouncement: deps.requestNextDispatchAnnouncement,
|
|
941
|
+
markActiveTurnNextAbortAnnouncement: deps.markActiveTurnNextAbortAnnouncement,
|
|
942
|
+
cancelNextTransitionAnnouncements: deps.cancelNextTransitionAnnouncements,
|
|
937
943
|
startTypingLoop: deps.startTypingLoop,
|
|
938
944
|
stopTypingLoop: deps.stopTypingLoop,
|
|
939
945
|
enqueueContinueTurn: deps.enqueueContinueTurn,
|
|
@@ -1039,6 +1045,7 @@ async function handleTelegramCommandRuntime(commandName, message, ctx, deps, com
|
|
|
1039
1045
|
await handleTelegramStopCommand({
|
|
1040
1046
|
hasAbortHandler: deps.hasAbortHandler,
|
|
1041
1047
|
clearPendingModelSwitch: deps.clearPendingModelSwitch,
|
|
1048
|
+
cancelNextTransitionAnnouncements: deps.cancelNextTransitionAnnouncements,
|
|
1042
1049
|
clearQueuedTelegramItems: () => deps.clearQueuedTelegramItems(commandCtx),
|
|
1043
1050
|
setFoldQueuedPromptsIntoHistory: deps.setFoldQueuedPromptsIntoHistory,
|
|
1044
1051
|
abortCurrentTurn: deps.abortCurrentTurn,
|
|
@@ -1085,6 +1092,7 @@ async function handleTelegramCommandRuntime(commandName, message, ctx, deps, com
|
|
|
1085
1092
|
hasAbortHandler: deps.hasAbortHandler,
|
|
1086
1093
|
hasActiveTelegramTurn: deps.hasActiveTelegramTurn,
|
|
1087
1094
|
clearPendingModelSwitch: deps.clearPendingModelSwitch,
|
|
1095
|
+
cancelNextTransitionAnnouncements: deps.cancelNextTransitionAnnouncements,
|
|
1088
1096
|
abortCurrentTurn: deps.abortCurrentTurn,
|
|
1089
1097
|
setFoldQueuedPromptsIntoHistory: deps.setFoldQueuedPromptsIntoHistory,
|
|
1090
1098
|
updateStatus: updateStatusFor(commandCtx),
|
|
@@ -1100,6 +1108,7 @@ async function handleTelegramCommandRuntime(commandName, message, ctx, deps, com
|
|
|
1100
1108
|
abortCurrentTurn: deps.abortCurrentTurn,
|
|
1101
1109
|
dispatchNextQueuedTurn: () => deps.dispatchNextQueuedTelegramTurn(commandCtx),
|
|
1102
1110
|
requestNextDispatchAnnouncement: deps.requestNextDispatchAnnouncement,
|
|
1111
|
+
markActiveTurnNextAbortAnnouncement: deps.markActiveTurnNextAbortAnnouncement,
|
|
1103
1112
|
clearFoldForDispatch: () => deps.setFoldQueuedPromptsIntoHistory(false),
|
|
1104
1113
|
updateStatus: updateStatusFor(commandCtx),
|
|
1105
1114
|
sendTextReply: sendReplyFor(nextMessage),
|
|
@@ -642,7 +642,7 @@ export default function (pi) {
|
|
|
642
642
|
editMessageText: editTelegramMessageText,
|
|
643
643
|
},
|
|
644
644
|
});
|
|
645
|
-
const { mutation: queueMutationRuntime, dispatchNext: dispatchNextQueuedTelegramTurn, requestNextDispatchAnnouncement, watchdog: queueDispatchWatchdogRuntime, } = Bindings.createTelegramQueueBindingRuntime({
|
|
645
|
+
const { mutation: queueMutationRuntime, dispatchNext: dispatchNextQueuedTelegramTurn, requestNextDispatchAnnouncement, cancelNextDispatchAnnouncement, watchdog: queueDispatchWatchdogRuntime, } = Bindings.createTelegramQueueBindingRuntime({
|
|
646
646
|
store: telegramQueueStore,
|
|
647
647
|
queue,
|
|
648
648
|
lifecycle,
|
|
@@ -656,6 +656,9 @@ export default function (pi) {
|
|
|
656
656
|
updateStatus,
|
|
657
657
|
sendTextReply,
|
|
658
658
|
sendUserMessage,
|
|
659
|
+
reconcileNextDispatchAnnouncementReplyOwnership(item) {
|
|
660
|
+
Replies.preserveTransportReplyDedupOnNextReset(item.chatId, item.replyToMessageId, item.target);
|
|
661
|
+
},
|
|
659
662
|
recordRuntimeEvent,
|
|
660
663
|
});
|
|
661
664
|
const { finalizeMarkdownPreview, preparePreviewDelivery } = Outbound.createTelegramOutboundTextPreviewRuntime({
|
|
@@ -887,6 +890,7 @@ export default function (pi) {
|
|
|
887
890
|
isContextActive: telegramSessionContextStore.isCurrent,
|
|
888
891
|
dispatchNextQueuedTelegramTurn,
|
|
889
892
|
requestNextDispatchAnnouncement,
|
|
893
|
+
cancelNextDispatchAnnouncement,
|
|
890
894
|
requestDeferredDispatchNextQueuedTelegramTurn: deferredQueueDispatchRuntime.request,
|
|
891
895
|
hasDeferredDispatchContext: deferredQueueDispatchRuntime.isBound,
|
|
892
896
|
startTypingLoop: promptDispatchRuntime.startTypingLoop,
|
|
@@ -84,6 +84,8 @@ export interface PendingTelegramTurn extends TelegramQueueItemBase {
|
|
|
84
84
|
historyText: string;
|
|
85
85
|
priorityEmoji?: string;
|
|
86
86
|
reactionSuppressionEmoji?: string;
|
|
87
|
+
/** Emit the explicit aborted-turn notice when /next settles this active turn. */
|
|
88
|
+
announceNextAbortOnEnd?: boolean;
|
|
87
89
|
/** Turn should preferably be delivered as voice (mirror mode + user sent voice) */
|
|
88
90
|
voiceReplyPreferred?: boolean;
|
|
89
91
|
/** Turn must be delivered as voice (voice mode) */
|
|
@@ -165,6 +167,8 @@ export interface TelegramActiveTurnStore<TTurn extends PendingTelegramTurn = Pen
|
|
|
165
167
|
has: () => boolean;
|
|
166
168
|
set: (turn: TTurn) => void;
|
|
167
169
|
clear: () => void;
|
|
170
|
+
markNextAbortAnnouncement: () => boolean;
|
|
171
|
+
clearNextAbortAnnouncement: () => boolean;
|
|
168
172
|
getChatId: () => number | undefined;
|
|
169
173
|
getTarget: () => TelegramQueueTarget | undefined;
|
|
170
174
|
getReplyToMessageId: () => number | undefined;
|
|
@@ -391,6 +395,7 @@ export interface TelegramAgentEndPlan {
|
|
|
391
395
|
kind: "no-turn" | "aborted" | "error" | "text" | "attachments-only" | "empty";
|
|
392
396
|
shouldClearPreview: boolean;
|
|
393
397
|
shouldDispatchNext: boolean;
|
|
398
|
+
shouldSendAbortMessage: boolean;
|
|
394
399
|
shouldSendErrorMessage: boolean;
|
|
395
400
|
shouldSendAttachmentNotice: boolean;
|
|
396
401
|
}
|
|
@@ -517,6 +522,7 @@ export declare function buildTelegramAgentEndPlan(options: {
|
|
|
517
522
|
hasFinalText: boolean;
|
|
518
523
|
hasQueuedAttachments: boolean;
|
|
519
524
|
foldQueuedPromptsIntoHistory: boolean;
|
|
525
|
+
announceNextAbortOnEnd?: boolean;
|
|
520
526
|
}): TelegramAgentEndPlan;
|
|
521
527
|
export declare function createTelegramAgentEndHook<TTurn extends PendingTelegramTurn, TContext, TMessage, TReplyMarkup = unknown>(deps: TelegramAgentEndHookRuntimeDeps<TTurn, TContext, TMessage, TReplyMarkup>): (event: TelegramAgentEndHookEvent<TMessage>, ctx: TContext, assistantOverride?: TelegramAgentEndAssistantResult) => Promise<void>;
|
|
522
528
|
export declare function handleTelegramAgentEndRuntime<TTurn extends PendingTelegramTurn, TReplyMarkup = unknown>(deps: TelegramAgentEndRuntimeDeps<TTurn, TReplyMarkup>): Promise<void>;
|
|
@@ -759,6 +765,7 @@ export interface TelegramQueueDispatchControllerDeps<TContext = unknown> extends
|
|
|
759
765
|
}>["item"], ctx: TContext) => boolean;
|
|
760
766
|
sendUserMessage: TelegramDispatchRuntimeDeps<TContext>["sendUserMessage"];
|
|
761
767
|
onPromptDispatchFailure: (ctx: TContext, message: string) => void;
|
|
768
|
+
reconcileNextDispatchAnnouncementReplyOwnership?: (item: PendingTelegramTurn) => void;
|
|
762
769
|
isQueueItemTransportActive?: (item: TelegramQueueItem<TContext>) => boolean;
|
|
763
770
|
hasPendingInboundQueueMutationForItem?: (item: TelegramQueueItem<TContext>) => boolean;
|
|
764
771
|
isQueueItemAdmissionReady?: (item: TelegramQueueItem<TContext>) => boolean;
|
|
@@ -768,6 +775,7 @@ export interface TelegramQueueDispatchControllerDeps<TContext = unknown> extends
|
|
|
768
775
|
export interface TelegramQueueDispatchController<TContext = unknown> {
|
|
769
776
|
dispatchNext: (ctx: TContext) => void;
|
|
770
777
|
requestNextDispatchAnnouncement: () => void;
|
|
778
|
+
cancelNextDispatchAnnouncement: () => void;
|
|
771
779
|
}
|
|
772
780
|
export declare function executeTelegramQueueDispatchPlan<TContext = unknown>(plan: TelegramQueueDispatchAction<TContext>, deps: TelegramDispatchRuntimeDeps<TContext>): void;
|
|
773
781
|
export type TelegramQueueDispatchRuntimeDeps<TContext = unknown> = Omit<TelegramQueueDispatchControllerDeps<TContext>, "canDispatch"> & TelegramDispatchReadinessDeps<TContext>;
|
|
@@ -168,6 +168,18 @@ export function createTelegramActiveTurnStore() {
|
|
|
168
168
|
clear: () => {
|
|
169
169
|
activeTurn = undefined;
|
|
170
170
|
},
|
|
171
|
+
markNextAbortAnnouncement: () => {
|
|
172
|
+
if (!activeTurn)
|
|
173
|
+
return false;
|
|
174
|
+
activeTurn.announceNextAbortOnEnd = true;
|
|
175
|
+
return true;
|
|
176
|
+
},
|
|
177
|
+
clearNextAbortAnnouncement: () => {
|
|
178
|
+
if (!activeTurn?.announceNextAbortOnEnd)
|
|
179
|
+
return false;
|
|
180
|
+
delete activeTurn.announceNextAbortOnEnd;
|
|
181
|
+
return true;
|
|
182
|
+
},
|
|
171
183
|
getChatId: () => activeTurn?.chatId,
|
|
172
184
|
getTarget: () => activeTurn?.target ? { ...activeTurn.target } : undefined,
|
|
173
185
|
getReplyToMessageId: () => activeTurn?.replyToMessageId,
|
|
@@ -815,6 +827,7 @@ export function buildTelegramAgentEndPlan(options) {
|
|
|
815
827
|
kind: "no-turn",
|
|
816
828
|
shouldClearPreview: false,
|
|
817
829
|
shouldDispatchNext,
|
|
830
|
+
shouldSendAbortMessage: false,
|
|
818
831
|
shouldSendErrorMessage: false,
|
|
819
832
|
shouldSendAttachmentNotice: false,
|
|
820
833
|
};
|
|
@@ -824,6 +837,7 @@ export function buildTelegramAgentEndPlan(options) {
|
|
|
824
837
|
kind: "aborted",
|
|
825
838
|
shouldClearPreview: true,
|
|
826
839
|
shouldDispatchNext,
|
|
840
|
+
shouldSendAbortMessage: options.announceNextAbortOnEnd === true,
|
|
827
841
|
shouldSendErrorMessage: false,
|
|
828
842
|
shouldSendAttachmentNotice: false,
|
|
829
843
|
};
|
|
@@ -833,6 +847,7 @@ export function buildTelegramAgentEndPlan(options) {
|
|
|
833
847
|
kind: "error",
|
|
834
848
|
shouldClearPreview: true,
|
|
835
849
|
shouldDispatchNext,
|
|
850
|
+
shouldSendAbortMessage: false,
|
|
836
851
|
shouldSendErrorMessage: true,
|
|
837
852
|
shouldSendAttachmentNotice: false,
|
|
838
853
|
};
|
|
@@ -842,6 +857,7 @@ export function buildTelegramAgentEndPlan(options) {
|
|
|
842
857
|
kind: "text",
|
|
843
858
|
shouldClearPreview: false,
|
|
844
859
|
shouldDispatchNext,
|
|
860
|
+
shouldSendAbortMessage: false,
|
|
845
861
|
shouldSendErrorMessage: false,
|
|
846
862
|
shouldSendAttachmentNotice: false,
|
|
847
863
|
};
|
|
@@ -851,6 +867,7 @@ export function buildTelegramAgentEndPlan(options) {
|
|
|
851
867
|
kind: "attachments-only",
|
|
852
868
|
shouldClearPreview: true,
|
|
853
869
|
shouldDispatchNext,
|
|
870
|
+
shouldSendAbortMessage: false,
|
|
854
871
|
shouldSendErrorMessage: false,
|
|
855
872
|
shouldSendAttachmentNotice: true,
|
|
856
873
|
};
|
|
@@ -859,6 +876,7 @@ export function buildTelegramAgentEndPlan(options) {
|
|
|
859
876
|
kind: "empty",
|
|
860
877
|
shouldClearPreview: true,
|
|
861
878
|
shouldDispatchNext,
|
|
879
|
+
shouldSendAbortMessage: false,
|
|
862
880
|
shouldSendErrorMessage: false,
|
|
863
881
|
shouldSendAttachmentNotice: false,
|
|
864
882
|
};
|
|
@@ -991,6 +1009,7 @@ export async function handleTelegramAgentEndRuntime(deps) {
|
|
|
991
1009
|
hasFinalText: !!finalText || hasOutboundArtifacts,
|
|
992
1010
|
hasQueuedAttachments: (turn?.queuedAttachments.length ?? 0) > 0,
|
|
993
1011
|
foldQueuedPromptsIntoHistory: deps.foldQueuedPromptsIntoHistory,
|
|
1012
|
+
announceNextAbortOnEnd: turn?.announceNextAbortOnEnd === true,
|
|
994
1013
|
});
|
|
995
1014
|
if (!turn) {
|
|
996
1015
|
if (endPlan.shouldDispatchNext)
|
|
@@ -1116,17 +1135,26 @@ export async function handleTelegramAgentEndRuntime(deps) {
|
|
|
1116
1135
|
if (!isDeliveryActive())
|
|
1117
1136
|
return;
|
|
1118
1137
|
}
|
|
1119
|
-
if (endPlan.shouldSendErrorMessage) {
|
|
1138
|
+
if (endPlan.shouldSendAbortMessage || endPlan.shouldSendErrorMessage) {
|
|
1120
1139
|
const errorMessage = assistant.errorMessage ||
|
|
1121
1140
|
"Telegram bridge: Pi failed while processing the request.";
|
|
1122
|
-
const isOperationAborted =
|
|
1123
|
-
"This operation was aborted";
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1141
|
+
const isOperationAborted = endPlan.shouldSendAbortMessage ||
|
|
1142
|
+
errorMessage.trim().replace(/\.$/, "") === "This operation was aborted";
|
|
1143
|
+
try {
|
|
1144
|
+
await deps.sendTextReply(turn.chatId, turn.replyToMessageId, isOperationAborted
|
|
1145
|
+
? "<b>⏹️ This operation was aborted.</b>"
|
|
1146
|
+
: errorMessage, {
|
|
1147
|
+
target: turn.target,
|
|
1148
|
+
...(isOperationAborted ? { parseMode: "HTML" } : {}),
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
catch (error) {
|
|
1152
|
+
if (!isOperationAborted)
|
|
1153
|
+
throw error;
|
|
1154
|
+
deps.recordRuntimeEvent?.("dispatch", error, {
|
|
1155
|
+
phase: "next-abort-announcement",
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1130
1158
|
if (!isDeliveryActive())
|
|
1131
1159
|
return;
|
|
1132
1160
|
if (endPlan.shouldDispatchNext)
|
|
@@ -1267,7 +1295,7 @@ export async function handleTelegramAgentEndRuntime(deps) {
|
|
|
1267
1295
|
deps.dispatchNextQueuedTelegramTurn();
|
|
1268
1296
|
};
|
|
1269
1297
|
if (deps.scheduleActiveTurnDelivery &&
|
|
1270
|
-
(endPlan.kind === "text" || endPlan.kind === "attachments-only" || endPlan.shouldSendErrorMessage || endPlan.shouldClearPreview)) {
|
|
1298
|
+
(endPlan.kind === "text" || endPlan.kind === "attachments-only" || endPlan.shouldSendAbortMessage || endPlan.shouldSendErrorMessage || endPlan.shouldClearPreview)) {
|
|
1271
1299
|
deps.scheduleActiveTurnDelivery(deliverActiveTurn);
|
|
1272
1300
|
return;
|
|
1273
1301
|
}
|
|
@@ -1765,6 +1793,7 @@ export function createTelegramQueueDispatchRuntime(deps) {
|
|
|
1765
1793
|
commitPromptDispatch: deps.commitPromptDispatch,
|
|
1766
1794
|
sendUserMessage: deps.sendUserMessage,
|
|
1767
1795
|
onPromptDispatchFailure: deps.onPromptDispatchFailure,
|
|
1796
|
+
reconcileNextDispatchAnnouncementReplyOwnership: deps.reconcileNextDispatchAnnouncementReplyOwnership,
|
|
1768
1797
|
isQueueItemTransportActive: deps.isQueueItemTransportActive,
|
|
1769
1798
|
hasPendingInboundQueueMutationForItem: deps.hasPendingInboundQueueMutationForItem,
|
|
1770
1799
|
isQueueItemAdmissionReady: deps.isQueueItemAdmissionReady,
|
|
@@ -1776,12 +1805,19 @@ export function createTelegramQueueDispatchRuntime(deps) {
|
|
|
1776
1805
|
export function createTelegramQueueDispatchController(deps) {
|
|
1777
1806
|
let controlDispatchPending = false;
|
|
1778
1807
|
let nextDispatchAnnouncementRequested = false;
|
|
1808
|
+
let nextDispatchAnnouncementGeneration = 0;
|
|
1779
1809
|
let nextDispatchAnnouncementAnchor;
|
|
1780
1810
|
const controller = {
|
|
1781
1811
|
requestNextDispatchAnnouncement: () => {
|
|
1812
|
+
nextDispatchAnnouncementGeneration += 1;
|
|
1782
1813
|
nextDispatchAnnouncementRequested = true;
|
|
1783
1814
|
nextDispatchAnnouncementAnchor = undefined;
|
|
1784
1815
|
},
|
|
1816
|
+
cancelNextDispatchAnnouncement: () => {
|
|
1817
|
+
nextDispatchAnnouncementGeneration += 1;
|
|
1818
|
+
nextDispatchAnnouncementRequested = false;
|
|
1819
|
+
nextDispatchAnnouncementAnchor = undefined;
|
|
1820
|
+
},
|
|
1785
1821
|
dispatchNext: (ctx) => {
|
|
1786
1822
|
if (deps.hasDispatchContext && !deps.hasDispatchContext())
|
|
1787
1823
|
return;
|
|
@@ -1972,11 +2008,22 @@ export function createTelegramQueueDispatchController(deps) {
|
|
|
1972
2008
|
if (dispatchPlan.kind === "prompt" && nextDispatchAnnouncementRequested) {
|
|
1973
2009
|
nextDispatchAnnouncementRequested = false;
|
|
1974
2010
|
controlDispatchPending = true;
|
|
2011
|
+
const announcementGeneration = nextDispatchAnnouncementGeneration;
|
|
1975
2012
|
const dispatchGeneration = deps.getDispatchGeneration?.();
|
|
1976
2013
|
deps.updateStatus(ctx);
|
|
1977
2014
|
void deps.sendTextReply(dispatchPlan.item.chatId, dispatchPlan.item.replyToMessageId, "<b>⏩ Dispatching next queued turn.</b>", { target: dispatchPlan.item.target }).catch((error) => {
|
|
1978
|
-
deps.recordRuntimeEvent?.("dispatch", error, {
|
|
2015
|
+
deps.recordRuntimeEvent?.("dispatch", error, {
|
|
2016
|
+
phase: "next-announcement",
|
|
2017
|
+
});
|
|
1979
2018
|
}).finally(() => {
|
|
2019
|
+
try {
|
|
2020
|
+
deps.reconcileNextDispatchAnnouncementReplyOwnership?.(dispatchPlan.item);
|
|
2021
|
+
}
|
|
2022
|
+
catch (error) {
|
|
2023
|
+
deps.recordRuntimeEvent?.("dispatch", error, {
|
|
2024
|
+
phase: "next-announcement-reply-ownership",
|
|
2025
|
+
});
|
|
2026
|
+
}
|
|
1980
2027
|
controlDispatchPending = false;
|
|
1981
2028
|
if (deps.hasDispatchContext && !deps.hasDispatchContext())
|
|
1982
2029
|
return;
|
|
@@ -1984,6 +2031,11 @@ export function createTelegramQueueDispatchController(deps) {
|
|
|
1984
2031
|
deps.isDispatchGenerationActive &&
|
|
1985
2032
|
!deps.isDispatchGenerationActive(dispatchGeneration))
|
|
1986
2033
|
return;
|
|
2034
|
+
if (announcementGeneration !== nextDispatchAnnouncementGeneration) {
|
|
2035
|
+
if (nextDispatchAnnouncementRequested)
|
|
2036
|
+
controller.dispatchNext(ctx);
|
|
2037
|
+
return;
|
|
2038
|
+
}
|
|
1987
2039
|
executePlan();
|
|
1988
2040
|
});
|
|
1989
2041
|
return;
|
|
@@ -23,6 +23,10 @@ export interface ReplyDedupRuntime {
|
|
|
23
23
|
}
|
|
24
24
|
export declare function createReplyDedupRuntime(): ReplyDedupRuntime;
|
|
25
25
|
export declare function resetTransportReplyDedup(): void;
|
|
26
|
+
/** Keeps a successfully published transition notice as the first reply of the
|
|
27
|
+
* next agent turn. The following agent-start reset consumes this one-shot
|
|
28
|
+
* preservation, so later messages in that turn do not repeat the reply header. */
|
|
29
|
+
export declare function preserveTransportReplyDedupOnNextReset(chatId: number, messageId: number, target?: TelegramTarget): void;
|
|
26
30
|
export declare function buildTelegramReplyParameters(chatId: number, messageId: number | undefined, target?: TelegramTarget): TelegramReplyParameters | undefined;
|
|
27
31
|
export declare function withTelegramReplyParameters<T>(chatId: number, messageId: number | undefined, target: TelegramTarget | undefined, send: (parameters: TelegramReplyParameters | undefined) => Promise<T>): Promise<T>;
|
|
28
32
|
export declare function isAssistantAgentMessage(message: unknown): boolean;
|
|
@@ -31,6 +31,7 @@ export function createReplyDedupRuntime() {
|
|
|
31
31
|
}
|
|
32
32
|
// --- Transport-level dedup ---
|
|
33
33
|
const lastRepliedToMessageIdByTarget = new Map();
|
|
34
|
+
const replyDedupPreservedOnNextReset = new Map();
|
|
34
35
|
let replyDedupGeneration = 0;
|
|
35
36
|
function getReplyDedupTargetKey(chatId, target) {
|
|
36
37
|
const threadId = target?.threadId;
|
|
@@ -41,6 +42,19 @@ function getReplyDedupTargetKey(chatId, target) {
|
|
|
41
42
|
export function resetTransportReplyDedup() {
|
|
42
43
|
replyDedupGeneration += 1;
|
|
43
44
|
lastRepliedToMessageIdByTarget.clear();
|
|
45
|
+
for (const [key, messageId] of replyDedupPreservedOnNextReset) {
|
|
46
|
+
lastRepliedToMessageIdByTarget.set(key, messageId);
|
|
47
|
+
}
|
|
48
|
+
replyDedupPreservedOnNextReset.clear();
|
|
49
|
+
}
|
|
50
|
+
/** Keeps a successfully published transition notice as the first reply of the
|
|
51
|
+
* next agent turn. The following agent-start reset consumes this one-shot
|
|
52
|
+
* preservation, so later messages in that turn do not repeat the reply header. */
|
|
53
|
+
export function preserveTransportReplyDedupOnNextReset(chatId, messageId, target) {
|
|
54
|
+
const key = getReplyDedupTargetKey(chatId, target);
|
|
55
|
+
if (lastRepliedToMessageIdByTarget.get(key) !== messageId)
|
|
56
|
+
return;
|
|
57
|
+
replyDedupPreservedOnNextReset.set(key, messageId);
|
|
44
58
|
}
|
|
45
59
|
export function buildTelegramReplyParameters(chatId, messageId, target) {
|
|
46
60
|
if (messageId === undefined || messageId <= 0)
|
|
@@ -122,6 +122,7 @@ export interface TelegramInboundRouteRuntimeDeps<TMessage extends TelegramRouted
|
|
|
122
122
|
isContextActive?: (ctx: TContext) => boolean;
|
|
123
123
|
dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
|
|
124
124
|
requestNextDispatchAnnouncement?: () => void;
|
|
125
|
+
cancelNextDispatchAnnouncement?: () => void;
|
|
125
126
|
requestDeferredDispatchNextQueuedTelegramTurn?: (dispatch: (ctx: TContext) => void) => void;
|
|
126
127
|
hasDeferredDispatchContext?: () => boolean;
|
|
127
128
|
startTypingLoop?: (ctx: TContext, chatId?: number, options?: {
|
|
@@ -1418,6 +1418,10 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
1418
1418
|
isContextActive: deps.isContextActive,
|
|
1419
1419
|
dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
|
|
1420
1420
|
requestNextDispatchAnnouncement: deps.requestNextDispatchAnnouncement,
|
|
1421
|
+
cancelNextTransitionAnnouncements: () => {
|
|
1422
|
+
deps.activeTurnRuntime.clearNextAbortAnnouncement();
|
|
1423
|
+
deps.cancelNextDispatchAnnouncement?.();
|
|
1424
|
+
},
|
|
1421
1425
|
requestDeferredDispatchNextQueuedTelegramTurn: deps.requestDeferredDispatchNextQueuedTelegramTurn,
|
|
1422
1426
|
startTypingLoop: deps.startTypingLoop,
|
|
1423
1427
|
stopTypingLoop: deps.stopTypingLoop,
|
|
@@ -1476,6 +1480,7 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
1476
1480
|
},
|
|
1477
1481
|
getPromptTemplateCommands,
|
|
1478
1482
|
sendTextReply: deps.sendTextReply,
|
|
1483
|
+
markActiveTurnNextAbortAnnouncement: deps.activeTurnRuntime.markNextAbortAnnouncement,
|
|
1479
1484
|
getActiveTurnReply: () => {
|
|
1480
1485
|
const activeTurn = deps.activeTurnRuntime.get();
|
|
1481
1486
|
if (!activeTurn)
|
|
@@ -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
|
|
|
@@ -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.
|
|
@@ -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.
|
|
@@ -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,
|
|
@@ -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",
|
|
@@ -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),
|
|
@@ -820,6 +820,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
820
820
|
mutation: queueMutationRuntime,
|
|
821
821
|
dispatchNext: dispatchNextQueuedTelegramTurn,
|
|
822
822
|
requestNextDispatchAnnouncement,
|
|
823
|
+
cancelNextDispatchAnnouncement,
|
|
823
824
|
watchdog: queueDispatchWatchdogRuntime,
|
|
824
825
|
} = Bindings.createTelegramQueueBindingRuntime({
|
|
825
826
|
store: telegramQueueStore,
|
|
@@ -835,6 +836,13 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
835
836
|
updateStatus,
|
|
836
837
|
sendTextReply,
|
|
837
838
|
sendUserMessage,
|
|
839
|
+
reconcileNextDispatchAnnouncementReplyOwnership(item) {
|
|
840
|
+
Replies.preserveTransportReplyDedupOnNextReset(
|
|
841
|
+
item.chatId,
|
|
842
|
+
item.replyToMessageId,
|
|
843
|
+
item.target,
|
|
844
|
+
);
|
|
845
|
+
},
|
|
838
846
|
recordRuntimeEvent,
|
|
839
847
|
});
|
|
840
848
|
const { finalizeMarkdownPreview, preparePreviewDelivery } =
|
|
@@ -1093,6 +1101,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1093
1101
|
isContextActive: telegramSessionContextStore.isCurrent,
|
|
1094
1102
|
dispatchNextQueuedTelegramTurn,
|
|
1095
1103
|
requestNextDispatchAnnouncement,
|
|
1104
|
+
cancelNextDispatchAnnouncement,
|
|
1096
1105
|
requestDeferredDispatchNextQueuedTelegramTurn:
|
|
1097
1106
|
deferredQueueDispatchRuntime.request,
|
|
1098
1107
|
hasDeferredDispatchContext: deferredQueueDispatchRuntime.isBound,
|
|
@@ -126,6 +126,8 @@ export interface PendingTelegramTurn extends TelegramQueueItemBase {
|
|
|
126
126
|
historyText: string;
|
|
127
127
|
priorityEmoji?: string;
|
|
128
128
|
reactionSuppressionEmoji?: string;
|
|
129
|
+
/** Emit the explicit aborted-turn notice when /next settles this active turn. */
|
|
130
|
+
announceNextAbortOnEnd?: boolean;
|
|
129
131
|
|
|
130
132
|
/** Turn should preferably be delivered as voice (mirror mode + user sent voice) */
|
|
131
133
|
voiceReplyPreferred?: boolean;
|
|
@@ -235,6 +237,8 @@ export interface TelegramActiveTurnStore<
|
|
|
235
237
|
has: () => boolean;
|
|
236
238
|
set: (turn: TTurn) => void;
|
|
237
239
|
clear: () => void;
|
|
240
|
+
markNextAbortAnnouncement: () => boolean;
|
|
241
|
+
clearNextAbortAnnouncement: () => boolean;
|
|
238
242
|
getChatId: () => number | undefined;
|
|
239
243
|
getTarget: () => TelegramQueueTarget | undefined;
|
|
240
244
|
getReplyToMessageId: () => number | undefined;
|
|
@@ -461,6 +465,16 @@ export function createTelegramActiveTurnStore<
|
|
|
461
465
|
clear: () => {
|
|
462
466
|
activeTurn = undefined;
|
|
463
467
|
},
|
|
468
|
+
markNextAbortAnnouncement: () => {
|
|
469
|
+
if (!activeTurn) return false;
|
|
470
|
+
activeTurn.announceNextAbortOnEnd = true;
|
|
471
|
+
return true;
|
|
472
|
+
},
|
|
473
|
+
clearNextAbortAnnouncement: () => {
|
|
474
|
+
if (!activeTurn?.announceNextAbortOnEnd) return false;
|
|
475
|
+
delete activeTurn.announceNextAbortOnEnd;
|
|
476
|
+
return true;
|
|
477
|
+
},
|
|
464
478
|
getChatId: () => activeTurn?.chatId,
|
|
465
479
|
getTarget: () =>
|
|
466
480
|
activeTurn?.target ? { ...activeTurn.target } : undefined,
|
|
@@ -1488,6 +1502,7 @@ export interface TelegramAgentEndPlan {
|
|
|
1488
1502
|
kind: "no-turn" | "aborted" | "error" | "text" | "attachments-only" | "empty";
|
|
1489
1503
|
shouldClearPreview: boolean;
|
|
1490
1504
|
shouldDispatchNext: boolean;
|
|
1505
|
+
shouldSendAbortMessage: boolean;
|
|
1491
1506
|
shouldSendErrorMessage: boolean;
|
|
1492
1507
|
shouldSendAttachmentNotice: boolean;
|
|
1493
1508
|
}
|
|
@@ -1680,6 +1695,7 @@ export function buildTelegramAgentEndPlan(options: {
|
|
|
1680
1695
|
hasFinalText: boolean;
|
|
1681
1696
|
hasQueuedAttachments: boolean;
|
|
1682
1697
|
foldQueuedPromptsIntoHistory: boolean;
|
|
1698
|
+
announceNextAbortOnEnd?: boolean;
|
|
1683
1699
|
}): TelegramAgentEndPlan {
|
|
1684
1700
|
const shouldDispatchNext = shouldDispatchAfterTelegramAgentEnd({
|
|
1685
1701
|
hasTurn: options.hasTurn,
|
|
@@ -1691,6 +1707,7 @@ export function buildTelegramAgentEndPlan(options: {
|
|
|
1691
1707
|
kind: "no-turn",
|
|
1692
1708
|
shouldClearPreview: false,
|
|
1693
1709
|
shouldDispatchNext,
|
|
1710
|
+
shouldSendAbortMessage: false,
|
|
1694
1711
|
shouldSendErrorMessage: false,
|
|
1695
1712
|
shouldSendAttachmentNotice: false,
|
|
1696
1713
|
};
|
|
@@ -1700,6 +1717,7 @@ export function buildTelegramAgentEndPlan(options: {
|
|
|
1700
1717
|
kind: "aborted",
|
|
1701
1718
|
shouldClearPreview: true,
|
|
1702
1719
|
shouldDispatchNext,
|
|
1720
|
+
shouldSendAbortMessage: options.announceNextAbortOnEnd === true,
|
|
1703
1721
|
shouldSendErrorMessage: false,
|
|
1704
1722
|
shouldSendAttachmentNotice: false,
|
|
1705
1723
|
};
|
|
@@ -1709,6 +1727,7 @@ export function buildTelegramAgentEndPlan(options: {
|
|
|
1709
1727
|
kind: "error",
|
|
1710
1728
|
shouldClearPreview: true,
|
|
1711
1729
|
shouldDispatchNext,
|
|
1730
|
+
shouldSendAbortMessage: false,
|
|
1712
1731
|
shouldSendErrorMessage: true,
|
|
1713
1732
|
shouldSendAttachmentNotice: false,
|
|
1714
1733
|
};
|
|
@@ -1718,6 +1737,7 @@ export function buildTelegramAgentEndPlan(options: {
|
|
|
1718
1737
|
kind: "text",
|
|
1719
1738
|
shouldClearPreview: false,
|
|
1720
1739
|
shouldDispatchNext,
|
|
1740
|
+
shouldSendAbortMessage: false,
|
|
1721
1741
|
shouldSendErrorMessage: false,
|
|
1722
1742
|
shouldSendAttachmentNotice: false,
|
|
1723
1743
|
};
|
|
@@ -1727,6 +1747,7 @@ export function buildTelegramAgentEndPlan(options: {
|
|
|
1727
1747
|
kind: "attachments-only",
|
|
1728
1748
|
shouldClearPreview: true,
|
|
1729
1749
|
shouldDispatchNext,
|
|
1750
|
+
shouldSendAbortMessage: false,
|
|
1730
1751
|
shouldSendErrorMessage: false,
|
|
1731
1752
|
shouldSendAttachmentNotice: true,
|
|
1732
1753
|
};
|
|
@@ -1735,6 +1756,7 @@ export function buildTelegramAgentEndPlan(options: {
|
|
|
1735
1756
|
kind: "empty",
|
|
1736
1757
|
shouldClearPreview: true,
|
|
1737
1758
|
shouldDispatchNext,
|
|
1759
|
+
shouldSendAbortMessage: false,
|
|
1738
1760
|
shouldSendErrorMessage: false,
|
|
1739
1761
|
shouldSendAttachmentNotice: false,
|
|
1740
1762
|
};
|
|
@@ -1891,6 +1913,7 @@ export async function handleTelegramAgentEndRuntime<
|
|
|
1891
1913
|
hasFinalText: !!finalText || hasOutboundArtifacts,
|
|
1892
1914
|
hasQueuedAttachments: (turn?.queuedAttachments.length ?? 0) > 0,
|
|
1893
1915
|
foldQueuedPromptsIntoHistory: deps.foldQueuedPromptsIntoHistory,
|
|
1916
|
+
announceNextAbortOnEnd: turn?.announceNextAbortOnEnd === true,
|
|
1894
1917
|
});
|
|
1895
1918
|
if (!turn) {
|
|
1896
1919
|
if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
|
|
@@ -2017,22 +2040,29 @@ export async function handleTelegramAgentEndRuntime<
|
|
|
2017
2040
|
await clearTurnPreview();
|
|
2018
2041
|
if (!isDeliveryActive()) return;
|
|
2019
2042
|
}
|
|
2020
|
-
if (endPlan.shouldSendErrorMessage) {
|
|
2043
|
+
if (endPlan.shouldSendAbortMessage || endPlan.shouldSendErrorMessage) {
|
|
2021
2044
|
const errorMessage = assistant.errorMessage ||
|
|
2022
2045
|
"Telegram bridge: Pi failed while processing the request.";
|
|
2023
|
-
const isOperationAborted =
|
|
2024
|
-
"This operation was aborted";
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2046
|
+
const isOperationAborted = endPlan.shouldSendAbortMessage ||
|
|
2047
|
+
errorMessage.trim().replace(/\.$/, "") === "This operation was aborted";
|
|
2048
|
+
try {
|
|
2049
|
+
await deps.sendTextReply(
|
|
2050
|
+
turn.chatId,
|
|
2051
|
+
turn.replyToMessageId,
|
|
2052
|
+
isOperationAborted
|
|
2053
|
+
? "<b>⏹️ This operation was aborted.</b>"
|
|
2054
|
+
: errorMessage,
|
|
2055
|
+
{
|
|
2056
|
+
target: turn.target,
|
|
2057
|
+
...(isOperationAborted ? { parseMode: "HTML" as const } : {}),
|
|
2058
|
+
},
|
|
2059
|
+
);
|
|
2060
|
+
} catch (error) {
|
|
2061
|
+
if (!isOperationAborted) throw error;
|
|
2062
|
+
deps.recordRuntimeEvent?.("dispatch", error, {
|
|
2063
|
+
phase: "next-abort-announcement",
|
|
2064
|
+
});
|
|
2065
|
+
}
|
|
2036
2066
|
if (!isDeliveryActive()) return;
|
|
2037
2067
|
if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
|
|
2038
2068
|
return;
|
|
@@ -2180,7 +2210,7 @@ export async function handleTelegramAgentEndRuntime<
|
|
|
2180
2210
|
};
|
|
2181
2211
|
if (
|
|
2182
2212
|
deps.scheduleActiveTurnDelivery &&
|
|
2183
|
-
(endPlan.kind === "text" || endPlan.kind === "attachments-only" || endPlan.shouldSendErrorMessage || endPlan.shouldClearPreview)
|
|
2213
|
+
(endPlan.kind === "text" || endPlan.kind === "attachments-only" || endPlan.shouldSendAbortMessage || endPlan.shouldSendErrorMessage || endPlan.shouldClearPreview)
|
|
2184
2214
|
) {
|
|
2185
2215
|
deps.scheduleActiveTurnDelivery(deliverActiveTurn);
|
|
2186
2216
|
return;
|
|
@@ -3092,6 +3122,9 @@ export interface TelegramQueueDispatchControllerDeps<
|
|
|
3092
3122
|
) => boolean;
|
|
3093
3123
|
sendUserMessage: TelegramDispatchRuntimeDeps<TContext>["sendUserMessage"];
|
|
3094
3124
|
onPromptDispatchFailure: (ctx: TContext, message: string) => void;
|
|
3125
|
+
reconcileNextDispatchAnnouncementReplyOwnership?: (
|
|
3126
|
+
item: PendingTelegramTurn,
|
|
3127
|
+
) => void;
|
|
3095
3128
|
isQueueItemTransportActive?: (item: TelegramQueueItem<TContext>) => boolean;
|
|
3096
3129
|
hasPendingInboundQueueMutationForItem?: (
|
|
3097
3130
|
item: TelegramQueueItem<TContext>,
|
|
@@ -3107,6 +3140,7 @@ export interface TelegramQueueDispatchControllerDeps<
|
|
|
3107
3140
|
export interface TelegramQueueDispatchController<TContext = unknown> {
|
|
3108
3141
|
dispatchNext: (ctx: TContext) => void;
|
|
3109
3142
|
requestNextDispatchAnnouncement: () => void;
|
|
3143
|
+
cancelNextDispatchAnnouncement: () => void;
|
|
3110
3144
|
}
|
|
3111
3145
|
|
|
3112
3146
|
export function executeTelegramQueueDispatchPlan<TContext = unknown>(
|
|
@@ -3161,6 +3195,8 @@ export function createTelegramQueueDispatchRuntime<TContext = unknown>(
|
|
|
3161
3195
|
commitPromptDispatch: deps.commitPromptDispatch,
|
|
3162
3196
|
sendUserMessage: deps.sendUserMessage,
|
|
3163
3197
|
onPromptDispatchFailure: deps.onPromptDispatchFailure,
|
|
3198
|
+
reconcileNextDispatchAnnouncementReplyOwnership:
|
|
3199
|
+
deps.reconcileNextDispatchAnnouncementReplyOwnership,
|
|
3164
3200
|
isQueueItemTransportActive: deps.isQueueItemTransportActive,
|
|
3165
3201
|
hasPendingInboundQueueMutationForItem:
|
|
3166
3202
|
deps.hasPendingInboundQueueMutationForItem,
|
|
@@ -3176,12 +3212,19 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
|
|
|
3176
3212
|
): TelegramQueueDispatchController<TContext> {
|
|
3177
3213
|
let controlDispatchPending = false;
|
|
3178
3214
|
let nextDispatchAnnouncementRequested = false;
|
|
3215
|
+
let nextDispatchAnnouncementGeneration = 0;
|
|
3179
3216
|
let nextDispatchAnnouncementAnchor: TelegramQueueItem<TContext> | undefined;
|
|
3180
3217
|
const controller: TelegramQueueDispatchController<TContext> = {
|
|
3181
3218
|
requestNextDispatchAnnouncement: () => {
|
|
3219
|
+
nextDispatchAnnouncementGeneration += 1;
|
|
3182
3220
|
nextDispatchAnnouncementRequested = true;
|
|
3183
3221
|
nextDispatchAnnouncementAnchor = undefined;
|
|
3184
3222
|
},
|
|
3223
|
+
cancelNextDispatchAnnouncement: () => {
|
|
3224
|
+
nextDispatchAnnouncementGeneration += 1;
|
|
3225
|
+
nextDispatchAnnouncementRequested = false;
|
|
3226
|
+
nextDispatchAnnouncementAnchor = undefined;
|
|
3227
|
+
},
|
|
3185
3228
|
dispatchNext: (ctx) => {
|
|
3186
3229
|
if (deps.hasDispatchContext && !deps.hasDispatchContext()) return;
|
|
3187
3230
|
if (controlDispatchPending) {
|
|
@@ -3393,6 +3436,7 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
|
|
|
3393
3436
|
if (dispatchPlan.kind === "prompt" && nextDispatchAnnouncementRequested) {
|
|
3394
3437
|
nextDispatchAnnouncementRequested = false;
|
|
3395
3438
|
controlDispatchPending = true;
|
|
3439
|
+
const announcementGeneration = nextDispatchAnnouncementGeneration;
|
|
3396
3440
|
const dispatchGeneration = deps.getDispatchGeneration?.();
|
|
3397
3441
|
deps.updateStatus(ctx);
|
|
3398
3442
|
void deps.sendTextReply(
|
|
@@ -3401,8 +3445,19 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
|
|
|
3401
3445
|
"<b>⏩ Dispatching next queued turn.</b>",
|
|
3402
3446
|
{ target: dispatchPlan.item.target },
|
|
3403
3447
|
).catch((error) => {
|
|
3404
|
-
deps.recordRuntimeEvent?.("dispatch", error, {
|
|
3448
|
+
deps.recordRuntimeEvent?.("dispatch", error, {
|
|
3449
|
+
phase: "next-announcement",
|
|
3450
|
+
});
|
|
3405
3451
|
}).finally(() => {
|
|
3452
|
+
try {
|
|
3453
|
+
deps.reconcileNextDispatchAnnouncementReplyOwnership?.(
|
|
3454
|
+
dispatchPlan.item,
|
|
3455
|
+
);
|
|
3456
|
+
} catch (error) {
|
|
3457
|
+
deps.recordRuntimeEvent?.("dispatch", error, {
|
|
3458
|
+
phase: "next-announcement-reply-ownership",
|
|
3459
|
+
});
|
|
3460
|
+
}
|
|
3406
3461
|
controlDispatchPending = false;
|
|
3407
3462
|
if (deps.hasDispatchContext && !deps.hasDispatchContext()) return;
|
|
3408
3463
|
if (
|
|
@@ -3410,6 +3465,10 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
|
|
|
3410
3465
|
deps.isDispatchGenerationActive &&
|
|
3411
3466
|
!deps.isDispatchGenerationActive(dispatchGeneration)
|
|
3412
3467
|
) return;
|
|
3468
|
+
if (announcementGeneration !== nextDispatchAnnouncementGeneration) {
|
|
3469
|
+
if (nextDispatchAnnouncementRequested) controller.dispatchNext(ctx);
|
|
3470
|
+
return;
|
|
3471
|
+
}
|
|
3413
3472
|
executePlan();
|
|
3414
3473
|
});
|
|
3415
3474
|
return;
|
|
@@ -68,6 +68,7 @@ export function createReplyDedupRuntime(): ReplyDedupRuntime {
|
|
|
68
68
|
// --- Transport-level dedup ---
|
|
69
69
|
|
|
70
70
|
const lastRepliedToMessageIdByTarget = new Map<string, number>();
|
|
71
|
+
const replyDedupPreservedOnNextReset = new Map<string, number>();
|
|
71
72
|
let replyDedupGeneration = 0;
|
|
72
73
|
|
|
73
74
|
function getReplyDedupTargetKey(
|
|
@@ -83,6 +84,23 @@ function getReplyDedupTargetKey(
|
|
|
83
84
|
export function resetTransportReplyDedup(): void {
|
|
84
85
|
replyDedupGeneration += 1;
|
|
85
86
|
lastRepliedToMessageIdByTarget.clear();
|
|
87
|
+
for (const [key, messageId] of replyDedupPreservedOnNextReset) {
|
|
88
|
+
lastRepliedToMessageIdByTarget.set(key, messageId);
|
|
89
|
+
}
|
|
90
|
+
replyDedupPreservedOnNextReset.clear();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Keeps a successfully published transition notice as the first reply of the
|
|
94
|
+
* next agent turn. The following agent-start reset consumes this one-shot
|
|
95
|
+
* preservation, so later messages in that turn do not repeat the reply header. */
|
|
96
|
+
export function preserveTransportReplyDedupOnNextReset(
|
|
97
|
+
chatId: number,
|
|
98
|
+
messageId: number,
|
|
99
|
+
target?: TelegramTarget,
|
|
100
|
+
): void {
|
|
101
|
+
const key = getReplyDedupTargetKey(chatId, target);
|
|
102
|
+
if (lastRepliedToMessageIdByTarget.get(key) !== messageId) return;
|
|
103
|
+
replyDedupPreservedOnNextReset.set(key, messageId);
|
|
86
104
|
}
|
|
87
105
|
|
|
88
106
|
export function buildTelegramReplyParameters(
|
|
@@ -627,6 +627,7 @@ export interface TelegramInboundRouteRuntimeDeps<
|
|
|
627
627
|
isContextActive?: (ctx: TContext) => boolean;
|
|
628
628
|
dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
|
|
629
629
|
requestNextDispatchAnnouncement?: () => void;
|
|
630
|
+
cancelNextDispatchAnnouncement?: () => void;
|
|
630
631
|
requestDeferredDispatchNextQueuedTelegramTurn?: (
|
|
631
632
|
dispatch: (ctx: TContext) => void,
|
|
632
633
|
) => void;
|
|
@@ -2222,6 +2223,10 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2222
2223
|
isContextActive: deps.isContextActive,
|
|
2223
2224
|
dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
|
|
2224
2225
|
requestNextDispatchAnnouncement: deps.requestNextDispatchAnnouncement,
|
|
2226
|
+
cancelNextTransitionAnnouncements: () => {
|
|
2227
|
+
deps.activeTurnRuntime.clearNextAbortAnnouncement();
|
|
2228
|
+
deps.cancelNextDispatchAnnouncement?.();
|
|
2229
|
+
},
|
|
2225
2230
|
requestDeferredDispatchNextQueuedTelegramTurn:
|
|
2226
2231
|
deps.requestDeferredDispatchNextQueuedTelegramTurn,
|
|
2227
2232
|
startTypingLoop: deps.startTypingLoop,
|
|
@@ -2297,6 +2302,8 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2297
2302
|
},
|
|
2298
2303
|
getPromptTemplateCommands,
|
|
2299
2304
|
sendTextReply: deps.sendTextReply,
|
|
2305
|
+
markActiveTurnNextAbortAnnouncement:
|
|
2306
|
+
deps.activeTurnRuntime.markNextAbortAnnouncement,
|
|
2300
2307
|
getActiveTurnReply: () => {
|
|
2301
2308
|
const activeTurn = deps.activeTurnRuntime.get();
|
|
2302
2309
|
if (!activeTurn) return undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-kit",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.1",
|
|
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.18.0",
|
|
48
|
-
"@llblab/pi-telegram": "0.51.
|
|
48
|
+
"@llblab/pi-telegram": "0.51.1",
|
|
49
49
|
"@llblab/skills": "1.15.0"
|
|
50
50
|
},
|
|
51
51
|
"bundledDependencies": [
|