@llblab/pi-kit 0.11.2 → 0.11.4

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 CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  All notable changes to `@llblab/pi-kit` are documented here.
4
4
 
5
+ ## 0.11.4 - 2026-09-14
6
+
7
+ - `State Flow Inspection Hierarchy`: Advances the exact State Flow pin to `0.11.1`, placing Show state below Start/Stop, presenting Back and the four scope choices as one vertical composition axis, and rendering a fixed scope heading above four top-level disclosures.
8
+ - `Companion Ownership`: Advances the exact Telegram pin to `0.45.11`, retaining generic Rich Message headings while removing State Flow-specific scope semantics from the transport-owned UI registry. The package set, resource inventory, and explicit load order remain unchanged.
9
+
10
+ ## 0.11.3 - 2026-09-14
11
+
12
+ - `Telegram Rich Sections And Queue Continuation`: Advances the exact Telegram pin to `0.45.9`, adding callback-targeted Native Rich Messages for companion sections and binding `/next` notices to the exact dispatchable queue item across concurrent readiness, admission, mutation, reordering, clearing, and transport changes.
13
+ - `State Flow Inspection`: Advances the exact State Flow pin to `0.11.0`, adding read-only Global, CWD, Session, and Effective state inspection through the Telegram submenu with bounded collapsible Native Rich Message rendering. The package set, resource inventory, and explicit load order remain unchanged.
14
+
5
15
  ## 0.11.2 - 2026-09-13
6
16
 
7
17
  - `State Flow Compaction And Resilience`: Advances the exact State Flow pin to `0.10.2`, replacing byte-based early-compaction readiness with public context-token usage, retaining the complete latest accepted iteration after shortening, accepting unambiguous non-terminal `final:false`, and strengthening bounded memory stewardship. The package set, resource inventory, and explicit load order remain unchanged.
package/README.md CHANGED
@@ -14,8 +14,8 @@ Package links lead to the owning repositories for usage, documentation, issues,
14
14
  | [`@llblab/pi-clean-room`](https://github.com/llblab/pi-clean-room) | `0.1.1` | Isolated nested Pi TUI with explicitly selected extensions |
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.8.1` | Visible continuation scheduling and bounded worker Skills |
17
- | [`@llblab/pi-state-flow`](https://github.com/llblab/pi-state-flow) | `0.10.2` | Durable scoped state with native session compaction and exact publication |
18
- | [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.45.8` | Telegram companion, queues, files, voice, controls, and Generative Apps guidance |
17
+ | [`@llblab/pi-state-flow`](https://github.com/llblab/pi-state-flow) | `0.11.1` | Durable scoped state with native session compaction, exact publication, and read-only Telegram inspection |
18
+ | [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.45.11` | Telegram companion with exact queues, generic Rich section messages, files, voice, controls, and Generative Apps guidance |
19
19
  | [`@llblab/skills`](https://github.com/llblab/skills) | `1.15.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,49 +2,6 @@
2
2
 
3
3
  Completed release work belongs in [CHANGELOG.md](CHANGELOG.md).
4
4
 
5
- ## 0.10.2 hotfix: Native boundaries and patch robustness
6
-
7
- - **Outcome:** Correct runtime friction without weakening State Flow's semantic or durability guarantees: early post-acceptance compaction uses public context-token readiness and retains one complete accepted iteration, while non-terminal `patch_state.final` input degrades to an ordinary scoped update instead of failing. Model-facing guidance remains canonical and teaches only omission or `final:true`; tolerant Boolean normalization is a runtime resilience layer, not another advertised protocol form.
8
- - **Scope gate:** The hotfix stays extension-owned: retain State Flow-initiated early compaction through public Pi APIs, replace byte-based readiness with public context usage, and keep one complete latest iteration after successful shortening. Do not require or modify Pi itself.
9
- - **Compaction failure class:** State Flow previously admitted compaction from `JSON.stringify(activeEntries) >= 80_000`, although serialized bytes do not prove that Pi's configured recent-token suffix leaves a removable prefix. It also retained only the latest accepted assistant response. The hotfix uses public context-token usage with a 24,000-token floor and retains the complete latest accepted user iteration; custom Pi retention settings may still decline safely and permit a later retry.
10
- - **Patch failure class:** The public `patch_state` schema declares `final` as an optional Boolean, but execution rejects every supplied value except literal `true`. A model therefore produced a schema-valid `{ cwd: {...}, final: false }` non-terminal patch during a DEOS Grow Loop run and received `patch_state final must be exactly true when supplied`. The semantic patch was unambiguous and otherwise valid; rejecting it spent another tool turn and diagnostic record solely to enforce omission syntax.
11
- - **Ownership contract:** Pi owns native preparation, configured compaction settings, one prepared branch snapshot, cut-point validity, persistence, lifecycle events, and transcript projection. State Flow owns post-acceptance compaction admission, durable revision/step details, foreign-context protection, generation/leaf fencing, and `patch_state` normalization. The advertised TypeBox schema and model context remain the smallest canonical language; runtime may accept a bounded unambiguous compatibility superset without teaching it to the model.
12
-
13
- ### Checkpoint A: Token-guided early compaction
14
-
15
- - **Acceptance checkpoint:** After an eligible accepted run settles, State Flow initiates its own early native compaction only when public `getContextUsage()` reports enough context. A successful request retains the complete latest accepted user iteration and durable handoff; unknown or short usage remains untouched without invoking Pi.
16
- - [x] **Replace byte readiness:** Removed `STATE_FLOW_COMPACTION_MIN_ACTIVE_BYTES` and the serialized-entry byte gate. The extension now requires at least 24,000 estimated context tokens—a modest margin above Pi's default 20,000-token retained suffix—while preserving enabled, accepted, non-bootstrap, idle, empty-queue, durable-base, generation, and resolution guards.
17
- - [x] **Retain one complete iteration:** The custom compaction boundary now starts at the latest accepted user request, preserving its assistant/tool trajectory and final response instead of retaining only the answer.
18
- - [x] **Keep foreign context safe:** Foreign context-bearing custom entries block shortening only when they precede the retained iteration; entries within the retained iteration remain visible.
19
- - [x] **Keep benign refusal recoverable:** Unknown or sub-threshold usage skips compaction. A native refusal under custom Pi retention settings releases the attempt so later accepted work can retry without affecting state.
20
- - [x] **Documentation and protocol:** Project protocol, architecture, usage guidance, changelog, and tests now describe token readiness and complete-iteration retention. User manual and native threshold/overflow compaction remain Pi-owned.
21
-
22
- ### Checkpoint B: Quiet non-terminal patch degradation
23
-
24
- - **Acceptance checkpoint:** Model-facing contracts expose one canonical rule: omit `final` during ongoing work and set `final:true` only when the iteration may finish. Runtime treats explicit false as benign non-terminal intent and may normalize only unambiguous Boolean-like primitives before schema validation. No non-terminal value suppresses or reverses an existing eligibility latch, creates semantic work, changes transition identity or persistence behavior, or produces an invalid-patch diagnostic merely because no scope accompanied it.
25
- - [x] **Normalize explicit false:** `final:false` now accompanies useful global/CWD/session patches without latching or clearing terminal eligibility; pre-latch and post-latch tests preserve atomic state semantics.
26
- - [x] **Bound tolerant coercion:** `prepareArguments` normalizes only `true`, `false`, `1`, `0`, and case-insensitive trimmed `"true"`/`"false"`. Arrays, objects, null, unknown strings, empty strings, and other numbers remain invalid; raw JavaScript truthiness is not used.
27
- - [x] **Keep false-only inert:** `{final:false}` and normalized false-like-only calls return an acknowledged non-terminal no-op without semantic transition, eligibility, response, publication, or diagnostic. Bare `{}`, empty supplied scopes, unknown fields, null semantic values, and material scope no-ops remain invalid.
28
- - [x] **Keep model guidance canonical:** Tool descriptions, prompt snippets/guidelines, injected context, fallback prompts, `AGENTS.md`, the bundled memory Skill, and user-facing documentation teach only omission during ongoing work and `final:true` for terminal eligibility. Compatibility aliases remain implementation/test knowledge.
29
- - [x] **Reconcile diagnostics:** Invalid-input tests now use genuinely malformed values. Unit and real-Pi regressions prove scoped false/false-like updates are accepted without an invalid-patch record and remain terminal-ineligible until a later `final:true`.
30
-
31
- ### Checkpoint C: Continuous state stewardship
32
-
33
- - **Acceptance checkpoint:** State Flow makes memory care part of ordinary model responsibility without creating an automatic background agent, arbitrary maintenance counter, full-state rewrite ritual, or second mutation path. Every handoff curates newly affected state, while explicit project/feature/release phase changes trigger one bounded ownership and obsolescence pass before terminal completion.
34
- - [x] **Strengthen the baseline contract:** The compact runtime protocol, `patch_state` prompt metadata, `AGENTS.md`, and bundled memory Skill now make narrowest-scope placement, touched-branch reconciliation, supersession, and obsolete-progress deletion ordinary responsibilities while keeping detailed migration procedure in the Skill.
35
- - [x] **Define phase-boundary care:** Feature/release/campaign completion, project switches, and active-version changes now require one bounded reconciliation that removes transient prior-work state while retaining still-operative consequences.
36
- - [x] **Make scope movement ordinary:** The runtime contract and Skill define global/CWD/session applicability, targeted scoped reads when effective ownership is unclear, and destination-write/readback/source-delete/readback migration ordering.
37
- - [x] **Bound routine cost:** Ordinary turns curate only touched and obviously stale or mis-scoped visible branches. Full reconciliation is event-driven, with no background loop, arbitrary counter, size trigger, maintenance ledger, timestamp, or score.
38
- - [x] **Align Skill activation:** `state-flow-memory` now covers explicit requests and runtime-required phase boundaries while remaining one bounded, materialized-first, evidence-aware cohort that cannot launch itself or authorize external promotion.
39
- - [x] **Live behavior witness:** A clean temporary Pi 0.85.1 session with the release extension created scoped fixture memory, then a real model moved project-specific campaign state from global to CWD through destination-write/readback/source-delete/readback, removed completed session progress, retained the cross-project rule plus active commitment/uncertainty, and left unrelated state untouched. Durable scope tails verify the claimed movement and deletion.
40
-
41
- ### Checkpoint D: Integrated hotfix candidate
42
-
43
- - **Acceptance checkpoint:** Token-guided early compaction, quiet non-terminal patch degradation, and continuous state stewardship form one coherent release candidate, and validation proves all three without reopening completed 0.10.1 work.
44
- - [x] **Compatibility and versions:** The release preserves the existing public Pi API floor; `getContextUsage()` is present in the 0.84.4 public extension types, the complete suite passes on 0.85.1, and package/lockfile metadata agree on 0.10.2 without a new host capability dependency.
45
- - [x] **Regression validation:** Focused compaction and real-Pi witnesses pass; `npm run validate` passes 446/446 on Pi 0.85.1, context validation reports zero errors, and the 46-file dry-run package includes the renamed protocol and compaction domains. Real-Pi lifecycle coverage proves retained complete-iteration history and skipped short usage; patch/logging tests prove useful `final:false`; the clean real-model witness proves bounded phase-boundary cleanup.
46
- - [ ] **Release follow-through:** After the combined candidate is validated, release the State Flow hotfix through its guarded direct-main/tag automation and update the exact Pi Kit pin and synchronized lock/inventory surfaces.
47
-
48
5
  ## Candidate evolution
49
6
 
50
7
  - [ ] **Read-only global bootstrap layer:** Project the existing global materialization into enabled and non-enabled sessions by default, so even one-shot work starts carrying established cross-project facts, preferences, routing, and conventions. The global scope is the highest-value, lowest-cost half of the memory, while writing carries the protocol and curation tax; making the read side unconditional gives continuity without enabling mutation.
@@ -4,6 +4,14 @@
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.11.1: Telegram inspection hierarchy
8
+
9
+ - `Telegram inspection`: `👁 Show state` now follows Start/Stop, and its Back, 🌐 Global, 📂 CWD, 💬 Session, and 🧬 Effective controls form one vertical composition axis. A selected scope opens with a fixed emoji-bearing heading followed directly by the four top-level collapsible semantic fields, removing the redundant outer disclosure.
10
+
11
+ ## 0.11.0: Telegram state inspection
12
+
13
+ - `Telegram inspection`: The State Flow submenu now offers `👁 Show state` with Global, CWD, Session, and Effective choices. Selecting one scope sends exactly that projected materialized slice as a standalone Telegram Native Rich Message whose semantic fields use collapsible `details` and JSON `pre` blocks; inspection is read-only and the optional adapter remains fail-open without a compatible pi-telegram membrane.
14
+
7
15
  ## 0.10.2: Token-guided compaction and patch resilience
8
16
 
9
17
  - `Compaction readiness`: Early State Flow compaction now uses Pi's public context-token estimate with a 24,000-token floor instead of serialized-entry bytes, skipping unknown or short contexts without invoking native compaction. Successful shortening retains the complete latest accepted user iteration rather than only its final answer, while durable state and append-only session history remain intact.
@@ -53,7 +53,7 @@ Continue working normally. The agent receives the state protocol and uses `patch
53
53
 
54
54
  State Flow is **opt-in**. Starting in an existing conversation keeps its active context for one migration run. To enable genuinely new sessions automatically, set `"autoStart": true` in the optional [configuration](docs/usage.md#configuration).
55
55
 
56
- With `pi-telegram` installed, its main menu also exposes the same Start/Stop controls.
56
+ With a compatible `pi-telegram` installed, its main menu exposes the same Start/Stop controls followed by `👁 Show state`. The chooser presents Back, 🌐 Global, 📂 CWD, 💬 Session, and 🧬 Effective as one vertical composition axis. Selecting a scope sends exactly that materialized slice as a standalone native Rich Message with a fixed scope heading and four top-level collapsible semantic fields; inspection is read-only and does not create a State Flow transition.
57
57
 
58
58
  ## What carries forward
59
59
 
@@ -1001,6 +1001,11 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1001
1001
  bootstrap: snapshot.meta.bootstrap === true,
1002
1002
  startPending: telegramStartPending,
1003
1003
  }),
1004
+ state: (scope) => projectStateForModel(
1005
+ scope === "effective"
1006
+ ? overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session)
1007
+ : scopeStates[scope],
1008
+ ),
1004
1009
  canStartNow: () => activeContext === undefined || activeContext.isIdle(),
1005
1010
  start: () => {
1006
1011
  if (!activeContext) throw new Error("State Flow is not attached to an active session yet");
@@ -16,6 +16,25 @@ export interface StateFlowTelegramSnapshot {
16
16
  startPending: boolean;
17
17
  }
18
18
 
19
+ export type StateFlowTelegramScope = "global" | "cwd" | "session" | "effective";
20
+
21
+ export interface StateFlowTelegramState {
22
+ artifacts: Record<string, unknown>;
23
+ contract: Record<string, unknown>;
24
+ working: Record<string, unknown>;
25
+ response: string;
26
+ }
27
+
28
+ export type StateFlowTelegramRichBlock =
29
+ | { type: "heading"; text: string; size: 2 }
30
+ | { type: "pre"; text: string; language?: string }
31
+ | { type: "details"; summary: string | { type: "bold" | "code"; text: string }; blocks: StateFlowTelegramRichBlock[]; is_open?: true };
32
+
33
+ export interface StateFlowTelegramRichMessage {
34
+ blocks: StateFlowTelegramRichBlock[];
35
+ skip_entity_detection?: boolean;
36
+ }
37
+
19
38
  export interface StateFlowTelegramButton {
20
39
  text: string;
21
40
  callback_data: string;
@@ -30,6 +49,7 @@ export interface StateFlowTelegramView {
30
49
  export interface StateFlowTelegramSectionContext {
31
50
  callbackData(action: string, payload?: string): string;
32
51
  edit(view: StateFlowTelegramView): Promise<void>;
52
+ openRich(message: StateFlowTelegramRichMessage): Promise<void>;
33
53
  answerCallback(text?: string): Promise<void>;
34
54
  }
35
55
 
@@ -61,6 +81,7 @@ export interface StateFlowTelegramControlResult {
61
81
 
62
82
  export interface StateFlowTelegramPort {
63
83
  snapshot(): StateFlowTelegramSnapshot;
84
+ state(scope: StateFlowTelegramScope): StateFlowTelegramState;
64
85
  canStartNow(): boolean;
65
86
  start(): StateFlowTelegramControlResult;
66
87
  stop(): StateFlowTelegramControlResult;
@@ -103,10 +124,79 @@ export function buildStateFlowSectionView(
103
124
  return {
104
125
  text: [formatStateFlowSectionHeader(snapshot), "", STATE_FLOW_SECTION_HELP].join("\n"),
105
126
  parseMode: "html",
106
- replyMarkup: { inline_keyboard: [[action]] },
127
+ replyMarkup: { inline_keyboard: [
128
+ [action],
129
+ [{ text: "👁 Show state", callback_data: callbackData("show-state") }],
130
+ ] },
107
131
  };
108
132
  }
109
133
 
134
+ const STATE_FLOW_SCOPE_LABELS: Record<StateFlowTelegramScope, string> = {
135
+ global: "🌐 Global",
136
+ cwd: "📂 CWD",
137
+ session: "💬 Session",
138
+ effective: "🧬 Effective",
139
+ };
140
+
141
+ export function buildStateFlowScopeChooser(callbackData: (action: string, payload?: string) => string): StateFlowTelegramView {
142
+ return {
143
+ text: "<b>👁 Show state:</b>",
144
+ parseMode: "html",
145
+ replyMarkup: { inline_keyboard: [
146
+ [{ text: "⬅️ Back", callback_data: callbackData("back") }],
147
+ ...(["global", "cwd", "session", "effective"] as const).map((scope) => [
148
+ { text: STATE_FLOW_SCOPE_LABELS[scope], callback_data: callbackData("inspect", scope) },
149
+ ]),
150
+ ] },
151
+ };
152
+ }
153
+
154
+ // The complete message serializes each preformatted field one additional time;
155
+ // 3,000 leaves safe headroom for worst-case JSON escaping across all four fields.
156
+ const STATE_FLOW_TELEGRAM_FIELD_MAX_CHARS = 3_000;
157
+
158
+ function renderStateFlowTelegramField(value: unknown): string {
159
+ const json = JSON.stringify(value, null, 2);
160
+ if (json.length <= STATE_FLOW_TELEGRAM_FIELD_MAX_CHARS) return json;
161
+ let low = 0;
162
+ let high = json.length;
163
+ let rendered = "";
164
+ while (low <= high) {
165
+ const length = Math.floor((low + high) / 2);
166
+ const candidate = JSON.stringify({
167
+ truncated: true,
168
+ preview: json.slice(0, length),
169
+ omittedChars: json.length - length,
170
+ }, null, 2);
171
+ if (candidate.length <= STATE_FLOW_TELEGRAM_FIELD_MAX_CHARS) {
172
+ rendered = candidate;
173
+ low = length + 1;
174
+ } else {
175
+ high = length - 1;
176
+ }
177
+ }
178
+ return rendered;
179
+ }
180
+
181
+ export function renderStateFlowRichState(scope: StateFlowTelegramScope, state: StateFlowTelegramState): StateFlowTelegramRichMessage {
182
+ const fields = ["artifacts", "contract", "working", "response"] as const;
183
+ return {
184
+ blocks: [
185
+ { type: "heading", text: STATE_FLOW_SCOPE_LABELS[scope], size: 2 },
186
+ ...fields.map((field) => ({
187
+ type: "details" as const,
188
+ summary: { type: "code" as const, text: field },
189
+ blocks: [{ type: "pre" as const, language: "json", text: renderStateFlowTelegramField(state[field]) }],
190
+ })),
191
+ ],
192
+ skip_entity_detection: true,
193
+ };
194
+ }
195
+
196
+ function isStateFlowTelegramScope(value: string): value is StateFlowTelegramScope {
197
+ return value === "global" || value === "cwd" || value === "session" || value === "effective";
198
+ }
199
+
110
200
  function buildStateFlowTelegramSection(port: StateFlowTelegramPort) {
111
201
  return {
112
202
  id: STATE_FLOW_TELEGRAM_ID,
@@ -115,10 +205,21 @@ function buildStateFlowTelegramSection(port: StateFlowTelegramPort) {
115
205
  render: (ctx: StateFlowTelegramSectionContext) =>
116
206
  buildStateFlowSectionView(port.snapshot(), (action) => ctx.callbackData(action)),
117
207
  handleCallback: async (ctx: StateFlowTelegramCallbackContext) => {
118
- // cancel/refresh remain routable for keyboards sent by earlier versions; 0.9.4 presents only the state action.
119
- if (ctx.action !== "start" && ctx.action !== "stop" && ctx.action !== "cancel" && ctx.action !== "refresh") return "pass" as const;
208
+ // cancel/refresh remain routable for keyboards sent by earlier versions.
209
+ if (ctx.action !== "start" && ctx.action !== "stop" && ctx.action !== "cancel" && ctx.action !== "refresh" && ctx.action !== "show-state" && ctx.action !== "inspect" && ctx.action !== "back") return "pass" as const;
120
210
  let notice: string | undefined;
121
211
  try {
212
+ if (ctx.action === "show-state") {
213
+ await ctx.answerCallback();
214
+ await ctx.edit(buildStateFlowScopeChooser((action, payload) => ctx.callbackData(action, payload)));
215
+ return "handled" as const;
216
+ }
217
+ if (ctx.action === "inspect") {
218
+ if (!isStateFlowTelegramScope(ctx.payload)) throw new Error("Unknown State Flow scope");
219
+ await ctx.openRich(renderStateFlowRichState(ctx.payload, port.state(ctx.payload)));
220
+ await ctx.answerCallback();
221
+ return "handled" as const;
222
+ }
122
223
  if (ctx.action === "start") {
123
224
  if (port.canStartNow()) notice = port.start().message;
124
225
  else {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-state-flow",
3
- "version": "0.10.2",
3
+ "version": "0.11.1",
4
4
  "private": false,
5
5
  "description": "Incremental scoped state/context compiler for Pi, inspired by SKILL.state",
6
6
  "keywords": [
@@ -4,6 +4,20 @@
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.45.11: Consumer-owned companion semantics
8
+
9
+ - `Extension boundaries`: Removes consumer-specific state-scope emoji from the Telegram UI registry. The bridge continues to own generic Rich Message headings and inspection semantics, while companion extensions independently own their domain labels, composition rules, and presentation through the public section API.
10
+
11
+ ## 0.45.10: Rich headings and state-scope markers
12
+
13
+ - `Rich section headings`: Public Rich Message block types now include native headings, allowing companion sections to place a fixed title above sibling disclosures instead of wrapping the entire view in another collapsible block.
14
+ - `State-scope markers`: Registers 🌐 Global, 📂 CWD, 💬 Session, and 🧬 Effective as canonical state-scope semantics for chooser buttons and selected-scope headings, with composition flowing Global → CWD → Session → Effective.
15
+
16
+ ## 0.45.9: Rich sections and exact queue continuation
17
+
18
+ - `Extension Sections`: Public section contexts now expose `openRich(message)` for one standalone Telegram Native Rich Message routed to the exact callback chat/thread with normal message-ownership recording and scoped callback diagnostics. Companion extensions can reuse native `details`/`pre` disclosure without raw bot access; `👁` is the canonical read-only inspection marker.
19
+ - `/next` ordering: Active interruption settles before continuation is announced. The notice is bound to the exact selected queued item and revalidates queue identity, readiness, admission, pending mutations, and transport authority before dispatch; removal, clear, reordering, concurrent append, or transport loss cannot transfer stale intent to unrelated work.
20
+
7
21
  ## 0.45.8: Follower Forwarding Hotfix
8
22
 
9
23
  - `Follower Forwarding`: Message-ownership lookups now project the matching live registration's protocol identity before forwarding. Voice/message and edit retries, message-only callbacks, and reactions no longer stall on an incomplete cached owner after a rejection or re-registration. Exact generation, binding, protocol, and durable-receipt checks remain enforced.
@@ -4,6 +4,8 @@
4
4
  * Exposes the stable managed Telegram menu-section surface while keeping registry internals package-private
5
5
  */
6
6
 
7
+ export type { TelegramInputRichBlock, TelegramInputRichMessage, TelegramRichText } from "../lib/telegram-api.ts";
8
+
7
9
  export {
8
10
  getTelegramSectionDiagnostics,
9
11
  registerTelegramSection,
@@ -507,7 +507,7 @@ Immediate controls:
507
507
  - `/start` opens the main inline application menu.
508
508
  - `/model`, `/thinking`, `/queue`, and `/settings` are hidden shortcuts to menu sections.
509
509
  - `/compact` opens an inline confirmation dialog and then runs compaction when the bridge is idle.
510
- - `/next` dispatches the next queued turn, aborting Pi first when needed. When an active Telegram turn is aborted, its single Pi-aligned informational notice replies to a pre-abort snapshot of that turn; otherwise the command message is the fallback target. Aborted pending assistant text is not projected as a second reply, while already completed intermediate output remains visible.
510
+ - `/next` dispatches the next queued turn, aborting Pi first when needed. Active-turn settlement delivers its abort notice to the interrupted prompt before queue dispatch continues. The queue owner then emits `Dispatching next queued turn.` against the exact selected prompt's chat/thread/reply id before its one model dispatch; skipped, inactive, pending-mutation, and admission-blocked candidates never receive it. The `/next` command itself is never a lifecycle-notice reply target. Aborted pending assistant text is not projected as a second reply, while already completed intermediate output remains visible.
511
511
  - `/abort` aborts active work while preserving queued items. Abort-history preservation is enabled only for Telegram-owned active turns; later local/non-Telegram agent starts clear stale abort-history mode so the next Telegram prompt appends instead of absorbing old queued turns as history.
512
512
  - `/stop` aborts and clears waiting Telegram queue items.
513
513
 
@@ -52,7 +52,7 @@ Stable commands inside the paired Telegram DM:
52
52
 
53
53
  - `/start` — pair when needed and open the main application menu.
54
54
  - `/compact` — open confirmation and compact when idle.
55
- - `/next` — dispatch the next queued turn, aborting active work first when needed; one Pi-aligned informational reply anchors to a pre-abort snapshot of the Telegram turn or falls back to the command, and aborted pending assistant text is suppressed.
55
+ - `/next` — abort active work first when needed, let the interrupted prompt receive its abort notice, then reply `Dispatching next queued turn.` to the exact queued prompt selected for the next model turn. The command itself is never the lifecycle-notice reply target, and aborted pending assistant text is suppressed.
56
56
  - `/continue` — enqueue a priority `continue` prompt.
57
57
  - `/abort` — abort active work and keep the queue; abort-history is scoped to Telegram-owned active turns.
58
58
  - `/stop` — abort active Telegram-owned work and clear waiting Telegram queue items.
@@ -262,6 +262,8 @@ interface TelegramSectionContext {
262
262
  edit(view: TelegramSectionView): Promise<void>;
263
263
  /** Send a standalone chat message without auto-navigation */
264
264
  open(view: TelegramSectionView): Promise<void>;
265
+ /** Send one standalone Telegram Native Rich Message without exposing transport */
266
+ openRich(message: TelegramInputRichMessage): Promise<void>;
265
267
  /** Enqueue a plain-text prompt turn */
266
268
  enqueuePrompt(prompt: string): Promise<void>;
267
269
  /** Build a section-namespaced callback_data string */
@@ -285,6 +287,7 @@ interface TelegramSectionCallbackContext {
285
287
  answerCallback(text?: string): Promise<void>;
286
288
  edit(view: TelegramSectionView): Promise<void>;
287
289
  open(view: TelegramSectionView): Promise<void>;
290
+ openRich(message: TelegramInputRichMessage): Promise<void>;
288
291
  enqueuePrompt(prompt: string): Promise<void>;
289
292
  callbackData(action: string, payload?: string): string;
290
293
  /** Delete the message that triggered this callback */
@@ -292,6 +295,12 @@ interface TelegramSectionCallbackContext {
292
295
  }
293
296
  ```
294
297
 
298
+ ### `openRich` semantics
299
+
300
+ `ctx.openRich(message)` sends exactly one standalone Telegram Native Rich Message to the callback's exact chat/thread target. It accepts the public `TelegramInputRichMessage` tree (`details` and `pre` blocks) used by native tool evidence, records message ownership, and routes through the active direct/follower transport. It adds no menu navigation and exposes no raw bot client or arbitrary API method.
301
+
302
+ Use it when disclosure behavior is part of the document, such as one selected read-only state or evidence tree. Do not imitate nested disclosure with HTML or Markdown when the native tree is required.
303
+
295
304
  ### `enqueuePrompt` semantics
296
305
 
297
306
  Queues a `[telegram] <prompt>` turn in the default lane with the paired user's `chatId`. Uses `queueMutationRuntime.append()` and triggers `dispatchNextQueuedTelegramTurn()`. The prompt arrives as a normal Telegram-owned turn — the agent sees it as if the user typed it.
@@ -36,6 +36,7 @@ Use emoji as stable semantic markers, not decoration. Emoji carry transportable
36
36
  | `🔬` | Activity / technical detail | Activity settings row and detail card | Chooses quiet, thinking, tools, or verbose bridge activity; not a generic diagnostics marker. |
37
37
  | `🧠` | Model thinking controls | Thinking menus and status rows | Thinking activity quotes omit this icon and their header entirely to minimize chat height. |
38
38
  | `📎` | Attachment | Attachment summaries, queue rows for attachment-only turns | Not for thread binding. |
39
+ | `👁` | Read-only inspection | State/detail viewers and inspection entrypoints | Opens evidence without mutating the inspected object; do not use for edit or refresh actions. |
39
40
 
40
41
  ### Command And Control Actions
41
42
 
@@ -50,6 +50,7 @@ type TelegramAgentMessageRouter = NonNullable<
50
50
  export interface TelegramQueueBindingRuntime<TContext> {
51
51
  mutation: Queue.TelegramQueueMutationController<TContext>;
52
52
  dispatchNext: (ctx: TContext) => void;
53
+ requestNextDispatchAnnouncement: () => void;
53
54
  watchdog: Queue.TelegramQueueDispatchWatchdogRuntime<TContext>;
54
55
  }
55
56
 
@@ -122,7 +123,7 @@ export function createTelegramQueueBindingRuntime<TContext>(deps: {
122
123
  updateStatus: deps.updateStatus,
123
124
  recordRuntimeEvent: deps.recordRuntimeEvent,
124
125
  });
125
- const dispatchNext = Queue.createTelegramQueueDispatchRuntime({
126
+ const dispatch = Queue.createTelegramQueueDispatchRuntime({
126
127
  ...deps.store,
127
128
  isCompactionInProgress: deps.lifecycle.isCompactionInProgress,
128
129
  hasActiveTurn: deps.activeTurn.has,
@@ -162,13 +163,14 @@ export function createTelegramQueueBindingRuntime<TContext>(deps: {
162
163
  recordRuntimeEvent: deps.recordRuntimeEvent,
163
164
  ...deps.promptDispatch,
164
165
  sendUserMessage: deps.sendUserMessage,
165
- }).dispatchNext;
166
+ });
166
167
  return {
167
168
  mutation,
168
- dispatchNext,
169
+ dispatchNext: dispatch.dispatchNext,
170
+ requestNextDispatchAnnouncement: dispatch.requestNextDispatchAnnouncement,
169
171
  watchdog: Queue.createTelegramQueueDispatchWatchdogRuntime({
170
172
  hasQueuedItems: deps.store.hasQueuedItems,
171
- dispatchNextQueuedTelegramTurn: dispatchNext,
173
+ dispatchNextQueuedTelegramTurn: dispatch.dispatchNext,
172
174
  recordRuntimeEvent: deps.recordRuntimeEvent,
173
175
  }),
174
176
  };
@@ -1147,6 +1147,7 @@ export interface TelegramCommandRuntimeDeps<
1147
1147
  updateStatus: (ctx: TContext) => void;
1148
1148
  isContextActive?: (ctx: TContext) => boolean;
1149
1149
  dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
1150
+ requestNextDispatchAnnouncement?: () => void;
1150
1151
  requestDeferredDispatchNextQueuedTelegramTurn?: (
1151
1152
  dispatch: (ctx: TContext) => void,
1152
1153
  ) => void;
@@ -1416,6 +1417,7 @@ export async function handleTelegramNextCommand(deps: {
1416
1417
  clearPendingModelSwitch: () => void;
1417
1418
  abortCurrentTurn: () => void;
1418
1419
  dispatchNextQueuedTurn: () => void;
1420
+ requestNextDispatchAnnouncement?: () => void;
1419
1421
  clearFoldForDispatch: () => void;
1420
1422
  updateStatus: () => void;
1421
1423
  sendTextReply: (
@@ -1435,19 +1437,10 @@ export async function handleTelegramNextCommand(deps: {
1435
1437
  return;
1436
1438
  }
1437
1439
  if (!deps.isIdle() && deps.hasAbortHandler()) {
1438
- const activeTurnReply = deps.getActiveTurnReply?.();
1439
1440
  deps.clearFoldForDispatch();
1441
+ deps.requestNextDispatchAnnouncement?.();
1440
1442
  deps.abortCurrentTurn();
1441
1443
  deps.updateStatus();
1442
- const notice = formatTelegramInformationHeading(
1443
- "⏩",
1444
- "Dispatching next queued turn.",
1445
- );
1446
- if (activeTurnReply) {
1447
- await activeTurnReply(notice, { parseMode: "HTML" });
1448
- } else {
1449
- await deps.sendTextReply(notice, { parseMode: "HTML" });
1450
- }
1451
1444
  return;
1452
1445
  }
1453
1446
  if (!deps.isIdle()) {
@@ -1460,12 +1453,9 @@ export async function handleTelegramNextCommand(deps: {
1460
1453
  );
1461
1454
  return;
1462
1455
  }
1456
+ deps.requestNextDispatchAnnouncement?.();
1463
1457
  deps.dispatchNextQueuedTurn();
1464
1458
  deps.updateStatus();
1465
- await deps.sendTextReply(
1466
- formatTelegramInformationHeading("▶️", "Dispatching next queued turn."),
1467
- { parseMode: "HTML" },
1468
- );
1469
1459
  }
1470
1460
 
1471
1461
  export async function handleTelegramContinueCommand<TMessage, TContext>(
@@ -2029,6 +2019,7 @@ async function handleTelegramCommandRuntime<
2029
2019
  abortCurrentTurn: deps.abortCurrentTurn,
2030
2020
  dispatchNextQueuedTurn: () =>
2031
2021
  deps.dispatchNextQueuedTelegramTurn(commandCtx),
2022
+ requestNextDispatchAnnouncement: deps.requestNextDispatchAnnouncement,
2032
2023
  clearFoldForDispatch: () =>
2033
2024
  deps.setFoldQueuedPromptsIntoHistory(false),
2034
2025
  updateStatus: updateStatusFor(commandCtx),
@@ -629,8 +629,12 @@ export default function (pi: Pi.ExtensionAPI) {
629
629
  getAssistantRenderingMode: configControls.getAssistantRenderingMode,
630
630
  editMessage: editTelegramMessageText,
631
631
  });
632
- const { replyTransport, editInteractiveMessage, sendInteractiveMessage } =
633
- replyRuntime;
632
+ const {
633
+ replyTransport,
634
+ editInteractiveMessage,
635
+ sendInteractiveMessage,
636
+ sendSectionRichMessage,
637
+ } = replyRuntime;
634
638
  const deliveryTargetPolicyRuntime =
635
639
  Delivery.createTelegramDeliveryTargetPolicyRuntime({
636
640
  ownsDirect: lockRuntime.owns,
@@ -744,6 +748,7 @@ export default function (pi: Pi.ExtensionAPI) {
744
748
  const {
745
749
  mutation: queueMutationRuntime,
746
750
  dispatchNext: dispatchNextQueuedTelegramTurn,
751
+ requestNextDispatchAnnouncement,
747
752
  watchdog: queueDispatchWatchdogRuntime,
748
753
  } = Bindings.createTelegramQueueBindingRuntime({
749
754
  store: telegramQueueStore,
@@ -964,6 +969,7 @@ export default function (pi: Pi.ExtensionAPI) {
964
969
  openSettingsMenu: settingsMenuRuntime.openSettingsMenu,
965
970
  settingsMenuCallbackHandler: settingsMenuRuntime.handleCallbackQuery,
966
971
  sectionRegistry,
972
+ sendSectionRichMessage,
967
973
  buttonActionStore,
968
974
  invokeBoundButtonAction: invokeGenerativeAppBoundButtonAction,
969
975
  inboundHandlerRuntime,
@@ -972,6 +978,7 @@ export default function (pi: Pi.ExtensionAPI) {
972
978
  updateStatus,
973
979
  isContextActive: telegramSessionContextStore.isCurrent,
974
980
  dispatchNextQueuedTelegramTurn,
981
+ requestNextDispatchAnnouncement,
975
982
  requestDeferredDispatchNextQueuedTelegramTurn:
976
983
  deferredQueueDispatchRuntime.request,
977
984
  hasDeferredDispatchContext: deferredQueueDispatchRuntime.isBound,
@@ -32,6 +32,7 @@ import {
32
32
  type ScopedTelegramModel,
33
33
  type ThinkingLevel,
34
34
  } from "./model.ts";
35
+ import type { TelegramInputRichMessage } from "./telegram-api.ts";
35
36
  import {
36
37
  handleTelegramSectionCallback,
37
38
  handleTelegramSectionOpen,
@@ -225,6 +226,11 @@ export interface TelegramMenuCallbackRuntimeDeps<
225
226
  replyMarkup: TelegramReplyMarkup,
226
227
  options?: { target?: { chatId: number; threadId?: number } },
227
228
  ) => Promise<number | undefined>;
229
+ sendSectionRichMessage?: (
230
+ chatId: number,
231
+ message: TelegramInputRichMessage,
232
+ options?: { target?: { chatId: number; threadId?: number } },
233
+ ) => Promise<number | undefined>;
228
234
  enqueueSectionPrompt?: (
229
235
  prompt: string,
230
236
  ctx: TContext,
@@ -478,6 +484,11 @@ export interface TelegramMenuCallbackRuntimeAdapterDeps<
478
484
  replyMarkup: TelegramReplyMarkup,
479
485
  options?: { target?: { chatId: number; threadId?: number } },
480
486
  ) => Promise<number | undefined>;
487
+ sendSectionRichMessage?: (
488
+ chatId: number,
489
+ message: TelegramInputRichMessage,
490
+ options?: { target?: { chatId: number; threadId?: number } },
491
+ ) => Promise<number | undefined>;
481
492
  enqueueSectionPrompt?: (
482
493
  prompt: string,
483
494
  ctx: TContext,
@@ -527,6 +538,7 @@ export function createTelegramMenuCallbackHandlerForContext<
527
538
  sectionRegistry: deps.sectionRegistry,
528
539
  editInteractiveMessage: deps.editInteractiveMessage,
529
540
  sendInteractiveMessage: deps.sendInteractiveMessage,
541
+ sendSectionRichMessage: deps.sendSectionRichMessage,
530
542
  enqueueSectionPrompt: deps.enqueueSectionPrompt,
531
543
  deleteMessage: deps.deleteMessage,
532
544
  });
@@ -591,6 +603,9 @@ export async function handleTelegramMenuCallbackRuntime<
591
603
  deps.editInteractiveMessage ?? (async () => {}),
592
604
  sendInteractiveMessage:
593
605
  deps.sendInteractiveMessage ?? (async () => undefined),
606
+ sendRichMessage: deps.sendSectionRichMessage ?? (async () => {
607
+ throw new Error("Rich Message delivery is unavailable");
608
+ }),
594
609
  enqueuePrompt: deps.enqueueSectionPrompt
595
610
  ? (prompt: string) =>
596
611
  deps.enqueueSectionPrompt!(prompt, ctx, target, query)
@@ -614,6 +629,9 @@ export async function handleTelegramMenuCallbackRuntime<
614
629
  deps.editInteractiveMessage ?? (async () => {}),
615
630
  sendInteractiveMessage:
616
631
  deps.sendInteractiveMessage ?? (async () => undefined),
632
+ sendRichMessage: deps.sendSectionRichMessage ?? (async () => {
633
+ throw new Error("Rich Message delivery is unavailable");
634
+ }),
617
635
  enqueuePrompt: deps.enqueueSectionPrompt
618
636
  ? (prompt: string) =>
619
637
  deps.enqueueSectionPrompt!(prompt, ctx, target, query)
@@ -639,6 +657,9 @@ export async function handleTelegramMenuCallbackRuntime<
639
657
  deps.editInteractiveMessage ?? (async () => {}),
640
658
  sendInteractiveMessage:
641
659
  deps.sendInteractiveMessage ?? (async () => undefined),
660
+ sendRichMessage: deps.sendSectionRichMessage ?? (async () => {
661
+ throw new Error("Rich Message delivery is unavailable");
662
+ }),
642
663
  enqueuePrompt: deps.enqueueSectionPrompt
643
664
  ? (prompt: string) =>
644
665
  deps.enqueueSectionPrompt!(prompt, ctx, target, query)
@@ -3055,6 +3055,7 @@ export interface TelegramQueueDispatchControllerDeps<
3055
3055
 
3056
3056
  export interface TelegramQueueDispatchController<TContext = unknown> {
3057
3057
  dispatchNext: (ctx: TContext) => void;
3058
+ requestNextDispatchAnnouncement: () => void;
3058
3059
  }
3059
3060
 
3060
3061
  export function executeTelegramQueueDispatchPlan<TContext = unknown>(
@@ -3123,7 +3124,13 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
3123
3124
  deps: TelegramQueueDispatchControllerDeps<TContext>,
3124
3125
  ): TelegramQueueDispatchController<TContext> {
3125
3126
  let controlDispatchPending = false;
3127
+ let nextDispatchAnnouncementRequested = false;
3128
+ let nextDispatchAnnouncementAnchor: TelegramQueueItem<TContext> | undefined;
3126
3129
  const controller: TelegramQueueDispatchController<TContext> = {
3130
+ requestNextDispatchAnnouncement: () => {
3131
+ nextDispatchAnnouncementRequested = true;
3132
+ nextDispatchAnnouncementAnchor = undefined;
3133
+ },
3127
3134
  dispatchNext: (ctx) => {
3128
3135
  if (deps.hasDispatchContext && !deps.hasDispatchContext()) return;
3129
3136
  if (controlDispatchPending) {
@@ -3151,6 +3158,10 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
3151
3158
  }
3152
3159
  if (droppedInactiveItemCount > 0) {
3153
3160
  deps.setQueuedItems(retainedItems);
3161
+ if (retainedItems.length === 0) {
3162
+ nextDispatchAnnouncementRequested = false;
3163
+ nextDispatchAnnouncementAnchor = undefined;
3164
+ }
3154
3165
  deps.recordRuntimeEvent?.(
3155
3166
  "dispatch",
3156
3167
  new Error(
@@ -3209,6 +3220,21 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
3209
3220
  }
3210
3221
  const dispatchableItems = activeItems.slice(nextActiveIndex);
3211
3222
  const nextItem = dispatchableItems[0];
3223
+ if (nextDispatchAnnouncementRequested) {
3224
+ if (nextDispatchAnnouncementAnchor) {
3225
+ const anchorRetained = retainedItems.includes(nextDispatchAnnouncementAnchor);
3226
+ const anchorTransportActive =
3227
+ deps.isQueueItemTransportActive?.(nextDispatchAnnouncementAnchor) !== false;
3228
+ if (!anchorRetained || !anchorTransportActive) {
3229
+ nextDispatchAnnouncementRequested = false;
3230
+ nextDispatchAnnouncementAnchor = undefined;
3231
+ }
3232
+ } else if (nextItem) {
3233
+ nextDispatchAnnouncementAnchor = nextItem;
3234
+ } else if (retainedItems.length === 0) {
3235
+ nextDispatchAnnouncementRequested = false;
3236
+ }
3237
+ }
3212
3238
  if (
3213
3239
  nextItem &&
3214
3240
  deps.hasPendingInboundQueueMutationForItem?.(nextItem)
@@ -3224,17 +3250,49 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
3224
3250
  deps.updateStatus(ctx);
3225
3251
  return;
3226
3252
  }
3253
+ const dispatchBasisItems = deps.getQueuedItems();
3227
3254
  const dispatchPlan = planNextTelegramQueueAction(
3228
3255
  dispatchableItems,
3229
3256
  canDispatch,
3230
3257
  );
3231
- if (dispatchPlan.kind !== "none") {
3258
+ const commitDispatchPlan = (): boolean => {
3259
+ if (dispatchPlan.kind === "none") return true;
3260
+ const currentItems = deps.getQueuedItems();
3261
+ if (dispatchPlan.kind === "prompt") {
3262
+ const queueDrifted =
3263
+ currentItems.length !== dispatchBasisItems.length ||
3264
+ currentItems.some((item, index) => item !== dispatchBasisItems[index]);
3265
+ const dispatchEligibilityDrifted =
3266
+ !deps.canDispatch(ctx) ||
3267
+ deps.hasPendingInboundQueueMutationForItem?.(dispatchPlan.item) === true ||
3268
+ (deps.isQueueItemAdmissionReady?.(dispatchPlan.item) === false) ||
3269
+ (deps.isQueueItemTransportActive?.(dispatchPlan.item) === false);
3270
+ if (queueDrifted || dispatchEligibilityDrifted) {
3271
+ const selectedItemRetained = currentItems.includes(dispatchPlan.item);
3272
+ const selectedTransportActive =
3273
+ deps.isQueueItemTransportActive?.(dispatchPlan.item) !== false;
3274
+ nextDispatchAnnouncementRequested =
3275
+ selectedItemRetained && selectedTransportActive;
3276
+ nextDispatchAnnouncementAnchor = nextDispatchAnnouncementRequested
3277
+ ? dispatchPlan.item
3278
+ : undefined;
3279
+ deps.updateStatus(ctx);
3280
+ if (nextDispatchAnnouncementRequested && queueDrifted && !dispatchEligibilityDrifted) {
3281
+ controller.dispatchNext(ctx);
3282
+ }
3283
+ return false;
3284
+ }
3285
+ }
3232
3286
  deps.setQueuedItems([
3233
3287
  ...dispatchPlan.remainingItems,
3234
3288
  ...protectedInactiveItems,
3235
3289
  ]);
3236
- }
3237
- executeTelegramQueueDispatchPlan(dispatchPlan, {
3290
+ nextDispatchAnnouncementAnchor = undefined;
3291
+ return true;
3292
+ };
3293
+ const executePlan = (): void => {
3294
+ if (!commitDispatchPlan()) return;
3295
+ executeTelegramQueueDispatchPlan(dispatchPlan, {
3238
3296
  executeControlItem: (item) => {
3239
3297
  controlDispatchPending = true;
3240
3298
  const dispatchGeneration = deps.getDispatchGeneration?.();
@@ -3280,6 +3338,32 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
3280
3338
  deps.updateStatus(ctx);
3281
3339
  },
3282
3340
  });
3341
+ };
3342
+ if (dispatchPlan.kind === "prompt" && nextDispatchAnnouncementRequested) {
3343
+ nextDispatchAnnouncementRequested = false;
3344
+ controlDispatchPending = true;
3345
+ const dispatchGeneration = deps.getDispatchGeneration?.();
3346
+ deps.updateStatus(ctx);
3347
+ void deps.sendTextReply(
3348
+ dispatchPlan.item.chatId,
3349
+ dispatchPlan.item.replyToMessageId,
3350
+ "<b>⏩ Dispatching next queued turn.</b>",
3351
+ { target: dispatchPlan.item.target },
3352
+ ).catch((error) => {
3353
+ deps.recordRuntimeEvent?.("dispatch", error, { phase: "next-announcement" });
3354
+ }).finally(() => {
3355
+ controlDispatchPending = false;
3356
+ if (deps.hasDispatchContext && !deps.hasDispatchContext()) return;
3357
+ if (
3358
+ dispatchGeneration !== undefined &&
3359
+ deps.isDispatchGenerationActive &&
3360
+ !deps.isDispatchGenerationActive(dispatchGeneration)
3361
+ ) return;
3362
+ executePlan();
3363
+ });
3364
+ return;
3365
+ }
3366
+ executePlan();
3283
3367
  },
3284
3368
  };
3285
3369
  return controller;
@@ -730,6 +730,24 @@ export async function sendTelegramNativeMarkdownReply<TReplyMarkup = unknown>(
730
730
  return lastMessageId;
731
731
  }
732
732
 
733
+ export async function sendTelegramNativeRichMessage(
734
+ chatId: number,
735
+ richMessage: TelegramInputRichMessage,
736
+ deps: {
737
+ recordOwnership?: TelegramReplyOwnershipRecorder["record"];
738
+ sendRichMessage: (body: TelegramSendRichMessageBody) => Promise<TelegramSentMessage>;
739
+ },
740
+ options?: TelegramReplyTargetOptions,
741
+ ): Promise<number> {
742
+ const sent = await deps.sendRichMessage({
743
+ chat_id: chatId,
744
+ rich_message: richMessage,
745
+ ...(options?.target ? getTelegramTargetThreadParams(options.target) : {}),
746
+ });
747
+ deps.recordOwnership?.({ chatId, messageId: sent.message_id, target: options?.target });
748
+ return sent.message_id;
749
+ }
750
+
733
751
  // UI/compat regular-message runtime for bridge-owned text and interactive
734
752
  // surfaces. Assistant and guest Markdown delivery bypass this path and use
735
753
  // native Rich Message helpers above.
@@ -775,6 +793,11 @@ export interface TelegramRenderedMessageRuntime<TReplyMarkup> {
775
793
  replyMarkup: TReplyMarkup,
776
794
  options?: TelegramReplyTargetOptions,
777
795
  ) => Promise<number | undefined>;
796
+ sendSectionRichMessage: (
797
+ chatId: number,
798
+ message: TelegramInputRichMessage,
799
+ options?: TelegramReplyTargetOptions,
800
+ ) => Promise<number>;
778
801
  }
779
802
 
780
803
  export interface TelegramRenderedMessageDeliveryRuntime<
@@ -891,6 +914,11 @@ export function createTelegramRenderedMessageRuntime<TReplyMarkup>(
891
914
  },
892
915
  );
893
916
  },
917
+ sendSectionRichMessage: (chatId, message, options) =>
918
+ sendTelegramNativeRichMessage(chatId, message, {
919
+ recordOwnership: deps.recordOwnership,
920
+ sendRichMessage: deps.sendRichMessage,
921
+ }, options),
894
922
  };
895
923
  }
896
924
 
@@ -19,6 +19,7 @@ import * as Queue from "./queue.ts";
19
19
  import * as Replies from "./replies.ts";
20
20
  import type { TelegramBridgeRuntime } from "./runtime.ts";
21
21
  import type { TelegramSectionRegistry } from "./sections.ts";
22
+ import type { TelegramInputRichMessage } from "./telegram-api.ts";
22
23
  import * as TextGroups from "./text-groups.ts";
23
24
  import * as ThreadNaming from "./thread-naming.ts";
24
25
  import * as ThreadReconciler from "./thread-reconciler.ts";
@@ -625,6 +626,7 @@ export interface TelegramInboundRouteRuntimeDeps<
625
626
  updateStatus: (ctx: TContext, error?: string) => void;
626
627
  isContextActive?: (ctx: TContext) => boolean;
627
628
  dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
629
+ requestNextDispatchAnnouncement?: () => void;
628
630
  requestDeferredDispatchNextQueuedTelegramTurn?: (
629
631
  dispatch: (ctx: TContext) => void,
630
632
  ) => void;
@@ -706,6 +708,11 @@ export interface TelegramInboundRouteRuntimeDeps<
706
708
  details?: Record<string, unknown>,
707
709
  ) => void;
708
710
  sectionRegistry?: TelegramSectionRegistry;
711
+ sendSectionRichMessage?: (
712
+ chatId: number,
713
+ message: TelegramInputRichMessage,
714
+ options?: { target?: { chatId: number; threadId?: number } },
715
+ ) => Promise<number | undefined>;
709
716
  }
710
717
 
711
718
  const TELEGRAM_OWNED_CALLBACK_PREFIXES = [
@@ -1013,6 +1020,7 @@ export function createTelegramInboundRouteRuntime<
1013
1020
  sectionRegistry: deps.sectionRegistry,
1014
1021
  editInteractiveMessage: deps.editInteractiveMessage,
1015
1022
  sendInteractiveMessage: deps.sendInteractiveMessage,
1023
+ sendSectionRichMessage: deps.sendSectionRichMessage,
1016
1024
  deleteMessage: deps.deleteMessage,
1017
1025
  enqueueSectionPrompt: async (
1018
1026
  prompt: string,
@@ -2171,6 +2179,7 @@ export function createTelegramInboundRouteRuntime<
2171
2179
  updateStatus: deps.updateStatus,
2172
2180
  isContextActive: deps.isContextActive,
2173
2181
  dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
2182
+ requestNextDispatchAnnouncement: deps.requestNextDispatchAnnouncement,
2174
2183
  requestDeferredDispatchNextQueuedTelegramTurn:
2175
2184
  deps.requestDeferredDispatchNextQueuedTelegramTurn,
2176
2185
  startTypingLoop: deps.startTypingLoop,
@@ -8,6 +8,7 @@ import {
8
8
  assertTelegramCallbackData,
9
9
  type TelegramInlineKeyboardMarkup,
10
10
  } from "./keyboard.ts";
11
+ import type { TelegramInputRichMessage } from "./telegram-api.ts";
11
12
 
12
13
  const SECTION_REGISTRY_KEY = "__piTelegramSectionRegistry__";
13
14
 
@@ -66,6 +67,7 @@ export interface TelegramSectionContext {
66
67
  answerCallback(text?: string): Promise<void>;
67
68
  edit(view: TelegramSectionView): Promise<void>;
68
69
  open(view: TelegramSectionView): Promise<void>;
70
+ openRich(message: TelegramInputRichMessage): Promise<void>;
69
71
  enqueuePrompt(prompt: string): Promise<void>;
70
72
  callbackData(action: string, payload?: string): string;
71
73
  /** Delete the message that triggered this callback (dialog cleanup) */
@@ -81,6 +83,7 @@ export interface TelegramSectionCallbackContext {
81
83
  answerCallback(text?: string): Promise<void>;
82
84
  edit(view: TelegramSectionView): Promise<void>;
83
85
  open(view: TelegramSectionView): Promise<void>;
86
+ openRich(message: TelegramInputRichMessage): Promise<void>;
84
87
  enqueuePrompt(prompt: string): Promise<void>;
85
88
  callbackData(action: string, payload?: string): string;
86
89
  /** Delete the message that triggered this callback (dialog cleanup) */
@@ -160,6 +163,11 @@ export interface TelegramSectionRuntimeDeps {
160
163
  replyMarkup: TelegramInlineKeyboardMarkup,
161
164
  options?: { target?: TelegramSectionTarget },
162
165
  ) => Promise<number | undefined>;
166
+ sendRichMessage: (
167
+ chatId: number,
168
+ message: TelegramInputRichMessage,
169
+ options?: { target?: TelegramSectionTarget },
170
+ ) => Promise<number | undefined>;
163
171
  enqueuePrompt: (prompt: string) => Promise<void>;
164
172
  deleteMessage: (chatId: number, messageId: number) => Promise<void>;
165
173
  }
@@ -202,6 +210,12 @@ function buildTelegramSectionContext(
202
210
  deps.target ? { target: deps.target } : undefined,
203
211
  )
204
212
  .then(() => {}),
213
+ openRich: (message) =>
214
+ deps.sendRichMessage(
215
+ chatId,
216
+ message,
217
+ deps.target ? { target: deps.target } : undefined,
218
+ ).then(() => {}),
205
219
  enqueuePrompt: deps.enqueuePrompt,
206
220
  callbackData: (action, payload) =>
207
221
  buildTelegramSectionCallbackData(token, action, payload),
@@ -251,6 +265,12 @@ function buildTelegramSectionCallbackContext(
251
265
  deps.target ? { target: deps.target } : undefined,
252
266
  )
253
267
  .then(() => {}),
268
+ openRich: (message) =>
269
+ deps.sendRichMessage(
270
+ chatId,
271
+ message,
272
+ deps.target ? { target: deps.target } : undefined,
273
+ ).then(() => {}),
254
274
  enqueuePrompt: deps.enqueuePrompt,
255
275
  callbackData: (action, payload) =>
256
276
  buildTelegramSectionCallbackData(token, action, payload),
@@ -526,6 +546,11 @@ export interface TelegramSectionCallbackHandlerDeps {
526
546
  replyMarkup: TelegramInlineKeyboardMarkup,
527
547
  options?: { target?: TelegramSectionTarget },
528
548
  ) => Promise<number | undefined>;
549
+ sendRichMessage: (
550
+ chatId: number,
551
+ message: TelegramInputRichMessage,
552
+ options?: { target?: TelegramSectionTarget },
553
+ ) => Promise<number | undefined>;
529
554
  enqueuePrompt: (prompt: string) => Promise<void>;
530
555
  deleteMessage: (chatId: number, messageId: number) => Promise<void>;
531
556
  }
@@ -292,6 +292,7 @@ export type TelegramRichText =
292
292
  | { type: "bold" | "code"; text: TelegramRichText };
293
293
 
294
294
  export type TelegramInputRichBlock =
295
+ | { type: "heading"; text: TelegramRichText; size?: 1 | 2 | 3 }
295
296
  | { type: "pre"; text: TelegramRichText; language?: string }
296
297
  | {
297
298
  type: "details";
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.45.8",
3
+ "version": "0.45.11",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-kit",
3
- "version": "0.11.2",
3
+ "version": "0.11.4",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -44,8 +44,8 @@
44
44
  "@llblab/pi-clean-room": "0.1.1",
45
45
  "@llblab/pi-codex-usage": "0.9.4",
46
46
  "@llblab/pi-grow-loop": "0.8.1",
47
- "@llblab/pi-state-flow": "0.10.2",
48
- "@llblab/pi-telegram": "0.45.8",
47
+ "@llblab/pi-state-flow": "0.11.1",
48
+ "@llblab/pi-telegram": "0.45.11",
49
49
  "@llblab/skills": "1.15.0"
50
50
  },
51
51
  "bundledDependencies": [