@llblab/pi-telegram 0.52.2 → 0.53.0
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 +11 -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 +61 -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 +249 -126
- 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 +125 -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 +511 -300
- 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
package/AGENTS.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## 0. Meta-Protocol Principles
|
|
4
4
|
|
|
5
|
-
- `Mobile companion boundary`: Telegram extends a running Pi session; it is not a remote terminal, PTY supervisor, process launcher, session browser, or replacement TUI. Never emulate Pi navigation through private internals, ANSI/TTY injection, or a shadow `pi` process.
|
|
5
|
+
- `Mobile companion boundary`: Telegram extends a running Pi session; it is not a remote terminal, PTY supervisor, process launcher, session browser, or replacement TUI. Never emulate Pi navigation through private internals, ANSI/TTY injection, or a shadow `pi` process. Built-in Telegram controls manage work inside a running session; Pi process termination stays a local Pi/TUI operation, not a terminal command to mirror.
|
|
6
6
|
- `Runtime safety`: Prefer explicit, fenced, recoverable behavior over shortcuts that can desynchronize Telegram transport, durable admission, local queue state, or Pi lifecycle state.
|
|
7
7
|
- `Pi baseline`: Require Pi ≥1.0.0 across coding-agent, agent-core and AI peers; keep locks and public API compatibility tests aligned without weakening live acceptance gates.
|
|
8
8
|
- `Pi-native extensibility`: Add capabilities through stable Pi and pi-telegram contracts. Do not fork polling, transport, menu ownership, or package-private runtime internals.
|
|
@@ -27,7 +27,7 @@ Canonical terms:
|
|
|
27
27
|
|
|
28
28
|
Keep each fact in one authoritative layer:
|
|
29
29
|
|
|
30
|
-
- [`README.md`](./README.md): Public product entrypoint. Preserve the flow identity → install/connect → examples → product model → compact capabilities → controls/safety → docs. Balance strong positioning with a practical catalogue;
|
|
30
|
+
- [`README.md`](./README.md): Public product entrypoint. Preserve the flow identity → install/connect → examples → product model → compact capabilities → controls/safety → docs. Balance strong positioning with a practical catalogue of capabilities, operator gestures and meaningful consequences; keep detailed routing proofs, recovery algorithms and action-markup grammar in indexed `/docs` instead of accumulating release-by-release paragraphs.
|
|
31
31
|
- `AGENTS.md`: Stable engineering boundaries, recurring runtime invariants, and work protocol. Link to evolving subsystem contracts instead of copying them here.
|
|
32
32
|
- [`BACKLOG.md`](./BACKLOG.md): Canonical unresolved work. Keep only open top-level outcomes with nested decomposition and done criteria. Remove completed outcomes rather than retaining checked history.
|
|
33
33
|
- [`CHANGELOG.md`](./CHANGELOG.md): Completed user/operator/developer impact. A release has at most eight outcome bullets of at most 512 characters, each beginning with an inline-code domain label and colon. Exclude personal names and real user/chat/message/thread identifiers. Consolidate the current pre-release section before release; do not rewrite historical sections without an explicit retrospective request and evidence pass.
|
|
@@ -77,7 +77,7 @@ Use the relevant local skill before non-trivial work in its domain. Keep skill o
|
|
|
77
77
|
- Threaded Mode has exactly one live leader per bot profile. Followers are real operator-started Pi processes and must authenticate/register over local IPC; Telegram never spawns hidden Pi processes. A live but unreachable owner does not authorize split-brain polling.
|
|
78
78
|
- Local IPC is a trust boundary, not merely a private socket. Unknown, stale, mismatched-generation, or unauthorized requests must not inject prompts, callbacks, API sends, artifacts, liveness, or bindings. Follower registration identity may prepare a binding, but inbound generation authority is published only after successful preparation for the same generation and current context; pending or failed startup cannot append into a retained old journal. Readiness also binds the active Pi context and supplied session generation. Same-session refresh awaits binding preparation without re-registering; a changed session ID cannot publish readiness until its exact leader registration is acknowledged. Reusing a context object cannot carry readiness across a session-generation or ID change. Registration requests capture session authority before asynchronous startup and fence every publication/finalization against the current attempt. Stop or supersession invalidates that attempt; obsolete cleanup cannot erase a newer registration or a refreshed context.
|
|
79
79
|
- Protocol compatibility is independent from package version. Registration negotiates protocol version, runtime build, and canonical capabilities before target provisioning or live publication. `durable-follower-admission-v1` gates source forwarding; `queue-handoff-v1` independently gates semantic queue transfer for every participant and is advertised only with exact source/recipient journal-binding composition. `follower.register` and capability-gated restore-only `follower.restoreWorkspace` are bootstrap requests; other requests require exact live-registry generation authority, and `bus.ack` is response-only. `thread-display-mode-v1` gates follower display-setting requests; the leader owns their serialized profile preference and title application.
|
|
80
|
-
- Long-lived timers, pollers, watchers, receivers, heartbeats, background delivery, and deferred dispatch are session-bound. Shutdown cancels/drains diagnostics publications and fences deferred status reads before they can acquire guards or repair files. Replacement stops stale activity and makes late work inert; teardown must recheck the captured session generation even when context identity is reused. Same-process handoff may preserve exact profile/target identity but never stale Pi context or cross-profile authority. Follower heartbeat
|
|
80
|
+
- Long-lived timers, pollers, watchers, receivers, heartbeats, background delivery, and deferred dispatch are session-bound. Shutdown cancels/drains diagnostics publications and fences deferred status reads before they can acquire guards or repair files. Replacement stops stale activity and makes late work inert; teardown must recheck the captured session generation even when context identity is reused. Same-process handoff may preserve exact profile/target identity but never stale Pi context or cross-profile authority. Follower heartbeat and leader-to-follower Restore/live-rebind control reply deadlines align with the eight-second leader stale-liveness window rather than the generic one-second local-RPC default; an ordinary multi-second Pi/TUI event-loop stall or a slower Windows named pipe is not registration or recipient loss. Participating source observations must hold a reference for the actual read, including pending-mutation/count queries against a stopped worker's retained source; a scoped observation never restores receipt readiness or execution authority. A donor cancellation resumed after remote handoff awaits must also hold a source reference; only the existing exact journal CAS may cancel, never undo accepted recipient custody. Stable source keys do not certify captured callable lifetimes: snapshot prepared worker capabilities at construction and replace them on source-handle renewal without replaying unsettled input. Aborting a durable update generation does not release that `update_id`: replacement replay waits for its actual handler settlement, and effectful handlers use the shared execution fence immediately before commit and after awaited delegation. Admission also rechecks that fence after the default handler returns, before its outcome can settle custody; a stopped handler's ordinary return is not completion authority. Internal clones explicitly carry the hidden fence; reroute forwarding, thread-store mutation, cleanup, and Bot API boundaries retain the originating generation.
|
|
81
81
|
- Runtime state is event-driven reconciliation of local assumptions against Telegram signals, not a complete bot read-model and not permission to query Telegram on every action. Destructive thread cleanup goes through `thread-reconciler` with current proof and leader fencing. Fresh Workspace Thread creation derives its initial Bot API title from the active display mode before issuance; the stable generated `threadName` remains separate from the acknowledged `displayTitle`.
|
|
82
82
|
|
|
83
83
|
### 4.3 Durable Admission And Settlement
|
|
@@ -89,10 +89,10 @@ Use the relevant local skill before non-trivial work in its domain. Keep skill o
|
|
|
89
89
|
- Foreign forwarding settles as `accepted`, `retryable`, or `terminal-rejected`. Only an authenticated acknowledgement carrying the expected `deliveryId` and `sourceUpdateId` releases leader journal authority. Negative, missing, stale, mismatched, or capacity-failed settlement remains durable; callback error answers are side effects only. Exception: the operator-approved fixed chooser deadline may discard its exact unclaimed donor pending source without a body archive or delivery-success claim. Accepted recipient queue/work and bound Threads are untouched; old donor controls and late reports cannot replay it. The lifetime and temporary cleanup contract lives in `docs/architecture.md`.
|
|
90
90
|
- A forwarding delivery id is stable across registration replacement and derives from envelope kind, source `update_id`, and stable recipient binding. Runtime instance and registration generation remain separate attempt fences. Persisted message ownership carries the stable binding so replay can rebind only to its current authenticated registration.
|
|
91
91
|
- A queued receipt persists its acquiring runtime instance, OS pid/process-birth identity, session generation, acquisition id, and acquisition time. Only exact authority may settle or discard it. Cached presence is not current execution proof: prepared custody must revalidate the exact queued owner/group without recovery and refuse offers or uncertain reads. Completion requires an exact removal acknowledgement, never merely `!ready`; a retained acknowledgement permits local cleanup only, not replay. Same-process session replacement may reconstruct the claim and the original process may settle after transport ownership moves; a foreign process may neither replay nor settle it through generic removal or a copied acquisition id.
|
|
92
|
-
- Startup and elapsed time are not owner-death proof; queued authority has no time lease. Dead-owner cleanup groups the complete receipt and transactionally rechecks pid liveness plus process-birth identity: only an absent PID or mismatched stable Linux/macOS birth proof discards all session-owned sources without replay; a matching proof is `alive`, while
|
|
92
|
+
- Startup and elapsed time are not owner-death proof; queued authority has no time lease. Dead-owner cleanup groups the complete receipt and transactionally rechecks pid liveness plus process-birth identity: only an absent PID or mismatched stable Linux/macOS/Windows birth proof discards all session-owned sources without replay; a matching proof is `alive`, while inaccessible birth metadata is `unverifiable`, and both non-dead outcomes keep authority queued. Under actual `A`–`Z` allocation pressure, retirement may invoke that journal-owned CAS only for a current inactive binding after complete strict source inspection, no local work/live owner/delivery authority, whole unoffered receipt groups, and a preflight proving every grouped owner dead. It must then recapture all protection before preparing deletion; partial progress never grants deletion authority. The live-transfer contract is authenticated offer → exact-generation bounded payload staging → recipient CAS acceptance → exact receipt-and-owner ACK → donor removal → recipient readiness. The offer freezes donor settlement/recovery; controls rebuild local closures; negative/mismatched pre-acceptance ACK cancels only an unaccepted offer and retains donor work; a lost post-acceptance ACK cannot cancel recipient authority and leaves donor memory frozen for explicit reconciliation.
|
|
93
93
|
- Execution failures persist bounded diagnostics and attempt state as `retry-wait`, except that an exact Telegram HTTP 400 stale/deleted-thread API failure with a proven `{chatId, threadId}` terminally settles the currently executing source after best-effort shared binding invalidation. Automatic retry continues indefinitely with exponential `1s → 2s → 4s → 8s → 16s → 32s → 60s` delay capped at 60 seconds; later independent updates continue draining, durable authority is never silently discarded, and legacy `failed` entries resume automatically at startup. Snapshot-plus-segment journals compact only after 256 unapplied revisions or 4 MiB; snapshot-first cleanup tolerates redundant segments, and empty authority may atomically rebind bot/profile identity. Missing snapshots left by the retired broad temp cleanup rebuild only from a complete provably empty segment chain, while revisionless snapshots may recover from a validated later segment predecessor; otherwise the transaction-locked reset deletes damaged segments before publishing a fresh journal, with accepted input loss and informational evidence, not a quarantine copy. Unsupported versions remain untouched; strict protection reads never reset. Startup best-effort removes obsolete current-runtime root/session `recovery/` folders without touching pre-0.52.0 storage.
|
|
94
94
|
- Business connection chats are a separate namespace even when their chat/message IDs match bot-chat IDs. Default DM routing must never infer private-queue deletion intent from `deleted_business_messages`; raw companion handlers remain separate owners.
|
|
95
|
-
- Clock-bearing chooser sources survive cold spending until their fixed deadline; new-world forgetting preserves their positively unbound temporary frame
|
|
95
|
+
- Clock-bearing chooser sources survive cold spending until their fixed deadline. A restarted owner revives one in place only while its saved phase is still `waiting`, its recorded chooser location is known, its tab stays unbound and live threads exist; that waiting phase is the proof no selection or effect ran. Selected, expired or bound sources keep the protective hold, and new-world forgetting preserves their positively unbound temporary frame. Expiry is a donor discard, not recipient cancellation or source execution completion. Confirmed expiry releases worker custody before UI/cleanup awaits; metadata retries retain no prompt bodies and cannot repeat an issued delete.
|
|
96
96
|
- An unresolved reaction delays only the exact governed queue item identified by chat/message sources, not unrelated queue work. An explicit immutable `preApprovalExcluded: true` cannot govern accepted work, even after re-pairing; missing or false exclusion evidence must not bypass the dependency guard. Prepared v3 drain may dispose of excluded pending input only through journal-owned `removeExcluded`, never generic raw completion; mixed requests containing non-excluded input must fail atomically. Queue receipt publication follows in-memory append and precedes dispatch request; receipt-bearing turns remain queued until every exact source commits.
|
|
97
97
|
- The detailed implementation and release gates live in [`docs/architecture.md`](./docs/architecture.md), [`docs/multi-instance-bus.md`](./docs/multi-instance-bus.md), and [`BACKLOG.md`](./BACKLOG.md).
|
|
98
98
|
|
|
@@ -112,7 +112,7 @@ Use the relevant local skill before non-trivial work in its domain. Keep skill o
|
|
|
112
112
|
|
|
113
113
|
The detailed map is canonical in [`docs/architecture.md`](./docs/architecture.md). This index is only for routing work:
|
|
114
114
|
|
|
115
|
-
- `queue`, `runtime`, `lifecycle`, `locks`: Scheduling, session coordination, lifecycle, and
|
|
115
|
+
- `queue`, `runtime`, `lifecycle`, `locks`, `process-identity`: Scheduling, session coordination, lifecycle, locking, and process liveness proofs.
|
|
116
116
|
- `api`, `polling`, `bus*`, `ownership`, `target`, `sync`, `thread-reconciler`, `threads`, `updates`, `routing`, `media`, `turns`, `inbound`, `config`, `setup`: Telegram transport, profiles, durable admission, routing, and inbound flow.
|
|
117
117
|
- `preview`, `replies`, `rendering`, `keyboard`, `delivery`, `activity`, `outbound*`, `voice`, `status`: Response and delivery surfaces.
|
|
118
118
|
- `commands`, `menu*`, `model`, `prompts`: Controls and application-menu UI; core queue mechanics remain in `queue`.
|
|
@@ -122,6 +122,7 @@ The detailed map is canonical in [`docs/architecture.md`](./docs/architecture.md
|
|
|
122
122
|
## 6. Public And Integration Boundaries
|
|
123
123
|
|
|
124
124
|
- Companion extensions use documented package subpaths such as `@llblab/pi-telegram/sections`, `/delivery`, `/voice`, `/inbound`, `/outbound`, and `/updates`; never import `lib/*.ts`.
|
|
125
|
+
- `Producer-owned interactions`: Extensions that own questions or approvals implement optional Telegram support on their side through public companion APIs and retain normal local behavior when Telegram is unavailable. The producer owns correlation, answer validation, action/target context, authorization, expiry/cancellation and one-shot settlement. Core must not hard-code third-party tool names, schemas or event channels, or generically mirror/resolve arbitrary local UI dialogs; a versioned producer event does not transfer that responsibility. A question answer alone is not approval authority.
|
|
125
126
|
- Low-level handler buses have no caller-supplied ids; high-level registries use stable identities. Imperative delivery resolves the current runtime on every call and returns generation-bound logical handles rather than captured Pi contexts.
|
|
126
127
|
- Extension sections receive only documented context ports. They do not access raw bot clients/filesystems or run a second polling loop; unregister on shutdown.
|
|
127
128
|
- Unknown callback data may reach extension handlers only after built-in namespaces decline it. Follow [`docs/callback-namespaces.md`](./docs/callback-namespaces.md).
|
|
@@ -137,7 +138,7 @@ The detailed map is canonical in [`docs/architecture.md`](./docs/architecture.md
|
|
|
137
138
|
- Keep interfaces consistent with their owning exported contract. Use local structural `*Like`/view types only for deliberate narrow projections, not duplicate source-of-truth models.
|
|
138
139
|
- Remove dead code immediately. Reachability from composition roots, public exports, tests, registered surfaces, and documented APIs—not recent usefulness—determines whether code is live. `node scripts/audit-exports.mjs` provides a read-only TypeScript-symbol inventory including aliases, JS consumers and public `api/` reexports (including namespaces); `namespaceEscapes` flags whole-module alias usage. No-external-reference candidates are not deletion proof. Review generated text, dynamic namespace access and public declaration reachability before removing or hiding a symbol.
|
|
139
140
|
- Treat every meaningful `lib/extension.ts` edit as a composition-pressure check, but keep one-off live adapter wiring there when extraction would only hide cross-domain state.
|
|
140
|
-
- Follow [`docs/ui-style.md`](./docs/ui-style.md) for interface copy, emoji, buttons, menus, and dialogs. Update the registry before assigning a new UI emoji meaning. Standalone notices use one fully bold emoji-led sentence with a terminal period; menu or chooser headings use the same hierarchy with a terminal colon. Material names may add nested italic emphasis without breaking the outer bold span.
|
|
141
|
+
- Follow [`docs/ui-style.md`](./docs/ui-style.md) for interface copy, emoji, buttons, menus, and dialogs. Update the registry before assigning a new UI emoji meaning. Standalone notices use one fully bold emoji-led sentence with a terminal period; menu or chooser headings use the same hierarchy with a terminal colon. Material names may add nested italic emphasis without breaking the outer bold span. Callback toasts are fleeting plain text with no emoji and no terminal sentence period (question/exclamation marks and ellipses remain), written in that final form at their call site; no delivery boundary rewrites punctuation, and navigation taps answer silently. In-chat notices and explicit raw API payloads keep their punctuation.
|
|
141
142
|
- Markdown lists never contain blank lines between adjacent items; list items are not paragraphs. Use blank lines only between paragraphs or independently separated blocks. Markdown tables use compact source formatting with `---` separator cells and one surrounding space per cell. Preserve vendored references unchanged.
|
|
142
143
|
- Treat Windows filesystem, named-pipe, lock, heartbeat, and atomic-rename reports as high-signal evidence; reduce them to regressions or explicit platform caveats.
|
|
143
144
|
- Route significant runtime failures through the redacted recent-event recorder. Keep the compact TUI status at generic `error`; details belong in diagnostics.
|
|
@@ -159,6 +160,7 @@ While working:
|
|
|
159
160
|
- Read large artifacts search-first and range-bounded. For `CHANGELOG.md`, inspect only the current release section unless older history is relevant.
|
|
160
161
|
- Keep successful validation output compact; inspect focused failure tails. Prefer focused tests/typecheck during iteration and broad validation at a stable gate.
|
|
161
162
|
- Preserve unrelated work and do not commit, publish, tag, deploy, or perform external actions without explicit authorization.
|
|
163
|
+
- `Contributor integration`: Integrate accepted contributions into current `dev`, preserve author commits with merge rather than squash/rebase, and add maintainer adaptations separately. Verify actual contributor-branch push permission before editing or retargeting; otherwise use a maintainer branch rooted at the contributor head. Contributor claims do not replace current-baseline regression and CI evidence.
|
|
162
164
|
|
|
163
165
|
Before completion:
|
|
164
166
|
|
package/BACKLOG.md
CHANGED
|
@@ -2,21 +2,4 @@
|
|
|
2
2
|
|
|
3
3
|
_This file owns unresolved project work only. Completed behavior belongs in `CHANGELOG.md`; durable contracts belong in `AGENTS.md` and `/docs`._
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
- Release boundary: The operator authorized the hotfix on native regression and CI evidence; real Telegram smoke/fault acceptance remains open below, not a claimed completed check. Installing/reloading Pi or injecting live faults needs separate authorization.
|
|
8
|
-
- CI boundary: Native Linux/macOS/Windows checks exercise capped poll/admission backoff, preserved persistent-conflict stand-down and generation-fenced manual disconnect. Existing skipped strict journal witnesses remain a separate Windows boundary; green CI does not replace operator acceptance.
|
|
9
|
-
- [ ] Obtain operator acceptance for ordinary disconnect, unconfirmed-cleanup reporting and outage recovery without manual reconnect on a disposable setup. Local fake-transport/storage regressions do not certify real Telegram or platform fault behavior; installation/reload and live fault injection require separate authorization.
|
|
10
|
-
- Acceptance: Captured manual stop never affects a replacement connection or bypasses Thread deletion fences, uncertain stop is not retried, and accepted local work is preserved. Session-restart cleanup remains admission-required.
|
|
11
|
-
|
|
12
|
-
## Windows Reliability
|
|
13
|
-
|
|
14
|
-
- [ ] Strengthen native Windows reliability beyond the 0.52.0 ordinary-queue fix. Audit filesystem paths, named pipes, locking, owner fencing, heartbeat and atomic publication; cover interrupted writes, damaged state, ownership changes and lost-ACK retries with focused regressions.
|
|
15
|
-
- [ ] Investigate Windows-compatible strict journal evidence for Restore and cleanup without weakening source identity, scoped ACK or fail-closed protections. Inventory witnesses currently skipped on Windows and enable them only when equivalent native evidence exists.
|
|
16
|
-
- Acceptance: Local synthetic tests distinguish ordinary delivery from strict proofs; native Windows CI exercises supported paths and refusal boundaries. Linux emulation or a green matrix with skipped Restore witnesses does not establish Windows Restore acceptance.
|
|
17
|
-
|
|
18
|
-
## Consistent Callback Notification Copy Across Other Extensions
|
|
19
|
-
|
|
20
|
-
- [ ] Inventory Telegram button callback acknowledgements across all local extensions, including companion callbacks. Standardize the transient non-interactive notification bubbles (callback answers/toasts) to concise, consistent text with no trailing period.
|
|
21
|
-
- [ ] Distinguish transient callback bubbles from in-chat notices, chooser headings and blocking alerts; reconcile the owning UI-copy contracts before implementation rather than stripping punctuation from every message.
|
|
22
|
-
- Acceptance: All participating extensions follow the same callback-notification format, with focused copy checks for success, no-op and rejection outcomes; in-chat notice formatting and callback behavior remain unchanged.
|
|
5
|
+
No open work.
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,17 @@
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.53.0: Live Thread rebinding, routing tabs and Windows support
|
|
8
|
+
|
|
9
|
+
- `Live-rebind state`: Atomic same-session Workspace binding/operation publication, fenced recipient application and bounded one-shot cleanup scheduling. Failed/unknown cleanup keeps the new binding without blocking the next attempt; chooser retirement keeps saved inputs. Shared-state retries recheck captured authority before every rename, so a revoked grant cannot overwrite retained state.
|
|
10
|
+
- `Live-input preparation`: Fresh recipients hold groups before append; leaders reuse unsettled originals without copy or reexecution. Saved singleton readiness binds only its genuine warm carrier; cold/replaced/discarded sources cannot mint ACKs. Both roles use exact queue scopes, receipt/readiness publication and native completion observation. Caller/worker authority gates release; failed copies use fenced disposal. Queue acceptance is never removal, replay or another queue grant.
|
|
11
|
+
- `Live-rebind peers`: Both-role local wiring preserves exact donor CAS and detached recipient delivery. Non-ordering Commands work protects held rendering/API and unknown effects after disposal/rebinding; cleanup refuses pending/unknown work. Only the explicitly marked, read-only recipient work observation may outlive source custody; generic observation cannot revive it. Authenticated IPC and both-role cleanup witnesses stay distinct from activation and actual Pi/client acceptance.
|
|
12
|
+
- `Staged live routing`: Prompt and supported command selections share save/apply/release through native owners. Albums preserve grouped receipts and unrelated input across busy work/lost replies. Binding/claim conflicts and journal/profile-key drift refuse without replay or rollback; other bindings/profiles stay exact. Templates and `/continue` retain no-fold admission.
|
|
13
|
+
- `Command authority`: Registry plans retain both-role custody, template identity, exact ACKs, no-fold receipts, donor CAS and independent delivery. Retired facades are removed; inspection is private behind `settle`, and ordinary command/prompt dispatch returns one owned function. Control assembly uses Queue contracts/controller and preserves append → report → dispatch. Target projection preserves field absence. Native abort retires heavy receipt-index records to refusal markers, never fallback.
|
|
14
|
+
- `Handler and menu fixes`: Command-template `when` skips a falsy handler (selection falls through) and guarded steps, including enclosing-node guards; `recover` runs only between failed retries. Thinking-menu buttons refuse during voice replies. Every All-tab input, albums included, gets a 🚦 Routing tab; Cancel shows a notice and removes it right away, even if slow or other tabs wait; a consumed menu-picked command also leaves All. Route menus list 🧵 threads; restart revives a waiting chooser.
|
|
15
|
+
- `UI and coverage`: Toasts are plain text without emoji or a final period, sent as written; menu and routing-tab navigation answers silently; list items use `<code>-</code>`. TUI status keeps acknowledged Thread-name casing. Real Pi SDK fixtures cover queued prompts/buttons across held tools and compaction; damaged-state fixtures cover invalid admission and outbound delivery. Unsupported extension selection preserves originals and bindings. Stale downgrade and Thread-store measurement scripts are removed.
|
|
16
|
+
- `Windows`: Restore, live rebinding, cleanup census and scoped queue ACKs run on Windows through the same strict journal reads, guarded by handle identity where no-follow opens are missing. Process birth is proven from creation ticks, so reused PIDs no longer pin dead owners' work. Workspace and app file replacement retries sharing violations, follower control replies get the 8-second liveness window, and CI runs every suite on Windows.
|
|
17
|
+
|
|
7
18
|
## 0.52.2: Polling recovery, safe disconnect and Windows downloads
|
|
8
19
|
|
|
9
20
|
- `Polling recovery`: Non-conflict poll/admission failures back off from 1 to 30 seconds and keep retrying, allowing recovery after prolonged outages without manual reconnect. Backoff resets only after durable admission succeeds; persistent competing-client conflicts retain their existing terminal stand-down.
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
It is a **runtime adapter**, not a remote terminal. Start or supervise work in the Pi TUI, then continue from Telegram while away from the keyboard. In Threaded Mode, a durable Workspace binding uses Pi's stable public session identity to restore the same Telegram Thread when that session resumes; live instance ownership and the exact Telegram target still authorize routing. The bridge preserves Pi session semantics instead of pretending Telegram is a PTY, shell, process launcher, or session browser. That boundary is the product: Telegram gets safe runtime handles, not raw terminal power.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
While connected, Telegram receives intermediate updates and final replies from local Pi work as well as Telegram-originated turns. The `Activity` setting controls whether thinking and tool details are shown. See [Outbound](docs/outbound.md#public-assistant-output) for delivery behavior.
|
|
12
12
|
|
|
13
13
|
This repository is an actively maintained standalone fork of [`badlogic/pi-telegram`](https://github.com/badlogic/pi-telegram). It started from upstream commit [`cb34008`](https://github.com/badlogic/pi-telegram/commit/cb34008460b6c1ca036d92322f69d87f626be0fc) and has since diverged substantially.
|
|
14
14
|
|
|
@@ -59,16 +59,10 @@ Paste the bot token. If `~/.pi/agent/telegram.json` already contains a saved tok
|
|
|
59
59
|
|
|
60
60
|
The connected Pi instance owns Telegram polling. Use `/telegram-connect <profile>` to activate a named profile. Each profile is a parallel bot runtime with isolated polling, diagnostics, Threaded Mode state, and local bus transport; the `default` profile keeps unsuffixed runtime paths. In classic mode each profile uses a singleton lock. When Telegram private-chat Threaded Mode is available, one live instance becomes the profile's leader and later visible Pi instances register as followers. Reopening or resuming the same Pi session restores its remembered Thread at session startup. If `/resume` immediately follows `/telegram-connect`, or the bridge is already connected, connection intent follows the switch: the destination restores its own Thread/slot or receives a new binding, never the source session's Thread. Otherwise, a distinct unbound session still requires explicit `/telegram-connect`.
|
|
61
61
|
|
|
62
|
-
An eligible single-text unbound Thread chooser offers **⛔️ Cancel routing**: the original stays private and is not sent to Pi; its Telegram message and Thread are not deleted. This does not cancel already selected or accepted work. When protected cancellation attempts remain, **Status → ❌ Pending cancellations** lets the owner review and finish them, including after restart. Historical unbound startup text has no main-menu entry or historical-review submenu. Eligible raw unsupported command/media/unknown historical private-Thread originals use a separate retain-only path, including on bound Threads without surviving Restore/temporary membership. Each worker generation reclassifies them after cold forgetting, blocking handler replay, generic Cancel and cold spending. Ordinary bound startup dispatch stops for these originals; same-kind live arrivals and owner/custody paths remain unchanged. Old historical-review buttons are refused without routing or abandoning the original; removal of the UI does not cancel previously accepted work.
|
|
63
|
-
|
|
64
|
-
Eligible routing choosers and their donor sources share a **fixed 60-minute lifetime** from the first acknowledged publication, preserved across refresh/restart. Expiry drops unselected or unacknowledged routing attempts without privately archiving prompt bodies; nondelivery is accepted, so resend manually if needed. Old controls and late replies cannot replay the source. An expired disposable unbound tab is removed after all its owned groups resolve and fresh protection checks pass. Accepted queues, independent work and restored bound Threads remain intact. Confirmed expiry releases worker custody even if tab metadata fails; retries use only body-free tab/source proof, and uncertain deletion is never repeated. Explicit Cancel retains its separate private-original policy. Historical originals without a saved clock and unconfirmed publication receive no invented deadline. Local candidate checks do not certify live deletion.
|
|
65
|
-
|
|
66
62
|
`/telegram-connect` keeps working when the shared runtime `state.json` is damaged. If it is malformed, foreign or contains invalid evidence, the connecting leader replaces it with a fresh empty state and continues. Every profile then forgets old Thread bindings, slots, unfinished Restore/temporary work and polling continuity; `telegram.json`, Pi history and the released `tmp/telegram` are untouched. Permission errors still stop connection. See [Damaged-State Reset](./docs/architecture.md#damaged-state-reset-operator-approved).
|
|
67
63
|
|
|
68
64
|
The prompt queue is session-local. Before a reload or restart, let it drain or knowingly accept losing waiting work and re-send what still matters afterwards; nothing is replayed automatically.
|
|
69
65
|
|
|
70
|
-
Journal recovery is a separate existing loss policy, not shared-envelope recovery. A snapshot removed by older broad temp cleanup is rebuilt when its complete segment history proves an empty result, while a revisionless snapshot is repaired from the first surviving segment's exact predecessor when the reconstructed tail validates. Otherwise the damaged snapshot and segments are deleted (their inputs are lost), a fresh journal is published, and startup continues with an informational diagnostic. Unsupported journal versions block recovery without rewriting or quarantining retained files; use a compatible runtime rather than deleting journals. Saved `telegram.json` configuration and runtime diagnostics are outside that journal reset. Startup best-effort removes obsolete current-runtime root/session `recovery/` folders, so those directories are not a preservation location.
|
|
71
|
-
|
|
72
66
|
Ordinary network or admission failures use exponential retry pauses capped at 30 seconds, without permanently stopping after an arbitrary attempt count. The poller recovers when the outage clears. Explicit `/telegram-disconnect` still attempts to stop its captured local transport if Thread cleanup is unavailable; it reports unconfirmed cleanup rather than successful deletion, and an old disconnect cannot stop a new connection. Incomplete stop/release remains visible; check `/telegram-status --debug` and keep Pi open.
|
|
73
67
|
|
|
74
68
|
Persistent competing `getUpdates` clients cause a bounded transport stand-down rather than endless retries. Accepted local work remains queued/executable, but Telegram delivery stops. Inspect `/telegram-status --debug`, stop the competing client, then reconnect. See [Runtime Ownership](./docs/architecture.md#runtime-ownership).
|
|
@@ -239,7 +233,7 @@ Voice notes, audio, images, PDFs, and other media can pass through configured in
|
|
|
239
233
|
|
|
240
234
|
### Buttons And Callbacks
|
|
241
235
|
|
|
242
|
-
Assistant replies can
|
|
236
|
+
Assistant replies can include native prompt buttons between paragraphs or in a footer. Clicking a button queues its prompt or invokes its owning extension action. See the [button syntax and examples](./docs/public-api.md#tools-and-assistant-authored-actions).
|
|
243
237
|
|
|
244
238
|
### Threaded Mode And Multi-Instance Bus
|
|
245
239
|
|
|
@@ -251,7 +245,7 @@ Classic private DM mode is the base product mode. When Telegram private-chat Thr
|
|
|
251
245
|
- Queued work for a live follower transfers through authenticated exact-journal handoff rather than replaying under the transport owner.
|
|
252
246
|
- Reopening, resuming, or replacing the process for the same Pi session restores its remembered Thread; distinct sessions in one directory keep independent bindings and letter slots.
|
|
253
247
|
- Unknown threads are preserved and offered explicit reroute/restore choices.
|
|
254
|
-
- When all `A`–`Z` slots are reserved, a
|
|
248
|
+
- When all `A`–`Z` slots are reserved, a new connection may reclaim the oldest inactive, unprotected Thread. **This deletes that Thread and all its messages**, not Pi session history or project files. Active or protected work blocks reclamation; restoring a remembered session does not evict another binding.
|
|
255
249
|
- Telegram never launches hidden Pi processes.
|
|
256
250
|
|
|
257
251
|
In Threaded Mode, open Settings → **🧵 Thread display** to choose `letters` (default), `names`, `directory-title`, or `directory-snake` for this bot profile; a retained **directories** value remains available as an unchanged legacy presentation. Fresh tabs are created with the active mode's title instead of being visibly renamed afterward. Telegram tab titles, Pi terminal status, live Thread choosers/notices, prompt attribution, and named `telegram_message` targeting use the same acknowledged display name; target IDs and live registrations still own routing. Names shows the generated dictionary name chosen for the slot, such as `Anchor` for slot `A`; the directory formats produce `api_tools` or `Api Tools` and append `_a` or ` A` only while two or more authenticated live instances share that exact directory. Dormant retained bindings do not keep those suffixes visible. After leader election, automatic title contraction waits one follower-staleness window for live registrations to settle, avoiding a temporary suffix removal and restoration during startup. `/name` sets a manual Thread display name; **Reset to automatic** restores the selected automatic projection. Switching preserves Thread IDs, slots, generated recovery identity, and queue ownership. Partial application reports an error and can be retried without recreating Threads.
|
|
@@ -261,6 +255,8 @@ In Threaded Mode, open Settings → **🧵 Thread display** to choose `letters`
|
|
|
261
255
|
| Classic DM | One running Pi instance and its active session controlled from one private bot chat | One polling owner, one queue/runtime surface |
|
|
262
256
|
| Threaded Mode | Several visible Pi sessions sharing one bot | One leader owns transport; each private-chat Thread retains one session-qualified Workspace binding |
|
|
263
257
|
|
|
258
|
+
**Live rebinding (0.53.0):** Restore uses one same-session save/apply/release channel for prompts and supported commands/templates, with separately fenced best-effort old-Thread cleanup. Existing work and exact source/receipt custody remain protected; legacy Restore readers/receivers retain continuity with older instances. Windows runs the same paths, and CI covers it. See [Multi-Instance Bus](./docs/multi-instance-bus.md#live-rebind-channel).
|
|
259
|
+
|
|
264
260
|
## Environment Configuration
|
|
265
261
|
|
|
266
262
|
Most controls live in Pi commands or the Telegram menu. Environment variables remain for bootstrap and transport boundaries:
|
|
@@ -294,7 +290,7 @@ Stable public entrypoints are documented in [Public API](./docs/public-api.md),
|
|
|
294
290
|
|
|
295
291
|
## Safety Boundaries
|
|
296
292
|
|
|
297
|
-
Durable inbound admission is a **process-crash recovery** guarantee. Atomic private-file replacement preserves acknowledged journal authority and its journal-owned `acceptedThroughUpdateId` polling cursor across ordinary process exit, crash, kill, and replacement, but the extension does not flush files or parent directories for host/kernel/filesystem/device/power-loss durability. `telegram.json` contains configuration only. Keep `~/.pi/agent` on appropriately managed storage and backups if that stronger operational guarantee is required.
|
|
293
|
+
Durable inbound admission is a **process-crash recovery** guarantee. Atomic private-file replacement preserves acknowledged journal authority and its journal-owned `acceptedThroughUpdateId` polling cursor across ordinary process exit, crash, kill, and replacement, but the extension does not flush files or parent directories for host/kernel/filesystem/device/power-loss durability. `telegram.json` contains configuration only. Keep `~/.pi/agent` on appropriately managed storage and backups if that stronger operational guarantee is required. Downgrading below `0.37.0` is unsafe: an older runtime cannot recover the journal's polling cursor and could repoll admitted updates. See [Durable Admission And Recovery](./docs/architecture.md#durable-admission-and-recovery).
|
|
298
294
|
|
|
299
295
|
Runtime files use `tmp/pi-telegram` with only `state.json` and `logs.jsonl` at its root; pre-0.52.0 `tmp/telegram` is left untouched and never migrated. The transport section of `state.json` names a polling journal hosted in a session folder; successor leaders continue it rather than moving its cursor. Follower journals are session-owned. Unbound session families are disposable under the approved sweep policy; in-process `/new` adopts eligible pending input, while cold startup does not. See [Session-Owned Journal Storage](./docs/multi-instance-bus.md#session-owned-journal-storage) for compatibility fallbacks, loss boundaries and pending acceptance.
|
|
300
296
|
|
|
@@ -309,7 +305,7 @@ Runtime files use `tmp/pi-telegram` with only `state.json` and `logs.jsonl` at i
|
|
|
309
305
|
|
|
310
306
|
Telegram is a companion surface around a live Pi runtime, not a second runtime. It can compact the current session, and `/new` uses Pi's public session API to start a new session in the current classic chat or Thread once the bridge is idle. Resuming, forking, browsing, and switching other sessions remain Pi TUI operations, not Telegram navigation.
|
|
311
307
|
|
|
312
|
-
|
|
308
|
+
Telegram prompts run in the active Pi session and use its current context, just like prompts sent from the terminal.
|
|
313
309
|
|
|
314
310
|
## Documentation Map
|
|
315
311
|
|
package/api/commands.ts
CHANGED
package/dist/api/commands.d.ts
CHANGED
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
* Zones: package boundary, extension interop
|
|
4
4
|
* Exposes the stable Telegram slash-command registration surface while keeping registry internals package-private
|
|
5
5
|
*/
|
|
6
|
-
export { registerTelegramCommand, type TelegramExtensionCommandContext, type TelegramExtensionCommandRegistration, } from "../lib/commands.ts";
|
|
6
|
+
export { registerTelegramCommand, type PreparedSelectedCommand, type SelectedCommandExecution, type SelectedPreparationInput, type TelegramExtensionCommandContext, type TelegramExtensionCommandRegistration, } from "../lib/commands.ts";
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Owns persistent bounded thinking and tool disclosures; excludes activity normalization, assistant answer rendering, and transport authority policy
|
|
5
5
|
*/
|
|
6
6
|
import type { TelegramActivityEvent, TelegramActivityPublicationRuntime } from "./activity.ts";
|
|
7
|
-
import type { TelegramEditMessageTextBody, TelegramInputRichMessage, TelegramSendMessageBody, TelegramSendRichMessageBody, TelegramSentMessage } from "./telegram-api.ts";
|
|
8
7
|
import { type TelegramTarget } from "./target.ts";
|
|
8
|
+
import type { TelegramEditMessageTextBody, TelegramInputRichMessage, TelegramSendMessageBody, TelegramSendRichMessageBody, TelegramSentMessage } from "./telegram-api.ts";
|
|
9
9
|
export declare const TELEGRAM_ACTIVITY_MESSAGE_MAX_TOOLS = 6;
|
|
10
10
|
export declare const TELEGRAM_REASONING_BUFFER_MAX_CHARS = 1200;
|
|
11
11
|
export declare const TELEGRAM_TOOL_UPDATE_MAX_ENTRIES = 4;
|
|
@@ -28,10 +28,6 @@ export interface TelegramActivityVerbosityRuntime {
|
|
|
28
28
|
stop: () => void;
|
|
29
29
|
waitForIdle: () => Promise<void>;
|
|
30
30
|
}
|
|
31
|
-
export interface TelegramActivityVerbosityBinding extends TelegramActivityVerbosityRuntime {
|
|
32
|
-
bind: (runtime: TelegramActivityVerbosityRuntime) => void;
|
|
33
|
-
}
|
|
34
|
-
export declare function createTelegramActivityVerbosityBinding(): TelegramActivityVerbosityBinding;
|
|
35
31
|
export declare function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
|
|
36
32
|
enqueue?: TelegramActivityPublicationRuntime["enqueue"];
|
|
37
33
|
getActivityMode: () => "quiet" | "thinking" | "tools" | "verbose";
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* Zones: telegram activity, rich rendering, operational delivery
|
|
4
4
|
* Owns persistent bounded thinking and tool disclosures; excludes activity normalization, assistant answer rendering, and transport authority policy
|
|
5
5
|
*/
|
|
6
|
-
import { escapeHtml, renderTelegramInlineMarkdownHtml
|
|
7
|
-
import { areTelegramTargetsEqual as targetEquals } from "./target.js";
|
|
6
|
+
import { escapeHtml, renderTelegramInlineMarkdownHtml } from "./rendering.js";
|
|
7
|
+
import { areTelegramTargetsEqual as targetEquals, } from "./target.js";
|
|
8
8
|
const TELEGRAM_ACTIVITY_DETAIL_MAX_CHARS = 1_200;
|
|
9
9
|
const TELEGRAM_ACTIVITY_MESSAGE_MAX_CHARS = 3_900;
|
|
10
10
|
export const TELEGRAM_ACTIVITY_MESSAGE_MAX_TOOLS = 6;
|
|
@@ -129,11 +129,7 @@ function renderToolActivityHtml(tool) {
|
|
|
129
129
|
if (tool.complete && tool.result !== undefined) {
|
|
130
130
|
evidence.push(`"${tool.isError ? "error" : "result"}": ${tool.result}`);
|
|
131
131
|
}
|
|
132
|
-
const status = tool.complete
|
|
133
|
-
? tool.isError
|
|
134
|
-
? "failed"
|
|
135
|
-
: "done"
|
|
136
|
-
: "running";
|
|
132
|
+
const status = tool.complete ? (tool.isError ? "failed" : "done") : "running";
|
|
137
133
|
return [
|
|
138
134
|
`<b>${escapeHtml(formatToolActivityLabel(tool.name))}:</b> <code>${status}</code>`,
|
|
139
135
|
`<blockquote expandable>${escapeActivityEvidenceHtml(evidence.join("\n\n"))}</blockquote>`,
|
|
@@ -151,11 +147,7 @@ function createToolActivityDetail(summary, text, isOpen = false) {
|
|
|
151
147
|
};
|
|
152
148
|
}
|
|
153
149
|
function renderToolActivityRichBlocks(tool) {
|
|
154
|
-
const status = tool.complete
|
|
155
|
-
? tool.isError
|
|
156
|
-
? "failed"
|
|
157
|
-
: "done"
|
|
158
|
-
: "running";
|
|
150
|
+
const status = tool.complete ? (tool.isError ? "failed" : "done") : "running";
|
|
159
151
|
const evidenceBlocks = [
|
|
160
152
|
createToolActivityDetail("arguments", tool.args, true),
|
|
161
153
|
];
|
|
@@ -194,31 +186,11 @@ function toolMessageSize(tools) {
|
|
|
194
186
|
return renderTelegramToolActivityHtml(tools).length;
|
|
195
187
|
}
|
|
196
188
|
function isKnownSafeRichActivityRejection(error) {
|
|
197
|
-
return error instanceof Error && /HTTP 400: Bad Request:/i.test(error.message);
|
|
189
|
+
return (error instanceof Error && /HTTP 400: Bad Request:/i.test(error.message));
|
|
198
190
|
}
|
|
199
191
|
export function renderTelegramThinkingActivityHtml(text) {
|
|
200
192
|
return `<blockquote expandable>${renderThinkingActivityEvidenceHtml(text)}</blockquote>`;
|
|
201
193
|
}
|
|
202
|
-
export function createTelegramActivityVerbosityBinding() {
|
|
203
|
-
let runtime;
|
|
204
|
-
return {
|
|
205
|
-
bind(next) {
|
|
206
|
-
runtime = next;
|
|
207
|
-
},
|
|
208
|
-
accept(event) {
|
|
209
|
-
runtime?.accept(event);
|
|
210
|
-
},
|
|
211
|
-
reset() {
|
|
212
|
-
runtime?.reset();
|
|
213
|
-
},
|
|
214
|
-
stop() {
|
|
215
|
-
runtime?.stop();
|
|
216
|
-
},
|
|
217
|
-
waitForIdle() {
|
|
218
|
-
return runtime?.waitForIdle() ?? Promise.resolve();
|
|
219
|
-
},
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
194
|
export function createTelegramActivityVerbosityRuntime(deps) {
|
|
223
195
|
let active = true;
|
|
224
196
|
let generation = 0;
|
|
@@ -261,7 +233,10 @@ export function createTelegramActivityVerbosityRuntime(deps) {
|
|
|
261
233
|
toolOrder.length = 0;
|
|
262
234
|
};
|
|
263
235
|
const hasAuthority = () => authority !== undefined && deps.isAuthorityActive(authority);
|
|
264
|
-
const isCurrent = (acceptedGeneration, admittedAuthority) => active &&
|
|
236
|
+
const isCurrent = (acceptedGeneration, admittedAuthority) => active &&
|
|
237
|
+
generation === acceptedGeneration &&
|
|
238
|
+
admittedAuthority !== undefined &&
|
|
239
|
+
deps.isAuthorityActive(admittedAuthority);
|
|
265
240
|
const ensureActivity = (event, admittedTarget, admittedAuthority) => {
|
|
266
241
|
if (deps.getActivityMode() === "quiet")
|
|
267
242
|
return false;
|
|
@@ -341,9 +316,7 @@ export function createTelegramActivityVerbosityRuntime(deps) {
|
|
|
341
316
|
const scheduleReasoningPublish = (event, acceptedGeneration) => {
|
|
342
317
|
if (reasoningFlushTimer || reasoningBlocked)
|
|
343
318
|
return;
|
|
344
|
-
const elapsed = reasoningMessageFrames === 0
|
|
345
|
-
? 0
|
|
346
|
-
: getNowMs() - lastReasoningPublishMs;
|
|
319
|
+
const elapsed = reasoningMessageFrames === 0 ? 0 : getNowMs() - lastReasoningPublishMs;
|
|
347
320
|
const delayMs = Math.max(0, TELEGRAM_REASONING_MIN_INTERVAL_MS - elapsed);
|
|
348
321
|
const schedule = deps.setReasoningTimeout ?? setTimeout;
|
|
349
322
|
reasoningFlushTimer = schedule(() => {
|
|
@@ -580,8 +553,7 @@ export function createTelegramActivityVerbosityRuntime(deps) {
|
|
|
580
553
|
}
|
|
581
554
|
if (event.type === "agent-end" || event.type === "agent-settled") {
|
|
582
555
|
clearReasoningFlushTimer();
|
|
583
|
-
if (reasoningChars > lastReasoningMessageChars &&
|
|
584
|
-
!reasoningBlocked) {
|
|
556
|
+
if (reasoningChars > lastReasoningMessageChars && !reasoningBlocked) {
|
|
585
557
|
await publishReasoning(event, acceptedGeneration);
|
|
586
558
|
}
|
|
587
559
|
if (!isCurrent(acceptedGeneration, admittedAuthority))
|
|
@@ -599,11 +571,12 @@ export function createTelegramActivityVerbosityRuntime(deps) {
|
|
|
599
571
|
const admittedAuthority = deps.captureAuthority();
|
|
600
572
|
const enqueue = deps.enqueue ?? ((task) => tail.then(task));
|
|
601
573
|
tail = enqueue(async () => {
|
|
602
|
-
if (!active ||
|
|
574
|
+
if (!active ||
|
|
575
|
+
generation !== acceptedGeneration ||
|
|
576
|
+
!deps.isAuthorityActive(admittedAuthority))
|
|
603
577
|
return;
|
|
604
578
|
await process(event, acceptedGeneration, admittedTarget, admittedAuthority);
|
|
605
|
-
})
|
|
606
|
-
.catch((error) => {
|
|
579
|
+
}).catch((error) => {
|
|
607
580
|
deps.recordFailure?.("tool-send", event, error);
|
|
608
581
|
});
|
|
609
582
|
},
|
package/dist/lib/activity.d.ts
CHANGED
|
@@ -95,6 +95,7 @@ export declare function registerTelegramActivityHandler(registration: TelegramAc
|
|
|
95
95
|
export declare function clearTelegramActivityHandlers(): void;
|
|
96
96
|
/** @internal */
|
|
97
97
|
export interface TelegramActivityDispatcher {
|
|
98
|
+
hasPending?: () => boolean;
|
|
98
99
|
dispatch: (event: TelegramActivityEvent) => void;
|
|
99
100
|
stop: () => void;
|
|
100
101
|
}
|
|
@@ -151,6 +152,7 @@ export type TelegramAssistantStreamEvent = {
|
|
|
151
152
|
};
|
|
152
153
|
/** @internal */
|
|
153
154
|
export interface TelegramActivityRuntime {
|
|
155
|
+
hasPending?: () => boolean | undefined;
|
|
154
156
|
onSessionStart?: () => void;
|
|
155
157
|
recordInputSource: (source: TelegramActivityInputSource) => void;
|
|
156
158
|
onAgentStart: (activeTelegramTarget?: TelegramActivityTarget, replyToMessageId?: number) => void;
|
|
@@ -193,9 +195,16 @@ export interface TelegramActivityPublicationReservation {
|
|
|
193
195
|
publish: (task: () => Promise<void>) => Promise<void>;
|
|
194
196
|
cancel: () => void;
|
|
195
197
|
}
|
|
198
|
+
export interface TelegramActivityPublicationWork {
|
|
199
|
+
settle(outcome: "settled" | "unconfirmed"): void;
|
|
200
|
+
}
|
|
196
201
|
export interface TelegramActivityPublicationRuntime {
|
|
197
202
|
enqueue: (task: () => Promise<void>) => Promise<void>;
|
|
198
203
|
reserve: () => TelegramActivityPublicationReservation;
|
|
204
|
+
hasPending: () => boolean;
|
|
205
|
+
/** Observation only; detached work never occupies the ordered publication tail. */
|
|
206
|
+
beginWork: () => TelegramActivityPublicationWork;
|
|
207
|
+
hasUnconfirmed: () => boolean;
|
|
199
208
|
reset: () => void;
|
|
200
209
|
}
|
|
201
210
|
export declare function createTelegramActivityPublicationRuntime(): TelegramActivityPublicationRuntime;
|
package/dist/lib/activity.js
CHANGED
|
@@ -91,16 +91,14 @@ function createTelegramActivityContext(event, isActive) {
|
|
|
91
91
|
};
|
|
92
92
|
}
|
|
93
93
|
function canCoalesceActivityEvents(previous, next) {
|
|
94
|
-
if (previous.activityId !== next.activityId ||
|
|
95
|
-
previous.type !== next.type) {
|
|
94
|
+
if (previous.activityId !== next.activityId || previous.type !== next.type) {
|
|
96
95
|
return false;
|
|
97
96
|
}
|
|
98
97
|
if (previous.type === "assistant-text-delta" &&
|
|
99
98
|
next.type === "assistant-text-delta") {
|
|
100
99
|
return previous.contentIndex === next.contentIndex;
|
|
101
100
|
}
|
|
102
|
-
if (previous.type === "reasoning-delta" &&
|
|
103
|
-
next.type === "reasoning-delta") {
|
|
101
|
+
if (previous.type === "reasoning-delta" && next.type === "reasoning-delta") {
|
|
104
102
|
return previous.contentIndex === next.contentIndex;
|
|
105
103
|
}
|
|
106
104
|
if (previous.type === "tool-update" && next.type === "tool-update") {
|
|
@@ -113,8 +111,7 @@ function coalesceActivityEvents(previous, next) {
|
|
|
113
111
|
next.type === "assistant-text-delta") {
|
|
114
112
|
return { ...next, delta: previous.delta + next.delta };
|
|
115
113
|
}
|
|
116
|
-
if (previous.type === "reasoning-delta" &&
|
|
117
|
-
next.type === "reasoning-delta") {
|
|
114
|
+
if (previous.type === "reasoning-delta" && next.type === "reasoning-delta") {
|
|
118
115
|
return { ...next, delta: previous.delta + next.delta };
|
|
119
116
|
}
|
|
120
117
|
return next;
|
|
@@ -152,6 +149,7 @@ export function createTelegramActivityDispatcher(deps = {}) {
|
|
|
152
149
|
}
|
|
153
150
|
};
|
|
154
151
|
return {
|
|
152
|
+
hasPending: () => Array.from(queues.values()).some((queue) => queue.active && (queue.running || queue.events.length > 0)),
|
|
155
153
|
dispatch(event) {
|
|
156
154
|
if (stopped)
|
|
157
155
|
return;
|
|
@@ -194,6 +192,7 @@ export function createTelegramActivityBridgeRuntime(deps) {
|
|
|
194
192
|
let runtime;
|
|
195
193
|
const getRuntime = () => runtime;
|
|
196
194
|
return {
|
|
195
|
+
hasPending: () => getRuntime()?.hasPending?.(),
|
|
197
196
|
onSessionStart() {
|
|
198
197
|
runtime?.onSessionShutdown();
|
|
199
198
|
runtime = createTelegramActivityRuntime({
|
|
@@ -298,7 +297,9 @@ export function createTelegramActivityRuntime(deps) {
|
|
|
298
297
|
sequence,
|
|
299
298
|
source: activitySource,
|
|
300
299
|
...(activityTarget ? { target: activityTarget } : {}),
|
|
301
|
-
...(activityReplyToMessageId !== undefined
|
|
300
|
+
...(activityReplyToMessageId !== undefined
|
|
301
|
+
? { replyToMessageId: activityReplyToMessageId }
|
|
302
|
+
: {}),
|
|
302
303
|
timestamp: now(),
|
|
303
304
|
};
|
|
304
305
|
try {
|
|
@@ -342,13 +343,15 @@ export function createTelegramActivityRuntime(deps) {
|
|
|
342
343
|
clearActivity();
|
|
343
344
|
};
|
|
344
345
|
return {
|
|
346
|
+
hasPending: () => deps.dispatcher.hasPending?.(),
|
|
345
347
|
recordInputSource(source) {
|
|
346
348
|
pendingInputSource = source;
|
|
347
349
|
},
|
|
348
350
|
onAgentStart(activeTelegramTarget, replyToMessageId) {
|
|
349
351
|
abandonCompaction();
|
|
350
352
|
ensureActivity(activeTelegramTarget);
|
|
351
|
-
activityReplyToMessageId =
|
|
353
|
+
activityReplyToMessageId =
|
|
354
|
+
activitySource === "telegram" ? replyToMessageId : undefined;
|
|
352
355
|
emit({ type: "agent-start" });
|
|
353
356
|
},
|
|
354
357
|
onAssistantEvent(event) {
|
|
@@ -470,12 +473,15 @@ export function createTelegramActivityRuntime(deps) {
|
|
|
470
473
|
export function createTelegramActivityPublicationRuntime() {
|
|
471
474
|
let generation = 0;
|
|
472
475
|
let tail = Promise.resolve();
|
|
476
|
+
let outstanding = 0, unconfirmed = false;
|
|
473
477
|
const pending = new Set();
|
|
474
478
|
const reserve = () => {
|
|
475
479
|
const admittedGeneration = generation;
|
|
476
480
|
let state = "pending";
|
|
477
481
|
let resolve;
|
|
478
|
-
const ready = new Promise((accept) => {
|
|
482
|
+
const ready = new Promise((accept) => {
|
|
483
|
+
resolve = accept;
|
|
484
|
+
});
|
|
479
485
|
const cancel = () => {
|
|
480
486
|
if (state !== "pending")
|
|
481
487
|
return;
|
|
@@ -484,10 +490,15 @@ export function createTelegramActivityPublicationRuntime() {
|
|
|
484
490
|
resolve(undefined);
|
|
485
491
|
};
|
|
486
492
|
pending.add(cancel);
|
|
487
|
-
|
|
493
|
+
outstanding += 1;
|
|
494
|
+
const result = tail
|
|
495
|
+
.then(async () => {
|
|
488
496
|
const task = await ready;
|
|
489
497
|
if (admittedGeneration === generation && task)
|
|
490
498
|
await task();
|
|
499
|
+
})
|
|
500
|
+
.finally(() => {
|
|
501
|
+
outstanding -= 1;
|
|
491
502
|
});
|
|
492
503
|
tail = result.catch(() => { });
|
|
493
504
|
return {
|
|
@@ -507,8 +518,26 @@ export function createTelegramActivityPublicationRuntime() {
|
|
|
507
518
|
return {
|
|
508
519
|
reserve,
|
|
509
520
|
enqueue: (task) => reserve().publish(task),
|
|
521
|
+
hasPending: () => outstanding > 0,
|
|
522
|
+
hasUnconfirmed: () => unconfirmed,
|
|
523
|
+
beginWork() {
|
|
524
|
+
const admittedGeneration = generation;
|
|
525
|
+
let settled = false;
|
|
526
|
+
outstanding += 1;
|
|
527
|
+
return {
|
|
528
|
+
settle(outcome) {
|
|
529
|
+
if (settled)
|
|
530
|
+
return;
|
|
531
|
+
settled = true;
|
|
532
|
+
outstanding -= 1;
|
|
533
|
+
if (outcome === "unconfirmed" && admittedGeneration === generation)
|
|
534
|
+
unconfirmed = true;
|
|
535
|
+
},
|
|
536
|
+
};
|
|
537
|
+
},
|
|
510
538
|
reset() {
|
|
511
539
|
generation += 1;
|
|
540
|
+
unconfirmed = false;
|
|
512
541
|
for (const cancel of pending)
|
|
513
542
|
cancel();
|
|
514
543
|
tail = Promise.resolve();
|
|
@@ -12,7 +12,10 @@ export function createTelegramAgentMessageRuntime(deps) {
|
|
|
12
12
|
const leaderTarget = deps.getLeaderTarget();
|
|
13
13
|
if (leaderTarget?.threadId) {
|
|
14
14
|
targets.push({
|
|
15
|
-
target: {
|
|
15
|
+
target: {
|
|
16
|
+
chatId: leaderTarget.chatId,
|
|
17
|
+
threadId: leaderTarget.threadId,
|
|
18
|
+
},
|
|
16
19
|
threadName: deps.getDisplayTitle?.(leaderTarget) ?? deps.getLeaderThreadName(),
|
|
17
20
|
});
|
|
18
21
|
}
|
|
@@ -65,8 +68,9 @@ export function createTelegramAgentMessageRuntime(deps) {
|
|
|
65
68
|
if (allowedChatId === undefined || !ctx) {
|
|
66
69
|
throw new Error("Telegram agent turn routing is unavailable.");
|
|
67
70
|
}
|
|
68
|
-
const sourceTitle = (input.sourceTarget
|
|
69
|
-
|
|
71
|
+
const sourceTitle = (input.sourceTarget
|
|
72
|
+
? deps.getDisplayTitle?.(input.sourceTarget)
|
|
73
|
+
: undefined) ?? input.sourceThreadName;
|
|
70
74
|
const sourceLabel = sourceTitle
|
|
71
75
|
?.replace(/[\r\n\[\]]+/g, " ")
|
|
72
76
|
.trim()
|