@llblab/pi-telegram 0.38.0 → 0.39.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +3 -3
- package/BACKLOG.md +0 -1
- package/CHANGELOG.md +14 -0
- package/README.md +1 -1
- package/docs/architecture.md +4 -4
- package/docs/ui-style.md +22 -8
- package/index.ts +2 -0
- package/lib/bindings.ts +49 -13
- package/lib/commands.ts +122 -28
- package/lib/journal.ts +2 -9
- package/lib/lifecycle.ts +20 -18
- package/lib/menu-queue.ts +15 -4
- package/lib/menu.ts +10 -7
- package/lib/prompts.ts +2 -2
- package/lib/queue.ts +49 -12
- package/lib/routing.ts +5 -2
- package/lib/status.ts +4 -2
- package/lib/updates.ts +3 -3
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -80,14 +80,14 @@ Use the relevant local skill before non-trivial work in its domain. Keep skill o
|
|
|
80
80
|
- 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.
|
|
81
81
|
- 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.
|
|
82
82
|
- 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. 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.
|
|
83
|
-
- Startup and elapsed time are not owner-death proof; queued authority has no time lease. Dead-owner
|
|
83
|
+
- 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 Windows or inaccessible birth metadata is `unverifiable`, and both non-dead outcomes keep authority queued. 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.
|
|
84
84
|
- Execution failures persist bounded diagnostics and attempt state as `retry-wait`. 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 snapshot and segments move atomically under `tmp/telegram/recovery/` before a fresh journal is published and startup continues with informational recovery evidence.
|
|
85
85
|
- An unresolved reaction delays only the exact governed queue item identified by chat/message sources, not unrelated queue work. Queue receipt publication follows in-memory append and precedes dispatch request; receipt-bearing turns remain queued until every exact source commits.
|
|
86
86
|
- 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).
|
|
87
87
|
|
|
88
88
|
### 4.4 Queue, Delivery, And User Surfaces
|
|
89
89
|
|
|
90
|
-
- Queue lane/kind admission is explicit. Dispatch waits for active-turn, pending-dispatch, control, compaction, `ctx.isIdle()`, and Pi pending-message guards; a dispatched prompt stays queued until `agent_start` consumes it. Each prompt is one object with one active lane and no reserved return slot. Normal and Priority are separate FIFO lanes: crossing lanes removes it from the source and appends it at the destination tail, while Keep/Skip and same-category emoji changes preserve lane position. Complete reaction sets independently derive Priority from recognized positive emoji and Skip from recognized negative emoji; both may coexist, Skip
|
|
90
|
+
- Queue lane/kind admission is explicit. Dispatch waits for active-turn, pending-dispatch, control, compaction, `ctx.isIdle()`, and Pi pending-message guards; a dispatched prompt stays queued until `agent_start` consumes it. Each prompt is one object with one active lane and no reserved return slot. Normal and Priority are separate FIFO lanes: crossing lanes removes it from the source and appends it at the destination tail, while Keep/Skip and same-category emoji changes preserve lane position. Complete reaction sets independently derive Priority from recognized positive emoji and Skip from recognized negative emoji; both may coexist, suppressed turns retain durable receipts while waiting, and Skip settles them only when the prompt reaches dispatch before dropping it without inference. Suppressed turns remain visible at a struck-through physical ordinal without contributing to executable queue counters, while graceful session shutdown discards all remaining queue authority before clearing memory.
|
|
91
91
|
- `/stop`, `/abort`, `/next`, and `/continue` respectively reset+abort, abort while preserving queue, force the next turn, and enqueue a control-lane continuation. Abort-history folding applies only to Telegram-owned active turns.
|
|
92
92
|
- Telegram extension side effects must not hold Pi's core lifecycle hostage after semantic completion. Preserve ordering in extension-owned background work, record failures, and fence target/profile/transport/session authority.
|
|
93
93
|
- Complete assistant/guest model answers use Telegram-native Rich Markdown. Harness-owned menus, status, diagnostics, thinking, and tool evidence remain explicit HTML/plain or their documented native surface. Preserve literal code and structurally safe chunking; never split invalid markup.
|
|
@@ -125,7 +125,7 @@ The detailed map is canonical in [`docs/architecture.md`](./docs/architecture.md
|
|
|
125
125
|
- 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.
|
|
126
126
|
- Remove dead code immediately. Reachability from composition roots, public exports, tests, registered surfaces, and documented APIs—not recent usefulness—determines whether code is live.
|
|
127
127
|
- Treat every meaningful `index.ts` edit as a composition-pressure check, but keep one-off live adapter wiring there when extraction would only hide cross-domain state.
|
|
128
|
-
- 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.
|
|
128
|
+
- 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 heading with terminal punctuation; callback alerts preserve equivalent emoji-led plain text because Telegram does not support rich formatting there.
|
|
129
129
|
- Markdown tables use compact source formatting with `---` separator cells and one surrounding space per cell. Preserve vendored references unchanged.
|
|
130
130
|
- Treat Windows filesystem, named-pipe, lock, heartbeat, and atomic-rename reports as high-signal evidence; reduce them to regressions or explicit platform caveats.
|
|
131
131
|
- Route significant runtime failures through the redacted recent-event recorder. Keep the compact TUI status at generic `error`; details belong in diagnostics.
|
package/BACKLOG.md
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
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
|
-
- [ ] `Coherent voice reply policy`: Release the validated `manual`, `mirror`, and `always` policy cleanup as `0.38.0`, then reconcile issue #235 and open exact migration issues for `johnlam1968/pi-voice-telegram` and `luxus/pi-xai-voice`, whose TTS providers still consume the removed transcript-caption API.
|
|
6
5
|
- [ ] `Operator-confirmed pairing`: Replace first-contact automatic ownership with an explicit pending request that the trusted Pi interface can allow or reject before `allowedUserId` is persisted or any Telegram prompt executes; retain manual preconfiguration, bound pending-request lifetime and multiplicity, fail closed across session/transport replacement, and cover unauthorized-first-contact races.
|
|
7
6
|
- [ ] `Environment-backed bot tokens`: Let each profile store an exact Pi-native `$ENV_VAR` or `${ENV_VAR}` reference in `telegram.json` instead of copying the resolved secret; preserve the originating alias when `/telegram-setup` prefills from a supported environment variable, resolve only at validation/activation boundaries, fail closed with a redacted named-variable diagnostic when unresolved, retain literal-token compatibility, and cover named profiles, reload, persistence, status, and secret-redaction paths.
|
|
8
7
|
- [ ] [`Inference bypass Generative Apps`](./docs/generative-apps.md): Let the agent install and bind one managed JavaScript app owner for a generated-prompt prefix so deterministic controls can bypass model inference without creating another button grammar.
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
> Each release keeps at most 8 outcome records of at most 512 characters.
|
|
4
4
|
|
|
5
|
+
## 0.39.1: Compaction Failure Formatting Hotfix
|
|
6
|
+
|
|
7
|
+
- `Compaction Failure Formatting`: Preserves the HTML parse mode when a confirmed `/compact` callback reports asynchronous failure, preventing bold standalone failure headings from appearing as literal `<b>…</b>` markup in Telegram.
|
|
8
|
+
|
|
9
|
+
## 0.39.0: Session-Owned Queue Lifecycle
|
|
10
|
+
|
|
11
|
+
- `Queue Counters`: Excludes Skip-marked prompts immediately from the shared executable queue count used by the Pi status bar and Telegram main menu while preserving physical queue position, deferred drop semantics, and reversible Keep behavior.
|
|
12
|
+
- `Skip Settlement`: Preserves durable receipts while a skipped prompt waits, then settles them atomically when that prompt reaches dispatch before dropping it without inference; settlement failure retains the head instead of creating replay ambiguity.
|
|
13
|
+
- `Session-Owned Queue`: Graceful shutdown discards all remaining queue receipts before clearing memory, while startup atomically discards receipts whose former process is proven dead instead of replaying them; replacement and unrelated sessions therefore start without inherited work.
|
|
14
|
+
- `Consistent Information Headings`: Standardizes standalone progress, success, empty, busy, unavailable, cancellation, and failure notices as fully bold emoji-led headings, while callback alerts retain equivalent plain text and automatic compaction waits for the preceding final answer before announcing its start.
|
|
15
|
+
- `Queue Menu`: Keeps skipped prompts visible in place with their physical ordinal struck through in list and detail views, so deferred removal is legible without renumbering or moving later work.
|
|
16
|
+
- `Agent Context`: Routes connected and Telegram-turn guidance across `telegram-bridge`, `generated-control-surface`, and `generative-apps` by activation role, asks the agent to load only missing Skill instructions, and removes Telegram tool context while transport is unavailable.
|
|
17
|
+
- `Bot API Reference`: Updates the vendored Telegram Bot API reference and lookup Skill through Bot API 10.3 while retaining the 10.0–10.3 freshness history and validated symbol, anchor, and line indexes.
|
|
18
|
+
|
|
5
19
|
## 0.38.0: Coherent Voice Reply Policy
|
|
6
20
|
|
|
7
21
|
- `Voice Reply Policy`: Renames the default user-facing `hidden` mode to `manual` while retaining `hidden` as a read-only configuration and callback compatibility alias. `manual`, `mirror`, and `always` remain the complete policy set: explicit `telegram_voice`, modality mirroring, or automatic voice replacement.
|
package/README.md
CHANGED
|
@@ -199,7 +199,7 @@ Queue policy:
|
|
|
199
199
|
- One prompt is one queue object with exactly one current lane and one current position; it never reserves a shadow place in the other lane.
|
|
200
200
|
- Priority and Normal are separate FIFO lanes; Priority dispatches first.
|
|
201
201
|
- Moving `Normal → Priority` removes the prompt from Normal and places it at the Priority tail. Moving `Priority → Normal` removes it from Priority and places it at the Normal tail; no former position is restored.
|
|
202
|
-
- Keep/Skip never changes lane position. Skip
|
|
202
|
+
- Keep/Skip never changes lane position. Skip preserves durable authority while waiting so Keep remains reversible, then settles that authority and drops the prompt without a model turn when dispatch reaches it. Skipped prompts stay visible at their physical queue position with a struck-through ordinal, but are excluded immediately from the executable queue count shown in both the Pi status bar and Telegram main menu. Graceful session shutdown discards all remaining queued authority, so a new session starts empty.
|
|
203
203
|
- Reactions control two independent dimensions; changing one category preserves the other:
|
|
204
204
|
- `Positive`: `👍`, `⚡️`, `❤️`, `🕊`, `🔥` — controls Priority.
|
|
205
205
|
- `Negative`: `👎`, `👻`, `💔`, `💩`, `🗑` — controls Skip.
|
package/docs/architecture.md
CHANGED
|
@@ -213,13 +213,13 @@ Compaction runs under the journal transaction lock when either 256 unapplied seg
|
|
|
213
213
|
|
|
214
214
|
Queue receipt ownership is independent from the Telegram transport lock. A same-instance, same-process generation may reconstruct its local receipt across a fenced session replacement and may settle it after transport ownership moves. A different process reports the receipt as foreign, never republishes it into its local queue, and cannot complete it even if it reads the acquisition id. Startup no longer treats process replacement as proof that an owner died: foreign and legacy unowned receipts remain durable.
|
|
215
215
|
|
|
216
|
-
|
|
216
|
+
Cleanup and live handoff are compare-and-set under the journal transaction. Queue discard during exact queue-lifecycle cancellation requires the exact local owner/acquisition and removes all receipt sources atomically. Before admission worker start, the lifecycle groups each foreign receipt and asks the journal to recheck OS pid liveness plus process-birth identity under the same transaction; a live owner returns `owner-alive` and a live owner without stable birth proof returns `owner-unverifiable`, both without mutation, while exact negative proof atomically discards the complete session-owned receipt without replay. Replacement registration carries its exact pid/process-birth before this check; when registration and cleanup race, that live identity wins the liveness proof and the queued receipt remains untouched. A replacement or unrelated worker therefore never executes queue work whose prior owner is proven dead.
|
|
217
217
|
|
|
218
218
|
Authenticated live handoff uses journal CAS plus bounded local IPC. The donor creates a one-time high-entropy token and durably offers the complete receipt to one exact recipient runtime/process/session identity; the journal stores only a digest bound to queue kind, receipt sources, donor acquisition, and recipient identity. While offered, donor completion/discard and dead-owner recovery fail closed, so authority cannot disappear during payload transfer. Prompt payloads carry all queue fields; control payloads carry only their stable `status`/`model` identity and rebuild executable closures locally. The separately negotiated `queue-handoff-v1` capability gates this envelope for leader and both peer generations. Each receipt carries its exact source journal binding; the donor derives the recipient follower-journal binding from the authenticated stable follower profile before routing. The bus validates payload shape/size and exact donor/recipient registration generations, and the recipient selects only that matching active lifecycle, stages one complete receipt idempotently, accepts the journal CAS, and returns the exact receipt plus newly minted owner in its ACK. Malformed, legacy-unbound, inactive-generation, or unavailable bindings fail closed.
|
|
219
219
|
|
|
220
220
|
During recipient staging, presenting the token atomically replaces the journal owner with a fresh acquisition carrying the handoff digest, removes the offer, and permanently fences donor settlement. The donor treats only an ACK carrying that exact accepted owner as success and never repeats acceptance against a donor-bound journal runtime. The recipient can repeat the same acceptance idempotently; a different token cannot claim an already accepted receipt. The coordinator contract orders offer → stage/accept exact receipt-and-owner ACK → donor removal → recipient readiness for direct leader→follower and follower→follower routing. Before acceptance, negative or mismatched acknowledgement exactly cancels the offer and keeps donor work. After acceptance, a lost acknowledgement cannot roll authority back: cancellation fails closed and donor memory remains frozen until exact accepted-owner reconciliation removes it. Recipient registration carries exact process-birth/session identity, and staged payloads remain outside the live dispatch store until accepted journal authority has been reconstructed. Production advertises `queue-handoff-v1` only with this exact role/journal selection and uses the same coordinator ordering for direct leader→follower and follower→follower routes.
|
|
221
221
|
|
|
222
|
-
Queued semantic authority has no elapsed-time lease. A timeout cannot prove either owner death or effect quiescence, so it cannot safely
|
|
222
|
+
Queued semantic authority has no elapsed-time lease. A timeout cannot prove either owner death or effect quiescence, so it cannot safely resolve a receipt. Resolution is limited to authenticated live handoff, exact owner discard/settlement, or transaction-rechecked negative PID plus process-birth evidence that permits terminal cleanup without replay. Live or unverifiable owners remain queued rather than risking duplicate or cross-session execution.
|
|
223
223
|
|
|
224
224
|
The initial `offset: -1` cursor bootstrap is allowed only when both cursor and journal are absent or empty. Thereafter process-level ordering is journal atomic rename → one monotonic offset atomic rename → worker signal. Failure before journal publication leaves the offset unchanged; failure after journal publication but before offset publication permits Telegram redelivery and journal dedupe; failure after offset publication but before worker signal replays from the journal on restart. Queue-owner, retry, terminal, handoff, and completion transitions use the same journal publication primitive and therefore share this process-crash boundary. The final completion window is at-least-once, so replay-sensitive external effects must use `update_id` or the stable delivery id as an idempotency key.
|
|
225
225
|
|
|
@@ -236,7 +236,7 @@ The canonical update transition contract is:
|
|
|
236
236
|
- `retry-wait → executing`: only after `nextRetryAtMs`; repeated signals before eligibility do not execute the entry. Automatic retries continue indefinitely with exponential `1s → 2s → 4s → 8s → 16s → 32s → 60s` delay capped at `60s`, while later independent updates continue draining.
|
|
237
237
|
- Legacy `failed → retry-wait`: startup atomically resumes terminal entries written by earlier `0.28.0` candidates. Runtime policy never silently discards durable inbound authority and exposes no Pi command for manual retry/discard.
|
|
238
238
|
- `queued → offered → staged → queued`: only the exact persisted donor may offer or cancel a live handoff; an offer preserves donor ownership but freezes ordinary settlement and recovery. Authenticated bounded IPC stages one exact payload/receipt outside the live queue. Exact recipient acceptance mints a fresh acquisition, reconstructs local ownership, removes donor work, then publishes recipient dispatch readiness.
|
|
239
|
-
- `queued → completed
|
|
239
|
+
- `queued → completed`: only the exact persisted owner receipt may complete or discard queued sources; generic completion rejects queued state. Process-birth-proven owner death atomically discards the complete unoffered session-owned receipt without replay; live, unverifiable, or offered owners remain queued.
|
|
240
240
|
|
|
241
241
|
The worker executes at most 64 eligible entries from one validated journal snapshot, commits ordinary completions through one journal transaction, then yields through a generation-checked event-loop boundary. Retry, queue, or prior-generation boundaries first flush completed ids and force a fresh snapshot, preserving exact state-transition atomicity without per-entry parse/rewrite churn. A deterministic 2,048-entry stress gate requires exactly 32 completion publications, 33 reads including the final empty snapshot, continued 1ms timer progress, and less than 250ms maximum observed heartbeat delay. Byte-capacity tests cover failed and retry-wait diagnostics, queue receipt/owner and handoff metadata, and operator dispositions; every rejected growth leaves the prior authority bytes unchanged. It still scans later independent entries after retry or terminal persistence. An unresolved reaction remains a queue-mutation dependency even in `retry-wait` or `failed`, but dispatch checks that dependency against the candidate queue item's exact chat and source message ids instead of globally blocking unrelated targets. Successful replay or an exact discard disposition releases the dependency. Worker state, debug status, state snapshots, and redacted runtime events expose journal depth, retry/terminal counts, the next retry, latest terminal identity, copyable operator commands, and the exact first foreign queued owner identity (instance, PID/birth, session, and acquisition) when semantic authority belongs to another process.
|
|
242
242
|
|
|
@@ -270,7 +270,7 @@ A prompt is one queue object with exactly one active lane membership and one cur
|
|
|
270
270
|
|
|
271
271
|
Telegram reactions are shortcut controls over those dimensions. Positive reactions (`👍`, `⚡️`, `❤️`, `🕊`, `🔥`) control Priority; negative reactions (`👎`, `👻`, `💔`, `💩`, `🗑`) control Skip. The runtime compares the complete old and new reaction sets and mutates only categories that changed, so adding or removing a negative reaction cannot silently change Priority, and changing a positive reaction cannot silently change Skip. The listed order selects the retained display emoji when several recognized emoji from one category coexist; it does not let one category override the other.
|
|
272
272
|
|
|
273
|
-
Priority and Skip may coexist, including `👍 + 💩`. The prompt remains at its Priority-lane position while waiting,
|
|
273
|
+
Priority and Skip may coexist, including `👍 + 💩`. The prompt remains at its Priority-lane position while waiting, and its durable journal receipts remain intact so Keep stays reversible and exact live handoff remains possible. Skip wins when dispatch reaches the prompt: the dispatcher first settles those receipts durably, then drops it without a model turn and continues; settlement failure retains the skipped head instead of allowing replay ambiguity. The prompt stays visible with only its negative emoji, is excluded immediately from the executable queue count shared by the Pi status bar and Telegram main menu, and keeps a struck-through physical ordinal. Returning it to Keep restores its contribution to the count without moving it. Graceful session shutdown discards all remaining queue receipts before clearing session-local memory, and startup discards receipts only after proving their former process dead, so a new or unrelated session never inherits queued work. Queue item detail exposes symmetric Priority/Normal and Keep/Skip selectors instead of an irreversible Delete action.
|
|
274
274
|
|
|
275
275
|
Menu and reaction controls share the same canonical queue state. A menu Keep can clear internal Skip without changing Priority or queue position, but Telegram's Bot API cannot remove a reaction created by the user; the visible user reaction can therefore remain until that user removes it. Once Pi has consumed or dropped a prompt, later reactions cannot retract or restore it.
|
|
276
276
|
|
package/docs/ui-style.md
CHANGED
|
@@ -26,8 +26,8 @@ Use emoji as stable semantic markers, not decoration. Emoji carry transportable
|
|
|
26
26
|
| `🧠` | Thinking level | `/thinking`, thinking menu headings, thinking status rows | Use only for reasoning/thinking controls. |
|
|
27
27
|
| `🔢` | Queue list / ordered work | `/queue`, queue menu entrypoints | Queue item rows may also use numeric labels. |
|
|
28
28
|
| `⏱️` | Queue is ticking / current work is active | Inline main-menu Queue row only | Running-clock queue state: the narrow present moment is being worked now. |
|
|
29
|
-
| `⏳` |
|
|
30
|
-
| `⌛` |
|
|
29
|
+
| `⏳` | Waiting / temporarily busy | Inline main-menu Queue row, busy notices | Hourglass means work or availability is pending; the sentence must name what is busy. |
|
|
30
|
+
| `⌛` | Empty / standing idle | Inline main-menu Queue row, empty-queue notices | Standing hourglass means no future work is waiting above the neck. |
|
|
31
31
|
| `⚙️` | Settings / configuration | Settings menu headings and Settings navigation rows | Extension-injected rows appear before the built-in `⚙️ Settings` row. |
|
|
32
32
|
| `🧩` | Extension-provided surface | Extension command examples, extension section examples | Companion extensions may choose their own emoji, but `🧩` means generic extension/plugin. |
|
|
33
33
|
| `👄` | Voice reply policy | Voice reply settings row and detail card | Not a generic audio attachment marker. |
|
|
@@ -43,19 +43,28 @@ Use emoji as stable semantic markers, not decoration. Emoji carry transportable
|
|
|
43
43
|
| --- | --- | --- | --- |
|
|
44
44
|
| `🟢` | Start / active / current positive state | `/start`, active row, current selected option, active `On` toggle | In command context it means “open/start menu”; in state context it means selected/active. |
|
|
45
45
|
| `🗜` | Compact session | `/compact`, compact confirmation action | Do not use for generic cleanup/delete. |
|
|
46
|
-
| `⏩` |
|
|
47
|
-
| `▶️` |
|
|
48
|
-
| `⏹️` | Abort current Pi work | `/abort` command description | Stops active work
|
|
49
|
-
| `🟥` |
|
|
46
|
+
| `⏩` | Abort and advance | Busy `/next` command result and matching menu action | Means the active turn is aborted before advancing to queued work. |
|
|
47
|
+
| `▶️` | Play / continue immediately | Idle `/next` result, `/continue` command, and matching menu action | Means work can start or resume directly without first aborting an active turn. |
|
|
48
|
+
| `⏹️` | Abort current Pi work | `/abort` command description and active `/stop` result | Stops active work; accompanying copy states separately when queued work is cleared. |
|
|
49
|
+
| `🟥` | Destructive stop command | `/stop` command description | Strong warning at the command/action entrypoint; standalone results use the more precise idle or abort state icon. |
|
|
50
50
|
| `🆕` | New session / fresh start | Reserved visible extension command example for `/new`-like flows | Same-thread Telegram `/new` is currently blocked by Pi core API; keep this meaning reserved. |
|
|
51
51
|
| `🌀` | Refresh | Queue refresh row and future refresh buttons | Re-fetch/re-render current surface, not transport reconnect. |
|
|
52
52
|
| `↪️` | Reroute to an existing target | Thread chooser buttons that send a captured command/message from one thread to another live thread | Curved arrow means the message arrived here but bends to another target. |
|
|
53
53
|
| `🔁` | Replace/restore mode | Thread replace/restore chooser entrypoints | Opens a second step for moving a Pi instance binding to the current source thread. |
|
|
54
54
|
| `➡️` | Choose replacement target | Thread replace/restore target buttons that select which Pi instance should move to the current thread | Use inside the second replace/restore chooser, not for ordinary reroutes. |
|
|
55
55
|
| `☑️` | Activate / choose this item | Model detail activation action, generated button-only choice heading | Positive selection cue; use `🟢 Active` for already-current state. |
|
|
56
|
-
| `❌` | No / cancel | Confirmation cancel buttons |
|
|
56
|
+
| `❌` | No / cancel / terminal failure | Confirmation cancel buttons and terminal failure notices | Do not use for a recoverable operation failure that leaves session state intact. |
|
|
57
57
|
| `🗑` | Delete / defer removal | Destructive confirmations and removal reaction | In the queue menu, reversible Keep/Skip selectors replace immediate deletion. |
|
|
58
58
|
|
|
59
|
+
### Informational Feedback
|
|
60
|
+
|
|
61
|
+
| Emoji | Meaning | Canonical surfaces | Notes |
|
|
62
|
+
| --- | --- | --- | --- |
|
|
63
|
+
| `💤` | Nothing active | No-active-turn notices | Neutral idle result, not an error. |
|
|
64
|
+
| `✅` | Completed successfully | Compaction and other completion notices | Use only after the operation has completed. |
|
|
65
|
+
| `🚫` | Unavailable, denied, or cancelled operation | Missing capability/auth notices, access denial, and compaction cancellation | Callback alerts use the same emoji without bold markup. |
|
|
66
|
+
| `⚠️` | Recoverable operation failure | Compaction failure notice | The attempted operation failed, but the original session state remains usable. |
|
|
67
|
+
|
|
59
68
|
### State Indicators And Button Grammars
|
|
60
69
|
|
|
61
70
|
| Emoji | Meaning | Canonical surfaces | Notes |
|
|
@@ -71,6 +80,8 @@ Use emoji as stable semantic markers, not decoration. Emoji carry transportable
|
|
|
71
80
|
|
|
72
81
|
Queue reactions are shortcut controls for waiting turns. Preserve their semantics across Telegram reactions, queue-menu rows, status previews, and tests. Positive emoji control the Priority/Normal lane; negative emoji control Keep/Skip. These categories are independent, may coexist, and mutate only their own dimension. Crossing lanes appends the prompt at the destination FIFO tail; changing Keep/Skip or changing emoji within one category preserves lane position. Skip wins only when dispatch reaches the prompt.
|
|
73
82
|
|
|
83
|
+
Skipped queue ordinals strike only the numeric position. Detail HTML closes `<s>` before the period; list-button labels place a hair-space boundary between the combining-struck number and the plain period so Telegram font overhang cannot visually strike punctuation.
|
|
84
|
+
|
|
74
85
|
Queue item detail renders two independent selector rows:
|
|
75
86
|
|
|
76
87
|
- `🟡 Priority` / `⚫️ Normal` or `⚫️ Priority` / `🟣 Normal` selects the lane.
|
|
@@ -209,11 +220,14 @@ Examples:
|
|
|
209
220
|
|
|
210
221
|
## Message Cards
|
|
211
222
|
|
|
212
|
-
Message cards sent by the bot should start with a strong heading.
|
|
223
|
+
Message cards and standalone informational notices sent by the bot should start with a strong heading.
|
|
213
224
|
|
|
214
225
|
Rules:
|
|
215
226
|
|
|
216
227
|
- Start with a bold heading or, for dialogs, a bold question.
|
|
228
|
+
- Format standalone notices as one fully bold line: relevant emoji, one space, concise sentence, and terminal period or colon.
|
|
229
|
+
- Apply the same hierarchy to success, progress, empty, busy, unavailable, cancellation, and failure notices; do not bold only a fragment of a standalone notice.
|
|
230
|
+
- Callback alerts remain plain text because Telegram does not support rich text there, but still keep the relevant emoji and concise sentence.
|
|
217
231
|
- Setting detail cards may include an emoji in the heading, then a colon and the current value in `<code>`.
|
|
218
232
|
- Explain what the setting does and what the options mean only as much as needed.
|
|
219
233
|
- Order setting value descriptions exactly like the chooser: rows top-to-bottom and values in a shared row left-to-right. Keep `(default)` on the actual default wherever it falls; default status never changes order.
|
package/index.ts
CHANGED
|
@@ -359,6 +359,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
359
359
|
getActiveToolExecutions: lifecycle.getActiveToolExecutions,
|
|
360
360
|
hasPendingModelSwitch: pendingModelSwitchStore.has,
|
|
361
361
|
getQueuedItems: telegramQueueStore.getQueuedItems,
|
|
362
|
+
getQueuedItemCount: Queue.countExecutableTelegramQueueItems,
|
|
362
363
|
formatQueuedStatus: Queue.formatQueuedTelegramItemsStatus,
|
|
363
364
|
getRecentRuntimeEvents: runtimeEvents.getEvents,
|
|
364
365
|
getRuntimeLockState: lockRuntime.getStatusLabel,
|
|
@@ -1234,6 +1235,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1234
1235
|
prepareTempDir,
|
|
1235
1236
|
updateStatus,
|
|
1236
1237
|
unbindDeferredDispatchContext: deferredQueueDispatchRuntime.unbind,
|
|
1238
|
+
discardQueuedItems: queueMutationRuntime.clear,
|
|
1237
1239
|
clearModelMenuState: modelMenuRuntime.clear,
|
|
1238
1240
|
getActiveTurnChatId: activeTurnRuntime.getChatId,
|
|
1239
1241
|
getActiveTurnTarget: activeTurnRuntime.getTarget,
|
package/lib/bindings.ts
CHANGED
|
@@ -95,11 +95,28 @@ export function createTelegramQueueBindingRuntime<TContext>(deps: {
|
|
|
95
95
|
sendUserMessage: Queue.TelegramQueueDispatchRuntimeDeps<TContext>["sendUserMessage"];
|
|
96
96
|
recordRuntimeEvent?: TelegramRuntimeEventRecorder;
|
|
97
97
|
}): TelegramQueueBindingRuntime<TContext> {
|
|
98
|
+
const settleDiscardedItems = (
|
|
99
|
+
items: readonly Queue.TelegramQueueItem<TContext>[],
|
|
100
|
+
ctx: TContext,
|
|
101
|
+
): boolean => {
|
|
102
|
+
const durableItems = items.filter(
|
|
103
|
+
(item) => (item.admissionReceipts?.length ?? 0) > 0,
|
|
104
|
+
);
|
|
105
|
+
if (durableItems.length === 0) return true;
|
|
106
|
+
const settlement = deps.admission.getSettlement();
|
|
107
|
+
if (!settlement) return false;
|
|
108
|
+
settlement.onItemsDiscarded(durableItems, ctx);
|
|
109
|
+
return durableItems.every((item) => !settlement.isItemReady(item));
|
|
110
|
+
};
|
|
98
111
|
const mutation = Queue.createTelegramQueueMutationController({
|
|
99
112
|
...deps.store,
|
|
100
113
|
allocateLaneOrder: deps.queue.allocateItemOrder,
|
|
101
114
|
onItemsDiscarded(items, ctx) {
|
|
102
|
-
|
|
115
|
+
if (!settleDiscardedItems(items, ctx)) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
"Telegram queue items could not be discarded durably.",
|
|
118
|
+
);
|
|
119
|
+
}
|
|
103
120
|
},
|
|
104
121
|
updateStatus: deps.updateStatus,
|
|
105
122
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
@@ -136,6 +153,9 @@ export function createTelegramQueueBindingRuntime<TContext>(deps: {
|
|
|
136
153
|
onControlSettled(item, ctx) {
|
|
137
154
|
deps.admission.getSettlement()?.onControlSettled(item, ctx);
|
|
138
155
|
},
|
|
156
|
+
onPromptSkipped(item, ctx) {
|
|
157
|
+
return settleDiscardedItems([item], ctx);
|
|
158
|
+
},
|
|
139
159
|
updateStatus: deps.updateStatus,
|
|
140
160
|
sendTextReply: deps.sendTextReply,
|
|
141
161
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
@@ -909,6 +929,25 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
909
929
|
{ replyToPrompt: false },
|
|
910
930
|
);
|
|
911
931
|
};
|
|
932
|
+
let activeTurnDeliveryTail = Promise.resolve();
|
|
933
|
+
const scheduleActiveTurnDelivery = (task: () => Promise<void>): void => {
|
|
934
|
+
const previous = activeTurnDeliveryTail;
|
|
935
|
+
activeTurnDeliveryTail = (async () => {
|
|
936
|
+
await previous;
|
|
937
|
+
await new Promise<void>((resolve) => {
|
|
938
|
+
const timer = setTimeout(resolve, 0);
|
|
939
|
+
timer.unref?.();
|
|
940
|
+
});
|
|
941
|
+
await task();
|
|
942
|
+
})().catch((error) => {
|
|
943
|
+
recordRuntimeEvent("delivery", error, {
|
|
944
|
+
phase: "agent-end-background-delivery",
|
|
945
|
+
});
|
|
946
|
+
});
|
|
947
|
+
};
|
|
948
|
+
const waitForActiveTurnDelivery = async (): Promise<void> => {
|
|
949
|
+
await activeTurnDeliveryTail;
|
|
950
|
+
};
|
|
912
951
|
const agentLifecycleHooks = Queue.createTelegramAgentLifecycleHooks<
|
|
913
952
|
Queue.PendingTelegramTurn,
|
|
914
953
|
Pi.ExtensionContext,
|
|
@@ -950,16 +989,7 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
950
989
|
dispatchNextQueuedTelegramTurn,
|
|
951
990
|
requestDeferredDispatchNextQueuedTelegramTurn:
|
|
952
991
|
deferredQueueDispatchRuntime.request,
|
|
953
|
-
scheduleActiveTurnDelivery
|
|
954
|
-
const timer = setTimeout(() => {
|
|
955
|
-
void task().catch((error) => {
|
|
956
|
-
recordRuntimeEvent("delivery", error, {
|
|
957
|
-
phase: "agent-end-background-delivery",
|
|
958
|
-
});
|
|
959
|
-
});
|
|
960
|
-
}, 0);
|
|
961
|
-
timer.unref?.();
|
|
962
|
-
},
|
|
992
|
+
scheduleActiveTurnDelivery,
|
|
963
993
|
clearPreview: previewRuntime.clear,
|
|
964
994
|
setPreviewPendingText: previewRuntime.setPendingText,
|
|
965
995
|
finalizeMarkdownPreview,
|
|
@@ -1079,7 +1109,11 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
1079
1109
|
activityRuntime.onCompactionStart(Pi.getSessionCompactionReason(event));
|
|
1080
1110
|
compactionObserver.onSessionBeforeCompact(event, ctx);
|
|
1081
1111
|
if (shouldNotify) {
|
|
1082
|
-
await
|
|
1112
|
+
await waitForActiveTurnDelivery();
|
|
1113
|
+
if (!isSessionContextActive(ctx)) return;
|
|
1114
|
+
await sendCompactionNotice(
|
|
1115
|
+
Commands.TELEGRAM_COMPACTION_STARTED_MARKDOWN,
|
|
1116
|
+
);
|
|
1083
1117
|
}
|
|
1084
1118
|
},
|
|
1085
1119
|
async onSessionCompact(event, ctx) {
|
|
@@ -1088,7 +1122,9 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
1088
1122
|
compactionObserver.onSessionCompact(event, ctx);
|
|
1089
1123
|
if (observedAutomaticCompaction) {
|
|
1090
1124
|
observedAutomaticCompaction = false;
|
|
1091
|
-
await sendCompactionNotice(
|
|
1125
|
+
await sendCompactionNotice(
|
|
1126
|
+
Commands.TELEGRAM_COMPACTION_COMPLETED_MARKDOWN,
|
|
1127
|
+
);
|
|
1092
1128
|
}
|
|
1093
1129
|
},
|
|
1094
1130
|
async onAgentStart(event, ctx) {
|
package/lib/commands.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
TELEGRAM_DEFAULT_PROFILE_NAME,
|
|
10
10
|
} from "./config.ts";
|
|
11
11
|
import type { ExtensionAPI, ExtensionCommandContext } from "./pi.ts";
|
|
12
|
+
import { escapeHtml } from "./rendering.ts";
|
|
12
13
|
import type { TelegramBridgeStatusLineOptions } from "./status.ts";
|
|
13
14
|
import {
|
|
14
15
|
createTelegramControlItemBuilder,
|
|
@@ -185,9 +186,24 @@ export function formatTelegramCommandEmojiPrefix(
|
|
|
185
186
|
return `${getTelegramCommandEmoji(command)} `;
|
|
186
187
|
}
|
|
187
188
|
|
|
189
|
+
export function formatTelegramInformationHeading(
|
|
190
|
+
emoji: string,
|
|
191
|
+
text: string,
|
|
192
|
+
): string {
|
|
193
|
+
return `<b>${escapeHtml(emoji)} ${escapeHtml(text)}</b>`;
|
|
194
|
+
}
|
|
195
|
+
|
|
188
196
|
export const TELEGRAM_COMPACTION_STARTED_TEXT =
|
|
189
|
-
|
|
190
|
-
|
|
197
|
+
formatTelegramInformationHeading(
|
|
198
|
+
getTelegramCommandEmoji("compact"),
|
|
199
|
+
"Compaction started.",
|
|
200
|
+
);
|
|
201
|
+
export const TELEGRAM_COMPACTION_COMPLETED_TEXT =
|
|
202
|
+
formatTelegramInformationHeading("✅", "Compaction completed.");
|
|
203
|
+
export const TELEGRAM_COMPACTION_STARTED_MARKDOWN =
|
|
204
|
+
`**${formatTelegramCommandEmojiPrefix("compact")}Compaction started.**`;
|
|
205
|
+
export const TELEGRAM_COMPACTION_COMPLETED_MARKDOWN =
|
|
206
|
+
"**✅ Compaction completed.**";
|
|
191
207
|
|
|
192
208
|
function formatTelegramBotCommandDescription(
|
|
193
209
|
command: TelegramCommandEmojiName,
|
|
@@ -585,7 +601,10 @@ export interface TelegramStopCommandDeps {
|
|
|
585
601
|
setFoldQueuedPromptsIntoHistory: (fold: boolean) => void;
|
|
586
602
|
abortCurrentTurn: () => void;
|
|
587
603
|
updateStatus: () => void;
|
|
588
|
-
sendTextReply: (
|
|
604
|
+
sendTextReply: (
|
|
605
|
+
text: string,
|
|
606
|
+
options?: { parseMode?: "HTML" },
|
|
607
|
+
) => Promise<void>;
|
|
589
608
|
}
|
|
590
609
|
|
|
591
610
|
export interface TelegramRuntimeEventRecorderPort {
|
|
@@ -619,7 +638,10 @@ export interface TelegramCompactCommandDeps extends TelegramRuntimeEventRecorder
|
|
|
619
638
|
onComplete: () => void;
|
|
620
639
|
onError: (error: unknown) => void;
|
|
621
640
|
}) => void;
|
|
622
|
-
sendTextReply: (
|
|
641
|
+
sendTextReply: (
|
|
642
|
+
text: string,
|
|
643
|
+
options?: { parseMode?: "HTML" },
|
|
644
|
+
) => Promise<void>;
|
|
623
645
|
suppressStartNotice?: boolean;
|
|
624
646
|
}
|
|
625
647
|
|
|
@@ -921,8 +943,11 @@ export function createTelegramCommandTargetRuntime<
|
|
|
921
943
|
await deps.sendTextReply(
|
|
922
944
|
target.chatId,
|
|
923
945
|
target.replyToMessageId,
|
|
924
|
-
|
|
925
|
-
|
|
946
|
+
formatTelegramInformationHeading(
|
|
947
|
+
"🚫",
|
|
948
|
+
"Settings menu is unavailable.",
|
|
949
|
+
),
|
|
950
|
+
{ target, parseMode: "HTML" },
|
|
926
951
|
);
|
|
927
952
|
return;
|
|
928
953
|
}
|
|
@@ -1108,6 +1133,25 @@ function getTelegramCommandErrorMessage(error: unknown): string {
|
|
|
1108
1133
|
return error instanceof Error ? error.message : String(error);
|
|
1109
1134
|
}
|
|
1110
1135
|
|
|
1136
|
+
function formatTelegramCompactionFailure(error: unknown): string {
|
|
1137
|
+
let message = getTelegramCommandErrorMessage(error).trim();
|
|
1138
|
+
const redundantPrefixes = [
|
|
1139
|
+
"Compaction failed: ",
|
|
1140
|
+
"Turn prefix summarization failed: ",
|
|
1141
|
+
];
|
|
1142
|
+
let stripped = true;
|
|
1143
|
+
while (stripped) {
|
|
1144
|
+
stripped = false;
|
|
1145
|
+
for (const prefix of redundantPrefixes) {
|
|
1146
|
+
if (!message.startsWith(prefix)) continue;
|
|
1147
|
+
message = message.slice(prefix.length).trim();
|
|
1148
|
+
stripped = true;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
const sentence = /[.!?]$/u.test(message) ? message : `${message}.`;
|
|
1152
|
+
return `Compaction failed! ${sentence}`;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1111
1155
|
export function parseTelegramCommand(
|
|
1112
1156
|
text: string,
|
|
1113
1157
|
): ParsedTelegramCommand | undefined {
|
|
@@ -1165,7 +1209,10 @@ export async function handleTelegramStopCommand(
|
|
|
1165
1209
|
? ` Cleared ${formatTelegramQueuedTurnCount(clearedCount)}.`
|
|
1166
1210
|
: "";
|
|
1167
1211
|
if (clearedCount > 0) deps.updateStatus();
|
|
1168
|
-
await deps.sendTextReply(
|
|
1212
|
+
await deps.sendTextReply(
|
|
1213
|
+
formatTelegramInformationHeading("💤", `No active turn.${clearedSuffix}`),
|
|
1214
|
+
{ parseMode: "HTML" },
|
|
1215
|
+
);
|
|
1169
1216
|
return;
|
|
1170
1217
|
}
|
|
1171
1218
|
deps.abortCurrentTurn();
|
|
@@ -1174,7 +1221,13 @@ export async function handleTelegramStopCommand(
|
|
|
1174
1221
|
clearedCount > 0
|
|
1175
1222
|
? ` Cleared ${formatTelegramQueuedTurnCount(clearedCount)}.`
|
|
1176
1223
|
: "";
|
|
1177
|
-
await deps.sendTextReply(
|
|
1224
|
+
await deps.sendTextReply(
|
|
1225
|
+
formatTelegramInformationHeading(
|
|
1226
|
+
"⏹️",
|
|
1227
|
+
`Aborted current turn.${clearedSuffix}`,
|
|
1228
|
+
),
|
|
1229
|
+
{ parseMode: "HTML" },
|
|
1230
|
+
);
|
|
1178
1231
|
}
|
|
1179
1232
|
|
|
1180
1233
|
export async function handleTelegramAbortCommand(deps: {
|
|
@@ -1184,17 +1237,26 @@ export async function handleTelegramAbortCommand(deps: {
|
|
|
1184
1237
|
abortCurrentTurn: () => void;
|
|
1185
1238
|
setFoldQueuedPromptsIntoHistory: (fold: boolean) => void;
|
|
1186
1239
|
updateStatus: () => void;
|
|
1187
|
-
sendTextReply: (
|
|
1240
|
+
sendTextReply: (
|
|
1241
|
+
text: string,
|
|
1242
|
+
options?: { parseMode?: "HTML" },
|
|
1243
|
+
) => Promise<void>;
|
|
1188
1244
|
}): Promise<void> {
|
|
1189
1245
|
deps.clearPendingModelSwitch();
|
|
1190
1246
|
if (!deps.hasAbortHandler()) {
|
|
1191
|
-
await deps.sendTextReply(
|
|
1247
|
+
await deps.sendTextReply(
|
|
1248
|
+
formatTelegramInformationHeading("💤", "No active turn."),
|
|
1249
|
+
{ parseMode: "HTML" },
|
|
1250
|
+
);
|
|
1192
1251
|
return;
|
|
1193
1252
|
}
|
|
1194
1253
|
deps.setFoldQueuedPromptsIntoHistory(deps.hasActiveTelegramTurn());
|
|
1195
1254
|
deps.abortCurrentTurn();
|
|
1196
1255
|
deps.updateStatus();
|
|
1197
|
-
await deps.sendTextReply(
|
|
1256
|
+
await deps.sendTextReply(
|
|
1257
|
+
formatTelegramInformationHeading("⏹️", "Aborted current turn."),
|
|
1258
|
+
{ parseMode: "HTML" },
|
|
1259
|
+
);
|
|
1198
1260
|
}
|
|
1199
1261
|
|
|
1200
1262
|
export async function handleTelegramNextCommand(deps: {
|
|
@@ -1213,7 +1275,10 @@ export async function handleTelegramNextCommand(deps: {
|
|
|
1213
1275
|
}): Promise<void> {
|
|
1214
1276
|
deps.clearPendingModelSwitch();
|
|
1215
1277
|
if (!deps.hasQueuedItems()) {
|
|
1216
|
-
await deps.sendTextReply(
|
|
1278
|
+
await deps.sendTextReply(
|
|
1279
|
+
formatTelegramInformationHeading("⌛", "Queue is empty."),
|
|
1280
|
+
{ parseMode: "HTML" },
|
|
1281
|
+
);
|
|
1217
1282
|
return;
|
|
1218
1283
|
}
|
|
1219
1284
|
if (!deps.isIdle() && deps.hasAbortHandler()) {
|
|
@@ -1221,17 +1286,30 @@ export async function handleTelegramNextCommand(deps: {
|
|
|
1221
1286
|
deps.abortCurrentTurn();
|
|
1222
1287
|
deps.updateStatus();
|
|
1223
1288
|
await deps.sendTextReply(
|
|
1224
|
-
|
|
1289
|
+
formatTelegramInformationHeading(
|
|
1290
|
+
"⏩",
|
|
1291
|
+
"Aborted! Dispatching next queued turn.",
|
|
1292
|
+
),
|
|
1293
|
+
{ parseMode: "HTML" },
|
|
1225
1294
|
);
|
|
1226
1295
|
return;
|
|
1227
1296
|
}
|
|
1228
1297
|
if (!deps.isIdle()) {
|
|
1229
|
-
await deps.sendTextReply(
|
|
1298
|
+
await deps.sendTextReply(
|
|
1299
|
+
formatTelegramInformationHeading(
|
|
1300
|
+
"⏳",
|
|
1301
|
+
"Pi is busy. Send /abort or /stop first.",
|
|
1302
|
+
),
|
|
1303
|
+
{ parseMode: "HTML" },
|
|
1304
|
+
);
|
|
1230
1305
|
return;
|
|
1231
1306
|
}
|
|
1232
1307
|
deps.dispatchNextQueuedTurn();
|
|
1233
1308
|
deps.updateStatus();
|
|
1234
|
-
await deps.sendTextReply(
|
|
1309
|
+
await deps.sendTextReply(
|
|
1310
|
+
formatTelegramInformationHeading("▶️", "Dispatching next queued turn."),
|
|
1311
|
+
{ parseMode: "HTML" },
|
|
1312
|
+
);
|
|
1235
1313
|
}
|
|
1236
1314
|
|
|
1237
1315
|
export async function handleTelegramContinueCommand<TMessage, TContext>(
|
|
@@ -1301,15 +1379,15 @@ export async function handleTelegramCompactConfirmationCallback<TContext>(
|
|
|
1301
1379
|
const chatId = callbackMessage?.chat?.id;
|
|
1302
1380
|
const messageId = callbackMessage?.message_id;
|
|
1303
1381
|
if (typeof chatId !== "number" || typeof messageId !== "number") {
|
|
1304
|
-
await deps.answerCallbackQuery(query.id, "Interactive message expired.");
|
|
1382
|
+
await deps.answerCallbackQuery(query.id, "⌛ Interactive message expired.");
|
|
1305
1383
|
return true;
|
|
1306
1384
|
}
|
|
1307
1385
|
if (query.data === "compact:cancel") {
|
|
1308
1386
|
await deps.editInteractiveMessage(
|
|
1309
1387
|
chatId,
|
|
1310
1388
|
messageId,
|
|
1311
|
-
"Compaction cancelled
|
|
1312
|
-
"
|
|
1389
|
+
"<b>🚫 Compaction cancelled.</b>",
|
|
1390
|
+
"html",
|
|
1313
1391
|
{ inline_keyboard: [] },
|
|
1314
1392
|
);
|
|
1315
1393
|
await deps.answerCallbackQuery(query.id);
|
|
@@ -1319,7 +1397,7 @@ export async function handleTelegramCompactConfirmationCallback<TContext>(
|
|
|
1319
1397
|
chatId,
|
|
1320
1398
|
messageId,
|
|
1321
1399
|
TELEGRAM_COMPACTION_STARTED_TEXT,
|
|
1322
|
-
"
|
|
1400
|
+
"html",
|
|
1323
1401
|
{ inline_keyboard: [] },
|
|
1324
1402
|
);
|
|
1325
1403
|
await deps.answerCallbackQuery(query.id);
|
|
@@ -1345,7 +1423,11 @@ export async function handleTelegramCompactCommand(
|
|
|
1345
1423
|
deps.isCompactionInProgress()
|
|
1346
1424
|
) {
|
|
1347
1425
|
await deps.sendTextReply(
|
|
1348
|
-
|
|
1426
|
+
formatTelegramInformationHeading(
|
|
1427
|
+
"⏳",
|
|
1428
|
+
"Cannot compact while Pi or the Telegram queue is busy. Wait for queued turns to finish or send /abort first.",
|
|
1429
|
+
),
|
|
1430
|
+
{ parseMode: "HTML" },
|
|
1349
1431
|
);
|
|
1350
1432
|
return;
|
|
1351
1433
|
}
|
|
@@ -1359,7 +1441,9 @@ export async function handleTelegramCompactCommand(
|
|
|
1359
1441
|
deps.setCompactionInProgress(false);
|
|
1360
1442
|
deps.updateStatus();
|
|
1361
1443
|
dispatchNextQueuedTelegramTurnAfterCompact(deps);
|
|
1362
|
-
void deps.sendTextReply(TELEGRAM_COMPACTION_COMPLETED_TEXT
|
|
1444
|
+
void deps.sendTextReply(TELEGRAM_COMPACTION_COMPLETED_TEXT, {
|
|
1445
|
+
parseMode: "HTML",
|
|
1446
|
+
});
|
|
1363
1447
|
},
|
|
1364
1448
|
onError: (error) => {
|
|
1365
1449
|
deps.stopTypingLoop?.();
|
|
@@ -1367,8 +1451,13 @@ export async function handleTelegramCompactCommand(
|
|
|
1367
1451
|
deps.updateStatus();
|
|
1368
1452
|
dispatchNextQueuedTelegramTurnAfterCompact(deps);
|
|
1369
1453
|
deps.recordRuntimeEvent?.("compact", error);
|
|
1370
|
-
|
|
1371
|
-
|
|
1454
|
+
void deps.sendTextReply(
|
|
1455
|
+
formatTelegramInformationHeading(
|
|
1456
|
+
"⚠️",
|
|
1457
|
+
formatTelegramCompactionFailure(error),
|
|
1458
|
+
),
|
|
1459
|
+
{ parseMode: "HTML" },
|
|
1460
|
+
);
|
|
1372
1461
|
},
|
|
1373
1462
|
});
|
|
1374
1463
|
} catch (error) {
|
|
@@ -1376,14 +1465,19 @@ export async function handleTelegramCompactCommand(
|
|
|
1376
1465
|
deps.setCompactionInProgress(false);
|
|
1377
1466
|
deps.updateStatus();
|
|
1378
1467
|
deps.recordRuntimeEvent?.("compact", error);
|
|
1379
|
-
|
|
1380
|
-
|
|
1468
|
+
await deps.sendTextReply(
|
|
1469
|
+
formatTelegramInformationHeading(
|
|
1470
|
+
"⚠️",
|
|
1471
|
+
formatTelegramCompactionFailure(error),
|
|
1472
|
+
),
|
|
1473
|
+
{ parseMode: "HTML" },
|
|
1474
|
+
);
|
|
1381
1475
|
return;
|
|
1382
1476
|
}
|
|
1383
1477
|
if (!deps.suppressStartNotice) {
|
|
1384
|
-
await deps.sendTextReply(
|
|
1385
|
-
|
|
1386
|
-
);
|
|
1478
|
+
await deps.sendTextReply(TELEGRAM_COMPACTION_STARTED_TEXT, {
|
|
1479
|
+
parseMode: "HTML",
|
|
1480
|
+
});
|
|
1387
1481
|
}
|
|
1388
1482
|
}
|
|
1389
1483
|
|
package/lib/journal.ts
CHANGED
|
@@ -3381,15 +3381,8 @@ export function createTelegramUpdateJournalStore(
|
|
|
3381
3381
|
const recoveredUpdateIds = [...requestedIds].sort((a, b) => a - b);
|
|
3382
3382
|
const published = publishMutation(
|
|
3383
3383
|
current,
|
|
3384
|
-
current.file.entries.
|
|
3385
|
-
requestedIds.has(entry.updateId)
|
|
3386
|
-
? {
|
|
3387
|
-
updateId: entry.updateId,
|
|
3388
|
-
update: entry.update,
|
|
3389
|
-
admittedAtMs: entry.admittedAtMs,
|
|
3390
|
-
state: "pending" as const,
|
|
3391
|
-
}
|
|
3392
|
-
: entry,
|
|
3384
|
+
current.file.entries.filter(
|
|
3385
|
+
(entry) => !requestedIds.has(entry.updateId),
|
|
3393
3386
|
),
|
|
3394
3387
|
true,
|
|
3395
3388
|
);
|
package/lib/lifecycle.ts
CHANGED
|
@@ -345,25 +345,27 @@ export function createTelegramBridgeSessionLifecycleAssembly<
|
|
|
345
345
|
stopPolling: suspendForReplacement,
|
|
346
346
|
clearPendingMediaGroups: deps.services.suspendGroupedInput,
|
|
347
347
|
});
|
|
348
|
-
const servicesLifecycle =
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
},
|
|
358
|
-
async onSessionShutdown() {
|
|
359
|
-
await deps.services.delivery.onSessionShutdown();
|
|
360
|
-
deps.services.queueWatchdog.stop();
|
|
361
|
-
deps.services.capabilityMonitor.stop();
|
|
362
|
-
await deps.services.inboundWorker.onSessionShutdown();
|
|
363
|
-
},
|
|
348
|
+
const servicesLifecycle: TelegramSessionLifecycleHooks = {
|
|
349
|
+
async onSessionStart(event, ctx) {
|
|
350
|
+
await queueLifecycle.onSessionStart(event, ctx);
|
|
351
|
+
if (!isSessionActive(ctx)) return;
|
|
352
|
+
deps.services.resumeGroupedInput(ctx);
|
|
353
|
+
await deps.services.delivery.onSessionStart();
|
|
354
|
+
await deps.services.polling.onSessionStart(event, ctx);
|
|
355
|
+
deps.services.capabilityMonitor.start(ctx);
|
|
356
|
+
deps.services.queueWatchdog.start(ctx);
|
|
364
357
|
},
|
|
365
|
-
|
|
366
|
-
|
|
358
|
+
async onSessionShutdown(event, ctx) {
|
|
359
|
+
if (!isSessionActive(ctx)) return;
|
|
360
|
+
await deps.services.delivery.onSessionShutdown();
|
|
361
|
+
if (!isSessionActive(ctx)) return;
|
|
362
|
+
deps.services.queueWatchdog.stop();
|
|
363
|
+
deps.services.capabilityMonitor.stop();
|
|
364
|
+
await queueLifecycle.onSessionShutdown(event, ctx);
|
|
365
|
+
if (!isSessionActive(ctx)) return;
|
|
366
|
+
await deps.services.inboundWorker.onSessionShutdown();
|
|
367
|
+
},
|
|
368
|
+
};
|
|
367
369
|
const followerLifecycle = appendTelegramLifecycleHooks(
|
|
368
370
|
servicesLifecycle,
|
|
369
371
|
{
|
package/lib/menu-queue.ts
CHANGED
|
@@ -73,6 +73,10 @@ function toTelegramQueueMenuItems<Context>(
|
|
|
73
73
|
});
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
function formatSkippedTelegramQueuePosition(position: number): string {
|
|
77
|
+
return Array.from(String(position), (char) => `${char}\u0335`).join("");
|
|
78
|
+
}
|
|
79
|
+
|
|
76
80
|
function buildTelegramQueueMenuReplyMarkup(
|
|
77
81
|
items: readonly TelegramQueueMenuItem[],
|
|
78
82
|
emptyRefreshIndex = 0,
|
|
@@ -86,7 +90,7 @@ function buildTelegramQueueMenuReplyMarkup(
|
|
|
86
90
|
: "queue:refresh";
|
|
87
91
|
const refreshRow = [{ text: "🌀 Refresh", callback_data: refreshData }];
|
|
88
92
|
if (items.length === 0) return { inline_keyboard: [backRow, refreshRow] };
|
|
89
|
-
const rows = items.map((item
|
|
93
|
+
const rows = items.map((item) => {
|
|
90
94
|
const prefix = item.reactionSuppressionEmoji
|
|
91
95
|
? `${item.reactionSuppressionEmoji} `
|
|
92
96
|
: item.isPriority
|
|
@@ -94,7 +98,11 @@ function buildTelegramQueueMenuReplyMarkup(
|
|
|
94
98
|
: item.hasAttachments
|
|
95
99
|
? "📎 "
|
|
96
100
|
: "";
|
|
97
|
-
const
|
|
101
|
+
const position = item.reactionSuppressionEmoji
|
|
102
|
+
? formatSkippedTelegramQueuePosition(item.queuePosition)
|
|
103
|
+
: String(item.queuePosition);
|
|
104
|
+
const ordinalSeparator = item.reactionSuppressionEmoji ? "\u200A" : "";
|
|
105
|
+
const label = `${position}${ordinalSeparator}. ${prefix}${item.statusSummary}`;
|
|
98
106
|
return [
|
|
99
107
|
{
|
|
100
108
|
text: label,
|
|
@@ -162,7 +170,10 @@ function getTelegramQueueMenuItemText(item: TelegramQueueMenuItem): string {
|
|
|
162
170
|
: item.isPriority
|
|
163
171
|
? ` ${item.priorityEmoji ?? "⚡"}`
|
|
164
172
|
: "";
|
|
165
|
-
const
|
|
173
|
+
const position = item.reactionSuppressionEmoji
|
|
174
|
+
? `<s>${item.queuePosition}</s>.`
|
|
175
|
+
: `<b>${item.queuePosition}.</b>`;
|
|
176
|
+
const heading = `${position}${badge}`;
|
|
166
177
|
const preview = `<pre>${escapeTelegramQueueMenuHtmlPreview(item.promptText)}</pre>`;
|
|
167
178
|
return `${heading}\n${preview}`;
|
|
168
179
|
}
|
|
@@ -182,7 +193,7 @@ function buildTelegramQueueItemSubmenuReplyMarkup(
|
|
|
182
193
|
callback_data: `queue:prio-set:${chatId}:${replyToMessageId}:priority`,
|
|
183
194
|
},
|
|
184
195
|
{
|
|
185
|
-
text: isPriority ? "⚫️ Normal" : "
|
|
196
|
+
text: isPriority ? "⚫️ Normal" : "🔵 Normal",
|
|
186
197
|
callback_data: `queue:prio-set:${chatId}:${replyToMessageId}:normal`,
|
|
187
198
|
},
|
|
188
199
|
],
|
package/lib/menu.ts
CHANGED
|
@@ -255,7 +255,10 @@ export interface TelegramMenuActionRuntimeDeps<
|
|
|
255
255
|
chatId: number,
|
|
256
256
|
replyToMessageId: number,
|
|
257
257
|
text: string,
|
|
258
|
-
options?: {
|
|
258
|
+
options?: {
|
|
259
|
+
target?: { chatId: number; threadId?: number };
|
|
260
|
+
parseMode?: "HTML";
|
|
261
|
+
},
|
|
259
262
|
) => Promise<unknown>;
|
|
260
263
|
sectionRegistry?: TelegramSectionRegistry;
|
|
261
264
|
isVoiceReplyActive?: () => boolean;
|
|
@@ -798,8 +801,8 @@ export function createTelegramMenuActionRuntime<
|
|
|
798
801
|
await deps.sendTextReply(
|
|
799
802
|
chatId,
|
|
800
803
|
replyToMessageId,
|
|
801
|
-
"Cannot open status while Pi is busy. Send /abort, /next, or /stop
|
|
802
|
-
{ target: { chatId, threadId } },
|
|
804
|
+
"<b>⏳ Cannot open status while Pi is busy. Send /abort, /next, or /stop.</b>",
|
|
805
|
+
{ target: { chatId, threadId }, parseMode: "HTML" },
|
|
803
806
|
);
|
|
804
807
|
},
|
|
805
808
|
getModelMenuState: () => deps.getModelMenuState(chatId, ctx, threadId),
|
|
@@ -835,16 +838,16 @@ export function createTelegramMenuActionRuntime<
|
|
|
835
838
|
await deps.sendTextReply(
|
|
836
839
|
chatId,
|
|
837
840
|
replyToMessageId,
|
|
838
|
-
"Cannot switch model while Pi is busy. Send /abort, /next, or /stop
|
|
839
|
-
{ target: { chatId, threadId } },
|
|
841
|
+
"<b>⏳ Cannot switch model while Pi is busy. Send /abort, /next, or /stop.</b>",
|
|
842
|
+
{ target: { chatId, threadId }, parseMode: "HTML" },
|
|
840
843
|
);
|
|
841
844
|
},
|
|
842
845
|
sendNoModelsMessage: async () => {
|
|
843
846
|
await deps.sendTextReply(
|
|
844
847
|
chatId,
|
|
845
848
|
replyToMessageId,
|
|
846
|
-
"No available models with configured auth
|
|
847
|
-
{ target: { chatId, threadId } },
|
|
849
|
+
"<b>🚫 No available models with configured auth.</b>",
|
|
850
|
+
{ target: { chatId, threadId }, parseMode: "HTML" },
|
|
848
851
|
);
|
|
849
852
|
},
|
|
850
853
|
getModelMenuState: () => deps.getModelMenuState(chatId, ctx, threadId),
|
package/lib/prompts.ts
CHANGED
|
@@ -14,11 +14,11 @@ export const TELEGRAM_DISCONNECTED_CONTEXT_MESSAGE =
|
|
|
14
14
|
|
|
15
15
|
const LOCAL_SYSTEM_PROMPT_SUFFIX = `
|
|
16
16
|
|
|
17
|
-
${TELEGRAM_CONNECTED_CONTEXT_MESSAGE}
|
|
17
|
+
${TELEGRAM_CONNECTED_CONTEXT_MESSAGE} For Telegram work, consult bundled Skills in routing order: \`telegram-bridge\` for the transport and turn protocol, \`generated-control-surface\` when contextual controls materially shorten feedback, then \`generative-apps\` when the interaction warrants a reusable deterministic app. Load a Skill only if its instructions are not already present in the current context. Do not use Telegram-specific features from unrelated local/TUI prompts.`;
|
|
18
18
|
|
|
19
19
|
const TELEGRAM_TURN_SYSTEM_PROMPT_SUFFIX = `
|
|
20
20
|
|
|
21
|
-
Telegram turn note:
|
|
21
|
+
Telegram turn note: Follow the applicable bundled Telegram Skills in routing order; load only missing instructions.`;
|
|
22
22
|
|
|
23
23
|
export const TELEGRAM_ATTACH_PROMPT_SNIPPET =
|
|
24
24
|
"Queue files for the active Telegram reply; outside Telegram turns, send files directly to Telegram.";
|
package/lib/queue.ts
CHANGED
|
@@ -427,12 +427,22 @@ export function createTelegramTransportStampedQueueStore<TContext>(
|
|
|
427
427
|
};
|
|
428
428
|
}
|
|
429
429
|
|
|
430
|
+
export function isTelegramQueueItemSkipped<TContext = unknown>(
|
|
431
|
+
item: TelegramQueueItem<TContext>,
|
|
432
|
+
): boolean {
|
|
433
|
+
return item.kind === "prompt" && Boolean(item.reactionSuppressionEmoji);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export function countExecutableTelegramQueueItems<TContext = unknown>(
|
|
437
|
+
items: readonly TelegramQueueItem<TContext>[],
|
|
438
|
+
): number {
|
|
439
|
+
return items.filter((item) => !isTelegramQueueItemSkipped(item)).length;
|
|
440
|
+
}
|
|
441
|
+
|
|
430
442
|
export function createTelegramQueueItemCountGetter<TContext = unknown>(
|
|
431
443
|
store: Pick<TelegramQueueStore<TContext>, "getQueuedItems">,
|
|
432
444
|
): () => number {
|
|
433
|
-
return () =>
|
|
434
|
-
return store.getQueuedItems().length;
|
|
435
|
-
};
|
|
445
|
+
return () => countExecutableTelegramQueueItems(store.getQueuedItems());
|
|
436
446
|
}
|
|
437
447
|
|
|
438
448
|
export function createTelegramActiveTurnStore<
|
|
@@ -993,7 +1003,8 @@ export function consumeDispatchedTelegramPrompt<TContext = unknown>(
|
|
|
993
1003
|
export function formatQueuedTelegramItemsStatus<TContext = unknown>(
|
|
994
1004
|
items: TelegramQueueItem<TContext>[],
|
|
995
1005
|
): string {
|
|
996
|
-
|
|
1006
|
+
const count = countExecutableTelegramQueueItems(items);
|
|
1007
|
+
return count === 0 ? "" : ` +${count}`;
|
|
997
1008
|
}
|
|
998
1009
|
|
|
999
1010
|
export function truncateTelegramQueueSummary(
|
|
@@ -2062,6 +2073,7 @@ export interface TelegramSessionStartRuntimeDeps<TContext, TModel = unknown> {
|
|
|
2062
2073
|
export interface TelegramSessionShutdownRuntimeDeps<TQueueItem> {
|
|
2063
2074
|
isSessionActive?: () => boolean;
|
|
2064
2075
|
unbindDeferredDispatchContext?: () => void;
|
|
2076
|
+
discardQueuedItems?: () => void;
|
|
2065
2077
|
applyState: (state: TelegramSessionShutdownState<TQueueItem>) => void;
|
|
2066
2078
|
clearPendingMediaGroups: () => void;
|
|
2067
2079
|
clearModelMenuState: () => void;
|
|
@@ -2090,6 +2102,7 @@ export interface TelegramSessionLifecycleHookRuntimeDeps<
|
|
|
2090
2102
|
updateStatus: (ctx: TContext) => void;
|
|
2091
2103
|
isSessionActive?: (ctx: TContext) => boolean;
|
|
2092
2104
|
unbindDeferredDispatchContext?: () => void;
|
|
2105
|
+
discardQueuedItems?: (ctx: TContext) => void;
|
|
2093
2106
|
applySessionShutdownState: (
|
|
2094
2107
|
state: TelegramSessionShutdownState<TQueueItem>,
|
|
2095
2108
|
) => void;
|
|
@@ -2286,6 +2299,7 @@ export async function shutdownTelegramSessionRuntime<TQueueItem>(
|
|
|
2286
2299
|
deps.unbindDeferredDispatchContext?.();
|
|
2287
2300
|
await deps.stopPolling();
|
|
2288
2301
|
if (deps.isSessionActive?.() === false) return;
|
|
2302
|
+
deps.discardQueuedItems?.();
|
|
2289
2303
|
deps.applyState(buildTelegramSessionShutdownState<TQueueItem>());
|
|
2290
2304
|
deps.clearPendingMediaGroups();
|
|
2291
2305
|
deps.clearModelMenuState();
|
|
@@ -2339,6 +2353,7 @@ export function createTelegramSessionLifecycleRuntime<
|
|
|
2339
2353
|
updateStatus: deps.updateStatus,
|
|
2340
2354
|
isSessionActive: deps.isSessionActive,
|
|
2341
2355
|
unbindDeferredDispatchContext: deps.unbindDeferredDispatchContext,
|
|
2356
|
+
discardQueuedItems: deps.discardQueuedItems,
|
|
2342
2357
|
applySessionShutdownState: stateApplier.applyShutdownState,
|
|
2343
2358
|
clearPendingMediaGroups: deps.clearPendingMediaGroups,
|
|
2344
2359
|
clearModelMenuState: deps.clearModelMenuState,
|
|
@@ -2387,6 +2402,10 @@ export function createTelegramSessionLifecycleHooks<
|
|
|
2387
2402
|
isSessionActive: () =>
|
|
2388
2403
|
ctx === undefined ? true : (deps.isSessionActive?.(ctx) ?? true),
|
|
2389
2404
|
unbindDeferredDispatchContext: deps.unbindDeferredDispatchContext,
|
|
2405
|
+
discardQueuedItems:
|
|
2406
|
+
ctx === undefined || !deps.discardQueuedItems
|
|
2407
|
+
? undefined
|
|
2408
|
+
: () => deps.discardQueuedItems!(ctx),
|
|
2390
2409
|
applyState: deps.applySessionShutdownState,
|
|
2391
2410
|
clearPendingMediaGroups: deps.clearPendingMediaGroups,
|
|
2392
2411
|
clearModelMenuState: deps.clearModelMenuState,
|
|
@@ -2476,14 +2495,8 @@ export function clearTelegramQueueItemsRuntime<TContext>(
|
|
|
2476
2495
|
const removedItems = deps.getQueuedItems();
|
|
2477
2496
|
const removedCount = removedItems.length;
|
|
2478
2497
|
if (removedCount === 0) return 0;
|
|
2498
|
+
deps.onItemsDiscarded?.(removedItems, deps.ctx);
|
|
2479
2499
|
deps.setQueuedItems([]);
|
|
2480
|
-
try {
|
|
2481
|
-
deps.onItemsDiscarded?.(removedItems, deps.ctx);
|
|
2482
|
-
} catch (error) {
|
|
2483
|
-
deps.recordRuntimeEvent?.("queue", error, {
|
|
2484
|
-
phase: "discard-receipt-settlement",
|
|
2485
|
-
});
|
|
2486
|
-
}
|
|
2487
2500
|
updateTelegramQueueStatusRuntime(deps);
|
|
2488
2501
|
return removedCount;
|
|
2489
2502
|
}
|
|
@@ -2877,6 +2890,7 @@ export interface TelegramQueueDispatchControllerDeps<
|
|
|
2877
2890
|
item: PendingTelegramControlItem<TContext>,
|
|
2878
2891
|
ctx: TContext,
|
|
2879
2892
|
) => void;
|
|
2893
|
+
onPromptSkipped?: (item: PendingTelegramTurn, ctx: TContext) => boolean;
|
|
2880
2894
|
}
|
|
2881
2895
|
|
|
2882
2896
|
export interface TelegramQueueDispatchController<TContext = unknown> {
|
|
@@ -2940,6 +2954,7 @@ export function createTelegramQueueDispatchRuntime<TContext = unknown>(
|
|
|
2940
2954
|
deps.hasPendingInboundQueueMutationForItem,
|
|
2941
2955
|
isQueueItemAdmissionReady: deps.isQueueItemAdmissionReady,
|
|
2942
2956
|
onControlSettled: deps.onControlSettled,
|
|
2957
|
+
onPromptSkipped: deps.onPromptSkipped,
|
|
2943
2958
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
2944
2959
|
});
|
|
2945
2960
|
}
|
|
@@ -3007,7 +3022,29 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
|
|
|
3007
3022
|
deps.updateStatus(ctx);
|
|
3008
3023
|
return;
|
|
3009
3024
|
}
|
|
3025
|
+
try {
|
|
3026
|
+
if (deps.onPromptSkipped && !deps.onPromptSkipped(candidate, ctx)) {
|
|
3027
|
+
deps.updateStatus(
|
|
3028
|
+
ctx,
|
|
3029
|
+
"Telegram skipped prompt could not be settled durably.",
|
|
3030
|
+
);
|
|
3031
|
+
return;
|
|
3032
|
+
}
|
|
3033
|
+
} catch (error) {
|
|
3034
|
+
deps.recordRuntimeEvent?.("dispatch", error, {
|
|
3035
|
+
phase: "skip-receipt-settlement",
|
|
3036
|
+
});
|
|
3037
|
+
deps.updateStatus(
|
|
3038
|
+
ctx,
|
|
3039
|
+
"Telegram skipped prompt could not be settled durably.",
|
|
3040
|
+
);
|
|
3041
|
+
return;
|
|
3042
|
+
}
|
|
3010
3043
|
nextActiveIndex += 1;
|
|
3044
|
+
deps.setQueuedItems([
|
|
3045
|
+
...activeItems.slice(nextActiveIndex),
|
|
3046
|
+
...protectedInactiveItems,
|
|
3047
|
+
]);
|
|
3011
3048
|
}
|
|
3012
3049
|
}
|
|
3013
3050
|
const dispatchableItems = activeItems.slice(nextActiveIndex);
|
|
@@ -3031,7 +3068,7 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
|
|
|
3031
3068
|
dispatchableItems,
|
|
3032
3069
|
canDispatch,
|
|
3033
3070
|
);
|
|
3034
|
-
if (
|
|
3071
|
+
if (dispatchPlan.kind !== "none") {
|
|
3035
3072
|
deps.setQueuedItems([
|
|
3036
3073
|
...dispatchPlan.remainingItems,
|
|
3037
3074
|
...protectedInactiveItems,
|
package/lib/routing.ts
CHANGED
|
@@ -1677,9 +1677,12 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1677
1677
|
})
|
|
1678
1678
|
: undefined,
|
|
1679
1679
|
stopTypingLoop: deps.stopTypingLoop,
|
|
1680
|
-
sendTextReply: (text) =>
|
|
1680
|
+
sendTextReply: (text, options) =>
|
|
1681
1681
|
deps
|
|
1682
|
-
.sendTextReply(chatId, replyToMessageId, text, {
|
|
1682
|
+
.sendTextReply(chatId, replyToMessageId, text, {
|
|
1683
|
+
target,
|
|
1684
|
+
parseMode: options?.parseMode,
|
|
1685
|
+
})
|
|
1683
1686
|
.then(() => {}),
|
|
1684
1687
|
suppressStartNotice: true,
|
|
1685
1688
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
package/lib/status.ts
CHANGED
|
@@ -344,6 +344,7 @@ export interface TelegramBridgeStatusRuntimeDeps<
|
|
|
344
344
|
getActiveToolExecutions: () => number;
|
|
345
345
|
hasPendingModelSwitch: () => boolean;
|
|
346
346
|
getQueuedItems: () => TQueueItem[];
|
|
347
|
+
getQueuedItemCount?: (items: TQueueItem[]) => number;
|
|
347
348
|
formatQueuedStatus: (items: TQueueItem[]) => string;
|
|
348
349
|
getRecentRuntimeEvents: () => TelegramRuntimeEvent[];
|
|
349
350
|
getRuntimeLockState?: () => string;
|
|
@@ -658,6 +659,7 @@ export function createTelegramBridgeStatusRuntime<
|
|
|
658
659
|
getStatusBarState: (_ctx, error) => {
|
|
659
660
|
const config = deps.getConfig();
|
|
660
661
|
const queuedItems = deps.getQueuedItems();
|
|
662
|
+
const queuedItemCount = deps.getQueuedItemCount?.(queuedItems) ?? queuedItems.length;
|
|
661
663
|
const hasActiveTurn = deps.hasActiveTurn();
|
|
662
664
|
const hasPendingDispatch = deps.hasDispatchPending();
|
|
663
665
|
const hasPendingModelSwitch = deps.hasPendingModelSwitch();
|
|
@@ -677,13 +679,13 @@ export function createTelegramBridgeStatusRuntime<
|
|
|
677
679
|
hasPendingDispatch ||
|
|
678
680
|
hasPendingModelSwitch ||
|
|
679
681
|
activeToolExecutions > 0 ||
|
|
680
|
-
|
|
682
|
+
queuedItemCount > 0,
|
|
681
683
|
processingStatus: getTelegramStatusBarProcessingStatus({
|
|
682
684
|
hasActiveTurn,
|
|
683
685
|
hasPendingDispatch,
|
|
684
686
|
hasPendingModelSwitch,
|
|
685
687
|
activeToolExecutions,
|
|
686
|
-
queuedItems:
|
|
688
|
+
queuedItems: queuedItemCount,
|
|
687
689
|
}),
|
|
688
690
|
queuedStatus: deps.formatQueuedStatus(queuedItems),
|
|
689
691
|
error,
|
package/lib/updates.ts
CHANGED
|
@@ -4583,11 +4583,11 @@ export function createTelegramUpdateAdmissionLifecycleRuntime<TContext>(
|
|
|
4583
4583
|
try {
|
|
4584
4584
|
deps.recordRuntimeEvent?.(
|
|
4585
4585
|
"inbound-worker",
|
|
4586
|
-
"
|
|
4586
|
+
"Discarded session-owned queue authority from a confirmed-dead process.",
|
|
4587
4587
|
{
|
|
4588
|
-
phase: "dead-queue-owner-
|
|
4588
|
+
phase: "dead-queue-owner-cleanup",
|
|
4589
4589
|
receiptId,
|
|
4590
|
-
|
|
4590
|
+
removedUpdateCount: result.recoveredUpdateIds.length,
|
|
4591
4591
|
},
|
|
4592
4592
|
);
|
|
4593
4593
|
} catch {
|