@llblab/pi-kit 0.36.2 → 0.37.0
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 +10 -0
- package/README.md +2 -2
- package/node_modules/@llblab/pi-telegram/AGENTS.md +3 -3
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +27 -1
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +31 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +2 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +2 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +16 -14
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +8 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +6 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +12 -10
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +58 -39
- package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +21 -12
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/menu.d.ts +4 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +3 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +4 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +10 -9
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +1 -15
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +2 -15
- package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +2 -2
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +16 -2
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +3 -1
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +4 -0
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +18 -17
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +11 -4
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +8 -0
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +87 -51
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +22 -13
- package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/menu.ts +4 -4
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +5 -2
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +11 -7
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +2 -26
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/scripts/run-macos-tests.mjs +43 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@llblab/pi-kit` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.37.0: Bundled Package Updates (2026-10-25)
|
|
6
|
+
|
|
7
|
+
- `Exact pins`: `@llblab/pi-telegram` 0.57.11 → 0.58.1.
|
|
8
|
+
- `Packaging`: Synchronizes the lockfile, bundle aliases and README inventory. Package membership and resource order remain unchanged; packed validation does not certify extension safety or installed-client behavior.
|
|
9
|
+
|
|
10
|
+
## 0.36.3: Bundled Package Updates (2026-10-24)
|
|
11
|
+
|
|
12
|
+
- `Exact pins`: `@llblab/pi-telegram` 0.57.7 → 0.57.11.
|
|
13
|
+
- `Packaging`: Synchronizes the lockfile, bundle aliases and README inventory. Package membership and resource order remain unchanged; packed validation does not certify extension safety or installed-client behavior.
|
|
14
|
+
|
|
5
15
|
## 0.36.2: Bundled Package Updates (2026-10-23)
|
|
6
16
|
|
|
7
17
|
- `Exact pins`: `@llblab/pi-claude-usage` 0.2.2 → 0.2.3; `@llblab/pi-codex-usage` 0.13.1 → 0.13.2; `@llblab/pi-telegram` 0.57.3 → 0.57.7.
|
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ Package links lead to the owning repositories for usage, documentation, issues,
|
|
|
18
18
|
| [`@llblab/pi-codex-usage`](https://github.com/llblab/pi-codex-usage) | `0.13.2` | 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.5` | 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.
|
|
21
|
+
| [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.58.1` | 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.
|
|
@@ -27,7 +27,7 @@ Versions are exact by design. An upstream release does not change an installed k
|
|
|
27
27
|
|
|
28
28
|
Requires **Pi 1.0.0+** and **Node.js 22.19.0+**. State Flow requires its canonical checkpoint/tail storage format and does not convert unsupported stores in place. Preserve existing stores and consult the [owning package's storage guidance](https://github.com/llblab/pi-state-flow/blob/v0.26.5/docs/usage.md#moving-a-store-and-supported-formats) before changing installations.
|
|
29
29
|
|
|
30
|
-
From
|
|
30
|
+
From NPM:
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
33
|
pi install npm:@llblab/pi-kit
|
|
@@ -109,7 +109,7 @@ Use the relevant local skill before non-trivial work in its domain. Keep skill o
|
|
|
109
109
|
- In Threaded Mode, All is service/input-only. New output, previews, typing and routing UI require a concrete Thread; neither implicit fallback nor explicit aggregate delivery may write to All. Unavailable routing authority retains input without an All notice or silent settlement. Classic DM and explicit channel/business destinations remain separate; historical All output is not auto-cleaned.
|
|
110
110
|
- Complete assistant/guest model answers use Telegram-native Rich Markdown. Harness-owned menus, status, diagnostics, thinking, and tool evidence remain explicit HTML/plain or their documented native surface. Before Telegram preview or final delivery, strip every assistant-authored HTML comment regardless of Markdown position while keeping action activation top-level-only; a comment-only result sends no text message. Preserve literal code outside comments and structurally safe chunking; never split invalid markup.
|
|
111
111
|
- `preview` owns streaming lifecycle only, not assistant rendering. Finalization waits for the captured generation's preceding preview work, never later drafts blocked by that publication, and must not issue pre/post-final draft-clear calls that create transient Telegram draft UI. Reset/invalidation detaches predecessor dependencies; late settlements cannot mutate replacement state. Turns that already answer as one atomic reply (voice replies, Guest Mode queries) never stream previews.
|
|
112
|
-
- Native `sendChatAction(typing)` is the automatic activity signal for unsettled agent and compaction work while Telegram transport is authorized. Extension-owned blocking UI prompts pause it and completion resumes it while either work owner remains active. Compaction may stop only a typing loop it actually started; it must preserve a pre-existing agent-owned loop. Refresh only the assigned Thread on a conservative three-second cadence; do not mirror typing to aggregate `All` and multiply shared-chat flood pressure. Typing is best-effort presence only, never a Thread-existence or message-delivery probe: a failed exact-target action remains a structured diagnostic and must not replace a healthy connected/leader/follower status with `error`. Do not invent extra in-chat work indicators or emit activity for startup/connect/reload/recovery alone.
|
|
112
|
+
- Native `sendChatAction(typing)` is the automatic activity signal for unsettled agent and compaction work while Telegram transport is authorized. Extension-owned blocking UI prompts pause it and completion resumes it while either work owner remains active. Compaction may stop only a typing loop it actually started; it must preserve a pre-existing agent-owned loop. Refresh only the assigned Thread on a conservative three-second cadence; do not mirror typing to aggregate `All` and multiply shared-chat flood pressure. One Thread's pending action must not suppress another Thread's activity. Typing is best-effort presence only, never a Thread-existence or message-delivery probe: a failed exact-target action remains a structured diagnostic and must not replace a healthy connected/leader/follower status with `error`. Do not invent extra in-chat work indicators or emit activity for startup/connect/reload/recovery alone.
|
|
113
113
|
- Public activity handlers and connected companion delivery are asynchronous, target-bound, generation-fenced surfaces. Connected companion projection has no independent opt-out: disconnect or authority loss is its boundary. Token deltas, hidden reasoning, unknown sources, and stale authority never enter public projection.
|
|
114
114
|
- Profiles hold only bot identity and the paired owner; Settings-menu preferences live in shared top-level config. Thread display (`threads.displayMode`) defaults to the Letters strategy, with Names, Directory Snake, and Directory Title as the other automatic choices; Names projects the generated dictionary name for the slot. Unsupported retained display keys resolve to Letters without rewriting persisted configuration. A durable manual Thread display name retained on its Workspace binding overrides any automatic projection until exact reset; keep generated/recovery identity separate from manual and acknowledged display fields. After leader startup, automatic display contraction waits one follower-staleness window so election and follower re-registration cannot briefly remove and restore an acknowledged same-cwd suffix; stop/start generation cancels stale reconciliation. UI labels, emoji semantics, navigation, settings controls, callback namespaces, voice behavior, command templates, and assistant markup follow the linked `/docs` contracts. Generated human-readable action labels use `emoji + space + text`; emoji-free action text is only a reasoned no-semantic-marker fallback. State/value controls follow the [button control hierarchy](./docs/ui-style.md#button-control-hierarchy), not the action-label grammar. Non-spatial generated controls default to top-level vertical cells, with nested rows reserved for unmistakably compact peers. Do not restate other evolving UI details here.
|
|
115
115
|
|
|
@@ -134,7 +134,7 @@ The detailed map is canonical in [`docs/architecture.md`](./docs/architecture.md
|
|
|
134
134
|
- Command templates remain compact and shell-free. Use string leaves or ordered `template` arrays; shell operators are not an execution contract. Examples use portable executable placeholders, never machine-local paths.
|
|
135
135
|
- `telegram_attach` is the canonical file path and `telegram_message` the direct Markdown text/buttons path. Both require current direct or registered-follower authority and must not replace the normal active-turn reply.
|
|
136
136
|
- Inbound handlers transform text/media before queueing; outbound handlers precede programmatic/provider fallbacks. Public contracts and ordering live in `docs/inbound.md`, `docs/outbound.md`, and `docs/public-api.md`.
|
|
137
|
-
- Pi integration uses public hooks and APIs. Telegram `/new` is scheduled against the exact durable update, dispatched only after that update is removed from the journal, and then routed through the
|
|
137
|
+
- Pi integration uses public hooks and APIs. Telegram `/new` is scheduled against the exact durable update, dispatched only after that update is removed from the journal, and then routed through the `/telegram-connect --internal new <token>` Pi gateway via `pi.sendUserMessage(..., { expandPromptTemplates: true })`; the internal branch never enters profile activation or connection/recovery, only a runtime-armed one-use typed action may execute, manual invocation reports that the action cannot be run manually, and the handler receives the real `ExtensionCommandContext` before calling `ctx.newSession()`. Before replacement, CAS-publish one exact expiring handoff in the profile target snapshot. `workspace-thread` successors re-key the matching Workspace binding; `classic-chat` successors preserve Profile/CWD/session/chat continuity without creating a binding or invoking topic APIs. Both atomically claim the intent before one terminal result, and the old `withSession` path never publishes the same success. A registered follower cannot persist leader-owned state: its Workspace Thread intent is published and claimed by the leader over capability-gated exact-generation bus RPC after leader-side binding validation, and records its source runtime instance so only that follower lineage may re-key or claim it. Never replace the session while inbound authority is unsettled, store stale command contexts, accept an expired or mismatched handoff, inject terminal input, spawn a shadow Pi process, or mutate session files.
|
|
138
138
|
|
|
139
139
|
## 7. Engineering Conventions
|
|
140
140
|
|
|
@@ -145,7 +145,7 @@ The detailed map is canonical in [`docs/architecture.md`](./docs/architecture.md
|
|
|
145
145
|
- Treat every meaningful `lib/extension.ts` edit as a composition-pressure check, but keep one-off live adapter wiring there when extraction would only hide cross-domain state.
|
|
146
146
|
- Follow [`docs/ui-style.md`](./docs/ui-style.md) for interface copy, emoji, buttons, menus, and dialogs. Update the registry before assigning a new UI emoji meaning. Standalone notices use one fully bold emoji-led sentence with a terminal period; menu or chooser headings use the same hierarchy with a terminal colon. Material names may add nested italic emphasis without breaking the outer bold span. Callback toasts are fleeting plain text with no emoji and no terminal sentence period (question/exclamation marks and ellipses remain), written in that final form at their call site; no delivery boundary rewrites punctuation, and navigation taps answer silently. In-chat notices and explicit raw API payloads keep their punctuation.
|
|
147
147
|
- Markdown lists never contain blank lines between adjacent items; list items are not paragraphs. Use blank lines only between paragraphs or independently separated blocks. Markdown tables use compact source formatting with `---` separator cells and one surrounding space per cell. Preserve vendored references unchanged.
|
|
148
|
-
- Treat Windows filesystem, named-pipe, lock, heartbeat, and atomic-rename reports as high-signal evidence; reduce them to regressions or explicit platform caveats.
|
|
148
|
+
- Treat Windows filesystem, named-pipe, lock, heartbeat, and atomic-rename reports as high-signal evidence; reduce them to regressions or explicit platform caveats. Process fixtures must distinguish bounded startup from post-readiness natural exit: arm the exit guard only after the declared checkpoint, retain a separate total cap, and ensure extra startup allowance cannot hide retained post-readiness handles.
|
|
149
149
|
- Route significant runtime failures through the redacted recent-event recorder. Keep the compact TUI status at generic `error`; details belong in diagnostics.
|
|
150
150
|
|
|
151
151
|
## 8. Work Protocol
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
_This file owns unresolved project work only. Completed behavior belongs in `CHANGELOG.md`; durable contracts belong in `AGENTS.md` and `/docs`._
|
|
4
4
|
|
|
5
|
+
## 0.58.1 publication checkpoint
|
|
6
|
+
|
|
7
|
+
- [ ] Publish the independent Thread chat-action fix through the protected dev-to-main path, requiring fresh Ubuntu, Windows and both macOS shard checks at the exact candidate head and candidate/merged-tree equality. Keep the immutable Connect-only minor unchanged and verify its publication before issuing the hotfix tag.
|
|
8
|
+
- [ ] Verify the hotfix GitHub Release and public npm latest, exact tag/gitHead identity, canonical notes, packed runtime/skills and provenance before closing this checkpoint. No live session reload or Telegram probes; controlled transport tests do not prove live client presence.
|
|
9
|
+
|
|
5
10
|
## Disposable Thread live acceptance
|
|
6
11
|
|
|
7
12
|
- [ ] After separate operator authorization, use disposable Telegram tabs to check leader Settings review/Back, one confirmed cleanup of an inactive slotless binding, preservation of its replacement letter holder, and visible outcome/recovery copy. Native filesystem and injected-transport tests do not prove live Telegram/client or native Windows acceptance. Never use production history as the disposable fixture.
|
|
@@ -17,7 +22,28 @@ The prompt-cache report remains open for the reporter's offered live Anthropic c
|
|
|
17
22
|
|
|
18
23
|
Evidence (2026-10-23 07:11–07:16): a `/start` sent in a former leader tab moved into a follower's journal as a raw original. After that follower's Thread was re-created by the restore probe, every execution attempt failed with `Telegram bus API call is not allowed for this follower` and retried every 60 s indefinitely, because the reply targeted a Thread the follower no longer owned. Terminal settlement alone is unsafe: the same rejection can be a benign race during a live-rebind pre-dispatch.
|
|
19
24
|
|
|
20
|
-
-
|
|
25
|
+
### Post-refusal reply-only recovery — deferred contract gate
|
|
26
|
+
|
|
27
|
+
Remaining scope is recovery after a reply is refused or recipient authority changes during publication, not initial destination selection. The latter is specified under the [reply ownership boundary](./docs/multi-instance-bus.md#reply-recovery-ownership-boundary); its native regression covers a stale source address, not the historical refusal itself or live Telegram restoration.
|
|
28
|
+
|
|
29
|
+
A target-scoped reply refusal cannot prove that a command made no prior effects. Keep the handler-replay containment while preparing a replacement: unresolved inputs must remain in ordinary durable retry state, not be silently completed or retargeted as whole commands. Ordinary journal execution remains at-least-once, not globally exactly-once.
|
|
30
|
+
|
|
31
|
+
- [ ] Obtain explicit approval for a post-refusal recipient-delivery contract, then implement recipient-scoped reply-only recovery under the [reply ownership boundary](./docs/multi-instance-bus.md#reply-recovery-ownership-boundary), outside the containment hotfix. No ordinary recipient-scoped send grant is composed today: the local execution fence and queue-owner identity are insufficient, and receipt/admission namespaces cannot substitute for the registered recipient profile key. Ordinary sends return only a message ID, so the menu/delivery owner must participate in any destination change. Reuse selected-menu delivery only with its exact retained live-rebind/source grant; a raw journaled command cannot borrow it. Capture and recheck recipient/profile/session/registration authority at both ends, with no widening of the ordinary proxy.
|
|
32
|
+
- [ ] Prove the reply-only path with the real follower API caller and worker: one replacement send before any reply chunk is acknowledged, consistent callback/message ownership and destination across every chunk, no stale reply anchor, no recovery for deletion/editing/unknown or lost ACKs, and no second send after authority changes. A command effect before a refused reply must execute once within that admission attempt; original journal evidence stays unchanged and diagnostics cannot change a successful outcome. Run typecheck and the full suite at the stable boundary. If these proofs cannot be supplied, retain the input without handler re-entry.
|
|
33
|
+
- [ ] Live acceptance after the reply-only recovery checkpoint: a forwarded command whose follower Thread was re-created is answered in the current Thread (disposable tabs only).
|
|
34
|
+
|
|
35
|
+
### Remaining routing investigation
|
|
36
|
+
|
|
21
37
|
- [ ] Find why the temporary tab's selected `/start` stayed unresolved until expiry, and why that Restore moved custody without applying the rebind.
|
|
22
38
|
- [ ] Route an input that arrives in a tab bound to nothing through a fresh temporary routing tab (the All flow) instead of publishing the chooser in the orphan, deleting the orphan with it once nothing unspent remains. Needs its own durable-metadata design.
|
|
23
39
|
- [ ] Live acceptance on disposable tabs only: Cancel on an orphan tab deletes it; a second unspent input keeps it until the last Cancel; a plain `std::map` option button is delivered as an ordinary prompt.
|
|
40
|
+
|
|
41
|
+
## Passive-child cold-import follow-up
|
|
42
|
+
|
|
43
|
+
The 0.57.8 tag validation failed `Child process sharing the agent dir does not poll while parent owns Telegram lock` even when that file ran alone: `Child process did not exit. stdout= stderr=` at its existing child deadline. The other seven process tests and the identical pre-merge tree passed. That historical failure has no child-stage trace, so cold import, startup and a retained runtime handle remain competing explanations. Bounded diagnostic sampling is exhausted; passing traces showed imports up to 4.37 seconds against the five-second non-Windows child budget and natural exit within 8–14 ms after the event-loop checkpoint. These traces do not identify the original cause. A later main-validation recurrence stopped after `eval` and `import-start`, with parent-clock `elapsedMs=5009` and no import-complete checkpoint. That trace locates this recurrence before import completion, not in natural exit or a retained post-checkpoint handle; the slow dependency/startup cause remains unknown. The independent tag-owned Release validation passed the same committed tree, which does not erase the failed main check. The initial hotfix candidate then showed the same pre-import-completion state on Ubuntu at `elapsedMs=5026`; the recurrence is not macOS-only. Slow dependency/startup cost remains unexplained, separate from the fixture's corrected startup/exit observation contract.
|
|
44
|
+
- [ ] Validate the corrected phase-owned passive-child fixture in fresh complete platform CI before publication: bounded total startup, a strict post-readiness exit guard, unchanged no-poll/natural-exit assertions and no forced shutdown or retry. Controlled delayed-start/retained-work regressions and local native passes are not platform proof. For any genuine recurrence, preserve deadline and stage evidence before correcting runtime imports or budgets. Do not spend unchanged-head diagnostic samples or claim runner contention or the historical cause is proven.
|
|
45
|
+
|
|
46
|
+
## Windows Workspace-gate fixture timeout
|
|
47
|
+
|
|
48
|
+
`Leader assembly serializes follower provisioning through its shared Workspace gate` once exceeded its generic one-second bus-response deadline. It sends `follower.register` directly, outside the changed API caller; production registration has a separate budget. The later native standalone witness acknowledged at about 90 ms, and the full suite passed, but the original failure has no gate/persistence trace. A synthetic delayed-write probe verifies diagnostic capture, not the native cause.
|
|
49
|
+
- [ ] Use the next genuinely failing native trace to distinguish gate wait, persistence and acknowledgement delay before correcting this fixture or runtime. Retain its bounded stage evidence and standalone/full-suite execution; do not spend unchanged-head reruns or infer a cause from passing checks. Any deadline-policy correction must be justified against the registration owner's contract, not a guessed larger timeout.
|
|
@@ -4,6 +4,37 @@
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.58.1: Independent Thread Typing
|
|
8
|
+
|
|
9
|
+
- `Typing`: A pending activity request in one Thread no longer silently suppresses another Thread's typing. Exact chat/Thread/action calls still coalesce with their own minimum interval, while real Telegram 429 cooldowns remain shared across the chat, including late failures after a faster peer completes. Authority checks, one-attempt typing and bounded gate storage remain intact.
|
|
10
|
+
- `Process validation`: Passive-child checks distinguish cold startup from natural exit: a 15-second total cap retains the existing Windows bound, while a separate five-second guard starts after the event-loop checkpoint on every platform. Regressions reject both a missing checkpoint and retained post-checkpoint work; no polling assertions, natural-exit requirement or job budgets are weakened.
|
|
11
|
+
|
|
12
|
+
## 0.58.0: Unified Connect Gateway
|
|
13
|
+
|
|
14
|
+
- `Pi commands`: Removes the standalone `/telegram-internal` gateway. Runtime-armed session replacement now dispatches `/telegram-connect --internal new <token>` after exact durable update completion. Internal calls bypass profile activation and connection/recovery; malformed, unarmed and replayed calls fail closed. `internal` remains a valid profile name.
|
|
15
|
+
|
|
16
|
+
## 0.57.11: Follower Reply Target
|
|
17
|
+
|
|
18
|
+
- `Follower command replies`: Status/model/settings and text replies from an admitted follower command select its acknowledged Thread before publication, not the old input Thread. A changed Thread drops the reply anchor; delivery, menu state and ownership use one destination. The journaled original and explicit selected-menu grants stay intact; no refused-send recovery, handler replay or proxy widening is added.
|
|
19
|
+
|
|
20
|
+
## 0.57.10: Follower Request Identity
|
|
21
|
+
|
|
22
|
+
- `Follower request identity`: Follower API arguments use one JSON-wire snapshot before registration waits. Later caller mutation no longer changes the issued method/body or relabels a refused target and slow-call method. Ordinary and multipart calls retain wire JSON semantics and authority fences; refusal evidence still grants no resend or whole-command replay.
|
|
23
|
+
- `Windows CI diagnostics`: The Workspace-gate registration fixture records bounded gate and persistence stage timings, including them in failure context without paths or credentials. It also runs in the standalone Windows lifecycle probe before the full concurrent suite. Existing assertions, response deadline and job budgets are unchanged; instrumentation does not claim to solve the observed timeout.
|
|
24
|
+
|
|
25
|
+
## 0.57.9: Reply Safety and CI Recovery
|
|
26
|
+
|
|
27
|
+
- `Recipient safety`: Ships the containment prepared for unpublished 0.57.8: a refused follower reply no longer immediately reruns its whole command handler and repeats earlier effects. The original input stays unchanged in durable retry state, without silent completion. Automatic reply relocation remains withdrawn; stale-Thread commands may still retry and ordinary execution remains at-least-once.
|
|
28
|
+
- `Follower refusals`: Structured bus acknowledgements identify requests blocked before Telegram API delegation, without granting resend authority. Legacy error text, mismatched methods and lost acknowledgements cannot prove non-execution. Reply chunks keep their captured destination and ownership; refused chunks acquire no message ownership.
|
|
29
|
+
- `CI sharding`: macOS distributes its serialized test files across two deterministic jobs to avoid the measured whole-job overrun. A checked inventory includes every file exactly once, with real-process tests isolated and the local Pi CLI on PATH. Assertions, test deadlines and the 30-minute job budget are unchanged; publication requires every platform and shard to pass.
|
|
30
|
+
- `Process diagnostics`: Child-exit checks report bounded lifecycle timings and active resource types without logging fixture credentials or paths or adding referenced timers. Import, startup and natural-exit stages can now distinguish future failures. The original macOS child timeout remains unexplained; passing traces do not establish its cause.
|
|
31
|
+
|
|
32
|
+
## 0.57.8: Reply Recovery Safety
|
|
33
|
+
|
|
34
|
+
- `Recipient safety`: A refused follower reply no longer immediately re-runs the whole command handler, which could repeat changes made before the failed send. The original input stays unchanged in durable retry state, without silent completion. Automatic reply relocation is withdrawn pending transport-level safety proof; a stale-Thread command may still retry and ordinary execution remains at-least-once.
|
|
35
|
+
- `Follower refusals`: Structured bus acknowledgements distinguish requests blocked before Telegram API delegation from errors returned after execution. Legacy error text, mismatched methods and lost acknowledgements cannot certify non-execution or authorize a resend. Reply chunks retain their captured destination and ownership; refused chunks acquire no message ownership.
|
|
36
|
+
- `CI`: macOS runs real-process shutdown tests separately and the remaining test files one at a time, keeping the local Pi CLI on PATH. Assertions and deadlines are unchanged. This reduces observed runner-contention failures without claiming the process-exit flake is eliminated.
|
|
37
|
+
|
|
7
38
|
## 0.57.7: Routing Cleanup and Recipient Recovery
|
|
8
39
|
|
|
9
40
|
- `Recipient recovery`: A forwarded input journaled for a follower whose Thread was re-created no longer retries forever against the old Thread. After the leader refuses that reply, the input re-runs once against the follower's current Thread; a second refusal or any other failure keeps the durable retry state, and nothing is dropped.
|
|
@@ -201,6 +201,7 @@ export declare function createTelegramLiveTargetWorkObserver<TContext>(deps: {
|
|
|
201
201
|
}, ctx: TContext) => TelegramBusLiveRebindWorkState;
|
|
202
202
|
interface TelegramCommandsAndToolsBindingDeps {
|
|
203
203
|
pi: Pi.ExtensionAPI;
|
|
204
|
+
handleInternalAction: Commands.TelegramSessionActionRuntime["handleInternalAction"];
|
|
204
205
|
agentDir: string;
|
|
205
206
|
configStore: Config.TelegramConfigStore;
|
|
206
207
|
persistConfig: (config?: Config.TelegramConfig) => Promise<void>;
|
|
@@ -241,7 +242,7 @@ interface TelegramCommandsAndToolsBindingDeps {
|
|
|
241
242
|
connectionIntent: NonNullable<Commands.TelegramBridgeCommandRegistrationDeps["connectionIntent"]>;
|
|
242
243
|
recordRuntimeEvent: TelegramRuntimeEventRecorder;
|
|
243
244
|
}
|
|
244
|
-
export declare function registerTelegramCommandsAndTools({ pi, agentDir, configStore, persistConfig, setup, activeTurnRuntime, lockedPollingRuntime, stopPolling, getDisconnectThreadName, onTransportChanged, getStatusLines, buttonActionStore, sendMarkdownReply, sendChannelMarkdownMessage, sendChannelMediaMessage, listChannelPosts, mutateChannelPost, callMultipart, getDefaultChatId, getDefaultTarget, resolveAgentTarget, routeAgentMessage, canSendDirect, setGenerativeAppLiveSurfaceRuntime, recordRuntimeEvent, updateStatus, isContextCurrent, getSessionGeneration, connectionIntent, }: TelegramCommandsAndToolsBindingDeps): void;
|
|
245
|
+
export declare function registerTelegramCommandsAndTools({ pi, handleInternalAction, agentDir, configStore, persistConfig, setup, activeTurnRuntime, lockedPollingRuntime, stopPolling, getDisconnectThreadName, onTransportChanged, getStatusLines, buttonActionStore, sendMarkdownReply, sendChannelMarkdownMessage, sendChannelMediaMessage, listChannelPosts, mutateChannelPost, callMultipart, getDefaultChatId, getDefaultTarget, resolveAgentTarget, routeAgentMessage, canSendDirect, setGenerativeAppLiveSurfaceRuntime, recordRuntimeEvent, updateStatus, isContextCurrent, getSessionGeneration, connectionIntent, }: TelegramCommandsAndToolsBindingDeps): void;
|
|
245
246
|
interface TelegramLifecycleBindingDeps {
|
|
246
247
|
pi: Pi.ExtensionAPI;
|
|
247
248
|
publicationRuntime: TelegramBridgePublicationRuntime;
|
|
@@ -497,7 +497,7 @@ export function createTelegramLiveTargetWorkObserver(deps) {
|
|
|
497
497
|
};
|
|
498
498
|
};
|
|
499
499
|
}
|
|
500
|
-
export function registerTelegramCommandsAndTools({ pi, agentDir, configStore, persistConfig, setup, activeTurnRuntime, lockedPollingRuntime, stopPolling, getDisconnectThreadName, onTransportChanged, getStatusLines, buttonActionStore, sendMarkdownReply, sendChannelMarkdownMessage, sendChannelMediaMessage, listChannelPosts, mutateChannelPost, callMultipart, getDefaultChatId, getDefaultTarget, resolveAgentTarget, routeAgentMessage, canSendDirect, setGenerativeAppLiveSurfaceRuntime, recordRuntimeEvent, updateStatus, isContextCurrent, getSessionGeneration, connectionIntent, }) {
|
|
500
|
+
export function registerTelegramCommandsAndTools({ pi, handleInternalAction, agentDir, configStore, persistConfig, setup, activeTurnRuntime, lockedPollingRuntime, stopPolling, getDisconnectThreadName, onTransportChanged, getStatusLines, buttonActionStore, sendMarkdownReply, sendChannelMarkdownMessage, sendChannelMediaMessage, listChannelPosts, mutateChannelPost, callMultipart, getDefaultChatId, getDefaultTarget, resolveAgentTarget, routeAgentMessage, canSendDirect, setGenerativeAppLiveSurfaceRuntime, recordRuntimeEvent, updateStatus, isContextCurrent, getSessionGeneration, connectionIntent, }) {
|
|
501
501
|
GenerativeApps.registerTelegramBindTool(pi, {
|
|
502
502
|
agentDir,
|
|
503
503
|
getActiveProfileName: configStore.getActiveProfileName,
|
|
@@ -551,6 +551,7 @@ export function registerTelegramCommandsAndTools({ pi, agentDir, configStore, pe
|
|
|
551
551
|
}, { deliverAs: "nextTurn" });
|
|
552
552
|
};
|
|
553
553
|
Commands.registerTelegramBridgeCommands(pi, {
|
|
554
|
+
handleInternalAction,
|
|
554
555
|
promptForConfig: async (ctx, profileName) => {
|
|
555
556
|
const nextProfileName = profileName ?? undefined;
|
|
556
557
|
if (profileName && !Config.isValidTelegramProfileName(profileName)) {
|
|
@@ -955,6 +955,11 @@ export function createTelegramBusFollowerApiCaller(deps) {
|
|
|
955
955
|
const assertion = localOptions?.assertAuthority;
|
|
956
956
|
const assertCurrent = () => assertTelegramApiCallAuthority(assertion, false);
|
|
957
957
|
assertCurrent();
|
|
958
|
+
// Match IPC's JSON semantics once, before awaits; send and refusal metadata share this identity.
|
|
959
|
+
const requestArgs = JSON.parse(JSON.stringify(args));
|
|
960
|
+
const apiMethod = (method === "call" || method === "callMultipart") && typeof requestArgs[0] === "string"
|
|
961
|
+
? requestArgs[0] : method;
|
|
962
|
+
timing.apiMethod = apiMethod;
|
|
958
963
|
const registration = await resolveTelegramBusFollowerRegistration(deps, timeoutMs);
|
|
959
964
|
timing.registeredAtMs = getNowMs();
|
|
960
965
|
assertCurrent();
|
|
@@ -968,17 +973,13 @@ export function createTelegramBusFollowerApiCaller(deps) {
|
|
|
968
973
|
instanceId: deps.instanceId,
|
|
969
974
|
registrationGeneration: registration.generation,
|
|
970
975
|
method,
|
|
971
|
-
args,
|
|
976
|
+
args: requestArgs,
|
|
972
977
|
sentAtMs: getNowMs(),
|
|
973
978
|
}, assertion ? assertCurrent : undefined);
|
|
974
979
|
}
|
|
975
980
|
catch (error) {
|
|
976
981
|
if (error instanceof TelegramBusLocalAuthorityError)
|
|
977
982
|
throw new TelegramApiAuthorityError(error.requestIssued);
|
|
978
|
-
const apiMethod = (method === "call" || method === "callMultipart") &&
|
|
979
|
-
typeof args[0] === "string"
|
|
980
|
-
? args[0]
|
|
981
|
-
: method;
|
|
982
983
|
if (!isTelegramApiMethodRetrySafe(apiMethod)) {
|
|
983
984
|
throw new TelegramApiCommitUnknownError(apiMethod, error);
|
|
984
985
|
}
|
|
@@ -1002,15 +1003,19 @@ export function createTelegramBusFollowerApiCaller(deps) {
|
|
|
1002
1003
|
response.error?.code === "commit-unknown") {
|
|
1003
1004
|
throw new TelegramApiCommitUnknownError(response.error.method ?? method, new Error(message ?? "Telegram bus API call result is ambiguous."));
|
|
1004
1005
|
}
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1006
|
+
const refusedMethod = response?.kind === "bus.ack" && !response.ok &&
|
|
1007
|
+
response.error?.code === "follower-api-not-allowed" &&
|
|
1008
|
+
response.error.method === apiMethod
|
|
1009
|
+
? apiMethod : undefined;
|
|
1010
|
+
if (refusedMethod || message === TELEGRAM_BUS_FOLLOWER_API_NOT_ALLOWED_MESSAGE) {
|
|
1011
|
+
const body = (method === "call" || method === "callMultipart") && isRecord(requestArgs[1])
|
|
1012
|
+
? requestArgs[1]
|
|
1008
1013
|
: undefined;
|
|
1009
1014
|
const chatId = Number(body?.chat_id);
|
|
1010
1015
|
const threadId = Number(body?.message_thread_id);
|
|
1011
|
-
throw new TelegramApiFollowerNotAllowedError(message, Number.isSafeInteger(chatId) && Number.isSafeInteger(threadId)
|
|
1016
|
+
throw new TelegramApiFollowerNotAllowedError(message ?? TELEGRAM_BUS_FOLLOWER_API_NOT_ALLOWED_MESSAGE, Number.isSafeInteger(chatId) && Number.isSafeInteger(threadId)
|
|
1012
1017
|
? { chatId, threadId }
|
|
1013
|
-
: undefined);
|
|
1018
|
+
: undefined, refusedMethod);
|
|
1014
1019
|
}
|
|
1015
1020
|
throw new Error(message ?? "Telegram bus API call failed.");
|
|
1016
1021
|
};
|
|
@@ -1028,10 +1033,7 @@ export function createTelegramBusFollowerApiCaller(deps) {
|
|
|
1028
1033
|
finally {
|
|
1029
1034
|
const elapsedMs = getNowMs() - startedAtMs;
|
|
1030
1035
|
if (elapsedMs >= slowCallMs) {
|
|
1031
|
-
const apiMethod =
|
|
1032
|
-
typeof args[0] === "string"
|
|
1033
|
-
? args[0]
|
|
1034
|
-
: method;
|
|
1036
|
+
const apiMethod = timing.apiMethod ?? method;
|
|
1035
1037
|
try {
|
|
1036
1038
|
deps.recordRuntimeEvent?.("bus", `Telegram bus follower API call was slow (${elapsedMs}ms).`, {
|
|
1037
1039
|
phase: "follower-api-slow",
|
|
@@ -2770,7 +2770,14 @@ async function handleFollowerApiCall(envelope, deps) {
|
|
|
2770
2770
|
method: envelope.method,
|
|
2771
2771
|
args: envelope.args,
|
|
2772
2772
|
})) {
|
|
2773
|
-
|
|
2773
|
+
const apiMethod = getFollowerApiMethodAndBody(envelope).apiMethod;
|
|
2774
|
+
return {
|
|
2775
|
+
...rejectTelegramBusRequest(envelope.requestId, TELEGRAM_BUS_FOLLOWER_API_NOT_ALLOWED_MESSAGE),
|
|
2776
|
+
// Only this pre-dispatch authorizer refusal certifies that the API method was not called.
|
|
2777
|
+
...(/^[A-Za-z][A-Za-z0-9]{0,63}$/.test(apiMethod)
|
|
2778
|
+
? { error: { code: "follower-api-not-allowed", method: apiMethod } }
|
|
2779
|
+
: {}),
|
|
2780
|
+
};
|
|
2774
2781
|
}
|
|
2775
2782
|
if (!deps.callApi) {
|
|
2776
2783
|
return rejectTelegramBusRequest(envelope.requestId, "Telegram bus leader does not expose API calling.");
|
|
@@ -548,7 +548,7 @@ export type TelegramBusEnvelope = ({
|
|
|
548
548
|
result?: unknown;
|
|
549
549
|
protocol?: TelegramBusProtocolIdentity;
|
|
550
550
|
error?: {
|
|
551
|
-
code: "commit-unknown" | "request-id-collision" | "ledger-overloaded" | "incompatible-protocol" | "stale-target" | "workspace-binding-unavailable";
|
|
551
|
+
code: "commit-unknown" | "request-id-collision" | "ledger-overloaded" | "incompatible-protocol" | "stale-target" | "follower-api-not-allowed" | "workspace-binding-unavailable";
|
|
552
552
|
method?: string;
|
|
553
553
|
chatId?: number;
|
|
554
554
|
threadId?: number;
|
|
@@ -2657,9 +2657,15 @@ function parseAckEnvelope(value, requestId) {
|
|
|
2657
2657
|
code === "ledger-overloaded" ||
|
|
2658
2658
|
code === "incompatible-protocol" ||
|
|
2659
2659
|
code === "stale-target" ||
|
|
2660
|
+
code === "follower-api-not-allowed" ||
|
|
2660
2661
|
code === "workspace-binding-unavailable") {
|
|
2661
2662
|
const chatId = value.error.chatId;
|
|
2662
2663
|
const threadId = value.error.threadId;
|
|
2664
|
+
if (code === "follower-api-not-allowed" &&
|
|
2665
|
+
(value.ok || Object.hasOwn(value, "result") ||
|
|
2666
|
+
typeof value.error.method !== "string" ||
|
|
2667
|
+
!/^[A-Za-z][A-Za-z0-9]{0,63}$/.test(value.error.method)))
|
|
2668
|
+
return undefined;
|
|
2663
2669
|
if (code === "stale-target" &&
|
|
2664
2670
|
(!Number.isSafeInteger(chatId) || !Number.isSafeInteger(threadId))) {
|
|
2665
2671
|
return undefined;
|
|
@@ -11,6 +11,7 @@ import type { TelegramPromptTemplateCommand } from "./prompt-templates.ts";
|
|
|
11
11
|
import { createTelegramControlItemBuilder, type PendingTelegramControlItem, type TelegramControlQueueController, type TelegramControlQueueControllerDeps, type TelegramQueueAdmissionReceipt } from "./queue.ts";
|
|
12
12
|
import { type TelegramBridgeStatusLineOptions } from "./status.ts";
|
|
13
13
|
import { type TelegramApiCallOptions } from "./telegram-api.ts";
|
|
14
|
+
import type { TelegramTarget } from "./target.ts";
|
|
14
15
|
import type { TelegramSessionReplacementIntent } from "./threads.ts";
|
|
15
16
|
import { type TelegramDeferredSourceEvidence, type TelegramLiveSourceCompletionReadiness, type TelegramQueueAdmissionReceiptLike } from "./updates.ts";
|
|
16
17
|
export type TelegramHeldCommandName = "status" | "start" | "help" | "abort" | "stop" | "next" | "continue";
|
|
@@ -165,6 +166,7 @@ export interface TelegramBridgeCommandStartPollingResult {
|
|
|
165
166
|
owner?: string;
|
|
166
167
|
}
|
|
167
168
|
export interface TelegramBridgeCommandRegistrationDeps {
|
|
169
|
+
handleInternalAction?: TelegramSessionActionRuntime["handleInternalAction"];
|
|
168
170
|
promptForConfig: (ctx: ExtensionCommandContext, profileName?: string) => Promise<void>;
|
|
169
171
|
getStatusLines: (options?: TelegramBridgeStatusLineOptions) => string[];
|
|
170
172
|
reloadConfig: () => Promise<void>;
|
|
@@ -383,11 +385,13 @@ export interface TelegramCommandTargetRuntimeDeps<TContext> {
|
|
|
383
385
|
enqueueControlItem: (target: TelegramCommandMessageTarget, ctx: TContext, controlType: TelegramControlCommandType, statusSummary: string, execute: (ctx: TContext) => Promise<void>, admissionReceipts?: TelegramQueueAdmissionReceipt[], onQueued?: (item: PendingTelegramControlItem<TContext>) => void) => void;
|
|
384
386
|
getAdmissionScope?: () => string | undefined;
|
|
385
387
|
getAdmissionJournalBinding?: () => string | undefined;
|
|
388
|
+
/** Ordinary reply destination selected before publication, not resend authority. */
|
|
389
|
+
getReplyTarget?: (message: TelegramCommandRuntimeMessage) => TelegramTarget | undefined;
|
|
386
390
|
onControlQueued?: (message: TelegramCommandRuntimeMessage, receipt: TelegramQueueAdmissionReceipt) => void;
|
|
387
|
-
showStatus: (chatId: number, replyToMessageId: number, ctx: TContext, threadId?: number, options?: Pick<TelegramApiCallOptions, "assertAuthority">) => Promise<void>;
|
|
388
|
-
openModelMenu: (chatId: number, replyToMessageId: number, ctx: TContext, threadId?: number, options?: Pick<TelegramApiCallOptions, "assertAuthority">) => Promise<void>;
|
|
389
|
-
openSettingsMenu?: (chatId: number, replyToMessageId: number, ctx: TContext, threadId?: number, options?: Pick<TelegramApiCallOptions, "assertAuthority">) => Promise<void>;
|
|
390
|
-
sendTextReply: (chatId: number, replyToMessageId: number, text: string, options?: {
|
|
391
|
+
showStatus: (chatId: number, replyToMessageId: number | undefined, ctx: TContext, threadId?: number, options?: Pick<TelegramApiCallOptions, "assertAuthority">) => Promise<void>;
|
|
392
|
+
openModelMenu: (chatId: number, replyToMessageId: number | undefined, ctx: TContext, threadId?: number, options?: Pick<TelegramApiCallOptions, "assertAuthority">) => Promise<void>;
|
|
393
|
+
openSettingsMenu?: (chatId: number, replyToMessageId: number | undefined, ctx: TContext, threadId?: number, options?: Pick<TelegramApiCallOptions, "assertAuthority">) => Promise<void>;
|
|
394
|
+
sendTextReply: (chatId: number, replyToMessageId: number | undefined, text: string, options?: {
|
|
391
395
|
parseMode?: "HTML";
|
|
392
396
|
target?: {
|
|
393
397
|
chatId: number;
|
|
@@ -658,11 +662,10 @@ export declare function createTelegramCommandHandler<TMessage extends TelegramCo
|
|
|
658
662
|
prepareSelectedMenuCommand(command: ParsedTelegramCommand, messages: readonly TMessage[], ctx: TContext, admission: TelegramSelectedCommandAdmission): (() => Promise<boolean>) | undefined;
|
|
659
663
|
};
|
|
660
664
|
export declare function createTelegramCommandOrPromptDispatcher<TMessage, TContext>(deps: TelegramCommandOrPromptDispatcherDeps<TMessage, TContext>): (messages: TMessage[], ctx: TContext) => Promise<void>;
|
|
661
|
-
export declare const
|
|
662
|
-
export declare const
|
|
663
|
-
export declare const TELEGRAM_INTERNAL_MANUAL_USE_MESSAGE = "This internal Telegram
|
|
665
|
+
export declare const TELEGRAM_CONNECT_COMMAND_NAME = "telegram-connect";
|
|
666
|
+
export declare const TELEGRAM_INTERNAL_COMMAND_FLAG = "--internal";
|
|
667
|
+
export declare const TELEGRAM_INTERNAL_MANUAL_USE_MESSAGE = "This internal Telegram action cannot be run manually.";
|
|
664
668
|
export interface TelegramSessionActionRuntimeDeps {
|
|
665
|
-
registerCommand: Pi.ExtensionAPI["registerCommand"];
|
|
666
669
|
sendUserMessage: Pi.ExtensionAPI["sendUserMessage"];
|
|
667
670
|
notifyResult: (target: {
|
|
668
671
|
chatId: number;
|
|
@@ -694,7 +697,6 @@ export interface TelegramSessionReplacementSettlementDeps {
|
|
|
694
697
|
}
|
|
695
698
|
export declare function settleTelegramSessionReplacement(deps: TelegramSessionReplacementSettlementDeps): Promise<"none" | "settled" | "expired" | "failed" | "stale">;
|
|
696
699
|
export interface TelegramSessionActionAssemblyDeps {
|
|
697
|
-
registerCommand: Pi.ExtensionAPI["registerCommand"];
|
|
698
700
|
sendUserMessage: Pi.ExtensionAPI["sendUserMessage"];
|
|
699
701
|
store: {
|
|
700
702
|
load: () => Promise<void>;
|
|
@@ -750,7 +752,7 @@ export declare function createTelegramSessionActionAssembly(deps: TelegramSessio
|
|
|
750
752
|
};
|
|
751
753
|
};
|
|
752
754
|
export interface TelegramSessionActionRuntime {
|
|
753
|
-
|
|
755
|
+
handleInternalAction: (args: string, ctx: Pi.ExtensionCommandContext) => Promise<void>;
|
|
754
756
|
scheduleAfterUpdate: (updateId: number, target: {
|
|
755
757
|
chatId: number;
|
|
756
758
|
threadId?: number;
|
|
@@ -405,6 +405,7 @@ function formatTelegramTakeoverPrompt(ctx, owner) {
|
|
|
405
405
|
return `${action}\n\n${from} ${source}\n${to} ${ctx.cwd}`;
|
|
406
406
|
}
|
|
407
407
|
export function registerTelegramBridgeCommands(pi, deps) {
|
|
408
|
+
const handleInternalAction = deps.handleInternalAction;
|
|
408
409
|
pi.registerCommand("telegram-setup", {
|
|
409
410
|
description: "<profile> — Configure Telegram bot token",
|
|
410
411
|
handler: async (args, ctx) => {
|
|
@@ -418,9 +419,19 @@ export function registerTelegramBridgeCommands(pi, deps) {
|
|
|
418
419
|
ctx.ui.notify(deps.getStatusLines({ verbose }).join("\n"), "info");
|
|
419
420
|
},
|
|
420
421
|
});
|
|
421
|
-
pi.registerCommand(
|
|
422
|
+
pi.registerCommand(TELEGRAM_CONNECT_COMMAND_NAME, {
|
|
422
423
|
description: "<profile> — Start Telegram bridge",
|
|
423
424
|
handler: async (args, ctx) => {
|
|
425
|
+
const words = args.trim().split(/\s+/);
|
|
426
|
+
// Service dispatch never inherits profile activation or explicit-connect recovery authority.
|
|
427
|
+
if (words.some(word => word.toLowerCase().startsWith(TELEGRAM_INTERNAL_COMMAND_FLAG))) {
|
|
428
|
+
if (words[0] !== TELEGRAM_INTERNAL_COMMAND_FLAG || !handleInternalAction) {
|
|
429
|
+
ctx.ui.notify(TELEGRAM_INTERNAL_MANUAL_USE_MESSAGE, "warning");
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
await handleInternalAction(words.slice(1).join(" "), ctx);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
424
435
|
const sessionGeneration = deps.getSessionGeneration?.();
|
|
425
436
|
let intentId;
|
|
426
437
|
// Pi context getters throw after replacement; check plain intent/generation first.
|
|
@@ -629,6 +640,7 @@ export function createTelegramCommandTargetQueueRuntime(deps) {
|
|
|
629
640
|
createControlItem,
|
|
630
641
|
enqueueControlItem: controlQueueController.enqueue,
|
|
631
642
|
}),
|
|
643
|
+
getReplyTarget: deps.getReplyTarget,
|
|
632
644
|
getAdmissionScope: deps.getAdmissionScope,
|
|
633
645
|
getAdmissionJournalBinding: deps.getAdmissionJournalBinding,
|
|
634
646
|
onControlQueued: deps.onControlQueued,
|
|
@@ -639,6 +651,22 @@ export function createTelegramCommandTargetQueueRuntime(deps) {
|
|
|
639
651
|
});
|
|
640
652
|
}
|
|
641
653
|
export function createTelegramCommandTargetRuntime(deps) {
|
|
654
|
+
const getReplyTarget = (message, options) => {
|
|
655
|
+
const source = getTelegramCommandMessageTarget(message);
|
|
656
|
+
const recipient = options?.assertAuthority
|
|
657
|
+
? undefined
|
|
658
|
+
: deps.getReplyTarget?.(message);
|
|
659
|
+
if (!recipient)
|
|
660
|
+
return source;
|
|
661
|
+
if (recipient.chatId !== source.chatId)
|
|
662
|
+
throw new Error("Telegram command reply cannot change its recipient chat.");
|
|
663
|
+
if (source.threadId !== undefined && recipient.threadId === undefined)
|
|
664
|
+
throw new Error("Telegram command cannot move a Thread reply to All.");
|
|
665
|
+
return {
|
|
666
|
+
...recipient,
|
|
667
|
+
replyToMessageId: recipient.threadId === source.threadId ? source.replyToMessageId : undefined,
|
|
668
|
+
};
|
|
669
|
+
};
|
|
642
670
|
return {
|
|
643
671
|
enqueueControlItem: (message, ctx, controlType, statusSummary, execute) => {
|
|
644
672
|
const sourceUpdateId = message.pi_telegram_source_update_id;
|
|
@@ -659,15 +687,15 @@ export function createTelegramCommandTargetRuntime(deps) {
|
|
|
659
687
|
deps.enqueueControlItem(getTelegramCommandMessageTarget(message), ctx, controlType, statusSummary, execute, receipt ? [receipt] : undefined, receipt ? () => deps.onControlQueued?.(message, receipt) : undefined);
|
|
660
688
|
},
|
|
661
689
|
showStatus: (message, ctx, options) => {
|
|
662
|
-
const target =
|
|
690
|
+
const target = getReplyTarget(message, options);
|
|
663
691
|
return deps.showStatus(target.chatId, target.replyToMessageId, ctx, target.threadId, options);
|
|
664
692
|
},
|
|
665
693
|
openModelMenu: (message, ctx, options) => {
|
|
666
|
-
const target =
|
|
694
|
+
const target = getReplyTarget(message, options);
|
|
667
695
|
return deps.openModelMenu(target.chatId, target.replyToMessageId, ctx, target.threadId, options);
|
|
668
696
|
},
|
|
669
697
|
openSettingsMenu: async (message, ctx, options) => {
|
|
670
|
-
const target =
|
|
698
|
+
const target = getReplyTarget(message, options);
|
|
671
699
|
if (!deps.openSettingsMenu) {
|
|
672
700
|
await deps.sendTextReply(target.chatId, target.replyToMessageId, formatTelegramInformationHeading("🚫", "Settings menu is unavailable."), { target, parseMode: "HTML" });
|
|
673
701
|
return;
|
|
@@ -675,7 +703,7 @@ export function createTelegramCommandTargetRuntime(deps) {
|
|
|
675
703
|
await deps.openSettingsMenu(target.chatId, target.replyToMessageId, ctx, target.threadId, options);
|
|
676
704
|
},
|
|
677
705
|
sendTextReply: async (message, text, options) => {
|
|
678
|
-
const target =
|
|
706
|
+
const target = getReplyTarget(message, options);
|
|
679
707
|
const assertAuthority = options?.assertAuthority, send = deps.sendTextReply;
|
|
680
708
|
const capturedOptions = { ...options, target };
|
|
681
709
|
assertAuthority?.();
|
|
@@ -1061,6 +1089,7 @@ export function createTelegramCommandHandlerTargetRuntime(deps) {
|
|
|
1061
1089
|
}),
|
|
1062
1090
|
appendControlItem: deps.appendControlItem,
|
|
1063
1091
|
dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
|
|
1092
|
+
getReplyTarget: deps.getReplyTarget,
|
|
1064
1093
|
getAdmissionScope: deps.getAdmissionScope,
|
|
1065
1094
|
getAdmissionJournalBinding: deps.getAdmissionJournalBinding,
|
|
1066
1095
|
onControlQueued: deps.onControlQueued,
|
|
@@ -2397,9 +2426,9 @@ async function handleTelegramCommandRuntime(commandName, message, ctx, deps, com
|
|
|
2397
2426
|
},
|
|
2398
2427
|
}, commandArgs);
|
|
2399
2428
|
}
|
|
2400
|
-
export const
|
|
2401
|
-
export const
|
|
2402
|
-
export const TELEGRAM_INTERNAL_MANUAL_USE_MESSAGE = "This internal Telegram
|
|
2429
|
+
export const TELEGRAM_CONNECT_COMMAND_NAME = "telegram-connect";
|
|
2430
|
+
export const TELEGRAM_INTERNAL_COMMAND_FLAG = "--internal";
|
|
2431
|
+
export const TELEGRAM_INTERNAL_MANUAL_USE_MESSAGE = "This internal Telegram action cannot be run manually.";
|
|
2403
2432
|
function delayTelegramSessionAction(delayMs) {
|
|
2404
2433
|
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
2405
2434
|
}
|
|
@@ -2469,7 +2498,6 @@ export function createTelegramSessionActionAssembly(deps) {
|
|
|
2469
2498
|
report(new Error("Telegram new-session result delivery failed."));
|
|
2470
2499
|
};
|
|
2471
2500
|
const action = createTelegramSessionActionRuntime({
|
|
2472
|
-
registerCommand: deps.registerCommand,
|
|
2473
2501
|
sendUserMessage: deps.sendUserMessage,
|
|
2474
2502
|
notifyResult(target, result) {
|
|
2475
2503
|
return sendTerminalResult(target, result);
|
|
@@ -2567,7 +2595,6 @@ export function createTelegramSessionActionRuntime(deps) {
|
|
|
2567
2595
|
let pendingUpdateId;
|
|
2568
2596
|
let pendingTarget;
|
|
2569
2597
|
let pendingAction;
|
|
2570
|
-
let registered = false;
|
|
2571
2598
|
const reportFailure = (error) => {
|
|
2572
2599
|
try {
|
|
2573
2600
|
deps.recordRuntimeEvent?.("new-session", error);
|
|
@@ -2577,35 +2604,29 @@ export function createTelegramSessionActionRuntime(deps) {
|
|
|
2577
2604
|
}
|
|
2578
2605
|
};
|
|
2579
2606
|
return {
|
|
2580
|
-
|
|
2581
|
-
|
|
2607
|
+
async handleInternalAction(args, ctx) {
|
|
2608
|
+
const action = pendingAction;
|
|
2609
|
+
const [operation, token, ...extra] = args.trim().split(/\s+/);
|
|
2610
|
+
if (!action || operation !== "new" ||
|
|
2611
|
+
token !== action.token || extra.length !== 0) {
|
|
2612
|
+
ctx.ui.notify(TELEGRAM_INTERNAL_MANUAL_USE_MESSAGE, "warning");
|
|
2582
2613
|
return;
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
ctx.
|
|
2590
|
-
|
|
2614
|
+
}
|
|
2615
|
+
pendingAction = undefined;
|
|
2616
|
+
switch (action.kind) {
|
|
2617
|
+
case "replace-session":
|
|
2618
|
+
try {
|
|
2619
|
+
await deps.prepareReplacement?.(ctx, action.updateId, action.target);
|
|
2620
|
+
const result = await ctx.newSession();
|
|
2621
|
+
if (result.cancelled)
|
|
2622
|
+
await deps.notifyResult(action.target, "cancelled");
|
|
2591
2623
|
}
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
try {
|
|
2596
|
-
await deps.prepareReplacement?.(ctx, action.updateId, action.target);
|
|
2597
|
-
const result = await ctx.newSession();
|
|
2598
|
-
if (result.cancelled)
|
|
2599
|
-
await deps.notifyResult(action.target, "cancelled");
|
|
2600
|
-
}
|
|
2601
|
-
catch (error) {
|
|
2602
|
-
reportFailure(error);
|
|
2603
|
-
await deps.notifyResult(action.target, "failure");
|
|
2604
|
-
}
|
|
2605
|
-
return;
|
|
2624
|
+
catch (error) {
|
|
2625
|
+
reportFailure(error);
|
|
2626
|
+
await deps.notifyResult(action.target, "failure");
|
|
2606
2627
|
}
|
|
2607
|
-
|
|
2608
|
-
}
|
|
2628
|
+
return;
|
|
2629
|
+
}
|
|
2609
2630
|
},
|
|
2610
2631
|
scheduleAfterUpdate(updateId, target) {
|
|
2611
2632
|
if (pendingUpdateId !== undefined || pendingAction !== undefined)
|
|
@@ -2625,9 +2646,7 @@ export function createTelegramSessionActionRuntime(deps) {
|
|
|
2625
2646
|
const token = randomUUID();
|
|
2626
2647
|
pendingAction = { kind: "replace-session", token, updateId, target };
|
|
2627
2648
|
void Promise.resolve()
|
|
2628
|
-
.then(() => deps.sendUserMessage(`/${
|
|
2629
|
-
expandPromptTemplates: true,
|
|
2630
|
-
}))
|
|
2649
|
+
.then(() => deps.sendUserMessage(`/${TELEGRAM_CONNECT_COMMAND_NAME} ${TELEGRAM_INTERNAL_COMMAND_FLAG} new ${token}`, { expandPromptTemplates: true }))
|
|
2631
2650
|
.catch((error) => {
|
|
2632
2651
|
pendingAction = undefined;
|
|
2633
2652
|
reportFailure(error);
|