@llblab/pi-kit 0.11.2 → 0.11.3
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 +5 -0
- package/README.md +2 -2
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +0 -43
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +4 -0
- package/node_modules/@llblab/pi-state-flow/README.md +1 -1
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +5 -0
- package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +96 -3
- package/node_modules/@llblab/pi-state-flow/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +5 -0
- package/node_modules/@llblab/pi-telegram/api/sections.ts +2 -0
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/sections.md +9 -0
- package/node_modules/@llblab/pi-telegram/docs/ui-style.md +1 -0
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +6 -4
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +5 -14
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +9 -2
- package/node_modules/@llblab/pi-telegram/lib/menu.ts +21 -0
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +87 -3
- package/node_modules/@llblab/pi-telegram/lib/replies.ts +28 -0
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +9 -0
- package/node_modules/@llblab/pi-telegram/lib/sections.ts +25 -0
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@llblab/pi-kit` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.11.3 - 2026-09-14
|
|
6
|
+
|
|
7
|
+
- `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.
|
|
8
|
+
- `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.
|
|
9
|
+
|
|
5
10
|
## 0.11.2 - 2026-09-13
|
|
6
11
|
|
|
7
12
|
- `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.
|
|
18
|
-
| [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.45.
|
|
17
|
+
| [`@llblab/pi-state-flow`](https://github.com/llblab/pi-state-flow) | `0.11.0` | 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.9` | Telegram companion with exact queues, 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,10 @@
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.11.0: Telegram state inspection
|
|
8
|
+
|
|
9
|
+
- `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.
|
|
10
|
+
|
|
7
11
|
## 0.10.2: Token-guided compaction and patch resilience
|
|
8
12
|
|
|
9
13
|
- `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 also exposes the same Start/Stop controls and a `👁 Show state` chooser for Global, CWD, Session, or Effective memory. Selecting one scope sends exactly that materialized slice as a standalone native Rich Message with 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,24 @@ 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: "pre"; text: string; language?: string }
|
|
30
|
+
| { type: "details"; summary: string | { type: "bold" | "code"; text: string }; blocks: StateFlowTelegramRichBlock[]; is_open?: true };
|
|
31
|
+
|
|
32
|
+
export interface StateFlowTelegramRichMessage {
|
|
33
|
+
blocks: StateFlowTelegramRichBlock[];
|
|
34
|
+
skip_entity_detection?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
19
37
|
export interface StateFlowTelegramButton {
|
|
20
38
|
text: string;
|
|
21
39
|
callback_data: string;
|
|
@@ -30,6 +48,7 @@ export interface StateFlowTelegramView {
|
|
|
30
48
|
export interface StateFlowTelegramSectionContext {
|
|
31
49
|
callbackData(action: string, payload?: string): string;
|
|
32
50
|
edit(view: StateFlowTelegramView): Promise<void>;
|
|
51
|
+
openRich(message: StateFlowTelegramRichMessage): Promise<void>;
|
|
33
52
|
answerCallback(text?: string): Promise<void>;
|
|
34
53
|
}
|
|
35
54
|
|
|
@@ -61,6 +80,7 @@ export interface StateFlowTelegramControlResult {
|
|
|
61
80
|
|
|
62
81
|
export interface StateFlowTelegramPort {
|
|
63
82
|
snapshot(): StateFlowTelegramSnapshot;
|
|
83
|
+
state(scope: StateFlowTelegramScope): StateFlowTelegramState;
|
|
64
84
|
canStartNow(): boolean;
|
|
65
85
|
start(): StateFlowTelegramControlResult;
|
|
66
86
|
stop(): StateFlowTelegramControlResult;
|
|
@@ -103,10 +123,72 @@ export function buildStateFlowSectionView(
|
|
|
103
123
|
return {
|
|
104
124
|
text: [formatStateFlowSectionHeader(snapshot), "", STATE_FLOW_SECTION_HELP].join("\n"),
|
|
105
125
|
parseMode: "html",
|
|
106
|
-
replyMarkup: { inline_keyboard: [
|
|
126
|
+
replyMarkup: { inline_keyboard: [
|
|
127
|
+
[{ text: "👁 Show state", callback_data: callbackData("show-state") }],
|
|
128
|
+
[action],
|
|
129
|
+
] },
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function buildStateFlowScopeChooser(callbackData: (action: string, payload?: string) => string): StateFlowTelegramView {
|
|
134
|
+
return {
|
|
135
|
+
text: "<b>👁 Show state:</b>",
|
|
136
|
+
parseMode: "html",
|
|
137
|
+
replyMarkup: { inline_keyboard: [
|
|
138
|
+
["global", "cwd"].map((scope) => ({ text: scope === "global" ? "Global" : "CWD", callback_data: callbackData("inspect", scope) })),
|
|
139
|
+
["session", "effective"].map((scope) => ({ text: scope === "session" ? "Session" : "Effective", callback_data: callbackData("inspect", scope) })),
|
|
140
|
+
] },
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// The complete message serializes each preformatted field one additional time;
|
|
145
|
+
// 3,000 leaves safe headroom for worst-case JSON escaping across all four fields.
|
|
146
|
+
const STATE_FLOW_TELEGRAM_FIELD_MAX_CHARS = 3_000;
|
|
147
|
+
|
|
148
|
+
function renderStateFlowTelegramField(value: unknown): string {
|
|
149
|
+
const json = JSON.stringify(value, null, 2);
|
|
150
|
+
if (json.length <= STATE_FLOW_TELEGRAM_FIELD_MAX_CHARS) return json;
|
|
151
|
+
let low = 0;
|
|
152
|
+
let high = json.length;
|
|
153
|
+
let rendered = "";
|
|
154
|
+
while (low <= high) {
|
|
155
|
+
const length = Math.floor((low + high) / 2);
|
|
156
|
+
const candidate = JSON.stringify({
|
|
157
|
+
truncated: true,
|
|
158
|
+
preview: json.slice(0, length),
|
|
159
|
+
omittedChars: json.length - length,
|
|
160
|
+
}, null, 2);
|
|
161
|
+
if (candidate.length <= STATE_FLOW_TELEGRAM_FIELD_MAX_CHARS) {
|
|
162
|
+
rendered = candidate;
|
|
163
|
+
low = length + 1;
|
|
164
|
+
} else {
|
|
165
|
+
high = length - 1;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return rendered;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function renderStateFlowRichState(scope: StateFlowTelegramScope, state: StateFlowTelegramState): StateFlowTelegramRichMessage {
|
|
172
|
+
const title = scope === "cwd" ? "CWD" : `${scope[0].toUpperCase()}${scope.slice(1)}`;
|
|
173
|
+
const fields = ["artifacts", "contract", "working", "response"] as const;
|
|
174
|
+
return {
|
|
175
|
+
blocks: [{
|
|
176
|
+
type: "details",
|
|
177
|
+
summary: { type: "bold", text: title },
|
|
178
|
+
blocks: fields.map((field) => ({
|
|
179
|
+
type: "details",
|
|
180
|
+
summary: { type: "code", text: field },
|
|
181
|
+
blocks: [{ type: "pre", language: "json", text: renderStateFlowTelegramField(state[field]) }],
|
|
182
|
+
})),
|
|
183
|
+
}],
|
|
184
|
+
skip_entity_detection: true,
|
|
107
185
|
};
|
|
108
186
|
}
|
|
109
187
|
|
|
188
|
+
function isStateFlowTelegramScope(value: string): value is StateFlowTelegramScope {
|
|
189
|
+
return value === "global" || value === "cwd" || value === "session" || value === "effective";
|
|
190
|
+
}
|
|
191
|
+
|
|
110
192
|
function buildStateFlowTelegramSection(port: StateFlowTelegramPort) {
|
|
111
193
|
return {
|
|
112
194
|
id: STATE_FLOW_TELEGRAM_ID,
|
|
@@ -115,10 +197,21 @@ function buildStateFlowTelegramSection(port: StateFlowTelegramPort) {
|
|
|
115
197
|
render: (ctx: StateFlowTelegramSectionContext) =>
|
|
116
198
|
buildStateFlowSectionView(port.snapshot(), (action) => ctx.callbackData(action)),
|
|
117
199
|
handleCallback: async (ctx: StateFlowTelegramCallbackContext) => {
|
|
118
|
-
// cancel/refresh remain routable for keyboards sent by earlier versions
|
|
119
|
-
if (ctx.action !== "start" && ctx.action !== "stop" && ctx.action !== "cancel" && ctx.action !== "refresh") return "pass" as const;
|
|
200
|
+
// cancel/refresh remain routable for keyboards sent by earlier versions.
|
|
201
|
+
if (ctx.action !== "start" && ctx.action !== "stop" && ctx.action !== "cancel" && ctx.action !== "refresh" && ctx.action !== "show-state" && ctx.action !== "inspect") return "pass" as const;
|
|
120
202
|
let notice: string | undefined;
|
|
121
203
|
try {
|
|
204
|
+
if (ctx.action === "show-state") {
|
|
205
|
+
await ctx.answerCallback();
|
|
206
|
+
await ctx.edit(buildStateFlowScopeChooser((action, payload) => ctx.callbackData(action, payload)));
|
|
207
|
+
return "handled" as const;
|
|
208
|
+
}
|
|
209
|
+
if (ctx.action === "inspect") {
|
|
210
|
+
if (!isStateFlowTelegramScope(ctx.payload)) throw new Error("Unknown State Flow scope");
|
|
211
|
+
await ctx.openRich(renderStateFlowRichState(ctx.payload, port.state(ctx.payload)));
|
|
212
|
+
await ctx.answerCallback();
|
|
213
|
+
return "handled" as const;
|
|
214
|
+
}
|
|
122
215
|
if (ctx.action === "start") {
|
|
123
216
|
if (port.canStartNow()) notice = port.start().message;
|
|
124
217
|
else {
|
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.45.9: Rich sections and exact queue continuation
|
|
8
|
+
|
|
9
|
+
- `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.
|
|
10
|
+
- `/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.
|
|
11
|
+
|
|
7
12
|
## 0.45.8: Follower Forwarding Hotfix
|
|
8
13
|
|
|
9
14
|
- `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.
|
|
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` —
|
|
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
|
|
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
|
-
})
|
|
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 {
|
|
633
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-kit",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.3",
|
|
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.
|
|
48
|
-
"@llblab/pi-telegram": "0.45.
|
|
47
|
+
"@llblab/pi-state-flow": "0.11.0",
|
|
48
|
+
"@llblab/pi-telegram": "0.45.9",
|
|
49
49
|
"@llblab/skills": "1.15.0"
|
|
50
50
|
},
|
|
51
51
|
"bundledDependencies": [
|