@llblab/pi-kit 0.21.0 → 0.21.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +3 -3
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +2 -2
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +6 -0
- package/node_modules/@llblab/pi-state-flow/README.md +2 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +17 -6
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.d.ts +4 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +29 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/logging.d.ts +3 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/logging.js +33 -7
- package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +1 -1
- package/node_modules/@llblab/pi-state-flow/docs/performance.md +10 -2
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +4 -4
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +15 -6
- package/node_modules/@llblab/pi-state-flow/lib/git.ts +24 -1
- package/node_modules/@llblab/pi-state-flow/lib/logging.ts +31 -6
- package/node_modules/@llblab/pi-state-flow/package.json +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 +3 -3
|
@@ -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.
|