@llblab/pi-kit 0.34.3 → 0.35.1

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.
Files changed (50) hide show
  1. package/BACKLOG.md +2 -0
  2. package/CHANGELOG.md +10 -0
  3. package/README.md +3 -1
  4. package/node_modules/@llblab/pi-telegram/AGENTS.md +3 -3
  5. package/node_modules/@llblab/pi-telegram/BACKLOG.md +6 -1
  6. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +11 -0
  7. package/node_modules/@llblab/pi-telegram/README.md +1 -1
  8. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +3 -2
  9. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +12 -8
  10. package/node_modules/@llblab/pi-telegram/dist/lib/bus-api.js +2 -2
  11. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +39 -23
  12. package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +2 -0
  13. package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +21 -8
  14. package/node_modules/@llblab/pi-telegram/dist/lib/config.d.ts +6 -7
  15. package/node_modules/@llblab/pi-telegram/dist/lib/config.js +6 -8
  16. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +40 -45
  17. package/node_modules/@llblab/pi-telegram/dist/lib/journal.d.ts +7 -0
  18. package/node_modules/@llblab/pi-telegram/dist/lib/journal.js +80 -0
  19. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.d.ts +12 -0
  20. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.js +33 -3
  21. package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +6 -2
  22. package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +46 -4
  23. package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +5 -1
  24. package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +2 -2
  25. package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +2 -0
  26. package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +137 -13
  27. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +10 -5
  28. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +19 -2
  29. package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +2 -0
  30. package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +41 -14
  31. package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
  32. package/node_modules/@llblab/pi-telegram/docs/architecture.md +5 -1
  33. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +5 -3
  34. package/node_modules/@llblab/pi-telegram/docs/outbound.md +4 -2
  35. package/node_modules/@llblab/pi-telegram/docs/public-api.md +3 -3
  36. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +14 -8
  37. package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +2 -1
  38. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +45 -24
  39. package/node_modules/@llblab/pi-telegram/lib/commands.ts +21 -8
  40. package/node_modules/@llblab/pi-telegram/lib/config.ts +11 -17
  41. package/node_modules/@llblab/pi-telegram/lib/extension.ts +43 -55
  42. package/node_modules/@llblab/pi-telegram/lib/journal.ts +80 -0
  43. package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +36 -3
  44. package/node_modules/@llblab/pi-telegram/lib/queue.ts +43 -4
  45. package/node_modules/@llblab/pi-telegram/lib/replies.ts +3 -2
  46. package/node_modules/@llblab/pi-telegram/lib/routing.ts +140 -18
  47. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +22 -5
  48. package/node_modules/@llblab/pi-telegram/lib/threads.ts +54 -18
  49. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  50. package/package.json +2 -2
package/BACKLOG.md CHANGED
@@ -4,6 +4,8 @@ Package pins, resource order and bundled runtime ownership remain authoritative
4
4
 
5
5
  ## Carried checks
6
6
 
7
+ - **Installed 0.35.1 Telegram smoke (operator-owned):** After separately authorized kit installation/reload, verify Reroute removes an exhausted temporary tab and Restore keeps the new tab while deleting its unprotected predecessor. Confirm protected work survives. Guest PNG/JPEG, document and MP3 replacement plus staging cleanup remain explicitly deferred upstream live checks; verify unsupported OGG/Opus feedback separately. Upstream idle-leader acceptance and kit packaging checks do not certify this installation.
8
+ - **Installed 0.35.0 Telegram smoke (operator-owned):** After separately authorized installation/reload of both peers, verify same-process follower session replacement keeps the Thread, letter slot and title without a second connected notice. Check shared `threads.displayMode` through Settings across profiles and confirm the retired profile key is ignored without migration. Package validation does not certify installed clients.
7
9
  - **Installed 0.34.3 typing smoke (operator-owned):** After separately authorized installation/reload of both peers, verify native typing during streaming, UI prompts and compaction without recurring local authorization churn. Check actual client versions, including iOS; successful HTTP issuance and synthetic tests do not prove client visibility. Use disposable sessions and preserve accepted work.
8
10
  - **Installed 0.34.2 smoke (operator-owned):** After separately authorized installation/reload, verify Pi 1.2 custom-message completions no longer raise the Telegram prompt-hook error, adjacent button matrices retain their rows and callbacks, and native Windows Codex CLI fallback works on an actual Windows host. Use disposable settings; package validation and synthetic SDK probes do not certify installed clients or authorize live fault injection.
9
11
  - **Installed 0.34.0 Telegram smoke (operator-owned):** After separately authorized installation/reload of both peers, verify per-prompt Steer, canonical thumbs-up/thumbs-down/fire controls with other reactions ignored, reconnection after a deleted tab, follower `/start` Restore and first-click leader Restore confirmation with separately proven old-tab cleanup. Unknown creation can leave an extra tab; do not erase accepted work or replay uncertain input. Package/CI validation does not certify live clients.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  All notable changes to `@llblab/pi-kit` are documented here.
4
4
 
5
+ ## 0.35.1: Telegram Guest Media and Routing Cleanup
6
+
7
+ - `Telegram cleanup`: Advances only the exact Telegram pin to `0.57.1`. Exhausted Reroute tabs and Restore's old tab are no longer blocked by unrelated targetless creation attempts, while accepted work and uncertain deletion stay protected. Fresh local menu Reroute and supported Restore hold command effects through their cleanup decision. Other pins, package membership and resource order are unchanged.
8
+ - `Guest media`: Queued photos, documents and MP3 now replace an acknowledged Guest Mode placeholder instead of being silently skipped. Confirmed failures report errors, uncertain edits are not replayed, and unsupported OGG/Opus replacement is rejected explicitly. Guest media client acceptance remains a deferred upstream follow-up, not proof supplied by kit validation.
9
+
10
+ ## 0.35.0: Telegram Thread Continuity and Shared Settings
11
+
12
+ - `Telegram continuity`: Advances only the exact Telegram pin to `0.57.0`. Same-process follower session replacement preserves the Thread, letter slot and acknowledged title without a second connected notice; cross-process handoff notices use the acknowledged title. Other pins, package membership and resource order are unchanged.
13
+ - `Shared settings`: Automatic Thread display now uses top-level `threads.displayMode` for every bot profile. The old profile-scoped key is ignored without migration; configurations without the new key use Letters until Settings saves a choice. Profiles hold only bot identity and the paired owner.
14
+
5
15
  ## 0.34.3: Telegram Typing Continuity
6
16
 
7
17
  - `Telegram typing`: Advances only the exact Telegram pin to `0.56.4`. Streaming keepalives preserve the same owner's in-flight typing request, while session, transport and target changes still revoke old authority. Compaction cannot claim an existing agent loop. Other pins, package membership and resource order are unchanged; client-specific typing visibility remains a live check.
package/README.md CHANGED
@@ -18,7 +18,7 @@ Package links lead to the owning repositories for usage, documentation, issues,
18
18
  | [`@llblab/pi-codex-usage`](https://github.com/llblab/pi-codex-usage) | `0.13.1` | OpenAI/legacy Codex subscription quota and Business credit status, with persistent priority Fast toggle mirrored in Telegram |
19
19
  | [`@llblab/pi-grow-loop`](https://github.com/llblab/pi-grow-loop) | `0.9.0` | Visible continuation scheduling and bounded worker Skills through compiled, manifest-owned resources |
20
20
  | [`@llblab/pi-state-flow`](https://github.com/llblab/pi-state-flow) | `0.26.4` | Scoped context/memory compiler with step-independent current memory, stable Passive/Active modes and visible blocked-state status |
21
- | [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.56.4` | Telegram companion with per-prompt Steer, queued controls, Reroute/Restore, recoverable connections, previews, follower Threads, files, and voice |
21
+ | [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.57.1` | Telegram companion with per-prompt Steer, queued controls, Reroute/Restore, recoverable connections, previews, follower Threads, files, and voice |
22
22
  | [`@llblab/skills`](https://github.com/llblab/skills) | `1.15.0` | Portable workflows for engineering, review, design, context maintenance, and other focused tasks |
23
23
 
24
24
  Versions are exact by design. An upstream release does not change an installed kit until this repository explicitly advances the dependency and publishes a new kit version. Runtime defects and package-specific feature requests belong in the linked repository; package selection and kit installation issues belong here.
@@ -47,6 +47,8 @@ Prefer the kit instead of separately loading the same packages. If you already u
47
47
 
48
48
  Telegram provides Normal/Priority/Steer prompt lanes and independent Keep/Skip controls. Only thumbs-up (Priority), thumbs-down (Skip) and fire (Steer) control the queue; other reactions are ignored. Steer inserts a correction at a model-step boundary, while Normal remains the default. Reroute preserves visible prompt copies and reply anchors, while Restore keeps command completion and old-tab cleanup separate. Explicit connection recovery may leave an extra tab after a lost creation response, but does not clear accepted input.
49
49
 
50
+ Telegram keeps the follower's Thread, letter slot and acknowledged title silently across same-process session replacement. Automatic Thread display is shared across bot profiles through top-level `threads.displayMode`; choose it through Telegram Settings. Profiles hold only bot identity and the paired owner.
51
+
50
52
  Telegram keeps bus protocol v3: update every Pi process that runs pi-telegram together; 0.53.x followers cannot register with current leaders. Restore and connection recovery additionally negotiate compatible capabilities, including follower `/start` and `/help` menu restoration; update both peers to use the new paths. Runtime storage stays in `tmp/pi-telegram`. Drain the prompt queue or accept losing waiting work before reload/restart; damaged-state reset discards all profiles' runtime continuity. `npm run validate` checks exact installed pins, declared resources, dependency audit and bundled inventory. It does not certify installed-client rendering; carried checks remain in [Backlog](./BACKLOG.md).
51
53
 
52
54
  ```bash
@@ -77,7 +77,7 @@ Use the relevant local skill before non-trivial work in its domain. Keep skill o
77
77
  - Runtime files live under `<agent-dir>/tmp/pi-telegram` (releases before 0.52.0 used `tmp/telegram`, which a new release never writes, migrates or deletes). The older `owners.json` is read-only evidence: a live fresh owner there blocks acquisition so two release generations never poll one bot, and only its identity is exposed, never its bus endpoint or secret. Only `state.json` and `logs.jsonl` are persistent root files: the per-profile `transport` section of `state.json` is the sole transport-owner authority, `workspace` owns canonical Workspace/Restore evidence and `admission` its leases; the `runtime` section and `logs.jsonl` never grant routing authority. Each section changes only through the shared transaction under its own domain authority (nonleader admission remains process/birth-owned, not transport-owned); Workspace/runtime publication captures exact session authority, and acquisition, refresh, release, takeover, and irreversible leader work fence the exact owner/epoch. Ordinary reads and section mutations still refuse a damaged shared envelope without repair. Explicit `/telegram-connect` additionally grants one profile-admitted reset of matching untargeted unfinished Thread-creation metadata before Workspace identity allocation; it may leave an unknown Telegram tab behind, but never deletes a known target, Workspace binding or journal receipt. Automatic startup/re-registration and Restore-only admission cannot grant that reset. Known pending targets retain their exact binding key and slot; fresh provisioning uses unique operation IDs so late recovery evidence cannot attach to a replacement attempt. Follower reset intent uses authenticated `explicit-connect-recovery-v1` registration. Only a non-election leader start (`/telegram-connect` or session auto-start) runs the operator-approved optimistic reset: under the shared transaction guard it replaces damaged envelope/transport/Workspace/admission evidence with an empty envelope, accepting loss of every profile's runtime continuity; filesystem access errors still refuse (see [Damaged-State Reset](./docs/architecture.md#damaged-state-reset-operator-approved)). Use the [session journal contract](./docs/multi-instance-bus.md#session-owned-journal-storage) for owners-named polling continuity and the explicitly approved optimistic session-family sweep; that sweep is not Thread deletion or receipt settlement.
78
78
  - Threaded Mode has exactly one live leader per bot profile. Followers are real operator-started Pi processes and must authenticate/register over local IPC; Telegram never spawns hidden Pi processes. A live but unreachable owner does not authorize split-brain polling.
79
79
  - Local IPC is a trust boundary, not merely a private socket. Unknown, stale, mismatched-generation, or unauthorized requests must not inject prompts, callbacks, API sends, artifacts, liveness, or bindings. Follower registration identity may prepare a binding, but inbound generation authority is published only after successful preparation for the same generation and current context; pending or failed startup cannot append into a retained old journal. Readiness also binds the active Pi context and supplied session generation. Same-session refresh awaits binding preparation without re-registering; a changed session ID cannot publish readiness until its exact leader registration is acknowledged. Reusing a context object cannot carry readiness across a session-generation or ID change. Registration requests capture session authority before asynchronous startup and fence every publication/finalization against the current attempt. Stop or supersession invalidates that attempt; obsolete cleanup cannot erase a newer registration or a refreshed context.
80
- - Protocol compatibility is independent from package version. Registration negotiates protocol version, runtime build, and canonical capabilities before target provisioning or live publication. `durable-follower-admission-v1` gates source forwarding; `queue-handoff-v1` independently gates semantic queue transfer for every participant and is advertised only with exact source/recipient journal-binding composition. `follower.register`, capability-gated restore-only `follower.restoreWorkspace` and side-effect-free liveness `bus.probe` are bootstrap requests; other requests require exact live-registry generation authority, and `bus.ack` is response-only. `thread-display-mode-v1` gates follower display-setting requests; the leader owns their serialized profile preference and title application.
80
+ - Protocol compatibility is independent from package version. Registration negotiates protocol version, runtime build, and canonical capabilities before target provisioning or live publication. `durable-follower-admission-v1` gates source forwarding; `queue-handoff-v1` independently gates semantic queue transfer for every participant and is advertised only with exact source/recipient journal-binding composition. `follower.register`, capability-gated restore-only `follower.restoreWorkspace` and side-effect-free liveness `bus.probe` are bootstrap requests; other requests require exact live-registry generation authority, and `bus.ack` is response-only. `thread-display-mode-v1` gates follower display-setting requests; the leader serializes the shared preference write and owns title application.
81
81
  - Long-lived timers, pollers, watchers, receivers, heartbeats, background delivery, and deferred dispatch are session-bound. Shutdown cancels/drains diagnostics publications and fences deferred status reads before they can acquire guards or repair files. Replacement stops stale activity and makes late work inert; teardown must recheck the captured session generation even when context identity is reused. Same-process handoff may preserve exact profile/target identity but never stale Pi context or cross-profile authority. Follower heartbeat and leader-to-follower Restore/live-rebind control reply deadlines align with the eight-second leader stale-liveness window rather than the generic one-second local-RPC default; an ordinary multi-second Pi/TUI event-loop stall or a slower Windows named pipe is not registration or recipient loss. Participating source observations must hold a reference for the actual read, including pending-mutation/count queries against a stopped worker's retained source; a scoped observation never restores receipt readiness or execution authority. A donor cancellation resumed after remote handoff awaits must also hold a source reference; only the existing exact journal CAS may cancel, never undo accepted recipient custody. Stable source keys do not certify captured callable lifetimes: snapshot prepared worker capabilities at construction and replace them on source-handle renewal without replaying unsettled input. Aborting a durable update generation does not release that `update_id`: replacement replay waits for its actual handler settlement, and effectful handlers use the shared execution fence immediately before commit and after awaited delegation. Admission also rechecks that fence after the default handler returns, before its outcome can settle custody; a stopped handler's ordinary return is not completion authority. Internal clones explicitly carry the hidden fence; reroute forwarding, thread-store mutation, cleanup, and Bot API boundaries retain the originating generation.
82
82
  - Runtime state is event-driven reconciliation of local assumptions against Telegram signals, not a complete bot read-model and not permission to query Telegram on every action. Destructive thread cleanup goes through `thread-reconciler` with current proof and leader fencing. Fresh Workspace Thread creation derives its initial Bot API title from the active display mode before issuance; the stable generated `threadName` remains separate from the acknowledged `displayTitle`.
83
83
 
@@ -90,7 +90,7 @@ Use the relevant local skill before non-trivial work in its domain. Keep skill o
90
90
  - Foreign forwarding settles as `accepted`, `retryable`, or `terminal-rejected`. Only an authenticated acknowledgement carrying the expected `deliveryId` and `sourceUpdateId` releases leader journal authority. Negative, missing, stale, mismatched, or capacity-failed settlement remains durable; callback error answers are side effects only. Exception: the operator-approved fixed chooser deadline may discard its exact unclaimed donor pending source without a body archive or delivery-success claim. Accepted recipient queue/work and bound Threads are untouched; old donor controls and late reports cannot replay it. The lifetime and temporary cleanup contract lives in `docs/architecture.md`.
91
91
  - A forwarding delivery id is stable across registration replacement and derives from envelope kind, source `update_id`, and stable recipient binding. Runtime instance and registration generation remain separate attempt fences. Persisted message ownership carries the stable binding so replay can rebind only to its current authenticated registration.
92
92
  - A queued receipt persists its acquiring runtime instance, OS pid/process-birth identity, session generation, acquisition id, and acquisition time. Only exact authority may settle or discard it. Cached presence is not current execution proof: prepared custody must revalidate the exact queued owner/group without recovery and refuse offers or uncertain reads. Completion requires an exact removal acknowledgement, never merely `!ready`; a retained acknowledgement permits local cleanup only, not replay. Same-process session replacement may reconstruct the claim and the original process may settle after transport ownership moves; a foreign process may neither replay nor settle it through generic removal or a copied acquisition id.
93
- - Startup and elapsed time are not owner-death proof; queued authority has no time lease. Dead-owner cleanup groups the complete receipt and transactionally rechecks pid liveness plus process-birth identity: only an absent PID or mismatched stable Linux/macOS/Windows birth proof discards all session-owned sources without replay; a matching proof is `alive`, while inaccessible birth metadata is `unverifiable`, and both non-dead outcomes keep authority queued. Under actual `A`–`Z` allocation pressure, retirement may invoke that journal-owned CAS only for a current inactive binding after complete strict source inspection, no local work/live owner/delivery authority, whole unoffered receipt groups, and a preflight proving every grouped owner dead. It must then recapture all protection before preparing deletion; partial progress never grants deletion authority. The live-transfer contract is authenticated offer → exact-generation bounded payload staging → recipient CAS acceptance → exact receipt-and-owner ACK → donor removal → recipient readiness. The offer freezes donor settlement/recovery; controls rebuild local closures; negative/mismatched pre-acceptance ACK cancels only an unaccepted offer and retains donor work; a lost post-acceptance ACK cannot cancel recipient authority and leaves donor memory frozen for explicit reconciliation.
93
+ - Startup and elapsed time are not owner-death proof; queued authority has no time lease. Dead-owner cleanup groups the complete receipt and transactionally rechecks pid liveness plus process-birth identity: only an absent PID or mismatched stable Linux/macOS/Windows birth proof discards all session-owned sources without replay; a matching proof is `alive`, while inaccessible birth metadata is `unverifiable`, and both non-dead outcomes keep authority queued. Under actual `A`–`Z` allocation pressure, retirement may invoke that journal-owned CAS only for a current inactive binding after complete strict source inspection, no local work/live owner/delivery authority, whole unoffered receipt groups, and a preflight proving every grouped owner dead. It must then recapture all protection before preparing deletion; partial progress never grants deletion authority. Fully resolved, positively unbound temporary-tab cleanup may independently recover historical v1 queued receipts under its profile admission and journal source serialization: complete strict namespace/reference inspection, whole unoffered receipt groups, fresh process-death proof and the existing journal CAS are required. Alive/unverifiable owners and unsupported custody remain protected; this recovery never runs from read-only protection checks, and every ordinary target/source protection is recaptured before deletion. The live-transfer contract is authenticated offer → exact-generation bounded payload staging → recipient CAS acceptance → exact receipt-and-owner ACK → donor removal → recipient readiness. The offer freezes donor settlement/recovery; controls rebuild local closures; negative/mismatched pre-acceptance ACK cancels only an unaccepted offer and retains donor work; a lost post-acceptance ACK cannot cancel recipient authority and leaves donor memory frozen for explicit reconciliation.
94
94
  - Execution failures persist bounded diagnostics and attempt state as `retry-wait`, except that an exact Telegram HTTP 400 stale/deleted-thread API failure with a proven `{chatId, threadId}` terminally settles the currently executing source after best-effort shared binding invalidation. Automatic retry continues indefinitely with exponential `1s → 2s → 4s → 8s → 16s → 32s → 60s` delay capped at 60 seconds; later independent updates continue draining, durable authority is never silently discarded, and legacy `failed` entries resume automatically at startup. Snapshot-plus-segment journals compact only after 256 unapplied revisions or 4 MiB; snapshot-first cleanup tolerates redundant segments, and empty authority may atomically rebind bot/profile identity. Missing snapshots left by the retired broad temp cleanup rebuild only from a complete provably empty segment chain, while revisionless snapshots may recover from a validated later segment predecessor; otherwise the transaction-locked reset deletes damaged segments before publishing a fresh journal, with accepted input loss and informational evidence, not a quarantine copy. Unsupported versions remain untouched; strict protection reads never reset. Startup best-effort removes obsolete current-runtime root/session `recovery/` folders without touching pre-0.52.0 storage.
95
95
  - Business connection chats are a separate namespace even when their chat/message IDs match bot-chat IDs. Default DM routing must never infer private-queue deletion intent from `deleted_business_messages`; raw companion handlers remain separate owners.
96
96
  - Clock-bearing chooser sources survive cold spending until their fixed deadline. A restarted owner revives one in place only while its saved phase is still `waiting`, its recorded chooser location is known, its tab stays unbound and live threads exist; that waiting phase is the proof no selection or effect ran. Selected, expired or bound sources keep the protective hold, and new-world forgetting preserves their positively unbound temporary frame. Expiry is a donor discard, not recipient cancellation or source execution completion. Confirmed expiry releases worker custody before UI/cleanup awaits; metadata retries retain no prompt bodies and cannot repeat an issued delete.
@@ -109,7 +109,7 @@ Use the relevant local skill before non-trivial work in its domain. Keep skill o
109
109
  - `preview` owns streaming lifecycle only, not assistant rendering. Finalization waits for the captured generation's preceding preview work, never later drafts blocked by that publication, and must not issue pre/post-final draft-clear calls that create transient Telegram draft UI. Reset/invalidation detaches predecessor dependencies; late settlements cannot mutate replacement state. Turns that already answer as one atomic reply (voice replies, Guest Mode queries) never stream previews.
110
110
  - Native `sendChatAction(typing)` is the automatic activity signal for unsettled agent and compaction work while Telegram transport is authorized. Extension-owned blocking UI prompts pause it and completion resumes it while either work owner remains active. Compaction may stop only a typing loop it actually started; it must preserve a pre-existing agent-owned loop. Refresh only the assigned Thread on a conservative three-second cadence; do not mirror typing to aggregate `All` and multiply shared-chat flood pressure. Typing is best-effort presence only, never a Thread-existence or message-delivery probe: a failed exact-target action remains a structured diagnostic and must not replace a healthy connected/leader/follower status with `error`. Do not invent extra in-chat work indicators or emit activity for startup/connect/reload/recovery alone.
111
111
  - Public activity handlers and connected companion delivery are asynchronous, target-bound, generation-fenced surfaces. Connected companion projection has no independent opt-out: disconnect or authority loss is its boundary. Token deltas, hidden reasoning, unknown sources, and stale authority never enter public projection.
112
- - Thread display defaults to the profile-scoped Letters strategy, with Names, Directory Snake, and Directory Title as the other automatic choices; Names projects the generated dictionary name for the slot. Unsupported retained display keys resolve to Letters without rewriting persisted configuration. A durable manual Thread display name retained on its Workspace binding overrides any automatic projection until exact reset; keep generated/recovery identity separate from manual and acknowledged display fields. After leader startup, automatic display contraction waits one follower-staleness window so election and follower re-registration cannot briefly remove and restore an acknowledged same-cwd suffix; stop/start generation cancels stale reconciliation. UI labels, emoji semantics, navigation, settings controls, callback namespaces, voice behavior, command templates, and assistant markup follow the linked `/docs` contracts. Generated human-readable action labels use `emoji + space + text`; emoji-free action text is only a reasoned no-semantic-marker fallback. State/value controls follow the [button control hierarchy](./docs/ui-style.md#button-control-hierarchy), not the action-label grammar. Non-spatial generated controls default to top-level vertical cells, with nested rows reserved for unmistakably compact peers. Do not restate other evolving UI details here.
112
+ - Profiles hold only bot identity and the paired owner; Settings-menu preferences live in shared top-level config. Thread display (`threads.displayMode`) defaults to the Letters strategy, with Names, Directory Snake, and Directory Title as the other automatic choices; Names projects the generated dictionary name for the slot. Unsupported retained display keys resolve to Letters without rewriting persisted configuration. A durable manual Thread display name retained on its Workspace binding overrides any automatic projection until exact reset; keep generated/recovery identity separate from manual and acknowledged display fields. After leader startup, automatic display contraction waits one follower-staleness window so election and follower re-registration cannot briefly remove and restore an acknowledged same-cwd suffix; stop/start generation cancels stale reconciliation. UI labels, emoji semantics, navigation, settings controls, callback namespaces, voice behavior, command templates, and assistant markup follow the linked `/docs` contracts. Generated human-readable action labels use `emoji + space + text`; emoji-free action text is only a reasoned no-semantic-marker fallback. State/value controls follow the [button control hierarchy](./docs/ui-style.md#button-control-hierarchy), not the action-label grammar. Non-spatial generated controls default to top-level vertical cells, with nested rows reserved for unmistakably compact peers. Do not restate other evolving UI details here.
113
113
 
114
114
  ## 5. Domain Ownership Index
115
115
 
@@ -2,7 +2,12 @@
2
2
 
3
3
  _This file owns unresolved project work only. Completed behavior belongs in `CHANGELOG.md`; durable contracts belong in `AGENTS.md` and `/docs`._
4
4
 
5
- - [ ] **Connection recovery live acceptance.** Verify operator-run leader/follower reconnect after interrupted creation on updated peers. Known target reuse and unknown-target abandonment have separate local regressions; do not infer live recovery from them. Also verify a long-deleted follower tab, follower `/start` Restore with negotiated menu-alias support, and leader Restore confirmation on the first click plus separately proven old-tab cleanup. Record leftover-tab behavior, exact peer capabilities and diagnostics; local regressions and release authorization do not replace this live evidence.
5
+ - [ ] **Routing extensions and follow-up coverage.** Preserve live/unverifiable owners, offered receipts, exact source references, session/transport fences and one-shot deletion while extending beyond the current supported handoffs.
6
+ - [ ] **Follower menu handoff.** Establish whether deletion-before-execution warrants a separately negotiated Reroute acceptance/release contract. Ordinary follower ACK confirms durable custody, not held execution; existing held-command RPCs belong to Restore relocation and cannot silently change ordinary forwarding. Require exact recipient generation, bounded observation and no replay after uncertain release.
7
+ - [ ] **Non-menu and historical-source boundary.** Resolve the intended ordering for mutating/queue-producing commands and cold/revived originals before expanding the fresh local menu path. Queue receipt acceptance and historical retention must remain authoritative; never pre-complete sources, weaken fresh-source readiness or block accepted work solely to force deletion. Preserve current behavior until a supported handoff is proven.
8
+ - [ ] **Protected-work live coverage.** Exercise Reroute and Restore with protected earlier work to confirm cleanup waits without losing work. Idle leader acceptance and native regressions do not prove every busy-session client scenario. No manual journal edits or deletion of live tabs to obtain evidence.
9
+ - [ ] **Guest inline media live acceptance.** Explicitly deferred beyond 0.57.1 by operator release approval; not completed. After separately authorized reload, verify queued PNG/JPEG, documents and MP3 replace the acknowledged placeholder, with and without caption, and staging messages are removed. Confirm OGG/Opus attachment rejection and visible voice-markup explanation. Mocked Bot API coverage does not prove live-client rendering; use disposable artifacts and preserve accepted work.
10
+ - [ ] **Connection recovery live acceptance.** Verify operator-run leader/follower reconnect after interrupted creation on updated peers. Known target reuse and unknown-target abandonment have separate local regressions; do not infer live recovery from them. Also verify a long-deleted follower tab and follower `/start` Restore with negotiated menu-alias support. Record leftover-tab behavior, exact peer capabilities and diagnostics; local regressions and release authorization do not replace this live evidence.
6
11
  - [ ] **Steer follow-up live acceptance.** Release authorization explicitly defers these checks beyond 0.56.0; do not treat automated coverage or the accepted busy-run smoke as live proof for unrun scenarios. Preserve the [queue and dispatch contract](./docs/architecture.md#queue-and-dispatch-safety).
7
12
  - [ ] **Operator scenarios.** Verify final selector/counter rendering; idle packets, native pending input, compaction, model switching, abort/Continue, complete media groups, Reroute/Restore, negotiated leader/follower transfer, reply/preview ownership and session-generation replacement. Record actual Pi/OS/Telegram versions, peer capabilities and redacted outcomes. Reload and disposable live tests require operator intent; do not manipulate runtime files to obtain acceptance.
8
13
  - [ ] **Restore recipient refusal after leadership change.** Diagnose a retained routing chooser refusing the selected current leader with unavailable recipient authority. Identify the exact context/session/Workspace mismatch before proposing recovery; do not treat an absent follower registry as the cause of a leader-target refusal, bypass fences or mutate live state.
@@ -4,6 +4,17 @@
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.57.1: Guest Media and Routing Cleanup
8
+
9
+ - `Guest attachments`: Queued photos, documents and MP3 audio now replace the acknowledged Guest Mode placeholder instead of being silently skipped. Upload failures and confirmed edit rejections report errors; uncertain edits are not replayed or overwritten. OGG/Opus attachments are rejected before queueing on this path, and voice markup explains the limitation. Live-client media acceptance remains a deferred follow-up. Fixes #346.
10
+ - `Routing cleanup`: Exhausted Reroute tabs and a restored instance's old tab are no longer blocked by unrelated creation attempts with unknown targets. Uncertain creation records stay intact; binding, slot, owner and concrete or recovered target conflicts still protect. Temporary cleanup can recover whole historical receipts from proven-dead owners; live/unverifiable owners, handoff offers and damaged sources remain protected. Idle leader Reroute and Restore passed operator live checks.
11
+ - `Command ordering`: Fresh local menu Reroute waits for exact source removal and a temporary-tab cleanup attempt before rendering. Restore confirms the new binding, decides old-tab cleanup, then releases held input. Busy work retains protection, and uncertain deletion never repeats. Other Reroute commands, follower delivery and sources without fresh completion readiness retain their existing paths.
12
+
13
+ ## 0.57.0: Thread Continuity and Shared Settings
14
+
15
+ - `Config`: Thread display now lives in shared top-level `threads.displayMode`, beside the other Settings preferences; profiles hold only bot identity and the paired owner. The old `profiles.<name>.threadDisplayMode` key is not migrated and is ignored, so configs without the new key use the Letters default until Settings saves a choice.
16
+ - `Session replacement`: A follower's `/new` or `/fork` in the same Pi process now keeps its Thread silently. It keeps the same tab, letter slot and acknowledged display title, with no second connected notice. A connected notice for a handoff from another process now shows the acknowledged title instead of the generated dictionary name.
17
+
7
18
  ## 0.56.4: Typing Keepalive Continuity
8
19
 
9
20
  - `Typing continuity`: Streaming updates keep the current work's pending presence request alive instead of cancelling it through repeated loop replacement. Explicit starts, session or transport changes, Thread changes and stopping still revoke the previous action; compaction cannot claim an existing agent loop.
@@ -261,7 +261,7 @@ Classic private DM mode is the base product mode. When Telegram private-chat Thr
261
261
  - When all `A`–`Z` slots are reserved, a new connection may reclaim the oldest inactive, unprotected Thread. **This deletes that Thread and all its messages**, not Pi session history or project files. Active or protected work blocks reclamation; restoring a remembered session does not evict another binding.
262
262
  - Telegram never launches hidden Pi processes.
263
263
 
264
- In Settings → **🧵 Thread display**, choose Letters (default), Names, Directory Title or Directory Snake. Use `/name` for a custom Thread name and **Reset to automatic** to restore the selected naming style. Renaming preserves the Thread and its queued work. See [Thread Display](./docs/architecture.md#threaded-mode-multi-instance-bus) for naming details.
264
+ In Settings → **🧵 Thread display**, choose Letters (default), Names, Directory Title or Directory Snake. The choice is shared across all profiles through `threads.displayMode`; profiles contain only bot identity and the paired owner. Use `/name` for a custom Thread name and **Reset to automatic** to restore the selected naming style. Renaming preserves the Thread and its queued work. See [Thread Display](./docs/architecture.md#threaded-mode-multi-instance-bus) for naming details.
265
265
 
266
266
  | Mode | Best for | Runtime shape |
267
267
  | --- | --- | --- |
@@ -269,7 +269,7 @@ interface TelegramLifecycleBindingDeps {
269
269
  shutdownGenerativeAppLiveSurfaces?: () => void;
270
270
  resolveAutomaticThreadCleanupEnabled?: () => boolean | Promise<boolean>;
271
271
  buttonActionStore: OutboundHandlers.TelegramButtonActionStore;
272
- callMultipart: OutboundHandlers.TelegramVoiceReplySenderDeps["sendMultipart"];
272
+ callMultipart: (...args: Parameters<TelegramApi.TelegramBridgeApiRuntime["callMultipart"]>) => Promise<unknown>;
273
273
  sendChatAction: NonNullable<OutboundHandlers.TelegramVoiceReplySenderDeps["sendChatAction"]>;
274
274
  sendRecordVoiceAction: NonNullable<OutboundHandlers.TelegramVoiceReplySenderDeps["sendRecordVoiceAction"]>;
275
275
  sendMarkdownReply: Queue.TelegramAgentEndHookRuntimeDeps<Queue.PendingTelegramTurn, Pi.ExtensionContext, Pi.AgentEndEvent["messages"][number], Keyboard.TelegramInlineKeyboardMarkup>["sendMarkdownReply"];
@@ -278,6 +278,7 @@ interface TelegramLifecycleBindingDeps {
278
278
  onSteerTurnEnd?: TelegramQueueBindingRuntime<Pi.ExtensionContext>["onTurnEnd"];
279
279
  onPromptHandedOff?: (turn: Queue.PendingTelegramTurn, ctx: Pi.ExtensionContext) => void;
280
280
  answerGuestQuery: TelegramApi.TelegramBridgeApiRuntime["answerGuestQuery"];
281
+ editGuestInlineMessage?: TelegramApi.TelegramBridgeApiRuntime["editGuestInlineMessage"];
281
282
  deleteMessage: TelegramApi.TelegramBridgeApiRuntime["deleteMessage"];
282
283
  sendGuestReply: NonNullable<Queue.TelegramAgentEndHookRuntimeDeps<Queue.PendingTelegramTurn, Pi.ExtensionContext, Pi.AgentEndEvent["messages"][number], Keyboard.TelegramInlineKeyboardMarkup>["sendGuestReply"]>;
283
284
  editGuestReply?: Queue.TelegramAgentEndHookRuntimeDeps<Queue.PendingTelegramTurn, Pi.ExtensionContext, Pi.AgentEndEvent["messages"][number], Keyboard.TelegramInlineKeyboardMarkup>["editGuestReply"];
@@ -300,5 +301,5 @@ interface TelegramLifecycleBindingDeps {
300
301
  /** Settled-session hint for owners that wait on agent idleness, such as old-tab cleanup after a live rebind. */
301
302
  onSessionSettled?: (ctx: Pi.ExtensionContext) => void;
302
303
  }
303
- export declare function registerTelegramLifecycleRuntimeHooks({ pi, publicationRuntime, activityRuntime, activityVerbosityRuntime, diagnostics, assistantOutputRuntime, sessionLifecycleRuntime, configStore, abort, typing, lifecycle, activeTurnRuntime, telegramQueueStore, modelSwitchController, previewRuntime, promptDispatchRuntime, deferredQueueDispatchRuntime, modelContextAvailabilityRuntime, disconnectOnQuit, onSessionStarted, shutdownGenerativeAppLiveSurfaces, resolveAutomaticThreadCleanupEnabled, buttonActionStore, callMultipart, sendChatAction, sendRecordVoiceAction, sendMarkdownReply, sendTextReply, dispatchNextQueuedTelegramTurn, onSteerTurnEnd, onPromptHandedOff, answerGuestQuery, deleteMessage, sendGuestReply, editGuestReply, stopGuestPlaceholder, preparePreviewDelivery, finalizeMarkdownPreview, proactivePushTargetGetter, getAssistantRenderingMode, recordMessageOwnership, canSendAgentActivity, isSessionContextActive, getSessionGeneration, isTurnTransportActive, updateStatus, recordRuntimeEvent, onSessionSettled, }: TelegramLifecycleBindingDeps): void;
304
+ export declare function registerTelegramLifecycleRuntimeHooks({ pi, publicationRuntime, activityRuntime, activityVerbosityRuntime, diagnostics, assistantOutputRuntime, sessionLifecycleRuntime, configStore, abort, typing, lifecycle, activeTurnRuntime, telegramQueueStore, modelSwitchController, previewRuntime, promptDispatchRuntime, deferredQueueDispatchRuntime, modelContextAvailabilityRuntime, disconnectOnQuit, onSessionStarted, shutdownGenerativeAppLiveSurfaces, resolveAutomaticThreadCleanupEnabled, buttonActionStore, callMultipart, sendChatAction, sendRecordVoiceAction, sendMarkdownReply, sendTextReply, dispatchNextQueuedTelegramTurn, onSteerTurnEnd, onPromptHandedOff, answerGuestQuery, editGuestInlineMessage, deleteMessage, sendGuestReply, editGuestReply, stopGuestPlaceholder, preparePreviewDelivery, finalizeMarkdownPreview, proactivePushTargetGetter, getAssistantRenderingMode, recordMessageOwnership, canSendAgentActivity, isSessionContextActive, getSessionGeneration, isTurnTransportActive, updateStatus, recordRuntimeEvent, onSessionSettled, }: TelegramLifecycleBindingDeps): void;
304
305
  export {};
@@ -575,11 +575,7 @@ export function registerTelegramCommandsAndTools({ pi, agentDir, configStore, pe
575
575
  throw new Error(`Telegram profile "${profileName}" has no token.`);
576
576
  }
577
577
  await configStore.load();
578
- const latestProfile = configStore.getStoredConfig().profiles?.[profileName];
579
- configStore.setProfile(profileName, {
580
- ...profile,
581
- threadDisplayMode: latestProfile?.threadDisplayMode,
582
- });
578
+ configStore.setProfile(profileName, profile);
583
579
  configStore.activateProfile(profileName);
584
580
  await onTransportChanged?.();
585
581
  await persistConfig(configStore.get());
@@ -661,7 +657,7 @@ export function registerTelegramCommandsAndTools({ pi, agentDir, configStore, pe
661
657
  },
662
658
  });
663
659
  }
664
- export function registerTelegramLifecycleRuntimeHooks({ pi, publicationRuntime, activityRuntime, activityVerbosityRuntime, diagnostics, assistantOutputRuntime, sessionLifecycleRuntime, configStore, abort, typing, lifecycle, activeTurnRuntime, telegramQueueStore, modelSwitchController, previewRuntime, promptDispatchRuntime, deferredQueueDispatchRuntime, modelContextAvailabilityRuntime, disconnectOnQuit, onSessionStarted, shutdownGenerativeAppLiveSurfaces, resolveAutomaticThreadCleanupEnabled, buttonActionStore, callMultipart, sendChatAction, sendRecordVoiceAction, sendMarkdownReply, sendTextReply, dispatchNextQueuedTelegramTurn, onSteerTurnEnd, onPromptHandedOff, answerGuestQuery, deleteMessage, sendGuestReply, editGuestReply, stopGuestPlaceholder, preparePreviewDelivery, finalizeMarkdownPreview, proactivePushTargetGetter, getAssistantRenderingMode, recordMessageOwnership, canSendAgentActivity, isSessionContextActive = () => true, getSessionGeneration, isTurnTransportActive, updateStatus, recordRuntimeEvent, onSessionSettled, }) {
660
+ export function registerTelegramLifecycleRuntimeHooks({ pi, publicationRuntime, activityRuntime, activityVerbosityRuntime, diagnostics, assistantOutputRuntime, sessionLifecycleRuntime, configStore, abort, typing, lifecycle, activeTurnRuntime, telegramQueueStore, modelSwitchController, previewRuntime, promptDispatchRuntime, deferredQueueDispatchRuntime, modelContextAvailabilityRuntime, disconnectOnQuit, onSessionStarted, shutdownGenerativeAppLiveSurfaces, resolveAutomaticThreadCleanupEnabled, buttonActionStore, callMultipart, sendChatAction, sendRecordVoiceAction, sendMarkdownReply, sendTextReply, dispatchNextQueuedTelegramTurn, onSteerTurnEnd, onPromptHandedOff, answerGuestQuery, editGuestInlineMessage, deleteMessage, sendGuestReply, editGuestReply, stopGuestPlaceholder, preparePreviewDelivery, finalizeMarkdownPreview, proactivePushTargetGetter, getAssistantRenderingMode, recordMessageOwnership, canSendAgentActivity, isSessionContextActive = () => true, getSessionGeneration, isTurnTransportActive, updateStatus, recordRuntimeEvent, onSessionSettled, }) {
665
661
  const agentEndResetter = Runtime.createTelegramAgentEndResetter({
666
662
  abort,
667
663
  typing,
@@ -681,7 +677,7 @@ export function registerTelegramLifecycleRuntimeHooks({ pi, publicationRuntime,
681
677
  recordOwnership: recordMessageOwnership,
682
678
  recordRuntimeEvent,
683
679
  });
684
- const sendGuestAttachment = async (turn, attachment, caption) => {
680
+ const sendGuestAttachment = async (turn, attachment, caption, options) => {
685
681
  const stagingTarget = proactivePushTargetGetter();
686
682
  const stagingChatId = stagingTarget?.chatId;
687
683
  if (stagingChatId === undefined) {
@@ -689,11 +685,19 @@ export function registerTelegramLifecycleRuntimeHooks({ pi, publicationRuntime,
689
685
  }
690
686
  await OutboundAttachments.deliverTelegramGuestCachedAttachment({
691
687
  guestQueryId: turn.guestQueryId,
688
+ guestInlineMessageId: turn.guestInlineMessageId,
689
+ editGuestInlineMessage,
690
+ isDeliveryActive: options?.isDeliveryActive,
692
691
  stagingChatId,
693
692
  stagingTarget,
694
693
  attachment,
695
694
  caption,
696
- sendMultipart: callMultipart,
695
+ sendMultipart: (method, fields, fileField, path, name) => callMultipart(method, fields, fileField, path, name, {
696
+ assertAuthority() {
697
+ if (options?.isDeliveryActive?.() === false)
698
+ throw new Error("Guest attachment upload lost authority.");
699
+ },
700
+ }),
697
701
  answerGuestQuery: (guestQueryId, result) => answerGuestQuery(guestQueryId, undefined, { result }),
698
702
  answerGuestText: (guestQueryId, text) => answerGuestQuery(guestQueryId, text),
699
703
  fallbackText: caption ||
@@ -339,9 +339,9 @@ export function createTelegramBusAwareApiRuntime(deps) {
339
339
  ? deps.directRuntime.answerGuestQueryForInlineMessage(guestQueryId, text, options)
340
340
  : rejectTelegramDirectOwnership("answerGuestQueryForInlineMessage");
341
341
  },
342
- editGuestInlineMessage(inlineMessageId, content) {
342
+ editGuestInlineMessage(inlineMessageId, content, options) {
343
343
  return deps.ownsDirect()
344
- ? deps.directRuntime.editGuestInlineMessage(inlineMessageId, content)
344
+ ? deps.directRuntime.editGuestInlineMessage(inlineMessageId, content, options)
345
345
  : rejectTelegramDirectOwnership("editGuestInlineMessage");
346
346
  },
347
347
  async deleteMessage(chatId, messageId) {
@@ -927,6 +927,7 @@ export function createTelegramBusFollowerTargetProvisioner(deps) {
927
927
  ? deps.topicTargetStore.claimWorkspaceIdentity(registration.cwd, registration.instanceId, registration.previousInstanceId ?? restoredPredecessor?.instanceId, {
928
928
  existingBindingOnly: options?.existingWorkspaceBindingOnly === true,
929
929
  sessionId: registration.sessionId,
930
+ sessionHandoff: registration.previousInstanceId !== undefined,
930
931
  provisionProfileKey: registration.profileKey ?? `manual:${registration.instanceId}`,
931
932
  onCapacityUnavailable() {
932
933
  capacityUnavailable = true;
@@ -1140,10 +1141,45 @@ export function createTelegramBusFollowerTargetProvisioner(deps) {
1140
1141
  };
1141
1142
  }
1142
1143
  const probeRequiredRecord = reconnectRecord?.status === "probe-required";
1143
- const requiresVisibilityProbe = crossSessionReuse ||
1144
+ // Pi /new or /fork inside one live process keeps its Thread; that process already proves the tab.
1145
+ const sameProcessSessionHandoff = crossSessionReuse &&
1146
+ reconnectRecord?.status === "active" &&
1147
+ registration.previousInstanceId === reconnectRecord.instanceId &&
1148
+ recoverableTarget === undefined &&
1149
+ Threads.isSameTelegramProcessInstance(reconnectRecord.instanceId, registration.instanceId);
1150
+ const requiresVisibilityProbe = (crossSessionReuse && !sameProcessSessionHandoff) ||
1144
1151
  probeRequiredRecord ||
1145
1152
  recoverableTarget !== undefined;
1146
- const retainedDisplayBinding = deps.topicTargetStore.getWorkspaceBindingByTarget(result.target, registration.sessionId);
1153
+ const transferSessionHandoffRecord = async () => {
1154
+ const nowMs = getNowMs();
1155
+ const transferredRecord = deps.topicTargetStore.upsert({
1156
+ ...result.record,
1157
+ profileKey: followerProfileKey,
1158
+ owner: followerOwner.kind === "manual-follower"
1159
+ ? followerOwner
1160
+ : {
1161
+ kind: "manual-follower",
1162
+ instanceId: registration.instanceId,
1163
+ },
1164
+ instanceId: registration.instanceId,
1165
+ updatedAtMs: nowMs,
1166
+ lastSyncObservedAtMs: nowMs,
1167
+ lastReconcileAction: "follower-session-handoff",
1168
+ });
1169
+ await deps.topicTargetStore.persist();
1170
+ result = {
1171
+ target: transferredRecord.target,
1172
+ reused: true,
1173
+ record: transferredRecord,
1174
+ };
1175
+ };
1176
+ if (sameProcessSessionHandoff) {
1177
+ assertRestoreRegistration();
1178
+ await transferSessionHandoffRecord();
1179
+ }
1180
+ // A session handoff still owns the predecessor session's binding until the Workspace commit re-keys it.
1181
+ const retainedDisplayBinding = deps.topicTargetStore.getWorkspaceBindingByTarget(result.target, registration.sessionId) ??
1182
+ deps.topicTargetStore.getWorkspaceBindingByTarget(result.target);
1147
1183
  let connectedAnnouncement = !result.reused || requiresVisibilityProbe
1148
1184
  ? createTelegramBusInstanceLifecycleAnnouncement({
1149
1185
  target: result.target,
@@ -1194,27 +1230,7 @@ export function createTelegramBusFollowerTargetProvisioner(deps) {
1194
1230
  };
1195
1231
  }
1196
1232
  else if (crossSessionReuse && reconnectRecord) {
1197
- const nowMs = getNowMs();
1198
- const transferredRecord = deps.topicTargetStore.upsert({
1199
- ...result.record,
1200
- profileKey: followerProfileKey,
1201
- owner: followerOwner.kind === "manual-follower"
1202
- ? followerOwner
1203
- : {
1204
- kind: "manual-follower",
1205
- instanceId: registration.instanceId,
1206
- },
1207
- instanceId: registration.instanceId,
1208
- updatedAtMs: nowMs,
1209
- lastSyncObservedAtMs: nowMs,
1210
- lastReconcileAction: "follower-session-handoff",
1211
- });
1212
- await deps.topicTargetStore.persist();
1213
- result = {
1214
- target: transferredRecord.target,
1215
- reused: true,
1216
- record: transferredRecord,
1217
- };
1233
+ await transferSessionHandoffRecord();
1218
1234
  }
1219
1235
  connectedAnnouncement = undefined;
1220
1236
  }
@@ -627,6 +627,8 @@ export interface TelegramSelectedCommandAdmission {
627
627
  /** Independent exact recipient/target lifetime, including inside captured menu transport adapters. */
628
628
  assertRecipientCurrent(): void;
629
629
  reportCompleted(): boolean;
630
+ /** Optional recipient release barrier after acceptance; rejection suppresses effects without replay. */
631
+ beforeEffects?(): Promise<void>;
630
632
  }
631
633
  export declare function createTelegramCommandHandler<TMessage extends TelegramCommandRuntimeMessage, TContext>(deps: TelegramCommandRuntimeDeps<TMessage, TContext>): ((commandName: string | undefined, message: TMessage, ctx: TContext, commandArgs?: string) => Promise<boolean>) & {
632
634
  /** Command-only producer execution; Routing must supply the captured plan and released exact-source authority. */
@@ -1667,7 +1667,7 @@ export function createTelegramCommandHandler(deps) {
1667
1667
  const captured = { ...deps }, parsed = { ...command }, message = parsed.name === "status"
1668
1668
  ? copySelectedMenuCarrier(original)
1669
1669
  : copySelectedReplyAnchor(original);
1670
- const assertRecipientCurrent = admission.assertRecipientCurrent, report = admission.reportCompleted;
1670
+ const assertRecipientCurrent = admission.assertRecipientCurrent, report = admission.reportCompleted, beforeEffects = admission.beforeEffects;
1671
1671
  const effects = parsed.name === "status"
1672
1672
  ? undefined
1673
1673
  : {
@@ -1693,8 +1693,13 @@ export function createTelegramCommandHandler(deps) {
1693
1693
  throw new Error(`Selected ${parsed.name} plan owner is unavailable.`);
1694
1694
  };
1695
1695
  return issueSelectedOnce(assertCurrent, async () => {
1696
- if (plan.completion === "before-effect" && !report.call(admission))
1697
- return false;
1696
+ if (plan.completion === "before-effect") {
1697
+ if (!report.call(admission))
1698
+ return false;
1699
+ if (beforeEffects)
1700
+ await beforeEffects();
1701
+ assertRecipientCurrent();
1702
+ }
1698
1703
  return prepared.run({
1699
1704
  message,
1700
1705
  command: parsed,
@@ -1720,7 +1725,7 @@ export function createTelegramCommandHandler(deps) {
1720
1725
  ...copySelectedReplyAnchor(original),
1721
1726
  from: { ...original.from },
1722
1727
  };
1723
- const assertRecipientCurrent = admission.assertRecipientCurrent, reportCompleted = admission.reportCompleted;
1728
+ const assertRecipientCurrent = admission.assertRecipientCurrent, reportCompleted = admission.reportCompleted, beforeEffects = admission.beforeEffects;
1724
1729
  const assertAdmissionCurrent = createSelectedSemanticGuard(admission, () => captured.assertExecutionCurrent?.(original), false);
1725
1730
  const assertSemanticCurrent = () => {
1726
1731
  assertAdmissionCurrent();
@@ -1732,7 +1737,7 @@ export function createTelegramCommandHandler(deps) {
1732
1737
  persistAllowedUserId: async () => {
1733
1738
  throw new Error(`Selected ${name} cannot acquire pairing authority.`);
1734
1739
  },
1735
- }, { assertSemanticCurrent, assertRecipientCurrent, reportCompleted }));
1740
+ }, { assertSemanticCurrent, assertRecipientCurrent, reportCompleted, beforeEffects }));
1736
1741
  };
1737
1742
  const prepareSelectedConfirmation = (name) => (command, messages, ctx, admission) => {
1738
1743
  const original = messages[0], captured = { ...deps };
@@ -2036,7 +2041,7 @@ export function createTelegramCommandHandler(deps) {
2036
2041
  typeof admission.reportCompleted !== "function")
2037
2042
  return undefined;
2038
2043
  const assertSourceCurrent = admission.assertSourceCurrent, assertRecipientCurrent = admission.assertRecipientCurrent;
2039
- const reportCompleted = admission.reportCompleted, assertExecutionCurrent = deps.assertExecutionCurrent;
2044
+ const reportCompleted = admission.reportCompleted, beforeEffects = admission.beforeEffects, assertExecutionCurrent = deps.assertExecutionCurrent;
2040
2045
  const assertIssuanceCurrent = () => {
2041
2046
  assertSourceCurrent.call(admission);
2042
2047
  assertExecutionCurrent?.call(deps, original);
@@ -2060,8 +2065,14 @@ export function createTelegramCommandHandler(deps) {
2060
2065
  openSettingsMenu: menuDeps.openSettingsMenu &&
2061
2066
  ((value, context) => menuDeps.openSettingsMenu(value, context, recipientOptions)),
2062
2067
  };
2063
- return issueSelectedOnce(assertIssuanceCurrent, async () => reportCompleted.call(admission) &&
2064
- handleTelegramCommandRuntime(name, message, ctx, recipientDeps, args));
2068
+ return issueSelectedOnce(assertIssuanceCurrent, async () => {
2069
+ if (!reportCompleted.call(admission))
2070
+ return false;
2071
+ if (beforeEffects)
2072
+ await beforeEffects();
2073
+ assertRecipientCurrent();
2074
+ return handleTelegramCommandRuntime(name, message, ctx, recipientDeps, args);
2075
+ });
2065
2076
  },
2066
2077
  });
2067
2078
  }
@@ -2157,6 +2168,8 @@ async function handleTelegramHelpCommand(commandName, message, ctx, deps, admiss
2157
2168
  assertSemanticCurrent();
2158
2169
  if (!admission.reportCompleted())
2159
2170
  return false;
2171
+ if (admission.beforeEffects)
2172
+ await admission.beforeEffects();
2160
2173
  admission.assertRecipientCurrent();
2161
2174
  }
2162
2175
  const assertEffectCurrent = admission?.assertRecipientCurrent ??
@@ -36,7 +36,7 @@ export interface ResolvedTelegramTimeConfig {
36
36
  timezone: string;
37
37
  }
38
38
  export type TelegramThreadDisplayMode = "letters" | "names" | "directories" | "directory-snake" | "directory-title";
39
- export declare function resolveTelegramThreadDisplayMode(config: Pick<TelegramConfig, "threadDisplayMode">): TelegramThreadDisplayMode;
39
+ export declare function resolveTelegramThreadDisplayMode(config: Pick<TelegramConfig, "threads">): TelegramThreadDisplayMode;
40
40
  export declare function setTelegramThreadDisplayMode(store: TelegramConfigStore, mode: TelegramThreadDisplayMode, isCurrent: () => boolean): Promise<void>;
41
41
  export type TelegramAssistantRenderingMode = "rich" | "html";
42
42
  export type TelegramActivityVerbosity = "quiet" | "thinking" | "tools" | "verbose";
@@ -49,8 +49,6 @@ export interface TelegramConfig {
49
49
  botId?: number;
50
50
  /** @deprecated persisted identity belongs in profiles.default; retained for effective/legacy views */
51
51
  allowedUserId?: number;
52
- /** Effective view; persisted under profiles.<name>. */
53
- threadDisplayMode?: TelegramThreadDisplayMode;
54
52
  inboundHandlers?: TelegramInboundHandlerConfig[];
55
53
  attachmentHandlers?: TelegramInboundHandlerConfig[];
56
54
  outboundHandlers?: TelegramOutboundHandlerConfig[];
@@ -76,22 +74,23 @@ export interface TelegramConfig {
76
74
  threads?: {
77
75
  /** Delete this instance's bound Telegram thread on graceful Pi quit. */
78
76
  automaticCleanup?: boolean;
77
+ /** Automatic Thread title strategy shared by every profile; absent or invalid resolves to `letters`. */
78
+ displayMode?: TelegramThreadDisplayMode;
79
79
  };
80
80
  /** Canonical bot/session profiles, including profiles.default. */
81
81
  profiles?: Record<string, TelegramBotProfile>;
82
82
  }
83
83
  /**
84
- * Per-profile bot/session identity and Thread display preference.
84
+ * Per-profile bot identity and paired owner only.
85
85
  * Stored under `profiles.<name>` in telegram.json.
86
- * Shared bridge settings (inboundHandlers, outboundHandlers, voice, time,
87
- * assistant) stay at the top level.
86
+ * Shared bridge settings (handlers, assistant, voice, time, threads) stay at
87
+ * the top level and never leak into profiles.
88
88
  */
89
89
  export interface TelegramBotProfile {
90
90
  botToken: string;
91
91
  botUsername?: string;
92
92
  botId?: number;
93
93
  allowedUserId?: number;
94
- threadDisplayMode?: TelegramThreadDisplayMode;
95
94
  }
96
95
  export declare function isValidTelegramProfileName(name: string): boolean;
97
96
  /** List defined profile names. */
@@ -86,8 +86,9 @@ const TELEGRAM_THREAD_DISPLAY_MODES = [
86
86
  "directory-title",
87
87
  ];
88
88
  export function resolveTelegramThreadDisplayMode(config) {
89
- return TELEGRAM_THREAD_DISPLAY_MODES.includes(config.threadDisplayMode)
90
- ? config.threadDisplayMode
89
+ const mode = config.threads?.displayMode;
90
+ return TELEGRAM_THREAD_DISPLAY_MODES.includes(mode)
91
+ ? mode
91
92
  : "letters";
92
93
  }
93
94
  export async function setTelegramThreadDisplayMode(store, mode, isCurrent) {
@@ -102,7 +103,8 @@ export async function setTelegramThreadDisplayMode(store, mode, isCurrent) {
102
103
  if (!current() || !store.hasBotToken()) {
103
104
  throw new Error("Telegram Thread display setting lost its configured profile.");
104
105
  }
105
- await store.persist({ ...store.get(), threadDisplayMode: mode }, { isCurrent: current });
106
+ const latest = store.get();
107
+ await store.persist({ ...latest, threads: { ...latest.threads, displayMode: mode } }, { isCurrent: current });
106
108
  if (!current())
107
109
  throw new Error("Telegram Thread display setting changed during persistence.");
108
110
  }
@@ -253,14 +255,11 @@ function pickTelegramRootProfileFields(config) {
253
255
  ...(config.allowedUserId !== undefined
254
256
  ? { allowedUserId: config.allowedUserId }
255
257
  : {}),
256
- ...(config.threadDisplayMode !== undefined
257
- ? { threadDisplayMode: config.threadDisplayMode }
258
- : {}),
259
258
  ...(legacyCursor !== undefined ? { lastUpdateId: legacyCursor } : {}),
260
259
  };
261
260
  }
262
261
  function omitTelegramRootProfileFields(config) {
263
- const { botToken: _botToken, botUsername: _botUsername, botId: _botId, allowedUserId: _allowedUserId, threadDisplayMode: _threadDisplayMode, lastUpdateId: _lastUpdateId, ...sharedConfig } = config;
262
+ const { botToken: _botToken, botUsername: _botUsername, botId: _botId, allowedUserId: _allowedUserId, lastUpdateId: _lastUpdateId, ...sharedConfig } = config;
264
263
  return sharedConfig;
265
264
  }
266
265
  function omitRetiredProactivePush(config) {
@@ -286,7 +285,6 @@ export function normalizeTelegramDefaultProfileConfig(config) {
286
285
  "botUsername",
287
286
  "botId",
288
287
  "allowedUserId",
289
- "threadDisplayMode",
290
288
  "lastUpdateId",
291
289
  ].some((field) => Object.hasOwn(config, field));
292
290
  if (!hasLegacyRootProfile) {