@llblab/pi-telegram 0.11.2 → 0.12.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.
@@ -1,304 +1,239 @@
1
1
  # Telegram Bridge Architecture
2
2
 
3
- ## Overview
3
+ ## Purpose
4
4
 
5
- `pi-telegram` is a session-local π extension that binds one Telegram DM to one running π session. The bridge owns four main responsibilities:
5
+ `pi-telegram` is a session-local π extension that binds one Telegram DM to one running π session. It owns the Telegram bridge boundary:
6
6
 
7
- - Poll Telegram updates and enforce single-user pairing
8
- - Translate Telegram messages and media into π inputs
9
- - Stream and deliver π responses back to Telegram
10
- - Manage Telegram-specific controls such as queue reactions, π prompt-template commands, `/start` application menu sections, `/compact`, `/next`, `/abort`, and `/stop`
7
+ - Poll Telegram updates and enforce single-user pairing.
8
+ - Translate Telegram text, callbacks, media, and files into π turns.
9
+ - Stream previews and deliver final π responses back to Telegram.
10
+ - Provide Telegram-native controls for queueing, model/thinking/settings menus, compaction, abort/stop, prompt templates, reactions, and outbound artifacts.
11
11
 
12
- ## Runtime Structure
12
+ This document is the architectural map. Focused behavior standards live in sibling docs:
13
13
 
14
- `index.ts` remains the extension entrypoint and composition root. Reusable runtime logic is split into flat domain files under `/lib` rather than into a deep local module tree.
14
+ - [Public API](./public-api.md) stable commands, config, package entrypoints, assistant markup, extension APIs, and compatibility boundaries.
15
+ - [UI Style](./ui-style.md) — inline UI labels, navigation, state markers, cards, and dialogs.
16
+ - [Callback Namespaces](./callback-namespaces.md) — callback prefix ownership and fallback rules.
17
+ - [Sections](./sections.md) — structured Telegram menu sections.
18
+ - [Updates](./updates.md) — update classification, default-routing plans, and raw Telegram update interception.
19
+ - [Voice Integration](./voice.md) — voice reply policy and STT/TTS provider surface.
20
+ - [Command Templates](./command-templates.md) — shell-free command-template contract.
15
21
 
16
- Architecture shorthand: this repository uses a `Flat Domain DAG`: cohesive bridge domains live as flat `/lib/*.ts` modules, local imports must form a directed acyclic graph, shared buckets are avoided, and `index.ts` wires live π/Telegram ports plus session state. Source-module opening comments include `Zones:` tags such as `telegram`, `pi agent`, `tui`, or `shared utils` so cross-cutting responsibility areas stay visible without folder nesting.
22
+ ## Runtime Topology
17
23
 
18
- Domain grouping rule: prefer cohesive domain files over atomizing every helper into its own file. A `shared` domain is allowed only for types or constants that genuinely span multiple bridge domains.
24
+ `index.ts` is the only composition root. It wires live π ports, Telegram Bot API ports, session-local stores, lifecycle hooks, and domain runtimes. Reusable logic lives in flat `/lib/*.ts` domain modules rather than a deep local module tree.
19
25
 
20
- Interface consistency rule: when two modules mean the same runtime entity, they should converge on the owning domain's exported contract. Local structural `*Like` or view contracts are appropriate only when a domain intentionally needs a narrow projection to avoid unnecessary coupling; they should not become duplicate source-of-truth shapes for the same entity.
26
+ The repository uses a **Flat Domain DAG**:
21
27
 
22
- Naming rule: because the repository already scopes this codebase to Telegram, extracted module and test filenames use bare domain names such as `api.ts`, `queue.ts`, `updates.ts`, and `queue.test.ts` rather than repeating `telegram-*` in every filename.
28
+ - Local imports must form a directed acyclic graph.
29
+ - Cohesive domain files are preferred over atomizing every helper.
30
+ - Shared buckets such as `lib/constants.ts` or `lib/types.ts` are avoided.
31
+ - Constants and state types live with their owning domain.
32
+ - Narrow structural projections are allowed when they avoid importing broader runtime or wire DTOs.
33
+ - Source file headers include `Zones:` tags so cross-cutting responsibility stays visible without folder nesting.
23
34
 
24
- Current runtime areas use these ownership boundaries:
35
+ ### Domain Ownership Map
25
36
 
26
- - `index.ts`: single composition root for live π/Telegram ports, session state, API-bound transport adapters, and status updates.
27
- - `api`: Bot API transport shapes/helpers, retries, file download, temp-dir lifecycle, inbound limits, chat actions, lazy bot-token clients, runtime error recording, and the `TELEGRAM_API_BASE` constant for the Bot API endpoint.
28
- - `config` / `setup`: persisted bot/session pairing state, authorization, first-user pairing, token prompting, env fallback, validation, and config persistence.
29
- - `locks` / `polling`: singleton `locks.json` ownership, takeover/restart semantics, long-poll controller state, update offset persistence, and poll-loop runtime wiring.
30
- - `updates` / `routing`: update classification/execution planning, paired authorization, reactions, edits, callbacks, and inbound route composition.
31
- - `media` / `text-groups` / `time-injection` / `turns` / `inbound-handlers`: text/media extraction, media-group debounce, long-text split coalescing, optional per-chat wall-clock prompt context, inbound downloads, configured and programmatic inbound text/media handler execution, turn building/editing, image reads, and legacy `attachmentHandlers` compatibility.
32
- - `queue`: queue item contracts, lane admission/order, stores, mutations, dispatch readiness/runtime, prompt/control enqueueing, and session/agent/tool lifecycle sequencing.
33
- - `runtime`: session-local coordination primitives: counters, lifecycle flags, setup guard, abort handler, typing-loop timers, prompt-dispatch flags, and agent-end reset binding.
34
- - `model` / `menu-model` / `menu-thinking` / `menu-status` / `menu` / `menu-queue` / `menu-settings` / `commands`: model identity/thinking levels, scoped model resolution, in-flight switching, model/thinking/status/queue/settings menu UI, inline application callback composition, slash commands, and bot command registration.
35
- - `extension-sections`: structured external Telegram menu sections registered by ordinary pi extensions; owns section registry, compact section callback tokens, section render/callback dispatch, safe section runtime ports, and diagnostics.
36
- - `keyboard`: shared Telegram inline-keyboard reply-markup structure; feature domains own callback semantics and button construction.
37
- - `preview` / `replies` / `rendering`: preview lifecycle/transports, final reply delivery and reply parameters, Telegram HTML Markdown rendering, chunking, and stable-preview snapshots.
38
- - `outbound-handlers`: outbound text transformation, assistant-authored outbound comments, generated reply artifacts, inline-keyboard callbacks, and post-`agent_end` outbound action delivery.
39
- - `outbound-attachments`: `telegram_attach` registration, outbound attachment queueing, stat/limit checks, and photo/document delivery classification.
40
- - `status`: status-bar/status-message rendering, queue-lane status views, redacted runtime event ring, and grouped π diagnostics.
41
- - `lifecycle` / `prompts` / `prompt-templates` / `pi`: π hook registration, Telegram-specific before-agent prompt injection, π prompt-template discovery/expansion, and centralized direct pi SDK imports/context adapters.
42
- - `command-templates`: portable shell-free command-template standard helpers, composition expansion, placeholder substitution, and executable resolution.
37
+ - `index.ts`: composition root for live ports, session state, transport adapters, and lifecycle registration.
38
+ - `api`: Bot API helpers, retries, uploads/downloads, temp cleanup, byte limits, chat actions, lazy token clients, and API error recording.
39
+ - `config` / `setup`: `telegram.json`, bot token setup, first-user pairing, authorization, env fallback, atomic persistence, and live config accessors.
40
+ - `locks` / `polling`: singleton polling ownership, takeover/restart behavior, long-poll controller state, offset persistence, and poll-loop wiring.
41
+ - `updates` / `routing`: update classification, authorization planning, callbacks, edited messages, reactions, and inbound route composition.
42
+ - `media` / `text-groups` / `time-injection` / `turns` / `inbound`: inbound text/media/file extraction, media-group debounce, long-text coalescing, optional `[time]` context, handler execution, and prompt-turn assembly/editing.
43
+ - `queue`: queue item contracts, lane admission/order, readiness gates, mutations, dispatch runtime, prompt/control enqueueing, and session/agent/tool lifecycle sequencing.
44
+ - `runtime`: session-local coordination primitives: counters, flags, setup guard, abort handler, typing timers, dispatch flags, and reset binding.
45
+ - `model` / `menu-model` / `menu-thinking` / `menu-status` / `menu-queue` / `menu-settings` / `menu` / `commands`: model identity, thinking levels, scoped model handling, menu render/callback behavior, slash commands, bot commands, and interactive controls.
46
+ - `sections`: Telegram menu-section registry, opaque section callback tokens, render/callback dispatch, safe section ports, and diagnostics.
47
+ - `keyboard`: shared inline-keyboard reply-markup shape only; feature domains own labels, callback data, and behavior.
48
+ - `preview` / `replies` / `rendering`: streaming preview lifecycle, final reply delivery, reply parameters, Telegram HTML rendering, chunking, and stable preview snapshots.
49
+ - `outbound`: outbound text transformations, assistant-authored action comments, voice/button artifacts, and generated callback actions.
50
+ - `outbound-attachments`: `telegram_attach`, queued outbound files, stat/limit checks, and photo/document delivery classification.
51
+ - `status`: status bar/status-message rendering, queue-lane summaries, redacted event ring, and grouped diagnostics.
52
+ - `lifecycle` / `prompts` / `prompt-templates` / `pi`: π hook registration, Telegram prompt guidance, prompt-template discovery/expansion, and centralized direct π SDK imports.
53
+ - `command-templates`: shell-free command-template helpers, composition expansion, placeholder substitution, executable resolution, warnings, and retry/timeout semantics.
43
54
 
44
- Boundary invariants:
55
+ ### Guarded Invariants
45
56
 
46
- - Constants and state types live with their owning domains; do not reintroduce shared buckets such as `lib/constants.ts` or `lib/types.ts`
47
- - Shared Telegram inline-keyboard structure belongs to `keyboard`; application-control labels, callback data, and callback behavior stay in `menu`/`menu-model`/`menu-thinking`/`menu-status`/`menu-queue`; external section labels, callbacks, and dispatch stay in `extension-sections`; core queue mechanics stay in `queue`
48
- - Domain helpers use narrow structural projections when that avoids importing concrete wire DTOs or broader runtime objects unnecessarily
49
- - Preview appearance stays in `rendering`; preview transport/lifecycle stays in `preview`
50
- - Direct `node:*` file-operation imports stay in owning domains, not in `index.ts`
51
- - `index.ts` uses namespace imports for local bridge domains so orchestration reads as `Queue.*`, `Turns.*`, and `Rendering.*`
52
- - Architecture-invariant tests guard the acyclic import graph, pi SDK centralization, entrypoint purity, runtime-domain isolation, structural leaf-domain isolation, menu/model boundaries, API/config separation, media/update/API separation, and outbound-attachment boundary isolation
53
- - Mirrored domain regression coverage lives in `/tests/*.test.ts`; test helpers stay local to the mirrored suite by default, and shared fixture folders are justified only by reuse across multiple domain suites
57
+ Architecture invariant tests protect:
54
58
 
55
- ## Configuration UX
59
+ - Acyclic local imports.
60
+ - Direct π SDK imports centralized in the `pi` adapter.
61
+ - `index.ts` as a composition root without local runtime adapter logic.
62
+ - Runtime state isolation from local domain imports.
63
+ - Structural leaf-domain isolation.
64
+ - Menu/model boundary direction.
65
+ - API/config separation.
66
+ - Media/update/API decoupling.
67
+ - Outbound attachment isolation from queue, inbound media, and API helpers.
56
68
 
57
- `/telegram-setup` uses a progressive-enhancement flow for the bot token prompt:
69
+ Mirrored domain regressions live in `/tests/*.test.ts`. Shared test fixtures should exist only when multiple suites genuinely reuse them.
58
70
 
59
- 1. Show the locally saved token from `~/.pi/agent/telegram.json` when one already exists
60
- 2. Otherwise use the first configured environment variable from the supported Telegram token list
61
- 3. Fall back to the example placeholder when no real value exists
71
+ ## Configuration And Ownership
62
72
 
63
- Because `ctx.ui.input()` only exposes placeholder text, the bridge uses `ctx.ui.editor()` whenever a real default value must appear already filled in. The persisted `telegram.json` config is written through a private temp file plus atomic rename, then left with `0600` permissions because it contains the bot token.
73
+ Telegram configuration lives in `~/.pi/agent/telegram.json`. Polling ownership lives separately in `~/.pi/agent/locks.json` under `@llblab/pi-telegram`.
64
74
 
65
- ## Runtime Ownership
75
+ ### Setup Flow
66
76
 
67
- Telegram bot configuration stays in `~/.pi/agent/telegram.json`. Singleton runtime ownership lives separately in `~/.pi/agent/locks.json` under `@llblab/pi-telegram`.
77
+ `/telegram-setup` progressively resolves the bot token:
68
78
 
69
- Ownership lifecycle:
79
+ 1. Use the locally saved token when present.
80
+ 2. Otherwise use the first supported Telegram token environment variable.
81
+ 3. Otherwise show the example placeholder.
70
82
 
71
- - `/telegram-connect` acquires or moves the singleton lock before polling starts.
72
- - `/telegram-disconnect` stops polling and releases the lock.
73
- - Session start resumes polling when the existing lock already points at the current `pid`/`cwd`.
74
- - After a full π process restart, session start may replace a stale lock from the same `cwd` and resume polling automatically.
75
- - Session start does not create new ownership from an inactive lock, a live external lock, or a stale lock from another directory.
76
- - Session replacement suspends polling and ownership watchers without releasing the lock, allowing the next session-start hook in the same `pid`/`cwd` to resume from explicit ownership.
77
- - When a live external owner exists, `/telegram-connect` asks whether to move singleton ownership to the current π instance.
83
+ `ctx.ui.input()` only supports placeholder text, so setup uses `ctx.ui.editor()` when a real default must appear already filled in. Persisted config is written through a private temp file plus atomic rename and left with `0600` permissions.
78
84
 
79
- Active owners poll the lock through a snapshotted ownership context. Long-lived timers therefore avoid stale π contexts after `/new`; they stop local polling when `locks.json` no longer points at their own `pid`/`cwd`, without deleting the new owner lock. Deleting `locks.json` resets runtime ownership without deleting Telegram configuration.
85
+ ### Runtime Ownership
80
86
 
81
- ## Message And Queue Flow
87
+ - `/telegram-connect` acquires or moves singleton ownership before polling starts.
88
+ - `/telegram-disconnect` stops polling and releases ownership.
89
+ - Session start resumes polling only when the existing lock already points at the current `pid`/`cwd`, or when a stale same-`cwd` lock can be safely replaced after process restart.
90
+ - Session replacement suspends polling/watchers without releasing ownership so the next session-start hook in the same process can resume.
91
+ - Live polling owners require explicit takeover confirmation.
92
+ - Long-lived timers use snapshotted ownership context and stop local polling when the lock no longer points at their own process.
82
93
 
83
- ### Inbound Path
94
+ Deleting `locks.json` resets runtime ownership without deleting Telegram configuration.
84
95
 
85
- 1. Telegram updates are polled through `getUpdates`
86
- 2. Each update offset is persisted only after the update handler succeeds; repeated handler failures are bounded so one poisoned update cannot stall polling forever
87
- 3. The bridge filters to the paired private user
88
- 4. Media groups are coalesced into a single Telegram turn when needed
89
- 5. Slash command parsing uses only the new message text/caption, while Telegram `reply_to_message` text/caption is injected later as prompt-only `[reply]` context for normal queued turns
90
- 6. Files are streamed into `~/.pi/agent/tmp/telegram` with a default 50 MiB size limit, partial-download cleanup on failures, and stale temp cleanup on session start; operators can tune the limit with `PI_TELEGRAM_INBOUND_FILE_MAX_BYTES` or `TELEGRAM_MAX_FILE_SIZE_BYTES`
91
- 7. Configured inbound handlers may run on raw text or downloaded files by MIME wildcard, Telegram attachment type, or generic match selector; command templates receive safe command-arg substitution for `{text}`, `{file}`, `{mime}`, and `{type}` where applicable
92
- 8. Matching media/file handlers are tried in config order: a non-zero exit records diagnostics and falls back to the next matching handler, while the first successful handler stops the chain
93
- 9. Local attachments stay visible under `[attachments] <directory>` with relative file entries, and handler stdout is appended under `[outputs]` before the agent sees the turn; failed handlers omit output while keeping the attachment entry
94
- 10. Optional `time` config may add a compact final `[time]` prompt line after attachment/output/voice sections, either every turn or per-chat after the configured millisecond interval, using the system timezone
95
- 11. A `PendingTelegramTurn` is created and queued locally
96
- 12. Telegram `edited_message` updates are routed separately and update a matching queued turn when the original message has not been dispatched yet
97
- 13. The queue dispatcher sends the turn into π only when dispatch is safe
96
+ ## Core Flows
98
97
 
99
- ### Queue Safety Model
98
+ ### Inbound Turn Flow
100
99
 
101
- The bridge keeps its own Telegram queue and does not rely only on π's internal pending-message state.
100
+ 1. Poll updates through `getUpdates`.
101
+ 2. Persist update offsets only after successful handling; repeated handler failures are bounded.
102
+ 3. Filter to the paired private user.
103
+ 4. Dispatch owned callbacks and controls before fallback prompt forwarding.
104
+ 5. Coalesce media groups and likely split long text when needed.
105
+ 6. Download files into `~/.pi/agent/tmp/telegram` with size limits and partial-download cleanup.
106
+ 7. Run configured/programmatic inbound handlers in order, appending successful stdout under `[outputs]`.
107
+ 8. Add local attachments under `[attachments]`, optional voice context, and optional final `[time]` context.
108
+ 9. Build a `PendingTelegramTurn` and append it to the bridge queue.
109
+ 10. Handle `edited_message` updates separately while the original turn is still queued.
110
+ 11. Dispatch only when all safety gates are clear.
102
111
 
103
- Queued items now use two explicit dimensions:
112
+ Long-text split recovery is intentionally conservative: only human text at or above the near-limit threshold opens the debounce window; commands, bots, captions, media groups, and normal short follow-ups bypass it.
104
113
 
105
- - `kind`: prompt vs control
106
- - `queueLane`: control vs priority vs default
114
+ ### Queue And Dispatch Safety
107
115
 
108
- Admission contract:
116
+ The bridge keeps its own Telegram queue. Queue items have two explicit dimensions:
109
117
 
110
- - Immediate execution: `/compact`, `/queue`, `/stop`, `/help`, and `/start` do not enter the Telegram queue. `/help` opens the same menu as `/start`; `/stop` also clears queued items. Dispatch rank: N/A.
111
- - Queued prompt command: `/continue` enqueues a priority Telegram-owned `continue` prompt. Prompt-template commands such as `/template_name args` expand the matching π template before entering the normal prompt queue. Dispatch rank: priority for `/continue`, otherwise default.
112
- - Control queue: model-switch continuation turns and future deferred controls use `queueLane: control`, accept control items and continuation prompts, and dispatch at rank `0`.
113
- - Priority prompt queue: a waiting prompt promoted by `👍`, `⚡️`, `❤️`, `🕊`, or `🔥` uses `kind: prompt`, `queueLane: priority`, and dispatches at rank `1`.
114
- - Default prompt queue: normal Telegram text/media turns use `kind: prompt`, `queueLane: default`, and dispatch at rank `2`.
118
+ - `kind`: `prompt` or `control`.
119
+ - `queueLane`: `control`, `priority`, or `default`.
115
120
 
116
- The command action itself carries its execution mode. The queue domain exposes lane contracts for admission mode, dispatch rank, and allowed item kinds.
121
+ Dispatch rank:
117
122
 
118
- Queue validation rules:
123
+ 1. `control` lane.
124
+ 2. `priority` prompt lane.
125
+ 3. `default` prompt lane.
119
126
 
120
- - Queue append and planning paths validate lane admission.
121
- - Malformed control/default or other invalid lane pairings fail predictably instead of silently changing priority.
122
- - Synthetic control actions and Telegram prompts share one stable ordering model while still rendering distinctly in status output.
127
+ Admission and planning validate lane contracts. Invalid lane/kind pairings fail predictably instead of being silently coerced.
123
128
 
124
- Status rendering rules:
129
+ Dispatch requires:
125
130
 
126
- - Busy labels distinguish `active`, `dispatching`, `queued`, `tool running`, `model`, and `compacting`.
127
- - Priority prompts and priority control items are marked with `⚡`.
128
- - If a queue mutation removes the last waiting item while Telegram-owned work still has running tools, the status remains yellow `active` instead of degrading to green `connected`.
131
+ - No active Telegram turn.
132
+ - No pending Telegram dispatch already sent to π.
133
+ - No compaction in progress.
134
+ - `ctx.isIdle()` is true.
135
+ - `ctx.hasPendingMessages()` is false.
129
136
 
130
- A dispatched prompt remains in the queue until `agent_start` consumes it. That keeps the active Telegram turn bound correctly for previews, attachments, abort handling, and final reply delivery.
137
+ A dispatched prompt remains queued until `agent_start` consumes it. This keeps the active Telegram turn bound for previews, attachments, aborts, and final replies.
131
138
 
132
- Dispatch is gated by:
139
+ Post-agent-end queue dispatch uses a session-bound deferred dispatcher. It is activated on session start, clears timers on shutdown, and skips callbacks from older generations before touching `ExtensionContext`.
133
140
 
134
- - No active Telegram turn
135
- - No pending Telegram dispatch already sent to π
136
- - No compaction in progress
137
- - `ctx.isIdle()` being true
138
- - `ctx.hasPendingMessages()` being false
141
+ ### Controls And Menus
139
142
 
140
- These gates prevent queue races around rapid follow-ups, `/compact`, and mixed local plus Telegram activity.
143
+ Telegram controls execute through command/callback domains, not by entering the normal prompt queue unless they intentionally create a prompt turn.
141
144
 
142
- Post-agent-end dispatch retries use a session-bound deferred dispatcher:
145
+ Immediate controls:
143
146
 
144
- - It activates on session start.
145
- - It cancels timers on session shutdown.
146
- - It skips callbacks from older generations before they touch `ExtensionContext`.
147
+ - `/start` opens the main inline application menu.
148
+ - `/model`, `/thinking`, `/queue`, and `/settings` are hidden shortcuts to menu sections.
149
+ - `/compact` opens an inline confirmation dialog and then runs compaction when the bridge is idle.
150
+ - `/next` dispatches the next queued turn, aborting π first when needed.
151
+ - `/abort` aborts the active Telegram-owned run while preserving queued items.
152
+ - `/stop` aborts and clears waiting Telegram queue items.
147
153
 
148
- Telegram `/start` and hidden compatibility shortcuts `/status`, `/model`, `/thinking`, `/queue`, and `/settings` execute immediately. The dispatch controller still serializes deferred control items so a queued control action must settle before the next queued action can dispatch.
154
+ Queued controls:
149
155
 
150
- ### Application Menu Shape
156
+ - `/continue` creates a priority Telegram-owned `continue` prompt.
157
+ - Prompt-template commands expand Telegram-safe π template aliases before entering the prompt queue.
158
+ - Model-switch continuation uses the control lane when an in-flight Telegram-owned run must be stopped and resumed.
151
159
 
152
- `/start` opens the main application menu. It contains visible command help, compact command-only prompt-template rows when π exposes Telegram-compatible prompt-template names, status rows (`Status`, `Usage`, `Cost`, `Context`), and top-level buttons for model, thinking, and queue sections.
160
+ UI label, navigation, tab, toggle, card, and dialog rules are defined in [UI Style](./ui-style.md). Callback prefix ownership is defined in [Callback Namespaces](./callback-namespaces.md).
153
161
 
154
- Menu rules:
162
+ ### Compaction And Typing Status
155
163
 
156
- - The `Status` row reports `compacting` while a Telegram `/compact` run is active, and the bridge sends Telegram's native `typing` chat action as a keepalive for the same compaction window.
157
- - The Queue button includes the current queued-item count.
158
- - Hidden compatibility shortcuts `/help`, `/status`, `/model`, `/thinking`, and `/queue` jump directly to their corresponding menu screens.
159
- - `/settings` opens the hidden settings menu for bridge toggles such as proactive push, voice reply mode, and `time.injectionMode`.
160
- - Settings options open detail submenus. Boolean settings use Back plus green/black/yellow `on` and `off` controls; list-like settings such as time injection use explicit mode names like `hidden`, `always`, and `interval`.
161
- - Command emoji come from the `commands` domain map so visible command descriptions and matching menu buttons share one fixed adornment source.
162
- - Prompt-template commands use a fixed `🧩` marker, map π template names to Telegram-safe aliases such as `fix-tests` → `/fix_tests`, stay visible only inside the `/start` menu, and expand before queueing because `ExtensionAPI.sendUserMessage()` bypasses π prompt-template expansion for extension-originated messages.
164
+ Manual `/compact` requires inline confirmation because accidental taps are disruptive. Auto-compaction and confirmed manual compaction both:
163
165
 
164
- Navigation and ownership:
166
+ - Set the bridge compaction flag.
167
+ - Block queued prompt dispatch.
168
+ - Update status to `compacting`.
169
+ - Start Telegram native `typing` keepalive.
170
+ - Stop typing on compact completion, timeout fallback, or session shutdown.
165
171
 
166
- - Every submenu starts with a top Back row so navigation stays anchored near the original user message above the inline keyboard.
167
- - Model-menu pagination controls sit near the top; tapping the pagination indicator opens a compact page picker headed by `<b>Choose a page:</b>`.
168
- - Tapping a model opens a detail submenu with Back, ☑️ Activate/🟢 Active selection, and yellow/black-marked Scoped/All membership tabs.
169
- - `model` owns core model identity/switching semantics.
170
- - `menu-model` owns model-menu state, scoped model pages, model detail rendering, scoped-list persistence planning, and model-menu rendering.
171
- - `menu-thinking` owns thinking-menu text, reply markup, callback handling, and message rendering.
172
- - `menu-status` owns status-menu payloads, status callback handling, and status-message rendering.
173
- - `menu-queue` owns queue-menu UI only.
172
+ During active Telegram-owned turns, assistant message start/update hooks re-arm typing so transient provider/model errors do not leave a continuing run without Telegram activity feedback.
174
173
 
175
- Queue menu rendering:
174
+ ### Rendering And Delivery
176
175
 
177
- - Queue items render under a compact `<b>Queue:</b>` heading, top-to-bottom in dispatch order.
178
- - Items are numbered and marked with `⚡` for priority prompts or `📎` for prompts with attachments.
179
- - An empty queue renders bold message text with the bottom-filled `⌛` hourglass plus the top Main menu button.
180
- - Non-empty queue states keep the running `⏳` hourglass.
181
- - Selecting an item opens a submenu with the queue item number, full queued prompt text, Back, side-by-side Priority/Normal tabs, and Cancel.
182
- - If a callback targets an item that has already left the queue, the menu refreshes the list instead of applying a stale mutation.
176
+ Telegram replies are rendered as Telegram HTML, not raw Markdown. The renderer is Telegram-specific and regression-prone.
183
177
 
184
- ### Abort Behavior
178
+ Key guarantees:
185
179
 
186
- When `/stop` runs from Telegram, it clears pending model-switch state, clears every waiting Telegram queue item, resets aborted-turn history preservation, and then aborts the active Telegram turn when an abort handler exists. This intentionally favors recovery over preservation: priority/default/control queue items are dropped so the next Telegram message can enter a clean queue and dispatch like a fresh TUI prompt after an interrupted run.
180
+ - Real code blocks stay literal and escaped.
181
+ - Supported absolute links stay clickable; unsupported links degrade safely.
182
+ - Markdown tables render as compact monospace blocks and count grapheme/display width.
183
+ - Lists, task lists, quotes, headings, and blank-line spacing have Telegram-specific preservation rules.
184
+ - Long replies are chunked below Telegram limits with balanced HTML where possible.
185
+ - Streaming previews prefer stable rich blocks and append the unstable tail conservatively as readable plain text.
186
+ - Preview flushes are serialized so older edits cannot race newer snapshots.
187
187
 
188
- ## Rendering Model
188
+ Final delivery attaches reply metadata only where requested. Reply parameters apply only to the first chunk of split messages; continuation chunks are adjacent normal messages. Media-group turns reply to the representative message id.
189
189
 
190
- Telegram replies are rendered as Telegram HTML rather than raw Markdown.
190
+ ### Outbound Artifacts And Assistant Actions
191
191
 
192
- Key rules:
192
+ Outbound files are delivered after the active Telegram turn completes. They must be staged with `telegram_attach`, are checked atomically per tool call, and use configurable size limits before photo/document upload.
193
193
 
194
- - Rich text should render cleanly in Telegram chats
195
- - Real code blocks must remain literal and escaped
196
- - Supported absolute HTTP(S) and mailto links should stay clickable, with generated HTML attributes escaped separately from text content, while unsupported link forms such as unresolved references, footnotes, or relative links without a known base should degrade safely instead of producing broken Telegram anchors
197
- - Markdown tables should keep their internal separators but drop the outer left and right borders when rendered as monospace blocks so narrow Telegram clients keep more usable width; table padding should count grapheme/display width for multi-codepoint emoji, combining marks, and wide Unicode where possible, and the Telegram before-agent prompt suffix also asks the assistant to prefer narrow table columns because many chats are read on phone-width screens
198
- - Unordered Markdown lists should render with a monospace `-` marker and ordered Markdown lists should render with monospace numeric markers so list indentation stays more predictable on narrow Telegram clients
199
- - Real Markdown task-list items should render with checkbox markers, while standalone `[x]` and `[ ]` prose should stay literal instead of being reinterpreted as checklists
200
- - Nested Markdown quotes should flatten into one Telegram blockquote with added non-breaking-space indentation because Telegram does not render nested blockquotes reliably
201
- - Original blank-line spacing between Markdown blocks should stay intact in both preview and final rendering instead of being collapsed to one generic block separator, while headings should still keep readable separation from following blocks such as code fences even when source Markdown omits a blank line
202
- - Long replies, including raw HTML-mode replies used by interactive/status flows, must be split below Telegram's 4096-character limit
203
- - Raw HTML chunking lives with the rendering helpers in `/lib/rendering.ts` and should preserve/reopen active tags across chunk boundaries where possible
204
- - Preview rendering uses stable top-level Markdown blocks for rich Telegram HTML and appends the still-growing tail conservatively as readable plain text so the preview stays valid even when the answer is incomplete
194
+ Assistant-authored final-message actions use hidden top-level comments:
205
195
 
206
- The renderer is a Telegram-specific formatter, not a general Markdown engine, so rendering changes should be treated as regression-prone.
196
+ - `telegram_voice` creates voice reply artifacts through configured outbound handlers, programmatic voice handlers, or registered synthesis providers.
197
+ - `telegram_button` creates inline buttons whose callbacks enqueue the configured prompt text as a normal Telegram prompt turn.
207
198
 
208
- ## Streaming And Delivery
199
+ Preview rendering strips top-level action comments while streaming. Comments inside code fences, quotes, lists, or indented examples stay literal.
209
200
 
210
- During generation, the bridge streams previews back to Telegram.
201
+ Unknown callback data outside owned prefixes is forwarded as `[callback] <data>` only after built-in and extension handlers decline it.
211
202
 
212
- Preferred order:
203
+ ## Extension Surfaces
213
204
 
214
- 1. Re-render the current Markdown buffer into a preview snapshot that renders closed top-level blocks as rich Telegram HTML and keeps the unstable tail conservative and readable
215
- 2. Send or update that preview through `sendMessage` plus `editMessageText`, because `sendMessageDraft` is text-only for rich previews
216
- 3. Serialize overlapping preview flushes so older Telegram edit calls cannot race newer streamed snapshots
217
- 4. Replace the preview with the final rendered reply when generation ends
205
+ `pi-telegram` intentionally owns one `getUpdates` loop per bot. `polling` owns that internal loop; `updates` owns classification/default-routing plans plus the public handler registry layered extensions use to observe or consume updates without opening a competing polling connection. Layered extensions should integrate through extension surfaces instead of polling the same bot independently.
218
206
 
219
- Draft streaming can remain as a plain-text fallback path, but rich Telegram previews are driven through editable messages and stable-block snapshot selection.
207
+ - Raw update observation/consumption: [Updates](./updates.md).
208
+ - Structured inline UI sections: [Sections](./sections.md).
209
+ - Callback namespace discipline: [Callback Namespaces](./callback-namespaces.md).
210
+ - Voice/STT/TTS providers: [Voice Integration](./voice.md).
211
+ - Inbound/outbound command-template handlers: [Command Templates](./command-templates.md).
220
212
 
221
- ### Response Context
213
+ Extension callbacks must avoid `pi-telegram` owned prefixes such as `compact:`, `tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`, `settings:`, and `section:`.
222
214
 
223
- Telegram prompt responses use explicit delivery context to attach outbound text, rich previews, errors, attachment notices, and uploads as Telegram replies to the source prompt when possible.
215
+ ## Diagnostics And Operational Behavior
224
216
 
225
- Reply metadata rules:
217
+ Status rendering distinguishes connected, active, dispatching, queued, tool-running, model-switching, and compacting states. If a queue mutation removes the last waiting item while Telegram-owned work still has running tools, status remains active instead of degrading to connected.
226
218
 
227
- - Reply metadata is opt-in per delivery path.
228
- - It uses `reply_parameters` with `allow_sending_without_reply: true`.
229
- - It is applied only to the first chunk of split long responses; continuation chunks are sent as normal adjacent messages.
230
- - Media-group turns reply to the turn's representative `replyToMessageId`, not to every source message in the group.
219
+ Queue reactions are shortcut controls for waiting turns. Promotion reactions (`👍`, `⚡️`, `❤️`, `🕊`, `🔥`) move prompts to priority; removal reactions (`👎`, `👻`, `💔`, `💩`, `🗑`) remove waiting turns because ordinary Telegram DM deletions are not exposed through Bot API polling.
231
220
 
232
- Long text split coalescing is intentionally conservative. Only human text messages at or above the 3600-character near-limit threshold open the short debounce window. Immediate same-chat/user contiguous text tails join that prompt; commands, bot messages, captions, media groups, and normal short follow-ups bypass the coalescer.
221
+ `/telegram-status` records grouped diagnostics for transport/API, polling/update, prompt dispatch, controls, typing, compaction, setup, session lifecycle, attachment queue/delivery, and recent redacted runtime events. Expected preview noise such as unchanged edit responses is filtered out.
233
222
 
234
- ### Outbound Files
223
+ When proactive push is enabled, successful local non-Telegram final replies are sent to the paired chat. Local prompt text is not mirrored because the bot does not own terminal user messages.
235
224
 
236
- Outbound files are sent only after the active Telegram turn completes. They must be staged through the `telegram_attach` tool, are staged atomically per tool call, and are checked against a default 50 MiB limit configurable through `PI_TELEGRAM_OUTBOUND_ATTACHMENT_MAX_BYTES` or `TELEGRAM_MAX_ATTACHMENT_SIZE_BYTES`. Delivery uses file-backed multipart blobs so large sends do not require preloading whole files into memory.
237
-
238
- ### Assistant-Authored Actions
239
-
240
- Assistant-authored outbound actions use final-message markup instead of agent tool calls. Preview updates strip closed top-level HTML comments and currently open/partial top-level comment starts before rendering, so users do not see transient metadata even when streaming flushes happen after only `<`, `<!`, or `<!--`.
241
-
242
- On `agent_end`, the bridge removes top-level comments from the Markdown text reply, but treats these column-zero top-level blocks specially before delivery:
243
-
244
- - `<!-- telegram_voice ... -->`
245
- - `<!-- telegram_button ... -->`
246
-
247
- Comments inside fenced code, quotes, lists, or indented examples stay literal, including fenced blocks with Markdown-valid indented closing fences.
248
-
249
- Voice delivery uses one fallback pipeline:
250
-
251
- 1. Configured `outboundHandlers` with `type: "voice"`
252
- 2. Programmatic `voice` handlers
253
- 3. Registered synthesis providers from `lib/voice.ts`
254
-
255
- The bridge extracts body text, `text="..."`, or colon shorthand, asks the pipeline for an `.ogg`/`.opus` artifact, validates native voice format, and uploads the generated file via Telegram `sendVoice`. When delivery fails, the queue runtime records diagnostics and falls back to the planned text reply when no text was already delivered. Synthesis providers own TTS, speech rewriting, transcript choice, and format conversion.
256
-
257
- Button blocks are built in. Each `telegram_button` block becomes one inline-keyboard button on the final text, and callback clicks enqueue the configured prompt text as a normal Telegram prompt turn. The `telegram_button: Label` shorthand uses the same text for label and prompt, `prompt="..."` supports explicit one-line prompts, and body-form buttons use the body as the prompt.
258
-
259
- Unknown callback data that does not match pi-telegram-owned prefixes (`tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`, `section:`) is forwarded to π as `[callback] <data>` after built-in handlers decline it. Layered callback payloads should follow the [Callback Namespace Standard](./callback-namespaces.md). Structured menu integrations should use the [Telegram Extension Sections Standard](./extension-sections.md) instead of hand-rolled fallback callbacks.
260
-
261
- ### Proactive Push And Mobile Guidance
262
-
263
- When proactive push is enabled, successful local non-Telegram final replies are sent to the paired chat. Local prompt text is not sent because the bot does not own or mirror terminal user messages. This keeps terminal-originated results visible in Telegram without changing Telegram-originated turn delivery.
264
-
265
- Technical Markdown, code, tables, formulas, and numbered lists stay in the text channel when appropriate while TTS-friendly voice messages and tappable continuations do not require `telegram_attach` or extra transport tools. Telegram prompt guidance targets about 37 visible cells for tables, dense list items, and compact text blocks because emoji and other wide glyphs make raw character counts misleading on mobile screens.
266
-
267
- ## Interactive Controls
268
-
269
- The bridge exposes Telegram-side session controls in addition to regular chat forwarding. Menu layout details live in [Application Menu Shape](#application-menu-shape); this section summarizes the command/control surface.
270
-
271
- Telegram chat controls:
272
-
273
- - `/start`: opens the main application menu and runs immediately even while generation is active.
274
- - `/model`, `/thinking`, `/queue`: hidden shortcuts for opening the matching menu sections directly.
275
- - `/compact`: triggers π session compaction when the bridge is idle.
276
- - `/next`: dispatches the next queued turn, aborting the active run first when π is busy.
277
- - `/continue`: enqueues a Telegram-owned priority `continue` prompt without aborting the current turn.
278
- - `/abort`: aborts the active Telegram-owned run while preserving queued items for manual continuation.
279
- - `/stop`: aborts the active Telegram-owned run and clears waiting Telegram queue items.
280
-
281
- Pi-side diagnostics and settings:
282
-
283
- - `/telegram-status`: renders grouped diagnostics for connection, polling, execution, queue, and the recent redacted runtime/API event ring.
284
- - `/telegram-settings`: exposes π-side bridge settings; currently this includes proactive push backed by the same `telegram.json` flag as the hidden Telegram `/settings` menu.
285
-
286
- Queue reactions are shortcut controls for waiting text, voice, file, image, and media-group turns. Matching uses the turn's source Telegram message ids. `👍`, `⚡️`, `❤️`, `🕊`, and `🔥` promote waiting prompts; `👎`, `👻`, `💔`, `💩`, and `🗑` remove waiting turns because ordinary Telegram DM message deletions are not exposed through the Bot API polling path this bridge uses.
287
-
288
- The `/telegram-status` event ring records transport/API, polling/update, prompt-dispatch, control-action, typing, compaction, setup, session-lifecycle, and attachment queue/delivery failures. Benign unchanged edit responses and unsupported empty draft-clear attempts are filtered out so expected preview transport noise does not obscure real failures.
225
+ Telegram prompt guidance asks assistants to keep dense mobile-visible text around 37 display cells where possible, because emoji and wide Unicode make raw character counts misleading.
289
226
 
290
227
  ## In-Flight Model Switching
291
228
 
292
- When `/model` is used during an active Telegram-owned run, the bridge can emulate the interactive π workflow of stopping, switching model, and continuing.
293
-
294
- The current implementation does this by:
229
+ When `/model` is used during an active Telegram-owned run, the bridge can emulate π's interactive stop/switch/continue workflow:
295
230
 
296
- 1. Applying the newly selected model immediately
297
- 2. Queuing or staging a synthetic Telegram continuation turn
298
- 3. Aborting the active Telegram turn immediately, or delaying the abort until the current tool finishes when a tool call is in flight
299
- 4. Dispatching the continuation turn after the abort completes
231
+ 1. Apply the selected model immediately.
232
+ 2. Queue or stage a synthetic Telegram continuation turn.
233
+ 3. Abort the active Telegram turn immediately, or wait for the current tool to finish before aborting.
234
+ 4. Dispatch the continuation after abort completion.
300
235
 
301
- This behavior is intentionally limited to runs currently owned by the Telegram bridge. If π is busy with non-Telegram work, the bridge still refuses the switch instead of hijacking unrelated session activity.
236
+ This is limited to Telegram-owned runs. If π is busy with non-Telegram work, the bridge refuses the switch instead of hijacking unrelated activity.
302
237
 
303
238
  ## Related
304
239
 
@@ -20,7 +20,7 @@ myext:page:2
20
20
 
21
21
  - Use a stable extension-owned namespace, preferably the package or extension name without scope punctuation.
22
22
  - Keep the namespace lowercase ASCII: `a-z`, `0-9`, `_`, `-`.
23
- - Do not use `pi-telegram` owned prefixes: `tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`, `settings:`, `section:`. Current app navigation uses `menu:`; `status:` remains reserved for legacy/owned status callbacks but is not emitted by current UI. `section:` is owned by the Extension Sections platform (0.10.0+), documented in [Extension Sections](./extension-sections.md). `settings:` is owned for the built-in Settings submenu.
23
+ - Do not use `pi-telegram` owned prefixes: `compact:`, `tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`, `settings:`, `section:`. Current app navigation uses `menu:`; `status:` remains reserved for legacy/owned status callbacks but is not emitted by current UI. `compact:` is owned by the manual compaction confirmation dialog. `section:` is owned by the Extension Sections platform (0.10.0+), documented in [Extension Sections](./sections.md). `settings:` is owned for the built-in Settings submenu.
24
24
  - Keep the full `callback_data` within Telegram's 64-byte limit.
25
25
  - Put only opaque ids or small enum values in payloads; do not store secrets, full prompts, or large state.
26
26
  - Treat callbacks as untrusted input. Validate namespace, action, and payload before executing side effects.
@@ -37,7 +37,7 @@ Layered extensions may intercept that message and handle their own namespace. If
37
37
 
38
38
  ## Extension sections
39
39
 
40
- [Telegram Extension Sections](./extension-sections.md) are a higher-level UI contract over this namespace rule. A section owns a canonical extension identity such as `@llblab/pi-telegram-explorer`, but its Telegram `callback_data` should use the `pi-telegram` owned `section:` prefix plus a compact token, because Telegram limits callback payloads to 64 bytes.
40
+ [Telegram Extension Sections](./sections.md) are a higher-level UI contract over this namespace rule. A section owns a canonical extension identity such as `@llblab/pi-telegram-explorer`, but its Telegram `callback_data` should use the `pi-telegram` owned `section:` prefix plus a compact token, because Telegram limits callback payloads to 64 bytes.
41
41
 
42
42
  Conceptual form:
43
43
 
@@ -45,4 +45,4 @@ Conceptual form:
45
45
  section:<token>:<action>[:<payload>]
46
46
  ```
47
47
 
48
- The token maps back to the full section identity inside the section registry. Section authors should not hand-roll `section:` callbacks outside the section context helpers, and ordinary layered extensions should continue using their own namespace plus external handlers or the `[callback]` fallback.
48
+ The token maps back to the full section identity inside the section registry. Section authors should not hand-roll `section:` callbacks outside the section context helpers, and ordinary layered extensions should continue using their own namespace plus update handlers or the `[callback]` fallback.
@@ -88,9 +88,9 @@ If a matching handler fails with a non-zero exit code, the runtime records diagn
88
88
 
89
89
  ## Programmatic Inbound Handlers And STT Fallbacks
90
90
 
91
- Extensions can register programmatic inbound handlers with `registerTelegramInboundHandler(kind, handler)` from `@llblab/pi-telegram/lib/inbound-handlers.ts`. This is the code-level counterpart to configured `inboundHandlers`; use it for extension-owned transformations that are not voice-specific.
91
+ Extensions can register programmatic inbound handlers with `registerTelegramInboundHandler(kind, handler)` from `@llblab/pi-telegram/inbound`. This is the code-level counterpart to configured `inboundHandlers`; use it for extension-owned transformations that are not voice-specific.
92
92
 
93
- Voice extensions can register STT providers with `registerTelegramVoiceTranscriptionProvider()` from `@llblab/pi-telegram/lib/voice.ts`. This is the zero-config extension path for voice/audio input: an extension such as `pi-xai-voice` can transcribe Telegram voice notes without requiring the operator to write an `inboundHandlers` command template.
93
+ Voice extensions can register STT providers with `registerTelegramVoiceTranscriptionProvider()` from `@llblab/pi-telegram/voice`. This is the zero-config extension path for voice/audio input: an extension such as `pi-xai-voice` can transcribe Telegram voice notes without requiring the operator to write an `inboundHandlers` command template.
94
94
 
95
95
  Priority stays explicit and predictable:
96
96
 
@@ -101,14 +101,17 @@ Priority stays explicit and predictable:
101
101
  5. built-in text-file fallback for text attachments
102
102
 
103
103
  ```ts
104
- import { registerTelegramInboundHandler } from "@llblab/pi-telegram/lib/inbound-handlers.ts";
105
- import { registerTelegramVoiceTranscriptionProvider } from "@llblab/pi-telegram/lib/voice.ts";
106
-
107
- const disposeInbound = registerTelegramInboundHandler("document", async ({ file }) => {
108
- if (!file?.mimeType?.includes("pdf")) return undefined;
109
- const text = await extractPdf(file.path);
110
- return text || undefined;
111
- });
104
+ import { registerTelegramInboundHandler } from "@llblab/pi-telegram/inbound";
105
+ import { registerTelegramVoiceTranscriptionProvider } from "@llblab/pi-telegram/voice";
106
+
107
+ const disposeInbound = registerTelegramInboundHandler(
108
+ "document",
109
+ async ({ file }) => {
110
+ if (!file?.mimeType?.includes("pdf")) return undefined;
111
+ const text = await extractPdf(file.path);
112
+ return text || undefined;
113
+ },
114
+ );
112
115
 
113
116
  const dispose = registerTelegramVoiceTranscriptionProvider(
114
117
  async (file) => {
package/docs/locks.md CHANGED
@@ -58,13 +58,13 @@ During a user-initiated start/connect event, an extension should:
58
58
  1. Read its lock entry
59
59
  2. If `pid` is stale, replace the entry
60
60
  3. If `pid` and `cwd` match the current pi instance, refresh or keep the entry
61
- 4. If a live external owner exists, ask interactively whether to move singleton ownership here
61
+ 4. If a live polling owner exists, ask interactively whether to move singleton ownership here
62
62
 
63
63
  ## Acquisition timing
64
64
 
65
65
  Lock writes must be caused by an explicit user-initiated runtime event, such as a start/connect command or a confirmed takeover prompt.
66
66
 
67
- Extension initialization and session-start hooks may read `locks.json`, update local status, install ownership watchers, and resume local work when the existing lock already points at the current `pid`/`cwd`. After a full process restart, a session-start hook may replace a stale lock from the same `cwd` to restore explicitly requested ownership. They must not create ownership from an inactive lock, take over a live external owner, or replace a stale lock from another directory by themselves. Such locks should stay visible as state until the user runs the start/connect command. Session replacement should suspend local runtime work and ownership watchers without releasing the lock, so the next session in the same `pid`/`cwd` can resume from explicit ownership.
67
+ Extension initialization and session-start hooks may read `locks.json`, update local status, install ownership watchers, and resume local work when the existing lock already points at the current `pid`/`cwd`. After a full process restart, a session-start hook may replace a stale lock from the same `cwd` to restore explicitly requested ownership. They must not create ownership from an inactive lock, take over a live polling owner, or replace a stale lock from another directory by themselves. Such locks should stay visible as state until the user runs the start/connect command. Session replacement should suspend local runtime work and ownership watchers without releasing the lock, so the next session in the same `pid`/`cwd` can resume from explicit ownership.
68
68
 
69
69
  ## Optional fields
70
70
 
@@ -105,7 +105,7 @@ Extensions may prefix those states with their own compact name, such as `wakeup
105
105
  Start/connect commands should make singleton moves easy:
106
106
 
107
107
  1. If no live owner exists, take ownership without an extra prompt
108
- 2. If a live external owner exists, ask whether to move singleton ownership to this pi instance
108
+ 2. If a live polling owner exists, ask whether to move singleton ownership to this pi instance
109
109
  3. On confirmation, write the current `{ "pid": ..., "cwd": ... }` to this extension's key in `locks.json`
110
110
  4. The previous owner must notice that `locks.json` no longer points at its own `pid`/`cwd` and stop local runtime work without deleting the new lock
111
111