@llblab/pi-telegram 0.55.1 → 0.56.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +5 -4
- package/BACKLOG.md +6 -1
- package/CHANGELOG.md +15 -0
- package/README.md +28 -17
- package/dist/lib/bindings.d.ts +8 -4
- package/dist/lib/bindings.js +41 -1
- package/dist/lib/bus-follower.d.ts +1 -0
- package/dist/lib/bus-follower.js +10 -5
- package/dist/lib/bus-leader.d.ts +2 -0
- package/dist/lib/bus-leader.js +34 -20
- package/dist/lib/bus.d.ts +8 -0
- package/dist/lib/bus.js +21 -7
- package/dist/lib/commands.d.ts +1 -1
- package/dist/lib/commands.js +33 -28
- package/dist/lib/extension.js +18 -3
- package/dist/lib/lifecycle.d.ts +2 -1
- package/dist/lib/lifecycle.js +28 -20
- package/dist/lib/locks.d.ts +6 -2
- package/dist/lib/locks.js +3 -3
- package/dist/lib/menu-queue.d.ts +1 -0
- package/dist/lib/menu-queue.js +102 -99
- package/dist/lib/pi.d.ts +3 -2
- package/dist/lib/pi.js +3 -0
- package/dist/lib/polling.d.ts +6 -2
- package/dist/lib/polling.js +5 -3
- package/dist/lib/queue.d.ts +57 -5
- package/dist/lib/queue.js +313 -25
- package/dist/lib/routing.d.ts +4 -0
- package/dist/lib/routing.js +62 -1
- package/dist/lib/status.d.ts +1 -2
- package/dist/lib/status.js +6 -7
- 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 +12 -7
- package/dist/lib/updates.js +44 -9
- package/dist/package.json +1 -1
- package/docs/README.md +1 -1
- package/docs/architecture.md +24 -13
- package/docs/callback-namespaces.md +4 -0
- package/docs/multi-instance-bus.md +6 -4
- package/docs/ui-style.md +14 -10
- package/lib/bindings.ts +52 -3
- package/lib/bus-follower.ts +13 -5
- package/lib/bus-leader.ts +37 -23
- package/lib/bus.ts +23 -7
- package/lib/commands.ts +52 -47
- package/lib/extension.ts +18 -2
- package/lib/lifecycle.ts +34 -28
- package/lib/locks.ts +5 -2
- package/lib/menu-queue.ts +116 -162
- package/lib/pi.ts +8 -0
- package/lib/polling.ts +7 -2
- package/lib/queue.ts +328 -33
- package/lib/routing.ts +51 -1
- package/lib/status.ts +7 -7
- package/lib/sync.ts +11 -0
- package/lib/threads.ts +57 -5
- package/lib/updates.ts +44 -15
- package/package.json +1 -1
package/docs/architecture.md
CHANGED
|
@@ -61,6 +61,8 @@ The repository uses a **Flat Domain DAG**:
|
|
|
61
61
|
|
|
62
62
|
### Live-Rebind Module Map
|
|
63
63
|
|
|
64
|
+
For an already-issued leader menu/control command whose semantics finish before delivery, the original chooser may observe its exact warm source-removal ACK for one window of up to eight seconds before answering. Later taps only reobserve; they do not restart that wait. Naming/confirmation/extension producers whose source completion can depend on later publication keep their independent completion path. Updates notifies subscribers only after its carrier retains that ACK, not at the earlier journal-completion hook. Routing rechecks source, recipient, canonical binding and execution generation; observation cannot issue a second command or turn source absence into confirmation. Menu/reply delivery is not awaited. Timeout, abort or authority loss removes subscriptions and retains uncertainty without replay; old-Thread cleanup remains an independent fenced operation.
|
|
65
|
+
|
|
64
66
|
Release `0.53.0` adds the [live-rebind channel](./multi-instance-bus.md#live-rebind-channel) and bounded old-Thread cleanup. It advertises live save/apply/settle, command-set and selected-menu-delivery through bus protocol v3, retaining Workspace Restore for legacy continuity. A loaded 0.53.0 build prefers the live path; an instance still running an older build keeps Restore until it is reloaded. Windows runs the same channel and strict reads, exercised by the CI matrix; there is no platform-specific alternate protocol.
|
|
65
67
|
|
|
66
68
|
The channel reuses existing owners rather than adding a parallel command bus, queue or recovery store:
|
|
@@ -110,7 +112,7 @@ Only the live cleanup origin uses the approved historical-protection reduction.
|
|
|
110
112
|
- `outbound-attachments`: `telegram_attach`, queued outbound files, stat/limit checks, ordinary photo/document delivery, and narrow single-artifact Rich Message planning/sending for probe-confirmed photo/video/audio formats. It owns known-failure fallback eligibility and ambiguous-send no-replay classification through structural error contracts without importing Bot API helpers.
|
|
111
113
|
- `channel-posts`: profile/token-bound authority for agent-authored channel publication intents, one-shot send/edit/delete fencing, outcome-unknown retention, exact successful post identity, capacity refusal, and bounded local listing. It never reads Telegram history; the composition root supplies direct-leader Bot API effects only after its durable grants.
|
|
112
114
|
- `status` / `logs`: status bar/status-message rendering, queue-lane summaries, the structural redacted event ring, profile-aware JSONL scope/reset/append behavior, exact-owner destructive commits, fail-soft synchronous and queued diagnostics persistence, status snapshot scheduling, grouped diagnostics, and allowlisted compact connection-failure copy shared by commands and lifecycle. `status` remains a structural leaf; `logs` composes filesystem evidence with status projections and contains every persistence failure so diagnostics cannot terminate or poison the runtime queue.
|
|
113
|
-
- `bindings` / `lifecycle` / `prompts` / `prompt-templates` / `pi`: Pi-facing command/tool/hook registration and cohesive cross-domain binding assembly, including queue mutation/dispatch/watchdog composition over admission and transport ports; session-generation fencing and start/shutdown sequencing across Queue, grouped input, Delivery, polling, capability monitor, follower refresh, and assistant-output projection; Telegram prompt guidance; prompt-template discovery/expansion; and centralized direct Pi SDK imports. `lifecycle` also owns bounded same-process connect intent across local `/resume`, cancellation/supersession, and fresh-context startup scheduling; it does not own transport authority, durable bindings, queue custody, or source-target continuity.
|
|
115
|
+
- `bindings` / `lifecycle` / `prompts` / `prompt-templates` / `pi`: Pi-facing command/tool/hook registration and cohesive cross-domain binding assembly, including queue mutation/dispatch/watchdog composition over admission and transport ports; session-generation fencing and start/shutdown sequencing across Queue, grouped input, Delivery, polling, capability monitor, follower refresh, and assistant-output projection; Telegram prompt guidance; prompt-template discovery/expansion; and centralized direct Pi SDK imports. `lifecycle` also owns bounded same-process connect intent across local `/resume`, cancellation/supersession, and fresh-context startup scheduling; it does not own transport authority, durable bindings, queue custody, or source-target continuity. Each Queue start/shutdown invocation captures its session generation for inner post-await checks, including config/temp preparation and polling suspension. Reused context identity cannot authorize an obsolete reset/discard or successor service startup. Follower refresh uses that same invocation-local predicate through its awaited registration/context publication; no mutable shared generation or target-preservation flag crosses overlapping invocations.
|
|
114
116
|
- `command-templates`: shell-free command-template helpers, composition expansion, placeholder substitution, executable resolution, warnings, and retry/timeout semantics.
|
|
115
117
|
|
|
116
118
|
### Host Compatibility Boundary
|
|
@@ -441,7 +443,7 @@ Named Telegram profiles are orthogonal to Threaded Mode. The selected profile ch
|
|
|
441
443
|
|
|
442
444
|
Profile reality follows three explicit storage classes. `telegram.json` shared settings and extension registries are process-global platform configuration; `profiles.default` and `profiles.<name>` bot/session fields plus observable transport/routing authority are profile-scoped; queues, active turns, ownership caches, menu state, and runtime controllers are session-local memory. Config persistence serializes cross-process writers and applies each recursive mutation delta to the latest disk snapshot, so unrelated global/profile updates do not stale-replace one another. Each profile's journal transaction independently publishes its monotonic admission cursor together with admitted work; polling never writes runtime state through config persistence. Runtime profile switching follows stop-old/commit-new ordering: reload keeps the selected identity stable, old polling/lock/bus teardown finishes while all dynamic resolvers still point at the old profile, and only then may activation expose the new token, lock key, state path, target namespace, and IPC endpoint. Downloaded attachments are flat `kind-scope-messageId[-index][-name|.ext]` files under `tmp/pi-telegram/attachments` and are session artifacts rather than identity or routing authority. The scope is the bot username (else bot ID) for the bot's private chat, a group username or numeric ID, and for Guest Mode the remote peer's username or numeric ID, because guest message IDs belong to the peer chat. Scratch cleanup removes regular files older than 24 hours. It never age-deletes journals, ownership, state, logs, or other top-level runtime files and therefore cannot redirect live traffic or orphan immutable journal segments.
|
|
443
445
|
|
|
444
|
-
When Threaded Mode is active, the current polling owner is also the Telegram bus leader. The leader owns the local bus endpoint (Unix-domain socket on Unix-like platforms, named pipe on native Windows), polls `getUpdates`, performs direct Bot API calls, records follower heartbeats, prunes stale followers, and provisions Telegram UI thread targets through live runtime/bus state. Followers heartbeat every `1s`; the leader uses a `15s` stale grace and a `1s` prune loop so transient IPC stalls do not create false routing gaps while active forwarded updates/API calls still refresh liveness. Heartbeat pruning is silent liveness bookkeeping and does not send a Telegram-visible disconnected notice, because the common cause may be leader reload or IPC handoff rather than a dead follower. Heartbeat pruning alone preserves stored owner records and Workspace bindings. The same leader can retain bounded, non-routing observations of actually registered PID/generation/target tuples for later absence proof and retry of uncommitted preservation; the [pruning contract](./multi-instance-bus.md#follower-heartbeat-is-missed) owns cancellation, admission identity and lifetime limits. When Thread cleanup is enabled, only a subsequent OS check that confirms the exact registered PID absent may create fenced cleanup intent, serialized ahead of replacement registration. With cleanup disabled, that proof instead permits a profile-admitted, fenced non-destructive detachment: the store removes the uniquely matching owner record and records first inactivity while retaining the exact Workspace binding and its letter. Publication rechecks PID absence, runtime generation, profile, leader epoch, replacement registrations and the record snapshot; missing or ambiguous restoration identity blocks it. The Thread and its messages remain untouched, accepted-work protection remains independent, and no deleted-Thread observation is invented. Successful follower target reuse refreshes the binding and clears inactivity. Durable Workspace bindings remain restoration hints after detachment; heartbeat silence or unverifiable process evidence cannot stamp inactivity. If an authenticated live follower carries an exact target that is absent from current bindings, the leader recovers it only behind a synchronous visibility probe: success activates it, explicit stale evidence provisions a replacement, and ambiguous failure rejects registration. A carried slot is restored only when it is not already occupied. `tmp/pi-telegram/logs.jsonl` is a session-local redacted runtime evidence stream for race debugging; it resets on extension start and runtime scope changes, and must not become routing/provisioning authority. `tmp/pi-telegram/state.json` is an extension+bot observable/debug snapshot aligned with status diagnostics: `source: "snapshot"` and `writtenAtMs` mark it as observational, not authoritative. All instances on one Telegram profile read the same snapshot, but only the active transport lock owner persists it; followers become writers only after promotion. Status-only persistence reloads current disk bindings before serialization, preventing a stale follower/status view from erasing newer leader-owned targets. Fresh capability observations may skip redundant startup probes, but stale snapshots re-probe before suppressing bus/thread behavior. Top-level `bot` mirrors bot-wide capabilities such as thread mode, `runtime` describes process role/status, `liveRoster` mirrors followers/current targets/reservations, `diagnostics` mirrors recent status/debug signals including the latest thread-reconciler phase/counts, `threads` stores current routeable bindings, `workspaceBindings` stores profile-scoped normalized exact-`cwd` target/name/slot reuse hints, TTL-bounded reservations explain short-lived slot collision guards, and TTL-pruned `pendingProvisions` protects in-flight topic creation slots from cleanup/allocation races. Fresh provisioning writes pending state before the Bot API create call, adds the returned target to the pending record, persists a `starting` binding, then promotes it to `active` and clears pending state. If final binding persistence fails after Telegram returns a thread id, the targeted pending provision remains as cleanup/retry evidence. Once targeted pending provisions expire, they are retained for `thread-reconciler` close/delete cleanup and pending scratchpad removal after a successful cleanup apply; untargeted expired pending records can prune without cleanup because no Telegram thread id exists. Runtime events coalesce status-snapshot writes so transient bus/API/update failures remain inspectable even when the operator has not opened `/telegram-status`. The bridge must not keep a durable `telegram-targets.json` target history; stale/offline/failed thread observations are pruned instead of reused. Previous-process leader bindings that still probe alive become reservations/collision guards, not routeable active threads, so a reloaded leader can take the next free slot without duplicating the same visible tab name. The thread chat is always the private bot DM with the paired owner (`allowedUserId`). In Telegram private-chat Threaded Mode, the leader creates/reuses its own thread before polling — it is a real bound instance, not a dispatcher. Followers authenticate bus envelopes with the leader-minted capability secret stored in the active lock entry. Bot capability monitoring does not probe through the bus until the process either owns that direct lock or has completed authenticated follower registration. Leader lock entries also carry a stable `leaderEpoch` minted on acquisition and preserved across heartbeat refreshes; leader-owned cleanup/provisioning plans stamp that epoch, and Thread Reconciler apply skips destructive work if leadership has moved on before side effects run. Followers own their own Pi session state, queue, active turns, previews, menus, and lifecycle hooks, but route allowlisted, target-scoped Telegram API calls through the leader. When a follower promotes after heartbeat loss, status/state diagnostics expose only the transient `electing` lifecycle phase; stable `leader`/`follower` identity stays in the bus role so diagnostics do not duplicate role state. The TUI status bar and `/telegram-status` report `leader` or `follower` role so a registered follower is not shown as generically disconnected. Terminal status identity and the `[telegram|thread:name]` prompt label use the same target-aware current-instance resolver: registered local metadata wins over a stale shared binding for the matching target, while the binding remains a fallback for partial metadata.
|
|
446
|
+
When Threaded Mode is active, the current polling owner is also the Telegram bus leader. The leader owns the local bus endpoint (Unix-domain socket on Unix-like platforms, named pipe on native Windows), polls `getUpdates`, performs direct Bot API calls, records follower heartbeats, prunes stale followers, and provisions Telegram UI thread targets through live runtime/bus state. Followers heartbeat every `1s`; the leader uses a `15s` stale grace and a `1s` prune loop so transient IPC stalls do not create false routing gaps while active forwarded updates/API calls still refresh liveness. Heartbeat pruning is silent liveness bookkeeping and does not send a Telegram-visible disconnected notice, because the common cause may be leader reload or IPC handoff rather than a dead follower. Heartbeat pruning alone preserves stored owner records and Workspace bindings. The same leader can retain bounded, non-routing observations of actually registered PID/generation/target tuples for later absence proof and retry of uncommitted preservation; the [pruning contract](./multi-instance-bus.md#follower-heartbeat-is-missed) owns cancellation, admission identity and lifetime limits. When Thread cleanup is enabled, only a subsequent OS check that confirms the exact registered PID absent may create fenced cleanup intent, serialized ahead of replacement registration. With cleanup disabled, that proof instead permits a profile-admitted, fenced non-destructive detachment: the store removes the uniquely matching owner record and records first inactivity while retaining the exact Workspace binding and its letter. Publication rechecks PID absence, runtime generation, profile, leader epoch, replacement registrations and the record snapshot; missing or ambiguous restoration identity blocks it. The Thread and its messages remain untouched, accepted-work protection remains independent, and no deleted-Thread observation is invented. Successful follower target reuse refreshes the binding and clears inactivity. Durable Workspace bindings remain restoration hints after detachment; heartbeat silence or unverifiable process evidence cannot stamp inactivity. If an authenticated live follower carries an exact target that is absent from current bindings, the leader recovers it only behind a synchronous visibility probe: success activates it, explicit stale evidence provisions a replacement, and ambiguous failure rejects registration. A carried slot is restored only when it is not already occupied. `tmp/pi-telegram/logs.jsonl` is a session-local redacted runtime evidence stream for race debugging; it resets on extension start and runtime scope changes, and must not become routing/provisioning authority. `tmp/pi-telegram/state.json` is an extension+bot observable/debug snapshot aligned with status diagnostics: `source: "snapshot"` and `writtenAtMs` mark it as observational, not authoritative. All instances on one Telegram profile read the same snapshot, but only the active transport lock owner persists it; followers become writers only after promotion. Status-only persistence reloads current disk bindings before serialization, preventing a stale follower/status view from erasing newer leader-owned targets. Fresh capability observations may skip redundant startup probes, but stale snapshots re-probe before suppressing bus/thread behavior. Top-level `bot` mirrors bot-wide capabilities such as thread mode, `runtime` describes process role/status, `liveRoster` mirrors followers/current targets/reservations, `diagnostics` mirrors recent status/debug signals including the latest thread-reconciler phase/counts, `threads` stores current routeable bindings, `workspaceBindings` stores profile-scoped normalized exact-`cwd` target/name/slot reuse hints, TTL-bounded reservations explain short-lived slot collision guards, and TTL-pruned `pendingProvisions` protects in-flight topic creation slots from cleanup/allocation races. Fresh provisioning writes pending state before the Bot API create call, adds the returned target to the pending record, persists a `starting` binding, then promotes it to `active` and clears pending state. If final binding persistence fails after Telegram returns a thread id, the targeted pending provision remains as cleanup/retry evidence. Once targeted pending provisions expire, they are retained for `thread-reconciler` close/delete cleanup and pending scratchpad removal after a successful cleanup apply; untargeted expired pending records can prune without cleanup because no Telegram thread id exists. Runtime events coalesce status-snapshot writes so transient bus/API/update failures remain inspectable even when the operator has not opened `/telegram-status`. The bridge must not keep a durable `telegram-targets.json` target history; stale/offline/failed thread observations are pruned instead of reused. Previous-process leader bindings that still probe alive become reservations/collision guards, not routeable active threads, so a reloaded leader can take the next free slot without duplicating the same visible tab name. The thread chat is always the private bot DM with the paired owner (`allowedUserId`). In Telegram private-chat Threaded Mode, the leader creates/reuses its own thread before polling — it is a real bound instance, not a dispatcher. Followers authenticate bus envelopes with the leader-minted capability secret stored in the active lock entry. Bot capability monitoring does not probe through the bus until the process either owns that direct lock or has completed authenticated follower registration. A retained `enabled` capability is not proof of a running local bus: after a startup probe timeout leaves classic polling active, a fresh positive monitor result restarts Threaded transport under the current lock owner even when the persisted mode is unchanged. Unknown results do not certify this recovery, healthy buses are not restarted, and followers do not acquire leader authority through the monitor. Leader lock entries also carry a stable `leaderEpoch` minted on acquisition and preserved across heartbeat refreshes; leader-owned cleanup/provisioning plans stamp that epoch, and Thread Reconciler apply skips destructive work if leadership has moved on before side effects run. Followers own their own Pi session state, queue, active turns, previews, menus, and lifecycle hooks, but route allowlisted, target-scoped Telegram API calls through the leader. When a follower promotes after heartbeat loss, status/state diagnostics expose only the transient `electing` lifecycle phase; stable `leader`/`follower` identity stays in the bus role so diagnostics do not duplicate role state. The TUI status bar and `/telegram-status` report `leader` or `follower` role so a registered follower is not shown as generically disconnected. Terminal status identity and the `[telegram|thread:name]` prompt label use the same target-aware current-instance resolver: registered local metadata wins over a stale shared binding for the matching target, while the binding remains a fallback for partial metadata.
|
|
445
447
|
|
|
446
448
|
Fresh follower binding is manual and process-first: the operator starts another Pi process, then runs `/telegram-connect`; only then may that process allocate a profile-scoped normalized exact-`cwd` Workspace identity and cause the leader to create a Thread. A later process reopening that remembered Workspace automatically sends capability-gated restore-only admission under a live leader. The leader may reclaim, visibility-probe, or stale-replace the remembered target, but an absent binding returns quietly without creating a Thread. `/telegram-connect [profile] as=Name` supplies a unique capitalized Latin-word identity only to fresh Workspace provisioning; an existing Workspace keeps its persisted name. Telegram `/name Name` stores the owning Thread's durable `manualThreadName` and immediately applies it over the active automatic display projection; bare `/name` opens five-minute exact-target input whose next valid text is consumed before agent dispatch. Name input, cancel, and reset are consume-once; stale scope, target, message ID, expiry, and duplicate callbacks cannot mutate. Reset clears only the override and restores the current automatic projection. Leader command routing reuses its already-held profile admission for the rename body rather than recursively entering the non-reentrant Workspace gate; standalone leader renames acquire their own admission. Followers send an authenticated exact-generation `workspace-thread-rename-v1` request, and the leader owns any Bot API mutation plus durable binding persistence. Concurrent processes from one directory receive deterministic Workspace suffixes, while leader/follower roles remain transient projections over that durable identity. Telegram does not expose `/thread`, auto-spawn arbitrary unbound threads, or launch hidden follower subprocesses. In Threaded Mode, `/telegram-connect` does not offer manual takeover while a live leader exists; takeover is reserved for stale-leader election/recovery. Leadership remains an ephemeral transport role that another live follower can take over after stale heartbeat detection. A confirmed runtime transition from Threaded to Singleton stops threaded transport and suspends the process-local leader target before classic polling can accept new work; durable Workspace slot, generated name, manual display name, and binding evidence remain retained. Re-enabling Threaded Mode restores or replaces that logical binding before publishing one new live target. Already-admitted turns keep their captured destination and are never silently retargeted or duplicated.
|
|
447
449
|
|
|
@@ -575,37 +577,46 @@ Polling and inbound-worker diagnostics remain separate so an executing, deferred
|
|
|
575
577
|
|
|
576
578
|
Long-text split recovery remains conservative: only human text at or above the near-limit threshold opens its debounce window. Forward annotation has two semantic layers: the forward owns its source text/caption/media, while an optional separate owner-authored annotation normally precedes it. A bounded one-second pairing window joins that annotation and adjacent forward in either transport order, including a media-only forward without source caption text; the matching opposite-kind message flushes immediately. Same-kind rapid messages, commands, bots, ordinary non-forward captions, media groups, different senders/targets, reversed ids, and distant message ids do not enter this pairing path. Prompt construction always places the owner annotation first, followed by `[forward|from:...]` with the forward's own source text/caption, then source-attributed forwarded attachments, regardless of arrival order.
|
|
577
579
|
|
|
580
|
+
### Explicit Connection Recovery
|
|
581
|
+
|
|
582
|
+
An explicit `/telegram-connect` is permission for one fresh connection attempt, not permission to take over a live leader or clear accepted input. Under the existing profile-wide Workspace admission and mutation gate, the leader resets only matching unfinished creation records without a known Thread target before claiming Workspace identity. The reset verifies the exact retained record and refuses sidecar target-recovery evidence; durable publication and current ownership are required before another create. Known targets and other owners' pending records remain intact. A late response is isolated by a fresh provisioning ID, independent of wall-clock resolution.
|
|
583
|
+
|
|
584
|
+
If Telegram committed the abandoned create without returning its Thread ID, a spare tab may remain. This is the operator-approved availability trade-off: no unknown tab is deleted, and no journal, receipt, live binding or queued prompt is erased. A new API outage can still fail the new attempt; the command does not promise connectivity without a working Telegram transport.
|
|
585
|
+
|
|
586
|
+
Followers carry this explicit intent only on authenticated `follower.register`, gated by `explicit-connect-recovery-v1` at caller and leader. Automatic heartbeat recovery and `follower.restoreWorkspace` do not reset creation evidence. A matching targeted pending provision contributes its original Workspace key/slot when the exact session is claimed again, so retry can complete known publication rather than allocate a conflicting identity. Cross-session targets never become recovery grants.
|
|
587
|
+
|
|
578
588
|
### Queue And Dispatch Safety
|
|
579
589
|
|
|
580
|
-
The bridge keeps its own Telegram queue. The Pi status bar
|
|
590
|
+
The bridge keeps its own Telegram queue. The Pi status bar projects executable waiting work across Normal, Priority, Steer and Control into one warning-yellow `+N` suffix, omitted when zero. It uses Queue's executability filter, so Skip and frozen attempts never contribute; current agent work from Telegram, terminal, or autonomous sources never contributes either. The dispatched Telegram head is subtracted while retained pending `agent_start` consumption.
|
|
581
591
|
|
|
582
592
|
Queue items have two explicit dimensions:
|
|
583
593
|
|
|
584
594
|
- `kind`: `prompt` or `control`.
|
|
585
|
-
- `queueLane`: `control`, `priority`, or `default`.
|
|
595
|
+
- `queueLane`: `control`, `steer`, `priority`, or `default`.
|
|
586
596
|
|
|
587
|
-
Dispatch rank:
|
|
597
|
+
Dispatch rank (distinct from the ascending menu selector order):
|
|
588
598
|
|
|
589
599
|
1. `control` lane.
|
|
590
|
-
2. `
|
|
591
|
-
3. `
|
|
600
|
+
2. `steer` prompt lane, using its guarded active-boundary or idle-packet attempt.
|
|
601
|
+
3. `priority` prompt lane.
|
|
602
|
+
4. `default` prompt lane.
|
|
592
603
|
|
|
593
604
|
#### Priority, Reactions, Keep, and Skip
|
|
594
605
|
|
|
595
|
-
Waiting
|
|
606
|
+
Bridge-owned Steer uses the existing public Pi SDK. Busy-run operator smoke coverage is established; broader live and native-Windows acceptance remains follow-up work in the [backlog](../BACKLOG.md). Waiting ordinary prompts expose two independent dimensions:
|
|
596
607
|
|
|
597
|
-
- `Priority` / `
|
|
608
|
+
- `Normal` / `Priority` / `Steer` selects the FIFO lane and scheduling opportunity in increasing intervention order. Control remains producer-owned.
|
|
598
609
|
- `Keep` / `Skip` selects whether the prompt executes when dispatch reaches it.
|
|
599
610
|
|
|
600
611
|
A prompt is one queue object with exactly one active lane membership and one current position. It has no duplicate, shadow entry, or reserved return slot in the other lane. Each prompt admitted directly to a lane joins that lane's tail. A `Normal → Priority` transition removes it from Normal and appends that same object to the Priority tail; a later `Priority → Normal` transition removes it from Priority and appends it to the current Normal tail rather than restoring any historical position. The immutable `queueOrder` records original admission identity only and is never a return address; `laneOrder` records the current destination-lane position. Keep/Skip changes and emoji changes within the same reaction category preserve both lane and lane position exactly.
|
|
601
612
|
|
|
602
|
-
Telegram reactions are shortcut controls over those dimensions.
|
|
613
|
+
Telegram reactions are shortcut controls over those dimensions. Fire selects Steer and dominates other positives; thumbs-up/heart/dove select Priority and recognized negatives control Skip. Lightning is now a Control marker only, not a reaction shortcut; the emoji sets live in the [queue reaction registry](./ui-style.md#queue-reaction-shortcuts). The runtime compares effective intent from complete old and new reaction sets and mutates only dimensions that changed. Weaker positives under fire and decorative changes cannot reset a menu lane choice; so adding or removing a negative reaction cannot silently change Priority, and changing a positive reaction cannot silently change Skip. The registry order selects the retained display emoji when several recognized emoji from one category coexist; it does not let one category override the other.
|
|
603
614
|
|
|
604
|
-
Priority and Skip may coexist when a prompt carries both
|
|
615
|
+
Steer or Priority and Skip may coexist when a prompt carries both positive and negative reactions. The prompt remains at its current lane position while waiting, and its durable journal receipts remain intact so Keep stays reversible and exact live handoff remains possible. Skip wins when dispatch reaches the prompt: the dispatcher first settles those receipts durably, then drops it without a model turn and continues; settlement failure retains the skipped head instead of allowing replay ambiguity. The prompt stays visible with only its negative emoji, is excluded immediately from the executable queue count shared by the Pi status bar and Telegram main menu, and keeps a struck-through physical ordinal. Returning it to Keep restores its contribution to the count without moving it. Graceful session shutdown discards all remaining queue receipts before clearing session-local memory, and startup discards receipts only after proving their former process dead, so a new or unrelated session never inherits queued work. Queue item detail exposes horizontal Normal/Priority/Steer and Keep/Skip selectors instead of irreversible Delete. Legacy published Priority callbacks retain explicit lane intent. Dispatched/frozen items expose inspection only; an oversized idle Steer item stays editable with a Priority/Normal escape and never splits its source group.
|
|
605
616
|
|
|
606
|
-
Menu and reaction controls share the same canonical queue state. A menu Keep can clear internal Skip without changing Priority or queue position, but Telegram's Bot API cannot remove a reaction created by the user; the visible user reaction can therefore remain until that user removes it. Once
|
|
617
|
+
Menu and reaction controls share the same canonical queue state. A menu Keep can clear internal Skip without changing Priority or queue position, but Telegram's Bot API cannot remove a reaction created by the user; the visible user reaction can therefore remain until that user removes it. Once a prompt is dispatched, frozen for an attempt, consumed or dropped, later reactions cannot change or reissue it. Steer freezes exact original objects before whole-receipt attempt disposition, which is not SDK/model-use proof. Active `turn_end` inserts one packet without resetting Telegram reply/preview ownership; packet content preserves original text/image blocks with blank-line separators only between prompts, without numbered Steer labels. Idle origin detection retains its Telegram prefix. Idle `sendUserMessage` pins the first original and its tail until `agent_start` projects one reply/voice/attachment owner. Partial/refused/unknown outcomes remain protected without replay, and Steer queue transfer requires independently negotiated `queue-steer-v1` plus base handoff support at every participant before custody changes. Frozen/pending work never transfers as unspent input; missing support or malformed attempted/projection fields fail closed.
|
|
607
618
|
|
|
608
|
-
Guest Mode is the deliberate exception to deferred Keep/Skip semantics. A guest prompt has no source message reaction surface and may share zero-valued chat/message placeholders with other guest prompts, so queue-menu callbacks address it by immutable `queueOrder` and expose an immediate `Skip`. The leader settles its durable admission, removes it while other agent work may continue, stops placeholder animation, and replaces the inline globe placeholder with non-printing content. Bot API inline messages have no delete method, so this visual clear is the supported deletion equivalent.
|
|
619
|
+
Guest Mode is the deliberate exception to deferred Keep/Skip semantics. A guest prompt has no source message reaction surface and may share zero-valued chat/message placeholders with other guest prompts, so queue-menu callbacks address it by immutable `queueOrder` and expose an immediate `Skip`. The leader settles its durable admission, removes it while other agent work may continue, stops placeholder animation, and replaces the inline globe placeholder with non-printing content. Bot API inline messages have no delete method, so this visual clear is the supported deletion equivalent. A successfully skipped waiting guest prompt never reaches a model turn. Once dispatched, its retained head is inspection-only until `agent_start` consumes that exact reply owner; stale Guest Skip callbacks refuse without receipt disposal or placeholder dismissal. Later waiting guests remain independently skippable.
|
|
609
620
|
|
|
610
621
|
A standalone `/continue` is a source-addressed control-lane prompt, not a Normal/Priority prompt. An authorized negative reaction to its original chat/message immediately settles the exact waiting continuation's receipts before removing it, even while unrelated work is active. Failed or unknown settlement retains it for the existing reaction retry path; duplicate reactions cannot settle twice. Positive reactions and removal of the negative reaction neither change its control lane nor reconstruct cancelled work. The dispatched head awaiting `agent_start` and already-started work are never cancelled; appends and reorders preserve that Pi-owned head. Synthetic model-switch continuations have no source message IDs and are not reaction-addressable by their fallback reply anchor. Reaction authorization and chat/message namespace isolation remain unchanged.
|
|
611
622
|
|
|
@@ -25,6 +25,10 @@ myext:page:2
|
|
|
25
25
|
- Put only opaque ids or small enum values in payloads; do not store secrets, full prompts, or large state.
|
|
26
26
|
- Treat callbacks as untrusted input. Validate namespace, action, and payload before executing side effects.
|
|
27
27
|
|
|
28
|
+
## Queue Controls
|
|
29
|
+
|
|
30
|
+
The Steer surface owns `queue:lane-set:<chat>:<message>:steer|priority|normal` and independent `queue:skip-set:<chat>:<message>:keep|skip`. Previously published `queue:prio-set` and `queue:prio` controls remain explicit Priority/Normal compatibility routes. Payloads only identify a current view; they cannot grant source/session authority or edit a protected pending/frozen item. Control lanes remain producer-owned.
|
|
31
|
+
|
|
28
32
|
## pi-telegram fallback
|
|
29
33
|
|
|
30
34
|
If `pi-telegram` receives callback data that is not owned by its built-in prefixes and no built-in handler consumes it, it forwards the click to Pi as:
|
|
@@ -85,6 +85,8 @@ tmp/pi-telegram/state.json / profiles.<profile>.transport -> bus leader identity
|
|
|
85
85
|
|
|
86
86
|
Followers do not poll. They register with the leader and receive routed inbound updates from it. Followers still own their local queue, active-turn state, previews, final delivery planning, model switches, and Pi lifecycle. The leader owns only Telegram transport and update fanout. Same-session context refresh preserves bus membership; a changed Pi session ID suspends the old receiver/heartbeat and requires acknowledged successor registration before readiness, without an explicit Thread disconnect. A new follower process also attempts a restore-only registration at session startup when the selected profile's local state contains a Workspace binding for the exact `cwd` and session ID. If an active retained manual-follower record blocks that binding, the leader may transfer its claim only for one exact eligible predecessor: no registered peer owns its runtime or target, and an OS probe proves the actual runtime PID absent. A live, inaccessible, missing or malformed PID proof and ambiguous candidates remain protective; neither elapsed time nor an empty registry alone proves death. This is only claim continuity, not receipt settlement, deletion or a fabricated immediate session handoff. The same binding, non-default instance slot, acknowledged title and journal history survive without provisioning a new Thread. The recent-event log distinguishes missing session bindings, disabled Threaded Mode, leader-side restore refusal, and a restore that returned without connecting; none of these refusals silently provisions a new Thread. The Telegram bus belongs to the local set of cooperating visible Pi instances rather than to the first terminal session forever: if the visible terminal leader exits, a live registered follower can take over leadership.
|
|
87
87
|
|
|
88
|
+
Explicit connection recovery is capability-gated by `explicit-connect-recovery-v1`. Only a manual `follower.register` may carry `recoverConnection: true`; malformed values and restore-only requests reject it. The leader resets matching untargeted creation metadata under ordinary profile admission before allocation, accepting a possible orphan tab without touching accepted input. Automatic registration never carries this grant. See [Explicit Connection Recovery](./architecture.md#explicit-connection-recovery).
|
|
89
|
+
|
|
88
90
|
### Session-Owned Journal Storage
|
|
89
91
|
|
|
90
92
|
Runtime storage lives under `<agent-dir>/tmp/pi-telegram`. The pre-0.52.0 `tmp/telegram` tree is neither written, migrated nor deleted. A live fresh owner there blocks competing polling as read-only evidence; its endpoint and secret grant no current authority.
|
|
@@ -273,7 +275,7 @@ Followers first try to re-register after leader reload or unknown-heartbeat resp
|
|
|
273
275
|
|
|
274
276
|
### Protocol identity and compatibility
|
|
275
277
|
|
|
276
|
-
The session-native local wire contract has protocol version `3`, independent from the npm package version. Version 3 moved journal source serialization from the `telegram.json` transaction to the journal-owned `runtime/journals.transaction`; because v2 peers would not exclude v3 journal writers, the base version check rejects them instead of mixing lock semantics. Follower registration and the leader acknowledgement carry `{ protocolVersion, runtimeBuild, capabilities }`. Capability names are canonical, unique, and sorted. A leader rejects missing or mismatched protocol identity before provisioning a target or publishing the follower into live routing; a strict follower likewise rejects an acknowledgement without compatible leader identity. Different package builds remain compatible when their protocol versions agree. `durable-follower-admission-v1` gates source forwarding, while `queue-handoff-v1` independently gates live semantic queue transfer; every participant in a routed handoff must advertise it. `workspace-follower-auto-connect-v1` gates restore-only startup admission. Session identity has been part of the base protocol since v2 rather than an optional capability. Every cwd-scoped registration carries the exact bounded session ID; missing identity is rejected before provisioning or publication. Reconnect, replacement, and promotion preserve it. Older-protocol peers are rejected by the base version check, avoiding mixed semantic branches. `thread-display-mode-v1` gates exact-generation follower display-setting requests and Letters/Directories require compatible connected followers; returning to Names allows legacy peers. Registration checks the current display-mode requirement before provisioning and again before live publication. The leader serializes config persistence and title application. `workspace-thread-rename-v1` independently gates follower rename requests whose exact registration generation is checked before the leader mutates Telegram and persists the Workspace binding. `session-replacement-intent-v1` gates Telegram `/new` from a follower Thread: followers never write `state.json`, so `follower.publishSessionReplacement` asks the leader to CAS-publish the durable intent only when the live registration generation, leader epoch, Profile, exact `cwd`, source session, Thread target, and the leader's own Workspace binding slot/name all match, with a bounded unexpired TTL. The intent records the source runtime instance; only a successor registration from that instance or its authenticated same-process handoff (`previousInstanceId`) may re-key the binding, and `follower.settleSessionReplacement` claims it once only after the leader's store shows that successor session bound to the same target. A missing capability, stale generation, or mismatch fails the command closed.
|
|
278
|
+
The session-native local wire contract has protocol version `3`, independent from the npm package version. Version 3 moved journal source serialization from the `telegram.json` transaction to the journal-owned `runtime/journals.transaction`; because v2 peers would not exclude v3 journal writers, the base version check rejects them instead of mixing lock semantics. Follower registration and the leader acknowledgement carry `{ protocolVersion, runtimeBuild, capabilities }`. Capability names are canonical, unique, and sorted. A leader rejects missing or mismatched protocol identity before provisioning a target or publishing the follower into live routing; a strict follower likewise rejects an acknowledgement without compatible leader identity. Different package builds remain compatible when their protocol versions agree. `durable-follower-admission-v1` gates source forwarding, while `queue-handoff-v1` independently gates live semantic queue transfer; every participant in a routed handoff must advertise it. Local experimental 0.56.0 adds `queue-steer-v1`: donor, leader and recipient must additionally support the unspent Steer lane before offer/staging. Protocol version and package version alone never imply this support. `workspace-follower-auto-connect-v1` gates restore-only startup admission. Session identity has been part of the base protocol since v2 rather than an optional capability. Every cwd-scoped registration carries the exact bounded session ID; missing identity is rejected before provisioning or publication. Reconnect, replacement, and promotion preserve it. Older-protocol peers are rejected by the base version check, avoiding mixed semantic branches. `thread-display-mode-v1` gates exact-generation follower display-setting requests and Letters/Directories require compatible connected followers; returning to Names allows legacy peers. Registration checks the current display-mode requirement before provisioning and again before live publication. The leader serializes config persistence and title application. `workspace-thread-rename-v1` independently gates follower rename requests whose exact registration generation is checked before the leader mutates Telegram and persists the Workspace binding. `session-replacement-intent-v1` gates Telegram `/new` from a follower Thread: followers never write `state.json`, so `follower.publishSessionReplacement` asks the leader to CAS-publish the durable intent only when the live registration generation, leader epoch, Profile, exact `cwd`, source session, Thread target, and the leader's own Workspace binding slot/name all match, with a bounded unexpired TTL. The intent records the source runtime instance; only a successor registration from that instance or its authenticated same-process handoff (`previousInstanceId`) may re-key the binding, and `follower.settleSessionReplacement` claims it once only after the leader's store shows that successor session bound to the same target. A missing capability, stale generation, or mismatch fails the command closed.
|
|
277
279
|
|
|
278
280
|
The current root advertises `workspace-restore-v1`; all participating peers and potential leader successors must be upgraded before operator-controlled use. The retired `leader.replaceFollowerTarget` envelope is no longer parsed or produced, even with current authentication and registration generation. Its `leader.workspaceRestore` envelope carries only an operation ID, exact recipient registration generation and `apply`/`inspect` mode; the receiver derives binding and destination from the retained intent, not caller-supplied target data. Authentication and an explicitly enabled receiver are mandatory. The controller checks both peers' capability and protocol compatibility, captures live registration/session/slot/endpoint authority, sends once without transport retry, then validates the request, operation, recipient, observed target, slot and boolean readiness against the current registration. A lost response leaves the durable issuance consumed; a separate read-only inspection can prove readiness without applying again. Neither the controller nor receiver settles source input or writes follower-owned canonical state. A successor registration/process may inspect only its actual same-session canonical binding and local target; readiness records the new observer separately without changing the original issuance. An old local target cannot be repaired through inspection, and successor apply is rejected. Interrupted startup, terminal source/cleanup settlement and live behavior were accepted in the operator's 0.52.0 live smoke.
|
|
279
281
|
|
|
@@ -287,7 +289,7 @@ The delivery id excludes the replaceable runtime instance and registration gener
|
|
|
287
289
|
|
|
288
290
|
Transport leadership does not own already-queued semantics. Each queued journal receipt binds the acquiring runtime instance, OS process birth, session generation, and acquisition. A replacement leader or follower process sees another live process's receipt as foreign and cannot replay or settle it; the original process may complete its local Pi queue after transport moves. Startup preserves foreign and legacy unowned receipts. Before a replacement admission worker starts, tri-state pid/process-birth proof: an absent PID or mismatched stable Linux/macOS birth identity permits recovery, while a live matching owner stays `alive` and Windows or inaccessible birth metadata stays `unverifiable`; both non-dead outcomes preserve the receipt may transactionally recover a dead owner's complete receipt to pending, after which replacement replay creates fresh queue authority. Registration publishes the replacement's exact pid/process-birth identity first; a concurrent recovery that observes it returns `owner-alive`, while live or unverifiable owners remain untouched.
|
|
289
291
|
|
|
290
|
-
Live-process handoff combines journal CAS with authenticated bounded bus payloads. A donor-generated one-time token is hashed together with the exact receipt, donor acquisition, and recipient runtime/process/session identity. Offering retains donor ownership but freezes donor completion/discard and dead-owner recovery. Prompt payloads serialize queue data; control payloads serialize only stable `status`/`model` identity and rebuild closures at the recipient. Leader and follower receivers require exact live donor/recipient registration generations, reject malformed/oversized payloads, stage one complete receipt idempotently, accept its journal authority, and acknowledge only that exact receipt plus newly minted owner. Receipts name their source journal binding, while the donor derives the recipient follower-journal binding from the authenticated stable profile. The recipient accepts only that matching active lifecycle. Production advertises `queue-handoff-v1` with this exact role/path composition; legacy-unbound or unavailable bindings fail closed.
|
|
292
|
+
Live-process handoff combines journal CAS with authenticated bounded bus payloads. A donor-generated one-time token is hashed together with the exact receipt, donor acquisition, and recipient runtime/process/session identity. Offering retains donor ownership but freezes donor completion/discard and dead-owner recovery. Prompt payloads serialize queue data; control payloads serialize only stable `status`/`model` identity and rebuild closures at the recipient. Leader and follower receivers require exact live donor/recipient registration generations, reject malformed/oversized payloads, stage one complete receipt idempotently, accept its journal authority, and acknowledge only that exact receipt plus newly minted owner. Receipts name their source journal binding, while the donor derives the recipient follower-journal binding from the authenticated stable profile. The recipient accepts only that matching active lifecycle. Production advertises `queue-handoff-v1` with this exact role/path composition; legacy-unbound or unavailable bindings fail closed. The Steer codec preserves original lane/order, content/media, Keep/Skip and exact source receipts; syntax decoding alone grants no custody. Legacy peers refuse before offer, and every routing/receiver boundary rechecks support. Frozen/attempted packets, Pi-owned pending heads, synthetic packet projections and Steer guests cannot become transferable waiting input. Capability loss before staging cancels only an unaccepted offer; existing exact owner/ACK rules govern lost post-acceptance replies.
|
|
291
293
|
|
|
292
294
|
As part of staging, the exact recipient presents the token and atomically receives a fresh acquisition carrying the handoff digest; donor settlement is then stale, and the donor trusts only an ACK carrying that accepted owner. Registration carries recipient process-birth and session generation so journal authority matches the live runtime exactly. The coordinator contract shares one ordering across leader→follower and follower→follower paths: offer, route bounded payload, require the exact staged-and-accepted receipt-and-owner ACK, remove donor work, then publish recipient dispatch readiness. Negative or mismatched acknowledgement cancels the still-unaccepted offer and retains donor work. A lost acknowledgement after recipient acceptance cannot revoke the accepted owner; cancellation fails closed and donor memory remains frozen for exact accepted-owner reconciliation. Repeated exact acceptance is idempotent; a different token cannot claim the accepted receipt. Queue authority has no elapsed-time lease: only exact handoff, owner action, or transaction-rechecked PID/process-birth death proof may move it.
|
|
293
295
|
|
|
@@ -305,7 +307,7 @@ After the recipient confirms the applied rebind and before the source is release
|
|
|
305
307
|
|
|
306
308
|
Follower live apply, held-work observation and release-readiness checks use the exact read-only live-rebind Workspace snapshot, including with consolidated storage and no follower publication authority. The captured store/reader identity stays fenced across awaits; a missing or replaced reader refuses without falling back to the publication-backed legacy Restore observer. Profile admission, exact canonical intent/binding and source/custody checks still apply. Observation changes only the follower's local target registration, never leader-owned Workspace evidence; journal reads and receipt settlement remain separate owners.
|
|
307
309
|
|
|
308
|
-
Every participating peer must negotiate the required capabilities and retain current authentication, registration generation, Pi context/session, journal binding and exact recipient authority. The wire validates syntax; Commands' captured registry decides availability before hold/append. Supported built-ins and registered prompt templates reuse one plan set across compatible leader selections and held followers; extension commands/groups/legacy registrations do not gain follower execution. Continue keeps its control lane, while expanded templates are ordinary default-lane prompts even when their text starts with a slash.
|
|
310
|
+
Every participating peer must negotiate the required capabilities and retain current authentication, registration generation, Pi context/session, journal binding and exact recipient authority. The wire validates syntax; Commands' captured registry decides availability before hold/append. Supported built-ins and registered prompt templates reuse one plan set across compatible leader selections and held followers. Both peers additionally negotiate `held-menu-aliases-v1` for follower `/start` and `/help`: these already-authenticated selections open the restricted main menu using the status plan, never pair a user, bootstrap a Thread or synchronize BotFather commands through ordinary follower transport; extension commands/groups/legacy registrations do not gain follower execution. Continue keeps its control lane, while expanded templates are ordinary default-lane prompts even when their text starts with a slash.
|
|
309
311
|
|
|
310
312
|
Semantic reports, queued receipts, completion observation and delivery remain separate. Native exact ACK and donor CAS still own source disposition; lost or mismatched evidence retains uncertainty without handler replay, another queue or fallback admission. This section owns the unified command-channel policy; [architecture](./architecture.md#live-rebind-module-map) owns the module map. Existing Workspace Restore receivers/readers and queue-handoff retain their custody contracts; the new loaded root prefers live rebinding rather than using legacy Restore as an uncertain-input fallback. Source-enabled bounded cleanup is not proof of an actual deletion or client acceptance. Supported leader/follower prompts, commands and templates use `saved → binding applied → cleanup decision → input issuance → native admission/execution`. The chooser holds its existing profile admission through the unchanged whole first delay and one fresh cleanup attempt, without nested leader admission or another queue. A negotiated follower reports work through `observe-held` before release; binding readiness is not execution permission. `inputRelease: held/issued/accepted` is independent of cleanup: an issued input is never replayed after a lost admission reply, and a finished cleanup with held input remains active binding/source protection. Confirmed, failed or unknown terminal deletion permits the one native input attempt only on the confirmed binding. Issued and unknown old-target evidence remains canonically protected against reuse and generic cleanup, including response/publication interruption; unknown evidence survives input acceptance and is never capacity-evicted or reissued. If earlier work prevents issuance, input proceeds on the confirmed new target and the existing paced scheduler owns deferred cleanup; no old-target clearance is invented. Detached command producers accept their one input grant independently from exact source ACKs and reply publication; menu/reply authority tolerates only that issued-to-accepted progress, never changed binding, cleanup, recipient or generation. Expanded templates retain their captured content/default lane and Continue its control lane. A session-replacing `/new` producer with earlier protected work records positively unissued cleanup plus durable `oldTargetProtected`; it cannot erase that old-target protection or transfer a delete grant into the successor. Actual replacement still uses the existing post-removal Pi gateway and confirmation, not Restore readiness. Follower command/control completion retains the exact prepared-source/native ACK before donor CAS; queued admission is not completion and cannot dispose the donor. Unsupported producers do not gain arbitrary follower execution. Older peers keep the preceding supported channel. In deferred cleanup, old-tab cleanup waits for the recipient session to be idle, which the moved input itself may keep busy; its paced retries keep a whole first delay so a detached command reply still sees the released, cleanup-free row, but after that a leader's `agent_settled` or drained output barrier wakes a waiting retry at once. Publication/command-work, Activity handlers, per-target companion queues and target/chat API counters capture lifetime-fenced notifications when work starts and emit only after their pending observation clears. The session adapter defers the hint without awaiting cleanup and rechecks its captured context generation, transport/profile and leader epoch/operator authority. Streaming technical events can drain the publication queue without ending the agent run. A delivery hint while SDK idle is false, unavailable or throwing is retained in the existing chain but does not clear its timer or acquire Workspace admission. `agent_settled` remains an unconditional recheck hint; after settlement, actual remaining delivery drains can wake the retry. A running attempt retains one coalesced hint without a concurrent check, then re-enters the same idle filter after releasing its admission. A fresh timer/settlement attempt consumes the previously deferred hint; signals arriving during that await are not lost. Timer retries, including busy-session samples, are unchanged. Terminal outcomes discard hints, including unknown issued deletion. The initial one-second delay, retry/window fallback, follower pacing and negotiated IPC contracts remain unchanged. Each chain also retains its original generation/profile-store/epoch/operator lifetime rather than rebasing to a reused context. The wake only re-samples every gate through fresh admission; it never grants cleanup, clears unconfirmed work, occupies the publication tail or holds Pi settlement for deletion. Windows runs the same strict paths, exercised by the CI matrix; actual Pi/client acceptance and release are not inferred from supplied IPC fixtures.
|
|
311
313
|
|
|
@@ -559,7 +561,7 @@ All files containing routing, chat ids, thread ids, or process details use priva
|
|
|
559
561
|
- Each unfinished preservation retains one admission operation ID across sequential attempts, so a lost acquisition acknowledgement reuses and releases the exact lease instead of leaking another. A rejected pre-rename write leaves the owner intact; a committed write with lost acknowledgement is recognized without rewriting first inactivity. A liveness interruption is retryable, not successful settlement. Every attempt reacquires profile admission and the shared mutation gate, rechecks authority at rename, and preserves accepted work. Cancellation or scope loss never guesses away an unresolved admission lease; existing exact lease/dead-owner recovery remains authoritative.
|
|
560
562
|
- Followers treat rejected/missing heartbeat acknowledgements as registration loss: retain the last known target locally, clear registered truth, show `reconnecting` instead of a healthy `follower` status, try to re-register with the current leader, wait a short leader-reload grace window, and retry. They promote only after the exact leader lease becomes stale or inactive; a live owner with an unreachable endpoint leaves the follower disconnected/retrying rather than creating a competing poller.
|
|
561
563
|
- Persisted current manual-follower bindings survive abrupt process absence as restoration hints when Thread cleanup is disabled. When enabled, graceful Pi quit requests exact-generation teardown before lifecycle suspension; if that envelope is missed, stale pruning may delete only after the leader's OS confirms the exact registered PID has exited.
|
|
562
|
-
- Fresh registration sends one compact connected notice in the assigned thread. Reusing a retained exact-session target resolves its acknowledged Workspace display title, even if the provisional claim uses another instance slot; a generated dictionary name is only fallback when no acknowledged title exists.
|
|
564
|
+
- Fresh registration sends one compact connected notice in the assigned thread. Reusing a retained exact-session target resolves its acknowledged Workspace display title, even if the provisional claim uses another instance slot; a generated dictionary name is only fallback when no acknowledged title exists. Cross-process/session restoration, including an exact immediate handoff, uses that connected message as its visibility probe before publishing registration. Typing is not proof that a retained tab accepts messages. A proven stale target enters existing replacement provisioning; an ambiguous send refuses registration without inventing deletion or retry authority.
|
|
563
565
|
- Registration requires a present generation, and explicit disconnect requires that same exact live generation. Leader-side registration and disconnect mutations serialize per durable follower profile across old and replacement runtime instance IDs, so a replacement registration cannot overtake awaited destructive cleanup and an old disconnect cannot remove its successor's routing authority.
|
|
564
566
|
- Successful forwarded updates and follower-originated API calls refresh liveness, so active followers are not pruned only because the interval heartbeat tick lagged. Each follower lifecycle owns at most one in-flight heartbeat for its exact registration generation; stop/replacement makes late settlement inert, and recovery or diagnostic failure cannot escape as an unhandled interval Promise.
|
|
565
567
|
- Destructive follower thread teardown belongs to confirmed `/telegram-disconnect`, graceful Pi quit, or confirmed reconciliation actions, not generic heartbeat pruning. Manual disconnect retains its destructive confirmation and clears restart ownership; quit deletes the tab without prompting when Thread cleanup is enabled (default) but preserves the owner slot independently so a same-directory restart can reclaim leadership. Confirmed leader/follower teardown first persists an exact target/runtime-generation cleanup intent. The active leader attempts deletion under its current epoch; interruption preserves the intent so that leader or a successor can replay it under current authority, and confirmed deletion removes the binding plus intent in the same persisted state transition. If the graceful request is missed, stale heartbeat plus OS-confirmed absence of the exact registered PID may authorize the same cleanup while enabled; this action serializes ahead of replacement registration. Disabled cleanup, silence, heartbeat expiry alone, IPC/auth failure, and live or unknown process liveness remain non-destructive. Incomplete cleanup preserves durable intent for retry. A promoted leader uses its current owned leader epoch even when the inherited record still carries a historical `manual-follower` owner label.
|
package/docs/ui-style.md
CHANGED
|
@@ -72,32 +72,33 @@ Use emoji as stable semantic markers, not decoration. Emoji carry transportable
|
|
|
72
72
|
| Emoji | Meaning | Canonical surfaces | Notes |
|
|
73
73
|
| --- | --- | --- | --- |
|
|
74
74
|
| `🟢` | Current/active/enabled `On` | Current option in vertical lists, active state rows, active `On` toggle | One marker per selected list value; inactive list values stay unmarked. |
|
|
75
|
-
| `🟡` | Active `Off
|
|
75
|
+
| `🟡` | Active Steer, `Off`, or filter state | Steer queue selector, active `Off` toggle, Scoped active tab | Yellow distinguishes early insertion or off/filter state, not error. |
|
|
76
76
|
| `🔴` | Active destructive/deferred disposition | Active queue `Skip` selector | Red distinguishes a prompt that will be discarded at dispatch from reversible neutral or elevated state. |
|
|
77
|
-
| `🟣` |
|
|
77
|
+
| `🟣` | Active Priority lane or neutral navigation tab | Priority queue selector, All/default scope tab, active page picker | Queue lane meaning is distinct from neutral navigation. |
|
|
78
|
+
| `🔵` | Active Normal queue lane | Normal queue selector | Blue distinguishes ordinary queued tasks from Steer and Priority. |
|
|
78
79
|
| `⚫️` | Inactive placeholder | Inactive toggle values and inactive tabs | Keeps row width stable. |
|
|
79
80
|
| `⬆️` | Navigate upward | `⬆️ Main menu`, `⬆️ Back` | Always first row in submenus. |
|
|
80
81
|
|
|
81
82
|
### Queue Reaction Shortcuts
|
|
82
83
|
|
|
83
|
-
Queue reactions are shortcut controls for waiting turns. Preserve their semantics across Telegram reactions, queue-menu rows, status previews, and tests.
|
|
84
|
+
Queue reactions are shortcut controls for waiting turns. Preserve their semantics across Telegram reactions, queue-menu rows, status previews, and tests. Effective positive intent controls Steer/Priority/Normal; negative emoji control Keep/Skip. Fire selects Steer, dominates other positive reactions, and removing it falls back to remaining Priority aliases or Normal. Only changed effective intent overrides a menu choice; decorative changes and weaker positives under fire preserve lane/order. These categories are independent, may coexist, and mutate only their own dimension. Crossing lanes appends the prompt at the destination FIFO tail; changing Keep/Skip or changing emoji within one category preserves lane position. Skip wins only when dispatch reaches the prompt.
|
|
84
85
|
|
|
85
86
|
Skipped queue ordinals strike only the numeric position. Detail HTML closes `<s>` before the period; list-button labels place a hair-space boundary between the combining-struck number and the plain period so Telegram font overhang cannot visually strike punctuation.
|
|
86
87
|
|
|
87
88
|
Queue item detail renders two independent selector rows:
|
|
88
89
|
|
|
89
|
-
-
|
|
90
|
+
- `Normal` / `Priority` / `Steer` selects the lane in one horizontal row, increasing intervention from left to right. Active values are `🔵 Normal`, `🟣 Priority`, or `🟡 Steer`; inactive values use `⚫️`. Control is producer-owned, not a selectable lane.
|
|
90
91
|
- `🟢 Keep` / `⚫️ Skip` or `⚫️ Keep` / `🔴 Skip` selects dispatch disposition.
|
|
91
92
|
|
|
92
|
-
The menu may clear internal Skip but cannot remove a reaction created by the user through Telegram's Bot API.
|
|
93
|
+
The menu may clear internal Skip but cannot remove a reaction created by the user through Telegram's Bot API. Frozen attempts and the Pi-owned pending head expose inspection/Back only, with no success or model-use claim. An over-capacity idle Steer item remains editable and offers Priority/Normal rather than splitting its source group. Published legacy Priority/Normal callbacks remain explicit lane selections.
|
|
93
94
|
|
|
94
95
|
| Emoji | Meaning | Canonical surfaces | Notes |
|
|
95
96
|
| --- | --- | --- | --- |
|
|
96
97
|
| `👍` | Promote to priority | Queue reaction shortcut | Normalized from variants like `👍️`. |
|
|
97
|
-
| `⚡` |
|
|
98
|
+
| `⚡` | Producer-owned Control marker | Control queue rows | Not a positive-reaction alias or command classifier; old stored Priority badges project `👍` without changing lane authority. |
|
|
98
99
|
| `❤` / `❤️` | Promote to priority | Queue reaction shortcut | Normalize display consistently where code normalizes reactions. |
|
|
99
100
|
| `🕊` / `🕊️` | Promote to priority | Queue reaction shortcut | Soft/peaceful promotion gesture. |
|
|
100
|
-
| `🔥` |
|
|
101
|
+
| `🔥` | Select Steer | Queue reaction shortcut and Steer badge | Dominates `👍`, heart and dove; not immediate issuance or model-use proof. |
|
|
101
102
|
| `👎` | Defer removal of waiting turn | Queue reaction shortcut and queue emoji marker | Reversible until the marked turn reaches dispatch; not negative feedback to the agent. |
|
|
102
103
|
| `👻` | Defer removal of waiting turn | Queue reaction shortcut and queue emoji marker | Disappear/remove metaphor. |
|
|
103
104
|
| `💔` | Defer removal of waiting turn | Queue reaction shortcut and queue emoji marker | Reversible cancel metaphor. |
|
|
@@ -181,15 +182,16 @@ Rules:
|
|
|
181
182
|
- Use Capitalized labels.
|
|
182
183
|
- Always show an indicator on every tab to avoid horizontal label shift.
|
|
183
184
|
- Use active tab color to convey semantics:
|
|
184
|
-
- `🟣` for
|
|
185
|
-
- `🟡` for
|
|
185
|
+
- `🟣` for a default navigation scope (All models) or the current Priority lane choice.
|
|
186
|
+
- `🟡` for the current Steer lane choice or a filtered scope (Scoped models).
|
|
187
|
+
- `🔵` for the current Normal queue lane choice, independently of Keep/Skip.
|
|
186
188
|
- `🟣` for neutral navigation controls (page picker).
|
|
187
189
|
- Mark inactive tabs with `⚫️`.
|
|
188
190
|
|
|
189
191
|
Examples:
|
|
190
192
|
|
|
191
193
|
- `🟡 Scoped` / `⚫️ All`
|
|
192
|
-
- `⚫️ Priority` /
|
|
194
|
+
- `⚫️ Priority` / `🔵 Normal`
|
|
193
195
|
|
|
194
196
|
## Option Lists
|
|
195
197
|
|
|
@@ -250,6 +252,8 @@ A message (or All-tab command) held in an unbound or temporary tab gets one rout
|
|
|
250
252
|
|
|
251
253
|
Pi TUI connection notices use plain text: a short known cause and one recovery action. Unknown failures use a generic connection-failed notice with `/telegram-status --debug`; never interpolate raw exceptions, credentials, stack traces or Pi lifecycle guidance. Technical evidence belongs in the redacted runtime recorder. Failed disconnect must retain the instruction to keep Pi open; do not also rethrow the same error as a second Pi banner. These TUI notices are distinct from Telegram bot message cards below.
|
|
252
254
|
|
|
255
|
+
The compact TUI status bar shows one `+N` counter in yellow `warning` for total waiting work across Normal, Priority, Steer and Control, hidden when zero. Custom themes control the exact hue. It excludes the pending dispatched head, Skip and frozen attempts; active work never adds to the counter. Queue state and dispatch precedence are unchanged.
|
|
256
|
+
|
|
253
257
|
The compact TUI status bar preserves the acknowledged Thread display title and its casing, including manual `/name` overrides. Names equal to `Telegram`, `Leader` or `Follower` are ordinary titles, not fallback markers. Connection states, including disconnected and error, do not replace a supplied title; only an absent or empty title uses the generic `telegram` label.
|
|
254
258
|
|
|
255
259
|
## Message Cards
|
package/lib/bindings.ts
CHANGED
|
@@ -209,6 +209,10 @@ export interface TelegramQueueBindingRuntime<TContext> {
|
|
|
209
209
|
requestNextDispatchAnnouncement: () => void;
|
|
210
210
|
cancelNextDispatchAnnouncement: () => void;
|
|
211
211
|
watchdog: Queue.TelegramQueueDispatchWatchdogRuntime<TContext>;
|
|
212
|
+
onTurnEnd: (
|
|
213
|
+
event: Pick<Pi.TurnEndEvent, "turnIndex" | "outcome">,
|
|
214
|
+
ctx: TContext,
|
|
215
|
+
) => Pi.TurnEndEventResult | undefined;
|
|
212
216
|
}
|
|
213
217
|
|
|
214
218
|
export function createTelegramQueueBindingRuntime<TContext>(deps: {
|
|
@@ -217,8 +221,8 @@ export function createTelegramQueueBindingRuntime<TContext>(deps: {
|
|
|
217
221
|
lifecycle: Pick<
|
|
218
222
|
Runtime.TelegramBridgeRuntime["lifecycle"],
|
|
219
223
|
"isCompactionInProgress" | "hasDispatchPending"
|
|
220
|
-
|
|
221
|
-
activeTurn: Pick<Queue.TelegramActiveTurnStore, "has"
|
|
224
|
+
> & Partial<Pick<Runtime.TelegramBridgeRuntime["lifecycle"], "getActiveToolExecutions">>;
|
|
225
|
+
activeTurn: Pick<Queue.TelegramActiveTurnStore, "has"> & Partial<Pick<Queue.TelegramActiveTurnStore, "get">>;
|
|
222
226
|
admission: {
|
|
223
227
|
getSettlement: () =>
|
|
224
228
|
| {
|
|
@@ -228,7 +232,7 @@ export function createTelegramQueueBindingRuntime<TContext>(deps: {
|
|
|
228
232
|
) => boolean;
|
|
229
233
|
isItemReady: (item: Queue.TelegramQueueItem<TContext>) => boolean;
|
|
230
234
|
onPromptHandedOff?: (
|
|
231
|
-
item: Queue.PendingTelegramTurn,
|
|
235
|
+
item: Pick<Queue.PendingTelegramTurn, "admissionReceipts">,
|
|
232
236
|
ctx: TContext,
|
|
233
237
|
) => boolean;
|
|
234
238
|
onControlSettled: (
|
|
@@ -249,6 +253,8 @@ export function createTelegramQueueBindingRuntime<TContext>(deps: {
|
|
|
249
253
|
promptDispatch: Runtime.TelegramPromptDispatchRuntime<TContext>;
|
|
250
254
|
isIdle: (ctx: TContext) => boolean;
|
|
251
255
|
hasPendingMessages: (ctx: TContext) => boolean;
|
|
256
|
+
isContextCurrent?: (ctx: TContext) => boolean;
|
|
257
|
+
isContextAborted?: (ctx: TContext) => boolean;
|
|
252
258
|
updateStatus: (ctx: TContext, error?: string) => void;
|
|
253
259
|
sendTextReply: Queue.TelegramQueueDispatchRuntimeDeps<TContext>["sendTextReply"];
|
|
254
260
|
sendUserMessage: Queue.TelegramQueueDispatchRuntimeDeps<TContext>["sendUserMessage"];
|
|
@@ -269,6 +275,10 @@ export function createTelegramQueueBindingRuntime<TContext>(deps: {
|
|
|
269
275
|
if (!settlement) return false;
|
|
270
276
|
return settlement.onItemsDiscarded(durableItems, ctx) === true;
|
|
271
277
|
};
|
|
278
|
+
const settlePromptPacket = (items: readonly Queue.PendingTelegramTurn[], ctx: TContext): boolean => {
|
|
279
|
+
const receipts = items.flatMap(item => item.admissionReceipts ?? []);
|
|
280
|
+
return receipts.length === 0 || deps.admission.getSettlement()?.onPromptHandedOff?.({ admissionReceipts: receipts }, ctx) === true;
|
|
281
|
+
};
|
|
272
282
|
const mutation = Queue.createTelegramQueueMutationController({
|
|
273
283
|
...deps.store,
|
|
274
284
|
hasPendingDispatch: deps.lifecycle.hasDispatchPending,
|
|
@@ -309,6 +319,13 @@ export function createTelegramQueueBindingRuntime<TContext>(deps: {
|
|
|
309
319
|
if (!settlement?.onPromptHandedOff) return false;
|
|
310
320
|
return settlement.onPromptHandedOff(item, ctx) === true;
|
|
311
321
|
},
|
|
322
|
+
steerDispatch: {
|
|
323
|
+
commitPacket: settlePromptPacket,
|
|
324
|
+
isContextCurrent: (ctx: TContext) => deps.isContextCurrent?.(ctx) === true,
|
|
325
|
+
canSubmit: (ctx: TContext) => deps.isContextAborted?.(ctx) === false &&
|
|
326
|
+
deps.isIdle(ctx) && !deps.hasPendingMessages(ctx) && !deps.lifecycle.isCompactionInProgress() &&
|
|
327
|
+
!deps.activeTurn.has() && (deps.lifecycle.getActiveToolExecutions?.() ?? 0) === 0,
|
|
328
|
+
},
|
|
312
329
|
onControlSettled(item, ctx) {
|
|
313
330
|
deps.admission.getSettlement()?.onControlSettled(item, ctx);
|
|
314
331
|
},
|
|
@@ -323,8 +340,32 @@ export function createTelegramQueueBindingRuntime<TContext>(deps: {
|
|
|
323
340
|
...deps.promptDispatch,
|
|
324
341
|
sendUserMessage: deps.sendUserMessage,
|
|
325
342
|
});
|
|
343
|
+
const steer = Queue.createTelegramSteerBoundaryRuntime({
|
|
344
|
+
...deps.store,
|
|
345
|
+
getGeneration: deps.deferredDispatch.getGeneration,
|
|
346
|
+
isContextCurrent: (ctx: TContext) => deps.isContextCurrent?.(ctx) === true,
|
|
347
|
+
isBlocked: (ctx: TContext) =>
|
|
348
|
+
deps.isContextAborted?.(ctx) !== false || !deps.deferredDispatch.isBound() ||
|
|
349
|
+
deps.isIdle(ctx) || deps.hasPendingMessages(ctx) ||
|
|
350
|
+
deps.lifecycle.isCompactionInProgress() || deps.lifecycle.hasDispatchPending() ||
|
|
351
|
+
(deps.lifecycle.getActiveToolExecutions?.() ?? 0) > 0 ||
|
|
352
|
+
(deps.activeTurn.has() && !deps.activeTurn.get?.()),
|
|
353
|
+
isItemReady(item: Queue.TelegramQueueItem<TContext>) {
|
|
354
|
+
return deps.transportStamp.isActive(item.transportStamp) &&
|
|
355
|
+
!deps.admission.hasPendingQueueMutationForItem(item) &&
|
|
356
|
+
(deps.admission.getSettlement()?.isItemReady(item) ?? (item.admissionReceipts?.length ?? 0) === 0);
|
|
357
|
+
},
|
|
358
|
+
isItemCurrent: (item) => deps.transportStamp.isActive(item.transportStamp) &&
|
|
359
|
+
!deps.admission.hasPendingQueueMutationForItem(item),
|
|
360
|
+
getActiveTurn: () => deps.activeTurn.get?.(),
|
|
361
|
+
commitPacket: settlePromptPacket,
|
|
362
|
+
discardItem: (item, ctx) => settleDiscardedItems([item], ctx),
|
|
363
|
+
updateStatus: deps.updateStatus,
|
|
364
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
365
|
+
});
|
|
326
366
|
return {
|
|
327
367
|
mutation,
|
|
368
|
+
onTurnEnd: steer.onTurnEnd,
|
|
328
369
|
dispatchNext: dispatch.dispatchNext,
|
|
329
370
|
requestNextDispatchAnnouncement: dispatch.requestNextDispatchAnnouncement,
|
|
330
371
|
cancelNextDispatchAnnouncement: dispatch.cancelNextDispatchAnnouncement,
|
|
@@ -1156,6 +1197,7 @@ interface TelegramLifecycleBindingDeps {
|
|
|
1156
1197
|
>["sendTextReply"] &
|
|
1157
1198
|
NonNullable<OutboundHandlers.TelegramVoiceReplySenderDeps["sendTextReply"]>;
|
|
1158
1199
|
dispatchNextQueuedTelegramTurn: (ctx: Pi.ExtensionContext) => void;
|
|
1200
|
+
onSteerTurnEnd?: TelegramQueueBindingRuntime<Pi.ExtensionContext>["onTurnEnd"];
|
|
1159
1201
|
onPromptHandedOff?: (
|
|
1160
1202
|
turn: Queue.PendingTelegramTurn,
|
|
1161
1203
|
ctx: Pi.ExtensionContext,
|
|
@@ -1236,6 +1278,7 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
1236
1278
|
sendMarkdownReply,
|
|
1237
1279
|
sendTextReply,
|
|
1238
1280
|
dispatchNextQueuedTelegramTurn,
|
|
1281
|
+
onSteerTurnEnd,
|
|
1239
1282
|
onPromptHandedOff,
|
|
1240
1283
|
answerGuestQuery,
|
|
1241
1284
|
deleteMessage,
|
|
@@ -1566,6 +1609,12 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
1566
1609
|
isSessionActive: isSessionContextActive,
|
|
1567
1610
|
...sessionLifecycleRuntime,
|
|
1568
1611
|
...agentLifecycleHooks,
|
|
1612
|
+
...(onSteerTurnEnd ? {
|
|
1613
|
+
onTurnEnd(event: Pi.TurnEndEvent, ctx: Pi.ExtensionContext) {
|
|
1614
|
+
const result = onSteerTurnEnd(event, ctx);
|
|
1615
|
+
return result ? { ...result, entries: [...event.entries, ...(result.entries ?? [])] } : undefined;
|
|
1616
|
+
},
|
|
1617
|
+
} : {}),
|
|
1569
1618
|
onInput(event) {
|
|
1570
1619
|
activityRuntime.recordInputSource(event.source ?? "unknown");
|
|
1571
1620
|
},
|
package/lib/bus-follower.ts
CHANGED
|
@@ -35,13 +35,13 @@ import {
|
|
|
35
35
|
getTelegramBusProtocolCompatibility,
|
|
36
36
|
getTelegramBusSocketPath,
|
|
37
37
|
hasTelegramBusCapability,
|
|
38
|
+
hasTelegramBusSteerQueueHandoffCapability,
|
|
38
39
|
isTelegramBusEnvelopeAuthorized,
|
|
39
40
|
parseTelegramBusEnvelope,
|
|
40
41
|
resolveTelegramBusSocketPath,
|
|
41
42
|
sendTelegramBusLocalEnvelope,
|
|
42
43
|
TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT,
|
|
43
44
|
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY,
|
|
44
|
-
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_COMMAND_SET,
|
|
45
45
|
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SAVE,
|
|
46
46
|
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE,
|
|
47
47
|
TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND_EXECUTION,
|
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE,
|
|
52
52
|
TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
|
|
53
53
|
TELEGRAM_BUS_HELD_COMMAND_CAPABILITIES,
|
|
54
|
+
getTelegramBusHeldCommandCapabilities,
|
|
54
55
|
TELEGRAM_BUS_LIVE_REBIND_CAPABILITIES,
|
|
55
56
|
TelegramBusLocalAuthorityError,
|
|
56
57
|
type TelegramBusAgentMessage,
|
|
@@ -177,6 +178,7 @@ export interface TelegramBusFollowerRegistrationRuntime<TContext> {
|
|
|
177
178
|
target?: TelegramTarget;
|
|
178
179
|
previousInstanceId?: string;
|
|
179
180
|
restoreWorkspace?: boolean;
|
|
181
|
+
recoverConnection?: boolean;
|
|
180
182
|
},
|
|
181
183
|
) => Promise<boolean>;
|
|
182
184
|
setContext: (ctx: TContext) => void | Promise<void>;
|
|
@@ -2929,7 +2931,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
2929
2931
|
deps.setActiveAuthSecret?.(undefined);
|
|
2930
2932
|
await deps.stopReceiving?.();
|
|
2931
2933
|
};
|
|
2932
|
-
const pendingHandoff = options
|
|
2934
|
+
const pendingHandoff = options && !options.recoverConnection
|
|
2933
2935
|
? undefined
|
|
2934
2936
|
: getTelegramFollowerSessionHandoff();
|
|
2935
2937
|
const pendingHandoffOptions = isTelegramFollowerSessionHandoffFresh(
|
|
@@ -2945,8 +2947,9 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
2945
2947
|
target?: TelegramTarget;
|
|
2946
2948
|
previousInstanceId?: string;
|
|
2947
2949
|
restoreWorkspace?: boolean;
|
|
2950
|
+
recoverConnection?: boolean;
|
|
2948
2951
|
}
|
|
2949
|
-
| undefined = options
|
|
2952
|
+
| undefined = options ? { ...pendingHandoffOptions, ...options } : pendingHandoffOptions;
|
|
2950
2953
|
const leaderSocketPath =
|
|
2951
2954
|
leader.busSocketPath ??
|
|
2952
2955
|
deps.getLeaderSocketPath?.() ??
|
|
@@ -2967,6 +2970,8 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
2967
2970
|
: "follower.register",
|
|
2968
2971
|
requestId: registrationGeneration,
|
|
2969
2972
|
auth: activeAuthSecret,
|
|
2973
|
+
...(registrationOptions?.recoverConnection && !registrationOptions.restoreWorkspace
|
|
2974
|
+
? { recoverConnection: true as const } : {}),
|
|
2970
2975
|
registration: {
|
|
2971
2976
|
instanceId: deps.instanceId,
|
|
2972
2977
|
...(registrationOptions?.previousInstanceId
|
|
@@ -4545,8 +4550,7 @@ export function createTelegramBusForwardedUpdateReceiverRuntime<TContext>(
|
|
|
4545
4550
|
? [
|
|
4546
4551
|
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY,
|
|
4547
4552
|
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE,
|
|
4548
|
-
|
|
4549
|
-
TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
|
|
4553
|
+
...getTelegramBusHeldCommandCapabilities(commandInput.name),
|
|
4550
4554
|
]
|
|
4551
4555
|
: []),
|
|
4552
4556
|
];
|
|
@@ -4752,6 +4756,10 @@ export function createTelegramBusForwardedUpdateReceiverRuntime<TContext>(
|
|
|
4752
4756
|
result: receipt,
|
|
4753
4757
|
};
|
|
4754
4758
|
}
|
|
4759
|
+
if (envelope.payload.queueLane === "steer" &&
|
|
4760
|
+
![deps.getLocalProtocol?.(), deps.getLeaderProtocol?.()].every(hasTelegramBusSteerQueueHandoffCapability)) {
|
|
4761
|
+
throw new Error("Steer queue handoff capability was not negotiated.");
|
|
4762
|
+
}
|
|
4755
4763
|
if (!deps.handleQueueHandoff) {
|
|
4756
4764
|
throw new Error(
|
|
4757
4765
|
"Telegram bus receiver cannot accept queue handoff payloads.",
|