@llblab/pi-telegram 0.56.0 → 0.56.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -4
- package/BACKLOG.md +2 -0
- package/CHANGELOG.md +11 -0
- package/README.md +4 -2
- package/dist/lib/bus-follower.d.ts +1 -0
- package/dist/lib/bus-follower.js +6 -5
- package/dist/lib/bus-leader.d.ts +2 -0
- package/dist/lib/bus-leader.js +26 -20
- package/dist/lib/bus.d.ts +5 -0
- package/dist/lib/bus.js +15 -7
- package/dist/lib/commands.d.ts +1 -1
- package/dist/lib/commands.js +33 -28
- package/dist/lib/extension.js +4 -1
- package/dist/lib/locks.d.ts +6 -2
- package/dist/lib/locks.js +3 -3
- package/dist/lib/menu-queue.js +5 -7
- package/dist/lib/polling.d.ts +6 -2
- package/dist/lib/polling.js +2 -2
- package/dist/lib/routing.d.ts +4 -0
- package/dist/lib/routing.js +62 -1
- package/dist/lib/sync.js +11 -1
- package/dist/lib/threads.d.ts +11 -0
- package/dist/lib/threads.js +57 -6
- package/dist/lib/updates.d.ts +6 -27
- package/dist/lib/updates.js +25 -12
- package/dist/package.json +1 -1
- package/docs/architecture.md +12 -2
- package/docs/multi-instance-bus.md +6 -4
- package/docs/ui-style.md +4 -8
- package/lib/bus-follower.ts +8 -5
- package/lib/bus-leader.ts +28 -23
- package/lib/bus.ts +16 -7
- package/lib/commands.ts +52 -47
- package/lib/extension.ts +4 -1
- package/lib/locks.ts +5 -2
- package/lib/menu-queue.ts +6 -8
- package/lib/polling.ts +4 -1
- package/lib/routing.ts +51 -1
- package/lib/sync.ts +11 -0
- package/lib/threads.ts +57 -5
- package/lib/updates.ts +23 -12
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -74,7 +74,7 @@ Use the relevant local skill before non-trivial work in its domain. Keep skill o
|
|
|
74
74
|
- The bridge is session-local and paired to one allowed Telegram user. Preserve `{ chatId, threadId? }` through every inbound, queue, callback, reaction, media, preview, reply, menu, voice, attachment, and direct-delivery path.
|
|
75
75
|
- First-contact pairing grants in-memory authority only after profile/token/execution-fenced durable publication confirms that exact user; it never overwrites another configured owner. `profiles.<name>.botToken` may store an exact `$NAME`/`${NAME}` environment reference instead of a copied secret: resolve it only at validation or activation boundaries, fail closed with a redacted named-variable diagnostic when unresolved, and keep literal tokens compatible. Sender admission precedes user message/edit/callback/reaction delegation, including foreign ownership and unbound-Thread fallback paths. Reactions require an existing exact human owner; private chat type is not authorization. Queued config persistence must not replay observed authority as local grant edits or erase later local unpair. Setup and retry details belong in [`docs/architecture.md`](./docs/architecture.md#setup-flow).
|
|
76
76
|
- Telegram transport ownership is not semantic queue ownership. Losing the exact transport lock must not erase accepted local queue work or stop valid local Pi dispatch; direct Bot API mutations fail closed until exact direct or follower authority exists.
|
|
77
|
-
- Runtime files live under `<agent-dir>/tmp/pi-telegram` (releases before 0.52.0 used `tmp/telegram`, which a new release never writes, migrates or deletes). The older `owners.json` is read-only evidence: a live fresh owner there blocks acquisition so two release generations never poll one bot, and only its identity is exposed, never its bus endpoint or secret. Only `state.json` and `logs.jsonl` are persistent root files: the per-profile `transport` section of `state.json` is the sole transport-owner authority, `workspace` owns canonical Workspace/Restore evidence and `admission` its leases; the `runtime` section and `logs.jsonl` never grant routing authority. Each section changes only through the shared transaction under its own domain authority (nonleader admission remains process/birth-owned, not transport-owned); Workspace/runtime publication captures exact session authority, and acquisition, refresh, release, takeover, and irreversible leader work fence the exact owner/epoch. Ordinary reads and section mutations still refuse a damaged shared envelope without repair. Only a non-election leader start (`/telegram-connect` or session auto-start) runs the operator-approved optimistic reset: under the shared transaction guard it replaces damaged envelope/transport/Workspace/admission evidence with an empty envelope, accepting loss of every profile's runtime continuity; filesystem access errors still refuse (see [Damaged-State Reset](./docs/architecture.md#damaged-state-reset-operator-approved)). Use the [session journal contract](./docs/multi-instance-bus.md#session-owned-journal-storage) for owners-named polling continuity and the explicitly approved optimistic session-family sweep; that sweep is not Thread deletion or receipt settlement.
|
|
77
|
+
- Runtime files live under `<agent-dir>/tmp/pi-telegram` (releases before 0.52.0 used `tmp/telegram`, which a new release never writes, migrates or deletes). The older `owners.json` is read-only evidence: a live fresh owner there blocks acquisition so two release generations never poll one bot, and only its identity is exposed, never its bus endpoint or secret. Only `state.json` and `logs.jsonl` are persistent root files: the per-profile `transport` section of `state.json` is the sole transport-owner authority, `workspace` owns canonical Workspace/Restore evidence and `admission` its leases; the `runtime` section and `logs.jsonl` never grant routing authority. Each section changes only through the shared transaction under its own domain authority (nonleader admission remains process/birth-owned, not transport-owned); Workspace/runtime publication captures exact session authority, and acquisition, refresh, release, takeover, and irreversible leader work fence the exact owner/epoch. Ordinary reads and section mutations still refuse a damaged shared envelope without repair. Explicit `/telegram-connect` additionally grants one profile-admitted reset of matching untargeted unfinished Thread-creation metadata before Workspace identity allocation; it may leave an unknown Telegram tab behind, but never deletes a known target, Workspace binding or journal receipt. Automatic startup/re-registration and Restore-only admission cannot grant that reset. Known pending targets retain their exact binding key and slot; fresh provisioning uses unique operation IDs so late recovery evidence cannot attach to a replacement attempt. Follower reset intent uses authenticated `explicit-connect-recovery-v1` registration. Only a non-election leader start (`/telegram-connect` or session auto-start) runs the operator-approved optimistic reset: under the shared transaction guard it replaces damaged envelope/transport/Workspace/admission evidence with an empty envelope, accepting loss of every profile's runtime continuity; filesystem access errors still refuse (see [Damaged-State Reset](./docs/architecture.md#damaged-state-reset-operator-approved)). Use the [session journal contract](./docs/multi-instance-bus.md#session-owned-journal-storage) for owners-named polling continuity and the explicitly approved optimistic session-family sweep; that sweep is not Thread deletion or receipt settlement.
|
|
78
78
|
- 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.
|
|
79
79
|
- 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. Same-session refresh awaits binding preparation without re-registering; a changed session ID cannot publish readiness until its exact leader registration is acknowledged. Reusing a context object cannot carry readiness across a session-generation or ID 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.
|
|
80
80
|
- 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`, capability-gated restore-only `follower.restoreWorkspace` and side-effect-free liveness `bus.probe` 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.
|
|
@@ -99,15 +99,15 @@ Use the relevant local skill before non-trivial work in its domain. Keep skill o
|
|
|
99
99
|
|
|
100
100
|
### 4.4 Queue, Delivery, And User Surfaces
|
|
101
101
|
|
|
102
|
-
- Queue lane/kind admission is explicit. Ordinary new-task 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 shows one warning-yellow `+N` suffix for executable waiting work across all lanes (Normal/Priority/Steer/Control), omitted when zero. It excludes the dispatched head, frozen attempts and Skip, and never counts current agent work from any source. Each prompt is one object with one active lane and no reserved return slot. Control/Steer/Priority/Normal have explicit ranks. For ordinary prompts, crossing lanes removes it from the source and appends it at the destination tail; Keep/Skip and unchanged effective intent preserve lane position. Complete reaction sets derive Steer from fire, otherwise Priority from thumbs-up
|
|
102
|
+
- Queue lane/kind admission is explicit. Ordinary new-task 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 shows one warning-yellow `+N` suffix for executable waiting work across all lanes (Normal/Priority/Steer/Control), omitted when zero. It excludes the dispatched head, frozen attempts and Skip, and never counts current agent work from any source. Each prompt is one object with one active lane and no reserved return slot. Control/Steer/Priority/Normal have explicit ranks. For ordinary prompts, crossing lanes removes it from the source and appends it at the destination tail; Keep/Skip and unchanged effective intent preserve lane position. Only fire, thumbs-up and thumbs-down have queue reaction semantics. Complete reaction sets derive Steer from fire, otherwise Priority from thumbs-up, and independent Skip from thumbs-down. All other reactions are decorative and cannot flush or mutate queued work. Fire dominates thumbs-up; lightning is a Control marker, never a command classifier or Priority alias. Decorative changes cannot reset a menu lane choice; positive intent and Skip 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.
|
|
103
103
|
- Experimental Steer stays in the existing Queue/admission owner until one protected whole-packet attempt. Active insertion uses public `turn_end` after one response/full tool batch without resetting current Telegram reply/preview ownership; idle uses ordinary `sendUserMessage`. Recheck exact context/generation/source/native/compaction guards, freeze original objects before whole-receipt journal disposition, and never treat it as SDK/model-use proof. Partial/refused/unknown attempts remain non-executable and immutable without replay. Idle originals stay behind one pinned pending head until `agent_start` projects one active reply/voice/attachment owner. Keep identity fencing separate from same-session readiness refusal so old failures cannot clear replacement flags and unissued packets cannot contaminate native agent starts. Queue transfer allows unspent Steer only with `queue-steer-v1` plus baseline handoff support at donor/leader/recipient; missing capability refuses before offer, and frozen/pending work never transfers as unspent input.
|
|
104
|
-
- `/stop`, `/abort`, `/next`, and `/continue` respectively reset+abort, abort while preserving queue, force the next turn, and enqueue a control-lane continuation. A
|
|
104
|
+
- `/stop`, `/abort`, `/next`, and `/continue` respectively reset+abort, abort while preserving queue, force the next turn, and enqueue a control-lane continuation. A thumbs-down reaction to an original standalone `/continue` settles and removes only its exact waiting source-addressed continuation; positive reactions do not change its control lane. Protect the Pi-owned pending head and already-started work, preserve other queue positions, and never address synthetic model continuations by their reply anchor. Settlement failure retains the 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.
|
|
105
105
|
- A model selection from an authorized Telegram target may stop and continue any interruptible active agent run in that same Pi session, including local/TUI work. Queue the synthetic control-lane continuation before aborting; when no Telegram prompt owns the run, bind continuation and reply ownership to the exact model-menu chat/Thread/message. Delay abort until every active tool execution settles, and clear both selection and fallback target on cancellation, agent start, settlement, or session replacement.
|
|
106
106
|
- 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.
|
|
107
107
|
- In Threaded Mode, All is service/input-only. New output, previews, typing and routing UI require a concrete Thread; neither implicit fallback nor explicit aggregate delivery may write to All. Unavailable routing authority retains input without an All notice or silent settlement. Classic DM and explicit channel/business destinations remain separate; historical All output is not auto-cleaned.
|
|
108
108
|
- 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.
|
|
109
109
|
- `preview` owns streaming lifecycle only, not assistant rendering. Finalization waits for the captured generation's preceding preview work, never later drafts blocked by that publication, and must not issue pre/post-final draft-clear calls that create transient Telegram draft UI. Reset/invalidation detaches predecessor dependencies; late settlements cannot mutate replacement state. Turns that already answer as one atomic reply (voice replies, Guest Mode queries) never stream previews.
|
|
110
|
-
- Native `sendChatAction(typing)` is the automatic activity signal for unsettled agent and compaction work while Telegram transport is authorized. Extension-owned blocking UI prompts pause it and completion resumes it while either work owner remains active. Compaction may stop only a typing loop it actually started; it must preserve a pre-existing agent-owned loop. Refresh only the assigned Thread on a conservative three-second cadence; do not mirror typing to aggregate `All` and multiply shared-chat flood pressure. Typing is best-effort presence only: a failed exact-target action remains a structured diagnostic and must not replace a healthy connected/leader/follower status with `error`. Do not invent extra in-chat work indicators or emit activity for startup/connect/reload/recovery alone.
|
|
110
|
+
- Native `sendChatAction(typing)` is the automatic activity signal for unsettled agent and compaction work while Telegram transport is authorized. Extension-owned blocking UI prompts pause it and completion resumes it while either work owner remains active. Compaction may stop only a typing loop it actually started; it must preserve a pre-existing agent-owned loop. Refresh only the assigned Thread on a conservative three-second cadence; do not mirror typing to aggregate `All` and multiply shared-chat flood pressure. Typing is best-effort presence only, never a Thread-existence or message-delivery probe: a failed exact-target action remains a structured diagnostic and must not replace a healthy connected/leader/follower status with `error`. Do not invent extra in-chat work indicators or emit activity for startup/connect/reload/recovery alone.
|
|
111
111
|
- Public activity handlers and connected companion delivery are asynchronous, target-bound, generation-fenced surfaces. Connected companion projection has no independent opt-out: disconnect or authority loss is its boundary. Token deltas, hidden reasoning, unknown sources, and stale authority never enter public projection.
|
|
112
112
|
- Thread display defaults to the profile-scoped Letters strategy, with Names, Directory Snake, and Directory Title as the other automatic choices; Names projects the generated dictionary name for the slot. Unsupported retained display keys resolve to Letters without rewriting persisted configuration. A durable manual Thread display name retained on its Workspace binding overrides any automatic projection until exact reset; keep generated/recovery identity separate from manual and acknowledged display fields. After leader startup, automatic display contraction waits one follower-staleness window so election and follower re-registration cannot briefly remove and restore an acknowledged same-cwd suffix; stop/start generation cancels stale reconciliation. UI labels, emoji semantics, navigation, settings controls, callback namespaces, voice behavior, command templates, and assistant markup follow the linked `/docs` contracts. Generated human-readable action labels use `emoji + space + text`; emoji-free action text is only a reasoned no-semantic-marker fallback. State/value controls follow the [button control hierarchy](./docs/ui-style.md#button-control-hierarchy), not the action-label grammar. Non-spatial generated controls default to top-level vertical cells, with nested rows reserved for unmistakably compact peers. Do not restate other evolving UI details here.
|
|
113
113
|
|
package/BACKLOG.md
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
_This file owns unresolved project work only. Completed behavior belongs in `CHANGELOG.md`; durable contracts belong in `AGENTS.md` and `/docs`._
|
|
4
4
|
|
|
5
|
+
- [ ] **Connection recovery live acceptance.** Verify operator-run leader/follower reconnect after interrupted creation on updated peers. Known target reuse and unknown-target abandonment have separate local regressions; do not infer live recovery from them. Also verify a long-deleted follower tab, follower `/start` Restore with negotiated menu-alias support, and leader Restore confirmation on the first click plus separately proven old-tab cleanup. Record leftover-tab behavior, exact peer capabilities and diagnostics; local regressions and release authorization do not replace this live evidence.
|
|
5
6
|
- [ ] **Steer follow-up live acceptance.** Release authorization explicitly defers these checks beyond 0.56.0; do not treat automated coverage or the accepted busy-run smoke as live proof for unrun scenarios. Preserve the [queue and dispatch contract](./docs/architecture.md#queue-and-dispatch-safety).
|
|
6
7
|
- [ ] **Operator scenarios.** Verify final selector/counter rendering; idle packets, native pending input, compaction, model switching, abort/Continue, complete media groups, Reroute/Restore, negotiated leader/follower transfer, reply/preview ownership and session-generation replacement. Record actual Pi/OS/Telegram versions, peer capabilities and redacted outcomes. Reload and disposable live tests require operator intent; do not manipulate runtime files to obtain acceptance.
|
|
8
|
+
- [ ] **Restore recipient refusal after leadership change.** Diagnose a retained routing chooser refusing the selected current leader with unavailable recipient authority. Identify the exact context/session/Workspace mismatch before proposing recovery; do not treat an absent follower registry as the cause of a leader-target refusal, bypass fences or mutate live state.
|
|
7
9
|
- [ ] **Post-reload diagnostics.** Investigate recurring typing-authority refusals and Workspace publication retries observed alongside successful leader/follower reconnection. Establish whether they are expected fenced stale work or affect current-session delivery; successful registration alone does not establish error-free operation.
|
|
8
10
|
- [ ] **Native Windows scenarios.** Verify named-pipe custody/ACK, journal/atomic rename, reused-context lifecycle and the phone scenarios on an actual Windows host. Record skipped tests and missing platform evidence explicitly; a hosted CI result does not establish phone or full live-runtime acceptance.
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,17 @@
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.56.2: Canonical Queue Reactions
|
|
8
|
+
|
|
9
|
+
- `Queue reactions`: Only thumbs-up selects Priority, thumbs-down selects Skip, and fire selects Steer. Heart, dove, ghost, broken-heart, poop and wastebasket aliases are no longer controls. Unsupported reactions neither flush waiting input nor change queue intent; removing a supported reaction does not fall back to a retired alias. Only thumbs-down cancels a waiting standalone Continue.
|
|
10
|
+
- `Queue display`: Priority and Skip badges use thumbs-up and thumbs-down even for retained older metadata. Existing lane, disposition, source and receipt authority remain unchanged; Control keeps its lightning label, not a reaction shortcut.
|
|
11
|
+
|
|
12
|
+
## 0.56.1: Recover interrupted connections
|
|
13
|
+
|
|
14
|
+
- `Connection recovery`: Explicit `/telegram-connect` can abandon matching unknown Thread-creation attempts and connect afresh without deleting tabs or clearing accepted work. Known pending targets keep their Workspace key and slot; unique creation IDs prevent late replies from attaching to a new attempt. Follower recovery requires `explicit-connect-recovery-v1` on both peers. A lost API reply may leave an extra tab; automatic startup and Restore-only registration never reset it.
|
|
15
|
+
- `Restore`: Reconnecting followers verify retained tabs with a connected message rather than typing, replacing a proven deleted tab. Negotiated `held-menu-aliases-v1` lets `/start` and `/help` restore a follower and open its restricted menu without pairing or bootstrap. A leader Restore waits briefly for the exact command-removal ACK on the original click, independently of menu delivery; lost ACKs never replay input, and old-tab cleanup remains separately fenced.
|
|
16
|
+
- `Windows validation`: Follower control fixtures use fresh registration timestamps and the production Live Restore deadline; a delayed apply case retains exact command and receipt assertions. Commentary/Next integration tests await observed progress under a shared test deadline and always stop their runtime before restoring fixtures, including on failure. Runtime authority and retry policy are unchanged.
|
|
17
|
+
|
|
7
18
|
## 0.56.0: Steer from Telegram
|
|
8
19
|
|
|
9
20
|
- `Thread recovery`: A startup Telegram API timeout can leave the owner without a local bus despite retained Threaded Mode. A fresh positive capability probe now starts that owner's Threaded transport instead of treating the unchanged saved mode as readiness. Healthy buses stay running and followers cannot acquire leadership through this recovery.
|
package/README.md
CHANGED
|
@@ -61,6 +61,8 @@ The connected Pi instance owns Telegram polling. Use `/telegram-connect <profile
|
|
|
61
61
|
|
|
62
62
|
`/telegram-connect` keeps working when the shared runtime `state.json` is damaged. If it is malformed, foreign or contains invalid evidence, the connecting leader replaces it with a fresh empty state and continues. Every profile then forgets old Thread bindings, slots, unfinished Restore/temporary work and polling continuity; `telegram.json`, Pi history and the released `tmp/telegram` are untouched. Permission errors still stop connection. See [Damaged-State Reset](./docs/architecture.md#damaged-state-reset-operator-approved).
|
|
63
63
|
|
|
64
|
+
If Thread creation was interrupted, an explicit `/telegram-connect` can reset that unfinished attempt and try a fresh connection without clearing queued prompts or taking over a live leader. A lost Telegram response may leave an extra tab behind; known tabs are reused rather than blindly recreated. Both peers need connection-recovery support. See [Connection Recovery](./docs/architecture.md#explicit-connection-recovery).
|
|
65
|
+
|
|
64
66
|
The prompt queue is session-local. Before a reload or restart, let it drain or knowingly accept losing waiting work and re-send what still matters afterwards; nothing is replayed automatically.
|
|
65
67
|
|
|
66
68
|
Ordinary network or admission failures use exponential retry pauses capped at 30 seconds, without permanently stopping after an arbitrary attempt count. The poller recovers when the outage clears. Explicit `/telegram-disconnect` still attempts to stop its captured local transport if Thread cleanup is unavailable; it reports unconfirmed cleanup rather than successful deletion, and an old disconnect cannot stop a new connection. Incomplete stop/release remains visible; check `/telegram-status --debug` and keep Pi open.
|
|
@@ -206,7 +208,7 @@ In Threaded Mode, `All` is input-only: messages enter temporary routing tabs, an
|
|
|
206
208
|
|
|
207
209
|
Messages sent while Pi is busy become queued turns. Queue controls let you inspect, prioritize, keep or skip, and dispatch work without touching the terminal.
|
|
208
210
|
|
|
209
|
-
Use reactions or the queue menu to control waiting prompts.
|
|
211
|
+
Use reactions or the queue menu to control waiting prompts. Only `👍`, `👎`, and `🔥` have queue semantics; all other reactions are ignored.
|
|
210
212
|
|
|
211
213
|
Priority — when the prompt runs:
|
|
212
214
|
- **Normal — no dedicated reaction emoji.** The default lane for ordinary queued work.
|
|
@@ -225,7 +227,7 @@ Queue policy:
|
|
|
225
227
|
- One yellow terminal counter shows waiting work across all lanes, excluding current or already-dispatched work, Skip and frozen attempts.
|
|
226
228
|
- Dispatched or frozen prompts are inspection-only. An uncertain Steer attempt is not automatically retried.
|
|
227
229
|
- The bot cannot remove your Telegram reactions; changing a menu choice may leave the original reaction visible.
|
|
228
|
-
- A
|
|
230
|
+
- A `👎` reaction to your waiting standalone `/continue` cancels it immediately rather than marking it for reversible Skip. Already-running work is unaffected.
|
|
229
231
|
- The queue is session-local. Let it drain before reload or shutdown, or accept losing waiting work.
|
|
230
232
|
|
|
231
233
|
See [Queue Behavior and Safety](./docs/architecture.md#priority-reactions-keep-and-skip) for reaction precedence, dispatch rules and recovery details.
|
|
@@ -37,6 +37,7 @@ export interface TelegramBusFollowerRegistrationRuntime<TContext> {
|
|
|
37
37
|
target?: TelegramTarget;
|
|
38
38
|
previousInstanceId?: string;
|
|
39
39
|
restoreWorkspace?: boolean;
|
|
40
|
+
recoverConnection?: boolean;
|
|
40
41
|
}) => Promise<boolean>;
|
|
41
42
|
setContext: (ctx: TContext) => void | Promise<void>;
|
|
42
43
|
disconnectFromLeader?: () => Promise<boolean>;
|
package/dist/lib/bus-follower.js
CHANGED
|
@@ -9,7 +9,7 @@ import { createHash } from "node:crypto";
|
|
|
9
9
|
import { basename } from "node:path";
|
|
10
10
|
import { isDeepStrictEqual } from "node:util";
|
|
11
11
|
import { getTelegramBusTransportRetryPolicy, TELEGRAM_BUS_REGISTRATION_RETRY, } from "./bus-transport.js";
|
|
12
|
-
import { createTelegramBusFollowerDeliveryIdentity, createTelegramBusForeignOwnedUpdateForwarder, createTelegramBusLocalServer, createTelegramBusRequestIdFactory, createUnauthorizedBusAck, rejectTelegramBusRequest, getTelegramBusProtocolCompatibility, getTelegramBusSocketPath, hasTelegramBusCapability, hasTelegramBusSteerQueueHandoffCapability, isTelegramBusEnvelopeAuthorized, parseTelegramBusEnvelope, resolveTelegramBusSocketPath, sendTelegramBusLocalEnvelope, TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT, TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY,
|
|
12
|
+
import { createTelegramBusFollowerDeliveryIdentity, createTelegramBusForeignOwnedUpdateForwarder, createTelegramBusLocalServer, createTelegramBusRequestIdFactory, createUnauthorizedBusAck, rejectTelegramBusRequest, getTelegramBusProtocolCompatibility, getTelegramBusSocketPath, hasTelegramBusCapability, hasTelegramBusSteerQueueHandoffCapability, isTelegramBusEnvelopeAuthorized, parseTelegramBusEnvelope, resolveTelegramBusSocketPath, sendTelegramBusLocalEnvelope, TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT, TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY, TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SAVE, TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE, TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND_EXECUTION, TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY, TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME, TELEGRAM_BUS_HELD_COMMAND_CAPABILITIES, getTelegramBusHeldCommandCapabilities, TELEGRAM_BUS_LIVE_REBIND_CAPABILITIES, TelegramBusLocalAuthorityError, } from "./bus.js";
|
|
13
13
|
import { parseTelegramUpdateJournalQueueOwner, } from "./journal.js";
|
|
14
14
|
import { TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS, } from "./locks.js";
|
|
15
15
|
import * as Machine from "./machine.js";
|
|
@@ -1792,7 +1792,7 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
|
|
|
1792
1792
|
deps.setActiveAuthSecret?.(undefined);
|
|
1793
1793
|
await deps.stopReceiving?.();
|
|
1794
1794
|
};
|
|
1795
|
-
const pendingHandoff = options
|
|
1795
|
+
const pendingHandoff = options && !options.recoverConnection
|
|
1796
1796
|
? undefined
|
|
1797
1797
|
: getTelegramFollowerSessionHandoff();
|
|
1798
1798
|
const pendingHandoffOptions = isTelegramFollowerSessionHandoffFresh(pendingHandoff)
|
|
@@ -1801,7 +1801,7 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
|
|
|
1801
1801
|
previousInstanceId: pendingHandoff.instanceId,
|
|
1802
1802
|
}
|
|
1803
1803
|
: undefined;
|
|
1804
|
-
const registrationOptions = options
|
|
1804
|
+
const registrationOptions = options ? { ...pendingHandoffOptions, ...options } : pendingHandoffOptions;
|
|
1805
1805
|
const leaderSocketPath = leader.busSocketPath ??
|
|
1806
1806
|
deps.getLeaderSocketPath?.() ??
|
|
1807
1807
|
getTelegramBusSocketPath();
|
|
@@ -1821,6 +1821,8 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
|
|
|
1821
1821
|
: "follower.register",
|
|
1822
1822
|
requestId: registrationGeneration,
|
|
1823
1823
|
auth: activeAuthSecret,
|
|
1824
|
+
...(registrationOptions?.recoverConnection && !registrationOptions.restoreWorkspace
|
|
1825
|
+
? { recoverConnection: true } : {}),
|
|
1824
1826
|
registration: {
|
|
1825
1827
|
instanceId: deps.instanceId,
|
|
1826
1828
|
...(registrationOptions?.previousInstanceId
|
|
@@ -2875,8 +2877,7 @@ export function createTelegramBusForwardedUpdateReceiverRuntime(deps) {
|
|
|
2875
2877
|
? [
|
|
2876
2878
|
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY,
|
|
2877
2879
|
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE,
|
|
2878
|
-
|
|
2879
|
-
TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
|
|
2880
|
+
...getTelegramBusHeldCommandCapabilities(commandInput.name),
|
|
2880
2881
|
]
|
|
2881
2882
|
: []),
|
|
2882
2883
|
];
|
package/dist/lib/bus-leader.d.ts
CHANGED
|
@@ -187,6 +187,7 @@ export declare function createTelegramBusInstanceLifecycleAnnouncement(input: {
|
|
|
187
187
|
}): TelegramBusFollowerLifecycleAnnouncement;
|
|
188
188
|
export declare function createTelegramBusFollowerTargetProvisioner(deps: TelegramBusFollowerTargetProvisionerDeps): (registration: TelegramBusInstanceRegistration, options?: {
|
|
189
189
|
existingWorkspaceBindingOnly?: boolean;
|
|
190
|
+
recoverConnection?: boolean;
|
|
190
191
|
}) => Promise<(TelegramTarget & {
|
|
191
192
|
slot?: string;
|
|
192
193
|
threadName?: string;
|
|
@@ -248,6 +249,7 @@ export interface TelegramBusLeaderEnvelopeHandlerDeps {
|
|
|
248
249
|
}>) => Promise<unknown> | unknown;
|
|
249
250
|
provisionFollowerTarget?: (registration: TelegramBusInstanceRegistration, options?: {
|
|
250
251
|
existingWorkspaceBindingOnly?: boolean;
|
|
252
|
+
recoverConnection?: boolean;
|
|
251
253
|
}) => Promise<(TelegramTarget & {
|
|
252
254
|
slot?: string;
|
|
253
255
|
threadName?: string;
|
package/dist/lib/bus-leader.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { isDeepStrictEqual } from "node:util";
|
|
8
8
|
import { getTelegramBusTransportRetryPolicy } from "./bus-transport.js";
|
|
9
|
-
import { createTelegramBusLocalServer, createUnauthorizedBusAck, rejectTelegramBusRequest, getTelegramBusEnvelopeTrafficClass, getTelegramBusFollowerSocketPath, getTelegramBusProtocolCompatibility, hasTelegramBusCapability, isTelegramBusEnvelopeAuthorized, parseTelegramBusEnvelope, sendTelegramBusLocalEnvelope, stripTelegramBusApiMetadata, TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT, TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF, hasTelegramBusSteerQueueHandoffCapability, TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY, TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE, TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND, TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME, } from "./bus.js";
|
|
9
|
+
import { createTelegramBusLocalServer, createUnauthorizedBusAck, rejectTelegramBusRequest, getTelegramBusEnvelopeTrafficClass, getTelegramBusFollowerSocketPath, getTelegramBusProtocolCompatibility, hasTelegramBusCapability, isTelegramBusEnvelopeAuthorized, parseTelegramBusEnvelope, sendTelegramBusLocalEnvelope, stripTelegramBusApiMetadata, TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT, TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF, TELEGRAM_BUS_CAPABILITY_CONNECT_RECOVERY, hasTelegramBusSteerQueueHandoffCapability, TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY, TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE, TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND, TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME, } from "./bus.js";
|
|
10
10
|
import { escapeHtml } from "./rendering.js";
|
|
11
11
|
import * as Sync from "./sync.js";
|
|
12
12
|
import { parseTelegramIntegerId as asInteger, } from "./target.js";
|
|
@@ -881,6 +881,16 @@ export function createTelegramBusFollowerTargetProvisioner(deps) {
|
|
|
881
881
|
if (typeof chatId !== "number")
|
|
882
882
|
return registration.target;
|
|
883
883
|
await deps.topicTargetStore.load();
|
|
884
|
+
if (options?.recoverConnection && !options.existingWorkspaceBindingOnly) {
|
|
885
|
+
const epoch = deps.getCurrentLeaderEpoch?.();
|
|
886
|
+
await Threads.resetTelegramConnectionProvisioning({
|
|
887
|
+
store: deps.topicTargetStore, owner: "manual-follower", instanceId: registration.instanceId,
|
|
888
|
+
profileKey: registration.profileKey ?? `manual:${registration.instanceId}`,
|
|
889
|
+
isCurrent: () => !deps.getCurrentLeaderEpoch ||
|
|
890
|
+
(epoch !== undefined && deps.getCurrentLeaderEpoch() === epoch),
|
|
891
|
+
recordEvent: deps.recordRuntimeEvent,
|
|
892
|
+
});
|
|
893
|
+
}
|
|
884
894
|
let restoredPredecessor;
|
|
885
895
|
if (options?.existingWorkspaceBindingOnly && registration.cwd && registration.sessionId &&
|
|
886
896
|
!registration.previousInstanceId && deps.getLiveFollowers && deps.isFollowerProcessAlive) {
|
|
@@ -917,6 +927,7 @@ export function createTelegramBusFollowerTargetProvisioner(deps) {
|
|
|
917
927
|
? deps.topicTargetStore.claimWorkspaceIdentity(registration.cwd, registration.instanceId, registration.previousInstanceId ?? restoredPredecessor?.instanceId, {
|
|
918
928
|
existingBindingOnly: options?.existingWorkspaceBindingOnly === true,
|
|
919
929
|
sessionId: registration.sessionId,
|
|
930
|
+
provisionProfileKey: registration.profileKey ?? `manual:${registration.instanceId}`,
|
|
920
931
|
onCapacityUnavailable() {
|
|
921
932
|
capacityUnavailable = true;
|
|
922
933
|
},
|
|
@@ -1129,11 +1140,6 @@ export function createTelegramBusFollowerTargetProvisioner(deps) {
|
|
|
1129
1140
|
};
|
|
1130
1141
|
}
|
|
1131
1142
|
const probeRequiredRecord = reconnectRecord?.status === "probe-required";
|
|
1132
|
-
const exactSessionHandoff = crossSessionReuse &&
|
|
1133
|
-
!!requestedTarget &&
|
|
1134
|
-
registration.previousInstanceId === reconnectRecord?.instanceId &&
|
|
1135
|
-
requestedTarget.chatId === reconnectRecord.target.chatId &&
|
|
1136
|
-
requestedTarget.threadId === reconnectRecord.target.threadId;
|
|
1137
1143
|
const requiresVisibilityProbe = crossSessionReuse ||
|
|
1138
1144
|
probeRequiredRecord ||
|
|
1139
1145
|
recoverableTarget !== undefined;
|
|
@@ -1151,18 +1157,14 @@ export function createTelegramBusFollowerTargetProvisioner(deps) {
|
|
|
1151
1157
|
if (requiresVisibilityProbe && connectedAnnouncement) {
|
|
1152
1158
|
try {
|
|
1153
1159
|
assertRestoreRegistration();
|
|
1154
|
-
await deps.callApi(
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
:
|
|
1161
|
-
|
|
1162
|
-
message_thread_id: connectedAnnouncement.target.threadId,
|
|
1163
|
-
text: connectedAnnouncement.text,
|
|
1164
|
-
parse_mode: connectedAnnouncement.parseMode,
|
|
1165
|
-
});
|
|
1160
|
+
await deps.callApi(
|
|
1161
|
+
// Typing is presence, not evidence that a retained Thread still accepts messages.
|
|
1162
|
+
"sendMessage", {
|
|
1163
|
+
chat_id: connectedAnnouncement.target.chatId,
|
|
1164
|
+
message_thread_id: connectedAnnouncement.target.threadId,
|
|
1165
|
+
text: connectedAnnouncement.text,
|
|
1166
|
+
parse_mode: connectedAnnouncement.parseMode,
|
|
1167
|
+
});
|
|
1166
1168
|
assertRestoreRegistration();
|
|
1167
1169
|
if (recoverableTarget || probeRequiredRecord) {
|
|
1168
1170
|
const activatedRecord = deps.topicTargetStore.upsert({
|
|
@@ -2261,6 +2263,9 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
|
|
|
2261
2263
|
(hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE) &&
|
|
2262
2264
|
hasTelegramBusCapability(envelope.registration.protocol, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE));
|
|
2263
2265
|
const restoringWorkspace = envelope.kind === "follower.restoreWorkspace";
|
|
2266
|
+
const recoverConnection = envelope.kind === "follower.register" && envelope.recoverConnection === true;
|
|
2267
|
+
const supportsConnectionRecovery = [deps.protocolIdentity, envelope.registration.protocol]
|
|
2268
|
+
.every(identity => hasTelegramBusCapability(identity, TELEGRAM_BUS_CAPABILITY_CONNECT_RECOVERY));
|
|
2264
2269
|
const supportsWorkspaceAutoConnect = hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT) &&
|
|
2265
2270
|
hasTelegramBusCapability(envelope.registration.protocol, TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT);
|
|
2266
2271
|
if (!compatibility.compatible ||
|
|
@@ -2268,7 +2273,8 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
|
|
|
2268
2273
|
!readerCompatible() ||
|
|
2269
2274
|
(envelope.registration.cwd !== undefined &&
|
|
2270
2275
|
envelope.registration.sessionId === undefined) ||
|
|
2271
|
-
(restoringWorkspace && !supportsWorkspaceAutoConnect)
|
|
2276
|
+
(restoringWorkspace && !supportsWorkspaceAutoConnect) ||
|
|
2277
|
+
(recoverConnection && !supportsConnectionRecovery)) {
|
|
2272
2278
|
return {
|
|
2273
2279
|
kind: "bus.ack",
|
|
2274
2280
|
requestId: envelope.requestId,
|
|
@@ -2309,7 +2315,7 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
|
|
|
2309
2315
|
if (hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE) &&
|
|
2310
2316
|
!deps.commitFollowerRegistration)
|
|
2311
2317
|
throw new Error("Workspace Restore registration publication protection is unavailable.");
|
|
2312
|
-
const target = await deps.provisionFollowerTarget?.(envelope.registration, { existingWorkspaceBindingOnly: restoringWorkspace });
|
|
2318
|
+
const target = await deps.provisionFollowerTarget?.(envelope.registration, { existingWorkspaceBindingOnly: restoringWorkspace, ...(recoverConnection ? { recoverConnection: true } : {}) });
|
|
2313
2319
|
if (restoringWorkspace && !target) {
|
|
2314
2320
|
return {
|
|
2315
2321
|
kind: "bus.ack",
|
package/dist/lib/bus.d.ts
CHANGED
|
@@ -38,6 +38,7 @@ export declare const TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION: "durabl
|
|
|
38
38
|
export declare const TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF: "queue-handoff-v1";
|
|
39
39
|
/** Unspent Steer lane codec; independent from baseline receipt handoff support. */
|
|
40
40
|
export declare const TELEGRAM_BUS_CAPABILITY_QUEUE_STEER: "queue-steer-v1";
|
|
41
|
+
export declare const TELEGRAM_BUS_CAPABILITY_CONNECT_RECOVERY: "explicit-connect-recovery-v1";
|
|
41
42
|
export declare const TELEGRAM_BUS_CAPABILITY_INPUT_CUSTODY_REFERENCE: "input-custody-reference-v1";
|
|
42
43
|
export declare const TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME: "workspace-thread-rename-v1";
|
|
43
44
|
export declare const TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE: "thread-display-mode-v1";
|
|
@@ -58,6 +59,9 @@ export declare const TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_COMMAND_SET: "live-thre
|
|
|
58
59
|
/** Every live rebinding needs the save/apply/settle trio; held command selections additionally need these two. */
|
|
59
60
|
export declare const TELEGRAM_BUS_LIVE_REBIND_CAPABILITIES: readonly ["live-thread-rebind-save-v1", "live-thread-rebind-apply-v1", "live-thread-rebind-settle-v1"];
|
|
60
61
|
export declare const TELEGRAM_BUS_HELD_COMMAND_CAPABILITIES: readonly ["live-thread-rebind-command-set-v1", "selected-menu-delivery-v1"];
|
|
62
|
+
/** Start/help are restricted menu aliases, negotiated separately from the original held-command set. */
|
|
63
|
+
export declare const TELEGRAM_BUS_CAPABILITY_HELD_MENU_ALIASES: "held-menu-aliases-v1";
|
|
64
|
+
export declare function getTelegramBusHeldCommandCapabilities(name: string): readonly string[];
|
|
61
65
|
export interface TelegramBusSelectedCommandInput {
|
|
62
66
|
name: string;
|
|
63
67
|
target: TelegramTarget & {
|
|
@@ -327,6 +331,7 @@ export declare function createTelegramBusFollowerSourceReferenceDeliveryIdentity
|
|
|
327
331
|
export type TelegramBusEnvelope = ({
|
|
328
332
|
kind: "follower.register";
|
|
329
333
|
requestId: string;
|
|
334
|
+
recoverConnection?: true;
|
|
330
335
|
registration: TelegramBusInstanceRegistration;
|
|
331
336
|
} | {
|
|
332
337
|
kind: "follower.restoreWorkspace";
|
package/dist/lib/bus.js
CHANGED
|
@@ -52,6 +52,7 @@ export const TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION = "durable-follo
|
|
|
52
52
|
export const TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF = "queue-handoff-v1";
|
|
53
53
|
/** Unspent Steer lane codec; independent from baseline receipt handoff support. */
|
|
54
54
|
export const TELEGRAM_BUS_CAPABILITY_QUEUE_STEER = "queue-steer-v1";
|
|
55
|
+
export const TELEGRAM_BUS_CAPABILITY_CONNECT_RECOVERY = "explicit-connect-recovery-v1";
|
|
55
56
|
export const TELEGRAM_BUS_CAPABILITY_INPUT_CUSTODY_REFERENCE = "input-custody-reference-v1";
|
|
56
57
|
export const TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME = "workspace-thread-rename-v1";
|
|
57
58
|
export const TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE = "thread-display-mode-v1";
|
|
@@ -79,6 +80,13 @@ export const TELEGRAM_BUS_HELD_COMMAND_CAPABILITIES = [
|
|
|
79
80
|
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_COMMAND_SET,
|
|
80
81
|
TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
|
|
81
82
|
];
|
|
83
|
+
/** Start/help are restricted menu aliases, negotiated separately from the original held-command set. */
|
|
84
|
+
export const TELEGRAM_BUS_CAPABILITY_HELD_MENU_ALIASES = "held-menu-aliases-v1";
|
|
85
|
+
export function getTelegramBusHeldCommandCapabilities(name) {
|
|
86
|
+
return name === "start" || name === "help"
|
|
87
|
+
? [...TELEGRAM_BUS_HELD_COMMAND_CAPABILITIES, TELEGRAM_BUS_CAPABILITY_HELD_MENU_ALIASES]
|
|
88
|
+
: TELEGRAM_BUS_HELD_COMMAND_CAPABILITIES;
|
|
89
|
+
}
|
|
82
90
|
function parseSelectedCommandInput(value) {
|
|
83
91
|
if (!isRecord(value) ||
|
|
84
92
|
!hasOnlyWireKeys(value, ["name", "target"]) ||
|
|
@@ -1316,12 +1324,7 @@ export function createTelegramBusLiveRebindController(deps) {
|
|
|
1316
1324
|
: []),
|
|
1317
1325
|
...(settling || held ? [TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE] : []),
|
|
1318
1326
|
...(observingHeld ? [TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND_EXECUTION] : []),
|
|
1319
|
-
...(held
|
|
1320
|
-
? [
|
|
1321
|
-
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_COMMAND_SET,
|
|
1322
|
-
TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
|
|
1323
|
-
]
|
|
1324
|
-
: []),
|
|
1327
|
+
...(held ? getTelegramBusHeldCommandCapabilities(held.name) : []),
|
|
1325
1328
|
];
|
|
1326
1329
|
const follower = deps.getFollower(expected.instanceId), secret = deps.getAuthSecret();
|
|
1327
1330
|
if (!isCurrent() ||
|
|
@@ -2144,8 +2147,13 @@ async function handleTelegramBusSocketLine(line, socket, handleEnvelope, recordT
|
|
|
2144
2147
|
}
|
|
2145
2148
|
}
|
|
2146
2149
|
function parseRegisterEnvelope(value, requestId, kind) {
|
|
2150
|
+
if (value.recoverConnection !== undefined &&
|
|
2151
|
+
(kind !== "follower.register" || value.recoverConnection !== true))
|
|
2152
|
+
return undefined;
|
|
2147
2153
|
const registration = parseRegistration(value.registration);
|
|
2148
|
-
return registration ? { kind, requestId, registration
|
|
2154
|
+
return registration ? { kind, requestId, registration,
|
|
2155
|
+
...(kind === "follower.register" && value.recoverConnection === true ? { recoverConnection: true } : {}),
|
|
2156
|
+
} : undefined;
|
|
2149
2157
|
}
|
|
2150
2158
|
function parseFollowerPresenceEnvelope(value, requestId, kind) {
|
|
2151
2159
|
return typeof value.instanceId === "string" &&
|
package/dist/lib/commands.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { type TelegramBridgeStatusLineOptions } from "./status.ts";
|
|
|
13
13
|
import { type TelegramApiCallOptions } from "./telegram-api.ts";
|
|
14
14
|
import type { TelegramSessionReplacementIntent } from "./threads.ts";
|
|
15
15
|
import { type TelegramDeferredSourceEvidence, type TelegramLiveSourceCompletionReadiness, type TelegramQueueAdmissionReceiptLike } from "./updates.ts";
|
|
16
|
-
export type TelegramHeldCommandName = "status" | "abort" | "stop" | "next" | "continue";
|
|
16
|
+
export type TelegramHeldCommandName = "status" | "start" | "help" | "abort" | "stop" | "next" | "continue";
|
|
17
17
|
/** Commands-owned no-fold admission over the ordinary recipient queue; source reporting stays separate from removal. */
|
|
18
18
|
export interface TelegramHeldTurnAdmission {
|
|
19
19
|
assertCurrent(): void;
|
package/dist/lib/commands.js
CHANGED
|
@@ -1446,35 +1446,40 @@ export function createTelegramCommandHandler(deps) {
|
|
|
1446
1446
|
},
|
|
1447
1447
|
};
|
|
1448
1448
|
};
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
},
|
|
1475
|
-
}
|
|
1476
|
-
}
|
|
1449
|
+
// Held followers are already paired and bound. Start/help open the same restricted menu as status;
|
|
1450
|
+
// they cannot pair an operator, bootstrap another Thread or invoke unrestricted Bot API effects.
|
|
1451
|
+
const heldMenuPlan = {
|
|
1452
|
+
completion: "before-effect",
|
|
1453
|
+
effect: "showStatus",
|
|
1454
|
+
prepare(ctx, effects) {
|
|
1455
|
+
const scopedStatus = effects?.showStatus;
|
|
1456
|
+
const captured = {
|
|
1457
|
+
...deps,
|
|
1458
|
+
showStatus: scopedStatus
|
|
1459
|
+
? (message, context, options) => {
|
|
1460
|
+
const address = getTelegramCommandMessageTarget(message);
|
|
1461
|
+
return scopedStatus.call(effects, address.chatId, address.replyToMessageId, context, address.threadId, options);
|
|
1462
|
+
}
|
|
1463
|
+
: deps.showStatus,
|
|
1464
|
+
};
|
|
1465
|
+
return {
|
|
1466
|
+
captured,
|
|
1467
|
+
portsCurrent: () => !effects || effects.showStatus === scopedStatus,
|
|
1468
|
+
async run({ message, command, assertRecipient }) {
|
|
1469
|
+
const recipientOptions = { assertAuthority: assertRecipient };
|
|
1470
|
+
return handleTelegramCommandRuntime("status", message, ctx, {
|
|
1471
|
+
...captured,
|
|
1472
|
+
assertExecutionCurrent: assertRecipient,
|
|
1473
|
+
showStatus: (value, context) => captured.showStatus(value, context, recipientOptions),
|
|
1474
|
+
}, command.args);
|
|
1475
|
+
},
|
|
1476
|
+
};
|
|
1477
1477
|
},
|
|
1478
|
+
};
|
|
1479
|
+
const heldPlans = {
|
|
1480
|
+
status: heldMenuPlan,
|
|
1481
|
+
start: heldMenuPlan,
|
|
1482
|
+
help: heldMenuPlan,
|
|
1478
1483
|
abort: {
|
|
1479
1484
|
completion: "after-semantics",
|
|
1480
1485
|
effect: "sendTextReply",
|
package/dist/lib/extension.js
CHANGED
|
@@ -60,6 +60,8 @@ const telegramBusProtocolIdentity = Bus.createTelegramCurrentBusProtocolIdentity
|
|
|
60
60
|
Bus.TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION,
|
|
61
61
|
Bus.TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF,
|
|
62
62
|
Bus.TELEGRAM_BUS_CAPABILITY_QUEUE_STEER,
|
|
63
|
+
Bus.TELEGRAM_BUS_CAPABILITY_CONNECT_RECOVERY,
|
|
64
|
+
Bus.TELEGRAM_BUS_CAPABILITY_HELD_MENU_ALIASES,
|
|
63
65
|
Bus.TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT,
|
|
64
66
|
Bus.TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
|
|
65
67
|
Bus.TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE,
|
|
@@ -976,7 +978,7 @@ export default function (pi) {
|
|
|
976
978
|
isSelectedCommandAvailable(follower, name) {
|
|
977
979
|
return (Bus.hasTelegramBusSharedCapabilities(telegramBusProtocolIdentity, follower.protocol, [
|
|
978
980
|
...Bus.TELEGRAM_BUS_LIVE_REBIND_CAPABILITIES,
|
|
979
|
-
...Bus.
|
|
981
|
+
...Bus.getTelegramBusHeldCommandCapabilities(name),
|
|
980
982
|
]) &&
|
|
981
983
|
typeof selectedMenuDelivery === "function" &&
|
|
982
984
|
typeof inboundRouteRuntime.prepareHeldCommand === "function" &&
|
|
@@ -1225,6 +1227,7 @@ export default function (pi) {
|
|
|
1225
1227
|
threadStore,
|
|
1226
1228
|
runWorkspaceOperation: telegramWorkspaceOperationRuntime.run,
|
|
1227
1229
|
liveRebindCleanupSchedule: Routing.TELEGRAM_LIVE_REBIND_CLEANUP_SCHEDULE,
|
|
1230
|
+
liveRebindCommandCompletionTimeoutMs: 8_000,
|
|
1228
1231
|
updateStatus,
|
|
1229
1232
|
isContextActive: telegramSessionContextStore.isCurrent,
|
|
1230
1233
|
dispatchNextQueuedTelegramTurn,
|
package/dist/lib/locks.d.ts
CHANGED
|
@@ -259,7 +259,9 @@ export interface TelegramLockedPollingRuntime<TContext extends TelegramLockConte
|
|
|
259
259
|
captureTransportAuthority: (ctx: TContext) => (() => boolean) | undefined;
|
|
260
260
|
onPersistentConflict: (ctx: TContext, count: number) => Promise<void>;
|
|
261
261
|
onSessionStart: (_event: unknown, ctx: TContext) => Promise<void>;
|
|
262
|
-
registerFollowerWithOwner?: (ctx: TContext, owner: TelegramLockEntry
|
|
262
|
+
registerFollowerWithOwner?: (ctx: TContext, owner: TelegramLockEntry, options?: {
|
|
263
|
+
recoverConnection?: boolean;
|
|
264
|
+
}) => boolean | undefined | Promise<boolean | undefined>;
|
|
263
265
|
restoreFollowerWithOwner?: (ctx: TContext, owner: TelegramLockEntry) => boolean | undefined | Promise<boolean | undefined>;
|
|
264
266
|
stopFollowerRegistration?: () => void;
|
|
265
267
|
}
|
|
@@ -274,7 +276,9 @@ interface TelegramLockedPollingRuntimeDeps<TContext extends TelegramLockContext>
|
|
|
274
276
|
formatStartBlockedMessage?: (ctx: TContext) => string;
|
|
275
277
|
startPolling: (ctx: TContext, options?: TelegramLockedPollingStartOptions) => void | Promise<void>;
|
|
276
278
|
stopPolling: () => Promise<void>;
|
|
277
|
-
registerFollowerWithOwner?: (ctx: TContext, owner: TelegramLockEntry
|
|
279
|
+
registerFollowerWithOwner?: (ctx: TContext, owner: TelegramLockEntry, options?: {
|
|
280
|
+
recoverConnection?: boolean;
|
|
281
|
+
}) => boolean | undefined | Promise<boolean | undefined>;
|
|
278
282
|
restoreFollowerWithOwner?: (ctx: TContext, owner: TelegramLockEntry) => boolean | undefined | Promise<boolean | undefined>;
|
|
279
283
|
stopFollowerRegistration?: () => void;
|
|
280
284
|
/** Threaded Mode takeover evidence after failed follower registration; replaces the retired file heartbeat. */
|
package/dist/lib/locks.js
CHANGED
|
@@ -1625,7 +1625,7 @@ export function createTelegramLockedPollingRuntime(deps) {
|
|
|
1625
1625
|
if (deps.registerFollowerWithOwner) {
|
|
1626
1626
|
let failureMessage;
|
|
1627
1627
|
try {
|
|
1628
|
-
const registered = await deps.registerFollowerWithOwner(ctx, acquired.lock);
|
|
1628
|
+
const registered = await deps.registerFollowerWithOwner(ctx, acquired.lock, options.forceFreshLeaderThread ? { recoverConnection: true } : undefined);
|
|
1629
1629
|
if (!isCurrent())
|
|
1630
1630
|
return cancelled;
|
|
1631
1631
|
if (registered) {
|
|
@@ -1838,8 +1838,8 @@ export function createTelegramLockedPollingRuntime(deps) {
|
|
|
1838
1838
|
sessionAutoStartRun = run;
|
|
1839
1839
|
},
|
|
1840
1840
|
registerFollowerWithOwner: deps.registerFollowerWithOwner
|
|
1841
|
-
? async (ctx, owner) => {
|
|
1842
|
-
const registered = await deps.registerFollowerWithOwner?.(ctx, owner);
|
|
1841
|
+
? async (ctx, owner, options) => {
|
|
1842
|
+
const registered = await deps.registerFollowerWithOwner?.(ctx, owner, options);
|
|
1843
1843
|
if (registered)
|
|
1844
1844
|
deps.updateStatus(ctx);
|
|
1845
1845
|
return registered === true;
|
package/dist/lib/menu-queue.js
CHANGED
|
@@ -71,7 +71,7 @@ function buildTelegramQueueMenuReplyMarkup(items, emptyRefreshIndex = 0) {
|
|
|
71
71
|
return { inline_keyboard: [backRow, refreshRow] };
|
|
72
72
|
const rows = items.map((item) => {
|
|
73
73
|
const badge = getTelegramQueueMenuLaneBadge(item);
|
|
74
|
-
const prefix = item.reactionSuppressionEmoji ?
|
|
74
|
+
const prefix = item.reactionSuppressionEmoji ? "👎 " :
|
|
75
75
|
badge ? `${badge} ` : item.hasAttachments ? "📎 " : "";
|
|
76
76
|
const position = item.reactionSuppressionEmoji
|
|
77
77
|
? formatSkippedTelegramQueuePosition(item.queuePosition)
|
|
@@ -130,11 +130,10 @@ function getTelegramQueueMenuLaneBadge(item) {
|
|
|
130
130
|
return "🔥";
|
|
131
131
|
if (item.queueLane !== "priority")
|
|
132
132
|
return "";
|
|
133
|
-
|
|
134
|
-
return emoji === "❤" || emoji === "🕊" ? emoji : "👍";
|
|
133
|
+
return "👍";
|
|
135
134
|
}
|
|
136
135
|
function getTelegramQueueMenuItemText(item) {
|
|
137
|
-
const marker = item.reactionSuppressionEmoji
|
|
136
|
+
const marker = item.reactionSuppressionEmoji ? "👎" : getTelegramQueueMenuLaneBadge(item);
|
|
138
137
|
const badge = marker ? ` ${marker}` : "";
|
|
139
138
|
const position = item.reactionSuppressionEmoji
|
|
140
139
|
? `<s>${item.queuePosition}</s>.`
|
|
@@ -441,11 +440,10 @@ function applyQueuedTelegramPromptMenuFlags(chatId, replyToMessageId, ctx, deps,
|
|
|
441
440
|
const disposition = { kind: "reaction-transition" };
|
|
442
441
|
if (flags.lane !== undefined) {
|
|
443
442
|
disposition.promptLane = flags.lane;
|
|
444
|
-
disposition.priorityEmoji = flags.lane === "steer" ? "🔥" : flags.lane === "priority" ?
|
|
445
|
-
getTelegramQueueMenuLaneBadge({ queueLane: "priority", priorityEmoji: item.queueLane === "priority" ? item.priorityEmoji : undefined }) : null;
|
|
443
|
+
disposition.priorityEmoji = flags.lane === "steer" ? "🔥" : flags.lane === "priority" ? "👍" : null;
|
|
446
444
|
}
|
|
447
445
|
if (flags.skipped !== undefined)
|
|
448
|
-
disposition.suppressionEmoji = flags.skipped ?
|
|
446
|
+
disposition.suppressionEmoji = flags.skipped ? "👎" : null;
|
|
449
447
|
return deps.queueMutationRuntime.applyReactionByMessageId(replyToMessageId, disposition, ctx) ||
|
|
450
448
|
((flags.lane === undefined || flags.lane === item.queueLane) &&
|
|
451
449
|
(flags.skipped === undefined || flags.skipped === (item.reactionSuppressionEmoji !== undefined)));
|
package/dist/lib/polling.d.ts
CHANGED
|
@@ -221,7 +221,9 @@ export interface TelegramThreadAwarePollingPorts<TContext, TOwner> {
|
|
|
221
221
|
forceFreshLeaderThread?: boolean;
|
|
222
222
|
}) => Promise<void>;
|
|
223
223
|
stopPolling: () => Promise<void>;
|
|
224
|
-
registerFollowerWithOwner: (ctx: TContext, owner: TOwner
|
|
224
|
+
registerFollowerWithOwner: (ctx: TContext, owner: TOwner, options?: {
|
|
225
|
+
recoverConnection?: boolean;
|
|
226
|
+
}) => Promise<boolean | undefined>;
|
|
225
227
|
restoreFollowerWithOwner: (ctx: TContext, owner: TOwner) => Promise<boolean | undefined>;
|
|
226
228
|
stopFollowerRegistration: () => void;
|
|
227
229
|
}
|
|
@@ -233,7 +235,9 @@ export interface TelegramThreadAwarePollingControls<TContext, TOwner> {
|
|
|
233
235
|
stopBusLeaderPolling: () => Promise<void>;
|
|
234
236
|
startLeaderHealth: () => void;
|
|
235
237
|
stopLeaderHealth: () => void;
|
|
236
|
-
registerFollowerWithLeader: (ctx: TContext, owner: TOwner
|
|
238
|
+
registerFollowerWithLeader: (ctx: TContext, owner: TOwner, options?: {
|
|
239
|
+
recoverConnection?: boolean;
|
|
240
|
+
}) => Promise<boolean | undefined>;
|
|
237
241
|
restoreFollowerWithLeader?: (ctx: TContext, owner: TOwner) => Promise<boolean | undefined>;
|
|
238
242
|
hasRememberedWorkspaceBinding?: (ctx: TContext) => boolean;
|
|
239
243
|
stopFollowerRegistration: () => void;
|
package/dist/lib/polling.js
CHANGED
|
@@ -752,10 +752,10 @@ export function createTelegramThreadAwarePollingPorts(deps) {
|
|
|
752
752
|
}
|
|
753
753
|
return deps.topicTargetStore.getBotState().threadMode === "enabled";
|
|
754
754
|
};
|
|
755
|
-
const registerFollowerWithOwner = async (ctx, owner) => {
|
|
755
|
+
const registerFollowerWithOwner = async (ctx, owner, options) => {
|
|
756
756
|
if (!(await refreshFollowerState()))
|
|
757
757
|
return undefined;
|
|
758
|
-
return deps.registerFollowerWithLeader(ctx, owner);
|
|
758
|
+
return deps.registerFollowerWithLeader(ctx, owner, options);
|
|
759
759
|
};
|
|
760
760
|
const restoreFollowerWithOwner = async (ctx, owner) => {
|
|
761
761
|
if (!(await refreshFollowerState())) {
|