@llblab/pi-kit 0.6.0 → 0.7.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.
Files changed (123) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +4 -4
  3. package/node_modules/@llblab/pi-actors/AGENTS.md +1 -1
  4. package/node_modules/@llblab/pi-actors/CHANGELOG.md +6 -0
  5. package/node_modules/@llblab/pi-actors/README.md +1 -1
  6. package/node_modules/@llblab/pi-actors/dist/lib/async-runs.d.ts +3 -0
  7. package/node_modules/@llblab/pi-actors/dist/lib/async-runs.js +14 -1
  8. package/node_modules/@llblab/pi-actors/dist/lib/command-templates.js +45 -3
  9. package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.js +1 -1
  10. package/node_modules/@llblab/pi-actors/dist/lib/observability.d.ts +16 -3
  11. package/node_modules/@llblab/pi-actors/dist/lib/observability.js +92 -7
  12. package/node_modules/@llblab/pi-actors/dist/lib/pi.d.ts +0 -1
  13. package/node_modules/@llblab/pi-actors/dist/lib/pi.js +15 -24
  14. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery-lineage.d.ts +17 -0
  15. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery-lineage.js +44 -0
  16. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.d.ts +4 -0
  17. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.js +102 -4
  18. package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.js +58 -39
  19. package/node_modules/@llblab/pi-actors/dist/lib/runtime.js +14 -6
  20. package/node_modules/@llblab/pi-actors/dist/skills/actors/SKILL.md +1 -1
  21. package/node_modules/@llblab/pi-actors/docs/async-runs.md +1 -1
  22. package/node_modules/@llblab/pi-actors/docs/coordinator-delivery.md +18 -23
  23. package/node_modules/@llblab/pi-actors/lib/async-runs.ts +18 -1
  24. package/node_modules/@llblab/pi-actors/lib/command-templates.ts +41 -3
  25. package/node_modules/@llblab/pi-actors/lib/extension-runtime.ts +1 -1
  26. package/node_modules/@llblab/pi-actors/lib/observability.ts +119 -5
  27. package/node_modules/@llblab/pi-actors/lib/pi.ts +15 -28
  28. package/node_modules/@llblab/pi-actors/lib/run-delivery-lineage.ts +68 -0
  29. package/node_modules/@llblab/pi-actors/lib/run-delivery.ts +120 -4
  30. package/node_modules/@llblab/pi-actors/lib/run-ui-runtime.ts +69 -44
  31. package/node_modules/@llblab/pi-actors/lib/runtime.ts +17 -6
  32. package/node_modules/@llblab/pi-actors/package.json +1 -1
  33. package/node_modules/@llblab/pi-actors/skills/actors/SKILL.md +1 -1
  34. package/node_modules/@llblab/pi-state-flow/AGENTS.md +33 -12
  35. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +125 -2
  36. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +87 -45
  37. package/node_modules/@llblab/pi-state-flow/README.md +203 -107
  38. package/node_modules/@llblab/pi-state-flow/docs/README.md +4 -0
  39. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +188 -0
  40. package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +42 -0
  41. package/node_modules/@llblab/pi-state-flow/index.ts +164 -2
  42. package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +138 -0
  43. package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +273 -0
  44. package/node_modules/@llblab/pi-state-flow/lib/config.ts +48 -0
  45. package/node_modules/@llblab/pi-state-flow/lib/context.ts +18 -5
  46. package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +268 -0
  47. package/node_modules/@llblab/pi-state-flow/lib/discovery.ts +117 -0
  48. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +562 -0
  49. package/node_modules/@llblab/pi-state-flow/lib/episode.ts +24 -12
  50. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +606 -70
  51. package/node_modules/@llblab/pi-state-flow/lib/git.ts +666 -0
  52. package/node_modules/@llblab/pi-state-flow/lib/history.ts +95 -0
  53. package/node_modules/@llblab/pi-state-flow/lib/json.ts +24 -0
  54. package/node_modules/@llblab/pi-state-flow/lib/maintenance.ts +141 -0
  55. package/node_modules/@llblab/pi-state-flow/lib/memory.ts +52 -0
  56. package/node_modules/@llblab/pi-state-flow/lib/migration.ts +88 -0
  57. package/node_modules/@llblab/pi-state-flow/lib/publication.ts +296 -0
  58. package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +23 -7
  59. package/node_modules/@llblab/pi-state-flow/lib/rehydration.ts +79 -0
  60. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +264 -0
  61. package/node_modules/@llblab/pi-state-flow/lib/session.ts +6 -0
  62. package/node_modules/@llblab/pi-state-flow/lib/skills.ts +99 -7
  63. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +305 -48
  64. package/node_modules/@llblab/pi-state-flow/lib/state.ts +74 -7
  65. package/node_modules/@llblab/pi-state-flow/lib/status.ts +125 -6
  66. package/node_modules/@llblab/pi-state-flow/lib/storage.ts +196 -0
  67. package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +233 -0
  68. package/node_modules/@llblab/pi-state-flow/lib/terminal.ts +70 -24
  69. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +254 -29
  70. package/node_modules/@llblab/pi-state-flow/package.json +8 -2
  71. package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +128 -0
  72. package/node_modules/@llblab/pi-telegram/AGENTS.md +13 -8
  73. package/node_modules/@llblab/pi-telegram/BACKLOG.md +20 -3
  74. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +13 -0
  75. package/node_modules/@llblab/pi-telegram/README.md +12 -8
  76. package/node_modules/@llblab/pi-telegram/docs/README.md +1 -0
  77. package/node_modules/@llblab/pi-telegram/docs/architecture.md +220 -18
  78. package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +1 -1
  79. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +70 -19
  80. package/node_modules/@llblab/pi-telegram/docs/outbound.md +6 -6
  81. package/node_modules/@llblab/pi-telegram/docs/public-api.md +12 -5
  82. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +3 -1
  83. package/node_modules/@llblab/pi-telegram/index.ts +4 -1418
  84. package/node_modules/@llblab/pi-telegram/lib/agent-messages.ts +6 -3
  85. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +28 -1
  86. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +600 -135
  87. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +962 -55
  88. package/node_modules/@llblab/pi-telegram/lib/bus.ts +350 -26
  89. package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +544 -0
  90. package/node_modules/@llblab/pi-telegram/lib/commands.ts +234 -11
  91. package/node_modules/@llblab/pi-telegram/lib/config.ts +177 -24
  92. package/node_modules/@llblab/pi-telegram/lib/extension.ts +1792 -0
  93. package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +20 -2
  94. package/node_modules/@llblab/pi-telegram/lib/journal.ts +2184 -126
  95. package/node_modules/@llblab/pi-telegram/lib/locks.ts +38 -1
  96. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +152 -13
  97. package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +51 -10
  98. package/node_modules/@llblab/pi-telegram/lib/paths.ts +29 -0
  99. package/node_modules/@llblab/pi-telegram/lib/polling.ts +85 -17
  100. package/node_modules/@llblab/pi-telegram/lib/prompts.ts +5 -2
  101. package/node_modules/@llblab/pi-telegram/lib/queue.ts +52 -18
  102. package/node_modules/@llblab/pi-telegram/lib/replies.ts +2 -2
  103. package/node_modules/@llblab/pi-telegram/lib/routing.ts +305 -112
  104. package/node_modules/@llblab/pi-telegram/lib/status.ts +10 -0
  105. package/node_modules/@llblab/pi-telegram/lib/sync.ts +308 -39
  106. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +279 -4
  107. package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +664 -0
  108. package/node_modules/@llblab/pi-telegram/lib/thread-display.ts +226 -0
  109. package/node_modules/@llblab/pi-telegram/lib/thread-naming.ts +118 -0
  110. package/node_modules/@llblab/pi-telegram/lib/threads.ts +1686 -129
  111. package/node_modules/@llblab/pi-telegram/lib/updates.ts +1319 -97
  112. package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +1643 -0
  113. package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +968 -0
  114. package/node_modules/@llblab/pi-telegram/lib/workspace-slots.ts +84 -0
  115. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  116. package/node_modules/@llblab/pi-telegram/screenshot.png +0 -0
  117. package/node_modules/@llblab/pi-telegram/scripts/measure-bus.mjs +83 -0
  118. package/node_modules/@llblab/pi-telegram/scripts/measure-workspace.mjs +101 -0
  119. package/node_modules/@llblab/{skills → pi-telegram/skills}/show-me/SKILL.md +28 -6
  120. package/node_modules/@llblab/pi-telegram/skills/show-me/references/telegram-surfaces.md +43 -0
  121. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/delivery-and-threads.md +1 -1
  122. package/node_modules/@llblab/skills/package.json +2 -3
  123. package/package.json +6 -5
@@ -98,7 +98,7 @@ An absent, stale, or invalid bound app fails closed and never degrades into an a
98
98
 
99
99
  ## `telegram_bind` Tool
100
100
 
101
- One agent Tool owns installation and deliberate invocation through two mutually exclusive shapes.
101
+ One agent Tool owns installation and deliberate invocation through two mutually exclusive shapes. Its optional `argument` schema explicitly describes recursive JSON values (`null`, boolean, number, string, array, or object) rather than using an unconstrained subschema. Pi keeps the same JSON semantics, while schema aggregators cannot lower this field to a bare `true` schema that some llama-server grammars reject. The recursive definition lives in the tool root `$defs` and uses only local `#/$defs/TelegramBindJsonValue` references; it does not rely on named/remote reference resolution or newer TypeBox runtime helpers.
102
102
 
103
103
  Install an external self-contained module and initialize it:
104
104
 
@@ -42,8 +42,8 @@ one private bot chat -> one thread per live Pi instance
42
42
  The operator experience:
43
43
 
44
44
  1. Start one Pi instance; it becomes the Telegram bus leader and polls Telegram.
45
- 2. Start another Pi instance with `pi-telegram` and run `/telegram-connect`; the follower registers instead of fighting for `getUpdates`.
46
- 3. The leader provisions or reuses a Telegram thread target for that instance.
45
+ 2. Start another Pi instance with `pi-telegram` and run `/telegram-connect`; the follower registers instead of fighting for `getUpdates`. Use `/telegram-connect as=Navigator`, or `/telegram-connect work as=Navigator` for a named profile, to name a fresh Workspace Thread. On later process reopen, a profile-scoped exact-`cwd` Workspace with a remembered Thread reconnects as a follower automatically while the leader remains live.
46
+ 3. The leader provisions or reuses a Telegram thread target for that instance. Use `/name Navigator` in that Telegram Thread later to change its display name through leader-authorized persistence.
47
47
  4. Messages, callbacks, reactions, files, voice, previews, and menus in that target route to the owning live Pi instance.
48
48
  5. If the leader exits, remaining followers elect/promote a new leader, which resumes polling and keeps the registered target routes alive where possible.
49
49
 
@@ -83,7 +83,7 @@ Threaded Mode meaning:
83
83
  tmp/telegram/owners.json / <profile-slot> -> bus leader identity + heartbeat
84
84
  ```
85
85
 
86
- Followers do not poll. They register with the leader and receive routed inbound updates from it. Followers still own their local queue, active-turn state, previews, final delivery planning, model switches, and Pi lifecycle. The leader owns only Telegram transport and update fanout. Pi session replacement (`new`) changes follower agent context, not bus membership: a registered follower preserves its registration and refreshes the live context instead of disconnecting. The Telegram bus belongs to the local set of cooperating visible Pi instances rather than to the first terminal session forever: if the visible terminal leader exits, a live registered follower can take over leadership.
86
+ Followers do not poll. They register with the leader and receive routed inbound updates from it. Followers still own their local queue, active-turn state, previews, final delivery planning, model switches, and Pi lifecycle. The leader owns only Telegram transport and update fanout. Pi session replacement (`new`) changes follower agent context, not bus membership: a registered follower preserves its registration and refreshes the live context instead of disconnecting. A new follower process also attempts a restore-only registration at session startup when the selected profile's local state contains an exact-`cwd` Workspace binding. The Telegram bus belongs to the local set of cooperating visible Pi instances rather than to the first terminal session forever: if the visible terminal leader exits, a live registered follower can take over leadership.
87
87
 
88
88
  A follower may route Bot API work or capability checks only after authenticated registration; an unregistered process that does not own the direct transport lock remains transport-passive. Follower request settlement remains owned by the existing local IPC operation budget rather than being inferred from Bot API method names; followers never invoke `getUpdates`.
89
89
 
@@ -145,7 +145,38 @@ A registered instance exposes:
145
145
  }
146
146
  ```
147
147
 
148
- `instanceId` is liveness identity. `owner` is explicit current binding identity (`leader`, `manual-follower`, or `pending-topic`). Internal compatibility keys may be derived, but `state.json` should not hide ownership direction inside legacy string keys. `threadName` is the user-facing instance-thread name: it drives Telegram UI thread naming and the Telegram-originated prompt identity label. Fresh threads receive a baked compact thread name from the assigned slot's curated palette; bare slot labels are fallback state only, and role/cwd seeds never replace the thread label.
148
+ `instanceId` is liveness identity. `owner` is explicit current binding identity (`leader`, `manual-follower`, or `pending-topic`). Internal compatibility keys may be derived, but `state.json` should not hide ownership direction inside legacy string keys. `threadName` is the stable named-mode and restoration identity; `displayTitle` is the separately acknowledged user-facing projection. Fresh threads receive a compact palette name from the assigned slot, while Letters and Directories project other titles without replacing the saved name.
149
+
150
+ ## Approved Next Contract: Directory Names And Reclaimable Slots
151
+
152
+ Status: approved design with a locally tested pure selection policy in `lib/workspace-slots.ts` and profile-isolated display preference persistence/default resolution in `lib/config.ts`. Workspace claims now reserve global letters before provisioning and preserve legacy binding keys. An exact claim assigns the first free letter to a missing-slot binding or the selected member of a duplicate-slot set, but persistence waits for successful target recovery; unresolved duplicates block unrelated fresh allocation. Sticky suffix metadata and acknowledged `displayTitle` persist in Workspace bindings. `lib/thread-display.ts` provides the three-mode projection plus serialized title reconciliation wired into leader startup and follower registration. Heartbeat ACKs carry acknowledged display titles to followers and the current-thread/TUI projection uses them without changing restoration identity. Settings now exposes Letters, Names (default), and Directories; follower changes use the capability-gated leader-owned setting path. Live bot chooser/notice labels and cross-instance agent-target resolution use acknowledged titles without granting routing authority. Confirmed owner cleanup now persists the first proven `inactiveSinceMs` transition atomically with target invalidation; successful active provisioning clears it. Pressure selection, intents, mocked execution, and recovery are implemented. A 2/2 same-model independent post-fix quorum cleared the admission-composition blocker at 0.96 confidence per reviewer, but production deletion remains disconnected by this release scope; `BACKLOG.md` owns operator smoke and release readiness.
153
+
154
+ The pure policy distinguishes a free letter, a proposed pressure-reclamation victim, and protected/invalid capacity. Its caller must supply a validated profile-wide snapshot, reservations, proven inactivity start, and explicit protection classification; duplicate legacy letters block selection. The policy performs no filesystem or Telegram operations and does not establish liveness or deletion authority. It proposes a victim only when every profile-wide letter is occupied or reserved; elapsed time alone never triggers retirement.
155
+
156
+ ### Cross-Process Admission Fence
157
+
158
+ Production retirement requires a durable profile-scoped reader/writer ledger owned by a dedicated `workspace-admission` domain. The ledger uses the existing atomic file-transaction primitive but does not hold a filesystem mutex across asynchronous work. It stores versioned admission leases and at most one destructive fence per exact Workspace target. Every record carries bot/profile identity, exact or conservative scope, a collision-resistant retry-stable operation id, operation kind, process id plus process-birth identity, and acquisition time. A destructive fence additionally carries the exact retirement-intent id, binding key, slot, target, and leader epoch. Missing, malformed, unreadable, ambiguously published, or unverifiable-owner state fails closed.
159
+
160
+ - Journal append, target-scoped Bot API issuance, provisioning, registration, and binding/target replacement must first transact against the ledger. They prune only leases whose process-birth owner is proven dead, reject a matching destructive fence, durably add an admission lease, perform the asynchronous or durable operation, then remove the exact lease transactionally. Exact-target leases conflict with that target; chat-only API work uses chat-wide scope; undecodable journal admission uses profile-wide scope. Duplicate release and process-crash recovery are idempotent, while an unknown release outcome remains protective.
161
+ - Retirement acquires its fence transactionally only after exact profile/epoch/intent validation and only when no exact, chat-wide, or profile-wide lease conflicts. Once the fence is durable, new matching admission is rejected. After the final protection recheck, the ledger durably advances `fenced` → `deletion-issued` and returns the sole deletion permit; a retry after an ambiguous publication or crash observes the issued phase but receives no second permit. Exact confirmed deletion or already-absence advances to `commit-ready`, and only durable binding plus intent removal permits fence completion. A protection change before permit issuance releases the fence; an unknown deletion, ambiguous commit, authority loss after issuance, or crash retains it.
162
+ - A successor may adopt a retained fence only together with the exact durable retirement intent and the existing profile/binding/leader-epoch adoption checks. It preserves original request/acquisition time and phase, replacing only the fence owner/epoch. An adopted `deletion-issued` fence must resolve through exact already-absence or retained unknown outcome and can never issue another deletion request. A different intent never clears or steals the fence. Startup restore-only and fresh capacity paths cannot bypass it; they return a truthful temporary-unavailable result without allocation or eviction.
163
+ - The isolated ledger and process regressions now cover admission-before-fence, fence-before-admission, chat/profile-wide conflicts, concurrent contenders, proven-dead lease cleanup, unverifiable-owner protection, malformed-state rejection, exact release idempotence, ambiguous publication recovery, one-shot deletion permits, and successor fence adoption. Production journal stores resolve an admission adapter that conservatively derives batch scopes, holds leases through atomic publication, and releases partial acquisitions on rejection. The production common JSON/multipart client likewise resolves its adapter per current bot/profile: target/chat scopes span all transport retries through settlement, malformed declared targets use profile scope, targetless methods bypass the ledger, and release failure cannot replay a settled request. One production Workspace operation runtime resolves admission before a shared local gate. Leader assembly uses it around chat-wide leader provisioning and profile-wide follower provisioning/registration, disconnect/dead cleanup, leader/follower rename, and display preference/reconciliation; Sync topic lifecycle and routing restore/reclaim use the same profile-wide boundary, so fence rejection precedes store reads and mutation. The isolated retirement executor now requires the matching durable fence, closes admission before its final protection recheck, persists `deletion-issued` before calling an executor-only deletion port with the sole permit, retains unknown outcomes without reissue, and commits only from confirmed `commit-ready`; successor recovery is process-tested. Retained fences now project their uppercase slot into the Thread store's optional external reservations, blocking generic allocation, Workspace claims, and reclamation selection even after binding commit; malformed or unreadable evidence fails closed. The profile runtime resolves a separate `workspace-admission[.<profile>].json`, stores only token-hash authority, preserves named-profile identity across switching, permits changed-token rebind only from a provably empty ledger, and rejects rotation while leases or a fence remain. The admission side is production-wired through leader/follower mutations, topic lifecycle, reroute restore/reclaim, manual disconnect/session-restart cleanup, and exact stale-target recovery; journal-evidence pruning also requires exact-target admission through durable publication. Disconnect/restart cleanup acquires profile admission before the shared leader gate and holds it across cleanup intent, API, binding, and durable settlement, so both retained fence phases reject it before state access. The common async admission runner and API adapter reject concurrent reuse of one live operation ID before lease acquisition, while clearing that process-local guard after acquisition failure or complete settlement so retry-stable durable recovery remains separate. A 2/2 same-model post-fix quorum independently verified the complete production mutation map, delayed reconciliation fix, fence-first and lease-held schedules, and absence of nested-gate deadlock at 0.96 confidence per reviewer. Destructive retirement remains disconnected by release scope; the PASS clears this blocker but does not authorize live deletion.
164
+ - Production mutation coverage is intentionally caller-owned rather than embedded in generic Thread-store primitives.
165
+ - The shared profile-wide Workspace operation runtime covers observed topic lifecycle, the complete unbound-target and reroute restore/reclaim handlers, manual disconnect/session-restart cleanup, and leader assembly provisioning, registration, cleanup, rename, and display operations. Detached post-provision reconciliation reacquires a fresh profile lease through the same runtime before delayed API/store work. Every admission lease precedes one process-local gate and lasts through API plus durable settlement.
166
+ - Journal append and common JSON/multipart transport use their own exact/chat/profile leases through publication or settlement. Follower promotion/target replacement, exact stale-target recovery, and journal-evidence pruning likewise hold dedicated admission through their complete mutation. These operations cannot overlap retirement because fence acquisition conflicts with the retained lease even when they do not need the shared local queue.
167
+ - Retirement intent preparation/adoption/execution owns its exact gate-and-ledger protocol but remains absent from production composition. Status projection and polling/routing bot-mode writes change only diagnostic or capability metadata; they neither create nor remove Thread/Workspace authority and serialize through the store's local persistence queue.
168
+ - Production callers of reservation, provision/cleanup intent, target-record, Workspace-binding, and display mutators are contained by the owners above. The generic store remains policy-free for isolated tests and domain composition; calling a primitive directly is not production retirement authority.
169
+ - Workspace identity remains the selected bot profile plus normalized exact full `cwd`; directory basenames are presentation, never routing keys. Each concurrent binding receives one profile-wide unique lowercase slot from `a` through `z`, persisted on the wire/store as its uppercase equivalent, independent of directory and leader/follower role. This replaces the two competing displayed allocation identities; immutable legacy `instanceSlot` and `bindingKey` remain recovery keys, not another displayed pool.
170
+ - Automatic display mode is a bot-profile setting shared by Telegram Thread titles and Pi TUI status. The selector offers `letters` then `directories`; absent or invalid values resolve to `letters`. Letters show `A`, `B`, `C`; directories use the directory basename. A durable per-Workspace `manualThreadName`, set through Telegram `/name`, overrides either automatic projection until explicitly reset. Bare `/name` immediately enters exact-target rename input: cancel is always available, while reset is shown only when a manual override exists; no intermediate action-selection step exists. Legacy persisted `names` configuration is read compatibly but resolves to Letters; it is no longer an effective or offered automatic choice. Store the automatic preference at `profiles.<name>.threadDisplayMode`, not as a process-local choice or a setting shared by unrelated bots.
171
+ - In directory mode, a singleton may hide its suffix; once another retained binding for that Workspace exists, all its labels expose their globally assigned suffixes (for example `extensions_a`, `skills_b`, `extensions_c`). Persist the decision to show suffixes so later closure does not make names oscillate. Equal basenames from different paths require a deterministic parent-path qualifier. Preserve the existing `threadName` as generated/recovery identity while automatic modes are selected. New manual names live only in `manualThreadName`; do not guess manual provenance from a legacy name or palette membership. `showSlotSuffix: true` is sticky binding metadata; sibling creation and legacy multi-binding loads expose it, and later upserts that omit it cannot reset it. Telegram `/name Name` changes the manual override and displayed title only for its exact originating target. Leader and follower requests carry that target through final generation/binding checks, so replacement cannot redirect a stale dialog mutation. Reset uses the same negotiated `workspace-thread-rename-v1` capability and exact follower generation; the leader computes the current automatic projection, edits the exact target, clears only `manualThreadName`, and persists before acknowledging. Follower metadata refresh preserves an acknowledged display title only while target and registration generation stay unchanged. Named-profile setup preserves the latest saved automatic preference even if another instance changes it while the token form is open.
172
+ - Fresh provisioning projects the candidate together with retained bindings and sends the active mode's title in `createForumTopic`. The exact targeted provision retains creation-title evidence until the Workspace commit publishes the binding and consumes that evidence together. Recovery preserves it even when a starting record already exists; an untargeted or unknown creation never authorizes a title commit. Proven deletion removes exact-target pending creation evidence, including when no current record was committed. Older contradictory pending/deleted snapshots settle that evidence durably before replacement; closed targets and pending cleanup block recovery until reconciliation, rather than becoming active again. The same exact-target check protects follower reconnect/carried-target shortcuts and the final Workspace commit, before creation evidence can be consumed. A matching carried pending target resumes through the provisioner that owns its reserved slot and acknowledged title instead of allocating that slot again. The shared provision-commit helper first commits the claim, then applies the acknowledged title with exact-binding comparison, preserving generic stale-title rejection on target replacement. Switching display mode changes projection only: preserve Thread ID, binding identity, slot, queue ownership, and routing. `displayTitle` records a successful Telegram edit independently of `threadName`, survives same-target registration updates, and is cleared on target replacement. The title reconciler captures profile, mode, leader epoch, and exact live-binding authority before each edit, rechecks after ACK and persistence, and skips dormant bindings. Failed persistence retains acknowledged dirty metadata for a later persist without repeating that API edit; a late or unknown ACK never commits a title to a replacement binding. Keep the stable palette/manual name separate from the current display title so switching back does not generate a different name. The leader owns Telegram title edits and acknowledged follower/TUI convergence, with generation/profile fencing and truthful partial-failure recovery. Successful registration ACKs optionally carry the acknowledged `displayTitle` with the exact target and registration generation, making it available before the initial status refresh. Heartbeats carry later title changes; stale generations cannot update display state. Connected notices use acknowledged titles while runtime `threadName` remains the stable restoration identity. Live bot chooser/notice labels, prompt attribution, and cross-instance agent-target name selection use the same acknowledged projection, but candidate liveness and the captured numeric `{chatId, threadId}` remain authoritative. Ambiguous projected names fail closed. Older peers can ignore the optional field, and no separate polling connection or follower snapshot-read loop is needed; do not expose a setting control that merely stores a preference without updating its promised surfaces.
173
+ - Reopening a retained inactive binding restores its slot and name without taking leadership. Explicit connection from the same directory may allocate a second binding. Startup restore remains restore-only: it must not evict another Workspace or allocate a fresh binding merely because all remembered bindings are owned. Explicit connection already skips a live peer's migrated binding instead of attempting to adopt it; that admission rule is independent of the display redesign.
174
+ - Prefer free letters in deterministic order. Only under full slot exhaustion may retirement choose the eligible binding with the oldest proven inactivity start, not necessarily slot `a` after `z`. Time alone never retires a binding. If every slot is protected or its ownership is unverifiable, reject new allocation with a truthful capacity explanation; never evict a live owner or silently expand into `aa`.
175
+ - Inactivity begins at a verified transition to no live owner; an idle but connected process is active. Do not derive it from last user message, heartbeat silence alone, snapshot-write time, file age, or any elapsed-time threshold. Legacy records without sufficient evidence have no `inactiveSinceMs` and remain ineligible rather than inheriting an invented old timestamp. Malformed inactivity values are ignored without dropping the binding. Repeated inactive observations retain the first timestamp for pressure ordering; stale same-target upserts cannot postpone it, while target replacement or successful active provisioning clears it.
176
+ - Eligibility excludes live or unverifiable owners, exact transient claims, accepted pending/active work, unresolved delivery authority, and pending provisioning, handoff, or cleanup operations. The store can capture one candidate snapshot from bindings, live records, process-local exact claims, reservations, and persisted provision/cleanup intents, but requires the caller to supply separate tri-state evidence for external live ownership, accepted work, and delivery authority. Only three explicit `clear` results plus valid continuous inactivity produce `eligible`; any protected or unknown dimension fails closed. Before future deletion, `workspace-retirement` selects the oldest eligible pressure victim only after all slots are occupied or reserved. It captures the profile and leader epoch, rechecks protection, then persists one durable Workspace retirement intent containing the complete expected binding, pressure reason, leader epoch, and request time. The intent itself protects that binding from competing selection; only the exact captured intent may be excluded during a fenced recheck. Duplicate intent ids, bindings, targets, or letters fail closed. Each new Workspace binding durably accumulates the registration/profile keys used to route its historical follower journals and marks that set complete. An existing legacy binding may learn current keys but cannot claim its unknown historical set is complete without separate discovery proof. Accepted-work policy treats an exact local queued/active target or any unsettled entry in a binding-specific follower journal as protected. Shared leader-journal entries protect only a decoded exact target; undecodable relevant entries, unreadable sources, and incomplete source enumeration remain unknown. The read-only evidence capture resolves the shared leader journal and every recorded follower key; missing resolvers, read errors, or an incomplete legacy key set make coverage unknown. A complete readable capture may prune a known empty follower-journal key only when a separate process/writer check is explicitly clear and the exact binding, profile, and leader epoch remain current; nonempty, unreadable, writable, or unknown keys remain. For incomplete legacy metadata, `journal` can discover canonical profile-exact follower snapshot and segment roots. Matching paths with unexpected/unreadable shape make discovery incomplete; successfully read discovered journals are target-scoped evidence because their one-way filename hash cannot recover the historical routing key. Legacy completeness remains unset, so later retirement repeats discovery rather than forgetting that historical domain. Live composition includes decoded process-birth proof, registry liveness, queue/journal state, and delivery-authority discovery.
177
+ - Reclamation deletes the exact inactive Telegram Thread and therefore removes its history; this is distinct from dropping a local label. The leader owns a durable, exact-binding-and-epoch-fenced retirement intent. The store validates and persists this contract, and preparation resumes an exact current-epoch intent after persistence failure or reload. The isolated executor requires both the caller-owned exclusive gate and durable admission ledger. It acquires or exactly adopts the matching fence, rechecks protection after admissions close, persists `deletion-issued`, and invokes an executor-only `deleteForumTopic` port with the sole non-retried permit. Unknown/rejected results keep the binding, intent, slot, and issued fence; retries and successors cannot issue again and require exact already-absence proof. Success or confirmed absence advances to `commit-ready`, then a store-owned commit hides the free slot while persistence is in flight, restores in-memory authority on known failure, and removes binding+intent before exact fence completion. One failure-resilient Workspace operation runtime owns the shared gate for topic lifecycle, reroute restore/reclaim, leader/follower provisioning, delayed post-provision reconciliation, disconnect and confirmed-dead cleanup, rename, and display reconciliation; registration cannot enter the live registry until its gated provisioning completes. Timer-owned mutation never inherits expired registration authority: it reacquires profile admission before cleanup and remains blocked without store/API effects behind either retained fence phase. The executor consumes that runtime's exposed gate rather than nesting another lock. A successor leader cannot execute the old epoch directly. It may durably adopt exactly one intent into its current epoch only when the profile, complete binding snapshot, and fresh protection recheck still match; known persistence failure restores the old intent and request time. Leader composition now exposes external protection from exact live registry targets, active/queued local targets, shared and historical follower journals, and profile-exact discovery for incomplete legacy history. An accepted item without an exact target for the candidate chat remains unknown. Exact JSON and multipart Bot API operations are counted at the shared direct-client boundary through final settlement; a message-scoped edit/delete without thread identity conservatively protects all bindings in its chat. Historical follower owner keys are decoded before process-birth liveness checks. Exact durable intents block matching claims, activation, title/journal commits, and binding upserts, preventing normal mutation from making their snapshots stale. Closed-but-existing topics do not count as confirmed absence. The former process-local TOCTOU is closed in the isolated ledger/executor and production journal/API/mutation/allocation composition, including delayed post-provision cleanup. Independent post-fix review passed, but production pressure retry and leader execution remain disconnected by release scope; capacity still fails closed instead of evicting. Reuse changes only the letter assignment: routing remains exact numeric target authority, so traffic for the retired target cannot reach the replacement. Recheck eligibility before Telegram mutation and serialize against re-registration. Release the letter only after confirmed deletion or exact already-absent evidence and a successful durable retirement commit. The current confirmed cleanup path records inactivity only after its exact deletion/already-absent result; an unconfirmed API outcome records no inactivity. Unknown API outcomes, failed persistence, owner replacement, and interrupted cleanup retain the reservation and recovery evidence, preventing reassignment or deletion of a replacement target.
178
+ - A retired binding no longer promises its former name/letter on reopen; the Workspace may be provisioned anew. Retained message ownership and journal evidence must never route old work through a recycled letter: full binding generation and target remain mandatory independently of display labels.
179
+ - Implement and validate using isolated stores and mocked Telegram APIs. Do not delete live Threads, migrate live journals/locks manually, publish, or restart operator instances as part of local preparation. Operator smoke on disposable Threads is a separate gate.
149
180
 
150
181
  ## Leader Election
151
182
 
@@ -153,23 +184,23 @@ Leader election is heartbeat-gated and lock-backed. The polling owner checks exa
153
184
 
154
185
  1. On startup, read the Telegram lock.
155
186
  2. If no leader exists, acquire leadership and start polling.
156
- 3. If a live leader exists, register as follower.
187
+ 3. If a live leader exists, restore a remembered profile-scoped exact-`cwd` follower binding automatically; otherwise wait for explicit `/telegram-connect` rather than provisioning a new Thread from startup alone.
157
188
  4. If the leader heartbeat is stale, attempt an atomic leadership takeover; ordinary `/telegram-connect` on a follower is not a leadership move while the leader is live.
158
189
  5. Heartbeat acknowledgements carry the authenticated live follower-slot roster. If several followers detect stale leadership, the lowest observed live slot attempts promotion immediately; higher slots defer one bounded election grace and re-check the lock. Atomic compare/write acquisition remains the final ownership authority, and a missing lower-slot follower cannot block a higher survivor beyond that grace.
159
190
 
160
- Followers first try to re-register after leader reload or unknown-heartbeat responses, carrying their last known target, slot, and thread name so the new leader can reuse the same binding. Follower Bot API calls already admitted by the active Pi turn wait for that bounded re-registration and capture its new exact generation before entering transport; they do not fail merely because recovery temporarily cleared local registration, and they never replay after an ambiguous transport commit. After the grace window followers promote only when the exact observed leader lease has become stale or inactive; an unavailable IPC endpoint never authorizes replacing a still-live owner. If the exact carried target is absent from persisted bindings, the leader first runs the same synchronous visibility probe: success recovers it instead of creating another Telegram thread, explicit stale evidence provisions a replacement, and ambiguous failure rejects registration. An ambiguous absent-target probe persists only non-routable `probe-required` restoration evidence, so targetless retries and leader reloads must probe that exact target again instead of activating it or provisioning a speculative replacement. A carried slot survives only when that slot remains free. Every successful reuse refreshes the binding timestamp. The leader never restores persisted followers into the live registry speculatively. Absent follower records remain durable restart hints until explicit stale, deleted, offline, or reconciliation evidence invalidates them; only fresh authenticated registration creates live routing authority. This preserves real thread bindings through reload and process-absence gaps without allowing historical records or competing pollers to masquerade as live state.
191
+ Followers first try to re-register after leader reload or unknown-heartbeat responses, carrying their last known target, slot, and thread name so the new leader can reuse the same binding. Follower Bot API calls already admitted by the active Pi turn wait for that bounded re-registration and capture its new exact generation before entering transport; they do not fail merely because recovery temporarily cleared local registration, and they never replay after an ambiguous transport commit. After the grace window followers promote only when the exact observed leader lease has become stale or inactive; an unavailable IPC endpoint never authorizes replacing a still-live owner. If the exact carried target is absent from persisted bindings, the leader first runs the same synchronous visibility probe: success recovers it instead of creating another Telegram thread, explicit stale evidence provisions a replacement, and ambiguous failure rejects registration. An ambiguous absent-target probe persists only non-routable `probe-required` restoration evidence, so targetless retries, successor follower processes with the same exact Workspace claim, and leader reloads probe that exact target again instead of activating it or provisioning a speculative replacement. A carried slot survives only when that slot remains free. Every successful reuse refreshes the binding timestamp. The leader never restores persisted followers into the live registry speculatively. Absent follower records remain durable restart hints until explicit stale, deleted, offline, or reconciliation evidence invalidates them; only fresh authenticated registration creates live routing authority. This preserves real thread bindings through reload and process-absence gaps without allowing historical records or competing pollers to masquerade as live state.
161
192
 
162
193
  ## Leader/Follower Communication
163
194
 
164
195
  ### Protocol identity and compatibility
165
196
 
166
- The local wire contract has protocol version `1`, independent from the npm package version. Follower registration and the leader acknowledgement carry `{ protocolVersion, runtimeBuild, capabilities }`. Capability names are canonical, unique, and sorted. A leader rejects missing or mismatched protocol identity before provisioning a target or publishing the follower into live routing; a strict follower likewise rejects an acknowledgement without compatible leader identity. Different package builds remain compatible when their protocol versions agree. `durable-follower-admission-v1` gates source forwarding, while `queue-handoff-v1` independently gates live semantic queue transfer; every participant in a routed handoff must advertise it.
197
+ The local wire contract has protocol version `1`, independent from the npm package version. Follower registration and the leader acknowledgement carry `{ protocolVersion, runtimeBuild, capabilities }`. Capability names are canonical, unique, and sorted. A leader rejects missing or mismatched protocol identity before provisioning a target or publishing the follower into live routing; a strict follower likewise rejects an acknowledgement without compatible leader identity. Different package builds remain compatible when their protocol versions agree. `durable-follower-admission-v1` gates source forwarding, while `queue-handoff-v1` independently gates live semantic queue transfer; every participant in a routed handoff must advertise it. `workspace-follower-auto-connect-v1` gates restore-only startup admission. `thread-display-mode-v1` gates exact-generation follower display-setting requests and Letters/Directories require compatible connected followers; returning to Names allows legacy peers. Registration checks the current display-mode requirement before provisioning and again before live publication. The leader serializes config persistence and title application. `workspace-thread-rename-v1` independently gates follower rename requests whose exact registration generation is checked before the leader mutates Telegram and persists the Workspace binding.
167
198
 
168
- Negotiated identities remain on the live follower registry and appear in `/telegram-status --debug` plus the observational state snapshot. The Threaded Mode capability monitor owns one in-flight probe across lifecycle generations: stop/restart invalidates a late read, and a replacement monitor waits for the previous request to settle instead of creating overlapping transport transitions. Durable follower admission is authorized only when both peers advertise `durable-follower-admission-v1`, never inferred from package version: a capable runtime rejects missing support before provisioning, inbound routing, or election-roster eligibility. Authentication and exact registration generation remain mandatory independently of protocol compatibility. `follower.register` is the sole bootstrap request and must carry a fresh generation before provisioning; every other request is exact-generation-fenced against a live registry entry. `bus.ack` is response-only and is rejected if submitted as a server request. Leader forwarding never synthesizes authority for an unknown recipient and preserves the follower's exact durable receipt end to end.
199
+ Negotiated identities remain on the live follower registry and appear in `/telegram-status --debug` plus the observational state snapshot. The Threaded Mode capability monitor owns one in-flight probe across lifecycle generations: stop/restart invalidates a late read, and a replacement monitor waits for the previous request to settle instead of creating overlapping transport transitions. Durable follower admission is authorized only when both peers advertise `durable-follower-admission-v1`, never inferred from package version: a capable runtime rejects missing support before provisioning, inbound routing, or election-roster eligibility. Authentication and exact registration generation remain mandatory independently of protocol compatibility. `follower.register` is the explicit bootstrap request and may provision; capability-gated `follower.restoreWorkspace` is startup-only and may claim, probe, or replace a remembered binding but returns `workspace-binding-unavailable` without allocating when no binding or claim-fenced legacy exact-`cwd` record exists. Both carry a fresh generation; every later request is exact-generation-fenced against the live registry entry. `bus.ack` is response-only and is rejected if submitted as a server request. Leader forwarding never synthesizes authority for an unknown recipient and preserves the follower's exact durable receipt end to end.
169
200
 
170
201
  Foreign update forwarding returns an explicit `accepted`, `retryable`, or `terminal-rejected` settlement. Acceptance requires an acknowledgement for the exact request whose receipt contains the expected stable `deliveryId` and source `update_id`; a callback error popup never substitutes for that receipt. Missing or negative acknowledgements, stale registrations, absent follower context, binding rejection, journal admission failure, and missing or mismatched receipts all retain the leader source. Message, edited-message, reaction, and callback paths share this contract.
171
202
 
172
- The delivery id excludes the replaceable runtime instance and registration generation: it derives from envelope kind, source `update_id`, and the stable manual-follower binding. Stored message ownership carries that binding and may rebind to the current authenticated registration after follower replacement, preserving one retry identity while still fencing each attempt by the current generation. A lost acknowledgement can therefore replay idempotently into the follower journal and becomes accepted only when the exact durable receipt returns.
203
+ The delivery id excludes the replaceable runtime instance and registration generation: it derives from envelope kind, source `update_id`, and the stable manual-follower binding. Stored message ownership carries that binding and may rebind to the current authenticated registration after follower replacement, preserving one retry identity while still fencing each attempt by the current generation. A lost acknowledgement retries with the same delivery identity and becomes accepted only when the exact durable receipt returns. Journal deduplication covers retained entries, not lifetime completion history: after ordinary receipt completion removes an entry, cursorless admission can accept that source again. Stable delivery IDs therefore do not by themselves establish exactly-once execution. Owned local IPC evidence reproduces the gap: a proxy drops actual ACK bytes; real forwarding, authenticated receiving, paired admission, journals and admission workers leave the leader source in retry-wait after follower completion. The next attempt reaches an injected authorized callback handler twice in total, then both journals settle. A queued-message variant likewise re-admits the source under a fresh acquisition after a real worker receipt handoff. This proves repeated handler/queue-admission invocation in the composed local pipeline, not actual Pi queue dispatch, live Telegram effects or model execution. Delivery custody, including role-dependent local replay, must be reconciled before a consume-once guarantee can be claimed; the [design counterexamples](./architecture.md#busjournal-design-acceptance) rule out treating origin disappearance as proof of a specific handoff.
173
204
 
174
205
  Transport leadership does not own already-queued semantics. Each queued journal receipt binds the acquiring runtime instance, OS process birth, session generation, and acquisition. A replacement leader or follower process sees another live process's receipt as foreign and cannot replay or settle it; the original process may complete its local Pi queue after transport moves. Startup preserves foreign and legacy unowned receipts. Before a replacement admission worker starts, tri-state pid/process-birth proof: an absent PID or mismatched stable Linux/macOS birth identity permits recovery, while a live matching owner stays `alive` and Windows or inaccessible birth metadata stays `unverifiable`; both non-dead outcomes preserve the receipt may transactionally recover a dead owner's complete receipt to pending, after which replacement replay creates fresh queue authority. Registration publishes the replacement's exact pid/process-birth identity first; a concurrent recovery that observes it returns `owner-alive`, while live or unverifiable owners remain untouched.
175
206
 
@@ -196,6 +227,25 @@ Cons:
196
227
 
197
228
  Alternative transports such as file-backed mailboxes or an external daemon remain out of the current product boundary. Local IPC is the default internal bus while the public design stays compatible with a future daemon if deployment needs outgrow one host.
198
229
 
230
+ ## 0.45.0 Disposable Operator Acceptance
231
+
232
+ Local evidence is not this operator gate: the initial-title/pending-recovery fixes have passing persisted-store/caller regressions and two successful same-model independent PASS reviews of the final recovery guards (`run:telegram-follower-pending-guard-verification`). Both raw reports and terminal branch evidence were inspected. Review scope covers generic recovery, follower shortcuts, and final Workspace settlement; broader cross-session/persistence conclusions remain bounded by the inspected paths and existing tests, not exhaustive fault injection. Model diversity, live-client behavior, and native Windows evidence are absent.
233
+
234
+ Run this only with an operator-approved disposable bot/profile and disposable Threads. Do not reuse production journals, locks, admission ledgers, accepted work, or retirement intents. Record the package commit/build, Pi version, OS, Telegram client/version, profile name, exact working directories, and test Thread IDs before starting.
235
+
236
+ 1. Start one Pi in directory A, enable private-chat Threaded Mode, and run `/telegram-connect`. Confirm leader ownership, one Thread created directly with the selected display-mode title, the same title in the connected notice and initial Pi status, and no generated-name flash or second polling owner.
237
+ 2. Start a Pi in directory B and connect it. Confirm follower registration rather than takeover, a distinct globally ordered slot/name, the selected title in creation/notice/initial status without waiting for a heartbeat, exact prompt/reply routing, and no traffic in the leader Thread.
238
+ 3. Explicitly connect a second Pi from directory A. Confirm it receives a separate binding/slot without copying the first target. Restart each follower independently and confirm restore-only startup reuses its remembered exact-directory binding without allocating a new Thread.
239
+ 4. Rename the leader and a follower from their respective Telegram Threads with `/name Name`. Confirm each manual override converges in Telegram, Pi status, choosers, notices, and agent-target labels under both Letters and Directories. Reset each override to the current automatic projection; confirm target IDs never change and same-basename directory suffixes remain sticky after a sibling disconnects.
240
+ 5. From leader and follower Threads, exercise ordinary prompts, callback buttons, one file, and one voice response. Confirm each result remains reply-anchored to the originating numeric Thread and no upload, notice, or final is duplicated.
241
+ 6. From `All`, create an unbound disposable Thread. Test forward and Replace/restore separately. Confirm accepted content reaches only the selected live instance, Restore carries the selected identity onto the source target, and only the confirmed old/chooser targets are deleted.
242
+ 7. Replace a follower session, then stop the leader and allow follower promotion. Confirm profile, target, slot, saved name, acknowledged title, accepted queue work, and routing survive without a new Thread or competing poller.
243
+ 8. With cleanup enabled, run confirmed `/telegram-disconnect` and one session-restart cleanup on disposable Threads. Confirm cleanup intent/API/store settlement completes before transport release; on an induced transient failure, the exact intent/slot remains pending rather than deleting or reusing another target.
244
+ 9. Leave pressure retirement untouched: this release intentionally keeps automatic retirement disconnected. Confirm capacity failure is truthful and no Thread is deleted or slot reused merely from pressure, elapsed time, or heartbeat silence.
245
+ 10. Capture `/telegram-status --debug`, relevant redacted runtime logs, screenshots of Telegram titles/routes, and the disposable state snapshots after each role/mode transition. Stop immediately on wrong-target delivery, duplicate publication, unexpected deletion, slot reuse, stale-title authority, split-brain polling, or loss of accepted work.
246
+
247
+ Native Windows must additionally complete the named-pipe and process-lifecycle checklist below. A PASS requires every applicable step with captured client/platform evidence; local tests and mocked Telegram do not substitute for this acceptance.
248
+
199
249
  ## Native Windows Smoke Plan
200
250
 
201
251
  Native Windows support should not require WSL. The baseline transport uses Windows named pipes for leader/follower IPC, but live verification still needs an operator with a native Windows Pi install.
@@ -237,8 +287,8 @@ In Telegram private-chat Threaded Mode:
237
287
  - `All` is an aggregate view, not a process launcher. Explicit new instances use live Pi follower registration: the operator starts Pi in a terminal and runs `/telegram-connect`; owner-created empty threads are observed but not treated as a Pi instance until the user chooses a route or restore action.
238
288
  - The leader proactively creates or reclaims its own thread on startup/activation when Threaded Mode is available, so the visible leader has the same two-way binding as followers.
239
289
  - **Unbound thread detection**: when the owner writes in an unknown `message_thread_id`, the bridge checks effective Threaded Mode state. If the current leader has no active bound thread, that new thread is reclaimed for the leader and the prompt is served locally. Otherwise the bridge preserves prompts and commands in that Telegram thread and shows the complete forward plus replace/restore chooser. Successful forward deletes the chooser and confirmed temporary source through `thread-reconciler`; successful restore always deletes the chooser, rebinds the source, and deletes only the selected instance's replaced old thread. Partial foreign batch delivery retries only remaining messages; incomplete thread or chooser deletion retains a cleanup-only retry control without redispatching routed content or leaving an expired visible button.
240
- - Unknown later threads and threadless prompt messages are not silently routed to the leader and never launch hidden Pi processes. The default and only operator path for a new visible instance is starting a visible second Pi process and letting it register as follower through `/telegram-connect`. A manual follower with the same stable binding identity reclaims its current persisted thread across process restart; only an authenticated live registration becomes routing authority. Explicit stale/deleted observations invalidate that restoration hint before a fresh thread is provisioned.
241
- - Thread lifecycle service messages (`forum_topic_created`, `forum_topic_closed`, `forum_topic_reopened`, deletion/stale send errors) update observations and binding state. Closed/deleted leader or follower threads can be reclaimed or recreated deliberately. Leader startup also probes reused own threads with a non-visible chat action; if Telegram reports the thread closed/deleted, the binding is marked stale and a fresh leader thread is created. Unknown `forum_topic_created` service events are observation-only and are not destructive cleanup proof.
290
+ - Unknown later threads and threadless prompt messages are not silently routed to the leader and never launch hidden Pi processes. The default and only operator path for a new visible instance is starting a visible second Pi process and letting it register as follower through `/telegram-connect`. Returning to the same normalized working-directory Workspace reclaims its persisted Thread after an authenticated process claim and required visibility proof; the dormant binding alone is never routing authority. Explicit stale/deleted observations invalidate that restoration hint before a fresh Thread is provisioned.
291
+ - Thread lifecycle service messages (`forum_topic_created`, `forum_topic_closed`, `forum_topic_reopened`, deletion/stale send errors) update observations and binding state. Closed/deleted leader or follower threads can be reclaimed or recreated deliberately. Current same-process targets remain quiet across reload; reclaiming a dormant Workspace target sends the truthful connected notice as its visibility probe, marks a proven stale target deleted, and provisions a fresh Thread. Unknown `forum_topic_created` service events are observation-only and are not destructive cleanup proof.
242
292
  - Bidirectional binding is a core UX requirement, not an implementation detail: Pi instances actively advertise/remember their thread identity, while the bot observes Telegram-client thread state and reflects it back into instance state. This keeps the system responsive, recognizable, and controllable even when the operator closes tabs, writes from `All`, or a follower later becomes leader.
243
293
 
244
294
  In Telegram private-chat Threaded Mode:
@@ -247,14 +297,15 @@ In Telegram private-chat Threaded Mode:
247
297
  - Each live bound instance gets one visible thread.
248
298
  - Each instance has a durable single-letter slot (`A`-`Z`) assigned by the extension and a bridge-authored `threadName`.
249
299
  - New slots advance through the alphabet and wrap after `Z` only to a free slot, intentionally capping concurrent visible instances to the alphabet without duplicating occupied letters. The compact `bot.lastSlot` cursor persists while its binding remains live or recoverable, including true `Z → A` wraparound. Pending provisions, reservations, and retained restart bindings occupy their slots until explicit stale/deleted evidence invalidates them.
250
- - A follower that later becomes leader keeps its existing slot and thread name; leadership changes are transport role changes, not identity resets. Immediately after follower promotion succeeds, the new leader retains a short-lived process-local handoff bound to its exact Telegram profile owner key and refreshes it before session replacement. The replacement session consumes it only after acquiring leader authority, converts any surviving manual-follower record for that target into the current leader binding, persists the target/slot/name, and only then runs ordinary topic provisioning; this handoff is restoration evidence, never live routing authority.
251
- - Instance-thread names are short and recognizable. Default provisioning chooses one baked 4-6 letter single-word Latin thread name from the assigned slot's five-name palette using provisioning timestamp entropy and creates the Telegram thread with that title immediately. The slot remains internal ordering metadata and is not redundantly included in the thread name. Bare slot titles are fallback/legacy state only; do not prompt agents to self-name and do not expose a rename tool. Existing human-named threads are preserved across reloads and leadership changes when they remain the current live binding. If reload creates a new runtime instance while the previous leader thread is still alive, the new leader should take the next free slot instead of reusing the old slot immediately.
300
+ - Workspace identity is profile-scoped normalized exact `cwd`, independent from process role and lifetime. Readable bounded directory keys are collision-verified against the exact path. Workspace claims reserve a profile-wide letter before asynchronous Bot API work, across both same-directory and different-directory instances; dormant bindings and transient claims reserve their letters. The pure policy uses lowercase `a`–`z`, while the existing transport `slot` field carries the same letter in uppercase. Legacy `instanceSlot` suffixes remain immutable binding-key components, not a second display-slot allocator. Fresh Workspace allocation prefers the first free letter and fails closed at protected capacity; pressure retirement is not yet connected. Claims are transient; successful target/name/slot bindings persist as restart hints. On upgrade, an exact-`cwd` leader record or a manual-follower record matching the current/previous authenticated process is claim-fenced into the first compatible Workspace slot, preserving its target, name, and ordering slot without creating a replacement Thread. After a fully cold restart with several dormant bindings for the same directory and no exact session handoff, the bindings are reclaimed in new claim/opening order; process labels or prior terminal opening order do not identify a particular lowercase slot.
301
+ - A follower that later becomes leader keeps its Workspace target, uppercase slot, and thread name; leadership changes are transport role changes, not identity resets. Immediately after follower promotion succeeds, the new leader retains a short-lived process-local handoff bound to its exact Telegram profile owner key and refreshes it before session replacement. The replacement session consumes it only after acquiring leader authority, converts any surviving manual-follower record for that target into the current leader binding, persists the Workspace target/slot/name under its exact claim, and only then runs ordinary topic provisioning; neither handoff nor dormant binding is live routing authority.
302
+ - Instance-thread names are short and recognizable. Default provisioning chooses an unused baked 4-6 letter single-word Latin name, excluding current bindings, dormant Workspace hints, and in-flight provisions before creating the Telegram Thread. If the assigned slot's five-name palette is occupied, selection continues through the remaining palettes rather than duplicating an identity. The uppercase slot remains separate ordering metadata. Bare slot titles are fallback/legacy state only. Existing human-named Threads are preserved across reloads and leadership changes; a concurrent process in the same Workspace receives the next Workspace suffix and a distinct Thread.
252
303
  - A thread-local `/start` opens that instance's menu.
253
304
  - Prompts typed in a thread route to the owning instance.
254
305
  - Replies, previews, files, voice, and buttons stay in that thread.
255
306
  - Queue controls and reactions affect only that instance target.
256
307
  - Telegram's native `…typing` indicator for real agent work is sent to that instance thread and mirrored to `All`; `All` is the aggregate surface and should show activity when any bound instance is running a Telegram turn, local prompt, or autonomous continuation. Terminal `Active` remains Telegram-turn-specific. Startup/connect/reload/recovery must not send activity by themselves.
257
- - Generic heartbeat pruning remains silent and preserves the thread as a restart hint. With cleanup enabled, a later exact-PID death confirmation may delete it without posting an `Instance offline` notice.
308
+ - Generic heartbeat pruning remains silent and preserves the thread as a restart hint. With cleanup enabled, a later exact-PID death confirmation may delete it without posting an `Instance offline` notice. Confirmed deletion removes live routing authority but retains the Workspace binding's friendly name and uppercase ordering slot; a later authenticated reopen probes the old target, replaces it only on exact stale/deleted evidence, and carries that name and slot onto the new Thread.
258
309
  - If the same binding identity returns, authenticated registration can reclaim the thread after the required visibility proof.
259
310
 
260
311
  ## Bot API Evidence For Private-Chat Threaded Mode
@@ -367,7 +418,7 @@ Rules:
367
418
  - Classic mode is selected by Telegram capability: when private-chat threads are unavailable or disabled, the polling owner uses ordinary single-DM behavior and blocked instances do not register as followers. During a live downgrade from Threaded Mode, the current bus leader becomes the classic polling owner after two 2.5-second capability-monitor probes and followers disconnect; if classic polling restore fails transiently, later monitor ticks retry the restore instead of allowing a follower takeover. Followers must not turn the downgrade into a takeover while active thread bindings prove the singleton owner was already established by the bus leader.
368
419
  - Telegram private-chat Threaded Mode enables local leader/follower behavior automatically. The leader owns `getUpdates`; registered followers route Telegram API work through the leader. `/telegram-connect` registers as follower when a live leader exists and does not offer manual takeover in that state. The TUI status bar reports `telegram leader` or `telegram follower` while idle so transport role is visible without opening diagnostics, and both roles switch to `active`/`compacting` processing labels during local Telegram work. Follower registration is unique by live profile/target: a reload or session replacement must replace stale registry entries rather than leaving multiple routable ids for one Telegram thread, and fallback target ownership must not classify leader records as followers.
369
420
  - The thread chat is the owner's private bot DM (`allowedUserId`); no `topics.chatId` config is needed. Thread names are assigned by the bridge from a baked compact per-slot palette. There is no agent-facing `telegram_rename_thread` tool and no separate user-facing slash command for manual thread renames.
370
- - Thread reuse is extension-owned through current live binding identity; there is no separate `topics` config surface in the active private-chat thread model. Manual followers use instance-scoped internal keys by default so multiple terminal processes in the same cwd can receive separate threads.
421
+ - Thread reuse is extension-owned through profile-scoped Workspace bindings; there is no separate `topics` config surface in the active private-chat thread model. Leader/follower record keys remain live role projections, while normalized exact-`cwd` bindings preserve identity and deterministic same-directory process suffixes across role and process changes.
371
422
  - Thread cleanup remains conservative and centralized: destructive close/delete actions are planned and applied through `thread-reconciler` with proof-before-delete checks, leader-epoch fencing, and retry-preserving failure semantics.
372
423
  - `allowedUserId` remains the primary authorization boundary unless explicit allowlists are added. Forum/group membership alone must not grant control.
373
424
 
@@ -376,12 +427,12 @@ Rules:
376
427
  Current state under the agent dir:
377
428
 
378
429
  - `tmp/telegram/owners.json`: authoritative extension-local transport owners keyed by `default` or named profile. Each owner contains the bus leader identity, capability secret, heartbeat, generation, and cleanup fencing epoch. Mutations serialize through `owners.json.transaction`; followers never write owner slots. The local bus endpoint is derived from the agent directory by default; legacy `busSocketPath` entry fields are tolerated inside current owner records but are not required.
379
- - `tmp/telegram/state.json`: volatile extension+bot observable/debug snapshot, not routing authority. It writes `source: "snapshot"` and `writtenAtMs` so consumers do not confuse it with an authoritative database. Every process on one Telegram profile reads this shared path, but only the active transport owner may persist it; followers become writers only after promotion. Status-only persistence refreshes disk-backed bindings before serialization so an already-loaded stale view cannot erase newer leader records. It mirrors `/telegram-status`-style projections: top-level `bot` stores bot-wide capability state such as `threadMode: "unknown" | "enabled" | "disabled"`; `runtime` identifies leader/follower role, lifecycle activity, and the exact polling phase/progress snapshot; `liveRoster` mirrors followers/current targets/reservations; `diagnostics` mirrors status/debug signals; `threads` stores current routeable bindings; `bot.lastSlot` stores the compact slot cursor used when all current threads are gone; and `reservations` records short-lived slot collision guards.
430
+ - `tmp/telegram/state.json`: volatile extension+bot observable/debug snapshot, not routing authority. It writes `source: "snapshot"` and `writtenAtMs` so consumers do not confuse it with an authoritative database. Every process on one Telegram profile reads this shared path, but only the active transport owner may persist it; followers become writers only after promotion. Status-only persistence refreshes disk-backed bindings before serialization so an already-loaded stale view cannot erase newer leader records. It mirrors `/telegram-status`-style projections: top-level `bot` stores bot-wide capability state such as `threadMode: "unknown" | "enabled" | "disabled"`; `runtime` identifies leader/follower role, lifecycle activity, and the exact polling phase/progress snapshot; `liveRoster` mirrors followers/current targets/reservations; `diagnostics` mirrors status/debug signals; `threads` stores current routeable bindings; `workspaceBindings` stores dormant exact-`cwd` target/name/slot reuse hints; `bot.lastSlot` stores the compact slot cursor used when all current threads are gone; and `reservations` records short-lived slot collision guards.
380
431
  - Local bus endpoints: Unix-like platforms expose stable `tmp/telegram/bus.sock` and `tmp/telegram/followers/*` symlinks backed by private generation sockets; native Windows uses deterministic named pipes under `\\.\pipe\pi-telegram-...`. These are transient IPC endpoints, not durable routing state.
381
432
 
382
433
  If an unclean host shutdown truncates `owners.json`, a profile `state*.json`, or the ownership transaction guard, `/telegram-connect` classifies the damage before recovery. With no verifiable live owner, one cross-process recovery winner quarantines only those damaged disposable artifacts and startup retries once; followers or leaders appearing during the final guarded reread stop the reset. `telegram.json`, `logs*.jsonl`, other profiles' valid state, and unrelated extension data remain untouched. A blocked or failed reset reports which Pi must restart instead of emitting repeated raw parse/transaction errors.
383
434
 
384
- The bridge must not keep a separate durable `telegram-targets.json` history. `state.json` retains current stable manual-follower bindings as restart hints, but they never authorize routing without a matching authenticated live registration. Stale/offline/failed observations are not reusable delivery authority. `sync` remains event-driven assumption reconciliation rather than a full Telegram bot-state mirror because Bot API exposes no complete thread listing surface. Non-current routeable thread bindings are pruned during load/persist; old session records must not be retained just to compute the next slot because `bot.lastSlot` is the only durable cursor. Previous-process leader bindings are treated as occupied TTL-bounded reservations until Telegram confirms deletion: reload/startup may close/delete/probe the old thread, known reservations are retried proactively on leader startup, and if Telegram still accepts the old thread id, the new leader should provision the next free slot (`B`, `C`, …) rather than creating a duplicate same-letter tab or blocking startup on Telegram UI convergence. Routing must use live current threads/follower registry, never reservations. The bus leader provisions its own thread during bus startup/connect and provisions follower threads on `follower.register`; registered followers also live in the leader's in-memory registry and communicate over the local bus socket. The live follower registry can resolve a follower by exact `{ chatId, threadId? }`; the leader uses that target ownership to forward message and edited-message updates to followers, and the follower receiver accepts those updates in addition to callbacks and reactions. Terminal status and `[telegram|thread:name]` resolve the matching current-instance identity through the same target-aware path, preferring registered local metadata over stale shared bindings. Media album grouping and split-text coalescing keys include the thread target, queue reaction mutations can scope by chat/thread to avoid cross-target message-id collisions, active-turn target is exposed for lifecycle cleanup and local direct-tool defaults, transport reply dedup is chat/thread-scoped, stored menu state is keyed by chat/message so callback state lookup cannot collide across chats, and generated button turns plus section prompt/open actions preserve the callback thread target. `telegram_message` and immediate `telegram_attach` delivery can also carry an explicit `thread_id` with `chat_id`; when a follower is registered, their default direct-tool target is the assigned thread target and the bus-aware API runtime routes the send through the leader instead of calling Bot API transport locally.
435
+ The bridge must not keep a separate durable `telegram-targets.json` history. Profile-specific `state.json` retains exact-`cwd` Workspace bindings as restart hints, but they never authorize routing without a matching authenticated process claim and role-appropriate liveness/visibility proof. Stale/offline/failed observations are not reusable delivery authority. `sync` remains event-driven assumption reconciliation rather than a full Telegram bot-state mirror because Bot API exposes no complete thread listing surface. Non-current routeable thread bindings are pruned during load/persist; old session records must not be retained merely to drive allocation. Workspace allocation uses retained binding/claim reservations; `bot.lastSlot` remains a compatibility cursor only for provisioning without Workspace identity. Previous-process leader bindings are treated as occupied TTL-bounded reservations until Telegram confirms deletion: reload/startup may close/delete/probe the old thread, known reservations are retried proactively on leader startup, and if Telegram still accepts the old thread id, the new leader should provision the next free slot (`B`, `C`, …) rather than creating a duplicate same-letter tab or blocking startup on Telegram UI convergence. Routing must use live current threads/follower registry, never reservations. The bus leader provisions its own thread during bus startup/connect and provisions follower threads on `follower.register`; registered followers also live in the leader's in-memory registry and communicate over the local bus socket. The live follower registry can resolve a follower by exact `{ chatId, threadId? }`; the leader uses that target ownership to forward message and edited-message updates to followers, and the follower receiver accepts those updates in addition to callbacks and reactions. Terminal status and `[telegram|thread:name]` resolve the matching current-instance identity through the same target-aware path, preferring registered local metadata over stale shared bindings. Media album grouping and split-text coalescing keys include the thread target, queue reaction mutations can scope by chat/thread to avoid cross-target message-id collisions, active-turn target is exposed for lifecycle cleanup and local direct-tool defaults, transport reply dedup is chat/thread-scoped, stored menu state is keyed by chat/message so callback state lookup cannot collide across chats, and generated button turns plus section prompt/open actions preserve the callback thread target. `telegram_message` and immediate `telegram_attach` delivery can also carry an explicit `thread_id` with `chat_id`; when a follower is registered, their default direct-tool target is the assigned thread target and the bus-aware API runtime routes the send through the leader instead of calling Bot API transport locally.
385
436
 
386
437
  All files containing routing, chat ids, thread ids, or process details use private permissions and represent current state rather than historical target caches.
387
438
 
@@ -402,9 +453,9 @@ All files containing routing, chat ids, thread ids, or process details use priva
402
453
 
403
454
  ### Follower heartbeat is missed
404
455
 
405
- - Leader prunes the follower from the live registry after missed heartbeats, but heartbeat pruning is only immediate liveness bookkeeping. One leader generation owns at most one prune operation; stop makes late endpoint, policy, and cleanup settlement inert, while durable-profile mutation serialization prevents replacement registration from crossing confirmed-dead cleanup.
456
+ - Leader tolerates up to 15 seconds without a follower heartbeat before pruning it from the live registry, so short local event-loop or IPC stalls do not create false routing gaps. A follower response deadline defers its final timeout through one socket poll phase: an acknowledgement already buffered while the Pi/TUI event loop was blocked wins, while a genuinely silent peer still fails in the same event-loop turn. Heartbeat pruning remains liveness bookkeeping. One leader generation owns at most one prune operation; stop makes late endpoint, policy, and cleanup settlement inert, while durable-profile mutation serialization prevents replacement registration from crossing confirmed-dead cleanup.
406
457
  - A missed heartbeat does not delete, close, mark offline, or send a disconnected notice for the follower's Telegram thread binding because the common cause may be leader reload, IPC handoff, or transient reconnect rather than a dead follower.
407
- - Followers treat rejected/missing heartbeat acknowledgements as registration loss: retain the last known target locally, clear registered truth, try to re-register with the current leader, wait a short leader-reload grace window, and retry. They promote only after the exact leader lease becomes stale or inactive; a live owner with an unreachable endpoint leaves the follower disconnected/retrying rather than creating a competing poller.
458
+ - Followers treat rejected/missing heartbeat acknowledgements as registration loss: retain the last known target locally, clear registered truth, show `reconnecting` instead of a healthy `follower` status, try to re-register with the current leader, wait a short leader-reload grace window, and retry. They promote only after the exact leader lease becomes stale or inactive; a live owner with an unreachable endpoint leaves the follower disconnected/retrying rather than creating a competing poller.
408
459
  - Persisted current manual-follower bindings survive abrupt process absence as restoration hints when Thread cleanup is disabled. When enabled, graceful Pi quit requests exact-generation teardown before lifecycle suspension; if that envelope is missed, stale pruning may delete only after the leader's OS confirms the exact registered PID has exited.
409
460
  - Fresh registration sends one compact connected notice in the assigned thread. An exact immediate session handoff uses a target-scoped `sendChatAction` as its synchronous visibility probe, avoiding a duplicate notice while retaining stale/ambiguous recovery; other cross-session restoration keeps the connected notice as its probe.
410
461
  - Registration requires a present generation, and explicit disconnect requires that same exact live generation. Leader-side registration and disconnect mutations serialize per durable follower profile across old and replacement runtime instance IDs, so a replacement registration cannot overtake awaited destructive cleanup and an old disconnect cannot remove its successor's routing authority.
@@ -12,7 +12,7 @@ Every completed `assistant-segment` with `placement: "intermediate"` from a Tele
12
12
 
13
13
  While Telegram is connected, local, autonomous, and unclassified extension follow-up Pi work always projects every completed public block—including visible commentary/checkpoints and the final answer—to the instance's authorized target in source order. There is no projection setting or opt-out: disconnecting Telegram is the boundary that stops this companion surface. Both paths consume normalized complete Activity segments rather than raw token deltas, reasoning, or tool traffic.
14
14
 
15
- Projected blocks use `assistant.rendering` independently of voice policy. Rich mode sends native Rich Markdown and HTML mode keeps the established HTML renderer; assistant-authored `telegram_button` comments are planned into prompt buttons before either renderer runs, while projection does not synthesize voice or attach queued files merely because Rich rendering is active. Ordered admission revalidates the exact target, profile/token transport generation, leader epoch or follower registration generation, and session generation before each send. Active-turn final delivery joins the shared [activity-publication order](./architecture.md), preserving commentary-before-final order without blocking Pi lifecycle completion. A `commit-unknown` outcome never permits replay.
15
+ Projected blocks use `assistant.rendering` independently of voice policy. Rich mode sends native Rich Markdown and HTML mode keeps the established HTML renderer; assistant-authored `telegram_button` comments are planned into prompt buttons before either renderer runs, while projection does not synthesize voice or attach queued files merely because Rich rendering is active. Semantic Rich replies, previews, guest answers, and attachment captions leave Telegram entity detection enabled so valid bot commands and URLs retain native affordances; technical activity remains literal under its separate policy below. Ordered admission revalidates the exact target, profile/token transport generation, leader epoch or follower registration generation, and session generation before each send. Active-turn final delivery joins the shared [activity-publication order](./architecture.md), preserving commentary-before-final order without blocking Pi lifecycle completion. A `commit-unknown` outcome never permits replay.
16
16
 
17
17
  Assistant-message completion seals its preview state: queued follow-up drafts and late updates are suppressed. Native final delivery still waits for the already-issued draft request before sending the permanent answer, so an older draft is not deliberately allowed to overtake the final. Intermediate publication seals and drains its captured preview before sending permanent text. Preview rollover itself sends no permanent message: it carries the preceding delivery boundary and draft identity into the next state without holding the Pi message-start hook. The next draft waits for that publication to settle, including failure or cancellation. Active-turn final delivery captures its preview operations before entering the background queue: it drains only the originating draft, leaves a successor's preview untouched, and cannot wait for a successor publication queued behind itself. Delivery authority is rechecked after the captured flush; if the original preview has been replaced, ordinary final sending remains the queue's responsibility. This does not promise instant delivery or eliminate Telegram/client latency.
18
18
 
@@ -70,9 +70,9 @@ The voice pipeline is detailed below: configured `type: "voice"` handlers first,
70
70
 
71
71
  ### Single Rich attachment result
72
72
 
73
- When `assistant.rendering` is `"rich"`, a Telegram-originated turn that queues exactly one probe-confirmed PNG/JPEG photo, MP4 video, or MP3 audio file through `telegram_attach` can combine that artifact with the final assistant Markdown in one multipart `sendRichMessage` result. The bridge normalizes the Markdown, adds one `tg://photo`, `tg://video`, or `tg://audio` reference, preserves the assigned thread and once-per-turn reply policy, carries assistant-authored inline buttons, and records the returned message id under the exact local/follower ownership scope.
73
+ When `assistant.rendering` is `"rich"`, a Telegram-originated turn that queues exactly one probe-confirmed PNG/JPEG photo, MP4 video, or MP3 audio file through `telegram_attach` can combine that artifact with the final assistant Markdown in one multipart `sendRichMessage` result. The bridge normalizes the Markdown, places one `tg://photo`, `tg://video`, or `tg://audio` reference before the text, preserves the assigned thread and once-per-turn reply policy, carries assistant-authored inline buttons, and records the returned message id under the exact local/follower ownership scope.
74
74
 
75
- The optimization is deliberately narrow. HTML rendering, empty final text, multiple files, documents and other unsupported formats, Guest Mode, explicit `telegram_voice`, voice-preferred turns, and OGG/Opus artifacts retain their established text/attachment/voice paths. A known-safe Rich upload rejection falls back to those paths. A `commit-unknown` transport outcome or a nominally successful upload without a verifiable message id never falls back or replays because the first non-idempotent send may already have committed.
75
+ The optimization is deliberately narrow. HTML rendering, empty final text, multiple files, documents and other unsupported formats, Guest Mode, explicit `telegram_voice`, voice-preferred turns, and OGG/Opus artifacts retain their established text/attachment/voice paths. For a normal active turn, those queued files clear any existing preview and upload before a separate final reply, matching the causal `telegram_attach` → answer order instead of appending files afterward. A known-safe Rich upload rejection falls back to those paths. A `commit-unknown` transport outcome or a nominally successful upload without a verifiable message id never falls back or replays because the first non-idempotent send may already have committed.
76
76
 
77
77
  This behavior does not generate media or alter voice policy. `telegram_attach` still represents an explicit assistant artifact decision, while `hidden`, `mirror`, and `always` continue to decide voice synthesis independently.
78
78
 
@@ -196,9 +196,9 @@ Buttons are built in and do not need a command template because they are pure Te
196
196
  The extension injects prompt guidance by context:
197
197
 
198
198
  - If no bot token is configured, no Telegram bridge suffix is injected.
199
- - For ordinary local/TUI prompts, the compact routing note points to the bundled `telegram-bridge` Skill and forbids Telegram use unless explicitly requested.
200
- - For Telegram-originated turns, the compact note routes the agent to `telegram-bridge`, which owns voice/button/direct-delivery/Threaded Mode/formatting/debug guidance.
201
- - For Telegram-originated turns, write the full technical answer as normal Markdown.
199
+ - For ordinary local/TUI prompts, the compact routing note points to bundled Skills, including portable `show-me`, while forbidding Telegram use unless explicitly requested.
200
+ - For Telegram-originated turns, the compact note routes the agent to `telegram-bridge` for voice/button/direct-delivery/Threaded Mode/formatting/debug guidance and to `show-me` when the user needs a visual explanation of work or behavior.
201
+ - For ordinary Telegram-originated answers, write the full technical answer as phone-width Markdown. `show-me` may instead deliver a focused self-contained HTML artifact when explicitly requested or when spatial density earns it, while keeping the immediate reply useful.
202
202
  - Add `telegram_voice` with positional CML by default or JSON when multiline content, named fields, or escaping requires it. A companion summary is optional, no specific summary format is required.
203
203
  - Add `telegram_button` with a JSON object, JSON matrix, or positional CML. Use a hidden comment for footer placement or a standalone triple-backtick `telegram_button` block for placement between paragraphs. Prefer one matrix for multiple controls. Use `label` plus `prompt`, or `value` when they are identical; `selected_style` and boolean `disabled` are optional. Keep at least one useful enabled action on an interactive surface. A button-only reply may omit parent text because the bridge supplies `☑️ **Choose an option:**` automatically.
204
204
  - For ordinary Telegram-turn replies, do not call transport tools for voice or buttons; the bridge owns delivery, while registered voice synthesis providers own TTS and OGG/Opus conversion. For explicit local/TUI direct sends, `telegram_message` may include top-level `telegram_button` comments in its Markdown text because those buttons are attached to that text message.
@@ -40,12 +40,14 @@ import {
40
40
  Stable commands inside Pi:
41
41
 
42
42
  - `/telegram-setup` — configure/update the bot token.
43
- - `/telegram-connect` — start polling here and acquire external Telegram control ownership. Accepted queue/reply state stays local if ownership later moves elsewhere. A successful command queues a hidden connection-state note for delivery with the agent's next turn without triggering one; it says Telegram is connected and that connectivity alone is not user intent.
43
+ - `/telegram-connect` — start polling here and acquire external Telegram control ownership. `/telegram-connect [profile] as=Name` assigns an optional unique capitalized Latin-word identity only when provisioning a fresh Workspace Thread. Accepted queue/reply state stays local if ownership later moves elsewhere. A successful command queues a hidden connection-state note for delivery with the agent's next turn without triggering one; it says Telegram is connected and that connectivity alone is not user intent.
44
44
  - `/telegram-disconnect` — after destructive confirmation, stop polling and release ownership without deleting or silencing accepted local queue state. A successful command queues the corresponding hidden, non-triggering disconnected context note; cancelled or failed disconnects do not publish a false state transition. In Threaded Mode it deletes this instance's current Telegram thread; a follower waits for its active leader to confirm generation-fenced cleanup before stopping. Graceful Pi `quit` performs the same teardown without prompting, while `reload`, `new`, `resume`, and `fork` preserve same-process handoff.
45
45
  - `/telegram-status` — show connection, polling, execution, queue, and recent event diagnostics; debug output separates poller and worker progress, durable automatic-retry state, exact foreign queued-owner identity, and negotiated protocol/build/capabilities.
46
46
 
47
47
  ### Telegram commands
48
48
 
49
+ - `/name Name` — set the durable manual display name of the current Thread. The routed leader or follower uses the authenticated target-fenced mutation and edits the visible title under either automatic mode. Bare `/name` immediately opens expiring exact-target input; the next valid name is consumed before agent dispatch. Cancel is always available; **Reset to automatic** appears only when a manual name exists. Entering a bare uppercase slot letter such as `A` is also treated as an explicit reset to the current automatic display projection rather than as a manual name.
50
+
49
51
  Stable commands inside the paired Telegram DM:
50
52
 
51
53
  - `/start` — pair when needed and open the main application menu.
@@ -64,9 +66,11 @@ This command surface is a mobile companion subset, not a raw terminal-command br
64
66
  Every assistant-authored HTML comment is transport-private on Telegram: previews and final replies remove `<!-- … -->` blocks regardless of Markdown nesting or which extension owns the comment, while only recognized top-level column-zero comments can activate voice or buttons. Unclosed comment tails are withheld and a comment-only result sends no text message; Pi's terminal transcript remains unchanged.
65
67
 
66
68
  - `telegram_bind({ app, script, argument? } | { app, method, argument? })` installs and initializes one canonical managed Generative App module under `<agent-dir>/genapps/<app>/<app>.mjs`, or invokes one named method on an installed app. Installation rejects silent replacement and noncanonical/symlink sources. Methods receive immutable JSON state, one optional JSON argument, cancellation, revision, and a bounded non-shell process port; successful state changes commit to `state.json` plus `states.jsonl`, while output-only methods leave history unchanged. After one-shot `tgbtn` resolution, a complete `app::method` or `app::method(<strict JSON>)` prompt invokes the installed app before Pi queue admission and sends its planned Markdown/buttons directly; malformed or failed bound actions never fall back to a model prompt. Direct app-output buttons retain hidden source revisions and stale actions fail before method execution; sibling processes serialize transitions and recover dead lock owners. Bound actions send a fresh message by default and retain the clicked button's selected state on its prior surface. A result may opt into `viewMode: "edit"` to replace the callback message and keyboard in place, with one fresh-send fallback only for that explicit action. Agent-mediated initial-surface revisions, process-birth lock proof, automatic refresh, and voice output remain open.
67
- - `telegram_attach(paths, chat_id?, thread_id?, caption?)` is the stable artifact delivery tool for generated files. During Telegram turns it queues files for the active reply; with `assistant.rendering: "rich"`, exactly one PNG/JPEG, MP4, or MP3 artifact plus non-empty final Markdown can become one reply-anchored Rich Message. HTML mode, multiple/unsupported files, Guest Mode, and voice outputs retain their established paths. Outside Telegram turns the tool sends files directly to the paired/default chat, the registered follower's assigned thread, or an explicit `chat_id` plus optional `thread_id` when this Pi instance owns `/telegram-connect` or is registered with the multi-instance bus.
68
- - `telegram_message(text, chat_id?, thread_id?)` sends a direct Telegram Markdown message when this Pi instance owns `/telegram-connect` or is registered with the multi-instance bus. During an active Telegram turn, omitted targeting and an explicit target equal to that turn are rejected so the ordinary final-reply path remains the sole current-target response; an explicit different chat/thread target remains allowed for requested cross-target delivery. Outside active turns, paired/default local/TUI delivery remains unchanged. Top-level `telegram_button` comments inside `text` are parsed with the same planner used for normal replies and attached to that message; buttons are never standalone Telegram messages.
69
- - The bundled `telegram-bridge` Skill owns action syntax, target routing, Threaded Mode, formatting, Generative App operation, and profile-specific debugging guidance. The regular prompt routes applicable turns to that Skill. `telegram_attach`, `telegram_bind`, and `telegram_message` remain registered but are model-active only while this instance owns direct transport or holds a live follower registration; disconnect/loss suppresses their schemas and prompt metadata, and recovery restores only the operator's previously active pi-telegram subset.
69
+ - `telegram_attach(paths, chat_id?, thread_id?, caption?)` is the stable artifact delivery tool for generated files. During Telegram turns it queues files before any separate final text; with `assistant.rendering: "rich"`, exactly one PNG/JPEG, MP4, or MP3 artifact plus non-empty final Markdown can become one reply-anchored Rich Message with media first. HTML mode, multiple/unsupported files, Guest Mode, and voice outputs retain their established paths. Outside Telegram turns the tool sends files directly to the paired/default chat, the registered follower's assigned thread, or an explicit `chat_id` plus optional `thread_id` when this Pi instance owns `/telegram-connect` or is registered with the multi-instance bus.
70
+ - `telegram_channel_post(action, operation_id, markdown?)` edits or deletes one exact `published` record returned by `telegram_channel_posts`. Edit requires Markdown and delete forbids it. The direct leader fences the tool call as outcome-unknown before `editMessageText` or `deleteMessage`, so ambiguous failures are never replayed automatically.
71
+ - `telegram_channel_posts(chat_id?, limit?)` lists newest bounded records from the active profile's agent-owned post journal. It returns publication, edit/delete outcome-unknown, confirmed, and deleted local records only; it never reads or claims completeness for Telegram channel history. This explicit successful listing is the only tool response that exposes retained authored Markdown; channel tool failures use fixed redacted messages.
72
+ - `telegram_message(text, chat_id?, channel?, thread_id?, thread?)` sends a direct Telegram Markdown message when this Pi instance owns `/telegram-connect` or is registered with the multi-instance bus. A public `@username`, or an exact negative numeric channel ID with `channel: true`, is passed as `chat_id` without a local registry; channel delivery requires the direct leader, and Telegram enforces whether the bot has channel posting permission. `thread` accepts a live numeric Thread id or its current acknowledged display title; name matching is case-insensitive and fails closed when absent or ambiguous, while delivery captures the numeric target. During an active Telegram turn, omitted targeting and an explicit target equal to that turn are rejected so the ordinary final-reply path remains the sole current-target response; an explicit different chat/thread target remains allowed. Outside active turns, paired/default local/TUI delivery remains unchanged. Top-level `telegram_button` comments inside `text` are parsed with the same planner used for normal replies and attached to that message; buttons are never standalone Telegram messages.
73
+ - The bundled `telegram-bridge` Skill owns action syntax, target routing, Threaded Mode, formatting, Generative App operation, and profile-specific debugging guidance. The bundled `show-me` Skill owns portable evidence-honest explanations and adapts them to phone-width Markdown or self-contained HTML artifacts when Telegram is the active surface. The regular prompt routes applicable turns to these and the other bundled Skills. `telegram_attach`, `telegram_bind`, and `telegram_message` remain registered but are model-active only while this instance owns direct transport or holds a live follower registration; disconnect/loss suppresses their schemas and prompt metadata, and recovery restores only the operator's previously active pi-telegram subset.
70
74
  - `telegram_voice` hidden comments request Telegram-native voice delivery through `{text}`, `{text|lang}`, `{text|lang|rate}`, or a JSON object. JSON is the fallback for multiline content, named fields, or escaping; equivalent `text` or `value` supplies the spoken payload, with explicit `text` taking precedence.
71
75
  - `telegram_button` hidden comments create footer buttons; standalone column-zero triple-backtick `telegram_button` blocks create button rows between paragraphs in Native Rich Markdown. Both accept the same singleton or mixed JSON/CML matrix and share prompt/app routing. Native rows allow at most eight buttons and must fit one Rich Message chunk; invalid or incomplete blocks register nothing. Drafts hide action fences. HTML compatibility projects fenced controls into the footer. In-body clicks acknowledge without recoloring the Rich body; selected-style highlighting remains footer-only. One marker accepts a JSON object, adaptive JSON/CML matrix, or positional [Compact Matrix Literal](./compact-matrix-literal.md). Named JSON objects and positional cells may coexist in one matrix or row; separators are optional and one trailing comma is tolerated at matrix, row, and JSON-object boundaries. Top-level cells become full-width rows, while nested rows group one or more buttons horizontally without an artificial parser-width cap. CML uses `{value}`, `{label|prompt}`, prompt-only `{|prompt}`, or the corresponding three-atom form with `selected_style`; an omitted label uses the existing prompt-as-label fallback, and the optional third atom requires a non-empty prompt and accepts only `primary`, `success`, or `danger`. A fourth atom accepts `1` or `true` (disabled), and `0` or `false` (enabled), with exact lowercase spelling; an omitted fourth position stays enabled, and the third atom may be empty in this form (`{|Next||1}`). JSON uses boolean `disabled`. Disabled cells need no prompt or selected style: `{Next|||1}` is label-only and `{|||1}` is blank (JSON `{"label":"Next","disabled":true}` and `{"disabled":true}`). The Telegram renderer supplies a non-breaking space only when the label is empty. Disabled cells stay visible but carry `disabled: {}` instead of callback data and register no prompt or bound action; invalid disabled values reject the candidate matrix. It trims atom boundaries and supports only the minimal escapes `\|`, `\}`, and `\\`. Prefer one matrix comment for multiple buttons. Use JSON `label` plus `prompt`, or `value` when both strings are identical. Action markers are colon-free; colon-prefixed payloads are rejected. Use top-level column-zero action wrappers, outside quotes, lists, or enclosing code examples. Ordinary code fences and larger outer fences preserve literal examples; bare JSON/CML in prose never activates.
72
76
 
@@ -86,6 +90,7 @@ interface TelegramBotProfile {
86
90
  botUsername?: string; // runtime-managed
87
91
  botId?: number; // runtime-managed
88
92
  allowedUserId?: number;
93
+ threadDisplayMode?: "letters" | "names" | "directories";
89
94
  }
90
95
 
91
96
  interface TelegramConfig {
@@ -115,6 +120,8 @@ Bot/session identity always persists under `profiles.<name>`. The ordinary setup
115
120
 
116
121
  The file is global across Pi instances and contains configuration only. The per-profile polling/admission cursor is `acceptedThroughUpdateId` in that profile's private durable update journal; it is not a config key. On first connection after this cut, a legacy config cursor is transferred directly into the journal before polling and then removed from config. Journal publication failure preserves the legacy source; config publication failure leaves the journal authoritative so retry is idempotent. Cooperating instances serialize recursive config delta merges through `telegram.json.transaction` and preserve unrelated global/profile changes from newer disk snapshots. A semantically unchanged merge adopts the latest disk state in memory without replacing the file; later commits win when two deltas intentionally change the same leaf. Same-parent temp-file replacement retries bounded transient `EPERM`, `EACCES`, and `EBUSY` destination contention without deleting the live config or leaving transaction serialization. For manual edits, stop or idle the connected instances, publish a complete valid file atomically, and let them reload. A non-transactional editor racing Pi persistence has no same-leaf conflict guarantee.
117
122
 
123
+ Threaded Mode Settings exposes **Thread display** as Letters (default) or Directories. `profiles.<name>.threadDisplayMode` is profile-scoped; absent, invalid, and legacy `names` values resolve to `letters`. The leader serializes preference persistence and title reconciliation, while a follower sends an authenticated `follower.setThreadDisplayMode` request gated by `thread-display-mode-v1` and its exact registration generation. Directories requires compatible connected followers and rechecks compatibility before live publication. Config writes check the originating authority inside the config transaction; mode changes preserve target IDs, slots, generated recovery names, manual overrides, and queue ownership. `/name` mutations carry their originating target through final binding validation. The caller confirms only after application succeeds. A partial failure may leave the preference saved and some titles updated; Settings reports that state and permits retry. Acknowledged follower titles arrive through heartbeat rather than a new read loop.
124
+
118
125
  Hidden/default semantics are represented by absence:
119
126
 
120
127
  - `threads.automaticCleanup` defaults to `true`; graceful Pi quit deletes the instance's bound Threaded Mode tab without prompting but preserves the owner slot as independent restart intent. Set it to `false`, or use `🧹 Thread cleanup` in Telegram Settings, to preserve the tab too. A confirmed `/telegram-disconnect`, unlike quit, clears restart ownership. Settings views and cleanup reload shared config before evaluating this switch, so another live Pi instance's update takes effect without restarting. Confirmed leader/follower teardown persists an exact target/runtime-generation cleanup intent before Telegram deletion; an interrupted attempt remains retryable by the current or successor leader under current authority and clears only after confirmed deletion. A same-profile replacement leader first adopts any still-active binding and cancels its superseded cleanup, so startup never deletes and recreates a reusable thread. If a follower's graceful envelope is missed, the leader may create the same fenced cleanup only after its heartbeat is stale, the OS confirms the exact registered PID no longer exists, cleanup remains enabled, and no replacement registration can overtake deletion. Heartbeat loss alone, live/unknown process liveness, IPC failure, and auth failure remain non-destructive. Invalid-config recovery makes the setting unresolved and therefore skips destructive cleanup. Manual `/telegram-disconnect` keeps its confirmation and teardown behavior regardless of this setting.
@@ -125,7 +132,7 @@ Hidden/default semantics are represented by absence:
125
132
  - Agent activity status is not configurable. While Telegram transport remains authorized, Telegram uses native `sendChatAction(typing)` / product `...active` status as the automatic in-chat work signal for unsettled agent and compaction work. Extension-owned blocking UI prompts pause it and completion resumes it while either work owner remains active.
126
133
  - `assistant.timeInjection` accepts `hidden`, `always`, or `interval` and defaults to `interval` when absent without migrating an explicit stored value. Settings writes the selected value there, including `hidden`; the old `time.injectionMode` key is ignored and is not migrated. `time.interval` remains the optional interval duration in milliseconds.
127
134
 
128
- With `assistant.rendering: "rich"` (the default), assistant Markdown delivery is native: final replies are sent as `InputRichMessage.markdown` via `sendRichMessage`, and draft previews use `sendRichMessageDraft` when a structurally closed preview frame is available. Draft-frame failures are recorded and skipped rather than converted into raw plain preview messages, because partial Markdown can be temporarily invalid while the final answer remains valid. Long native replies are split at Telegram Rich Message transport limits, with oversized fenced code, display-math, and fully wrapped inline-formatting blocks rewrapped per chunk so persisted chunks remain structurally valid. Guest replies use `InputRichMessageContent` in `answerGuestQuery` results. Bridge-owned UI surfaces such as menus, status, queue controls, commands, and sections keep explicit Telegram HTML/plain rendering by default because those texts are authored by the bridge or companion extensions for Telegram UI. Companion extension sections may explicitly request `"markdown"`, `"html"`, or `"plain"` per view. `assistant.rendering: "html"` keeps the compatibility path that converts assistant Markdown to Telegram HTML before ordinary message delivery. The bridge sets `skip_entity_detection: true` for assistant and guest Markdown so technical text such as `/commands`, hashtags, URLs, phone numbers, and card-like numbers does not gain unintended automatic entities; explicit Markdown links still belong in the Markdown source.
135
+ With `assistant.rendering: "rich"` (the default), assistant Markdown delivery is native: final replies are sent as `InputRichMessage.markdown` via `sendRichMessage`, and draft previews use `sendRichMessageDraft` when a structurally closed preview frame is available. Draft-frame failures are recorded and skipped rather than converted into raw plain preview messages, because partial Markdown can be temporarily invalid while the final answer remains valid. Long native replies are split at Telegram Rich Message transport limits, with oversized fenced code, display-math, and fully wrapped inline-formatting blocks rewrapped per chunk so persisted chunks remain structurally valid. Guest replies use `InputRichMessageContent` in `answerGuestQuery` results. Bridge-owned UI surfaces such as menus, status, queue controls, commands, and sections keep explicit Telegram HTML/plain rendering by default because those texts are authored by the bridge or companion extensions for Telegram UI. Companion extension sections may explicitly request `"markdown"`, `"html"`, or `"plain"` per view. `assistant.rendering: "html"` keeps the compatibility path that converts assistant Markdown to Telegram HTML before ordinary message delivery. Assistant replies, previews, guest replies, and Rich attachment captions leave automatic entity detection enabled, so valid Telegram bot commands and URLs in semantic model output retain their native affordances; explicit Markdown links still belong in the Markdown source. Technical thinking/tool evidence remains a separate literal surface that suppresses link previews and automatic entities.
129
136
 
130
137
  Environment variables are stable only where documented in the README: bot-token bootstrap, proxy behavior, agent root, and inbound/outbound file size limits.
131
138
 
@@ -8,6 +8,7 @@ Small standard for inline buttons, menu rows, state controls, cards, and confirm
8
8
  - Put emoji where they help scanning, not everywhere.
9
9
  - Use one strong indicator for current selection; avoid emoji noise on every option.
10
10
  - Match label casing to control role.
11
+ - Keep Telegram bot commands such as `/start` and `/abort` as plain text so clients expose their native command links; bot command names use Telegram-compatible characters and never hyphens. Render Pi TUI commands mentioned inside Telegram HTML, such as `<code>/telegram-connect</code>`, as code so Telegram does not mis-tokenize their hyphenated names; callback alerts remain plain because Telegram does not support rich formatting there.
11
12
  - Prefer minimal, clear configuration UI over exhaustive explanation.
12
13
  - Preserve domain-owned callback prefixes and behavior in the owning module.
13
14
 
@@ -19,7 +20,7 @@ Use emoji as stable semantic markers, not decoration. Emoji carry transportable
19
20
 
20
21
  | Emoji | Meaning | Canonical surfaces | Notes |
21
22
  | --- | --- | --- | --- |
22
- | `🧵` | Telegram/Pi thread routing | Thread chooser headings, unbound-thread warnings, thread lifecycle/status copy | Canonical thread marker. Do not add it to every concrete target button; target buttons use `threadName` or slot fallback. |
23
+ | `🧵` | Telegram/Pi thread routing | Thread chooser headings, unbound-thread warnings, thread lifecycle/status copy | Canonical thread marker. Do not add it to every concrete target button; target buttons use the acknowledged display title or stable-name fallback. |
23
24
  | `📡` | Telegram transport / bridge connection | Instance connected notices, polling/transport role, bridge online copy | Transport is not thread identity; use `🧵` for thread concepts. |
24
25
  | `📊` | Status / overview | `/status` command description, status cards or status rows | Use for status summaries, not queue priority. |
25
26
  | `🤖` | Model selection | `/model`, model menu headings, model status rows | Keep model-control surfaces visually distinct from thinking. |
@@ -231,6 +232,7 @@ Rules:
231
232
  - Explain what the setting does and what the options mean only as much as needed.
232
233
  - Order setting value descriptions exactly like the chooser: rows top-to-bottom and values in a shared row left-to-right. Keep `(default)` on the actual default wherever it falls; default status never changes order.
233
234
  - Keep descriptions short and clear.
235
+ - Automatic Thread display uses the same setting card: current value in `<code>`, then descriptions ordered `letters`, `directories`, with `(default)` only on `letters`. Its vertical chooser marks only the current option. A manual `/name Name` sets the current Thread display name and supersedes either automatic projection until reset; switching automatic mode preserves the slot and override.
234
236
 
235
237
  Examples:
236
238