@llblab/pi-kit 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -0
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +21 -0
- package/node_modules/@llblab/pi-telegram/README.md +2 -0
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +6 -4
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +5 -1
- package/node_modules/@llblab/pi-telegram/docs/outbound.md +19 -3
- package/node_modules/@llblab/pi-telegram/index.ts +10 -0
- package/node_modules/@llblab/pi-telegram/lib/activity-verbosity.ts +43 -25
- package/node_modules/@llblab/pi-telegram/lib/activity.ts +60 -1
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +101 -90
- package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +7 -2
- package/node_modules/@llblab/pi-telegram/lib/locks.ts +99 -16
- package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +18 -7
- package/node_modules/@llblab/pi-telegram/lib/outbound-voice.ts +11 -0
- package/node_modules/@llblab/pi-telegram/lib/outbound.ts +10 -3
- package/node_modules/@llblab/pi-telegram/lib/polling.ts +142 -30
- package/node_modules/@llblab/pi-telegram/lib/preview.ts +19 -3
- package/node_modules/@llblab/pi-telegram/lib/prompts.ts +8 -6
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +101 -66
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +192 -58
- package/node_modules/@llblab/pi-telegram/lib/status.ts +4 -0
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +33 -35
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/diagnosis.md +2 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@llblab/pi-kit` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.5.2 - 2026-09-06
|
|
6
|
+
|
|
7
|
+
- `Telegram Ordering Hotfix`: Advances the exact Telegram pin to `0.43.2`, bringing bounded threadless command lifetime, causal final/artifact/compaction publication order, stale-delivery fencing, and suppression of voice replay after uncertain acknowledgement. Other pins and the resource inventory remain unchanged; operator-coordinated live smoke follows release.
|
|
8
|
+
|
|
9
|
+
## 0.5.1 - 2026-09-06
|
|
10
|
+
|
|
11
|
+
- `Telegram Continuity Hotfix`: Advances the exact Telegram pin to `0.43.1`, bringing bounded persistent polling-conflict stand-down with accepted queue preservation, missing/null system-prompt normalization, and authority-fenced preview cleanup without changing the resource inventory.
|
|
12
|
+
|
|
5
13
|
## 0.5.0 - 2026-09-05
|
|
6
14
|
|
|
7
15
|
- `Telegram Native Controls`: Advances the exact Telegram pin to `0.43.0`, bringing in-body button blocks, shared JSON/CML disabled controls, HTML footer fallback, and updated agent Skills while preserving package-owned implementations and the existing resource inventory.
|
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ Package links lead to the owning repositories for usage, documentation, issues,
|
|
|
15
15
|
| [`@llblab/pi-codex-usage`](https://github.com/llblab/pi-codex-usage) | `0.9.4` | Compact Codex/Spark subscription-limit status |
|
|
16
16
|
| [`@llblab/pi-grow-loop`](https://github.com/llblab/pi-grow-loop) | `0.7.4` | Visible continuation scheduling and bounded worker Skills |
|
|
17
17
|
| [`@llblab/pi-state-flow`](https://github.com/llblab/pi-state-flow) | `0.3.0` | Opt-in validated state handoffs between agent runs |
|
|
18
|
-
| [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.43.
|
|
18
|
+
| [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.43.2` | Telegram companion, queues, files, voice, controls, and Generative Apps guidance |
|
|
19
19
|
| [`@llblab/skills`](https://github.com/llblab/skills) | `1.14.0` | Portable workflows for engineering, review, design, context maintenance, and other focused tasks |
|
|
20
20
|
|
|
21
21
|
Versions are exact by design. An upstream release does not change an installed kit until this repository explicitly advances the dependency and publishes a new kit version. Runtime defects and package-specific feature requests belong in the linked repository; package selection and kit installation issues belong here.
|
|
@@ -2,6 +2,7 @@
|
|
|
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
|
+
- [ ] `Post-release command and ordering smoke`: By explicit operator decision, perform live smoke after the combined `0.43.2` hotfix rather than gate publication on it. Verify threadless command routing and Telegram-originated final/artifact/compaction ordering, including follower delivery, without deleting live journals or accepted work. Deterministic verification does not establish the operator's exact incident or constitute live-client evidence. Retain the limits in `docs/outbound.md#publication-limits`; no exactly-once or bounded-pending-queue guarantee is claimed. Installed-runtime replacement remains a separate operator action.
|
|
5
6
|
- [ ] `Live-thread continuity and recovery`: Make loss and restoration of a live thread truthful and safe without discarding accepted work or deleting a currently owned target; the initial cause of the observed stale-thread API failures remains unproven.
|
|
6
7
|
- [ ] Complete the remaining operator-coordinated live smoke beyond the operator-confirmed successful leader Restore: verify follower Restore, inaccessible callbacks, already-absent chooser cleanup, direct stale-target diagnostics, and preservation of accepted local work/active-turn target. Use disposable test threads. The successful leader flow does not independently prove these failure paths or establish the initial stale-thread failure cause.
|
|
7
8
|
- [ ] `Native in-body controls live smoke`: Extend the operator-confirmed successful current-client smoke (singleton CML and mixed JSON/CML rows between paragraphs, ordinary prompt callbacks, labeled disabled control, separate footer callback, and HTML-mode footer fallback) to an independently identified second client, follower routing, blank disabled cells, and app-method dispatch/revision rejection. Do not infer coverage from the confirmed ordinary-button flow; in-body selection uses callback acknowledgement, not body recoloring.
|
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
> Each release keeps at most 8 outcome records of at most 512 characters.
|
|
4
4
|
|
|
5
|
+
## 0.43.2: Command Lifetime And Publication Order
|
|
6
|
+
|
|
7
|
+
- `All-Tab Command Expiry`: Uses the original Telegram timestamp to expire unselected Threaded Mode command choosers after 60 minutes, settle their deferred source, and reject stale buttons; replay of an expired command creates no new chooser. Active dispatch pauses expiry; failed attempts retain the original deadline, and accepted queue receipts remain protected. Excludes `/thread`, bound threads, classic mode, and invalid timestamps; storage failures retain journal authority.
|
|
8
|
+
- `Selected All-Tab Commands`: Settles still-deferred sources after successful local command dispatch or confirmed follower acceptance, preventing replay after chooser cleanup. Failed follower transfers retain their sources. Background menu rendering remains non-blocking, and commands admitted to the Pi queue retain receipt-governed settlement.
|
|
9
|
+
- `Repeated All-Tab Starts`: A newly delivered chooser for an identical unselected `/start` supersedes older sources from the same user/chat and active admission worker. Different arguments, other commands, and previously selected intents remain separate; superseded callbacks become inert. Failed chooser sends discard their in-memory attempt without settling the journal source, so retries do not exhaust chooser capacity.
|
|
10
|
+
|
|
11
|
+
- `Causal Publication Order`: Serializes bridge-owned assistant blocks, activity disclosures, active-turn finals/artifacts, and automatic compaction notices through their existing activity domain before transport routing. Compaction notices cannot overtake delayed local finals; Pi hooks do not wait for their network delivery. Captures notice/activity target and authority at admission and fences queued publications across session replacement; independent handler queues remain separate.
|
|
12
|
+
|
|
13
|
+
- `Artifact Delivery Authority`: Rechecks active-turn/session authority after voice/file preparation and recording actions. Cancellation suppresses later uploads, provider/text fallbacks, and stale Rich-message ownership writes without discarding the attachment list. Already-issued requests and in-flight synthesis are not undone; ambiguous uploads never authorize replay.
|
|
14
|
+
|
|
15
|
+
- `Final Admission`: Reserves final/error publication at the terminal message boundary and transfers it only to the originating turn before asynchronous config loading. Compaction uses the same queue instead of a separate buffer. Empty outcomes reserve no slot; replacement, preparation failure, settlement, and session reset release unused reservations. Terminal preview cleanup is background and bound to the captured draft.
|
|
16
|
+
|
|
17
|
+
- `Compaction Observation`: Fences superseded timeout callbacks and stale-context terminal hooks so they cannot clear a newer observation. The five-minute fallback remains an observer timeout, not proof of Pi completion or cancellation.
|
|
18
|
+
|
|
19
|
+
- `Activity Replacement`: Fences late config refresh, thinking acknowledgements, tool edits, failures, and settlement cleanup before state mutation or HTML fallback. Old work can no longer overwrite a replacement target/message, erase new tool arguments, or block new thinking output; loss of transport authority also suppresses fallback without requiring a session reset.
|
|
20
|
+
|
|
21
|
+
## 0.43.1: Transport And Preview Continuity
|
|
22
|
+
|
|
23
|
+
- `Conflict Stand-Down`: Stops transport after ten consecutive competing getUpdates conflicts, including ownership checks, heartbeat, monitoring, and bus teardown. Releases only the exact local lock and revokes direct authority even if release fails, while preserving accepted local queue work. A persistent terminal status and one diagnostic distinguish lost ownership from a competing external client; cancelled reconnects and stale admission cannot supersede current lifecycle work.
|
|
24
|
+
- `Prompt And Preview Continuity`: Treats absent/null host system prompts as empty while retaining Telegram guidance, clears completed text from preview state after successful delivery, and rejects stale session/transport completions before they can clear a replacement preview. Receipt-conflict diagnostics now include the receipt and source update ids; durable handoff verification remains fail-closed.
|
|
25
|
+
|
|
5
26
|
## 0.43.0: Native Button Blocks
|
|
6
27
|
|
|
7
28
|
- `In-Body Controls`: Renders standalone `telegram_button` fences as native button rows between paragraphs using the same singleton/mixed JSON/CML grammar and callback ownership as footer comments. Invalid/incomplete blocks register nothing, previews hide action payloads, literal examples remain inert, and HTML mode moves controls to the footer. In-body clicks acknowledge without recoloring; operator-confirmed current-client smoke passed; cross-client/follower checks remain pending.
|
|
@@ -59,6 +59,8 @@ The connected Pi instance owns Telegram polling. Use `/telegram-connect <name>`
|
|
|
59
59
|
|
|
60
60
|
After an unclean computer shutdown, `/telegram-connect` detects truncated or structurally invalid temporary ownership/routing files, quarantines only the damaged files under `tmp/telegram/recovery/`, and retries once. A journal 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 snapshot and segments are quarantined as recovery evidence, a fresh journal is published, and startup continues with an informational diagnostic instead of requiring manual JSON repair. Saved `telegram.json` configuration and runtime diagnostics remain intact. Recovery never replaces a verifiable live owner; if safe automatic recovery cannot complete, the command gives one explicit Pi-restart instruction instead of requiring deletion of the whole `tmp/` directory.
|
|
61
61
|
|
|
62
|
+
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).
|
|
63
|
+
|
|
62
64
|
### 4. Pair your Telegram account
|
|
63
65
|
|
|
64
66
|
Open the bot DM and send:
|
|
@@ -92,7 +92,7 @@ The repository uses a **Flat Domain DAG**:
|
|
|
92
92
|
|
|
93
93
|
Pi is the primary and only officially supported host. `pi-telegram` may still accept narrow, host-neutral representation differences at its existing Pi-facing boundary when they preserve native Pi behavior and do not create a second runtime policy layer:
|
|
94
94
|
|
|
95
|
-
- `prompts` preserves either Pi's plain system-prompt string or an ordered block array supplied by a compatible host, appending Telegram guidance without collapsing host-owned blocks.
|
|
95
|
+
- `prompts` preserves either Pi's plain system-prompt string or an ordered block array supplied by a compatible host, appending Telegram guidance without collapsing host-owned blocks. An absent/null host system prompt is treated as empty, including the disconnected metadata-stripping path.
|
|
96
96
|
- `pi` normalizes settings-manager construction that is either synchronous or asynchronous, then adapts either Pi's legacy enabled-model methods or a generic `get` / `set` settings service before model-menu reads and scoped-model persistence use it. Hosts without an explicit reload method rely on fresh asynchronous construction; durable writes still require `flush`.
|
|
97
97
|
- `lifecycle` continues to require Pi's semantic `agent_settled` boundary. It does not infer terminal settlement from host-specific `agent_end`, retry, or stop events; a compatibility shim must reproduce that contract before it can safely support activity identity and unrecovered-error finalization.
|
|
98
98
|
|
|
@@ -135,6 +135,8 @@ Telegram configuration lives in `~/.pi/agent/telegram.json`. Bot/session identit
|
|
|
135
135
|
- `/telegram-connect` acquires or moves the active profile's owner slot before polling starts. `/telegram-disconnect` keeps its destructive confirmation, then stops polling and releases only that exact slot. In Threaded Mode it tears down the disconnecting instance's bound Telegram thread: leaders delete their own thread directly, and followers send an authenticated exact-generation disconnect envelope and wait for confirmed leader cleanup before unregistering. Graceful Pi `quit` always preserves the owner slot as restart intent, allowing a reopened same-`cwd` session to reclaim the stale lease. When `threads.automaticCleanup` is enabled (the default), quit also deletes the bound Telegram tab without releasing that restart intent; disabling it preserves the tab through replacement-style suspension. Failed automatic cleanup records diagnostics and falls back to safe suspension so remaining lifecycle cleanup still runs.
|
|
136
136
|
- Session start schedules polling resume asynchronously only when the owner slot already points at the current `pid`/`cwd`, or when a stale same-`cwd` owner can be safely replaced after process restart. Startup and `/resume` do not wait on leader election, Bot API probes, poller handoff, or thread reconciliation before restoring the Pi session.
|
|
137
137
|
- The polling owner alone bounds `getUpdates`: each request derives its cancellation budget from Telegram's declared long-poll timeout plus 10 seconds of transport grace (10 seconds for the zero-timeout initial sync and 40 seconds for the normal 30-second poll). The request-local controller inherits poller cancellation, rejects its owner at the budget, and fences any late transport result. Ordinary Bot API and media operations do not receive speculative blanket deadlines. Existing caller signals remain authoritative through API retry waits, only retry-safe methods replay explicit retryable responses, and non-idempotent sends preserve commit-unknown evidence instead of risking duplicate mutation.
|
|
138
|
+
- Ten consecutive `getUpdates` conflict responses, including initial cursor sync, terminate polling with `persistent-conflict`; a successful response or a different error resets the count. The controller detaches its inner promise before notifying the locked lifecycle, avoiding teardown waiting on itself. That lifecycle stops ownership checks, lease refresh, capability monitoring, typing, and classic or bus transport (including leader health, pruning, and IPC), withdraws local direct authority, and transactionally releases only its exact lock. A failed durable release leaves local authority revoked; explicit reacquisition mints a fresh epoch. Accepted queue receipts and local Pi dispatch survive. One terminal diagnostic distinguishes lost local ownership from a competing client despite an apparently owned lock and reports cleanup failures; ordinary status refreshes retain generic `error` until transport recovers. Remove the competing client, then use `/telegram-connect`; no automatic retry continues after the terminal threshold.
|
|
139
|
+
- Manual and automatic polling starts share a lifecycle generation. A later suspend, disconnect, persistent-conflict stop, or accepted start invalidates older startup continuations; reconnect captures its generation before waiting for transport teardown and rechecks it before acquiring ownership and after awaited startup work. Obsolete completion or failure cannot report a successful connection or roll back a replacement. Admission rejects stale/unauthorized contexts before advancing its own startup generation, so a rejected call cannot cancel valid in-flight initialization. After awaited bus startup, thread-aware completion checks its generation before starting leader health or changing fallback/startup-option state; teardown clears health independently of the current mode flag. Startup probes, capability-monitor transitions, and observed-target transitions share an orchestration lifecycle fence and check it inside their effect-owning helpers after awaited queries, persistence, or transport work. Monitor stop also invalidates pending observations. These checks suppress subsequent state changes, fallback, health, and status effects; already-issued API/persistence calls retain their own transport/storage fencing.
|
|
138
140
|
- `pollingActive` reports only whether this runtime still owns an unresolved polling lifecycle; it is not health evidence. A separate observable state records `starting`, `long-poll`, `persisting-journal`, `persisting-offset`, `retrying`, or `stopped`, together with phase start, current update id, last successful response time/count, and terminal stop reason. This distinguishes a stuck HTTP poll from downstream update work without a wall-clock stale heuristic.
|
|
139
141
|
- Built-in read-only menu commands return after required local mutation and schedule context-fenced rendering and command synchronization independently, so those effects cannot withhold the next inbound offset.
|
|
140
142
|
- Pi `print`/`json` run modes stay passive. Inherited child sessions that share `telegram.json` but do not own the exact `pid`/`cwd` slot must not poll or call `getUpdates` unless the operator force-takes ownership.
|
|
@@ -339,7 +341,7 @@ Assistant delivery guarantees:
|
|
|
339
341
|
- Quoted rich replies use Telegram `rich_message` blocks as the prompt-context source when available, so `[reply]` context receives rendered plain text instead of raw `InputRichMessage.markdown` fallback text. Replied media runs through the same inbound handlers and voice transcription providers as current-message media, with provenance-scoped `[outputs|from:…]` appended inside the reply block.
|
|
340
342
|
- Long native Markdown replies are split only at Telegram Rich Message transport limits; oversized fenced code, display-math, and fully wrapped inline-formatting blocks are rewrapped per chunk so persisted Rich Markdown chunks remain structurally valid.
|
|
341
343
|
- When Draft previews are enabled, streaming previews pass structurally closed assistant Markdown prefixes through to `sendRichMessageDraft` with ownership checks, voice suppression, and serialized flushes. Unclosed inline spans, links, fenced code, comments, and display-math blocks are held back until a safe boundary exists. Draft failures are recorded and the failing frame is skipped instead of degrading to raw plain-message previews, because partial Markdown can be invalid while the final message remains valid.
|
|
342
|
-
- Preview flushes are serialized so older edits cannot race newer drafts; final delivery waits for active draft flushes and does not perform a post-final draft-clear call.
|
|
344
|
+
- Preview flushes are serialized so older edits cannot race newer drafts; final delivery waits for active draft flushes and does not perform a post-final draft-clear call. Successful final text delivery clears the local pending preview text only while the captured session and transport remain active, so a late delivery or Rich-attachment cleanup cannot erase replacement preview state.
|
|
343
345
|
|
|
344
346
|
UI/compat rendering guarantees:
|
|
345
347
|
|
|
@@ -392,7 +394,7 @@ The bridge does not mirror arbitrary `ctx.ui.confirm/input/select/custom` prompt
|
|
|
392
394
|
|
|
393
395
|
## Diagnostics And Operational Behavior
|
|
394
396
|
|
|
395
|
-
Status rendering distinguishes connected, active, dispatching, queued, tool-running, model-switching, and compacting states; the Telegram status menu gives compaction precedence over generic active or pending work. Its compact Tokens row shows only input and output totals, while the adjacent Cache row groups `R` cache-read tokens, `W` cache-write tokens, and `CH` for the latest assistant request's cache-read share of prompt tokens rather than a misleading cumulative-session ratio; the labels remain distinct from companion-provided usage limits. Observed automatic compaction sends the same start and completion notices as the manual command without duplicating notices for command-owned compaction. If a queue mutation removes the last waiting item while Telegram-owned work still has running tools, status remains active instead of degrading to connected.
|
|
397
|
+
Status rendering distinguishes connected, active, dispatching, queued, tool-running, model-switching, and compacting states; the Telegram status menu gives compaction precedence over generic active or pending work. Its compact Tokens row shows only input and output totals, while the adjacent Cache row groups `R` cache-read tokens, `W` cache-write tokens, and `CH` for the latest assistant request's cache-read share of prompt tokens rather than a misleading cumulative-session ratio; the labels remain distinct from companion-provided usage limits. Observed automatic compaction sends the same start and completion notices as the manual command without duplicating notices for command-owned compaction. If no terminal hook arrives, the current observation times out after five minutes: it releases only observer-owned status/typing, records a diagnostic, and requests a guarded queue-dispatch recheck. Timeout is not proof that Pi compaction completed or was cancelled and emits no invented terminal notice. Superseded timeout callbacks and stale-context terminal hooks cannot abandon or cancel a newer observation. If a queue mutation removes the last waiting item while Telegram-owned work still has running tools, status remains active instead of degrading to connected.
|
|
396
398
|
|
|
397
399
|
Queue reaction behavior, lane-tail transitions, Keep/Skip independence, multi-reaction precedence, and the Bot API reaction-removal limitation are defined in [Priority, Reactions, Keep, and Skip](#priority-reactions-keep-and-skip). Reaction changes first flush a matching delayed text or media group so the governed turn exists before mutation, and dropping marked heads cannot leave status permanently queued.
|
|
398
400
|
|
|
@@ -400,7 +402,7 @@ Queue reaction behavior, lane-tail transitions, Keep/Skip independence, multi-re
|
|
|
400
402
|
|
|
401
403
|
Complete intermediate assistant text blocks from Telegram-originated activity are sent once to the immutable originating target before active-turn final delivery; final and terminal-partial segments stay with settlement so replies are not duplicated. While this instance has exact direct or follower transport authority, completed public blocks from local/autonomous work are always sent once and in source order to the instance's authorized target. Connected companion projection is not configurable; disconnect or authority loss is its boundary. Both paths use the configured Rich or HTML renderer and exclude reasoning, tool traffic, token deltas, local prompt text, unknown sources, and stale generations. Each admitted block remains fenced to its exact target, profile/token stamp, leader epoch or follower registration generation, and session generation; non-idempotent acknowledgement ambiguity never authorizes replay.
|
|
402
404
|
|
|
403
|
-
`assistant.activity` is an independent bridge-owned projection over normalized Activity events. Each process reloads the shared file-backed setting at `agent-start` before activity admission, so multi-instance mode cannot continue projecting a stale broader process-local selection. Omitted values resolve to `verbose`, while invalid values fail closed to `quiet`; `thinking` and `tools` select one technical class, while `verbose` enables both. Provider-exposed thinking uses persistent ordinary HTML containing only a standard expandable blockquote with a bounded redacted latest-text window and inline Markdown rendered as Telegram HTML. Completed executed tools use native Rich Messages: each closed `<Tool>: <status>` root details node renders snake-case names as title words while preserving an uppercase two- or three-letter repeated prefix per word, then the native disclosure chevron reveals an open-by-default `arguments` child plus closed retained `update N` and `result`/`error` child details with lowercase monospaced, marker-free summaries and JSON pre blocks; known-safe Rich rejections fall back to the previous HTML disclosure. The projection captures the exact target and transport stamp at activity admission, serializes updates, preserves tool-start order, closes coalescing across assistant/thinking boundaries, bounds retained text/update memory plus edit frames and message/tool size, disables previews and HTTP(S) auto-link recognition inside technical evidence, and never replays a possibly committed send. Session generations own independent queues, so replacement drops queued old work without waiting on an old call.
|
|
405
|
+
`assistant.activity` is an independent bridge-owned projection over normalized Activity events. Each process reloads the shared file-backed setting at `agent-start` before activity admission, so multi-instance mode cannot continue projecting a stale broader process-local selection. Omitted values resolve to `verbose`, while invalid values fail closed to `quiet`; `thinking` and `tools` select one technical class, while `verbose` enables both. Provider-exposed thinking uses persistent ordinary HTML containing only a standard expandable blockquote with a bounded redacted latest-text window and inline Markdown rendered as Telegram HTML. Completed executed tools use native Rich Messages: each closed `<Tool>: <status>` root details node renders snake-case names as title words while preserving an uppercase two- or three-letter repeated prefix per word, then the native disclosure chevron reveals an open-by-default `arguments` child plus closed retained `update N` and `result`/`error` child details with lowercase monospaced, marker-free summaries and JSON pre blocks; known-safe Rich rejections fall back to the previous HTML disclosure. The projection captures the exact target and transport stamp at activity admission, serializes updates, preserves tool-start order, closes coalescing across assistant/thinking boundaries, bounds retained text/update memory plus edit frames and message/tool size, disables previews and HTTP(S) auto-link recognition inside technical evidence, and never replays a possibly committed send. Session generations own independent queues, so replacement drops queued old work without waiting on an old call. Bridge-owned assistant output and activity projection share one activity-publication sequence admitted synchronously from the activity bus, so later tool disclosures cannot overtake earlier assistant blocks. Activity targets and transport authority are captured before queued work starts. Active Telegram-turn final replies/artifacts and automatic compaction notices use that same publication owner before the existing direct/follower transport split, without mutually waiting on separate output tails. Final settlement captures the exact active turn and assistant result and reserves its publication position before config loading. Empty outcomes without a publication candidate reserve no position. Replacement, preparation failure, or an unused reservation releases the position without resetting or replying for a replacement turn. Final errors also publish through this owner. Reservations accept one task; cancellation cannot undo a published task, and session reset releases unresolved reservations while fencing old queued tasks. Terminal assistant messages with a publication candidate reserve the final position synchronously; settlement consumes that reservation only for the exact originating turn. Compaction notices enter the same queue immediately, behind the reserved final rather than through a separate notice buffer. Settlement, a new agent run, or session replacement cancels an unconsumed reservation. Notice target and authority are captured when the event is observed. Pi lifecycle completion does not wait for network publication. This order is process-local and does not serialize unrelated instances or independent registered activity handlers. Settlement, replacement, disconnect, failure, or stale authority clears only local ownership; already-sent activity messages remain in chat.
|
|
404
406
|
|
|
405
407
|
Telegram prompt guidance is context- and authority-aware. The package and source-checkout extension contribute `telegram-bridge`, optional `generated-control-surface`, and `generative-apps` Skills through Pi resource discovery. Generated Control Surface treats `interface = f(state, capabilities, intent)` as a renderer-neutral primitive, compiling transient evidence-backed controls over domain-owned workflows, systems, navigation, supervision, and decisions without creating parallel application state. It composes an ordered ragged sequence of independently sized semantic rows rather than filling a rectangular grid: compact rows contain genuine peers, singleton rows isolate structurally independent actions, and rectangular layouts remain reserved for genuinely spatial state. Text-bearing controls use at most two columns and flow into additional rows, while denser rows are reserved for short position-bearing glyphs or codes and never exceed the eight-column phone-width UX maximum. Vertical extent is independent: a true spatial surface may retain substantially more rows, while non-spatial button walls route to grouping, disclosure, or pagination. Symmetry is treated as an evidence claim about equal relationships or real spatial topology; an abstract layout catalog supplies adaptable singleton, peer, staged, navigational, repeated-pair, and rectangular shapes without forcing tasks into preset grids. Repeated controls carry the smallest sufficient action delta when visible conversation is unambiguous; larger or error-prone state moves to a deterministic task-owned Markdown artifact, correctness-sensitive transitions move to a small domain-owned transition implementation, and repeated clicks are adjudicated against current state rather than stale button appearance. Its filesystem adapter reserves the first full-width row for parent traversal outside root, places available Previous/Next controls together in one compact row immediately afterward, orders visible directories, hidden directories, visible files, and hidden files alphabetically within each category before fixed ten-entry pagination, renders path/range metadata as stacked status-style key-value rows instead of middle-dot prose, emits the complete Telegram control set through one JSON-matrix action, suppresses duplicate plain/monospaced listings and default Refresh unless user preference overrides presentation, and retains an ordinary numbered fallback when buttons are unavailable. Only an exact direct owner or live registered follower exposes the two pi-telegram delivery tools, their active-tool metadata, and the compact routing suffix. Disconnect or authority loss removes those tool surfaces for subsequent requests without touching foreign tools; reconnect/recovery restores only the pi-telegram subset that was active before suspension, including across same-process reload. Repeated stable interactions may graduate from that model-mediated surface into a reviewed Generative App whose deterministic bound methods bypass Pi queue admission; the `generative-apps` Skill owns this compilation and operating workflow while the underlying capability retains domain authority. Telegram-originated turns route to the stable Skill contracts and retain dynamic blocks such as `[voice] delivery: automatic voice`; the Skills and public documentation own syntax, target routing, Threaded Mode behavior, Generative App operation, and diagnostics.
|
|
406
408
|
|
|
@@ -424,6 +424,10 @@ Local regression evidence covers continuity and cleanup authority; operator-coor
|
|
|
424
424
|
|
|
425
425
|
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.
|
|
426
426
|
|
|
427
|
+
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.
|
|
428
|
+
|
|
429
|
+
For repeated unselected All-tab `/start`, a newer durable source supersedes older exact-text equivalents only after its chooser message ID is returned. Coalescing requires the same chat, user, and active admission worker; different arguments, other commands, and previously selected intents are not combined. Superseded chooser messages may remain visible, but their callbacks are inert. Storage failure retains the source according to the worker's fail-closed settlement contract. Restart may recreate a chooser for a still-unexpired source using currently routable targets; it does not automatically forward that command into a restored thread. This does not purge historical Telegram messages or promise that no fresh chooser appears after restart.
|
|
430
|
+
|
|
427
431
|
The bounded pending reroute owns its original source target and the returned chooser message ID independently from remaining messages. Authenticated callbacks must match the stored chat/chooser and any supplied thread field. `InaccessibleMessage` may omit the thread field; a missing callback message, unknown chooser, conflicting identity, threadless source, or already-owned Restore source fails closed rather than forwarding to the old target. Restart or expiry without that pending identity does not authorize reconstruction from callback data alone. When the original input itself has no thread ID (including an All-tab `/start`), the chooser offers routing only, not Restore; old Restore callbacks explain that a plain message must first be sent in the destination thread. Client tab selection is not inferred from recent topic creation or from the chosen Pi instance.
|
|
428
432
|
|
|
429
433
|
After dispatch, cleanup retries retain source identity but never redispatch accepted messages. Confirmed typed HTTP 400 `message to delete not found` completes message deletion idempotently; permission and transient failures remain errors. Reroute cleanup rechecks current bindings, live targets, reservations, and pending provisions before close, before delete, and on retry. Other destructive cleanup origins use the shared synchronous target-protection policy: explicit retirement permits only its unchanged departing binding, persisted shutdown intent permits only its original pre-intent binding, and reservation/provision cleanup permits only the corresponding unchanged claim. Protection checks also guard post-API local invalidation, reservation, and disconnect completion. A newly protected target cancels remaining cleanup. Already-issued remote operations cannot be undone by a later local ownership change; checks prevent subsequent effects, not retroactive cancellation.
|
|
@@ -434,7 +438,7 @@ Follower Restore requires exact registration generation and expected old target
|
|
|
434
438
|
|
|
435
439
|
- Two leaders calling `getUpdates` is the main safety failure.
|
|
436
440
|
- Lock heartbeat/takeover must be atomic enough to prevent this under normal local concurrency.
|
|
437
|
-
-
|
|
441
|
+
- Persistent competing `getUpdates` clients trigger a full transport stand-down even when the local lock still appears owned; the stopped runtime preserves accepted local work and releases only its exact ownership. See [Runtime Ownership](./architecture.md#runtime-ownership) for the threshold, diagnostic, and reconnect contract.
|
|
438
442
|
|
|
439
443
|
## Security Boundaries
|
|
440
444
|
|
|
@@ -12,17 +12,31 @@ Every completed `assistant-segment` with `placement: "intermediate"` from a Tele
|
|
|
12
12
|
|
|
13
13
|
While Telegram is connected, local, autonomous, and unclassified extension follow-up Pi work always projects every completed public block—including visible commentary/checkpoints and the final answer—to the instance's authorized target in source order. There is no projection setting or opt-out: disconnecting Telegram is the boundary that stops this companion surface. Both paths consume normalized complete Activity segments rather than raw token deltas, reasoning, or tool traffic.
|
|
14
14
|
|
|
15
|
-
Projected blocks use `assistant.rendering` independently of voice policy. Rich mode sends native Rich Markdown and HTML mode keeps the established HTML renderer; assistant-authored `telegram_button` comments are planned into prompt buttons before either renderer runs, while projection does not synthesize voice or attach queued files merely because Rich rendering is active. Ordered admission revalidates the exact target, profile/token transport generation, leader epoch or follower registration generation, and session generation before each send. Active-turn final delivery
|
|
15
|
+
Projected blocks use `assistant.rendering` independently of voice policy. Rich mode sends native Rich Markdown and HTML mode keeps the established HTML renderer; assistant-authored `telegram_button` comments are planned into prompt buttons before either renderer runs, while projection does not synthesize voice or attach queued files merely because Rich rendering is active. Ordered admission revalidates the exact target, profile/token transport generation, leader epoch or follower registration generation, and session generation before each send. Active-turn final delivery joins the shared [activity-publication order](./architecture.md), preserving commentary-before-final order without blocking Pi lifecycle completion. A `commit-unknown` outcome never permits replay.
|
|
16
|
+
|
|
17
|
+
### Publication Limits
|
|
18
|
+
|
|
19
|
+
The ordered publication queue is process-local, in-memory, and not a durable outbox. Slow preparation or delivery holds later publications from the same instance; Pi lifecycle hooks do not wait for network publication. Terminal preview cleanup also runs in the background, captures the original draft before scheduling, and rechecks draft identity and delivery authority after a pending flush; it cannot clear a replacement draft. A new session drops queued old-generation work and starts its own sequence without waiting for an already-running call. Separate instances have separate publication queues, but still share Telegram/leader transport limits.
|
|
20
|
+
|
|
21
|
+
There is no independent hard cap on pending publications and no publication-wide timeout. Transport and command-template deadlines apply to their own operations; a programmatic handler/provider that never settles can hold this instance's output indefinitely and allow pending work to accumulate. Session reset invalidates queued work but cannot forcibly stop arbitrary provider code or undo requests already issued. Bounded rendered thinking/tool payloads are not a bound on this pending queue.
|
|
22
|
+
|
|
23
|
+
A failed task does not poison later tasks. Pending publications are not replayed after restart, and uncertain non-idempotent acknowledgements never authorize automatic replay. These are ordering and authority guarantees, not exactly-once delivery or a guarantee that every remote send succeeds.
|
|
24
|
+
|
|
25
|
+
## Attachment Delivery
|
|
26
|
+
|
|
27
|
+
An uncertain `sendVoice` acknowledgement stops the logical voice batch: it cannot advance to another synthesis provider or voice item, and active-turn delivery records the uncertainty without issuing text fallback. Ordinary synthesis failures retain provider fallback; later independent publications can proceed.
|
|
28
|
+
|
|
29
|
+
Active Telegram-turn queued attachments recheck turn/session authority after file checks, before each upload, and before failure notices. Cancellation leaves the attachment list intact and suppresses later files and fallback text. Rich upload acknowledgements do not publish ownership into a replacement context. Already-issued uploads cannot be undone, and ambiguous results never authorize replay.
|
|
16
30
|
|
|
17
31
|
## Technical Activity
|
|
18
32
|
|
|
19
33
|
`assistant.activity` defaults to `verbose` when absent and accepts four modes: `quiet`, `thinking`, `tools`, and `verbose`. Explicit stored values remain unchanged; invalid values fail closed to `quiet`. Every Pi instance reloads this shared file-backed mode at `agent-start`, so a setting changed from one thread applies to subsequent runs in the other live instances without requiring process reload. `thinking` shows only provider-exposed thinking, `tools` shows only completed executed tools, and `verbose` shows both without mixing technical UI into assistant Markdown.
|
|
20
34
|
|
|
21
35
|
- Provider-exposed thinking updates one persistent ordinary HTML message containing only a standard Telegram `<blockquote expandable>` with a bounded redacted latest-text window. Omitting a separate icon/level header saves one chat row while the disclosure's unique shape remains recognizable. Inline Markdown emphasis and code render as Telegram HTML instead of leaking raw markers. The bridge sends the message once, edits it only as thinking grows, and leaves the disclosure in chat; it never uses Rich drafts/Rich Messages. Providers that expose no thinking produce none.
|
|
22
|
-
- Completed executed tools use native `sendRichMessage` block objects. Each tool is one closed root details node summarized as bold `<Tool>:` plus monospaced `<status>`; snake-case root labels render as title words (`telegram_attach` → `Telegram Attach`), while each word preserves a leading two- or three-character repeated-letter prefix in uppercase (`ff_find` → `FF Find`). The native disclosure chevron identifies the row, and opening it immediately reveals the open `arguments` child plus separate closed retained `update N`, `result`, or `error` child details. Each child summary is one lowercase monospaced label—visually a quote-free outer JSON key with no icon, list marker, or heading emphasis—and contains one preformatted `json` block. Updates remain chronological, dropped-update counts appear in the first retained update summary, and arrays of object entries keep the denser `[{ ... }, { ... }]` layout. A known-safe Rich HTTP 400 rejection falls back once to the previous expandable HTML representation; ambiguous non-idempotent outcomes never replay.
|
|
36
|
+
- Completed executed tools use native `sendRichMessage` block objects. Each tool is one closed root details node summarized as bold `<Tool>:` plus monospaced `<status>`; snake-case root labels render as title words (`telegram_attach` → `Telegram Attach`), while each word preserves a leading two- or three-character repeated-letter prefix in uppercase (`ff_find` → `FF Find`). The native disclosure chevron identifies the row, and opening it immediately reveals the open `arguments` child plus separate closed retained `update N`, `result`, or `error` child details. Each child summary is one lowercase monospaced label—visually a quote-free outer JSON key with no icon, list marker, or heading emphasis—and contains one preformatted `json` block. Updates remain chronological, dropped-update counts appear in the first retained update summary, and arrays of object entries keep the denser `[{ ... }, { ... }]` layout. A known-safe Rich HTTP 400 rejection falls back once to the previous expandable HTML representation only while the admitted generation and authority remain current; ambiguous non-idempotent outcomes never replay.
|
|
23
37
|
- Thinking `sendMessage`/`editMessageText` disables link previews and breaks HTTP(S) auto-link recognition inside evidence. Rich tool messages set `skip_entity_detection: true`, so URL-like arguments, updates, and results remain literal code. Consecutive tools coalesce by editing one message only while target, activity, generation, ordering boundary, tool count, and serialized-size bounds still match. Assistant or thinking content closes the batch. A non-idempotent send with unknown commit state is never replayed; failed or ambiguous edits start no fallback send.
|
|
24
38
|
|
|
25
|
-
Thinking retains only a bounded latest-text window, tool updates retain only a bounded latest-entry window, and a session reset abandons queued old-generation work without making the replacement session wait for an old transport call.
|
|
39
|
+
Thinking retains only a bounded latest-text window, tool updates retain only a bounded latest-entry window, and a session reset abandons queued old-generation work without making the replacement session wait for an old transport call. Late refresh results, acknowledgements, failures, and settlement continuations recheck their captured generation and authority before changing state; old work cannot replace new message handles, clear new tool arguments, or block the replacement's thinking output.
|
|
26
40
|
|
|
27
41
|
Technical activity is operational evidence, not part of the semantic answer stream. Final-answer rendering, voice policy, artifacts, and quiet behavior remain unchanged.
|
|
28
42
|
|
|
@@ -127,6 +141,8 @@ const dispose = registerTelegramVoiceSynthesisProvider(
|
|
|
127
141
|
|
|
128
142
|
Synthesis providers receive the extracted `telegram_voice` text plus optional `lang`/`rate` hints. Stable registrations pass a durable `id`; omitted ids remain a compatibility path for older providers. Providers own translation, TTS, speech rewriting, and OGG/Opus conversion. The bridge validates that the returned file ends in `.ogg` or `.opus`, sends it through Telegram `sendVoice`, and falls back to planned text if delivery fails before any visible text was delivered. Providers run after configured and programmatic voice handlers in the priority chain above.
|
|
129
143
|
|
|
144
|
+
Active Telegram-turn voice artifacts carry the turn/session currentness check through synthesis, file conversion, recording actions, and upload. Authority loss suppresses not-yet-issued transport effects, later voice replies, provider fallbacks, and final-text fallback. It does not undo an already-issued request or interrupt a provider already synthesizing.
|
|
145
|
+
|
|
130
146
|
## Voice Markup
|
|
131
147
|
|
|
132
148
|
Assistant replies can include hidden voice actions as a positional compact cell or JSON object:
|
|
@@ -557,6 +557,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
557
557
|
activityRuntime,
|
|
558
558
|
activityVerbosityRuntime,
|
|
559
559
|
assistantOutputRuntime,
|
|
560
|
+
publicationRuntime,
|
|
560
561
|
} = Bindings.createTelegramActivityBindingRuntime({
|
|
561
562
|
generation: deliveryGenerationSeed,
|
|
562
563
|
assistantOutput: {
|
|
@@ -1034,6 +1035,12 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1034
1035
|
Pi.ExtensionContext
|
|
1035
1036
|
>({
|
|
1036
1037
|
state: pollingControllerState,
|
|
1038
|
+
canStart(ctx) {
|
|
1039
|
+
return telegramSessionContextStore.isCurrent(ctx) && lockRuntime.owns(ctx);
|
|
1040
|
+
},
|
|
1041
|
+
onPersistentConflict(ctx, count): Promise<void> {
|
|
1042
|
+
return lockedPollingRuntime.onPersistentConflict(ctx, count);
|
|
1043
|
+
},
|
|
1037
1044
|
getConfig: configStore.get,
|
|
1038
1045
|
hasBotToken: configStore.hasBotToken,
|
|
1039
1046
|
deleteWebhook,
|
|
@@ -1205,8 +1212,10 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1205
1212
|
const threadAwarePollingPorts = telegramThreadCapabilityRuntime.pollingPorts;
|
|
1206
1213
|
const lockedPollingRuntime = Locks.createTelegramLockedPollingRuntime({
|
|
1207
1214
|
lock: lockRuntime,
|
|
1215
|
+
transportMonitor: telegramThreadCapabilityMonitor,
|
|
1208
1216
|
hasBotToken: configStore.hasBotToken,
|
|
1209
1217
|
canStartPolling: Pi.canStartPollingInExtensionContext,
|
|
1218
|
+
isContextCurrent: telegramSessionContextStore.isCurrent,
|
|
1210
1219
|
formatStartBlockedMessage: Pi.formatPollingStartBlockedByRunMode,
|
|
1211
1220
|
startPolling: threadAwarePollingPorts.startPolling,
|
|
1212
1221
|
stopPolling: threadAwarePollingPorts.stopPolling,
|
|
@@ -1357,6 +1366,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1357
1366
|
activityRuntime,
|
|
1358
1367
|
activityVerbosityRuntime,
|
|
1359
1368
|
assistantOutputRuntime,
|
|
1369
|
+
publicationRuntime,
|
|
1360
1370
|
configStore,
|
|
1361
1371
|
abort,
|
|
1362
1372
|
typing,
|
|
@@ -4,7 +4,7 @@
|
|
|
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 { TelegramActivityEvent } from "./activity.ts";
|
|
7
|
+
import type { TelegramActivityEvent, TelegramActivityPublicationRuntime } from "./activity.ts";
|
|
8
8
|
import {
|
|
9
9
|
escapeHtml,
|
|
10
10
|
renderTelegramInlineMarkdownHtml,
|
|
@@ -321,6 +321,7 @@ export function createTelegramActivityVerbosityBinding(): TelegramActivityVerbos
|
|
|
321
321
|
}
|
|
322
322
|
|
|
323
323
|
export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
|
|
324
|
+
enqueue?: TelegramActivityPublicationRuntime["enqueue"];
|
|
324
325
|
getActivityMode: () => "quiet" | "thinking" | "tools" | "verbose";
|
|
325
326
|
refreshActivityMode?: () => Promise<void>;
|
|
326
327
|
getNowMs?: () => number;
|
|
@@ -380,15 +381,20 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
|
|
|
380
381
|
};
|
|
381
382
|
const hasAuthority = (): boolean =>
|
|
382
383
|
authority !== undefined && deps.isAuthorityActive(authority);
|
|
383
|
-
const
|
|
384
|
+
const isCurrent = (acceptedGeneration: number, admittedAuthority: TAuthority | undefined): boolean =>
|
|
385
|
+
active && generation === acceptedGeneration && admittedAuthority !== undefined && deps.isAuthorityActive(admittedAuthority);
|
|
386
|
+
const ensureActivity = (
|
|
387
|
+
event: TelegramActivityEvent,
|
|
388
|
+
admittedTarget: TelegramTarget | undefined,
|
|
389
|
+
admittedAuthority: TAuthority,
|
|
390
|
+
): boolean => {
|
|
384
391
|
if (deps.getActivityMode() === "quiet") return false;
|
|
385
392
|
if (activityId === event.activityId) return hasAuthority();
|
|
386
393
|
clearActivity();
|
|
387
|
-
|
|
388
|
-
if (!resolvedTarget) return false;
|
|
394
|
+
if (!admittedTarget) return false;
|
|
389
395
|
activityId = event.activityId;
|
|
390
|
-
target =
|
|
391
|
-
authority =
|
|
396
|
+
target = admittedTarget;
|
|
397
|
+
authority = admittedAuthority;
|
|
392
398
|
return hasAuthority();
|
|
393
399
|
};
|
|
394
400
|
const closeToolBatch = () => {
|
|
@@ -398,10 +404,10 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
|
|
|
398
404
|
event: TelegramActivityEvent,
|
|
399
405
|
acceptedGeneration: number,
|
|
400
406
|
) => {
|
|
407
|
+
const admittedAuthority = authority;
|
|
401
408
|
if (
|
|
402
|
-
|
|
409
|
+
!isCurrent(acceptedGeneration, admittedAuthority) ||
|
|
403
410
|
!target ||
|
|
404
|
-
!hasAuthority() ||
|
|
405
411
|
reasoningBlocked
|
|
406
412
|
) {
|
|
407
413
|
return;
|
|
@@ -440,22 +446,24 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
|
|
|
440
446
|
parse_mode: "HTML",
|
|
441
447
|
link_preview_options: { is_disabled: true },
|
|
442
448
|
});
|
|
449
|
+
if (!isCurrent(acceptedGeneration, admittedAuthority)) return;
|
|
443
450
|
reasoningMessage = {
|
|
444
451
|
messageId: sent.message_id,
|
|
445
452
|
target: { ...target },
|
|
446
453
|
};
|
|
447
454
|
}
|
|
448
|
-
if (
|
|
455
|
+
if (!isCurrent(acceptedGeneration, admittedAuthority)) return;
|
|
449
456
|
reasoningMessageFrames += 1;
|
|
450
457
|
lastReasoningMessageChars = reasoningChars;
|
|
451
458
|
lastReasoningPublishMs = getNowMs();
|
|
452
459
|
} catch (error) {
|
|
460
|
+
if (!isCurrent(acceptedGeneration, admittedAuthority)) return;
|
|
461
|
+
reasoningBlocked = true;
|
|
453
462
|
deps.recordFailure?.(
|
|
454
463
|
canEdit ? "reasoning-edit" : "reasoning-send",
|
|
455
464
|
event,
|
|
456
465
|
error,
|
|
457
466
|
);
|
|
458
|
-
reasoningBlocked = true;
|
|
459
467
|
}
|
|
460
468
|
};
|
|
461
469
|
const publishTool = async (
|
|
@@ -463,11 +471,8 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
|
|
|
463
471
|
tool: ToolActivity,
|
|
464
472
|
acceptedGeneration: number,
|
|
465
473
|
) => {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
!target ||
|
|
469
|
-
!hasAuthority()
|
|
470
|
-
) {
|
|
474
|
+
const admittedAuthority = authority;
|
|
475
|
+
if (!isCurrent(acceptedGeneration, admittedAuthority) || !target) {
|
|
471
476
|
return;
|
|
472
477
|
}
|
|
473
478
|
const canAppend =
|
|
@@ -495,6 +500,7 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
|
|
|
495
500
|
}),
|
|
496
501
|
});
|
|
497
502
|
} catch (error) {
|
|
503
|
+
if (!isCurrent(acceptedGeneration, admittedAuthority)) return;
|
|
498
504
|
if (
|
|
499
505
|
toolMessage.format !== "rich" ||
|
|
500
506
|
!isKnownSafeRichActivityRejection(error)
|
|
@@ -508,9 +514,10 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
|
|
|
508
514
|
parse_mode: "HTML",
|
|
509
515
|
link_preview_options: { is_disabled: true },
|
|
510
516
|
});
|
|
517
|
+
if (!isCurrent(acceptedGeneration, admittedAuthority)) return;
|
|
511
518
|
toolMessage.format = "html";
|
|
512
519
|
}
|
|
513
|
-
if (
|
|
520
|
+
if (!isCurrent(acceptedGeneration, admittedAuthority)) return;
|
|
514
521
|
toolMessage.tools = nextTools;
|
|
515
522
|
return;
|
|
516
523
|
}
|
|
@@ -528,6 +535,7 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
|
|
|
528
535
|
rich_message: renderTelegramToolActivityRichMessage([tool]),
|
|
529
536
|
});
|
|
530
537
|
} catch (error) {
|
|
538
|
+
if (!isCurrent(acceptedGeneration, admittedAuthority)) return;
|
|
531
539
|
if (!isKnownSafeRichActivityRejection(error)) throw error;
|
|
532
540
|
sent = await deps.sendMessage({
|
|
533
541
|
...body,
|
|
@@ -537,7 +545,7 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
|
|
|
537
545
|
});
|
|
538
546
|
format = "html";
|
|
539
547
|
}
|
|
540
|
-
if (
|
|
548
|
+
if (!isCurrent(acceptedGeneration, admittedAuthority)) return;
|
|
541
549
|
toolMessage = {
|
|
542
550
|
messageId: sent.message_id,
|
|
543
551
|
tools: [tool],
|
|
@@ -545,25 +553,30 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
|
|
|
545
553
|
format,
|
|
546
554
|
};
|
|
547
555
|
} catch (error) {
|
|
548
|
-
|
|
556
|
+
if (!isCurrent(acceptedGeneration, admittedAuthority)) return;
|
|
549
557
|
closeToolBatch();
|
|
558
|
+
deps.recordFailure?.(canAppend ? "tool-edit" : "tool-send", event, error);
|
|
550
559
|
}
|
|
551
560
|
};
|
|
552
561
|
const process = async (
|
|
553
562
|
event: TelegramActivityEvent,
|
|
554
563
|
acceptedGeneration: number,
|
|
564
|
+
admittedTarget: TelegramTarget | undefined,
|
|
565
|
+
admittedAuthority: TAuthority,
|
|
555
566
|
) => {
|
|
556
567
|
if (event.type === "agent-start" && deps.refreshActivityMode) {
|
|
557
568
|
try {
|
|
558
569
|
await deps.refreshActivityMode();
|
|
559
570
|
} catch (error) {
|
|
560
|
-
|
|
571
|
+
if (!isCurrent(acceptedGeneration, admittedAuthority)) return;
|
|
561
572
|
clearActivity();
|
|
562
573
|
activityId = event.activityId;
|
|
574
|
+
deps.recordFailure?.("config-refresh", event, error);
|
|
563
575
|
return;
|
|
564
576
|
}
|
|
565
577
|
}
|
|
566
|
-
if (!
|
|
578
|
+
if (!isCurrent(acceptedGeneration, admittedAuthority)) return;
|
|
579
|
+
if (!ensureActivity(event, admittedTarget, admittedAuthority)) {
|
|
567
580
|
if (
|
|
568
581
|
activityId === event.activityId &&
|
|
569
582
|
deps.getActivityMode() === "quiet"
|
|
@@ -614,6 +627,7 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
|
|
|
614
627
|
) {
|
|
615
628
|
await publishReasoning(event, acceptedGeneration);
|
|
616
629
|
}
|
|
630
|
+
if (!isCurrent(acceptedGeneration, admittedAuthority)) return;
|
|
617
631
|
reasoningBuffer = "";
|
|
618
632
|
reasoningChars = 0;
|
|
619
633
|
reasoningMessageFrames = 0;
|
|
@@ -668,7 +682,7 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
|
|
|
668
682
|
toolOrder.shift();
|
|
669
683
|
tools.delete(next.id);
|
|
670
684
|
await publishTool(event, next, acceptedGeneration);
|
|
671
|
-
if (
|
|
685
|
+
if (!isCurrent(acceptedGeneration, admittedAuthority)) return;
|
|
672
686
|
}
|
|
673
687
|
return;
|
|
674
688
|
}
|
|
@@ -680,6 +694,7 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
|
|
|
680
694
|
) {
|
|
681
695
|
await publishReasoning(event, acceptedGeneration);
|
|
682
696
|
}
|
|
697
|
+
if (!isCurrent(acceptedGeneration, admittedAuthority)) return;
|
|
683
698
|
clearActivity();
|
|
684
699
|
}
|
|
685
700
|
};
|
|
@@ -687,10 +702,13 @@ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
|
|
|
687
702
|
accept(event) {
|
|
688
703
|
if (!active) return;
|
|
689
704
|
const acceptedGeneration = generation;
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
705
|
+
const resolvedTarget = deps.resolveTarget(event);
|
|
706
|
+
const admittedTarget = resolvedTarget ? { ...resolvedTarget } : undefined;
|
|
707
|
+
const admittedAuthority = deps.captureAuthority();
|
|
708
|
+
const enqueue = deps.enqueue ?? ((task: () => Promise<void>) => tail.then(task));
|
|
709
|
+
tail = enqueue(async () => {
|
|
710
|
+
if (!active || generation !== acceptedGeneration || !deps.isAuthorityActive(admittedAuthority)) return;
|
|
711
|
+
await process(event, acceptedGeneration, admittedTarget, admittedAuthority);
|
|
694
712
|
})
|
|
695
713
|
.catch((error) => {
|
|
696
714
|
deps.recordFailure?.("tool-send", event, error);
|
|
@@ -738,6 +738,63 @@ export function createTelegramActivityRuntime(deps: {
|
|
|
738
738
|
};
|
|
739
739
|
}
|
|
740
740
|
|
|
741
|
+
// --- Ordered Bridge-Owned Publication ---
|
|
742
|
+
|
|
743
|
+
export interface TelegramActivityPublicationReservation {
|
|
744
|
+
publish: (task: () => Promise<void>) => Promise<void>;
|
|
745
|
+
cancel: () => void;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
export interface TelegramActivityPublicationRuntime {
|
|
749
|
+
enqueue: (task: () => Promise<void>) => Promise<void>;
|
|
750
|
+
reserve: () => TelegramActivityPublicationReservation;
|
|
751
|
+
reset: () => void;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
export function createTelegramActivityPublicationRuntime(): TelegramActivityPublicationRuntime {
|
|
755
|
+
let generation = 0;
|
|
756
|
+
let tail = Promise.resolve();
|
|
757
|
+
const pending = new Set<() => void>();
|
|
758
|
+
const reserve = (): TelegramActivityPublicationReservation => {
|
|
759
|
+
const admittedGeneration = generation;
|
|
760
|
+
let state: "pending" | "published" | "cancelled" = "pending";
|
|
761
|
+
let resolve!: (task: (() => Promise<void>) | undefined) => void;
|
|
762
|
+
const ready = new Promise<(() => Promise<void>) | undefined>((accept) => { resolve = accept; });
|
|
763
|
+
const cancel = () => {
|
|
764
|
+
if (state !== "pending") return;
|
|
765
|
+
state = "cancelled";
|
|
766
|
+
pending.delete(cancel);
|
|
767
|
+
resolve(undefined);
|
|
768
|
+
};
|
|
769
|
+
pending.add(cancel);
|
|
770
|
+
const result = tail.then(async () => {
|
|
771
|
+
const task = await ready;
|
|
772
|
+
if (admittedGeneration === generation && task) await task();
|
|
773
|
+
});
|
|
774
|
+
tail = result.catch(() => {});
|
|
775
|
+
return {
|
|
776
|
+
publish(task) {
|
|
777
|
+
if (state === "cancelled") return result;
|
|
778
|
+
if (state === "published") return Promise.reject(new Error("Publication reservation already published."));
|
|
779
|
+
state = "published";
|
|
780
|
+
pending.delete(cancel);
|
|
781
|
+
resolve(task);
|
|
782
|
+
return result;
|
|
783
|
+
},
|
|
784
|
+
cancel,
|
|
785
|
+
};
|
|
786
|
+
};
|
|
787
|
+
return {
|
|
788
|
+
reserve,
|
|
789
|
+
enqueue: (task) => reserve().publish(task),
|
|
790
|
+
reset() {
|
|
791
|
+
generation += 1;
|
|
792
|
+
for (const cancel of pending) cancel();
|
|
793
|
+
tail = Promise.resolve();
|
|
794
|
+
},
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
|
|
741
798
|
// --- Public Assistant Output Projection ---
|
|
742
799
|
|
|
743
800
|
export interface TelegramAssistantOutputRuntime {
|
|
@@ -748,6 +805,7 @@ export interface TelegramAssistantOutputRuntime {
|
|
|
748
805
|
}
|
|
749
806
|
|
|
750
807
|
export function createTelegramAssistantOutputRuntime<TAuthority = undefined>(deps: {
|
|
808
|
+
enqueue?: TelegramActivityPublicationRuntime["enqueue"];
|
|
751
809
|
captureAuthority?: () => TAuthority;
|
|
752
810
|
isAuthorityActive?: (authority: TAuthority) => boolean;
|
|
753
811
|
canDeliver: (event: TelegramAssistantSegmentEvent) => boolean;
|
|
@@ -785,7 +843,8 @@ export function createTelegramAssistantOutputRuntime<TAuthority = undefined>(dep
|
|
|
785
843
|
admitted.add(key);
|
|
786
844
|
const admittedGeneration = generation;
|
|
787
845
|
const admittedAuthority = deps.captureAuthority?.();
|
|
788
|
-
|
|
846
|
+
const enqueue = deps.enqueue ?? ((task: () => Promise<void>) => tail.then(task));
|
|
847
|
+
tail = enqueue(async () => {
|
|
789
848
|
const isAdmittedAuthorityActive = () =>
|
|
790
849
|
running &&
|
|
791
850
|
generation === admittedGeneration &&
|