@llblab/pi-telegram 0.24.6 → 0.24.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +5 -5
- package/BACKLOG.md +0 -12
- package/CHANGELOG.md +6 -0
- package/README.md +2 -2
- package/docs/architecture.md +1 -1
- package/docs/multi-instance-bus.md +7 -6
- package/docs/outbound.md +1 -0
- package/docs/public-api.md +1 -1
- package/docs/ui-style.md +4 -0
- package/index.ts +10 -0
- package/lib/bus-leader.ts +204 -59
- package/lib/bus.ts +6 -1
- package/lib/config.ts +1 -1
- package/lib/keyboard.ts +1 -0
- package/lib/menu-settings.ts +8 -12
- package/lib/outbound-buttons.ts +34 -2
- package/lib/outbound.ts +1 -0
- package/lib/routing.ts +25 -2
- package/lib/sync.ts +42 -20
- package/lib/thread-reconciler.ts +100 -2
- package/lib/threads.ts +95 -0
- package/package.json +3 -26
- package/scripts/audit-dependencies.ts +0 -78
- package/scripts/dependency-audit-policy.ts +0 -300
package/AGENTS.md
CHANGED
|
@@ -82,10 +82,10 @@ The core product loop is mobile continuation: start or supervise work in the ter
|
|
|
82
82
|
- `/telegram-connect` is the bounded liveness-recovery boundary for unclean-shutdown corruption in disposable `tmp/telegram` state. It may quarantine only classifier-approved malformed `owners.json`, active-profile `state*.json`, and `owners.json.transaction` after serializing recoverers and revalidating under ownership fencing; it must preserve `telegram.json`, diagnostics, unrelated artifacts, and every verifiable live owner/transaction holder. Stale heartbeats older than eight seconds do not gain protection through PID reuse. Require confirmed local polling suspension before quarantine mutation; a suspension failure blocks recovery, while an ownership-release failure may continue only into final guarded live-owner revalidation. Use the established bounded Windows sharing-violation rename retries. Retry polling exactly once after recovery, preserve unrelated startup errors, and replace blocked/repeated recovery failure with one explicit restart instruction rather than broad `tmp/` deletion or an automatic loop.
|
|
83
83
|
- Telegram profiles are parallel bot/runtime realities, not merely alternate credentials. Persist every bot/session field (`botToken`, `botUsername`, `botId`, `allowedUserId`, `lastUpdateId`) under `telegram.json` `profiles.<name>`, including `profiles.default`; keep shared handlers and assistant/voice/time/thread-lifecycle settings top-level. Bare and explicit `default` setup/connect commands address the same profile. Legacy root identity may normalize atomically only when unambiguous; conflicts fail closed without mutation. Any owner slot, IPC endpoint, state snapshot, diagnostics log, thread/bus binding, runtime cache, ownership map, or operator-visible status surface that could mix bot identities must be deliberately classified as global, session-local, or profile-scoped. Use the `default` owner slot for the default profile and validated profile names for named slots; give named profiles isolated observable surfaces unless there is an explicit top-level shared-config reason not to.
|
|
84
84
|
- Telegram private-chat Threaded Mode is the public switch for the local multi-instance bus. There is no separate public `telegram.json` bus switch: classic/private-chat singleton DM behavior is the base product mode, and when Telegram reports Threaded Mode available for the bot the bridge enables leader/follower bus behavior. If Telegram reports Threaded Mode unavailable or thread operations prove it unavailable, the bridge returns to classic singleton behavior as a first-class mode. During a live Threaded Mode downgrade, the current transport leader keeps the singleton classic polling role and followers disconnect instead of taking over or offering takeover while active thread bindings prove this is a downgrade from a live bus organism. Do not inject slot/thread rename guidance in classic mode. Blocked instances should not silently register as followers unless the effective runtime mode is Threaded Mode.
|
|
85
|
-
- In Threaded Mode, exactly one live leader owns `getUpdates` and direct Telegram Bot API calls for one bot token. `/telegram-connect` means join the local Telegram organism: if no live leader exists the instance becomes leader; if a live leader exists the terminal-visible instance registers as follower; ordinary connect must not offer manual takeover while the leader heartbeat is live. Leadership is an ephemeral transport role, not permanent ownership by the first terminal-visible Pi instance. Heartbeat acknowledgements carry the authenticated live follower-slot roster: on leader loss, the lowest observed live slot attempts the atomic lock first, while higher slots defer for one bounded grace and then re-check so a missing lower follower cannot block recovery indefinitely. Followers must be started by the operator as real Pi processes, register over the local bus, heartbeat, receive only their routed inbound updates, and send outbound replies/previews/files/chat-actions through the leader transport; Telegram must not spawn hidden Pi processes, launch follower subprocesses, or expose a slash command that creates a new local Pi instance. If a leader reloads while followers stay alive, followers must treat rejected/unknown/unauthorized heartbeats as a registration-loss signal and optimistically re-register with the currently active leader before considering promotion; after a short grace window without successful re-registration, a live follower may promote only through an atomic expected-owner transition after the exact observed leader lease becomes stale or inactive. An unreachable IPC endpoint does not authorize replacing a still-live lock owner; remain disconnected/retrying rather than creating split-brain polling. Terminal status must not remain `follower` while the leader registry can no longer route that follower's thread. Heartbeat pruning is liveness bookkeeping, not permission to delete, mark offline, or send a Telegram-visible disconnected notice for a follower's thread; preserve active follower tabs/bindings across transient reload/reconnect incidents and reserve destructive teardown/offline transitions for confirmed manual disconnect, graceful Pi quit, or confirmed reconciliation cases. If the leader dies, a live follower should eventually be able to take over through stale-leader election/recovery so the local multi-instance organism keeps running. Explicit `/telegram-disconnect` retains destructive confirmation; graceful Pi `quit` invokes the same teardown without prompting when top-level `threads.automaticCleanup` is enabled (default), while disabling it and `reload`, `new`, `resume`, or `fork` preserve the binding through safe handoff/suspension. A leader deletes its own bound thread before releasing transport ownership, while a follower sends an authenticated exact-registration-generation disconnect envelope and waits for the leader to delete its assigned thread, mark the durable binding offline, and remove live routing authority before unregistering. Cleanup must be confirmed by successful deletion or explicit already-gone evidence;
|
|
85
|
+
- In Threaded Mode, exactly one live leader owns `getUpdates` and direct Telegram Bot API calls for one bot token. `/telegram-connect` means join the local Telegram organism: if no live leader exists the instance becomes leader; if a live leader exists the terminal-visible instance registers as follower; ordinary connect must not offer manual takeover while the leader heartbeat is live. Leadership is an ephemeral transport role, not permanent ownership by the first terminal-visible Pi instance. Heartbeat acknowledgements carry the authenticated live follower-slot roster: on leader loss, the lowest observed live slot attempts the atomic lock first, while higher slots defer for one bounded grace and then re-check so a missing lower follower cannot block recovery indefinitely. Followers must be started by the operator as real Pi processes, register over the local bus, heartbeat, receive only their routed inbound updates, and send outbound replies/previews/files/chat-actions through the leader transport; Telegram must not spawn hidden Pi processes, launch follower subprocesses, or expose a slash command that creates a new local Pi instance. If a leader reloads while followers stay alive, followers must treat rejected/unknown/unauthorized heartbeats as a registration-loss signal and optimistically re-register with the currently active leader before considering promotion; after a short grace window without successful re-registration, a live follower may promote only through an atomic expected-owner transition after the exact observed leader lease becomes stale or inactive. An unreachable IPC endpoint does not authorize replacing a still-live lock owner; remain disconnected/retrying rather than creating split-brain polling. Terminal status must not remain `follower` while the leader registry can no longer route that follower's thread. Heartbeat pruning is liveness bookkeeping, not permission by itself to delete, mark offline, or send a Telegram-visible disconnected notice for a follower's thread; preserve active follower tabs/bindings across transient reload/reconnect incidents and reserve destructive teardown/offline transitions for confirmed manual disconnect, graceful Pi quit, or confirmed reconciliation cases. A stale follower may become a confirmed reconciliation case only when Thread cleanup is enabled, the OS reports the exact registered PID absent, and cleanup serializes ahead of replacement registration; live/unknown PID state remains non-destructive. If the leader dies, a live follower should eventually be able to take over through stale-leader election/recovery so the local multi-instance organism keeps running. Explicit `/telegram-disconnect` retains destructive confirmation; graceful Pi `quit` invokes the same teardown without prompting when top-level `threads.automaticCleanup` is enabled (default), while disabling it and `reload`, `new`, `resume`, or `fork` preserve the binding through safe handoff/suspension. A leader deletes its own bound thread before releasing transport ownership, while a follower sends an authenticated exact-registration-generation disconnect envelope and waits for the leader to delete its assigned thread, mark the durable binding offline, and remove live routing authority before unregistering. Cleanup must be confirmed by successful deletion or explicit already-gone evidence. Confirmed leader/follower teardown persists an exact target/runtime-generation cleanup intent before destructive API work; interruption preserves that intent for the current or successor leader to replay under its own epoch, and confirmation removes both the binding and intent atomically from the next persisted snapshot. Silence, heartbeat expiry, process absence, IPC failure, and auth mismatch create no cleanup intent by themselves; only stale heartbeat combined with enabled cleanup and OS-confirmed absence of the exact registered follower PID may authorize fallback intent. Incomplete manual cleanup preserves binding/routing state, keeps the session open, and returns actionable retry guidance. A promoted leader authorizes inherited follower-thread cleanup by its current leader epoch rather than the historical owner label. Status is reactive to the effective runtime role and bot capability: classic/non-thread mode shows ordinary `telegram connected`, active bus leadership shows `telegram leader`, and registered followers show `telegram follower`; stable multi-instance truth belongs in the bus role (`leader`/`follower`), while lifecycle is reserved for transient recovery state such as `electing` and must not duplicate stable roles with labels like `leader-active`. When leadership or Threaded Mode availability changes at runtime, the status line and `/telegram-status` must converge promptly instead of preserving a stale role label.
|
|
86
86
|
- Runtime routing binds to a live `instanceId` plus its exact follower registration generation; current thread identity should use explicit owner records (`leader`, `manual-follower`, `pending-topic`) rather than hiding ownership direction in legacy string keys. Persisted manual-follower records may act as stable restart hints, but only an authenticated live registry entry authorizes inbound forwarding or outbound transport; stale session records must not be treated as live owners. A live instance owns at most one active bound thread; provisioning a new target for the same `instanceId` must remove older current-state bindings and close duplicate Telegram threads when possible. A reload creates a new runtime `instanceId`; if the previous leader slot is still known/alive, keep it occupied in reservations and assign the new leader the next free slot instead of creating a duplicate same-letter tab or blocking startup on Telegram UI convergence.
|
|
87
87
|
- Follower registration readiness should stay on the smallest critical path that makes the follower routable: create or reuse the Telegram target, persist the active binding, mark target-bindings fresh, and return the target to the follower. Registration requires a present exact generation. Cross-session target reuse and any carried target absent from current bindings require one synchronous visibility probe: explicit stale evidence may provision a replacement, while ambiguous/non-stale failure must reject registration without replay, binding invalidation, or speculative replacement. Persist an ambiguously observed absent target only as non-routable `probe-required` restoration evidence so targetless retries and leader reloads must probe the exact target again before activation or replacement. Registration and explicit disconnect mutations for one durable follower profile must serialize across awaited cleanup so an old runtime's disconnect cannot delete or unregister a replacement generation. Manual follower identity must survive a Pi process reload in the same terminal so reload is not treated as a new follower/thread; do not key durable follower ownership only by the short-lived Pi process id. Telegram-visible connected notices, duplicate/replaced thread cleanup, and other reconciliation work are important but non-critical; run them after registration and record slow/failing background steps as runtime events rather than delaying follower usability.
|
|
88
|
-
- Instance slots are extension-owned ring-order metadata. Allocate fresh slots from the `bot.lastSlot` cursor as `A` → `B` → ... → `Z` → `A`, allowing the cursor to move to lower letters after wraparound. Only current live/recovering bindings plus unexpired pending provisions and explicit reservations occupy slots; historical records must not block allocation indefinitely. Preserve a slot on ordinary current binding/thread reuse without advancing or rewinding the fresh-allocation cursor. Follower bindings survive process absence as stable restoration hints
|
|
88
|
+
- Instance slots are extension-owned ring-order metadata. Allocate fresh slots from the `bot.lastSlot` cursor as `A` → `B` → ... → `Z` → `A`, allowing the cursor to move to lower letters after wraparound. Only current live/recovering bindings plus unexpired pending provisions and explicit reservations occupy slots; historical records must not block allocation indefinitely. Preserve a slot on ordinary current binding/thread reuse without advancing or rewinding the fresh-allocation cursor. Follower bindings survive process absence as stable restoration hints unless enabled Thread cleanup turns stale heartbeat plus exact-PID death confirmation into fenced cleanup authority; otherwise only explicit stale/deleted/offline evidence invalidates them. Successful target reuse must refresh the binding timestamp and replacement runtime identity; generic leader startup or heartbeat pruning alone must not erase the target merely because no follower registration is currently live. An authenticated live follower whose carried exact target is missing from current persisted bindings should recover that target instead of creating another Telegram thread; preserve a carried slot only when it does not collide with another current record. Preserve a cursor that belongs to a live or recoverable binding and continue skipping retained restart hints plus unexpired pending/reserved slots. Explicit stale/deleted/offline reconciliation may release that slot; generic process absence may not. The alphabet cap is a feature — compact single-letter tabs are scannable.
|
|
89
89
|
- Instance thread names are provisioner-authored identity metadata, not model-authored output. Fresh Threaded Mode threads select one of five curated compact 4-6 letter Latin names for the assigned slot letter using provisioning timestamp entropy, then create the Telegram thread with that thread name immediately. The title is the thread name itself, not `{slot} {threadName}` or `{slot} — {threadName}`. Do not register or reintroduce an agent-facing thread rename tool; removing the extra rename prompt/tool turn is an intentional simplicity constraint. Thread names must mirror into terminal status, live diagnostics, and `[telegram|thread:name]` through one target-aware current-instance identity resolver. Registered follower/leader metadata takes precedence over a stale shared record for the same target, with the record used only as fallback; do not hide a valid baked/custom thread name behind old slot-prefix validation. A follower that later becomes leader keeps its existing name; leadership changes are transport role changes, not identity resets.
|
|
90
90
|
- Telegram private-chat Threaded Mode treats `All` as an aggregate/control surface, not a durable `General` thread or process launcher, and leader activation proactively creates or reuses its private-DM thread. When the owner writes in an unknown thread, the leader reclaims and routes the first one only if it lacks an active binding; later unknown threads remain intact and receive an in-thread chooser to reroute the captured prompt or restore a stale binding. Destructive cleanup requires explicit confirmation through `thread-reconciler`; another Pi instance still requires a manually started process followed by `/telegram-connect`. Without Threaded Mode, process the message through classic routing.
|
|
91
91
|
- Telegram extension work should not hold Pi's core agent lifecycle hostage once Pi has produced the semantic result. For Telegram-originated turns, final Telegram delivery, attachment upload, and transport cleanup are extension-owned side effects; schedule them off the critical `agent_end` path when ordering can still be preserved, record failures as runtime events, and keep dispatch of the next Telegram turn behind the delivery task when needed to avoid interleaving previews/finals. Treat a low-level error `agent_end` as retryable while its Telegram turn remains active; only a later successful `agent_end` or Pi's `agent_settled` boundary may finalize and release that turn. Public activity handlers run through isolated asynchronous per-handler queues; lifecycle hooks enqueue normalized events and never await consumer work. Proactive push defaults enabled and requires explicit `assistant.proactivePush: false` to opt out. While enabled, core delivery projects each completed Pi-visible assistant text block from local/autonomous work once and in order; bind admitted blocks to the exact target, profile/token transport stamp, direct leader epoch or follower registration generation, and session generation. Token deltas, hidden reasoning, tool traffic, stale authority, and Telegram-owned turns never enter that projection.
|
|
@@ -125,7 +125,7 @@ The core product loop is mobile continuation: start or supervise work in the ter
|
|
|
125
125
|
- Voice reply mode Settings UI standard: the top-level Settings row is `👄 Voice reply: hidden|mirror|always`; `hidden` is the true default, persists no `voice.replyMode`, adds no voice context, and still allows explicit agent-authored `telegram_voice`; `mirror` activates automatic voice context only for voice/audio input, and `always` activates it for every Telegram turn. The submenu title is `👄 Voice reply mode:`; choice buttons use lowercase labels with a model-style active dot (`🟢 hidden`, `🟢 mirror`) rather than per-mode emoji; the explanatory submenu body uses compact HTML-code bullets such as `<code>-</code> <code>hidden</code> (default): ...`. Preserve this wording/icons unless the operator explicitly asks to redesign it
|
|
126
126
|
- Outbound voice delivery is one fallback pipeline: configured `outboundHandlers` with `type: "voice"` run first in `telegram.json` order, then programmatic voice handlers, then registered voice synthesis providers as zero-config progressive fallbacks; provider extensions must not override operator-configured handlers
|
|
127
127
|
- `telegram_voice` text is arbitrary TTS-target text and the prompt should show only forms that include text to speak: `<!-- telegram_voice text="Short summary" -->` or `<!-- telegram_voice: Short summary -->`. Avoid raw Markdown/code/tables in voice text.
|
|
128
|
-
- `telegram_button` has three canonical forms: `<!-- telegram_button: OK -->` for label-only buttons, `<!-- telegram_button label=Continue prompt="Continue with the current plan." -->` for one-line prompts, or `<!-- telegram_button label="Show risks"\nList the main risks first.\n-->` for multiline prompts. Do not author JSON button specs, inline-after-text comments, standalone button tools, or comments inside code/quotes/lists/indented examples; write normal Markdown plus top-level hidden comments, and add visible parent text when buttons would otherwise be the only output
|
|
128
|
+
- `telegram_button` has three canonical forms: `<!-- telegram_button: OK -->` for label-only buttons, `<!-- telegram_button label=Continue prompt="Continue with the current plan." -->` for one-line prompts, or `<!-- telegram_button label="Show risks"\nList the main risks first.\n-->` for multiline prompts. The bridge preserves agent-authored label text and emoji, and owns post-selection state through Telegram's green `success` button style without changing that label. Do not author JSON button specs, inline-after-text comments, standalone button tools, or comments inside code/quotes/lists/indented examples; write normal Markdown plus top-level hidden comments, and add visible parent text when buttons would otherwise be the only output
|
|
129
129
|
|
|
130
130
|
## 6. Engineering Conventions
|
|
131
131
|
|
|
@@ -150,7 +150,7 @@ The core product loop is mobile continuation: start or supervise work in the ter
|
|
|
150
150
|
|
|
151
151
|
- Treat queue handling, compaction interaction, and lifecycle-hook state transitions as regression-prone areas; validate them after changing dispatch logic
|
|
152
152
|
- Keep the standard `npm run typecheck` gate strict about unused locals and parameters so stale imports and abandoned adapters fail validation instead of accumulating silently
|
|
153
|
-
- Keep push/PR CI on minimum Node `22.19.0` across Ubuntu, macOS, and Windows with typecheck, tests, and package dry-run on every OS; run audit once on Ubuntu, require the native Windows named-pipe roundtrip to execute, and keep Unix-only skips explicit by protected boundary.
|
|
153
|
+
- Keep push/PR CI on minimum Node `22.19.0` across Ubuntu, macOS, and Windows with typecheck, tests, and package dry-run on every OS; run the package-owned dependency audit once on Ubuntu, require the native Windows named-pipe roundtrip to execute, and keep Unix-only skips explicit by protected boundary. The release audit omits peer dependencies because Pi is the host-selected runtime and its shrinkwrapped graph is not shipped by this extension; keep a separate full installed-host audit command for visibility, and never use peer omission to hide a dependency owned or bundled by `pi-telegram`.
|
|
154
154
|
- Route important runtime failures through the recent runtime event recorder so `/telegram-status` remains useful for post-mortem debugging. Keep the compact TUI status bar bounded to the generic `error` state; detailed error messages belong only in diagnostics/logs, never inline beside the status label.
|
|
155
155
|
- Treat remaining Markdown-to-HTML rendering as Telegram UI/compat output work, not generic Markdown rendering or assistant reply delivery
|
|
156
156
|
- Preserve literal code content in Telegram rendering
|
|
@@ -216,7 +216,7 @@ The canonical detailed ownership map lives in [`docs/architecture.md`](./docs/ar
|
|
|
216
216
|
- Command help plus prompt-template commands and status/model/thinking/queue controls are driven through `/start`'s Telegram inline application menu and callback queries; the Queue button shows the queued-item count, model-menu scope/pagination controls stay at the top under Main menu, the model pagination indicator opens a compact page picker, and thinking-menu text stays a compact heading because the current level is marked by button state; `/status`, `/model`, `/thinking`, and `/queue` are hidden compatibility shortcuts
|
|
217
217
|
- Shared inline-keyboard structure belongs to `keyboard`; application-control button labels, callback data, and callback behavior stay in `menu`/`menu-model`/`menu-thinking`/`menu-status`/`menu-queue` while core queue mechanics stay in `queue`
|
|
218
218
|
- Telegram `/settings` options should open nested detail submenus by default: boolean options show a description plus Back, `on`, and `off`; list options show Back plus selectable values. One-shot actions such as syncing may run directly without a submenu when there is no meaningful choice or description step.
|
|
219
|
-
- Inline UI labels and dialogs follow [`docs/ui-style.md`](./docs/ui-style.md): action buttons use emoji plus capitalized action text, state & navigation buttons show state and lead to a submenu, first-level submenus start with `⬆️ Main menu` while deeper submenus start with `⬆️ Back`, boolean toggles use Capitalized horizontal `On`/`Off` with green active `On`, yellow active `Off`, and black inactive dots, tabs use capitalized labels with purple default-state and yellow elevated-state active dots and black inactive dots, vertical option lists mark only the current value green, and confirmation dialogs use a single bold text-only question with emoji on buttons only.
|
|
219
|
+
- Inline UI labels and dialogs follow [`docs/ui-style.md`](./docs/ui-style.md): action buttons use emoji plus capitalized action text, state & navigation buttons show state and lead to a submenu, first-level submenus start with `⬆️ Main menu` while deeper submenus start with `⬆️ Back`, boolean toggles use Capitalized horizontal `On`/`Off` with green active `On`, yellow active `Off`, and black inactive dots, tabs use capitalized labels with purple default-state and yellow elevated-state active dots and black inactive dots, vertical option lists mark only the current value green, accepted generated prompt buttons apply Telegram's green `success` style to the exact selected choice without changing its label, and confirmation dialogs use a single bold text-only question with emoji on buttons only.
|
|
220
220
|
- Inbound text/media may be transformed through configured `inboundHandlers` before queueing; legacy `attachmentHandlers` are deprecated compatibility aliases appended after `inboundHandlers`; active-turn outbound files must flow through `telegram_attach`, while explicit local/TUI Telegram sends use `telegram_attach` for files or `telegram_message` for text/buttons
|
|
221
221
|
- Long Telegram text split recovery belongs to `text-groups`: keep it conservative, short-debounced, same chat/user/message-id contiguous, and gated by near-limit human text so normal rapid follow-ups and slash commands stay separate
|
|
222
222
|
- Public API boundaries live in [`docs/public-api.md`](./docs/public-api.md): companion extensions must use public API domain subpaths such as `@llblab/pi-telegram/sections`, `/delivery`, `/voice`, `/inbound`, `/outbound`, and `/updates`; `0.12.0` intentionally removes the published `@llblab/pi-telegram/lib/*.ts` compatibility wildcard
|
package/BACKLOG.md
CHANGED
|
@@ -2,18 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
_This backlog tracks only open release-relevant work: hotfixes, bounded maintenance, live runtime verification, evidence-gated Telegram client follow-ups, and upstream Pi API blockers. Completed outcomes and validation evidence belong in `CHANGELOG.md`, not in this queue._
|
|
4
4
|
|
|
5
|
-
## P0 — Expiring Pi Shrinkwrap Audit Exception
|
|
6
|
-
|
|
7
|
-
Deadline: 2026-08-21 UTC. The validation gate intentionally fails at `2026-08-22T00:00:00Z` if either exception remains.
|
|
8
|
-
|
|
9
|
-
Context: `@earendil-works/pi-coding-agent@0.80.6` publishes its own `npm-shrinkwrap.json`, which prevents this consumer package from replacing two installed vulnerable copies. The repository temporarily permits only `brace-expansion@5.0.6` / source `1123898` / `GHSA-3jxr-9vmj-r5cp` and `protobufjs@7.6.4` / source `1123964` / `GHSA-j3f2-48v5-ccww`, plus parent findings whose complete audit graph resolves exclusively to those sources. `npm run audit` verifies the exact graph, installed paths and versions, and expiry; every unknown or changed finding fails closed.
|
|
10
|
-
|
|
11
|
-
Open work:
|
|
12
|
-
|
|
13
|
-
- [ ] Upgrade to a Pi release whose published shrinkwrap installs `brace-expansion>=5.0.7` and `protobufjs>=7.6.5`, remove the exception policy, and restore a zero-finding raw `npm audit` before the deadline.
|
|
14
|
-
|
|
15
|
-
Done when: a clean `npm ci` followed by raw `npm audit` reports zero vulnerabilities and the expiring policy/overrides are removed.
|
|
16
|
-
|
|
17
5
|
## P1 — Native Windows Runtime Smoke
|
|
18
6
|
|
|
19
7
|
Context: Deterministic ownership, persistence, recovery, process, and named-pipe coverage passes on native hosted Windows. A live Telegram client remains the only unverified platform boundary and may be exercised in a later release cycle rather than tied to a specific version.
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.24.7: Thread Cleanup Hotfix
|
|
6
|
+
|
|
7
|
+
- `Thread Cleanup`: Renamed the Telegram Settings control from `Auto thread cleanup` to `Thread cleanup` while preserving `threads.automaticCleanup`. Graceful leader/follower teardown now persists exact generation-fenced cleanup intent before deletion and lets the current or successor leader retry until Telegram confirms removal. If a follower's graceful envelope never arrives, cleanup runs only after stale heartbeat, enabled policy, OS-confirmed death of the exact registered PID, serialized exclusion of replacement registration, an unpublished follower endpoint during durable replay, and cancellation of intents superseded by a current replacement binding. Impact: normal and missed Ctrl-C exits remove only the intended tab without turning heartbeat loss, live/unknown process state, reload, or IPC uncertainty into deletion authority.
|
|
8
|
+
- `Prompt Button Feedback`: After acceptance, only the exact selected generated prompt button switches to Telegram's green `success` style while its agent-authored text, emoji, and the other available choices remain unchanged. Owned-message authorization also permits the markup update through follower transport, and visual failure remains non-fatal after queue admission. Impact: Telegram keeps accurate visible evidence of which prompt was sent instead of relying on a brief callback toast.
|
|
9
|
+
- `Dependency Audit Boundary`: The release audit now fails closed over dependencies owned and shipped by `pi-telegram` while omitting host-selected Pi peer packages; a separate full-host audit retains visibility into upstream findings. Removed the expired exception machinery and ineffective consumer overrides against Pi's nested shrinkwrap. Impact: extension releases remain zero-finding for their owned artifact without claiming control over the host's dependency graph.
|
|
10
|
+
|
|
5
11
|
## 0.24.6: README Positioning Hotfix
|
|
6
12
|
|
|
7
13
|
- `README Positioning`: Removed duplicated automatic thread-cleanup detail from the opening and defaults summary while retaining the canonical behavior in the lifecycle controls and configuration documentation. Impact: the package entrypoint stays focused on the companion runtime model without changing graceful-quit behavior or its configurable cleanup policy.
|
package/README.md
CHANGED
|
@@ -183,7 +183,7 @@ Voice notes, audio, images, PDFs, and other media can pass through configured in
|
|
|
183
183
|
|
|
184
184
|
### Buttons And Callbacks
|
|
185
185
|
|
|
186
|
-
Assistant replies can include top-level hidden `telegram_button` comments. The bridge strips the comments from visible text, renders inline buttons, and routes callbacks back into Pi as queued prompts or extension-owned callback actions.
|
|
186
|
+
Assistant replies can include top-level hidden `telegram_button` comments. The bridge strips the comments from visible text, renders inline buttons, and routes callbacks back into Pi as queued prompts or extension-owned callback actions. Once a generated prompt button is accepted, only that exact button switches to Telegram's green success style so the chosen prompt remains visible without altering its agent-authored label or emoji.
|
|
187
187
|
|
|
188
188
|
### Threaded Mode And Multi-Instance Bus
|
|
189
189
|
|
|
@@ -280,7 +280,7 @@ Full validation:
|
|
|
280
280
|
npm run validate
|
|
281
281
|
```
|
|
282
282
|
|
|
283
|
-
`npm run audit`
|
|
283
|
+
`npm run audit` fails closed over dependencies owned and shipped by `pi-telegram`, omitting Pi host packages declared as peers because the host selects and supplies their dependency graph. Use `npm run audit:host` separately to inspect the complete installed development graph, including upstream Pi advisories; host findings remain visible without being misattributed to this extension's release artifact.
|
|
284
284
|
|
|
285
285
|
Project context:
|
|
286
286
|
|
package/docs/architecture.md
CHANGED
|
@@ -149,7 +149,7 @@ Named Telegram profiles are orthogonal to Threaded Mode. The selected profile ch
|
|
|
149
149
|
|
|
150
150
|
Profile reality follows three explicit storage classes. `telegram.json` shared settings and extension registries are process-global platform configuration; `profiles.default` and `profiles.<name>` bot/session fields plus observable transport/routing authority are profile-scoped; queues, active turns, ownership caches, menu state, and runtime controllers are session-local memory. Config persistence serializes cross-process writers and applies each recursive mutation delta to the latest disk snapshot, so named-profile offsets and unrelated global/profile updates do not stale-replace one another. Polling publishes only its monotonic `lastUpdateId` into the current config-store snapshot; it never persists the detached full config object captured at poll start, which would interpret newer Settings fields as deletions. Runtime profile switching follows stop-old/commit-new ordering: reload keeps the selected identity stable, old polling/lock/bus teardown finishes while all dynamic resolvers still point at the old profile, and only then may activation expose the new token, lock key, state path, target namespace, and IPC endpoint. Downloaded attachments use UUID-prefixed names in the shared Telegram scratch directory and are session artifacts rather than identity or routing authority, so cross-profile cleanup is limited to stale scratch files and cannot redirect live traffic.
|
|
151
151
|
|
|
152
|
-
When Threaded Mode is active, the current polling owner is also the Telegram bus leader. The leader owns the local bus endpoint (Unix-domain socket on Unix-like platforms, named pipe on native Windows), polls `getUpdates`, performs direct Bot API calls, records follower heartbeats, prunes stale followers, and provisions Telegram UI thread targets through live runtime/bus state. Follower liveness is intentionally fast because heartbeat traffic is local IPC: followers heartbeat every `1s`, the leader treats them as stale after `2s`, and the prune loop runs every `1s` so stopped followers are detected promptly while active forwarded updates/API calls still refresh liveness. Heartbeat pruning is silent liveness bookkeeping
|
|
152
|
+
When Threaded Mode is active, the current polling owner is also the Telegram bus leader. The leader owns the local bus endpoint (Unix-domain socket on Unix-like platforms, named pipe on native Windows), polls `getUpdates`, performs direct Bot API calls, records follower heartbeats, prunes stale followers, and provisions Telegram UI thread targets through live runtime/bus state. Follower liveness is intentionally fast because heartbeat traffic is local IPC: followers heartbeat every `1s`, the leader treats them as stale after `2s`, and the prune loop runs every `1s` so stopped followers are detected promptly while active forwarded updates/API calls still refresh liveness. Heartbeat pruning is silent liveness bookkeeping and does not send a Telegram-visible disconnected notice, because the common cause may be leader reload or IPC handoff rather than a dead follower. Pruning alone preserves the binding; when Thread cleanup is enabled, only a subsequent OS check that confirms the exact registered PID absent may create fenced cleanup intent, and that cleanup serializes ahead of replacement registration. Successful follower target reuse refreshes the binding's recovery timestamp. Absent follower bindings remain durable restoration hints until explicit stale, deleted, offline, or reconciliation evidence invalidates them; process absence and heartbeat pruning alone do not remove them. If an authenticated live follower carries an exact target that is absent from current bindings, the leader recovers it only behind a synchronous visibility probe: success activates it, explicit stale evidence provisions a replacement, and ambiguous failure rejects registration. A carried slot is restored only when it is not already occupied. `tmp/telegram/logs.jsonl` is a session-local redacted runtime evidence stream for race debugging; it resets on extension start and runtime scope changes, and must not become routing/provisioning authority. `tmp/telegram/state.json` is an extension+bot observable/debug snapshot aligned with status diagnostics: `source: "snapshot"` and `writtenAtMs` mark it as observational, not authoritative. All instances on one Telegram profile read the same snapshot, but only the active transport lock owner persists it; followers become writers only after promotion. Status-only persistence reloads current disk bindings before serialization, preventing a stale follower/status view from erasing newer leader-owned targets. Fresh capability observations may skip redundant startup probes, but stale snapshots re-probe before suppressing bus/thread behavior. Top-level `bot` mirrors bot-wide capabilities such as thread mode, `runtime` describes process role/status, `liveRoster` mirrors followers/current targets/reservations, `diagnostics` mirrors recent status/debug signals including the latest thread-reconciler phase/counts, `threads` stores current routeable bindings, TTL-bounded reservations explain short-lived slot collision guards, and TTL-pruned `pendingProvisions` protects in-flight topic creation slots from cleanup/allocation races. Fresh provisioning writes pending state before the Bot API create call, adds the returned target to the pending record, persists a `starting` binding, then promotes it to `active` and clears pending state. If final binding persistence fails after Telegram returns a thread id, the targeted pending provision remains as cleanup/retry evidence. Once targeted pending provisions expire, they are retained for `thread-reconciler` close/delete cleanup and pending scratchpad removal after a successful cleanup apply; untargeted expired pending records can prune without cleanup because no Telegram thread id exists. Runtime events coalesce status-snapshot writes so transient bus/API/update failures remain inspectable even when the operator has not opened `/telegram-status`. The bridge must not keep a durable `telegram-targets.json` target history; stale/offline/failed thread observations are pruned instead of reused. Previous-process leader bindings that still probe alive become reservations/collision guards, not routeable active threads, so a reloaded leader can take the next free slot without duplicating the same visible tab name. The thread chat is always the private bot DM with the paired owner (`allowedUserId`). In Telegram private-chat Threaded Mode, the leader creates/reuses its own thread before polling — it is a real bound instance, not a dispatcher. Followers authenticate bus envelopes with the leader-minted capability secret stored in the active lock entry. Leader lock entries also carry a stable `leaderEpoch` minted on acquisition and preserved across heartbeat refreshes; leader-owned cleanup/provisioning plans stamp that epoch, and Thread Reconciler apply skips destructive work if leadership has moved on before side effects run. Followers own their own Pi session state, queue, active turns, previews, menus, and lifecycle hooks, but route allowlisted, target-scoped Telegram API calls through the leader. When a follower promotes after heartbeat loss, status/state diagnostics expose only the transient `electing` lifecycle phase; stable `leader`/`follower` identity stays in the bus role so diagnostics do not duplicate role state. The TUI status bar and `/telegram-status` report `leader` or `follower` role so a registered follower is not shown as generically disconnected. Terminal status identity and the `[telegram|thread:name]` prompt label use the same target-aware current-instance resolver: registered local metadata wins over a stale shared binding for the matching target, while the binding remains a fallback for partial metadata.
|
|
153
153
|
|
|
154
154
|
Follower binding is manual and process-first: the operator starts another Pi process, then runs `/telegram-connect`; only then does that process register as a follower with an instance-scoped internal binding identity and cause the leader to create/reuse a thread for it. Telegram does not expose `/thread`, auto-spawn arbitrary unbound threads, or launch hidden follower subprocesses. In Threaded Mode, `/telegram-connect` does not offer manual takeover while a live leader exists; takeover is reserved for stale-leader election/recovery. Leadership remains an ephemeral transport role that another live follower can take over after stale heartbeat detection.
|
|
155
155
|
|
|
@@ -189,8 +189,9 @@ Manual smoke checklist:
|
|
|
189
189
|
3. Start Pi in a second Windows terminal and run `/telegram-connect`; verify it registers as follower rather than offering takeover, creates/uses its assigned thread, terminal status shows `<ThreadName> Follower` while idle, and a follower prompt flips it to `<ThreadName> Active` while work is running.
|
|
190
190
|
4. From the follower thread, send a prompt that requests inline buttons; tap a button and verify the follow-up prompt queues in the follower instance.
|
|
191
191
|
5. From the follower thread, request a voice reply and/or attachment; verify upload routes through the leader transport into the follower thread.
|
|
192
|
-
6. With `🧹
|
|
193
|
-
7.
|
|
192
|
+
6. With `🧹 Thread cleanup` enabled (default), quit the follower Pi normally without an explicit disconnect; verify graceful shutdown deletes its current tab through the leader before local suspension. Repeat with a double `Ctrl+C`; if Pi misses the graceful envelope, verify stale-heartbeat recovery deletes the same exact tab only after the OS confirms that follower PID has exited. Disable the setting, quit another follower normally or abruptly, and verify its tab remains as a restart hint. Reconnect, run `/telegram-disconnect`, confirm the prompt, and verify the leader confirms deletion before local polling stops.
|
|
193
|
+
7. With `🧹 Thread cleanup` enabled, quit the leader normally and verify it deletes only its own tab before releasing transport; a remaining follower may then promote without recreating the deleted leader tab. If deletion is interrupted after intent persistence, start or promote a successor and verify it completes the exact pending cleanup before publishing its follower endpoint or provisioning its own tab; a current replacement binding must instead cancel the superseded cleanup without calling Telegram deletion APIs.
|
|
194
|
+
8. Reload the leader and verify status/debug output does not expose raw pipe internals except in explicit diagnostics.
|
|
194
195
|
|
|
195
196
|
If any step fails, capture `telegram-status --debug`, `tmp/telegram/state.json`, `tmp/telegram/logs.jsonl`, and, after a reload, `tmp/telegram/logs._prev.jsonl`. Debug status prints local leader/follower endpoints with their active transport kind (`pipe` or `socket`), while the runtime log records request-scoped transport failures with envelope kind, request id, retry attempt, endpoint, and classified IPC error. Reloads preserve the prior JSONL log as `logs._prev.jsonl` so the evidence that caused the reload is not immediately overwritten.
|
|
196
197
|
|
|
@@ -232,7 +233,7 @@ In Telegram private-chat Threaded Mode:
|
|
|
232
233
|
- Replies, previews, files, voice, and buttons stay in that thread.
|
|
233
234
|
- Queue controls and reactions affect only that instance target.
|
|
234
235
|
- Telegram's native `…typing` indicator for real agent work is sent to that instance thread and mirrored to `All`; `All` is the aggregate surface and should show activity when any bound instance is running a Telegram turn, local prompt, or autonomous continuation. Terminal `Active` remains Telegram-turn-specific. Startup/connect/reload/recovery must not send activity by themselves.
|
|
235
|
-
- Generic heartbeat pruning remains silent and preserves the thread as a restart hint
|
|
236
|
+
- Generic heartbeat pruning remains silent and preserves the thread as a restart hint. With cleanup enabled, a later exact-PID death confirmation may delete it without posting an `Instance offline` notice.
|
|
236
237
|
- If the same binding identity returns, authenticated registration can reclaim the thread after the required visibility proof.
|
|
237
238
|
|
|
238
239
|
## Bot API Evidence For Private-Chat Threaded Mode
|
|
@@ -381,12 +382,12 @@ All files containing routing, chat ids, thread ids, or process details use priva
|
|
|
381
382
|
- Leader prunes the follower from the live registry after missed heartbeats, but heartbeat pruning is only immediate liveness bookkeeping.
|
|
382
383
|
- A missed heartbeat does not delete, close, mark offline, or send a disconnected notice for the follower's Telegram thread binding because the common cause may be leader reload, IPC handoff, or transient reconnect rather than a dead follower.
|
|
383
384
|
- Followers treat rejected/missing heartbeat acknowledgements as registration loss: retain the last known target locally, clear registered truth, try to re-register with the current leader, wait a short leader-reload grace window, and retry. They promote only after the exact leader lease becomes stale or inactive; a live owner with an unreachable endpoint leaves the follower disconnected/retrying rather than creating a competing poller.
|
|
384
|
-
- Persisted current manual-follower bindings survive abrupt process absence as restoration hints
|
|
385
|
+
- Persisted current manual-follower bindings survive abrupt process absence as restoration hints when Thread cleanup is disabled. When enabled, graceful Pi quit requests exact-generation teardown before lifecycle suspension; if that envelope is missed, stale pruning may delete only after the leader's OS confirms the exact registered PID has exited.
|
|
385
386
|
- Fresh registration sends a compact connected notice in the assigned thread; cross-session restoration uses that same notice as the visibility probe and follows the stale/ambiguous recovery contract defined above.
|
|
386
387
|
- Registration requires a present generation, and explicit disconnect requires that same exact live generation. Leader-side registration and disconnect mutations serialize per durable follower profile across old and replacement runtime instance IDs, so a replacement registration cannot overtake awaited destructive cleanup and an old disconnect cannot remove its successor's routing authority.
|
|
387
388
|
- Successful forwarded updates and follower-originated API calls refresh liveness, so active followers are not pruned only because the interval heartbeat tick lagged.
|
|
388
|
-
- Destructive follower thread teardown belongs to confirmed `/telegram-disconnect`, graceful Pi quit, or confirmed reconciliation actions, not generic heartbeat pruning. Manual disconnect retains its destructive confirmation; quit performs the same action without prompting when
|
|
389
|
-
- Explicit stale/deleted/offline observations invalidate reuse
|
|
389
|
+
- Destructive follower thread teardown belongs to confirmed `/telegram-disconnect`, graceful Pi quit, or confirmed reconciliation actions, not generic heartbeat pruning. Manual disconnect retains its destructive confirmation; quit performs the same action without prompting when Thread cleanup is enabled (default). Confirmed leader/follower teardown first persists an exact target/runtime-generation cleanup intent. The active leader attempts deletion under its current epoch; interruption preserves the intent so that leader or a successor can replay it under current authority, and confirmed deletion removes the binding plus intent in the same persisted state transition. If the graceful request is missed, stale heartbeat plus OS-confirmed absence of the exact registered PID may authorize the same cleanup while enabled; this action serializes ahead of replacement registration. Disabled cleanup, silence, heartbeat expiry alone, IPC/auth failure, and live or unknown process liveness remain non-destructive. Incomplete cleanup preserves durable intent for retry. A promoted leader uses its current owned leader epoch even when the inherited record still carries a historical `manual-follower` owner label.
|
|
390
|
+
- Explicit stale/deleted/offline observations invalidate reuse. Process absence affects reuse only through the enabled, exact-PID confirmed-dead cleanup path.
|
|
390
391
|
|
|
391
392
|
### Thread is deleted
|
|
392
393
|
|
package/docs/outbound.md
CHANGED
|
@@ -147,6 +147,7 @@ Rules:
|
|
|
147
147
|
- Keep the canonical body form as `<!-- telegram_button label="Label"` + body + `-->`; closed heads must use `prompt="..."` or the colon shorthand to create a button.
|
|
148
148
|
- Use one block per button; this mirrors HTML's singular element model and avoids a nested button DSL inside comments.
|
|
149
149
|
- Button actions are stored in memory with short `callback_data`; Telegram never sees the full prompt in the button payload.
|
|
150
|
+
- After Telegram accepts a generated button callback as a queued prompt, the bridge changes that exact button to Telegram's green `success` style without changing its agent-authored text or emoji. Other choices stay visually unchanged and remain available; the callback acknowledgement remains the fallback on clients that do not render button styles.
|
|
150
151
|
|
|
151
152
|
Do not emit JSON button specs, inline comments after visible text, standalone button actions, or tool calls for ordinary Telegram-turn buttons. The agent writes Markdown plus hidden comments; the bridge strips comments and attaches Telegram `reply_markup` after `agent_end`. For local/TUI-originated direct sends, put the same Markdown and `telegram_button` comments in `telegram_message(text)`.
|
|
152
153
|
|
package/docs/public-api.md
CHANGED
|
@@ -116,7 +116,7 @@ The file is global across Pi instances. Cooperating instances serialize recursiv
|
|
|
116
116
|
|
|
117
117
|
Hidden/default semantics are represented by absence:
|
|
118
118
|
|
|
119
|
-
- `threads.automaticCleanup` defaults to `true`; graceful Pi quit deletes the instance's bound Threaded Mode tab without prompting. Set it to `false`, or use `🧹
|
|
119
|
+
- `threads.automaticCleanup` defaults to `true`; graceful Pi quit deletes the instance's bound Threaded Mode tab without prompting. Set it to `false`, or use `🧹 Thread cleanup` in Telegram Settings, to preserve the tab as a restart hint. Settings views and cleanup reload shared config before evaluating this switch, so another live Pi instance's update takes effect without restarting. Confirmed leader/follower teardown persists an exact target/runtime-generation cleanup intent before Telegram deletion; an interrupted attempt remains retryable by the current or successor leader under current authority and clears only after confirmed deletion. If a follower's graceful envelope is missed, the leader may create the same fenced cleanup only after its heartbeat is stale, the OS confirms the exact registered PID no longer exists, cleanup remains enabled, and no replacement registration can overtake deletion. Heartbeat loss alone, live/unknown process liveness, IPC failure, and auth failure remain non-destructive. Invalid-config recovery makes the setting unresolved and therefore skips destructive cleanup. Manual `/telegram-disconnect` keeps its confirmation and teardown behavior regardless of this setting.
|
|
120
120
|
- `assistant.proactivePush` defaults to `true`; omit it to keep projection enabled, or set it explicitly to `false` to disable it. When enabled, each completed public assistant text block from local or autonomous work is projected to the authorized Telegram target once and in source order. This includes visible intermediate commentary/checkpoints and the final block. It excludes token deltas, hidden reasoning, tool calls/arguments/results, Telegram-owned turns, empty blocks, and stale authority. Projection uses the configured Rich or HTML assistant renderer and binds admitted work to the exact target, profile/token transport generation, direct leader epoch or follower registration generation, and session generation. The old top-level `proactivePush` key is ignored; move the setting manually under `assistant`.
|
|
121
121
|
- Voice Reply `hidden`: no `voice.replyMode` key is persisted; legacy `manual` resolves to this silent default. `mirror` adds `[voice] delivery: automatic voice` only to voice/audio-input turns, while `always` adds the same effective line to every Telegram turn.
|
|
122
122
|
- Agent activity status is not configurable in this release. Telegram uses native `sendChatAction(typing)` / product `...active` status as the only automatic in-chat work signal before the final reply.
|
package/docs/ui-style.md
CHANGED
|
@@ -175,6 +175,10 @@ Examples:
|
|
|
175
175
|
- `🟢 mirror`
|
|
176
176
|
- `always`
|
|
177
177
|
|
|
178
|
+
## Generated Prompt Buttons
|
|
179
|
+
|
|
180
|
+
Assistant-generated prompt buttons use the default app style before selection. After queue admission, edit only the selected button to Telegram's `success` style; preserve its agent-authored text and emoji, and leave other choices at their default style. The callback acknowledgement remains the compatibility fallback when a client does not render button styles.
|
|
181
|
+
|
|
178
182
|
## Navigation
|
|
179
183
|
|
|
180
184
|
Inline submenu navigation is hierarchical.
|
package/index.ts
CHANGED
|
@@ -741,6 +741,13 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
741
741
|
stopTypingLoop: typing.stop,
|
|
742
742
|
answerCallbackQuery,
|
|
743
743
|
editInteractiveMessage,
|
|
744
|
+
async editMessageReplyMarkup(chatId, messageId, replyMarkup) {
|
|
745
|
+
await callTelegramApi("editMessageReplyMarkup", {
|
|
746
|
+
chat_id: chatId,
|
|
747
|
+
message_id: messageId,
|
|
748
|
+
reply_markup: replyMarkup,
|
|
749
|
+
});
|
|
750
|
+
},
|
|
744
751
|
sendInteractiveMessage,
|
|
745
752
|
deleteMessage: deleteTelegramMessage,
|
|
746
753
|
answerGuestQuery,
|
|
@@ -878,6 +885,9 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
878
885
|
startPolling: pollingRuntime.start,
|
|
879
886
|
stopPolling: pollingRuntime.stop,
|
|
880
887
|
authorizeFollowerApiCall,
|
|
888
|
+
isFollowerProcessAlive: Locks.isProcessAlive,
|
|
889
|
+
shouldCleanupConfirmedDeadFollower:
|
|
890
|
+
configControls.resolveAutomaticThreadCleanupEnabled,
|
|
881
891
|
recordFollowerMessageOwnership(record) {
|
|
882
892
|
messageOwnershipRuntime.recordFollower(record);
|
|
883
893
|
},
|