@llblab/pi-kit 0.34.1 → 0.34.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/BACKLOG.md +2 -0
- package/CHANGELOG.md +9 -0
- package/README.md +3 -3
- package/node_modules/@llblab/pi-claude-usage/CHANGELOG.md +4 -0
- package/node_modules/@llblab/pi-claude-usage/package.json +1 -1
- package/node_modules/@llblab/pi-codex-usage/CHANGELOG.md +5 -0
- package/node_modules/@llblab/pi-codex-usage/README.md +2 -0
- package/node_modules/@llblab/pi-codex-usage/lib/query.ts +45 -1
- package/node_modules/@llblab/pi-codex-usage/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +10 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +2 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.js +19 -9
- package/node_modules/@llblab/pi-telegram/dist/lib/prompts.d.ts +4 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/prompts.js +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/runtime.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/runtime.js +19 -1
- package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/dist/skills/telegram-bridge/SKILL.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +2 -2
- package/node_modules/@llblab/pi-telegram/docs/compact-matrix-literal.md +2 -2
- package/node_modules/@llblab/pi-telegram/docs/outbound.md +2 -1
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +1 -1
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +2 -1
- package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +17 -9
- package/node_modules/@llblab/pi-telegram/lib/prompts.ts +7 -4
- package/node_modules/@llblab/pi-telegram/lib/runtime.ts +25 -3
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/SKILL.md +1 -1
- package/package.json +4 -4
package/BACKLOG.md
CHANGED
|
@@ -4,6 +4,8 @@ Package pins, resource order and bundled runtime ownership remain authoritative
|
|
|
4
4
|
|
|
5
5
|
## Carried checks
|
|
6
6
|
|
|
7
|
+
- **Installed 0.34.3 typing smoke (operator-owned):** After separately authorized installation/reload of both peers, verify native typing during streaming, UI prompts and compaction without recurring local authorization churn. Check actual client versions, including iOS; successful HTTP issuance and synthetic tests do not prove client visibility. Use disposable sessions and preserve accepted work.
|
|
8
|
+
- **Installed 0.34.2 smoke (operator-owned):** After separately authorized installation/reload, verify Pi 1.2 custom-message completions no longer raise the Telegram prompt-hook error, adjacent button matrices retain their rows and callbacks, and native Windows Codex CLI fallback works on an actual Windows host. Use disposable settings; package validation and synthetic SDK probes do not certify installed clients or authorize live fault injection.
|
|
7
9
|
- **Installed 0.34.0 Telegram smoke (operator-owned):** After separately authorized installation/reload of both peers, verify per-prompt Steer, canonical thumbs-up/thumbs-down/fire controls with other reactions ignored, reconnection after a deleted tab, follower `/start` Restore and first-click leader Restore confirmation with separately proven old-tab cleanup. Unknown creation can leave an extra tab; do not erase accepted work or replay uncertain input. Package/CI validation does not certify live clients.
|
|
8
10
|
- **Installed 0.31.0 Telegram smoke (operator-owned):** After separately authorized installation/reload of every Pi process together (bus protocol v3), confirm an idle leader makes no periodic `state.json` or `telegram.json.transaction` writes, a frozen leader is replaced by its follower, and switching Threaded Mode off takes followers offline. Packed validation does not certify running clients.
|
|
9
11
|
- **Installed 0.30.0 Telegram smoke (operator-owned):** After separately authorized installation/reload, confirm live Restore/rebinding in both roles, immediate temporary-tab removal after Cancel and, on Windows, Restore and follower delivery with disposable runtime storage. Packed validation does not certify running clients.
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@llblab/pi-kit` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.34.3: Telegram Typing Continuity
|
|
6
|
+
|
|
7
|
+
- `Telegram typing`: Advances only the exact Telegram pin to `0.56.4`. Streaming keepalives preserve the same owner's in-flight typing request, while session, transport and target changes still revoke old authority. Compaction cannot claim an existing agent loop. Other pins, package membership and resource order are unchanged; client-specific typing visibility remains a live check.
|
|
8
|
+
|
|
9
|
+
## 0.34.2: Telegram Compatibility and Windows Usage Updates
|
|
10
|
+
|
|
11
|
+
- `Telegram`: Advances the exact pin to `0.56.3`, adding adjacent button matrices with preserved row grouping and fixing Pi 1.2 custom-message starts without a user prompt. Legacy prompt events and voice syntax remain supported; no client-rendering fallback is added.
|
|
12
|
+
- `Usage extensions`: Advances Codex Usage to `0.13.1` for native Windows Codex CLI fallback and Claude Usage to `0.2.2` for its Windows-validated release with unchanged runtime. All eight package pins were checked against npm stable releases; other pins, composition and resource order remain unchanged.
|
|
13
|
+
|
|
5
14
|
## 0.34.1: Canonical Telegram Reactions
|
|
6
15
|
|
|
7
16
|
- `Telegram reactions`: Advances only the exact Telegram pin to `0.56.2`. Only thumbs-up selects Priority, thumbs-down selects Skip, and fire selects Steer; retired aliases no longer flush or control queued input. Queue badges use canonical emoji without rewriting accepted state. Other pins, package membership, resources and load order are unchanged.
|
package/README.md
CHANGED
|
@@ -13,12 +13,12 @@ Package links lead to the owning repositories for usage, documentation, issues,
|
|
|
13
13
|
| Package | Version | Purpose |
|
|
14
14
|
| --- | ---: | --- |
|
|
15
15
|
| [`@llblab/pi-actors`](https://github.com/llblab/pi-actors) | `0.54.0` | Inspectable local Runs, reusable Recipes, persistent tools, and delegation Skills |
|
|
16
|
-
| [`@llblab/pi-claude-usage`](https://github.com/llblab/pi-claude-usage) | `0.2.
|
|
16
|
+
| [`@llblab/pi-claude-usage`](https://github.com/llblab/pi-claude-usage) | `0.2.2` | Claude subscription quota status and shared per-model Fast toggle for Opus, mirrored in Telegram |
|
|
17
17
|
| [`@llblab/pi-clean-room`](https://github.com/llblab/pi-clean-room) | `0.3.0` | Isolated nested Pi TUI with named npm extensions and compatible model selection |
|
|
18
|
-
| [`@llblab/pi-codex-usage`](https://github.com/llblab/pi-codex-usage) | `0.13.
|
|
18
|
+
| [`@llblab/pi-codex-usage`](https://github.com/llblab/pi-codex-usage) | `0.13.1` | OpenAI/legacy Codex subscription quota and Business credit status, with persistent priority Fast toggle mirrored in Telegram |
|
|
19
19
|
| [`@llblab/pi-grow-loop`](https://github.com/llblab/pi-grow-loop) | `0.9.0` | Visible continuation scheduling and bounded worker Skills through compiled, manifest-owned resources |
|
|
20
20
|
| [`@llblab/pi-state-flow`](https://github.com/llblab/pi-state-flow) | `0.26.4` | Scoped context/memory compiler with step-independent current memory, stable Passive/Active modes and visible blocked-state status |
|
|
21
|
-
| [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.56.
|
|
21
|
+
| [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.56.4` | Telegram companion with per-prompt Steer, queued controls, Reroute/Restore, recoverable connections, previews, follower Threads, files, and voice |
|
|
22
22
|
| [`@llblab/skills`](https://github.com/llblab/skills) | `1.15.0` | Portable workflows for engineering, review, design, context maintenance, and other focused tasks |
|
|
23
23
|
|
|
24
24
|
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.
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.2: Windows CI Validation
|
|
4
|
+
|
|
5
|
+
- Added Linux and Windows GitHub Actions validation that runs the full release check suite (typecheck, tests, extension import, dependency audit and package dry-run) on every push to `main` and pull request, so the shared-refresh SQLite mutex, atomic state writes and path handling are exercised on Windows. The extension runtime is unchanged from 0.2.1.
|
|
6
|
+
|
|
3
7
|
## 0.2.1: Telegram Fast Status
|
|
4
8
|
|
|
5
9
|
- The optional Telegram status row now appends lowercase ` fast` for the active eligible Opus model, or shows `fast` alone when quota is unavailable. It rereads the per-model preference at menu render time, so toggles and model changes do not wait for quota refresh. Unsupported Claude families ignore stale Fast overrides. Quota polling, auth, request adaptation and terminal behavior are unchanged; isolated tests cover eligibility, model/provider changes and unreadable configuration.
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.13.1: Windows Codex CLI Fallback
|
|
4
|
+
|
|
5
|
+
- The Codex CLI fallback now starts on Windows. When npm installs Codex behind a `codex.cmd` shim that a shell-free `spawn` cannot launch, the extension resolves the native `codex.exe` from PATH or from the npm platform package and starts it directly, so fallback quota stays available when the primary endpoint fails. Non-Windows platforms keep plain PATH lookup, and the helper window is hidden.
|
|
6
|
+
- Added Linux and Windows GitHub Actions validation for the full release check suite.
|
|
7
|
+
|
|
3
8
|
## 0.13.0: OpenAI Subscription Provider Support
|
|
4
9
|
|
|
5
10
|
- Added `openai` alongside `openai-codex` for `/fast`, request priority adaptation, terminal status and the optional Telegram row. Fast preferences remain isolated by provider/model, including identical model IDs. Both providers share the existing Codex subscription quota stream. Pi subscription OAuth under `openai` now fetches quota directly without requiring the legacy provider; `openai` API-key auth is skipped and the Codex CLI fallback is preserved.
|
|
@@ -172,6 +172,8 @@ The extension tries usage sources in this order:
|
|
|
172
172
|
1. Pi's subscription auth from `openai` or legacy `openai-codex` (active model first)
|
|
173
173
|
2. `codex app-server --listen stdio://`
|
|
174
174
|
|
|
175
|
+
On Windows, the fallback resolves the native `codex.exe` from PATH or from the npm platform package behind a `codex.cmd` shim, then launches it directly without a shell. If Codex is missing, install it with `npm install -g @openai/codex@latest` and restart Pi so it inherits the updated PATH.
|
|
176
|
+
|
|
175
177
|
Selecting an `openai` model enables the status and Fast toggle, and its ChatGPT subscription OAuth credentials can fetch the quota directly; a legacy `openai-codex` provider is not required. The extension uses Pi's resolved auth headers/token, checks `modelRegistry.isUsingOAuth` before using `openai` credentials, and keeps the Codex CLI fallback. OpenAI API keys are not ChatGPT subscription auth and do not expose these quotas; `openai` API-key credentials are skipped, not sent to the ChatGPT quota endpoint. Without subscription auth/quota, the quota status is `n/a` when both sources report an unavailable state. Fast works independently of subscription auth.
|
|
176
178
|
|
|
177
179
|
## License
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** Domain: quota transport. Owns: Pi auth, Codex app-server fallback and errors. Excludes: shared leadership and UI. */
|
|
2
2
|
import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
3
5
|
import { createInterface } from "node:readline";
|
|
4
6
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
7
|
import { assertObject, errorMessage, isOpenAICodexModel, normalizeAppServerResponse, normalizeBackendPayload, parseJsonObject, selectUsageSnapshot, type AppServerRateLimitResponse, type CodexUsageModel, type CodexUsageReport, type RateLimitStatusPayload, type UsageQueryError } from "./usage.ts";
|
|
@@ -196,6 +198,47 @@ async function queryViaCodexAppServer(
|
|
|
196
198
|
}
|
|
197
199
|
}
|
|
198
200
|
|
|
201
|
+
const WINDOWS_CODEX_TARGETS: Record<string, string> = {
|
|
202
|
+
x64: "x86_64-pc-windows-msvc",
|
|
203
|
+
arm64: "aarch64-pc-windows-msvc",
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* npm installs Codex behind a `codex.cmd` shim that a shell-free `spawn` cannot
|
|
208
|
+
* launch, so on Windows resolve the native executable used by the npm platform
|
|
209
|
+
* package. Other platforms keep plain PATH lookup. Launching the native binary
|
|
210
|
+
* attaches disposal to the app-server process instead of a shell or Node shim.
|
|
211
|
+
*/
|
|
212
|
+
export function resolveCodexExecutable(
|
|
213
|
+
platform: NodeJS.Platform = process.platform,
|
|
214
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
215
|
+
arch: string = process.arch,
|
|
216
|
+
): string {
|
|
217
|
+
if (platform !== "win32") return "codex";
|
|
218
|
+
const target = WINDOWS_CODEX_TARGETS[arch];
|
|
219
|
+
const path = Object.entries(env).find(([key]) => key.toLowerCase() === "path")?.[1] ?? "";
|
|
220
|
+
for (const entry of path.split(";")) {
|
|
221
|
+
// Ignore empty entries rather than searching the current working directory.
|
|
222
|
+
const directory = entry.replace(/^"(.*)"$/, "$1");
|
|
223
|
+
if (!directory) continue;
|
|
224
|
+
const direct = join(directory, "codex.exe");
|
|
225
|
+
if (existsSync(direct)) return direct;
|
|
226
|
+
// A `.cmd` entry means an npm install; its native binary lives in the platform package.
|
|
227
|
+
if (!target || !existsSync(join(directory, "codex.cmd"))) continue;
|
|
228
|
+
const packageDirs = [
|
|
229
|
+
join(directory, "node_modules", "@openai", "codex", "node_modules", "@openai", `codex-win32-${arch}`),
|
|
230
|
+
join(directory, "node_modules", "@openai", `codex-win32-${arch}`),
|
|
231
|
+
];
|
|
232
|
+
for (const packageDir of packageDirs) {
|
|
233
|
+
const binary = join(packageDir, "vendor", target, "bin", "codex.exe");
|
|
234
|
+
if (existsSync(binary)) return binary;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
throw new Error(
|
|
238
|
+
"Could not find a native Codex executable on PATH. Install Codex with npm install -g @openai/codex@latest, then restart Pi so it inherits the updated PATH.",
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
199
242
|
class CodexAppServerClient {
|
|
200
243
|
private child?: ChildProcessWithoutNullStreams;
|
|
201
244
|
private nextId = 1;
|
|
@@ -213,8 +256,9 @@ class CodexAppServerClient {
|
|
|
213
256
|
if (this.startPromise) return this.startPromise;
|
|
214
257
|
|
|
215
258
|
this.startPromise = new Promise((resolve, reject) => {
|
|
216
|
-
const child = spawn(
|
|
259
|
+
const child = spawn(resolveCodexExecutable(), ["app-server", "--listen", "stdio://"], {
|
|
217
260
|
stdio: ["pipe", "pipe", "pipe"],
|
|
261
|
+
windowsHide: true,
|
|
218
262
|
});
|
|
219
263
|
this.child = child;
|
|
220
264
|
|
|
@@ -4,6 +4,16 @@
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.56.4: Typing Keepalive Continuity
|
|
8
|
+
|
|
9
|
+
- `Typing continuity`: Streaming updates keep the current work's pending presence request alive instead of cancelling it through repeated loop replacement. Explicit starts, session or transport changes, Thread changes and stopping still revoke the previous action; compaction cannot claim an existing agent loop.
|
|
10
|
+
- `Windows validation`: Grouped Restore fixtures control album debounce after both originals are admitted and await chooser publication, so slow filesystem work cannot accidentally split their input group. Exact source, receipt and readiness assertions remain intact; production debounce and routing behavior are unchanged.
|
|
11
|
+
|
|
12
|
+
## 0.56.3: Button Matrices and Custom-Message Starts
|
|
13
|
+
|
|
14
|
+
- `Button grammar`: Adjacent complete JSON/CML cells and matrices now compose one button surface in source order instead of dropping fenced controls or truncating footer controls. Each matrix contributes its existing rows; horizontal grouping, disabled controls and callback semantics remain unchanged. Invalid fenced groups register no callbacks. This extends button syntax only; rendering mode and voice syntax are unchanged.
|
|
15
|
+
- `Pi compatibility`: Extension-triggered runs on Pi 1.2 no longer fail when `before_agent_start` carries a custom message without a user prompt. Custom starts retain connected bridge guidance without being mistaken for fresh Telegram user input; legacy user events, unavailable-transport filtering and ordered system-prompt blocks remain supported.
|
|
16
|
+
|
|
7
17
|
## 0.56.2: Canonical Queue Reactions
|
|
8
18
|
|
|
9
19
|
- `Queue reactions`: Only thumbs-up selects Priority, thumbs-down selects Skip, and fire selects Steer. Heart, dove, ghost, broken-heart, poop and wastebasket aliases are no longer controls. Unsupported reactions neither flush waiting input nor change queue intent; removing a supported reaction does not fall back to a retired alias. Only thumbs-down cancels a waiting standalone Continue.
|
|
@@ -847,7 +847,7 @@ export function registerTelegramLifecycleRuntimeHooks({ pi, publicationRuntime,
|
|
|
847
847
|
return false;
|
|
848
848
|
const turn = activeTurnRuntime.get();
|
|
849
849
|
const target = turn?.target ?? proactivePushTargetGetter();
|
|
850
|
-
return (promptDispatchRuntime.startTypingLoop(ctx, turn?.chatId ?? target?.chatId, { target }) !== false);
|
|
850
|
+
return (promptDispatchRuntime.startTypingLoop(ctx, turn?.chatId ?? target?.chatId, { target, ensure: true }) !== false);
|
|
851
851
|
};
|
|
852
852
|
const startActiveTurnTypingLoop = (ctx) => {
|
|
853
853
|
if (uiPromptActive)
|
|
@@ -855,6 +855,7 @@ export function registerTelegramLifecycleRuntimeHooks({ pi, publicationRuntime,
|
|
|
855
855
|
const turn = activeTurnRuntime.get();
|
|
856
856
|
promptDispatchRuntime.startTypingLoop(ctx, turn?.chatId, {
|
|
857
857
|
target: turn?.target,
|
|
858
|
+
ensure: true,
|
|
858
859
|
});
|
|
859
860
|
};
|
|
860
861
|
let observedAutomaticCompaction = false;
|
|
@@ -463,23 +463,33 @@ function parseTelegramAdaptiveActionPayloadRows(source, parseCompactPayload, opt
|
|
|
463
463
|
}
|
|
464
464
|
return undefined;
|
|
465
465
|
};
|
|
466
|
-
const
|
|
466
|
+
const parseTopLevelRows = () => {
|
|
467
467
|
const rows = [];
|
|
468
468
|
while (offset < source.length) {
|
|
469
469
|
skipWhitespace();
|
|
470
|
-
|
|
470
|
+
const character = source[offset];
|
|
471
|
+
if (character === "{") {
|
|
472
|
+
const cell = parseCell();
|
|
473
|
+
if (!cell)
|
|
474
|
+
return undefined;
|
|
475
|
+
rows.push([cell]);
|
|
476
|
+
}
|
|
477
|
+
else if (character === "[" && options.allowMatrixGroups) {
|
|
478
|
+
const matrix = parseMatrix();
|
|
479
|
+
if (!matrix)
|
|
480
|
+
return undefined;
|
|
481
|
+
rows.push(...matrix);
|
|
482
|
+
}
|
|
483
|
+
else {
|
|
471
484
|
break;
|
|
472
|
-
|
|
473
|
-
if (!cell)
|
|
474
|
-
return undefined;
|
|
475
|
-
rows.push([cell]);
|
|
485
|
+
}
|
|
476
486
|
if (!consumeOptionalSeparator())
|
|
477
487
|
return undefined;
|
|
478
488
|
}
|
|
479
489
|
return rows.length > 0 ? rows : undefined;
|
|
480
490
|
};
|
|
481
491
|
skipWhitespace();
|
|
482
|
-
const rows =
|
|
492
|
+
const rows = parseTopLevelRows();
|
|
483
493
|
if (!rows)
|
|
484
494
|
return undefined;
|
|
485
495
|
skipWhitespace();
|
|
@@ -527,10 +537,10 @@ function isPlausibleTelegramMatrixStart(source, start) {
|
|
|
527
537
|
return (source[offset] === "{" || source[offset] === "[" || source[offset] === "]");
|
|
528
538
|
}
|
|
529
539
|
export function parseTelegramButtonPayloadRows(source) {
|
|
530
|
-
return parseTelegramAdaptiveActionPayloadRows(source.trim(), parseTelegramButtonCompactActionPayload, { maxCompactAtoms: 4 })?.rows;
|
|
540
|
+
return parseTelegramAdaptiveActionPayloadRows(source.trim(), parseTelegramButtonCompactActionPayload, { maxCompactAtoms: 4, allowMatrixGroups: true })?.rows;
|
|
531
541
|
}
|
|
532
542
|
export function parseTelegramActionPayloadRows(comment, command) {
|
|
533
|
-
const scanned = scanTelegramActionComment(comment, command, (content, offset) => parseTelegramAdaptiveActionPayloadRows(content.slice(offset), parseTelegramButtonCompactActionPayload, { allowTrailing: true, maxCompactAtoms: 4 })?.rows);
|
|
543
|
+
const scanned = scanTelegramActionComment(comment, command, (content, offset) => parseTelegramAdaptiveActionPayloadRows(content.slice(offset), parseTelegramButtonCompactActionPayload, { allowTrailing: true, maxCompactAtoms: 4, allowMatrixGroups: true })?.rows);
|
|
534
544
|
if (!scanned)
|
|
535
545
|
return undefined;
|
|
536
546
|
if ("payload" in scanned)
|
|
@@ -29,7 +29,9 @@ export declare function createTelegramModelContextAvailabilityRuntime(deps: {
|
|
|
29
29
|
memory?: TelegramModelContextAvailabilityMemory;
|
|
30
30
|
}): TelegramModelContextAvailabilityRuntime;
|
|
31
31
|
export type TelegramSystemPrompt = string | string[];
|
|
32
|
-
type TelegramBeforeAgentStartEvent = Omit<BeforeAgentStartEvent, "systemPrompt"> & {
|
|
32
|
+
type TelegramBeforeAgentStartEvent = Omit<BeforeAgentStartEvent, "systemPrompt" | "prompt" | "messageType"> & {
|
|
33
|
+
messageType?: "user" | "custom";
|
|
34
|
+
prompt?: string;
|
|
33
35
|
systemPrompt?: TelegramSystemPrompt | null;
|
|
34
36
|
};
|
|
35
37
|
type TelegramBeforeAgentStartResult = {
|
|
@@ -37,7 +39,7 @@ type TelegramBeforeAgentStartResult = {
|
|
|
37
39
|
};
|
|
38
40
|
type TelegramBeforeAgentStartHook = (event: TelegramBeforeAgentStartEvent) => TelegramBeforeAgentStartResult;
|
|
39
41
|
export declare function buildTelegramBridgeSystemPrompt(options: {
|
|
40
|
-
prompt
|
|
42
|
+
prompt?: string;
|
|
41
43
|
systemPrompt?: TelegramSystemPrompt | null;
|
|
42
44
|
telegramPrefix?: string;
|
|
43
45
|
localSystemPromptSuffix: string;
|
|
@@ -113,7 +113,7 @@ export function buildTelegramBridgeSystemPrompt(options) {
|
|
|
113
113
|
const telegramHead = telegramPrefix.endsWith("]")
|
|
114
114
|
? telegramPrefix.slice(0, -1)
|
|
115
115
|
: telegramPrefix;
|
|
116
|
-
const trimmedPrompt = options.prompt
|
|
116
|
+
const trimmedPrompt = options.prompt?.trimStart() ?? "";
|
|
117
117
|
const telegramTurn = trimmedPrompt.startsWith(`${telegramHead}]`) ||
|
|
118
118
|
trimmedPrompt.startsWith(`${telegramHead}|`);
|
|
119
119
|
const telegramSuffix = telegramTurn
|
|
@@ -127,7 +127,7 @@ export function buildTelegramBridgeSystemPrompt(options) {
|
|
|
127
127
|
}
|
|
128
128
|
export function createTelegramBeforeAgentStartHook(options = {}) {
|
|
129
129
|
return (event) => buildTelegramBridgeSystemPrompt({
|
|
130
|
-
prompt: event.prompt,
|
|
130
|
+
prompt: event.messageType === "custom" ? undefined : event.prompt,
|
|
131
131
|
systemPrompt: event.systemPrompt,
|
|
132
132
|
telegramPrefix: options.telegramPrefix,
|
|
133
133
|
localSystemPromptSuffix: options.localSystemPromptSuffix ?? LOCAL_SYSTEM_PROMPT_SUFFIX,
|
|
@@ -119,6 +119,7 @@ export interface TelegramTypingLoopStarterDeps<TContext> extends TelegramRuntime
|
|
|
119
119
|
}
|
|
120
120
|
export declare function createTelegramTypingLoopStarter<TContext>(deps: TelegramTypingLoopStarterDeps<TContext>): (ctx: TContext, chatId?: number, options?: {
|
|
121
121
|
target?: TelegramTypingLoopTarget;
|
|
122
|
+
ensure?: boolean;
|
|
122
123
|
}) => boolean;
|
|
123
124
|
/** The whole typing-loop state; only `transitionTelegramTypingLoop` produces a new one. */
|
|
124
125
|
export interface TelegramTypingMachineState {
|
|
@@ -214,6 +215,7 @@ export interface TelegramPromptDispatchRuntimeDeps<TContext> extends TelegramTyp
|
|
|
214
215
|
export interface TelegramPromptDispatchRuntime<TContext> {
|
|
215
216
|
startTypingLoop: (ctx: TContext, chatId?: number, options?: {
|
|
216
217
|
target?: TelegramTypingLoopTarget;
|
|
218
|
+
ensure?: boolean;
|
|
217
219
|
}) => boolean | void;
|
|
218
220
|
onPromptDispatchStart: (ctx: TContext, chatId?: number, target?: TelegramTypingLoopTarget) => void;
|
|
219
221
|
onPromptDispatchFailure: (ctx: TContext, message: string) => void;
|
|
@@ -161,6 +161,7 @@ function updateTelegramRuntimeStatusSafely(updateStatus, ctx, options) {
|
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
export function createTelegramTypingLoopStarter(deps) {
|
|
164
|
+
let currentOwner;
|
|
164
165
|
return (ctx, chatId, options) => {
|
|
165
166
|
const sessionGeneration = deps.getSessionGeneration?.();
|
|
166
167
|
const transportAuthority = deps.getTransportAuthority?.();
|
|
@@ -177,9 +178,24 @@ export function createTelegramTypingLoopStarter(deps) {
|
|
|
177
178
|
// Queue dispatch precedes agent_start: only an explicit destination permits Threaded presence, never chat fallback.
|
|
178
179
|
if (!canContinue())
|
|
179
180
|
return false;
|
|
181
|
+
const resolvedChatId = chatId ?? deps.getDefaultChatId();
|
|
182
|
+
if (resolvedChatId === undefined || resolvedChatId === 0)
|
|
183
|
+
return false;
|
|
184
|
+
// Streaming updates ensure presence without revoking the same owner's pending HTTP/IPC action.
|
|
185
|
+
// Explicit starts still replace the loop, including a new turn on the same session and Thread.
|
|
186
|
+
if (options?.ensure && currentOwner?.canContinue() &&
|
|
187
|
+
(deps.getSessionGeneration
|
|
188
|
+
? Object.is(currentOwner.sessionGeneration, sessionGeneration)
|
|
189
|
+
: Object.is(currentOwner.ctx, ctx)) &&
|
|
190
|
+
Object.is(currentOwner.transportAuthority, transportAuthority) &&
|
|
191
|
+
currentOwner.chatId === resolvedChatId && currentOwner.threadId === target?.threadId)
|
|
192
|
+
return false;
|
|
193
|
+
const owner = { ctx, sessionGeneration, transportAuthority, chatId: resolvedChatId,
|
|
194
|
+
threadId: target?.threadId, canContinue };
|
|
195
|
+
currentOwner = owner;
|
|
180
196
|
let active = true;
|
|
181
197
|
return deps.typing.start({
|
|
182
|
-
chatId:
|
|
198
|
+
chatId: resolvedChatId,
|
|
183
199
|
target,
|
|
184
200
|
intervalMs: deps.intervalMs ?? TELEGRAM_TYPING_ACTION_INTERVAL_MS,
|
|
185
201
|
sendTypingAction: async (targetChatId, actionOptions) => {
|
|
@@ -219,6 +235,8 @@ export function createTelegramTypingLoopStarter(deps) {
|
|
|
219
235
|
shouldContinue: canContinue,
|
|
220
236
|
onStopped: () => {
|
|
221
237
|
active = false;
|
|
238
|
+
if (currentOwner === owner)
|
|
239
|
+
currentOwner = undefined;
|
|
222
240
|
},
|
|
223
241
|
});
|
|
224
242
|
};
|
|
@@ -60,7 +60,7 @@ Every enabled button has a self-contained prompt and an optional selection style
|
|
|
60
60
|
- `{label|prompt}` separates visible label from queued prompt.
|
|
61
61
|
- `{label|prompt|selected_style}` and `{|prompt|selected_style}` accept `primary`, `success`, or `danger`.
|
|
62
62
|
- Fourth-position `1`/`true` disables, `0`/`false` enables; omission means enabled. JSON uses boolean `disabled`. `{|Next||1}` omits label/style; `{Next|||1}` omits prompt/style; `{|||1}` is blank (Telegram receives a non-breaking space). Prefer meaningful labels and retain a useful enabled action. Enabled CML requires a prompt. Disabled controls stay visible but have no callback, queued prompt, or bound-method invocation.
|
|
63
|
-
-
|
|
63
|
+
- Both wrappers concatenate rows from adjacent JSON/CML cells or matrices, with optional commas. Cells are vertical; horizontal peers require a nested row: `[[{A|a}{B|b}]]`.
|
|
64
64
|
- Prefer one matrix per related group. Fenced blocks stay in place in Rich mode; HTML compatibility moves them to the footer. Native rows allow eight buttons. Malformed/oversized/unclosed blocks activate nothing; drafts hide them. Outer code fences and quoted/indented examples remain literal.
|
|
65
65
|
- Both placements share prompt/app routing. In-body clicks acknowledge without recoloring; selected-style highlighting remains footer-only.
|
|
66
66
|
|
|
@@ -681,7 +681,7 @@ Native typing during compaction follows connected-instance activity rather than
|
|
|
681
681
|
- Thread-targeted typing is sent only to the concrete thread. Aggregate `All` mirroring is intentionally omitted because duplicating every keepalive multiplies shared-chat flood pressure. Compaction completion, failure, or timeout stops only a loop actually started by the compaction observer; a pre-existing agent-owned loop remains active. Ownership is sticky across a repeated compaction start, whose shared-loop start reports `false` for the observer's own running loop. Authority loss and shutdown still stop the keyed loop.
|
|
682
682
|
- Pi `ui_prompt_start` pauses typing while an extension-owned local prompt waits for the operator; `ui_prompt_end` emits the matching Activity boundary and resumes typing whenever agent or compaction work remains unsettled.
|
|
683
683
|
|
|
684
|
-
At every connected instance `agent_start`, the lifecycle binding starts Telegram's native `…typing` indicator in that instance's assigned target, whether the run came from Telegram, the local TUI, or an autonomous continuation such as Grow Loop. Terminal `Active` remains Telegram-turn-specific; the native indicator answers the separate question of whether the instance is doing agent work. Queue dispatch carries the queued turn's exact target into typing before `agent_start`, rather than reducing it to a chat ID. In Threaded Mode, missing or invalid Thread targets are refused before starting a loop or invoking the API; mode changes also invalidate a captured Classic chat-only loop. A rejected start never replaces an existing valid Thread loop. The starter also requires the captured Pi context and session generation to remain current at start, queued send, tick and late failure boundaries, even if the transport epoch is unchanged; replacing a context object in place cannot revive old activity. Message activity hooks capture generation across awaited preview work, and agent-start bindings recheck it before post-await activity/typing, so an obsolete finally or continuation cannot acquire replacement ownership. The typing sender carries its captured loop/context/generation/transport assertion as process-local call authority: direct API issuance rechecks it after queued work, and the follower caller rechecks it after registration waits and immediately before socket issuance without putting callbacks in IPC data. Automatic typing uses one HTTP attempt with no transport-family fallback or delayed replay, including forwarded calls; a future keepalive is a fresh owner-checked attempt. An already issued HTTP/RPC cannot be retroactively unsent. Each loop refreshes its exact target every three seconds and keeps one action in flight. The leader API runtime coalesces identical chat/thread/action calls for two seconds, permits at most one concurrent chat action per chat, and shares a Telegram 429 `retry_after` fence across every Thread key in that chat; expired gates prune opportunistically and each gate family retains at most 256 active keys. Suppression never schedules a delayed retry. A failed typing action remains a structured diagnostic but cannot project `error` onto an otherwise healthy connected/leader/follower status. Assistant message start/update hooks still re-arm typing during Telegram-owned turns so transient provider/model errors do not leave a continuing run without activity feedback, and agent/session completion stops it.
|
|
684
|
+
At every connected instance `agent_start`, the lifecycle binding starts Telegram's native `…typing` indicator in that instance's assigned target, whether the run came from Telegram, the local TUI, or an autonomous continuation such as Grow Loop. Terminal `Active` remains Telegram-turn-specific; the native indicator answers the separate question of whether the instance is doing agent work. Streaming keepalives reuse a valid typing owner with the same session generation, transport and target rather than revoking its pending HTTP/IPC action. Fresh context wrappers may reuse that owner within a proven generation; without generation evidence, context identity must match. Explicit queue dispatch and Telegram prompt handoff still replace the owner. Queue dispatch carries the queued turn's exact target into typing before `agent_start`, rather than reducing it to a chat ID. In Threaded Mode, missing or invalid Thread targets are refused before starting a loop or invoking the API; mode changes also invalidate a captured Classic chat-only loop. A rejected start never replaces an existing valid Thread loop. The starter also requires the captured Pi context and session generation to remain current at start, queued send, tick and late failure boundaries, even if the transport epoch is unchanged; replacing a context object in place cannot revive old activity. Message activity hooks capture generation across awaited preview work, and agent-start bindings recheck it before post-await activity/typing, so an obsolete finally or continuation cannot acquire replacement ownership. The typing sender carries its captured loop/context/generation/transport assertion as process-local call authority: direct API issuance rechecks it after queued work, and the follower caller rechecks it after registration waits and immediately before socket issuance without putting callbacks in IPC data. Automatic typing uses one HTTP attempt with no transport-family fallback or delayed replay, including forwarded calls; a future keepalive is a fresh owner-checked attempt. An already issued HTTP/RPC cannot be retroactively unsent. Each loop refreshes its exact target every three seconds and keeps one action in flight. The leader API runtime coalesces identical chat/thread/action calls for two seconds, permits at most one concurrent chat action per chat, and shares a Telegram 429 `retry_after` fence across every Thread key in that chat; expired gates prune opportunistically and each gate family retains at most 256 active keys. Suppression never schedules a delayed retry. A failed typing action remains a structured diagnostic but cannot project `error` onto an otherwise healthy connected/leader/follower status. Assistant message start/update hooks still re-arm typing during Telegram-owned turns so transient provider/model errors do not leave a continuing run without activity feedback, and agent/session completion stops it.
|
|
685
685
|
|
|
686
686
|
### Rendering And Delivery
|
|
687
687
|
|
|
@@ -715,7 +715,7 @@ Assistant-authored final-message actions use hidden top-level comments, with an
|
|
|
715
715
|
- `telegram_voice` accepts one positional compact action cell or JSON object and creates one voice artifact through configured outbound handlers, programmatic voice handlers, or registered synthesis providers.
|
|
716
716
|
- `telegram_button` accepts a JSON object, adaptive JSON/CML matrix, or positional Compact Matrix Literal. Named JSON objects and positional cells may coexist, with commas optional only between completed matrix or row elements. Each top-level cell creates one full-width row, while a nested row groups buttons horizontally without an artificial parser-width cap; every callback enqueues its configured prompt text as a normal Telegram prompt turn. The JSON-first grammar, positional trim/escape rules, atomic rejection, and renderer-owned width policy are specified in [Adaptive Button Literal](./compact-matrix-literal.md).
|
|
717
717
|
|
|
718
|
-
Standalone column-zero triple-backtick `telegram_button` blocks reuse the same cell/matrix grammar and callback store, including adjacent
|
|
718
|
+
Standalone column-zero triple-backtick `telegram_button` blocks reuse the same cell/matrix grammar and callback store, including adjacent complete cells and matrices whose normalized rows concatenate in source order, and compile to native Rich Markdown button rows between paragraphs. Rendering validates each complete block before registering any callbacks, escapes label text, preserves disabled cells, and leaves larger enclosing fences literal. Previews hide complete and unfinished action fences. HTML mode projects those controls into the footer. In-body callbacks acknowledge without rewriting the Rich message; selected-style highlighting remains footer-only.
|
|
719
719
|
|
|
720
720
|
Action recognition remains restricted to top-level column-zero comments and exact-name button fences so nested examples cannot trigger voice, buttons, or callbacks. The Telegram surface independently strips every complete assistant-authored HTML comment from previews and final delivery regardless of Markdown position or comment owner; an unclosed comment is withheld through the remaining tail, and a comment-only result sends no text message. Pi's terminal transcript and model context remain unchanged.
|
|
721
721
|
|
|
@@ -150,11 +150,11 @@ For `telegram_button` hidden comments and fenced blocks:
|
|
|
150
150
|
- Positional `{label|prompt|selected_style|1}` is equivalent to JSON `{"label":"label","prompt":"prompt","selected_style":"selected_style","disabled":true}` with a valid selected style; `{|prompt||1}` is equivalent to `{"prompt":"prompt","disabled":true}`. Fourth-position `0` or `false` matches `disabled: false`; `true` is equivalent to `1`.
|
|
151
151
|
- `{label|||1}` matches JSON `{"label":"label","disabled":true}`; `{|||1}` matches `{"disabled":true}`. Disabled cells require no prompt and do not retain action or selected-style semantics.
|
|
152
152
|
- Disabled buttons serialize as `{ text, disabled: {} }`, without `callback_data`; they neither enqueue prompts nor invoke bound app methods. Enabled controls keep existing selection and callback behavior.
|
|
153
|
-
- Top-level cells become full-width rows.
|
|
153
|
+
- Top-level cells become full-width rows. Telegram button wrappers additionally accept adjacent complete JSON/CML cells and matrices, with optional commas between groups. Normalize each group independently and concatenate its rows in source order: `[{A|a}] [{B|b}]` and `{A|a} [{B|b}]` both equal `[{A|a}{B|b}]`. Horizontal rows remain nested inside a matrix: `[[{A|a}{B|b}]] [{C|c}]` equals `[[{A|a}{B|b}]{C|c}]`. This is a Telegram button wrapper extension, not a change to portable CML v4 or voice syntax.
|
|
154
154
|
- Nested rows become horizontal keyboard rows; horizontal intent still requires the explicit nested array.
|
|
155
155
|
- Invalid payloads are stripped with their recognized action comment and register no callbacks.
|
|
156
156
|
|
|
157
|
-
The wrapper selects placement without changing cell semantics. A hidden `telegram_button` HTML comment builds the footer keyboard. A standalone column-zero fenced block opened by exactly three backticks plus `telegram_button` renders rows between paragraphs in Native Rich Markdown. A singleton JSON/CML object needs no array in either wrapper. Fenced content must be one complete
|
|
157
|
+
The wrapper selects placement without changing cell semantics. A hidden `telegram_button` HTML comment builds the footer keyboard. A standalone column-zero fenced block opened by exactly three backticks plus `telegram_button` renders rows between paragraphs in Native Rich Markdown. A singleton JSON/CML object needs no array in either wrapper. Fenced content must be one complete group sequence without trailing envelope text; any invalid group rejects the entire block before callback registration. Larger outer fences and ordinary code blocks remain literal examples; unclosed action fences are withheld. Native Rich rows support at most eight buttons and must fit one message chunk; these are renderer constraints, not grammar limits. HTML compatibility places fenced controls in the footer. See [Outbound](./outbound.md) for delivery and callback behavior.
|
|
158
158
|
|
|
159
159
|
````markdown
|
|
160
160
|
Description.
|
|
@@ -182,7 +182,7 @@ Rules:
|
|
|
182
182
|
- The opening marker must start at column zero on a top-level line outside fenced code, quotes, lists, and indented examples; otherwise it does not activate a button action and is still removed from the Telegram surface.
|
|
183
183
|
- Prefer one matrix comment for multiple buttons. Each top-level JSON object or CML cell becomes one full-width inline-keyboard row in source order; a nested row groups one or more buttons horizontally. The parser imposes no artificial per-row width cap; empty rows, malformed cells, unknown/trailing CML escapes, a fourth unescaped CML separator, empty enabled-cell prompts or three-atom styles, invalid disabled flags, empty one-atom cells, unknown selected styles, and deeper nesting are rejected atomically. Multi-atom cells may omit the label; four-atom cells may additionally omit the selected style, and disabled cells may omit the prompt. Generated surfaces default to five columns and expand to six through eight only for short position-bearing labels. Repeated singular comments remain valid.
|
|
184
184
|
- Button actions are stored in memory with short `callback_data`; Telegram never sees the full prompt in the button payload.
|
|
185
|
-
- Standalone column-zero fences opened by exactly three backticks and `telegram_button` use the same singleton/matrix JSON/CML grammar.
|
|
185
|
+
- Standalone column-zero fences opened by exactly three backticks and `telegram_button` use the same singleton/matrix JSON/CML grammar. Button wrappers accept adjacent complete JSON/CML cells and matrices with optional commas between groups. Their normalized rows concatenate in source order: `[{A|a}] [{B|b}]` equals `[{A|a}{B|b}]`; a singleton cell is one vertical row and horizontal peers still require a row nested inside a matrix. Any invalid group in a fence rejects the entire block before callback registration. Complete valid blocks become native `<tg-button-row>` elements in `InputRichMessage.markdown`, at their original positions. Each row supports at most eight buttons and must fit a Rich Message chunk; validation precedes callback registration for the entire block. Labels are escaped, prompts never enter the native markup, and disabled cells have no callback. Missing closures, trailing payload garbage, and invalid blocks produce no controls. Other language names and enclosing code examples stay literal. Draft previews withhold complete and unfinished action fences without registering actions. HTML compatibility projects fenced rows into the bottom keyboard before comment-authored footer rows; source ordering within each group is retained.
|
|
186
186
|
- In-body controls share prompt/app dispatch, TTL, generation/revision checks, and sent-message target ownership with footer controls. Their acknowledgement is the selection feedback; `selected_style` does not recolor the Rich body. No callback-triggered body rewrite is introduced, preventing a stale click from overwriting newer app output. The operator confirmed the post-reload current-client smoke: singleton CML and mixed JSON/CML in-body rows, their ordinary prompt callbacks, a labeled disabled control, and the separate footer callback work as intended. Client type was not recorded. The operator also confirmed that HTML mode projects the in-body groups into the bottom keyboard as intended. Second-client coverage, follower routing, blank cells, and app-method dispatch/revision rejection remain live-gated. Bot API evidence: `InputRichBlockButtons` / Rich Markdown `tg-button-row` and `RichMessageButton` use ordinary `callback_data` and support disabled controls.
|
|
187
187
|
- After Telegram accepts a generated footer button callback as a queued prompt, the bridge changes that exact button to its configured selection style without changing agent-authored text or emoji. Set `selected_style` to `primary` (blue), `success` (green), or `danger` (red); omitted or invalid values fall back to `primary`. The style never suppresses queue admission. Other choices stay visually unchanged and remain available; the callback acknowledgement remains the fallback on clients that do not render button styles.
|
|
188
188
|
- When generated button markup is the entire assistant reply, the bridge supplies the standard `☑️ **Choose an option:**` heading as visible message text so Telegram has a message to which it can attach the inline keyboard.
|
|
@@ -197,6 +197,7 @@ The extension injects prompt guidance by context:
|
|
|
197
197
|
|
|
198
198
|
- If no bot token is configured, no Telegram bridge suffix is injected.
|
|
199
199
|
- For ordinary local/TUI prompts, the compact routing note points to bundled Skills, including portable `show-me`, while forbidding Telegram use unless explicitly requested.
|
|
200
|
+
- Pi 1.2 custom-message starts may omit the user `prompt`. They retain connected bridge guidance without acquiring Telegram-user origin from their message content; explicit custom origin also overrides any stale prompt field. Legacy user events without `messageType` retain prefix-based Telegram detection. Unavailable transport still strips Telegram tool guidance.
|
|
200
201
|
- For Telegram-originated turns, the compact note routes the agent to `telegram-bridge` for voice/button/direct-delivery/Threaded Mode/formatting/debug guidance and to `show-me` when the user needs a visual explanation of work or behavior.
|
|
201
202
|
- For ordinary Telegram-originated answers, write the full technical answer as phone-width Markdown. `show-me` may instead deliver a focused self-contained HTML artifact when explicitly requested or when spatial density earns it, while keeping the immediate reply useful.
|
|
202
203
|
- Add `telegram_voice` with positional CML by default or JSON when multiline content, named fields, or escaping requires it. A companion summary is optional, no specific summary format is required.
|
|
@@ -75,7 +75,7 @@ Every assistant-authored HTML comment is transport-private on Telegram: previews
|
|
|
75
75
|
- `telegram_message(text, chat_id?, media?, channel?, thread_id?, thread?)` sends a direct Telegram Markdown message when this Pi instance owns `/telegram-connect` or is registered with the multi-instance bus. A public `@username`, or an exact negative numeric channel ID with `channel: true`, is passed as `chat_id` without a local registry; channel delivery requires the direct leader, and Telegram enforces whether the bot has channel posting permission. Channel delivery accepts `media` as one local `.jpg`/`.jpeg`/`.png`/`.webp` photo or `.mp4` video (photo ≤ 10 MiB, video ≤ 50 MiB), uploaded through multipart `sendPhoto`/`sendVideo` with `text` as its HTML caption (≤ 1024 visible characters); unsupported media types and albums are rejected before issuance, and the durable channel-post journal binds media identity and caption so duplicate or lost-acknowledgement retries never re-upload. `thread` accepts a live numeric Thread id or its current acknowledged display title; name matching is case-insensitive and fails closed when absent or ambiguous, while delivery captures the numeric target. During an active Telegram turn, omitted targeting and an explicit target equal to that turn are rejected so the ordinary final-reply path remains the sole current-target response; an explicit different chat/thread target remains allowed. Outside active turns, paired/default local/TUI delivery remains unchanged. Top-level `telegram_button` comments inside `text` are parsed with the same planner used for normal replies and attached to that message; buttons are never standalone Telegram messages.
|
|
76
76
|
- The bundled `telegram-bridge` Skill owns action syntax, target routing, Threaded Mode, formatting, Generative App operation, and profile-specific debugging guidance. The bundled `show-me` Skill owns portable evidence-honest explanations and adapts them to phone-width Markdown or self-contained HTML artifacts when Telegram is the active surface. The regular prompt routes applicable turns to these and the other bundled Skills. Compiled npm/git installations expose them through the package `pi.skills` manifest so Pi resource filters remain authoritative; a raw TypeScript checkout under Pi's `extensions` directory contributes its source Skill root at runtime instead. `telegram_attach`, `telegram_bind`, and `telegram_message` remain registered but are model-active only while this instance owns direct transport or holds a live follower registration; disconnect/loss suppresses their schemas and prompt metadata, and recovery restores only the operator's previously active pi-telegram subset.
|
|
77
77
|
- `telegram_voice` hidden comments request Telegram-native voice delivery through `{text}`, `{text|lang}`, `{text|lang|rate}`, or a JSON object. JSON is the fallback for multiline content, named fields, or escaping; equivalent `text` or `value` supplies the spoken payload, with explicit `text` taking precedence.
|
|
78
|
-
- `telegram_button` hidden comments create footer buttons; standalone column-zero triple-backtick `telegram_button` blocks create button rows between paragraphs in Native Rich Markdown. Both accept the same singleton or mixed JSON/CML matrix and share prompt/app routing;
|
|
78
|
+
- `telegram_button` hidden comments create footer buttons; standalone column-zero triple-backtick `telegram_button` blocks create button rows between paragraphs in Native Rich Markdown. Both accept the same singleton or mixed JSON/CML matrix and share prompt/app routing; both wrappers also accept adjacent complete JSON/CML cells and matrices with optional separators, concatenating their normalized rows in source order. Cells remain singleton rows and nested horizontal rows retain their grouping. Native rows allow at most eight buttons and must fit one Rich Message chunk; invalid or incomplete blocks register nothing. Drafts hide action fences. HTML compatibility projects fenced controls into the footer. In-body clicks acknowledge without recoloring the Rich body; selected-style highlighting remains footer-only. One marker accepts a JSON object, adaptive JSON/CML matrix, or positional [Compact Matrix Literal](./compact-matrix-literal.md). Named JSON objects and positional cells may coexist in one matrix or row; separators are optional and one trailing comma is tolerated at matrix, row, and JSON-object boundaries. Top-level cells become full-width rows, while nested rows group one or more buttons horizontally without an artificial parser-width cap. CML uses `{value}`, `{label|prompt}`, prompt-only `{|prompt}`, or the corresponding three-atom form with `selected_style`; an omitted label uses the existing prompt-as-label fallback, and the optional third atom requires a non-empty prompt and accepts only `primary`, `success`, or `danger`. A fourth atom accepts `1` or `true` (disabled), and `0` or `false` (enabled), with exact lowercase spelling; an omitted fourth position stays enabled, and the third atom may be empty in this form (`{|Next||1}`). JSON uses boolean `disabled`. Disabled cells need no prompt or selected style: `{Next|||1}` is label-only and `{|||1}` is blank (JSON `{"label":"Next","disabled":true}` and `{"disabled":true}`). The Telegram renderer supplies a non-breaking space only when the label is empty. Disabled cells stay visible but carry `disabled: {}` instead of callback data and register no prompt or bound action; invalid disabled values reject the candidate matrix. It trims atom boundaries and supports only the minimal escapes `\|`, `\}`, and `\\`. Prefer one matrix comment for multiple buttons. Use JSON `label` plus `prompt`, or `value` when both strings are identical. Action markers are colon-free; colon-prefixed payloads are rejected. Use top-level column-zero action wrappers, outside quotes, lists, or enclosing code examples. Ordinary code fences and larger outer fences preserve literal examples; bare JSON/CML in prose never activates.
|
|
79
79
|
|
|
80
80
|
Prompt guidance is context-aware: local/TUI prompts see only explicit direct-delivery guidance, while Telegram-originated turns receive the full action-comment syntax and phone-width output contract.
|
|
81
81
|
|
|
@@ -1538,7 +1538,7 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
1538
1538
|
promptDispatchRuntime.startTypingLoop(
|
|
1539
1539
|
ctx,
|
|
1540
1540
|
turn?.chatId ?? target?.chatId,
|
|
1541
|
-
{ target },
|
|
1541
|
+
{ target, ensure: true },
|
|
1542
1542
|
) !== false
|
|
1543
1543
|
);
|
|
1544
1544
|
};
|
|
@@ -1547,6 +1547,7 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
1547
1547
|
const turn = activeTurnRuntime.get();
|
|
1548
1548
|
promptDispatchRuntime.startTypingLoop(ctx, turn?.chatId, {
|
|
1549
1549
|
target: turn?.target,
|
|
1550
|
+
ensure: true,
|
|
1550
1551
|
});
|
|
1551
1552
|
};
|
|
1552
1553
|
let observedAutomaticCompaction = false;
|
|
@@ -364,7 +364,7 @@ function parseTelegramVoiceCompactActionPayload(
|
|
|
364
364
|
function parseTelegramAdaptiveActionPayloadRows(
|
|
365
365
|
source: string,
|
|
366
366
|
parseCompactPayload: TelegramCompactActionPayloadParser,
|
|
367
|
-
options: { allowTrailing?: boolean; maxCompactAtoms?: number } = {},
|
|
367
|
+
options: { allowTrailing?: boolean; maxCompactAtoms?: number; allowMatrixGroups?: boolean } = {},
|
|
368
368
|
): { rows: Record<string, unknown>[][]; end: number } | undefined {
|
|
369
369
|
let offset = 0;
|
|
370
370
|
const isStructuralWhitespace = (character: string | undefined): boolean =>
|
|
@@ -512,21 +512,29 @@ function parseTelegramAdaptiveActionPayloadRows(
|
|
|
512
512
|
return undefined;
|
|
513
513
|
};
|
|
514
514
|
|
|
515
|
-
const
|
|
515
|
+
const parseTopLevelRows = (): Record<string, unknown>[][] | undefined => {
|
|
516
516
|
const rows: Record<string, unknown>[][] = [];
|
|
517
517
|
while (offset < source.length) {
|
|
518
518
|
skipWhitespace();
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
519
|
+
const character = source[offset];
|
|
520
|
+
if (character === "{") {
|
|
521
|
+
const cell = parseCell();
|
|
522
|
+
if (!cell) return undefined;
|
|
523
|
+
rows.push([cell]);
|
|
524
|
+
} else if (character === "[" && options.allowMatrixGroups) {
|
|
525
|
+
const matrix = parseMatrix();
|
|
526
|
+
if (!matrix) return undefined;
|
|
527
|
+
rows.push(...matrix);
|
|
528
|
+
} else {
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
523
531
|
if (!consumeOptionalSeparator()) return undefined;
|
|
524
532
|
}
|
|
525
533
|
return rows.length > 0 ? rows : undefined;
|
|
526
534
|
};
|
|
527
535
|
|
|
528
536
|
skipWhitespace();
|
|
529
|
-
const rows =
|
|
537
|
+
const rows = parseTopLevelRows();
|
|
530
538
|
if (!rows) return undefined;
|
|
531
539
|
skipWhitespace();
|
|
532
540
|
if (!options.allowTrailing && offset !== source.length) return undefined;
|
|
@@ -581,7 +589,7 @@ export function parseTelegramButtonPayloadRows(
|
|
|
581
589
|
return parseTelegramAdaptiveActionPayloadRows(
|
|
582
590
|
source.trim(),
|
|
583
591
|
parseTelegramButtonCompactActionPayload,
|
|
584
|
-
{ maxCompactAtoms: 4 },
|
|
592
|
+
{ maxCompactAtoms: 4, allowMatrixGroups: true },
|
|
585
593
|
)?.rows;
|
|
586
594
|
}
|
|
587
595
|
|
|
@@ -596,7 +604,7 @@ export function parseTelegramActionPayloadRows(
|
|
|
596
604
|
parseTelegramAdaptiveActionPayloadRows(
|
|
597
605
|
content.slice(offset),
|
|
598
606
|
parseTelegramButtonCompactActionPayload,
|
|
599
|
-
{ allowTrailing: true, maxCompactAtoms: 4 },
|
|
607
|
+
{ allowTrailing: true, maxCompactAtoms: 4, allowMatrixGroups: true },
|
|
600
608
|
)?.rows,
|
|
601
609
|
);
|
|
602
610
|
if (!scanned) return undefined;
|
|
@@ -150,10 +150,13 @@ export function createTelegramModelContextAvailabilityRuntime(deps: {
|
|
|
150
150
|
|
|
151
151
|
export type TelegramSystemPrompt = string | string[];
|
|
152
152
|
|
|
153
|
+
// Pi 1.0 user events lack messageType; Pi 1.2 custom starts carry no user prompt.
|
|
153
154
|
type TelegramBeforeAgentStartEvent = Omit<
|
|
154
155
|
BeforeAgentStartEvent,
|
|
155
|
-
"systemPrompt"
|
|
156
|
+
"systemPrompt" | "prompt" | "messageType"
|
|
156
157
|
> & {
|
|
158
|
+
messageType?: "user" | "custom";
|
|
159
|
+
prompt?: string;
|
|
157
160
|
systemPrompt?: TelegramSystemPrompt | null;
|
|
158
161
|
};
|
|
159
162
|
|
|
@@ -166,7 +169,7 @@ type TelegramBeforeAgentStartHook = (
|
|
|
166
169
|
) => TelegramBeforeAgentStartResult;
|
|
167
170
|
|
|
168
171
|
export function buildTelegramBridgeSystemPrompt(options: {
|
|
169
|
-
prompt
|
|
172
|
+
prompt?: string;
|
|
170
173
|
systemPrompt?: TelegramSystemPrompt | null;
|
|
171
174
|
telegramPrefix?: string;
|
|
172
175
|
localSystemPromptSuffix: string;
|
|
@@ -177,7 +180,7 @@ export function buildTelegramBridgeSystemPrompt(options: {
|
|
|
177
180
|
const telegramHead = telegramPrefix.endsWith("]")
|
|
178
181
|
? telegramPrefix.slice(0, -1)
|
|
179
182
|
: telegramPrefix;
|
|
180
|
-
const trimmedPrompt = options.prompt
|
|
183
|
+
const trimmedPrompt = options.prompt?.trimStart() ?? "";
|
|
181
184
|
const telegramTurn =
|
|
182
185
|
trimmedPrompt.startsWith(`${telegramHead}]`) ||
|
|
183
186
|
trimmedPrompt.startsWith(`${telegramHead}|`);
|
|
@@ -200,7 +203,7 @@ export function createTelegramBeforeAgentStartHook(
|
|
|
200
203
|
): TelegramBeforeAgentStartHook {
|
|
201
204
|
return (event) =>
|
|
202
205
|
buildTelegramBridgeSystemPrompt({
|
|
203
|
-
prompt: event.prompt,
|
|
206
|
+
prompt: event.messageType === "custom" ? undefined : event.prompt,
|
|
204
207
|
systemPrompt: event.systemPrompt,
|
|
205
208
|
telegramPrefix: options.telegramPrefix,
|
|
206
209
|
localSystemPromptSuffix:
|
|
@@ -364,8 +364,16 @@ export function createTelegramTypingLoopStarter<TContext>(
|
|
|
364
364
|
): (
|
|
365
365
|
ctx: TContext,
|
|
366
366
|
chatId?: number,
|
|
367
|
-
options?: { target?: TelegramTypingLoopTarget },
|
|
367
|
+
options?: { target?: TelegramTypingLoopTarget; ensure?: boolean },
|
|
368
368
|
) => boolean {
|
|
369
|
+
let currentOwner: {
|
|
370
|
+
ctx: TContext;
|
|
371
|
+
sessionGeneration: number | undefined;
|
|
372
|
+
transportAuthority: string | number | undefined;
|
|
373
|
+
chatId: number;
|
|
374
|
+
threadId: number | undefined;
|
|
375
|
+
canContinue: () => boolean;
|
|
376
|
+
} | undefined;
|
|
369
377
|
return (ctx, chatId, options) => {
|
|
370
378
|
const sessionGeneration = deps.getSessionGeneration?.();
|
|
371
379
|
const transportAuthority = deps.getTransportAuthority?.();
|
|
@@ -382,9 +390,22 @@ export function createTelegramTypingLoopStarter<TContext>(
|
|
|
382
390
|
(target?.threadId !== undefined && Number.isSafeInteger(target.threadId) && target.threadId > 0));
|
|
383
391
|
// Queue dispatch precedes agent_start: only an explicit destination permits Threaded presence, never chat fallback.
|
|
384
392
|
if (!canContinue()) return false;
|
|
393
|
+
const resolvedChatId = chatId ?? deps.getDefaultChatId();
|
|
394
|
+
if (resolvedChatId === undefined || resolvedChatId === 0) return false;
|
|
395
|
+
// Streaming updates ensure presence without revoking the same owner's pending HTTP/IPC action.
|
|
396
|
+
// Explicit starts still replace the loop, including a new turn on the same session and Thread.
|
|
397
|
+
if (options?.ensure && currentOwner?.canContinue() &&
|
|
398
|
+
(deps.getSessionGeneration
|
|
399
|
+
? Object.is(currentOwner.sessionGeneration, sessionGeneration)
|
|
400
|
+
: Object.is(currentOwner.ctx, ctx)) &&
|
|
401
|
+
Object.is(currentOwner.transportAuthority, transportAuthority) &&
|
|
402
|
+
currentOwner.chatId === resolvedChatId && currentOwner.threadId === target?.threadId) return false;
|
|
403
|
+
const owner = { ctx, sessionGeneration, transportAuthority, chatId: resolvedChatId,
|
|
404
|
+
threadId: target?.threadId, canContinue };
|
|
405
|
+
currentOwner = owner;
|
|
385
406
|
let active = true;
|
|
386
407
|
return deps.typing.start({
|
|
387
|
-
chatId:
|
|
408
|
+
chatId: resolvedChatId,
|
|
388
409
|
target,
|
|
389
410
|
intervalMs: deps.intervalMs ?? TELEGRAM_TYPING_ACTION_INTERVAL_MS,
|
|
390
411
|
sendTypingAction: async (targetChatId, actionOptions) => {
|
|
@@ -419,6 +440,7 @@ export function createTelegramTypingLoopStarter<TContext>(
|
|
|
419
440
|
shouldContinue: canContinue,
|
|
420
441
|
onStopped: () => {
|
|
421
442
|
active = false;
|
|
443
|
+
if (currentOwner === owner) currentOwner = undefined;
|
|
422
444
|
},
|
|
423
445
|
});
|
|
424
446
|
};
|
|
@@ -722,7 +744,7 @@ export interface TelegramPromptDispatchRuntime<TContext> {
|
|
|
722
744
|
startTypingLoop: (
|
|
723
745
|
ctx: TContext,
|
|
724
746
|
chatId?: number,
|
|
725
|
-
options?: { target?: TelegramTypingLoopTarget },
|
|
747
|
+
options?: { target?: TelegramTypingLoopTarget; ensure?: boolean },
|
|
726
748
|
) => boolean | void;
|
|
727
749
|
onPromptDispatchStart: (ctx: TContext, chatId?: number, target?: TelegramTypingLoopTarget) => void;
|
|
728
750
|
onPromptDispatchFailure: (ctx: TContext, message: string) => void;
|
|
@@ -60,7 +60,7 @@ Every enabled button has a self-contained prompt and an optional selection style
|
|
|
60
60
|
- `{label|prompt}` separates visible label from queued prompt.
|
|
61
61
|
- `{label|prompt|selected_style}` and `{|prompt|selected_style}` accept `primary`, `success`, or `danger`.
|
|
62
62
|
- Fourth-position `1`/`true` disables, `0`/`false` enables; omission means enabled. JSON uses boolean `disabled`. `{|Next||1}` omits label/style; `{Next|||1}` omits prompt/style; `{|||1}` is blank (Telegram receives a non-breaking space). Prefer meaningful labels and retain a useful enabled action. Enabled CML requires a prompt. Disabled controls stay visible but have no callback, queued prompt, or bound-method invocation.
|
|
63
|
-
-
|
|
63
|
+
- Both wrappers concatenate rows from adjacent JSON/CML cells or matrices, with optional commas. Cells are vertical; horizontal peers require a nested row: `[[{A|a}{B|b}]]`.
|
|
64
64
|
- Prefer one matrix per related group. Fenced blocks stay in place in Rich mode; HTML compatibility moves them to the footer. Native rows allow eight buttons. Malformed/oversized/unclosed blocks activate nothing; drafts hide them. Outer code fences and quoted/indented examples remain literal.
|
|
65
65
|
- Both placements share prompt/app routing. In-body clicks acknowledge without recoloring; selected-style highlighting remains footer-only.
|
|
66
66
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-kit",
|
|
3
|
-
"version": "0.34.
|
|
3
|
+
"version": "0.34.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -42,12 +42,12 @@
|
|
|
42
42
|
],
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@llblab/pi-actors": "0.54.0",
|
|
45
|
-
"@llblab/pi-claude-usage": "0.2.
|
|
45
|
+
"@llblab/pi-claude-usage": "0.2.2",
|
|
46
46
|
"@llblab/pi-clean-room": "0.3.0",
|
|
47
|
-
"@llblab/pi-codex-usage": "0.13.
|
|
47
|
+
"@llblab/pi-codex-usage": "0.13.1",
|
|
48
48
|
"@llblab/pi-grow-loop": "0.9.0",
|
|
49
49
|
"@llblab/pi-state-flow": "0.26.4",
|
|
50
|
-
"@llblab/pi-telegram": "0.56.
|
|
50
|
+
"@llblab/pi-telegram": "0.56.4",
|
|
51
51
|
"@llblab/skills": "1.15.0"
|
|
52
52
|
},
|
|
53
53
|
"bundledDependencies": [
|