@llblab/pi-telegram 0.52.2 → 0.53.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 +9 -7
- package/BACKLOG.md +1 -18
- package/CHANGELOG.md +15 -0
- package/README.md +7 -11
- package/api/commands.ts +3 -0
- package/dist/api/commands.d.ts +1 -1
- package/dist/lib/activity-verbosity.d.ts +1 -5
- package/dist/lib/activity-verbosity.js +15 -42
- package/dist/lib/activity.d.ts +9 -0
- package/dist/lib/activity.js +39 -10
- package/dist/lib/agent-messages.js +7 -3
- package/dist/lib/bindings.d.ts +43 -5
- package/dist/lib/bindings.js +163 -20
- package/dist/lib/bus-api.d.ts +12 -0
- package/dist/lib/bus-api.js +182 -79
- package/dist/lib/bus-follower.d.ts +111 -31
- package/dist/lib/bus-follower.js +1324 -359
- package/dist/lib/bus-leader.d.ts +41 -91
- package/dist/lib/bus-leader.js +969 -637
- package/dist/lib/bus-transport.js +16 -9
- package/dist/lib/bus.d.ts +215 -18
- package/dist/lib/bus.js +1048 -327
- package/dist/lib/channel-posts.d.ts +5 -0
- package/dist/lib/channel-posts.js +434 -261
- package/dist/lib/command-templates.d.ts +26 -5
- package/dist/lib/command-templates.js +92 -176
- package/dist/lib/commands.d.ts +168 -82
- package/dist/lib/commands.js +1409 -291
- package/dist/lib/config.d.ts +1 -8
- package/dist/lib/config.js +34 -76
- package/dist/lib/delivery.d.ts +2 -2
- package/dist/lib/delivery.js +16 -16
- package/dist/lib/extension.js +639 -171
- package/dist/lib/generative-app-worker.mjs +19 -7
- package/dist/lib/generative-apps.d.ts +0 -5
- package/dist/lib/generative-apps.js +61 -49
- package/dist/lib/inbound.d.ts +5 -0
- package/dist/lib/inbound.js +35 -59
- package/dist/lib/journal.d.ts +33 -3
- package/dist/lib/journal.js +2063 -782
- package/dist/lib/lifecycle.d.ts +2 -6
- package/dist/lib/lifecycle.js +75 -58
- package/dist/lib/locks.d.ts +29 -3
- package/dist/lib/locks.js +289 -108
- package/dist/lib/logging.js +50 -14
- package/dist/lib/media.d.ts +22 -1
- package/dist/lib/media.js +46 -28
- package/dist/lib/menu-model.d.ts +19 -5
- package/dist/lib/menu-model.js +53 -11
- package/dist/lib/menu-queue.d.ts +5 -4
- package/dist/lib/menu-queue.js +45 -63
- package/dist/lib/menu-settings.d.ts +5 -4
- package/dist/lib/menu-settings.js +114 -78
- package/dist/lib/menu-status.d.ts +1 -1
- package/dist/lib/menu-status.js +8 -25
- package/dist/lib/menu-thinking.d.ts +6 -1
- package/dist/lib/menu-thinking.js +14 -21
- package/dist/lib/menu.d.ts +8 -61
- package/dist/lib/menu.js +92 -164
- package/dist/lib/model.d.ts +1 -1
- package/dist/lib/model.js +2 -16
- package/dist/lib/outbound-attachments.d.ts +6 -27
- package/dist/lib/outbound-attachments.js +45 -38
- package/dist/lib/outbound-buttons.js +22 -16
- package/dist/lib/outbound-markup.d.ts +0 -2
- package/dist/lib/outbound-markup.js +51 -64
- package/dist/lib/outbound-voice.js +6 -3
- package/dist/lib/outbound.d.ts +6 -6
- package/dist/lib/outbound.js +65 -51
- package/dist/lib/ownership.js +6 -2
- package/dist/lib/paths.d.ts +5 -0
- package/dist/lib/paths.js +37 -21
- package/dist/lib/pi.d.ts +2 -0
- package/dist/lib/pi.js +8 -1
- package/dist/lib/polling.d.ts +21 -37
- package/dist/lib/polling.js +59 -50
- package/dist/lib/preview.js +39 -19
- package/dist/lib/process-identity.d.ts +24 -0
- package/dist/lib/process-identity.js +133 -0
- package/dist/lib/prompts.js +2 -7
- package/dist/lib/queue.d.ts +14 -16
- package/dist/lib/queue.js +252 -127
- package/dist/lib/recovery.d.ts +2 -83
- package/dist/lib/recovery.js +20 -274
- package/dist/lib/replies.d.ts +16 -24
- package/dist/lib/replies.js +99 -116
- package/dist/lib/routing.d.ts +120 -7
- package/dist/lib/routing.js +4667 -1152
- package/dist/lib/runtime.d.ts +1 -11
- package/dist/lib/sections.d.ts +4 -29
- package/dist/lib/sections.js +7 -20
- package/dist/lib/skills.js +4 -3
- package/dist/lib/status.js +86 -57
- package/dist/lib/sync.d.ts +1 -3
- package/dist/lib/sync.js +67 -43
- package/dist/lib/target.d.ts +4 -0
- package/dist/lib/target.js +18 -0
- package/dist/lib/telegram-api.d.ts +18 -13
- package/dist/lib/telegram-api.js +148 -53
- package/dist/lib/text-groups.js +4 -39
- package/dist/lib/thread-cleanup-manager.d.ts +6 -27
- package/dist/lib/thread-cleanup-manager.js +369 -186
- package/dist/lib/thread-display.d.ts +1 -2
- package/dist/lib/thread-display.js +58 -38
- package/dist/lib/thread-naming.d.ts +28 -8
- package/dist/lib/thread-naming.js +175 -66
- package/dist/lib/thread-reconciler.d.ts +42 -16
- package/dist/lib/thread-reconciler.js +90 -50
- package/dist/lib/threads.d.ts +82 -20
- package/dist/lib/threads.js +2247 -855
- package/dist/lib/time-injection.js +1 -2
- package/dist/lib/turns.js +1 -19
- package/dist/lib/updates.d.ts +169 -80
- package/dist/lib/updates.js +2418 -641
- package/dist/lib/voice.d.ts +7 -22
- package/dist/lib/voice.js +7 -22
- package/dist/lib/wire.js +1 -1
- package/dist/lib/workspace-admission.d.ts +3 -1
- package/dist/lib/workspace-admission.js +425 -211
- package/dist/lib/workspace-identity.js +8 -13
- package/dist/lib/workspace-retirement.d.ts +1 -1
- package/dist/lib/workspace-retirement.js +328 -140
- package/dist/lib/workspace-slots.js +8 -4
- package/dist/package.json +1 -1
- package/docs/architecture.md +31 -29
- package/docs/inbound.md +1 -1
- package/docs/multi-instance-bus.md +15 -3
- package/docs/outbound.md +1 -1
- package/docs/public-api.md +29 -1
- package/docs/ui-style.md +25 -8
- package/lib/activity-verbosity.ts +59 -82
- package/lib/activity.ts +146 -102
- package/lib/agent-messages.ts +12 -5
- package/lib/bindings.ts +389 -82
- package/lib/bus-api.ts +263 -102
- package/lib/bus-follower.ts +2440 -601
- package/lib/bus-leader.ts +2209 -1290
- package/lib/bus-transport.ts +46 -12
- package/lib/bus.ts +1673 -444
- package/lib/channel-posts.ts +848 -327
- package/lib/command-templates.ts +152 -265
- package/lib/commands.ts +2398 -555
- package/lib/config.ts +177 -170
- package/lib/delivery.ts +41 -29
- package/lib/extension.ts +1206 -433
- package/lib/generative-app-worker.mjs +19 -7
- package/lib/generative-apps.ts +324 -211
- package/lib/inbound.ts +95 -97
- package/lib/journal.ts +5311 -1763
- package/lib/keyboard.ts +1 -3
- package/lib/lifecycle.ts +128 -78
- package/lib/locks.ts +677 -203
- package/lib/logging.ts +147 -58
- package/lib/media.ts +82 -43
- package/lib/menu-model.ts +85 -18
- package/lib/menu-queue.ts +88 -129
- package/lib/menu-settings.ts +260 -155
- package/lib/menu-status.ts +20 -60
- package/lib/menu-thinking.ts +41 -56
- package/lib/menu.ts +112 -285
- package/lib/model.ts +8 -22
- package/lib/outbound-attachments.ts +156 -125
- package/lib/outbound-buttons.ts +76 -49
- package/lib/outbound-markup.ts +96 -94
- package/lib/outbound-voice.ts +33 -27
- package/lib/outbound.ts +128 -125
- package/lib/ownership.ts +19 -10
- package/lib/paths.ts +148 -49
- package/lib/pi.ts +35 -15
- package/lib/polling.ts +144 -147
- package/lib/preview.ts +93 -40
- package/lib/process-identity.ts +164 -0
- package/lib/prompts.ts +4 -11
- package/lib/queue.ts +516 -301
- package/lib/recovery.ts +50 -454
- package/lib/rendering.ts +1 -3
- package/lib/replies.ts +248 -209
- package/lib/routing.ts +8509 -2186
- package/lib/runtime.ts +1 -12
- package/lib/sections.ts +24 -89
- package/lib/setup.ts +1 -3
- package/lib/skills.ts +9 -4
- package/lib/status.ts +215 -110
- package/lib/sync.ts +448 -361
- package/lib/target.ts +22 -3
- package/lib/telegram-api.ts +302 -127
- package/lib/text-groups.ts +5 -39
- package/lib/thread-cleanup-manager.ts +833 -331
- package/lib/thread-display.ts +213 -97
- package/lib/thread-naming.ts +232 -87
- package/lib/thread-reconciler.ts +155 -72
- package/lib/threads.ts +5318 -1845
- package/lib/time-injection.ts +1 -4
- package/lib/turns.ts +15 -28
- package/lib/updates.ts +5184 -1612
- package/lib/voice.ts +7 -35
- package/lib/wire.ts +1 -1
- package/lib/workspace-admission.ts +996 -416
- package/lib/workspace-identity.ts +14 -17
- package/lib/workspace-retirement.ts +878 -376
- package/lib/workspace-slots.ts +22 -13
- package/package.json +1 -1
- package/scripts/check-downgrade.mjs +0 -472
- package/scripts/measure-workspace.mjs +0 -108
|
@@ -271,7 +271,7 @@ Followers first try to re-register after leader reload or unknown-heartbeat resp
|
|
|
271
271
|
|
|
272
272
|
The session-native local wire contract has protocol version `2`, independent from the npm package version. 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 is part of protocol v2 rather than an optional capability. Every cwd-scoped v2 registration carries the exact bounded session ID; missing identity is rejected before provisioning or publication. Reconnect, replacement, and promotion preserve it. Protocol-v1 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.
|
|
273
273
|
|
|
274
|
-
The
|
|
274
|
+
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.
|
|
275
275
|
|
|
276
276
|
Negotiated identities remain on the live follower registry and appear in `/telegram-status --debug` plus the observational state snapshot. The Threaded Mode capability monitor owns one in-flight probe across lifecycle generations: stop/restart invalidates a late read, and a replacement monitor waits for the previous request to settle instead of creating overlapping transport transitions. Durable follower admission is authorized only when both peers advertise `durable-follower-admission-v1`, never inferred from package version: a capable runtime rejects missing support before provisioning, inbound routing, or election-roster eligibility. Authentication and exact registration generation remain mandatory independently of protocol compatibility. `follower.register` is the explicit bootstrap request and may provision; capability-gated `follower.restoreWorkspace` is startup-only and may claim, probe, or replace a remembered binding but returns `workspace-binding-unavailable` without allocating when no binding or claim-fenced legacy exact-`cwd` record exists. Both carry a fresh generation; every later request is exact-generation-fenced against the live registry entry. `bus.ack` is response-only and is rejected if submitted as a server request. Leader forwarding never synthesizes authority for an unknown recipient and preserves the follower's exact durable receipt end to end.
|
|
277
277
|
|
|
@@ -285,7 +285,17 @@ Live-process handoff combines journal CAS with authenticated bounded bus payload
|
|
|
285
285
|
|
|
286
286
|
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.
|
|
287
287
|
|
|
288
|
-
|
|
288
|
+
### Live-Rebind Channel
|
|
289
|
+
|
|
290
|
+
The 0.53.0 channel preserves protocol v2 and negotiates live rebinding independently from package version. Release 0.53.0 advertises the following capabilities; an instance still running an older build does not acquire them until it is reloaded. Compatible package versions alone cannot supply missing capabilities:
|
|
291
|
+
|
|
292
|
+
- `live-thread-rebind-save-v1`, `live-thread-rebind-apply-v1`, `live-thread-rebind-settle-v1`: Exact recipient save/apply/release and source settlement through the shared channel.
|
|
293
|
+
- `live-thread-rebind-command-set-v1`: One `selectedCommand: { name, target }` descriptor and `observe-command` / `command-observed` completion observation, rather than separate status/control wire forms. Retired descriptors and capability names refuse without a compatibility shim.
|
|
294
|
+
- `selected-menu-delivery-v1`: Restricted captured-recipient text/menu effects, independently fenced from source settlement; it is not a generic guarded follower API or arbitrary Bot API grant.
|
|
295
|
+
|
|
296
|
+
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.
|
|
297
|
+
|
|
298
|
+
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. Windows runs the same strict paths, exercised by the CI matrix; actual Pi/client acceptance and release are not inferred from supplied IPC fixtures.
|
|
289
299
|
|
|
290
300
|
### Local IPC endpoint with bounded native paths
|
|
291
301
|
|
|
@@ -530,7 +540,7 @@ All files containing routing, chat ids, thread ids, or process details use priva
|
|
|
530
540
|
|
|
531
541
|
### Follower heartbeat is missed
|
|
532
542
|
|
|
533
|
-
- Leader tolerates up to 15 seconds without a follower heartbeat before pruning it from the live registry, so short local event-loop or IPC stalls do not create false routing gaps. Follower heartbeat requests use a dedicated eight-second response deadline aligned with leader stale-liveness policy rather than the generic one-second local-RPC default. The deadline also defers its final timeout through one socket poll phase: an acknowledgement already buffered while the Pi/TUI event loop was blocked wins, while a genuinely silent peer still fails in the same event-loop turn. Heartbeat pruning remains liveness bookkeeping. One leader generation owns at most one prune operation; stop makes late endpoint, policy, and cleanup settlement inert, while durable-profile mutation serialization prevents replacement registration from crossing confirmed-dead cleanup.
|
|
543
|
+
- Leader tolerates up to 15 seconds without a follower heartbeat before pruning it from the live registry, so short local event-loop or IPC stalls do not create false routing gaps. Follower heartbeat requests use a dedicated eight-second response deadline aligned with leader stale-liveness policy rather than the generic one-second local-RPC default; leader-to-follower Workspace Restore and live-rebind control requests share that window, so a follower busy with journal work or a slower Windows named pipe is not a lost recipient. The deadline also defers its final timeout through one socket poll phase: an acknowledgement already buffered while the Pi/TUI event loop was blocked wins, while a genuinely silent peer still fails in the same event-loop turn. Heartbeat pruning remains liveness bookkeeping. One leader generation owns at most one prune operation; stop makes late endpoint, policy, and cleanup settlement inert, while durable-profile mutation serialization prevents replacement registration from crossing confirmed-dead cleanup.
|
|
534
544
|
- A missed heartbeat does not delete, close, mark offline, or send a disconnected notice for the follower's Telegram thread binding because the common cause may be leader reload, IPC handoff, or transient reconnect rather than a dead follower.
|
|
535
545
|
- After removal from live routing, the current leader retains at most 26 volatile preservation observations with exact registered PID, generation, target, profile and leader epoch. The existing prune loop rechecks alive/unverifiable PIDs and retries non-destructive publication only after fresh explicit absence and a known disabled-cleanup policy. Missing identity or overflow stays protected. Observations never answer heartbeat/API/forwarding requests, launch processes, or authorize deletion. Enabling cleanup cancels deferred preservation rather than promoting it into destructive work; the existing first-prune cleanup path is not retried by this mechanism.
|
|
536
546
|
- Registry registration invalidates overlapping instance/profile-owner/target observations, even if that replacement is removed before the next prune. Admitted same-instance provisioning cancels the old observation before its store writes can precede live registration. Profile/epoch drift, registry clear and leader stop invalidate it; late old completion cannot erase a replacement runtime's observation. No observation survives leader restart, reconstructs a PID from an opaque instance ID, or invents legacy inactivity.
|
|
@@ -553,6 +563,8 @@ Local regression evidence covers continuity and cleanup authority; operator-coor
|
|
|
553
563
|
|
|
554
564
|
### Restore controls and cleanup
|
|
555
565
|
|
|
566
|
+
This section records the conservative pre-0.53.0 runtime that Workspace Restore keeps for continuity with older instances. The 0.53.0 [live Thread rebinding channel](#live-rebind-channel) replaces native-reader parity research with one same-session workflow. Its explicit historical-protection reduction is limited to the new live cleanup origin, not temporary-tab cleanup, retirement, session adoption or legacy journal proofs.
|
|
567
|
+
|
|
556
568
|
Operator-confirmed client flow: send ordinary text from the **All** tab; Telegram creates a new thread containing that text. Choose **Replace/restore thread…** and the existing Pi instance. Successful leader Restore binds and renames the new thread, dispatches the original message to the same Pi, and removes the old thread. The resulting prompt's thread-name label reflects the restored destination, not proof that the user typed in the old thread. A threadless `/start` is not equivalent to this ordinary-text flow.
|
|
557
569
|
|
|
558
570
|
Unselected All-tab command choosers expire 60 minutes after the original Telegram message timestamp. Expiry reports terminal settlement only for the exact still-deferred update and makes its callback inert; it does not execute the command or discard an accepted queue receipt. A valid destination selection pauses expiry during dispatch and suppresses concurrent repeat clicks. Failed attempts return to the original deadline without extending it, matching the replay expiry policy after restart. Successful local command dispatch settles a still-deferred source without waiting for background menu delivery; an accepted Pi queue receipt retains its own settlement contract. Follower command forwarding retires a still-deferred source only after confirmed acceptance; thrown or retryable transfer outcomes retain it. Expired replay completes without recreating the chooser. Worker stop invalidates pending chooser authority even without a usable timestamp. The expiry timer rechecks and rearms against its original deadline after backward wall-clock changes. Missing/invalid timestamps are not guessed, and `/thread`, bound-thread messages, ordinary prompts, and classic mode do not use this policy. Journal-write failure preserves the source for recovery; an expired visible chooser may remain, but its button cannot route the command. At pending capacity, new chooser admission fails retryably rather than silently evicting unresolved All commands.
|
package/docs/outbound.md
CHANGED
|
@@ -84,7 +84,7 @@ A Guest Mode reply is one `answerGuestQuery` call carrying exactly one `InlineQu
|
|
|
84
84
|
|
|
85
85
|
Telegram accepts public URLs or existing Telegram `file_id` values for inline media results, but pi-telegram does not publish local artifacts to external hosting. A local guest document, photo, MP3 audio, or OGG/OPUS voice therefore uses a temporary upload to the paired owner's bot chat, extraction of the returned `file_id`, one cached-media guest answer, and best-effort deletion of the staging message. That message can briefly appear or notify the owner. One guest query can carry only one media item, and its answer text must fit the media caption limit rather than a separate full Rich Markdown message.
|
|
86
86
|
|
|
87
|
-
Configured text handlers provide `template`. A string is one command; an array is ordered composition. Top-level `args` and `defaults` apply to all composed steps unless a step defines private values. The command-template default timeout applies automatically. Use `template: [...]` for composition; the old local `pipe` alias is removed in 0.13.0.
|
|
87
|
+
Configured text handlers provide `template`. A string is one command; an array is ordered composition. Top-level `args` and `defaults` apply to all composed steps unless a step defines private values. The command-template default timeout applies automatically. Use `template: [...]` for composition; the old local `pipe` alias is removed in 0.13.0. A falsy root `when` skips that handler: inbound and voice selection continue with the next matching handler, while a text transform passes its input through unchanged. A falsy `when` on a composed step, or on any enclosing composition node, skips that step and passes the previous output on. `recover` runs only between failed `retry` attempts of the node that declares it; its output is ignored and a recovery failure stops further retries. `parallel` and `delay` are not executed by these handlers.
|
|
88
88
|
|
|
89
89
|
## Text Handler Config
|
|
90
90
|
|
package/docs/public-api.md
CHANGED
|
@@ -62,7 +62,7 @@ Hidden compatibility shortcuts may open sections directly: `/help`, `/status`, `
|
|
|
62
62
|
|
|
63
63
|
This command surface is a mobile companion subset, not a raw terminal-command bridge or session browser. A Telegram destination follows its assigned Pi instance and sends prompts into that instance's currently active session; it is not permanently bound to one session identity. Compaction and new-session replacement operate on the current session; resume, fork, tree navigation, session switching, TUI transcript clearing, and arbitrary slash-command dispatch stay out of the stable Telegram API unless Pi exposes safe public extension hooks for them.
|
|
64
64
|
|
|
65
|
-
An
|
|
65
|
+
An unbound Thread chooser offers **⛔️ Cancel routing** to the current paired owner for any input it routes: text, commands, any attachment kind and albums alike. The only condition is that every source supports exact private retention. It retains the original privately without sending it to Pi or deleting its Telegram message/Thread. This is not `/abort` or `/stop`: already selected, partially forwarded, queued or running work cannot be cancelled here. Failed cancellation or chooser updates retain the same button for exact retry. When the current leader has protected attempts, **Status → ❌ Pending cancellations** opens owner-only review with up to five supported inputs per page. Finish cancellation preserves the original and starts no new delivery; previously accepted work may continue. Refresh, navigation, reload and ownership changes invalidate old controls. Corrupt retention evidence stays protected without implicit repair. After a restart, an input still waiting for its choice gets its original chooser back, with working buttons and the same deadline, while its tab is unbound; inputs whose choice was already made stay protected until their deadline. Existing retained attempts remain recoverable. Historical unbound startup text is still held before default routing, but the Historical inputs menu entry and its review/Stop retrying submenu are removed. Previously published historical callbacks are answered as unavailable; they neither reopen the view nor authorize routing or abandonment. Hiding this technical state does not establish non-delivery or cancel independently accepted work. Confirmed eligible unbound prompt choosers now retain a fixed 60-minute deadline from first positive publication acknowledgement; refresh/restart does not renew it. Validated selection freezes expiry before effects; browsing or unavailable targets does not. Expiry retains the exact unselected original privately before pending-journal removal and invalidates stale controls, without Thread deletion, Pi stop or task-completion reporting. Selected/accepted/uncertain work and historical or unconfirmed clocks remain protected; no tab-disappearance signal is required. The journal lifetime capability and carrier hooks are package-private, not a new companion API. Live acceptance passed in the operator's 0.52.0 live smoke.
|
|
66
66
|
|
|
67
67
|
### Tools and assistant-authored actions
|
|
68
68
|
|
|
@@ -250,6 +250,34 @@ Contract:
|
|
|
250
250
|
- The command context currently provides `name`, `args`, `reply(text)`, and `enqueuePrompt(prompt)`. Use `enqueuePrompt()` when a command should create normal queued Pi work rather than perform immediate Telegram-side handling.
|
|
251
251
|
- Handler failures are isolated: the bridge records a `telegram-command` runtime diagnostic, sends a compact failure reply, and keeps Telegram polling/routing alive.
|
|
252
252
|
|
|
253
|
+
### Selected Preparation (Staged 0.53.0 Contract)
|
|
254
|
+
|
|
255
|
+
Registrations may additionally opt in with `selected.prepare(input)`. Exported types are `SelectedPreparationInput`, `SelectedCommandExecution` and `PreparedSelectedCommand`:
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
interface SelectedPreparationInput {
|
|
259
|
+
readonly name: string;
|
|
260
|
+
readonly args: string;
|
|
261
|
+
}
|
|
262
|
+
interface SelectedCommandExecution {
|
|
263
|
+
assertCurrent(): void;
|
|
264
|
+
reportCompleted(): boolean;
|
|
265
|
+
reply(text: string): void;
|
|
266
|
+
}
|
|
267
|
+
type PreparedSelectedCommand =
|
|
268
|
+
| { kind: "command-only"; execute(ctx: SelectedCommandExecution): void | Promise<void> }
|
|
269
|
+
| { kind: "generated-prompt"; prompt: string };
|
|
270
|
+
// selected.prepare(input) returns a plan or undefined, synchronously or asynchronously.
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Preparation is a trusted side-effect-free producer contract, not a sandbox. It receives only frozen name/args copies: no ordinary reply/enqueue ports, raw update or Pi context. Return `undefined` to refuse. Plans must contain exactly the branch fields above; generated text must be nonblank. The bridge captures the exact registration/callback and a frozen plan, checking source, recipient and registration before/after preparation and plan copying. Errors, invalid plans or stale authority refuse without handler/template fallback.
|
|
274
|
+
|
|
275
|
+
The approved execution contract calls `execute` once after release. Producers recheck `assertCurrent()` before mutations and after awaits, and explicitly `reportCompleted()` after required semantics. Acceptance is not exact worker removal ACK, delivery success or cleanup authority; returns/errors/replies never auto-complete. The generated branch has no execution callback: the bridge owns one exact normal prompt receipt and selected no-fold admission. Detached replies use independent recipient/registration authority after original disposal; lost reports, receipts or delivery never permit blind replay.
|
|
276
|
+
|
|
277
|
+
**Implementation boundary:** Public types, Commands preparation and both single-message leader producer branches are locally composed behind the negotiated root gate, advertised since 0.53.0. Command-only execution/source-ACK and detached replies remain separate. Generated input captures its existing worker admission/receipt owner before binding, admits the frozen text once after release through Queue/Turns without raw command media/inbound transforms or history folding, and confirms only exact original/acquiring-owner/recipient/prepared-payload receipt proof. The same warm worker may positively resolve durable readback without another enqueue; missing proof remains unconfirmed. No producer receipt/enqueue port is added. Ordinary dispatch still invokes `handler` only. Legacy registrations, extension groups (including command captions entering prompt coalescing), control input and follower execution remain unsupported. Channel policy and negotiated capabilities live in [Multi-Instance Bus](./multi-instance-bus.md#live-rebind-channel).
|
|
278
|
+
|
|
279
|
+
The staged core [unified command channel](./multi-instance-bus.md#live-rebind-channel) is package-private and does not widen this registration contract. Its supported built-ins/templates use captured registry plans for compatible leader selections and held followers; that is not follower opt-in for extension producers. Held/selected preparation, queue completion scopes and native journal ACK readers are not public commands/updates exports. Release 0.53.0 advertises the live-rebind capabilities. Neither a matching package version nor `selected.prepare` registration alone supplies negotiated peer/recipient authority, and the running Pi is not reloaded by this source change.
|
|
280
|
+
|
|
253
281
|
Core commands stay reserved for bridge lifecycle, transport ownership, queue safety, and essential operator controls. Opinionated workflow commands should live in companion extensions through this registry.
|
|
254
282
|
|
|
255
283
|
## Sections
|
package/docs/ui-style.md
CHANGED
|
@@ -20,7 +20,8 @@ Use emoji as stable semantic markers, not decoration. Emoji carry transportable
|
|
|
20
20
|
|
|
21
21
|
| Emoji | Meaning | Canonical surfaces | Notes |
|
|
22
22
|
| --- | --- | --- | --- |
|
|
23
|
-
| `🧵` | Telegram/Pi thread
|
|
23
|
+
| `🧵` | Telegram/Pi thread | Unbound-thread warnings, thread lifecycle/status copy, and concrete thread target buttons in the Reroute and Restore submenus | Canonical thread marker. In route submenus the action lives in the heading and every target button is a thread, so each one carries `🧵` before its acknowledged display title or stable-name fallback. |
|
|
24
|
+
| `🚦` | Routing decision | Root route chooser heading in unbound or temporary tabs (`🚦 Route this message:`) | The heading only asks for a decision; the buttons name each available action. |
|
|
24
25
|
| `📡` | Telegram transport / bridge connection | Instance connected notices, polling/transport role, bridge online copy | Transport is not thread identity; use `🧵` for thread concepts. |
|
|
25
26
|
| `📊` | Status / overview | `/status` command description, status cards or status rows | Use for status summaries, not queue priority. |
|
|
26
27
|
| `🤖` | Model selection | `/model`, model menu headings, model status rows | Keep model-control surfaces visually distinct from thinking. |
|
|
@@ -50,11 +51,10 @@ Use emoji as stable semantic markers, not decoration. Emoji carry transportable
|
|
|
50
51
|
| `🟥` | Destructive stop command | `/stop` command description | Strong warning at the command/action entrypoint; standalone results use the more precise idle or abort state icon. |
|
|
51
52
|
| `🆕` | New session / fresh start | `/new`, session replacement notices | `/new` replaces the active Pi session while preserving the current classic chat or Thread target; use it only for a real session reset. |
|
|
52
53
|
| `🔄` | Refresh | Queue refresh row and future refresh buttons | Re-fetch/re-render current surface, not transport reconnect. |
|
|
53
|
-
|
|
|
54
|
-
| `🔁` | Replace/restore mode |
|
|
55
|
-
| `➡️` | Choose replacement target | Thread replace/restore target buttons that select which Pi instance should move to the current thread | Use inside the second replace/restore chooser, not for ordinary reroutes. |
|
|
54
|
+
| `🔀` | Reroute to an existing target | Root `🔀 Reroute: send it to a Pi thread` button, the `🔀 Reroute … to:` submenu heading | Crossing arrows mean the held message branches off to another live thread; its target buttons use `🧵`. |
|
|
55
|
+
| `🔁` | Replace/restore mode | Root `🔁 Restore: move a Pi into this tab` button and the `🔁 Restore into this tab & send …:` submenu heading | Same marker for the action at both levels, like `🔀` for Reroute; its target buttons use `🧵`. |
|
|
56
56
|
| `☑️` | Activate / choose this item | Model detail activation action, generated button-only choice heading | Positive selection cue; use `🟢 Active` for already-current state. |
|
|
57
|
-
| `⛔️` | Cancel source routing | `⛔️ Cancel routing` chooser action and confirmed `⛔️ Routing cancelled.` feedback | Not an abort of active Pi work. Private retention remains mandatory; disposable-tab removal
|
|
57
|
+
| `⛔️` | Cancel source routing | `⛔️ Cancel routing` chooser action and confirmed `⛔️ Routing cancelled.` feedback | Not an abort of active Pi work. Private retention remains mandatory; disposable-tab removal stays bound to exact eligible target authority and follows only once every input in the tab is resolved. |
|
|
58
58
|
| `❌` | No / cancel / terminal failure | Confirmation cancel buttons and terminal failure notices | Do not use for a recoverable operation failure that leaves session state intact. |
|
|
59
59
|
| `🗑` | Delete / defer removal | Destructive confirmations and removal reaction | In the queue menu, reversible Keep/Skip selectors replace immediate deletion. |
|
|
60
60
|
|
|
@@ -64,7 +64,7 @@ Use emoji as stable semantic markers, not decoration. Emoji carry transportable
|
|
|
64
64
|
| --- | --- | --- | --- |
|
|
65
65
|
| `💤` | Nothing active | No-active-turn notices | Neutral idle result, not an error. |
|
|
66
66
|
| `✅` | Completed successfully | Compaction and other completion notices | Use only after the operation has completed. |
|
|
67
|
-
| `🚫` | Unavailable, denied, or cancelled operation | Missing capability/auth notices, access denial, and compaction cancellation | Callback
|
|
67
|
+
| `🚫` | Unavailable, denied, or cancelled operation | Missing capability/auth notices, access denial, and compaction cancellation | Callback toasts carry no emoji. |
|
|
68
68
|
| `⚠️` | Recoverable operation failure | Compaction failure notice | The attempted operation failed, but the original session state remains usable. |
|
|
69
69
|
|
|
70
70
|
### State Indicators And Button Grammars
|
|
@@ -108,7 +108,7 @@ The menu may clear internal Skip but cannot remove a reaction created by the use
|
|
|
108
108
|
|
|
109
109
|
Some emoji are intentionally local examples or decorative variants, not global semantics. Empty-queue rotating messages (`🫙`, `🍃`, `🕳`, `🦗`, `🌙`, `🧘`, `🪐`, `🧺`, `🔭`, `🫧`, `🛸`) are copy flavor only and must not become controls. The Guest Mode placeholder frames (`🌎`, `🌍`, `🌏` stepping every second with dots growing once every two seconds) are the same kind of decorative copy: they complete whole 6-frame cycles over at least a ~20 s rotation while a guest answer is pending and then hold the cycle's final frame (a 26 s safety bound caps slow streams), never become controls, and must not carry another meaning. Example extension icons such as `🧪`, `🔧`, and `🗂` are documentation fixtures for companion extensions, not built-in pi-telegram meanings.
|
|
110
110
|
|
|
111
|
-
Thread UI rule:
|
|
111
|
+
Thread UI rule: thread lifecycle and status copy starts with `🧵`. Route choosers lead with their action instead: `🚦` asks for the routing decision at the root, the `🔀`/`🔁` submenu headings name the chosen action, and the concrete thread target buttons beneath them carry `🧵`, so the same marker always means "a thread".
|
|
112
112
|
|
|
113
113
|
## Button Control Hierarchy
|
|
114
114
|
|
|
@@ -232,10 +232,24 @@ Examples:
|
|
|
232
232
|
- Main menu → Settings: first row is `⬆️ Main menu`.
|
|
233
233
|
- Settings → Voice reply mode: first row is `⬆️ Back`.
|
|
234
234
|
|
|
235
|
+
## Route Chooser
|
|
236
|
+
|
|
237
|
+
A message (or All-tab command) held in an unbound or temporary tab gets one route chooser.
|
|
238
|
+
|
|
239
|
+
- Root text: bold heading `🚦 Route this message:` (or `🚦 Route <code>/start</code>:` for a command), a blank line, then the italic note `The choice expires in 60 minutes.` When Reroute is the only possible action (an All-tab command without a temporary tab), the italic note says `To restore a Pi instead, send a message in a new tab.` instead.
|
|
240
|
+
- Root buttons, one per row and only when available: `🔀 Reroute: send it to a Pi thread`, `🔁 Restore: move a Pi into this tab`, `⛔️ Cancel routing`. The `🚦` heading echoes these three choices, with `⛔️` as the red stop.
|
|
241
|
+
- Submenus start with `⬆️ Back`, which restores the root with its exact original text. Headings name the same subject as the root: bold `🔀 Reroute this message to:` / `🔀 Reroute <code>/start</code> to:`, and `🔁 Restore into this tab & send this message:` / `🔁 Restore into this tab & send <code>/start</code>:`. Every target button below is a thread, written `🧵 <display title>`.
|
|
242
|
+
- Cancel appears only at the root. A confirmed cancellation replaces the chooser with bold `⛔️ Routing cancelled.` and an empty keyboard.
|
|
243
|
+
- Old or expired controls answer `⌛ Routing choice expired.`
|
|
244
|
+
- Tab name: every temporary routing tab is named `🚦 Routing`, whether the bot created it (menu command or threadless prompt) or adopted the `New Chat` tab Telegram opened for a typed input. It is never renamed after its first input.
|
|
245
|
+
- Status → `❌ Pending cancellations` lists one row per protected original: its text, or `📎` plus the caption, file name or the message field carrying Telegram's `file_id`.
|
|
246
|
+
|
|
235
247
|
## Pi Connection Notices
|
|
236
248
|
|
|
237
249
|
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.
|
|
238
250
|
|
|
251
|
+
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.
|
|
252
|
+
|
|
239
253
|
## Message Cards
|
|
240
254
|
|
|
241
255
|
Message cards and standalone informational notices sent by the bot should start with a strong heading.
|
|
@@ -244,10 +258,13 @@ Rules:
|
|
|
244
258
|
|
|
245
259
|
- Start with a bold heading or, for dialogs, a bold question.
|
|
246
260
|
- Format standalone notices as one fully bold line: relevant emoji, one space, concise sentence, and terminal period. Menu or chooser headings use the same fully bold form but end in a colon when controls or detail follow. Empty-queue headings are the deliberate exception: fully bold, with no trailing period or colon.
|
|
261
|
+
- List items in bot-authored messages and cards start with `<code>-</code>` and a space, the same marker rendered Markdown lists use; never `•`.
|
|
247
262
|
- Keep the emoji and complete sentence or heading inside the single bold span; do not bold only a fragment. A material name or phrase may receive nested italic emphasis without breaking the outer bold hierarchy—for example `<b>📡 Instance <i>Cedar</i> connected.</b>`.
|
|
248
263
|
- Apply the same hierarchy to success, progress, empty, busy, unavailable, cancellation, and failure notices.
|
|
249
264
|
- Once an action has settled, describe only the completed result in completed-state language. Do not append transitional copy such as “returning” or “starting”; use a separate progress surface only while work is genuinely still pending.
|
|
250
|
-
-
|
|
265
|
+
- Distinguish the two surfaces. An in-chat notice stays in the conversation (a sent message or an edited chooser/menu/card) and uses the bold, period-terminated form above. A callback toast is the client's fleeting tooltip that vanishes after a couple of seconds: concise plain text with no emoji and no terminal sentence period; question/exclamation marks and ellipses stay. Every toast follows this one form, so emoji meanings in the registry apply to messages, headings and buttons, never to toasts.
|
|
266
|
+
- Write every notice and toast in its final form where it is used. No delivery boundary rewrites punctuation, so text shared by both surfaces is two separate strings, each in its own form; companion `ctx.answerCallback` text likewise reaches the client exactly as written.
|
|
267
|
+
- Navigation answers silently: opening a submenu or going Back, in the main menu and in temporary routing tabs alike, edits the message and acknowledges the tap without a toast. The edited message is the feedback.
|
|
251
268
|
- Setting detail cards may include an emoji in the heading, then a colon and the current value in `<code>`.
|
|
252
269
|
- Explain what the setting does and what the options mean only as much as needed.
|
|
253
270
|
- Order setting value descriptions exactly like the chooser: rows top-to-bottom and values in a shared row left-to-right. Keep `(default)` on the actual default wherever it falls; default status never changes order.
|
|
@@ -4,11 +4,15 @@
|
|
|
4
4
|
* Owns persistent bounded thinking and tool disclosures; excludes activity normalization, assistant answer rendering, and transport authority policy
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type {
|
|
7
|
+
import type {
|
|
8
|
+
TelegramActivityEvent,
|
|
9
|
+
TelegramActivityPublicationRuntime,
|
|
10
|
+
} from "./activity.ts";
|
|
11
|
+
import { escapeHtml, renderTelegramInlineMarkdownHtml } from "./rendering.ts";
|
|
8
12
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from "./
|
|
13
|
+
areTelegramTargetsEqual as targetEquals,
|
|
14
|
+
type TelegramTarget,
|
|
15
|
+
} from "./target.ts";
|
|
12
16
|
import type {
|
|
13
17
|
TelegramEditMessageTextBody,
|
|
14
18
|
TelegramInputRichBlock,
|
|
@@ -17,7 +21,6 @@ import type {
|
|
|
17
21
|
TelegramSendRichMessageBody,
|
|
18
22
|
TelegramSentMessage,
|
|
19
23
|
} from "./telegram-api.ts";
|
|
20
|
-
import { areTelegramTargetsEqual as targetEquals, type TelegramTarget } from "./target.ts";
|
|
21
24
|
|
|
22
25
|
const TELEGRAM_ACTIVITY_DETAIL_MAX_CHARS = 1_200;
|
|
23
26
|
const TELEGRAM_ACTIVITY_MESSAGE_MAX_CHARS = 3_900;
|
|
@@ -55,10 +58,7 @@ interface ReasoningMessage {
|
|
|
55
58
|
function redactActivityText(text: string): string {
|
|
56
59
|
return text
|
|
57
60
|
.replace(/\b\d{8,12}:[A-Za-z0-9_-]{30,}\b/g, "[REDACTED_BOT_TOKEN]")
|
|
58
|
-
.replace(
|
|
59
|
-
/\b(Bearer\s+)[A-Za-z0-9._~+/=-]{16,}\b/gi,
|
|
60
|
-
"$1[REDACTED]",
|
|
61
|
-
)
|
|
61
|
+
.replace(/\b(Bearer\s+)[A-Za-z0-9._~+/=-]{16,}\b/gi, "$1[REDACTED]")
|
|
62
62
|
.replace(
|
|
63
63
|
/(["']?(?:api[_-]?key|token|password|secret)["']?\s*[:=]\s*["']?)[^"',\s}]+/gi,
|
|
64
64
|
"$1[REDACTED]",
|
|
@@ -89,9 +89,7 @@ function formatActivityJson(value: unknown, depth = 0): string[] {
|
|
|
89
89
|
lines[lines.length - 1] += ",";
|
|
90
90
|
}
|
|
91
91
|
});
|
|
92
|
-
lines.push(
|
|
93
|
-
index < value.length - 1 ? `${indent}}, {` : `${indent}}]`,
|
|
94
|
-
);
|
|
92
|
+
lines.push(index < value.length - 1 ? `${indent}}, {` : `${indent}}]`);
|
|
95
93
|
});
|
|
96
94
|
return lines;
|
|
97
95
|
}
|
|
@@ -130,17 +128,14 @@ function serializeActivityValue(value: unknown): string {
|
|
|
130
128
|
let text: string;
|
|
131
129
|
try {
|
|
132
130
|
const normalized =
|
|
133
|
-
JSON.stringify(
|
|
134
|
-
|
|
135
|
-
(
|
|
136
|
-
if (
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
return nested;
|
|
142
|
-
},
|
|
143
|
-
) ?? JSON.stringify(String(value));
|
|
131
|
+
JSON.stringify(value, (_key, nested) => {
|
|
132
|
+
if (typeof nested === "bigint") return nested.toString();
|
|
133
|
+
if (nested && typeof nested === "object") {
|
|
134
|
+
if (seen.has(nested)) return "[Circular]";
|
|
135
|
+
seen.add(nested);
|
|
136
|
+
}
|
|
137
|
+
return nested;
|
|
138
|
+
}) ?? JSON.stringify(String(value));
|
|
144
139
|
text = formatActivityJson(JSON.parse(normalized)).join("\n");
|
|
145
140
|
} catch {
|
|
146
141
|
text = JSON.stringify(String(value));
|
|
@@ -185,18 +180,12 @@ function renderToolActivityHtml(tool: ToolActivity): string {
|
|
|
185
180
|
evidence.push(`… [${tool.droppedUpdates} earlier updates omitted]`);
|
|
186
181
|
}
|
|
187
182
|
tool.updates.forEach((update, index) => {
|
|
188
|
-
evidence.push(
|
|
189
|
-
`"update ${tool.droppedUpdates + index + 1}": ${update}`,
|
|
190
|
-
);
|
|
183
|
+
evidence.push(`"update ${tool.droppedUpdates + index + 1}": ${update}`);
|
|
191
184
|
});
|
|
192
185
|
if (tool.complete && tool.result !== undefined) {
|
|
193
186
|
evidence.push(`"${tool.isError ? "error" : "result"}": ${tool.result}`);
|
|
194
187
|
}
|
|
195
|
-
const status = tool.complete
|
|
196
|
-
? tool.isError
|
|
197
|
-
? "failed"
|
|
198
|
-
: "done"
|
|
199
|
-
: "running";
|
|
188
|
+
const status = tool.complete ? (tool.isError ? "failed" : "done") : "running";
|
|
200
189
|
return [
|
|
201
190
|
`<b>${escapeHtml(formatToolActivityLabel(tool.name))}:</b> <code>${status}</code>`,
|
|
202
191
|
`<blockquote expandable>${escapeActivityEvidenceHtml(evidence.join("\n\n"))}</blockquote>`,
|
|
@@ -225,11 +214,7 @@ function createToolActivityDetail(
|
|
|
225
214
|
function renderToolActivityRichBlocks(
|
|
226
215
|
tool: ToolActivity,
|
|
227
216
|
): TelegramInputRichBlock[] {
|
|
228
|
-
const status = tool.complete
|
|
229
|
-
? tool.isError
|
|
230
|
-
? "failed"
|
|
231
|
-
: "done"
|
|
232
|
-
: "running";
|
|
217
|
+
const status = tool.complete ? (tool.isError ? "failed" : "done") : "running";
|
|
233
218
|
const evidenceBlocks: TelegramInputRichBlock[] = [
|
|
234
219
|
createToolActivityDetail("arguments", tool.args, true),
|
|
235
220
|
];
|
|
@@ -278,7 +263,9 @@ function toolMessageSize(tools: readonly ToolActivity[]): number {
|
|
|
278
263
|
}
|
|
279
264
|
|
|
280
265
|
function isKnownSafeRichActivityRejection(error: unknown): boolean {
|
|
281
|
-
return
|
|
266
|
+
return (
|
|
267
|
+
error instanceof Error && /HTTP 400: Bad Request:/i.test(error.message)
|
|
268
|
+
);
|
|
282
269
|
}
|
|
283
270
|
|
|
284
271
|
export function renderTelegramThinkingActivityHtml(text: string): string {
|
|
@@ -292,32 +279,6 @@ export interface TelegramActivityVerbosityRuntime {
|
|
|
292
279
|
waitForIdle: () => Promise<void>;
|
|
293
280
|
}
|
|
294
281
|
|
|
295
|
-
export interface TelegramActivityVerbosityBinding
|
|
296
|
-
extends TelegramActivityVerbosityRuntime {
|
|
297
|
-
bind: (runtime: TelegramActivityVerbosityRuntime) => void;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
export function createTelegramActivityVerbosityBinding(): TelegramActivityVerbosityBinding {
|
|
301
|
-
let runtime: TelegramActivityVerbosityRuntime | undefined;
|
|
302
|
-
return {
|
|
303
|
-
bind(next) {
|
|
304
|
-
runtime = next;
|
|
305
|
-
},
|
|
306
|
-
accept(event) {
|
|
307
|
-
runtime?.accept(event);
|
|
308
|
-
},
|
|
309
|
-
reset() {
|
|
310
|
-
runtime?.reset();
|
|
311
|
-
},
|
|
312
|
-
stop() {
|
|
313
|
-
runtime?.stop();
|
|
314
|
-
},
|
|
315
|
-
waitForIdle() {
|
|
316
|
-
return runtime?.waitForIdle() ?? Promise.resolve();
|
|
317
|
-
},
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
|
|
321
282
|
export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
|
|
322
283
|
enqueue?: TelegramActivityPublicationRuntime["enqueue"];
|
|
323
284
|
getActivityMode: () => "quiet" | "thinking" | "tools" | "verbose";
|
|
@@ -391,8 +352,14 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
|
|
|
391
352
|
};
|
|
392
353
|
const hasAuthority = (): boolean =>
|
|
393
354
|
authority !== undefined && deps.isAuthorityActive(authority);
|
|
394
|
-
const isCurrent = (
|
|
395
|
-
|
|
355
|
+
const isCurrent = (
|
|
356
|
+
acceptedGeneration: number,
|
|
357
|
+
admittedAuthority: TAuthority | undefined,
|
|
358
|
+
): boolean =>
|
|
359
|
+
active &&
|
|
360
|
+
generation === acceptedGeneration &&
|
|
361
|
+
admittedAuthority !== undefined &&
|
|
362
|
+
deps.isAuthorityActive(admittedAuthority);
|
|
396
363
|
const ensureActivity = (
|
|
397
364
|
event: TelegramActivityEvent,
|
|
398
365
|
admittedTarget: TelegramTarget | undefined,
|
|
@@ -433,7 +400,9 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
|
|
|
433
400
|
);
|
|
434
401
|
body = renderTelegramThinkingActivityHtml(text);
|
|
435
402
|
if (body.length <= TELEGRAM_ACTIVITY_MESSAGE_MAX_CHARS) break;
|
|
436
|
-
retained = retained.slice(
|
|
403
|
+
retained = retained.slice(
|
|
404
|
+
-Math.max(1, Math.floor(retained.length * 0.75)),
|
|
405
|
+
);
|
|
437
406
|
} while (retained.length > 1);
|
|
438
407
|
const canEdit =
|
|
439
408
|
reasoningMessage && targetEquals(reasoningMessage.target, target);
|
|
@@ -481,9 +450,8 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
|
|
|
481
450
|
acceptedGeneration: number,
|
|
482
451
|
) => {
|
|
483
452
|
if (reasoningFlushTimer || reasoningBlocked) return;
|
|
484
|
-
const elapsed =
|
|
485
|
-
? 0
|
|
486
|
-
: getNowMs() - lastReasoningPublishMs;
|
|
453
|
+
const elapsed =
|
|
454
|
+
reasoningMessageFrames === 0 ? 0 : getNowMs() - lastReasoningPublishMs;
|
|
487
455
|
const delayMs = Math.max(0, TELEGRAM_REASONING_MIN_INTERVAL_MS - elapsed);
|
|
488
456
|
const schedule = deps.setReasoningTimeout ?? setTimeout;
|
|
489
457
|
reasoningFlushTimer = schedule(() => {
|
|
@@ -493,10 +461,12 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
|
|
|
493
461
|
if (
|
|
494
462
|
!isCurrent(acceptedGeneration, admittedAuthority) ||
|
|
495
463
|
reasoningChars <= lastReasoningMessageChars
|
|
496
|
-
)
|
|
464
|
+
)
|
|
465
|
+
return;
|
|
497
466
|
await publishReasoning(event, acceptedGeneration);
|
|
498
467
|
};
|
|
499
|
-
const enqueue =
|
|
468
|
+
const enqueue =
|
|
469
|
+
deps.enqueue ?? ((next: () => Promise<void>) => tail.then(next));
|
|
500
470
|
tail = enqueue(task).catch((error) => {
|
|
501
471
|
deps.recordFailure?.("reasoning-send", event, error);
|
|
502
472
|
});
|
|
@@ -721,10 +691,7 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
|
|
|
721
691
|
}
|
|
722
692
|
if (event.type === "agent-end" || event.type === "agent-settled") {
|
|
723
693
|
clearReasoningFlushTimer();
|
|
724
|
-
if (
|
|
725
|
-
reasoningChars > lastReasoningMessageChars &&
|
|
726
|
-
!reasoningBlocked
|
|
727
|
-
) {
|
|
694
|
+
if (reasoningChars > lastReasoningMessageChars && !reasoningBlocked) {
|
|
728
695
|
await publishReasoning(event, acceptedGeneration);
|
|
729
696
|
}
|
|
730
697
|
if (!isCurrent(acceptedGeneration, admittedAuthority)) return;
|
|
@@ -738,14 +705,24 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
|
|
|
738
705
|
const resolvedTarget = deps.resolveTarget(event);
|
|
739
706
|
const admittedTarget = resolvedTarget ? { ...resolvedTarget } : undefined;
|
|
740
707
|
const admittedAuthority = deps.captureAuthority();
|
|
741
|
-
const enqueue =
|
|
708
|
+
const enqueue =
|
|
709
|
+
deps.enqueue ?? ((task: () => Promise<void>) => tail.then(task));
|
|
742
710
|
tail = enqueue(async () => {
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
711
|
+
if (
|
|
712
|
+
!active ||
|
|
713
|
+
generation !== acceptedGeneration ||
|
|
714
|
+
!deps.isAuthorityActive(admittedAuthority)
|
|
715
|
+
)
|
|
716
|
+
return;
|
|
717
|
+
await process(
|
|
718
|
+
event,
|
|
719
|
+
acceptedGeneration,
|
|
720
|
+
admittedTarget,
|
|
721
|
+
admittedAuthority,
|
|
722
|
+
);
|
|
723
|
+
}).catch((error) => {
|
|
724
|
+
deps.recordFailure?.("tool-send", event, error);
|
|
725
|
+
});
|
|
749
726
|
},
|
|
750
727
|
reset() {
|
|
751
728
|
generation += 1;
|