@llblab/pi-kit 0.27.6 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BACKLOG.md +2 -5
- package/CHANGELOG.md +5 -0
- package/README.md +2 -2
- package/node_modules/@llblab/pi-telegram/AGENTS.md +10 -8
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -9
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +13 -0
- package/node_modules/@llblab/pi-telegram/LICENSE +21 -0
- package/node_modules/@llblab/pi-telegram/README.md +15 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.d.ts +1 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.js +5 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +5 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +6 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-api.js +4 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +54 -56
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +206 -139
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.d.ts +21 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +181 -25
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-transport.d.ts +4 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-transport.js +28 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +52 -56
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +78 -246
- package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.d.ts +1 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.js +23 -26
- package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.d.ts +0 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.js +5 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +0 -23
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +15 -15
- package/node_modules/@llblab/pi-telegram/dist/lib/config.d.ts +0 -17
- package/node_modules/@llblab/pi-telegram/dist/lib/config.js +12 -14
- package/node_modules/@llblab/pi-telegram/dist/lib/delivery.d.ts +0 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/delivery.js +2 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +255 -61
- package/node_modules/@llblab/pi-telegram/dist/lib/generative-apps.d.ts +0 -19
- package/node_modules/@llblab/pi-telegram/dist/lib/inbound.d.ts +0 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/inbound.js +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/journal.d.ts +177 -16
- package/node_modules/@llblab/pi-telegram/dist/lib/journal.js +1115 -249
- package/node_modules/@llblab/pi-telegram/dist/lib/keyboard.d.ts +0 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/keyboard.js +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/locks.d.ts +100 -12
- package/node_modules/@llblab/pi-telegram/dist/lib/locks.js +380 -14
- package/node_modules/@llblab/pi-telegram/dist/lib/logging.d.ts +30 -12
- package/node_modules/@llblab/pi-telegram/dist/lib/logging.js +129 -72
- package/node_modules/@llblab/pi-telegram/dist/lib/media.d.ts +28 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/media.js +26 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.d.ts +0 -11
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.js +8 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.d.ts +0 -18
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.js +18 -18
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.d.ts +4 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.js +12 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/menu.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/menu.js +4 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.d.ts +0 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-buttons.js +1 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.d.ts +1 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.js +4 -21
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound.d.ts +0 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound.js +5 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/paths.d.ts +50 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/paths.js +155 -17
- package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/pi.js +2 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/polling.d.ts +0 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/polling.js +5 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/preview.d.ts +0 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/preview.js +3 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/prompt-templates.d.ts +0 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/prompt-templates.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +3 -15
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +69 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/recovery.d.ts +29 -9
- package/node_modules/@llblab/pi-telegram/dist/lib/recovery.js +104 -33
- package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +0 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +73 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +2206 -307
- package/node_modules/@llblab/pi-telegram/dist/lib/sections.d.ts +0 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/sections.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/status.d.ts +44 -9
- package/node_modules/@llblab/pi-telegram/dist/lib/status.js +149 -27
- package/node_modules/@llblab/pi-telegram/dist/lib/sync.d.ts +0 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +5 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +11 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +34 -23
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.js +18 -21
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.d.ts +32 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.js +191 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +291 -59
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +1647 -369
- package/node_modules/@llblab/pi-telegram/dist/lib/turns.d.ts +0 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/turns.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +184 -28
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +1032 -114
- package/node_modules/@llblab/pi-telegram/dist/lib/wire.d.ts +12 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/wire.js +21 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.d.ts +25 -17
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.js +95 -20
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-identity.d.ts +20 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-identity.js +103 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +43 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +148 -52
- package/node_modules/@llblab/pi-telegram/dist/package.json +6 -6
- package/node_modules/@llblab/pi-telegram/dist/skills/generated-control-surface/SKILL.md +3 -3
- package/node_modules/@llblab/pi-telegram/dist/skills/telegram-bridge/references/diagnosis.md +3 -3
- package/node_modules/@llblab/pi-telegram/docs/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/activity.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +152 -34
- package/node_modules/@llblab/pi-telegram/docs/callback-namespaces.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/delivery.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +2 -2
- package/node_modules/@llblab/pi-telegram/docs/inbound.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +125 -19
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +3 -1
- package/node_modules/@llblab/pi-telegram/docs/ui-style.md +22 -8
- package/node_modules/@llblab/pi-telegram/docs/updates.md +1 -1
- package/node_modules/@llblab/pi-telegram/lib/activity-verbosity.ts +5 -9
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +6 -0
- package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +5 -2
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +243 -224
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +178 -31
- package/node_modules/@llblab/pi-telegram/lib/bus-transport.ts +30 -8
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +110 -334
- package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +29 -29
- package/node_modules/@llblab/pi-telegram/lib/command-templates.ts +5 -5
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +15 -15
- package/node_modules/@llblab/pi-telegram/lib/config.ts +12 -17
- package/node_modules/@llblab/pi-telegram/lib/delivery.ts +2 -8
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +257 -72
- package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +0 -22
- package/node_modules/@llblab/pi-telegram/lib/inbound.ts +2 -2
- package/node_modules/@llblab/pi-telegram/lib/journal.ts +1182 -326
- package/node_modules/@llblab/pi-telegram/lib/keyboard.ts +2 -2
- package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/locks.ts +402 -23
- package/node_modules/@llblab/pi-telegram/lib/logging.ts +151 -101
- package/node_modules/@llblab/pi-telegram/lib/media.ts +51 -9
- package/node_modules/@llblab/pi-telegram/lib/menu-model.ts +8 -8
- package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +18 -18
- package/node_modules/@llblab/pi-telegram/lib/menu-status.ts +11 -0
- package/node_modules/@llblab/pi-telegram/lib/menu-thinking.ts +2 -2
- package/node_modules/@llblab/pi-telegram/lib/menu.ts +5 -1
- package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +1 -10
- package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +5 -24
- package/node_modules/@llblab/pi-telegram/lib/outbound.ts +5 -5
- package/node_modules/@llblab/pi-telegram/lib/paths.ts +177 -17
- package/node_modules/@llblab/pi-telegram/lib/pi.ts +5 -2
- package/node_modules/@llblab/pi-telegram/lib/polling.ts +5 -5
- package/node_modules/@llblab/pi-telegram/lib/preview.ts +3 -3
- package/node_modules/@llblab/pi-telegram/lib/prompt-templates.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +67 -9
- package/node_modules/@llblab/pi-telegram/lib/recovery.ts +104 -44
- package/node_modules/@llblab/pi-telegram/lib/replies.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +1955 -375
- package/node_modules/@llblab/pi-telegram/lib/sections.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/status.ts +156 -36
- package/node_modules/@llblab/pi-telegram/lib/sync.ts +4 -4
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +48 -33
- package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +24 -21
- package/node_modules/@llblab/pi-telegram/lib/thread-naming.ts +267 -9
- package/node_modules/@llblab/pi-telegram/lib/thread-reconciler.ts +3 -1
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +1697 -488
- package/node_modules/@llblab/pi-telegram/lib/turns.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +1072 -151
- package/node_modules/@llblab/pi-telegram/lib/wire.ts +28 -0
- package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +97 -47
- package/node_modules/@llblab/pi-telegram/lib/workspace-identity.ts +147 -0
- package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +188 -89
- package/node_modules/@llblab/pi-telegram/package.json +6 -6
- package/node_modules/@llblab/pi-telegram/scripts/audit-exports.mjs +100 -0
- package/node_modules/@llblab/pi-telegram/scripts/check-downgrade.mjs +80 -43
- package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/SKILL.md +3 -3
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/diagnosis.md +3 -3
- package/package.json +2 -2
|
@@ -46,7 +46,7 @@ Live Telegram routing belongs to the active authenticated Pi instance, while a d
|
|
|
46
46
|
|
|
47
47
|
`/telegram-connect` never launches a hidden or headless Pi process. A long-lived background Pi process can own Telegram only when something else explicitly launched that process and it satisfies the normal lock/runtime rules. Pi `print` and `json` modes stay passive and exit rather than becoming hidden polling owners.
|
|
48
48
|
|
|
49
|
-
Pi session JSONL and pi-telegram runtime JSONL serve different purposes. Pi session files contain model conversation, tool, usage, branch, and compaction entries.
|
|
49
|
+
Pi session JSONL and pi-telegram runtime JSONL serve different purposes. Pi session files contain model conversation, tool, usage, branch, and compaction entries. The shared, profile-labelled `logs.jsonl` contains redacted bridge operations from one or more instances and never become model context. Sharing a Telegram profile or working directory does not by itself merge Pi session identities or model histories.
|
|
50
50
|
|
|
51
51
|
A Telegram prompt is a normal Pi model turn. It inherits the active post-compaction context just like a TUI prompt in the same session; pi-telegram does not promise context isolation or token cost proportional only to the new message. The bundled `telegram-bridge` Skill owns agent operation, while `show-me` owns portable evidence-honest explanation shape plus Telegram phone-width Markdown and browser-artifact adaptation; neither owns the other's transport boundary. A small authority-aware system note routes applicable turns to these and the other bundled Skills. Existing session files created by older versions may still contain historical repeated guidance until session replacement or compaction removes it from active context.
|
|
52
52
|
|
|
@@ -65,8 +65,12 @@ The repository uses a **Flat Domain DAG**:
|
|
|
65
65
|
- `api`: Bot API helpers, retries, uploads/downloads, temp cleanup, byte limits, chat actions, lazy token clients, and API error recording. Its optional Workspace-admission client classifies valid threaded requests exactly, chat-only requests chat-wide, malformed declared targets profile-wide, and targetless methods as outside the fence; JSON and multipart leases span internal retries through final settlement. Admission blocks prevent issuance, while release errors remain protective diagnostics and never convert an already-settled non-idempotent request into replay. Production composition resolves this adapter from the current bot/profile admission runtime.
|
|
66
66
|
- `config` / `setup`: `telegram.json`, bot token setup, named bot/session profiles, first-user pairing, authorization, env fallback, atomic persistence, effective config views, and live config accessors.
|
|
67
67
|
- `locks` / `polling`: extension-local transport owner storage, exact-owner epoch exposure, process-global reload generations, completed-generation suspension evidence, owner-aware polling lifecycle/takeover/follower registration, and classic-vs-Threaded capability orchestration. Polling owns long-poll state, worker-before-poller startup, strict batch validation, journal-before-offset admission, one offset commit per response, and non-awaited worker signaling.
|
|
68
|
-
- `journal`: private profile/bot-scoped raw-update authority with strict v1 identity/schema validation, exact deduplication, bounded transaction-serialized `0600` publication, process/session/acquisition-bound prompt/control receipts, owner-fenced completion, durable retry state, and generic removal that rejects queued or legacy failed authority. Runtime/recovery identity separates token rotation from future proof-gated queue-owner recovery. Read-only follower-journal discovery enumerates canonical profile-exact snapshots and segment roots and reports incomplete evidence for unexpected matching paths. Its optional Workspace-admission adapter conservatively classifies each append batch as exact-target, chat-wide, or profile-wide, holds all leases through publication, and releases acquired partial batches on rejection; production journal bindings resolve that adapter for leader, follower, recipient, and historical-path stores.
|
|
69
|
-
- `
|
|
68
|
+
- `journal`: private profile/bot-scoped raw-update authority with strict v1 identity/schema validation, exact deduplication, bounded transaction-serialized `0600` publication, process/session/acquisition-bound prompt/control receipts, owner-fenced completion, durable retry state, and generic removal that rejects queued or legacy failed authority. Prepared v1 `sourceCompletions` retain bounded acceptance-scoped ACKs atomically with exact removal, survive compaction, and require strict source-handle inspection; runtime consumption remains uncomposed. Runtime/recovery identity separates token rotation from future proof-gated queue-owner recovery. Read-only follower-journal discovery enumerates canonical profile-exact snapshots and segment roots and reports incomplete evidence for unexpected matching paths. Its optional Workspace-admission adapter conservatively classifies each append batch as exact-target, chat-wide, or profile-wide, holds all leases through publication, and releases acquired partial batches on rejection; production journal bindings resolve that adapter for leader, follower, recipient, and historical-path stores.
|
|
69
|
+
- `wire`: Non-coercing shallow decoded-value predicates reused by Config, IPC, journal, admission, shared-state, queue, markup and Restore codecs. `isWireRecord` means non-array object, not a plain-object/prototype or JSON-serializability guarantee; `hasOnlyWireKeys` checks own enumerable string keys without requiring fields or considering inherited, hidden or symbol keys; `isNonNegativeWireInteger` preserves safe-number bounds, including zero; `isNonEmptyWireString` deliberately preserves whitespace. Proxy/inspection errors propagate. Domain schemas, lossless/foreign-evidence checks, normalization, errors, physical protection and effect authority remain with their owners. Assistant action fields use the separate shared `outbound-markup.getTelegramActionString` trim policy for voice/buttons. Decimal-only safe API activity parsing must not inherit the legacy integer coercion used by thread/API compatibility paths; hash framing, canonical sorting, encoding, prefixes and truncation remain protocol-owned rather than a generic SHA wrapper.
|
|
70
|
+
- `thread-naming`: Name/title value policy and the exact-target manual-name dialog under one owner. Owns generated hints, identity normalization/grapheme fallback, palette/entropy selection, distinct identity/manual-display validation, the three-field template formatter and its title adapter, plus expiring session-scoped input/reset/cancel state. Dialog state exists only inside its runtime factory; value-policy calls do not instantiate it. Threads, Routing, Bus leader and extension validation consume this contract directly. Threads keeps old exports and the full provision-request formatter signature through the same typed function reference, not a wrapper. Live occupancy/current-record/slot policy, display projection and API/store effects remain elsewhere. Naming algorithms, palette order, limits, scope/target fencing, expiry and UI copy are unchanged.
|
|
71
|
+
- `workspace-identity`: Exact session/CWD normalization, bounded directory/session keys and local instance-slot encoding. Owns `TelegramWorkspaceBindingIdentity`, the precomputed-key constructor and the shared key-length bound used by collision handling; it depends only on Node crypto/path and existing platform/CWD defaults. Threads, Sync, Routing and both Bus roles consume identity functions directly from this owner; Threads retains old identity exports as compatibility reexports. These value calls do not borrow Threads storage or effect authority. No store, target allocation, admission, callbacks or reverse dependency; constructing a key grants no storage-reference or custody authority.
|
|
72
|
+
- `target`: Pure `{ chatId, threadId? }` address-value identity. `areTelegramTargetsEqual` compares both fields exactly; a missing Thread differs from Thread zero, and object identity is no shortcut. `getTelegramTargetKey` keeps the canonical `private` sentinel. Delivery, activity and Workspace domains reuse these functions; optional-target and differently keyed policies remain local. Address equality never grants transport, execution or deletion authority.
|
|
73
|
+
- `bus` / `bus-api` / `bus-leader` / `bus-follower` / `ownership`: Threaded Mode multi-instance bus contracts, profile-scoped process/endpoint identity, local leader/follower IPC, leader-only orchestration, follower-side manual registration/session runtime, follower-routed Bot API calls, and live message ownership. `bus` owns protocol v1 independently from package build, canonical capabilities, compatibility, process identity, profile-aware endpoints, IPC primitives, and replacement-invalidated non-routing registry observations. Registration rejects missing/mismatched protocol before provisioning while preserving compatible package skew; negotiated identity reaches status/state. Leader runtime, leader envelope handling, follower assembly, and follower registration construction require explicit protocol identity, preventing identity-less composition at both production and low-level runtime boundaries. `bus-leader` owns leader envelope handling and polling/server/prune orchestration; its Workspace-admission assembly holds a chat-wide lease across leader provisioning and profile-wide leases across follower provisioning/registration, disconnect/dead cleanup, leader/follower rename, display preference/reconciliation, and detached post-provision cleanup through its delayed API/store settlement. It consumes the profile operation runtime shared with Sync and routing; admission precedes that process-local mutation gate, fence rejection occurs before mutation, and release follows complete settlement. `bus-follower` owns registration/ack negotiation, active leader-auth/election state, heartbeat, authenticated clients, forwarded receiving, and recovery. Its production assembly holds profile-wide admission across follower-to-leader promotion, so a retained fence rejects it before leadership acquisition or store mutation. Production leader composition resolves the admission assembly at each operation boundary. The proof-aware follower Restore receiver replaces legacy target replacement: the old `leader.replaceFollowerTarget` producer, parser and receiver are removed, so even authenticated old-format requests are rejected before recipient effects. Profile admission surrounds exact retained-operation, authenticated leader/context, registration-generation, session/CWD and canonical binding/slot checks. `apply` updates only local registration after the leader's canonical commit; an already matching target does not switch again. `inspect` never switches targets and reports the observed local target plus readiness. Neither mode persists canonical state, manufactures deletion/probe evidence or dispatches input. Validated loading alone does not authorize the recipient effect. `threads.withWorkspaceRestoreSnapshot` now holds the canonical transaction while checking the exact retained intent, binding protection and recovery evidence, then supplies fresh binding/owner data to a synchronous read-only observation. Both `apply` and `inspect` run their existing recipient checks, local application and readiness construction inside this boundary, with fresh scoped-intent and live-context checks. The callback contract excludes asynchronous observers; neither cached projection nor callback completion grants canonical persistence. A follower with `canPersist: false` can observe but still cannot use the leader's registration publisher. Native regressions reject receipts, binding regression, owner detachment, intent adoption and context replacement arriving after load; valid reobservation never repeats apply. The transaction spans the local effect, releases on failure, and neither receiver mode changes canonical bytes. The leader recipient path uses the same observation: session/CWD/slot and active-owner selection consume its fresh binding/owner rows, not the warm store, and the transaction spans local identity application and readiness construction. Full-capacity native worker fixtures reject post-grant receipts, binding/owner regression and context changes both before first apply and during inspection after a lost apply reply, preserving the pending original and issued grant without undoing an earlier valid apply. These fixtures do not authorize activation or live fault injection. A composed capability-gated controller and authenticated envelope connect this receiver across native IPC, with one transport attempt and exact readiness-response validation; see the [bus contract](./multi-instance-bus.md#protocol-identity-and-compatibility). Successor inspection can verify a changed follower registration or process only against its actual session/CWD, one current canonical owner, the unchanged binding/slot and observed local target; an old local target remains not ready. Successors cannot use the original apply grant. Follower target preparation uses `threads.assertWorkspaceRestoreRegistration` before recovery allocation, visibility probes and owner transfer, after probe awaits, and before returning the prepared target. The read-only precondition checks fresh canonical and recovery evidence under the snapshot transaction; a protected target requires its exact binding key, slot and relocated target, never the old target. Lost evidence or late recovery conflicts cannot fall through to visibility-error recovery or leave a transient claim held. This is not registration/transport authority: callers still own authentication, profile/session/leader/generation checks and Workspace admission. Bootstrap now re-enters profile admission and the shared mutation gate for `commitWorkspaceRestoreRegistration`; declared Workspace bindings are checked again in the publication snapshot, even without retained Restore. The synchronous callback rechecks captured leader epoch, profile and operator before writing. Missing, unused, reused or expired publication authority cannot report success; the post-commit ACK rechecks current registration identity, allowing heartbeat, connection-time and Thread-name metadata changes. Lost post-publication authority never removes independently published state. Native authenticated bootstrap tests cover the real transaction and late recovery/authority changes. The root protocol identity now advertises `workspace-restore-v1`; every participating peer must run this build before its Restore is used.
|
|
70
74
|
- `sync`: demand-driven Telegram reconciliation, mutable sync-slice state, nested provisioning activity, and local assumption policy. It does not own a complete Telegram bot read-model; Bot API lacks a complete topic/thread listing surface. It owns sync slices, invalidation triggers, config-persist invalidation sequencing, exact-target admitted stale-topic API recovery, observation intake, status/debug freshness, paired manual-disconnect/session-restart cleanup assembly, prepared quiescent leader-quit detachment, and reconciliation scheduling across bot identity, pairing assumptions, live target bindings, reservations, and transport health after meaningful observable signals. Production topic lifecycle and disconnect/restart cleanup enter profile-wide admission before the shared Workspace operation gate. Lifecycle admission spans observation-driven store settlement; cleanup admission spans intent publication, Telegram cleanup, binding mutation, durable settlement, and transport release. It should call narrower domain primitives rather than letting `index.ts`, `threads`, or `status` accumulate cross-cutting reconciliation policy.
|
|
71
75
|
- `thread-reconciler`: Threaded Mode control-plane planning for Telegram thread/tab lifecycle. It owns the reconciliation state machine (`stable`, `provisioning`, `sync-required`, `cleanup-required`), pure plans, proof-before-delete rules, pending-provision protection, fresh-creation grace windows, leader-epoch checks, and the single policy authority for destructive thread cleanup actions. It excludes live Telegram API calls, inbound routing, menu rendering, and direct persistence.
|
|
72
76
|
- `thread-cleanup-manager`: Disconnected proof-only admission planning for manual inactive-Workspace cleanup. It emits exact profile/binding/target snapshots only when durable inactivity exists, all live-owner/accepted-work/delivery evidence is `clear`, identities are unique, and no reservation, provision, or cleanup competes. Missing, malformed, duplicate, or `unknown` evidence returns no candidates; age and ordering never create authority. Its disconnected bounded profile/token-scoped work store atomically persists exact candidate snapshots, records one exact Workspace-issued deletion permit as outcome-unknown, rejects mismatched permits, and confirms deleted state idempotently; strict reads reject malformed/private-file ambiguity. A disconnected executor requires an injected exclusive Workspace deletion boundary across fresh planner evidence, exact retained-snapshot comparison, permit acquisition/recording, delete callback, and confirmation. The future fence owner must acquire and recheck in admission-ledger order; a retirement fence cannot be nested inside an active ordinary admission lease. Regressions prove drift stops before permit, unavailable/already-issued state cannot fabricate authority, and ambiguous delete remains outcome-unknown without replay. A production-shaped adapter preserves full Workspace records through external-protection resolution, then snapshots exact cleanup fields, reservations, provisions, and cleanup intents; protection exceptions become `unknown`, while source failures propagate fail-closed. Production Settings exposes **Review inactive tabs** only: profile-wide admission surrounds fresh evidence capture, one canonical 128-bit-digest work-set is retained, and a separate summary reports proven count, explicit no-deletion state, and Back navigation. The exact confirmation callback fits Telegram's 64-byte bound and accepts only canonical work-set IDs. **Clean inactive tabs** renders only when composition supplies a destructive port; production intentionally omits it, so malformed/stale callbacks fail closed and review remains non-destructive. Permit composition must not call `acquireRetirementFence()` because retirement remains pressure-only. The one admission-ledger fence now carries discriminated `pressure-retirement | manual-thread-cleanup` authority, treats legacy missing kind as pressure, exposes `acquireThreadCleanupFence()`, includes kind in exact comparison/permits, and remains profile-singleton. Cleanup-specific adopt/issue/absence/release/complete APIs preserve existing retirement callers and reject cross-kind use. Review admission releases before cleanup-fence acquisition; that fence then spans exact full-record/protection revalidation, sole permit issuance, work-set recording, one delete attempt, absence confirmation, binding/work-set commit, and fence completion. The v1-compatible schema and kind-specific acquire/adopt/issue/absence/release/complete methods are implemented; pressure methods reject manual fences and cleanup methods reject pressure fences. A disconnected permit runtime acquires the cleanup fence, revalidates under it, releases drifted unissued fences, refuses already-issued replay, and retains `commit-ready` until an injected durable commit succeeds. `threads.commitInactiveWorkspaceCleanup()` now removes only an exact full inactive binding after rechecking local records, claims, reservations, provisions, cleanups, and retirement intents; the retained cleanup candidate carries sufficient exact cwd/workspace/instance/global-slot/binding/target/inactivity/update commit identity, and under the retained fence exact absence closes commit-unknown retry without reconstructing the deleted full binding. Commit composition removes the binding before confirming the work-set, and failure retains `commit-ready`. A hidden coordinator validates canonical review identity, resolves the full binding, records the sole permit before one injected delete call, then commits binding, work-set, and fence. A `commit-ready` retry finishes without another delete; ambiguous deletion remains `deletion-issued` and is never replayed. A typed Settings-port adapter exposes this coordinator only when explicitly supplied and reports deleted, outcome-unknown, and blocked counts. Fake-port tests exercise the callback lifecycle and successor recovery: takeover requires injected proof, exact fence identity is preserved, live/unverifiable predecessors fail closed, and `commit-ready` resumes without deletion replay. Cross-process workers prove stale `prepared` contenders call fake transport once: deletion requires successful work-set permit CAS, and redundant fences over deleted entries settle without replay. Work-set pre-rename failure and lost post-rename acknowledgement retain recoverable fence truth; durable `deleted` completes that fence without another transport call. Binding-snapshot pre-rename ownership loss reloads and restores the exact binding, while post-rename acknowledgement loss reloads durable absence as successful commit. Cleanup runtime composition additionally requires candidate/work-set/runtime/fence profile equality before mutation and captures one stable successor owner snapshot for adoption. The optional Settings port reports only redacted exact-authority recovery classes: commit pending, deletion outcome unknown with no retry, or unavailable authority. It exposes no target, path, token, or transport details. Production still omits `cleanInactiveThreads`, so Bot API activation remains separate.
|
|
@@ -74,9 +78,9 @@ The repository uses a **Flat Domain DAG**:
|
|
|
74
78
|
- `workspace-slots`: Pure bounded global-letter selection and pressure-reclamation proposals. It consumes explicit protection/inactivity evidence and never discovers owners, persists state, or performs deletion.
|
|
75
79
|
- `workspace-admission`: Durable profile-scoped reader/writer ledger for cross-process exact-target, chat-wide, and profile-wide admission leases plus one destructive retirement fence. It owns atomic lease/fence transactions, proven-dead process-birth recovery, conservative malformed/ambiguous-state handling, exact successor adoption, retained-slot projection, and the durable `fenced` → `deletion-issued` → `commit-ready` or `deletion-rejected` outcomes; one fence emits at most one deletion permit. Its runtime binding resolves `workspace-admission[.<profile>].json`, stores only the token SHA-256 profile authority, preserves separate named-profile identities across switching, permits changed-token rebind only when the prior ledger is provably empty, and fails closed while foreign leases or a fence remain. Issued fences cannot be released before confirmed absence and durable retirement commit; callers own journal/API/provisioning operations and retirement policy. Production composition supplies admission to journals, JSON/multipart API, leader/follower mutations, topic lifecycle, reroute restoration/reclamation, manual disconnect/session-restart cleanup, exact stale-target recovery, and Thread-store slot reservations; journal-evidence pruning also requires caller-supplied admission. Common async runners and the API adapter reject concurrent reuse of a live operation ID before a second caller can share or release its lease; once the first invocation exits, retry-stable recovery remains available. A 2/2 same-model independent post-fix quorum verified complete production-mutation composition at 0.96 confidence per reviewer. Demand-driven retirement is now operator-authorized and wired to fresh allocation; disposable operator smoke remains distinct from local validation.
|
|
76
80
|
- `workspace-retirement`: Profile/leader-fenced pressure preparation over the store snapshot. It counts standalone reservations, selects one candidate only at full slot capacity, rechecks protection, and persists/resumes an exact durable intent. Workspace bindings accumulate their historical follower-journal routing keys and distinguish complete fresh metadata from incomplete legacy evidence. Its read-only accepted-work policy resolves those binding-specific journals plus the shared leader journal and combines them with local exact targets, failing closed when source coverage or target decoding is incomplete. It can prune a known empty follower-journal key only from complete readable evidence plus explicit writer quiescence under exact binding/profile/epoch fences and an exact-target admission lease held through durable publication. Incomplete legacy bindings consume discovered hashed journals as target-scoped evidence but remain incomplete so every later retirement repeats discovery. The shared Workspace operation runtime serializes topic lifecycle, reroute restoration/reclamation, provisioning, delayed post-provision reconciliation, follower/manual cleanup, rename, and display mutation through one exposed gate. Detached mutation work must reacquire fresh admission rather than inherit a lease already released by its caller. A successor may durably adopt one exact stale-epoch intent after profile/binding/protection revalidation; direct old-epoch execution remains blocked. The isolated executor consumes that gate and requires the durable admission ledger. It acquires or exactly adopts the matching fence, rechecks protection after admissions close, advances to `deletion-issued` before invoking an executor-only `deleteForumTopic` port with the sole permit, and never reissues from that phase. Success or exact absence advances to `commit-ready`; store commit failure retains the fence, and exact completion follows durable binding+intent removal. A successor resolves an issued unknown outcome only through a separate exact-absence probe. Leader composition exposes exact registry, active/queued work, known journals, and profile-exact legacy discovery as protection evidence. Missing queue targets and incomplete reads remain unknown. The common direct Bot API client counts exact JSON/multipart targets until settlement; message-scoped edits/deletes without a thread conservatively protect every binding in their chat. Known historical follower owner keys decode to process-birth identity before liveness checks. Durable intents block matching claims and binding mutations. Fresh leader/follower allocation now uses a serialized capacity wrapper: try ordinary allocation, release its leases on a typed capacity failure, then prepare/adopt/execute retirement under the shared mutation gate and retry once. Restore-only follower startup never evicts. The executor-only direct-client deletion port validates the exact permit and disables both retries and transport fallback. Strict journal protection reads cannot recover/reset evidence. A confirmed `commit-ready` fence can finish after binding removal without another deletion; unknown outcomes remain fenced. See [demand-driven rotation](./multi-instance-bus.md#demand-driven-slot-rotation) for history loss and recovery boundaries.
|
|
77
|
-
- `threads`: Telegram UI thread/tab binding state mapped to Bot API `message_thread_id` / `ForumTopic` transport. Owns exact-`cwd` Workspace bindings and transient claims, first-proven inactivity metadata, fenced non-destructive owner detachment with retained Workspace identity/slot, fail-closed retirement occupancy snapshots, and exact durable retirement intents, leader/current-instance identity state, active-turn → follower → leader target preference, matching status projection assembly, profile-bound same-process handoff, exact-claim global-slot allocation and conservative missing/duplicate legacy migration, collision-safe compact thread-name selection, Workspace-aware rename persistence, and primitive provision helpers. Its optional external-slot source makes every generic allocation, Workspace claim, and occupancy snapshot reserve retained admission-fence slots; malformed, unreadable, or non-uppercase evidence fails allocation closed. Its synchronous provision-commit helper transfers exact targeted creation-title evidence into the claim-committed Workspace binding and consumes matching pending evidence; callers retain admission, epoch checks, and durable publication. It should not turn dormant bindings into routing authority, own destructive cleanup policy, or grow into the general Telegram synchronization domain.
|
|
78
|
-
- `updates` / `routing`: update classification, authorization, callbacks, edits, reactions, forwarding, and inbound composition. `updates` owns production journal workers, leader/follower admission lifecycle construction, binding and settlement selection, queue-handoff projection across recipient journals/admission/IPC/live queue state, process/session queue-owner projection, post-public source binding, exact-signal late settlement, durable receipt readiness, same-process claim reconstruction, and structural worker state. `routing` converts message, callback, guest, section, reroute, and control admissions into exact receipts; its complete unbound-target and reroute restore/reclaim handlers run under the shared profile-wide Workspace operation boundary before store access.
|
|
79
|
-
- `media` / `text-groups` / `time-injection` / `turns` / `inbound`: inbound extraction, rich reply plaintext, grouped debounce, split-text coalescing, optional time context, handlers, and prompt assembly/editing, including the `[guest]` Guest Mode speed note appended to guest turn text. Group replay replaces stale generation-local message/report bindings without duplicating content.
|
|
81
|
+
- `threads`: Telegram UI thread/tab binding state mapped to Bot API `message_thread_id` / `ForumTopic` transport. Owns exact-`cwd` Workspace bindings and transient claims, first-proven inactivity metadata, fenced non-destructive owner detachment with retained Workspace identity/slot, fail-closed retirement occupancy snapshots, and exact durable retirement intents, leader/current-instance identity state, active-turn → follower → leader target preference, matching status projection assembly, profile-bound same-process handoff, exact-claim global-slot allocation and conservative missing/duplicate legacy migration, collision-safe compact thread-name selection, Workspace-aware rename persistence, fenced exact-session target relocation, and primitive provision helpers. Its profile/token/path-bound `workspaceRestore` operations own Restore transitions and publish the exact full source-bound operation in `state[.<profile>].json.workspaceRestore` with binding/owner relocation in one ownership-fenced rename, preserving session, slot, naming preferences and journal evidence. There is one stored commit proof, not two stores exchanging receipts. The same owner enforces strict private regular-file/link/size inspection, scope checks inside publication, capacity and transactional revision CAS. Snapshot `read/relocate/update` mechanics are private; there is no separate Restore module or store factory. POSIX files are `0600`; Windows uses directory ACLs. Restore metadata remains bounded to 26 operations and 1 MiB, within an 8 MiB canonical snapshot. Movement clears stale display/probe evidence without claiming deletion. Conflicting source, binding, slot or targets fail closed. Every supporting snapshot publisher compares the full Restore state and its monotonic revision immediately before rename. It also preserves each retained binding's session identity, slot and relocated target, reserves both targets against other bindings, and rejects owner projections on the old target or conflicting slot. Pending creation and reservation staging reject a retained Restore's binding key, slot or either known target before mutating memory; a creation attempt lacking binding identity also conflicts on the retained owner profile or instance, rather than falling through to another slot. Primitive provisioning therefore cannot publish a conflicting creation intent or call `createForumTopic`, while valid existing-target reuse remains available. Candidate and final-disk validation also reject conflicting pending provisions/reservations, including late evidence, without discarding either intent. Restore publication and transitions inspect the private bounded `.provision-recovery.json` receipt file, matching pending ID, creator instance/profile and leader epoch before considering a recovered target. Both predecessor and candidate references remain protected at final publication. Snapshot publication, Restore transitions and recovery receipt writes serialize through the canonical snapshot transaction before the transport-owner publication fence. Conflicting targets or unreadable recovery evidence block advancement without erasing either source; Restore inspection remains available. Recovery writers never replace a differing receipt or repair damaged JSON, and exact duplicates perform no rewrite. Protected snapshot loading also uses strict recovery-file reads. Before publishing a new Restore, final transactional validation requires a known target for every creation retained in the predecessor snapshot, including entries that an in-memory expiry filter would omit. Canonical targets or exact creator/profile/epoch recovery receipts may establish a non-conflicting target; missing or foreign receipts cannot establish availability. Contradictory canonical/recovered targets fail closed. Loading reads recovery evidence once and, while Restore is retained, validates that exact evidence against the predecessor provisions before replacing any working projection. Conflicting, contradictory or invalid matching receipts reject warm and cold loading without changing disk or the previous projection; retained intent inspection remains available independently. Valid receipts merge before expiry filtering, so a late known creation cannot disappear solely because its original timer elapsed. A failed callback reload remains under the existing worker retry/diagnostic policy without another recipient apply, forwarding or cleanup grant; loading is not effect-time authority. Candidate validation refuses conflicting writes; final disk validation prevents a valid warm projection from silently repairing regressed canonical state. Restore transitions likewise refuse advancement or retirement over inconsistent canonical bindings, while retained intent inspection remains available for source protection. Metadata changes, binding-preserving owner succession and non-destructive owner detachment remain permitted; runtime still authenticates successor session authority. The revision survives final removal, closing empty-record ABA; a read-only Restore observation cannot bless an older binding projection. Warm missing/backward/contradictory evidence, corruption, unsupported schema and revision exhaustion refuse mutation. Old draft receipts or a separate Restore file block rather than migrate or manufacture missing originals. Callers hold Workspace admission and exact source/session/target authority. The initial phase is `relocated`, with no separate `prepared` record or receipt handoff. Subsequent `recipient-issued`, `ready`, source-dispatch and cleanup grants remain distinct; executor adoption never resets issuance. Only authenticated readiness observations may confirm the original recipient or retain a same-session `readyRecipient` successor. Terminal source settlement needs positive journal-owner disposition ACKs, never readiness, queue admission or absence. Canonical `queued` facts remain admission-only; `queue-completed` requires matching queued acceptance/receipt/kind and may upgrade only its proven IDs, leaving siblings nonterminal. Cleanup and retirement reject admission-only sources, including legacy facts; cold terminal receipt facts without acceptance or admission with cleanup fail closed without repair. The prepared `recordSourceAcceptance` stores a separate source-hashed positive execution/recipient result under an issued routing grant; it never counts as settlement or authorizes cleanup/source removal. Exact duplicates are read-only, malformed/contradictory cold evidence is retained without repair, and executor adoption preserves the proof. Follower forwarding composes this pre-report publication from the worker's fresh exact deferred-source hash under its existing dispatch admission; a lost publication reply reconciles only the identical retained proof. Its completion report carries that same hash into journal-owned `removeCompletedExact`; missing/changed sources reject atomically, with no ID-only fallback. Follower Restore now derives its scope hash from the immutable request/operator/retained acceptance, excludes mutable executor/progress, and carries it through admission into atomic source removal plus a scoped journal ACK. The worker requires matching returned and strictly retained evidence with fresh post-publication/inspection authority checks before notification. Production binding composition supplies serialized strict completion ports while preserving ordinary recovery. Follower cold ACK hints now consume exact scoped evidence under fresh admission and authenticated read-only recipient inspection, re-reading after awaits; missing/foreign evidence cannot adopt or settle. Leader command Restore now publishes `completed` acceptance before disposal through a private execution-fenced routed carrier; deferred/queued reports bypass that publisher. Memoized detached evidence handles duplicates without changing the original binding. An optional prepared worker queue-publication barrier now holds readiness through async acceptance and exact receipt/owner reinspection, including grouped and same-process cold reconstruction; serialized production bindings now supply strict full-group v1 queue inspection with exact owner/unoffered source and owner hashes, without recovery or writer admission. Production queued Restore publication now waits for fresh Workspace admission, canonical/live leader identity, exact receipt/source/owner evidence and retained `queued` acceptance, then rechecks receipt and recipient after publication/admission without replay. Same-process worker reconstruction resumes only proof publication, not semantic execution. Native two-source media-group integration now holds whole-receipt readiness through partial proof, changed digest and offered ownership, and resumes only proof on same-process worker reconstruction. A private Restore command carrier now suppresses trailing implicit completion after queued reporting and rejects explicit disposal guards, preserving receipt ownership even if queue commit fails. Native `/continue` keeps one queued continuation through publication faults/reconstruction; `/compact` keeps its confirmation-dialog/completed-source semantics. Direct prompt Restore uses queued admission, not another unqueued completion producer. Cold completed-command ACK continuation now uses fresh admission and exact scope inspection before adoption, read-only canonical current-leader ownership, post-await proof rereads and live/canonical cleanup fences without apply, handler, RPC or disposal replay. Missing/partial/foreign evidence and changed authority preserve protection; actual successor ownership/startup and accepted-work clearance remain fixture preconditions. The strict journal now prepares separate `completeQueuedExact` whole-receipt owner/hash-CAS removal plus atomic scoped ACKs, with complete-group cold continuity and no ordinary/v3 downgrade. It is supplied by the serialized private binding sibling. The worker's explicit scope API still requires full-batch immutable scopes. Cached prepared subsets now require a captured strict full queued-receipt origin inspector: exact complete group/full owner and matching queued-entry hashes precede readiness. Every scoped receipt needs an origin witness. Queue membership is immutable for that owner/acquisition; native scoped ACK publication and cold continuity require complete unoffered group disposal, so matching retained queued-origin witnesses acknowledge that whole receipt after lost replies. Unscoped siblings acquire no marker. The worker consumes those scopes through captured exact disposal/readback capabilities, clearing memory only after returned and retained ACK checks under current binding/context/process/session authority. Required scopes stay sticky; failed or uncertain issuance cannot downgrade to ordinary completion, replay disposal or lend its attempt across worker stop/start. Read-only reconciliation requires every requested exact scope and a proven origin witness for each whole receipt; it never infers ordinary sibling completion from absence. Cold queued ACK hints now share exact leader canonical ownership, pre-adoption scope reads and post-await rereads, publishing terminal receipt facts grouped by receipt/kind without replay. Native cases seed the post-disposal boundary and still supply startup/ownership and work clearance. Production queued acceptance now returns immutable full-receipt scopes through the publication barrier. The worker detaches and retains them before readiness, refuses missing terminal capabilities and uses cached scopes when lifecycle callers complete owned receipts. Its captured post-ACK observer emits a routing hint without requesting Pi dispatch; canonical queue-completed publication still rereads exact proof under fresh admission. Completion-only mux selection permits read-only reconciliation of an issued attempt despite lost execution readiness, never another dispatch or disposal. Subset-scoped sources within one receipt now need strict confirmed whole-receipt queued origin before readiness. Any issued disposition, including pre-write uncertainty, closes execution readiness while permitting completion-only proof reconciliation. Mixed batches of independent whole receipts now settle the scoped group before a separate ordinary group, rechecking owner/context/process/session/binding between them. A component failure cannot roll back another positive ACK. Mux/runtime completion retries reuse exact acknowledged receipt objects for local cleanup only, never readiness or another disposal. Ordinary sources acquire no scoped ACK; their unknown disposition retains ordinary protection. Post-ACK authority loss suppresses a stale scoped worker wake. Native producer/lifecycle fixtures cover grouped, discard, lost ACK/readback and canonical publication interruption; Pi handoff remains supplied. Terminal proof lifetime follows [settlement ordering](./multi-instance-bus.md#restore-settlement-ordering); actual startup was accepted in the operator's 0.52.0 live smoke. Cleanup can be issued only after all originals settle; unknown issuance cannot become not-issued. Exact old-target completion or positively unissued cleanup permits terminal retirement without touching accepted recipient queues or authorizing operation-ID reuse. Lost commit replies reconcile only the exact full operation. These operations perform no transport, source dispatch or deletion. Production resolves this native view for authenticated follower reception, exact unfinished-source startup holds even after the target becomes bound, and pressure-retirement protection. Restore capability is advertised; every participating peer must run 0.52.0 or later. The same scoped Restore snapshot also stores at most 26 source-bound temporary-Thread entries for All commands. Each entry records the exact journal binding and update ID, operator, executor, a unique 128-bit title token, `creating` or `created` phase and the acknowledged operator-chat target. It holds no Workspace binding or slot. `reserveTemporaryThread` publishes `creating` before the caller's single creation request. An existing source entry is returned instead of licensing another creation, and only explicit retirement frees the source. `acknowledgeTemporaryThread` records the target once; `adoptTemporaryThread` and `retireTemporaryThread` are executor-fenced exact CAS transitions that release protection only. Created targets are refused to provisions, reservations, Workspace bindings and owner records, both when staged and at final publication. The only exception is a retained Restore of the same source, which may rebind an existing Workspace and slot to the tab. Transition publication checks this protection against the resulting file, not only its predecessor. Strict parsing rejects malformed tokens, duplicate sources, tokens or targets, empty lists and foreign-chat targets. Followers cannot publish. Routing's `sendAllTabTemporaryThreadChooser` uses this store for a known threadless owner command whenever this process owns a leader epoch (no flag or configuration; each effect rechecks epoch, journal binding, operator and context). Under profile admission it adopts or reuses the source's entry, or reserves one and issues a single `createForumTopic` titled `Route /<command> · <token prefix>`. A positive thread ID is acknowledged; any error or missing ID leaves `creating` as an unknown outcome. The command original is reported deferred, never completed, and stays in the All journal. A created entry publishes the full reroute/restore chooser inside the tab, whose target becomes the pending chooser's `sourceTarget`. An unknown entry is held with an All notice and never retried. A failed publication leaves the source retryable, and a replay or restart reuses the same tab. The All-command age limit still terminally settles a never-presented replay but never expires a presented tab. Forward from the tab uses the existing command dispatch and unbound-Thread cleanup: the command runs once in the selected Pi, routing reports the deferred All original complete, and the tab is removed through `thread-reconciler`. A worker completion observer then retires the entry under fresh profile admission and current temporary-Thread authority, adopting a predecessor executor first; absence of the entry or lost authority leaves it retained. When the fresh source still supports deferred abandonment, the tab's chooser also offers one **Cancel routing** action and states that it keeps the original privately, without sending it to Pi, and removes this tab. That click first commits the existing private retention and discard tombstone. It then makes one `thread-reconciler` removal attempt for the entry's own acknowledged target and retires the entry only after confirmed removal. A skipped or failed removal edits the chooser to say the tab could not be removed; the entry keeps protecting the tab, and nothing repeats the deletion automatically. A duplicate click abandons nothing twice. After restart the source belongs to the worker's first snapshot, so Cancel is not offered. A retained tab is protected against other cleanup. The shared `createTelegramCleanupTargetProtection` used by bus, Sync and Thread lifecycle now calls the store's `listTemporaryThreadTargets()`, a fresh strict read of acknowledged targets that protects on any read failure. Routing's `isRerouteTargetProtected` exempts only the exact own entry (token, source and target) passed by that tab's Forward, Cancel or pending unbound cleanup. For example, a prompt typed inside the tab can be routed, but its Forward cannot remove the tab. After the entry retires, a still-pending Forward cleanup is ordinary unbound cleanup. Restore from the tab passes the same own-entry exemption to its source-ownership check and then uses the shared Restore producer with the tab as destination. `threads` accepts that relocation only for the Restore of the same source. The command is dispatched once to the restored recipient and reported complete, and the completion observer retires the entry. The Workspace keeps its slot on the tab, and the tab is not removed. A native leader fixture proves this path with Restore authority enabled. Without a leader epoch or the stores, the legacy All chooser remains the fallback. The lifecycle sections below own Restore from the tab, cancellation cleanup and cleanup protection; unknown creation or removal outcomes are never released automatically. Its optional external-slot source makes every generic allocation, Workspace claim, and occupancy snapshot reserve retained admission-fence slots; malformed, unreadable, or non-uppercase evidence fails allocation closed. Its synchronous provision-commit helper transfers exact targeted creation-title evidence into the claim-committed Workspace binding and consumes matching pending evidence; callers retain admission, epoch checks, and durable publication. It should not turn dormant bindings into routing authority, own destructive cleanup policy, or grow into the general Telegram synchronization domain.
|
|
82
|
+
- `updates` / `routing`: update classification, authorization, callbacks, edits, reactions, forwarding, and inbound composition. `updates` owns production journal workers, leader/follower admission lifecycle construction, binding and settlement selection, queue-handoff projection across recipient journals/admission/IPC/live queue state, process/session queue-owner projection, post-public source binding, exact-signal late settlement, durable receipt readiness, same-process claim reconstruction, and structural worker state. `routing` converts message, callback, guest, section, reroute, and control admissions into exact receipts; its complete unbound-target and reroute restore/reclaim handlers run under the shared profile-wide Workspace operation boundary before store access. Its composed producer captures original journal references and canonical binding authority, then uses `advanceTelegramWorkspaceRestore` for native transitions and authenticated recipient observations under admission. The leader adapter updates its actual local identity; the follower adapter invokes the native one-attempt controller and publishes its live registry target only after exact readiness validation. Its final synchronous registry write runs through `threads.commitWorkspaceRestoreRegistration`: fresh disk/recovery validation and the caller's current-authority check remain inside the same canonical transaction through publication. The preparation assertion shares that validator but cannot substitute for the commit boundary. A late recovery conflict or disk-only binding regression retains the original and issued recipient grant without publishing new routing authority, forwarding input, deleting a Thread or undoing an already applied recipient target. Readiness and dispatch recheck the canonical relocated target as well as session, generation and the recipient's local target; local readiness cannot substitute for a regressed binding/owner projection. Only fresh issuance selects `apply`; retained or unknown issuance selects read-only `inspect`. Exact operation/session/generation/target/slot proofs and post-await/final-observation checks remain mandatory. Same-session successor readiness never changes the original issuance recipient; foreign sessions, ambiguous owners and regressed local targets stay protected. Lost readiness replies reconcile exact retained proof. The shared control path preserves original selection across uncertainty and limits recipient/dispatch callbacks to one admitted invocation; there is no replaceable producer or exported cleanup callback. Reroute forwarding resolves full live recipient binding/generation authority for the selected target and rechecks it after delivery; only a positive matching delivery ACK reports original-source completion through the admission carrier. Restore first publishes that acceptance from a fresh exact worker-source observation and the current recipient binding/generation; failure retains the original without resending. The report carries a detached source digest through admission; the worker checks current binding/context/process/session identity and requires `journal.removeCompletedExact` to match it transactionally before removal. Unsupported exact-disposal ports fail closed; duplicate ordinary reports cannot downgrade the guard. For scoped Restore reports, only an exact returned ACK plus strict retained readback emits the existing post-removal observer; missing inspection capability blocks disposal, and foreign/missing evidence or post-commit authority changes suppress notification. Exact capabilities are snapshotted at construction, and passed marker/inspection arguments are detached. A report or absent source still is not settlement proof, and lost-ACK continuation requires the strict active-journal scope-reader port. Original journal message targets survive rerouting and are not proof of accepted execution destinations; target-only inspection cannot replace conservative binding-wide accepted-work protection. The existing `workspace-retirement` capture now offers the prepared `requireBindingProvenance` option: relevant binding-associated entries remain protected regardless of original target, while nonempty shared/discovered sources without binding provenance return unknown instead of target-based clearance. Complete empty evidence can clear; incomplete or unreadable evidence cannot. Native queued-receipt and corrupted-family fixtures use strict read-only inspection with real reference leases and prove that capture changes no source files or receipts. The default retirement policy is unchanged. Restore cleanup now invokes this strict capture through the composition root before issuing cleanup and at the existing close/delete protection boundaries. A read-only canonical transaction supplies fresh binding references, unioned with retained predecessor references; cache-only metadata cannot clear work added while close awaited. Missing capture, unavailable snapshots, unknown journals and observation errors (including errors after a clear callback result) remain protective. Both roles have native worker fixtures for queued work whose original target differs, damaged journals and references published during close; accepted recipient work remains intact and skipped deletion never retires the intent or replays an issued grant. Other dispatch/cleanup fixtures explicitly supply a clear-evidence precondition and do not prove journal clearance. No own-source exemption exists: own accepted originals and unclassified shared controls may hold cleanup. A confirmed completion in the same source journal now also wakes a ready operation whose originals already have positive settlements and whose cleanup remains unissued. The wake reacquires current profile admission and authority, re-reads the retained operation and strict protection, and never adds the unrelated completion to its settlements. An unrelated queued receipt, foreign journal, ended authority or missing original settlement cannot supply this wake; unknown protection still holds, and an issued cleanup is never retried. Native worker callback-completion fixtures cover both roles with a separately supplied clearance precondition. The optional `onWorkspaceRestoreRecipientObserved` runtime hook in `bus-leader` uses existing authenticated heartbeat traffic, not a new RPC or completion ACK. Both peers must advertise Restore; missing auth/epoch/operator/profile-reader evidence suppresses observation. Delivery and the callback's currentness fence recheck leader epoch, operator, profile, runtime generation and exact live registration; heartbeat/connection timestamps and names are incidental. Runtime stop invalidates the fence before teardown, and listener settlement ends it. Identical in-flight observations coalesce, while old finalization cannot erase a successor observation. Listeners receive isolated snapshots and must return asynchronous work to retain their fence; the heartbeat ACK does not await listener work, and listener or diagnostic-sink failure cannot turn it into a failed ACK. Native IPC fixtures cover these boundaries, including default-profile identity and stop/restart. The composition root now returns the routing controller's continuation promise to preserve that fence through fresh admission and cleanup. Source evidence and recipient hints use distinct typed inputs: a hint cannot manufacture a source settlement. The recipient-hint path independently matches the live registration, retained ready-recipient instance/session/generation, normalized CWD, slot and target; it selects only ready operations in the current source journal whose original IDs already have positive settlements and whose cleanup is unissued. It rechecks this evidence after acquiring admission and carries both local authority and notification currentness through awaited effects. Eight native worker/IPC cases use strict real-journal inspection rather than supplied clearance: a recipient marker worker positively consumes accepted input without emitting a source-leader completion callback, then heartbeat-driven cleanup resumes once. Independent accepted work remains protective until separately consumed; damaged journals, missing original ACKs and uninspected successor generations cannot clear cleanup. A registration change after close retains the issued grant; an uncertain deletion reply cannot be replayed even by a current matching recipient. These fixtures prove native marker consumption, not live Pi scenario acceptance. Cold-snapshot successor inspection also crosses authenticated native IPC: after a new leader adopts the retained operation, a same-session follower successor answers `inspect` under fresh admission and the read-only canonical transaction without applying a local target, even while the original grant is still `recipient-issued`. Confirmation records only `readyRecipient`; the original recipient, request, source settlements and issued cleanup remain unchanged and cannot be reissued or retired. Foreign sessions, ended epochs and unadopted executors observe nothing. These fixtures supply successor registration publication as a precondition and do not prove startup composition. `advanceTelegramWorkspaceRestore` now adopts a retained operation whose full request and operator exactly match but whose executor differs, provided the caller's authority is current; a lost adoption reply reconciles from the retained operation. Adoption changes only executor authority: a `relocated` operation may still receive its first issuance, while `recipient-issued` and `ready` operations proceed only through inspection, and retained routing/cleanup evidence is never reset, settled or retired. The predecessor executor's later transitions are fenced by the executor CAS. Mismatched requests, other operators and ended authority neither adopt nor rewrite canonical bytes. A chooser re-click reaches this path; a new leader can also adopt a ready forwarded or completed-command Restore through exact scoped-ACK continuation. A new leader does not reconstruct source-bound controls. Restore has one controller: the legacy leader new-slot allocation and follower replacement branches are removed. Without that controller or current Restore authority (an unnegotiated peer or lost leadership), a fresh Restore click is refused before selection or any Restore transition, so the chooser keeps its ordinary routes and Cancel. After restart, root composition holds retained Restore originals as historical inputs. The Historical inputs menu entry and its confirmation submenu are removed; old callbacks are refused without routing or abandonment. Existing exact retained abandonment may still retire an unsent Restore through the cold continuation below. `threads.workspaceRestore.retireAbandoned` requires the exact operation, executor and operator, `ready` phase, absent routing and the complete sorted original set. It releases protection without cleanup: the relocated binding, owner and slot stay on the restored Thread, and the previous Thread is kept. Nothing is delivered, deleted or reissued. An interrupted retirement keeps protection and a later independently completed update may recheck its retained proof. The journal's read-only `inspectAbandonedPending(updateId)` returns proof only when exactly one committed `abandon-<sha256>` discard tombstone exists, the source is no longer pending and its private retention validates against the same binding, entry hash and disposition. Missing or altered retention throws, and inspection never repairs. Root composition exposes this strictly for the active leader journal under an `operator-disposition` reference. Any later completion in that journal selects ready, undispatched Restores and requires committed abandonment evidence for every original, then rechecks under fresh profile admission. Retirement then requires matching journal binding, update and owner authority (`telegram-owner:<operator>`); a predecessor executor is adopted first. Startup alone, journal absence, unverifiable evidence or another owner's authority keep protection. Recipient heartbeat hints also select a follower-owned `recipient-issued` operation when the live same-session registration already names the relocated target, normalized CWD and slot. Under fresh admission, `advanceTelegramWorkspaceRestore` adopts it and asks the follower controller only to `inspect`. Positive readiness records `readyRecipient` without changing the original recipient, and issues no dispatch or cleanup; a lost reply or unready target keeps the grant issued. An old target, foreign session, `relocated` phase, leader owner, other journal or stale hint never reaches the recipient. Once ready and still undispatched, the operation can end only after exact retained abandonment proof covers every original. A source-journal completion similarly selects a leader-owned `recipient-issued` operation issued to another leader instance, when this process has the same session and normalized CWD and its local leader identity already holds the relocated slot and target. After a fresh load and admission it adopts and inspects only. The read-only snapshot must show one current binding at the relocated target and one active owner record for this leader instance. Confirmation records a `readyRecipient` for this instance and session generation; it never calls `setCurrentLeaderIdentity`, dispatch or cleanup. Same-process lost replies stay with the original chooser's inspect path. A still-owned predecessor record, old local target, foreign session or CWD, follower owner, other journal or inactive context keep the grant issued. Both recovery paths call `advanceTelegramWorkspaceRestore` with `inspectOnly`. This also lets them take an operation still in `relocated`: the canonical binding already moved, so a same-session successor starts on the relocated target. In that case, and only when the owner was another instance, the first grant is issued to the observed successor and proven by `inspect`; `apply` never runs. `inspectOnly` never commits a relocation when no operation is retained. A successor still on the old target, or the same leader process that owns the chooser, is not selected. Issued, unready, partially abandoned or gated Restores stay protected. Without the strict abandonment reader, source absence never makes a Restore finishable. Exact evidence for any own-source exemption and interrupted startup integration remain activation gates; journal absence is never source completion evidence. The composed worker-owner observers pass committed queue receipts and completion IDs with the journal binding captured before completion commit. Routing snapshots the proof and generation, then re-enters the existing profile admission/gate without awaiting that continuation inside the dispatch gate. It rereads exact retained operations, records only matching unsettled source IDs, issues old-target cleanup once through its existing owner, and retires only terminal evidence. Routing protects both targets of every retained Restore, including against another Restore and ordinary unbound cleanup. Local queued and active work also protects its target. Cleanup exempts only its own old target under an exact retained issued grant, rechecking that evidence and protection at the reconciler's API boundaries. Known protection prevents grant issuance; protection appearing during cleanup retains the issued attempt without retry. A reconciler skip is not deletion confirmation and cannot retire the operation. Stale authority and unknown issued cleanup stay protected; a lost ACK cannot be reconstructed from absence. The continuation never captures an expired dispatch callback. For a ready forwarded Restore with unissued cleanup, completion/recipient hints now read exact immutable removal scopes under fresh admission through the active-journal `operator-disposition` reference. Positive ACKs authorize exact executor adoption and authenticated follower `inspect`, never apply, handler, forwarding or removal replay. Fresh session/CWD/slot/target and protocol checks gate inspection even when registration generation changes. Every unsettled proof is re-read after the await before canonical settlement; partial/missing evidence cannot release another source. The observed live-recipient fence remains active through cleanup. Missing/foreign receipts cannot re-key intent; interrupted settlement publication resumes on a later hint from unchanged journal evidence. Actual successor registration and accepted-work clearance remain fixture preconditions; terminal ACK lifetime and startup composition are still gated. Producer, controller, receiver and settlement are composed and gated by the negotiated `workspace-restore-v1` capability on both peers. Exact local message ownership keeps a published chooser callback local even after its Thread moves to a follower. Transport stays in `bus*`. Native full-capacity tests drive original and callback updates through the real worker and production producer for both roles.
|
|
83
|
+
- `media` / `text-groups` / `time-injection` / `turns` / `inbound`: inbound extraction, rich reply plaintext, grouped debounce, split-text coalescing, optional time context, handlers, and prompt assembly/editing, including the `[guest]` Guest Mode speed note appended to guest turn text. Group replay replaces stale generation-local message/report bindings without duplicating content. Sticker format follows Bot API `Sticker.is_video` / `is_animated`: static WebP may enter image content, while video WebM and animated TGS remain file attachments with their native extension/MIME and are never read as image payloads. The Sticker object has no documented `mime_type`; frame extraction is not performed.
|
|
80
84
|
- `queue`: queue contracts, transport stamps, lanes, readiness, mutations, dispatch, enqueueing, and lifecycle sequencing. Durable admission uses deterministic receipts, canonical source sets, replay dedupe, multiple folded-history receipts, append-before-dispatch reporting, exact handoff/control/discard settlement, and a readiness gate. Receipt-bearing inactive-profile work is preserved after current-profile work rather than dropped.
|
|
81
85
|
- `runtime`: session-local coordination primitives: counters, flags, setup guard, abort handler, typing timers, dispatch flags, and reset binding.
|
|
82
86
|
- `model` / `menu-model` / `menu-thinking` / `menu-status` / `menu-queue` / `menu-settings` / `menu` / `commands`: model identity, thinking levels, scoped model handling, menu render/callback behavior, slash commands, bot commands, and interactive controls.
|
|
@@ -122,7 +126,7 @@ Mirrored domain regressions live in `/tests/*.test.ts`. Shared test fixtures sho
|
|
|
122
126
|
|
|
123
127
|
## Configuration And Ownership
|
|
124
128
|
|
|
125
|
-
Telegram configuration lives in `~/.pi/agent/telegram.json`. Bot/session identity (`botToken`, `botUsername`, `botId`, `allowedUserId`) persists only under `profiles.default` or `profiles.<name>`; shared handlers and assistant/voice/time settings stay top-level. Per-profile polling/admission state lives only in the durable update journal as `acceptedThroughUpdateId`. Authoritative transport ownership lives separately in the pi-telegram-private `~/.pi/agent/tmp/telegram/
|
|
129
|
+
Telegram configuration lives in `~/.pi/agent/telegram.json`. Bot/session identity (`botToken`, `botUsername`, `botId`, `allowedUserId`) persists only under `profiles.default` or `profiles.<name>`; shared handlers and assistant/voice/time settings stay top-level. Per-profile polling/admission state lives only in the durable update journal as `acceptedThroughUpdateId`. Authoritative transport ownership lives separately in the `transport` section of the pi-telegram-private `~/.pi/agent/tmp/pi-telegram/state.json`, one profile entry per `default` or validated named profile (see [Consolidated Runtime Root](#consolidated-runtime-root)); unrelated extensions never read or write this file.
|
|
126
130
|
|
|
127
131
|
`telegram.json` is one global cross-instance configuration document. Ordinary reads rely on atomic publication and do not take the mutation guard. Every cooperating Pi instance persists only its recursive delta from the snapshot it loaded, merges that delta into the latest disk document inside `telegram.json.transaction`, and publishes atomically only when the semantic result differs; a no-op merge adopts the newer disk snapshot in memory without replacing the file. Unrelated global and profile changes therefore survive stale writers. Two serialized writers changing the same leaf use commit order, so the later local delta wins. A non-transactional external editor cannot participate in that conflict protocol: it should write through same-directory atomic replacement while Pi is idle, then let instances reload; an editor racing the transaction may lose its same-leaf change and must retry from the resulting file.
|
|
128
132
|
|
|
@@ -188,7 +192,7 @@ Automatic approval stays disconnected until schema/migration, lock-order, and wo
|
|
|
188
192
|
|
|
189
193
|
#### Follower Source Policy Candidate
|
|
190
194
|
|
|
191
|
-
**Reviewed design;
|
|
195
|
+
**Reviewed design; not implemented or activated.** Keep cursor-ordered polling on v2 and out-of-order follower inboxes on v1. This is narrower than adding another storage mode or consent ledger: excluded polling entries must never reach forwarding, while new follower admission must require an already persisted exact user owner. Retained unpaired follower work is a reconciliation gate, not permission to discard it.
|
|
192
196
|
|
|
193
197
|
The config prerequisite is implemented as `withPairedUserAdmission(profile, tokenSha256, userId, publish, assertExecutionCurrent?)`: it returns explicit denial for an absent/different owner or invalid sender ID, observes exact persisted authority under config transaction, and refreshes an unpaired cache before the trusted synchronous callback without writing config. Changed local profile/token, a conflicting cached owner, or an unpublished local unpair refuses instead of silently switching authority. Default/named-profile tests cover peer grants, queued settings and local edits, stale guards, rejected publication, and unchanged config bytes. Combined interleavings also cover local unpair after observation and external revocation before queued completion, including a subsequent save that must not recreate the owner. This method is exercised by isolated journal and receiver tests, not by production follower factories, and does not replace receiver provenance or publication-time fences.
|
|
194
198
|
|
|
@@ -208,7 +212,7 @@ Required witnesses before accepting this policy: out-of-order first deliveries s
|
|
|
208
212
|
|
|
209
213
|
#### Strict Journal Inspection Candidate
|
|
210
214
|
|
|
211
|
-
**The isolated reader and local implementation review are complete; consumer
|
|
215
|
+
**The isolated reader and local implementation review are complete; it has no production consumer and is not activated.** `inspectTelegramUpdateJournalFamily({ directory, path, profile, botIdentity, limits })` returns absence or a validated file plus file/byte/work accounting. Its optional `knownBotId` result is a validation constraint, possibly inherited from the caller, not enrichment of the stored identity. It requires canonical paths and available `O_NOFOLLOW`/`O_NONBLOCK` flags, refusing unsupported platforms rather than weakening acquisition. Linux fixtures cover metadata, resource and mutation boundaries; canonicalized fixture roots also pass with a symlink-backed temp directory. On unavailable open flags, tests assert refusal and unchanged evidence instead of expecting successful decoding, with an explicit coverage diagnostic. Synthetic load-time variants cover each missing flag and both together; these are not native Windows or whole-profile readiness evidence. Isolated filesystem probes establish why wrapping the ordinary reader is insufficient: reading an empty foreign-profile journal rewrites its identity; revisioned snapshots skip redundant retained segments even when those segments declare an unsupported schema; follower discovery follows canonical snapshot symlinks outside the scanned directory. Source inspection also shows unbounded `readdirSync()` allocation and separate snapshot/unapplied-segment byte budgets. These are counterexamples to reuse as strict preflight, not evidence that normal legacy recovery has changed.
|
|
212
216
|
|
|
213
217
|
- `Owner and result`: Keep the inspector in `journal`, reuse its schema/entry/receipt validators, and extract shared pure replay only when needed. Inspect one exact journal family first; profile inventory is a later caller. Return validated evidence or absence, not permission, a cached ready flag, or a synthesized empty journal. Do not call `read()`, the current `readCurrentStrict()` closure, recovery, identity rebinding, compaction, publication, or lock-file creation from the inspector.
|
|
214
218
|
- `Acquisition`: Accept an approved canonical directory anchor, a journal path within it, expected profile/token identity, and explicit positive safe-integer file-count, aggregate-byte, per-collection/state-entry and aggregate-work limits. Bound directory enumeration before collecting/sorting names, including ignored entries and a bounded overflow witness. Open only regular non-symlink files; reject linked path components beneath the anchor, unexpected segment entries, wrong types, uncertain absence, and observable namespace/handle changes. Read bounded bytes from the opened handle, reject invalid UTF-8, and count snapshot plus every retained segment against one aggregate budget before parsing. A size check followed by unbounded `readFileSync(path)` is insufficient.
|
|
@@ -220,21 +224,26 @@ Required witnesses before accepting this policy: out-of-order first deliveries s
|
|
|
220
224
|
|
|
221
225
|
#### Canonical Profile Journal Inventory
|
|
222
226
|
|
|
223
|
-
`inspectTelegramProfileJournalNamespace()`
|
|
227
|
+
`inspectTelegramProfileJournalNamespace()` inventories only the legacy flat polling/follower namespace using the strict family reader; its isolated implementation review is complete. It refuses any `sessions` entry rather than silently certifying the new namespace. Active followers use `sessions/<id>/journal.<recipient hash>[.<profile>].json`. `inspectTelegramSessionJournalNamespace()` shares the same inspection kernel and adds the canonical session tree alongside polling and retained flat recipients; session families have the role-neutral `session` source label. Its read-only native tests cover shared limits, foreign non-reading and post-inspection nested-file changes. Candidate Workspace protection now invokes this strict evidence surface on each capture, even for a binding whose recorded sources are complete, and uses its bounded catalog for discovered current-profile readers. Invalid/incomplete evidence or missing discovered-path resolution yields unknown accepted-work protection. Native tests preserve every source byte, scoped read references and freshness after a later corruption. No tuple pruning or filesystem cleanup is enabled by this integration; private-retention classification, external consumer references and writer closure remain gates. Retention now participates in the shared census and file/byte/work accounting. Every original is classified against its exact journal; protection refuses uncommitted copies or missing committed originals instead of guessing cancellation. A retained-only family without its exact v1 snapshot remains unknown. Committed originals remain preserved and independently reference-audited; classifying them is not permission to erase history. It returns deterministic source roles/paths/evidence, shared accounting and a validation-only bot-ID constraint. Tests cover exact aggregate boundaries, unread foreign contents, segment-only cross-family conflicts and root changes after the last family inspection. It does not certify consumer-reference closure or authorize a grant, migration, cleanup, or startup.
|
|
224
228
|
|
|
225
|
-
- `Names and scope`:
|
|
226
|
-
- `Inventory and limits`: Stream a bounded root census before inspecting families, deduplicate snapshot/segment pairs, always inspect the polling path (including absence), and inspect current-profile follower paths deterministically. Share file/byte/work budgets across families rather than resetting each call. Keep the family collection/state ceiling; inventory work charges at least one unit per family visit, including absence, and otherwise the family's decoded/revalidated collection count. Reject exhaustion before another visit. Re-enumerate within the same root-entry bound and reject observable namespace changes; missing discovered families cannot silently become empty evidence. The census compares metadata for every root entry, including unrelated and foreign files,
|
|
229
|
+
- `Names and scope`: The legacy inventory covers `inbox[.<profile>].json` and `follower-inbox-<16 lowercase hex>[.<profile>].json` paths plus their `.segments` directories. Supported profile namespace components are the current lowercase ASCII alphanumeric names of at most 32 characters; `default` has no suffix. Treat any case-insensitive `inbox` substring as journal-like, even in otherwise unrelated names such as `personal-inbox-notes.txt`. Reject noncanonical aliases, malformed journal-like names, journal-shaped symlinks/wrong types, and sanitizing profile names rather than guessing ownership. Canonically distinct foreign-profile names can be classified without reading their contents; count them and unrelated entries against the directory limit. Never infer a Workspace binding or allocation authority from a filename hash.
|
|
230
|
+
- `Inventory and limits`: Stream a bounded root census before inspecting families, deduplicate snapshot/segment pairs, always inspect the polling path (including absence), and inspect current-profile follower paths deterministically. Share file/byte/work budgets across families rather than resetting each call. Keep the family collection/state ceiling; inventory work charges at least one unit per family visit, including absence, and otherwise the family's decoded/revalidated collection count. Reject exhaustion before another visit. Re-enumerate within the same root-entry bound and reject observable namespace changes; missing discovered families cannot silently become empty evidence. The legacy census compares metadata for every root entry, including unrelated and foreign files. The session-aware census additionally covers session folders, their leaves and every segment/private-original entry, including foreign-profile metadata without opening foreign contents. Directory accounting spans the entire observed tree; each recensus has the same whole-tree ceiling. Observable nested segment edits, new sessions and foreign-file metadata changes also invalidate the result. Operational quiescence must cover this whole observed root, not merely current-profile journal writers. Return source roles, paths, validated evidence and accounting, not a ready flag or partial success.
|
|
227
231
|
- `Cross-family identity`: Carry the validation-only known bot-ID constraint across families as well as across each family's retained files. The family reader may expose that constraint separately from its unchanged snapshot identity; do not enrich stored identities or recovery keys. This must catch contradictory IDs present only in segments while both snapshots omit the field.
|
|
232
|
+
- `Private retention`: `inspectTelegramUpdateJournalRetention()` is a separate read-only evidence surface over one exact v1 journal family and its `.retained` originals, using the existing no-follow bounded acquisition kernel. It validates canonical `abandon-<sha256>.json` names, the full private envelope, known journal identity, entry/disposition digest, unique source IDs and exact committed discard tombstones. No tombstone means `uncommitted`, even when the journal entry is absent; a copy alone never proves cancellation. Contradictory/foreign/damaged/linked evidence or a missing snapshot refuses classification. Reads and recensus share aggregate file/byte/work limits with the snapshot/segments. The session-aware whole-namespace guard now consumes this classification and returns original-preservation facts separately from executable entries. Reverse tombstone-to-original checks reject missing copies; a valid copy without commit stays `uncommitted` and blocks protection clearance. This does not authorize replay, deletion or writer closure; the legacy flat-only inventory continues to refuse private-retention directories.
|
|
233
|
+
- `Archive consumers`: The source audit finds no replay reader of private originals. `updates` invokes `inspectPendingRetention` before dispatch and holds copied/unverifiable pending inputs as `abandoning`; fresh cancellation uses the same copy for exact retry. `extension` retains `inspectAbandonedPending` under an operator-disposition reference for routing. `routing` uses that committed proof to settle an unsent Restore and rechecks every cancelled group before temporary-tab cleanup. A cold `inspectAbandonedPending` still reads the original even after executable entries are gone. Therefore its exact journal tombstone is a continuing durable dependency, not evidence that the archive is disposable. Retention inspection returns the validated original `journalBindingKey` and `failureId` alongside its path/update ID; callers must not reconstruct these from the current session, filename hash or a changed bot identity. This audit does not close arbitrary out-of-namespace references or writer lifetimes, and does not permit dropping tombstones or originals.
|
|
228
234
|
- `Unclassified storage`: A `recovery` entry initially blocks this inventory without archive traversal. Unknown journal-shaped temporary/legacy residue also blocks. Relaxation requires an audited proof of non-consumption or separately authorized reconciliation, not treating a directory name as permission to ignore accepted work. Arbitrary/out-of-namespace durable consumer references remain a separate mandatory reconciliation gate; a canonical directory census alone cannot establish their absence.
|
|
229
|
-
- `Integration boundary`:
|
|
235
|
+
- `Integration boundary`: Neither strict inventory is a production recipient selector or authorizes session-file cleanup. Production uses exact session recipients and combined discovery without claiming strict namespace closure. Caller-proven serialization/quiescence remains mandatory; repeated metadata/census checks do not defeat hostile same-user substitutions. Tests must cover exact shared-budget boundaries, default/named namespaces, foreign-file non-reading, ambiguous names/types/links, absent and orphaned families, hidden cross-family ID conflicts, observable census changes, unchanged evidence, and unsupported-platform refusal. Reader/writer closure and final grant-time contention follow only after this evidence layer is independently reviewed.
|
|
230
236
|
|
|
231
237
|
#### Source Closure Audit Boundaries
|
|
232
238
|
|
|
233
239
|
Source audit confirms that canonical inventory is not yet usable as a production readiness check:
|
|
234
240
|
|
|
235
|
-
- `Profile path correction`: Historical production wiring passed `(agentDir?, profileName?)` resolvers directly to profile-only ports, allowing a named profile to become a relative `<profile>/tmp/telegram/` root. The authorized bounded inventory inspected every current and retained historical CWD hint for exact candidates using the sole configured/retained `default` profile; no defective-path source existed. The canonical root's 30 v1 families were inspected without repair and retain five queued source records in two follower families, so they remain migration authority rather than empty-state evidence. Production now uses dedicated profile-only polling/admission resolvers that bind `resolveAgentDir()` and produce canonical suffixed paths; a composition invariant rejects the old callback shape. The isolated two-CWD fixture remains as historical counterexample evidence and preserves its queued work and leases. This correction does not establish writer exclusion, migrate canonical v1 custody or activate prepared v3 consumers.
|
|
241
|
+
- `Profile path correction`: Historical production wiring passed `(agentDir?, profileName?)` resolvers directly to profile-only ports, allowing a named profile to become a relative `<profile>/tmp/pi-telegram/` root. The authorized bounded inventory inspected every current and retained historical CWD hint for exact candidates using the sole configured/retained `default` profile; no defective-path source existed. The canonical root's 30 v1 families were inspected without repair and retain five queued source records in two follower families, so they remain migration authority rather than empty-state evidence. Production now uses dedicated profile-only polling/admission resolvers that bind `resolveAgentDir()` and produce canonical suffixed paths; a composition invariant rejects the old callback shape. The isolated two-CWD fixture remains as historical counterexample evidence and preserves its queued work and leases. This correction does not establish writer exclusion, migrate canonical v1 custody or activate prepared v3 consumers.
|
|
236
242
|
- `Prepared reference preflight`: `paths.requireTelegramStoragePathReference(selected, approved)` returns only an already absolute, normalized, exactly matching spelling; relative paths, traversal aliases and different resource/profile paths throw without filesystem access or normalization of the returned reference. The approved path must come from independent caller authority. A corrected resolver output is not evidence that historical references were reconciled. This lexical check proves neither physical identity nor consumer/writer closure, and it does not authorize migration. Production uses the corrected profile-only resolvers. `Storage reference preparation` in `tests/integration.test.ts` reconstructs the historical callback shape only in a disposable fixture: default-profile publication still works, named leader/admission resolution refuses before mutation, and correct follower/recipient paths cannot bypass a mismatched admission location. All fixture files, segments, receipts, leases and directory names remain unchanged after refusal. The no-op-guard negative control fails both this composition test and the mirrored path test. The completed inventory found no defective-path source in the identified scope; moving canonical v1 storage and activating consumers remain separate gates.
|
|
237
243
|
- `Reference coverage`: Inspected receipt/handoff callers select exact active lifecycle bindings rather than opening arbitrary receipt-supplied paths. Historical Workspace keys feed the canonical follower-path hasher; arbitrary path resolver wiring currently receives ordinary discovery results. No automatic reader of quarantined journal contents was found. Provision-recovery metadata and follower state hints do have automatic readers, but do not replay journal entries. None of these observations authorizes relaxing recovery refusal or proves live-reference completeness.
|
|
244
|
+
- `Coordinator reference roots`: Current Workspace bindings are not the only address owners. Retirement intents retain an exact `binding`; Restore operations retain `request.binding` independently of the relocated current binding plus an opaque source journal key and routing acceptance/settlement facts. Temporary tabs retain source keys in membership and cancellation/completion/issuance groups. Terminal membership is not permission to drop the underlying cold proof. Normalized snapshot cloning preserves exact session tuples in these independent roots. Metadata pruning of one current binding therefore cannot establish reference closure across the coordinator. Production abandonment, completion and queued-receipt proof adapters now follow the exact source journal key, not the active session. Historical receipt observation does not acquire readiness; matching durable completion is terminal evidence, while absence, another owner or an offered receipt is not. Actual `/new` and session-boundary lost-reply lifecycle reconciliation remain separate acceptance gates.
|
|
245
|
+
- `Historical journal proof lookup`: The binding runtime exposes narrow `inspectSourceAbandonment`, `inspectSourceCompletion` and `inspectQueuedReceipt` observations by exact journal key, not a historical store or execution port. Completion and whole unoffered receipt matching reuse the ordinary store's private validators/matchers rather than introducing a second proof dialect. It requires a canonical serialized key with the current profile/bot receipt scope and a polling, retained flat or canonical session filename beneath the approved polling root. Exact token-scope originals can remain inspectable after bot-ID discovery. Strict bounded no-follow retention acquisition validates the snapshot, tombstone and original; missing, linked or damaged evidence refuses rather than recovering. Cooperating source serialization surrounds the read, and profile/token/bot/root agreement is rechecked before and after acquisition and after serialization release. `routing` supplies each original group's key; `extension` owns its scoped operator-disposition reference. Native session-switch witnesses use the same recipient hash and update ID in both sessions, proving old cancellation without reading successor custody or publishing active readiness. Prompt/control native fixtures additionally preserve same-hash/same-update-ID/same-receipt-name successor custody, reject wrong owners, partial groups and offers, and inspect durable completion after a caller loses its disposal reply. Returned proof mutation cannot alter the source. This grants neither replay nor cleanup/writer closure.
|
|
246
|
+
- `Prepared address CAS`: `commitWorkspaceJournalEvidence` accepts an optional exact `journalSources` subset. Omission preserves tuples for every existing production caller. Supplied metadata must be a bounded canonical subset of the exact expected binding; additions, malformed records and stale expected snapshots refuse without partially updating legacy keys/completeness. The native persisted/cold Restore fixture demonstrates that current-binding pruning leaves the independent saved Restore addresses intact. This is only an in-memory metadata CAS followed by existing persistence, not a filesystem grant, all-writer transaction or reference-closure proof. No production caller yet supplies a subset; tuple-aware removal and physical cleanup remain gated.
|
|
238
247
|
- `Consumer ordering`: Replacement now snapshots originating runtime/recovery key values before awaited shutdown, rejects changed/missing bindings afterward, and uses a fresh matching descriptor before worker construction or recovery reads. Startup replacement and forced transport replacement have held-stop regressions, an old-code negative control and independent mutable-descriptor probes. Legacy same-key reuse without replacement remains unchanged; matching keys alone do not establish session/registration lifetime or compatibility of captured worker dependencies. Terminal retry and dead-owner cleanup precede worker start. Status and polling bootstrap also use recovery-capable reads. Workspace protection is now consumed by demand-driven slot rotation and uses a strict non-repairing binding reader rather than ordinary recovery-capable `read()`. Source-readiness and migration consumers retain their separate gates.
|
|
239
248
|
- `Quiescence gaps`: Worker stop aborts/awaits draining but can leave unsettled handlers. Config append guards do not serialize other journal mutations or recovery. Whole-root churn also includes transaction staging before acquisition, state staging before owner-fenced rename, logs, admission ledgers, recovery metadata and endpoints. Holding config or owners alone does not quiesce that root. A journal transaction itself creates an `inbox`-containing name rejected by inventory, so acquiring journal locks and then invoking this census is not a supported composition. Metadata stability cannot protect an unguarded interval through grant publication.
|
|
240
249
|
- `Prepared worker source lifetime`: A native config/journal/lifecycle fixture reproduced fresh maintenance reads followed by an old worker's revoked input context, blocking the next input despite unchanged source/runtime keys. The gated v3 resolver now keeps one narrow worker port per actual store handle in a `WeakMap`. Lifecycle snapshots the custody port captured at worker construction: renewing it triggers stop, fresh post-stop binding validation and worker replacement even with equal string keys or an in-place descriptor update. Repeated descriptors over the same store still reuse compatible dependencies; legacy bindings remain unchanged. No persistent key, schema, capability or production activation changed. Native leader-restart and active-follower tests cover renewal and stable reuse; a held-handler fixture preserves the exact durable `running` claim while independent input progresses, then rejects the cancelled origin's late effect. Negative controls prove both captured identity and stable per-store port reuse are required. This is source-handle freshness, not proof of arbitrary callback validity or global readiness; renewed callable lifetimes require a fresh store handle rather than in-place rewriting of its methods.
|
|
@@ -273,7 +282,7 @@ Keeping the current observation contract requires externally established exclusi
|
|
|
273
282
|
|
|
274
283
|
The approved direction separates controlled recovery/migration from live source consumption. Existing full family and whole-profile inspectors retain their current evidence contracts. The locally implemented, independently reviewed and unwired `readTelegramUpdateJournalSource()` shares their family decoder/acquisition machinery and requires selected version `1 | 2`, exact profile/token constraints, and equality between expected and unchanged stored receipt scopes. Its caller must serialize relevant cooperating writers through consumption. Snapshot/segment evidence remains strict; ancestor observations establish canonical directory type and endpoint `dev`/`ino`/`mode`/`uid`/`gid`, plus a final anchor realpath check, not detection of every sibling-entry mutation. Concurrent manual relocation, backup restoration, permission/ACL changes or other storage manipulation outside the protocol is unsupported; transient ancestor-change detection is deliberately not promised. This is a declared narrower contract, not equivalent protection inferred from inode equality. No reader alone proves whole-profile readiness or permits activation.
|
|
275
284
|
|
|
276
|
-
|
|
285
|
+
The legacy `createTelegramPollingStartRecoveryHandler` classifies standalone owners/state/transaction artifacts and supports one guarded deletion/retry attempt; it is not wired into the consolidated production command composition. Its owners-file checks do not establish shared-envelope or sibling-profile authority, so it must not receive the consolidated `state.json` as a recovery target. Current `/telegram-connect` instead uses the separate [damaged-state reset](#damaged-state-reset-operator-approved). Being first or becoming leader does not establish shared-root quiescence. Future journal recovery/preparation belongs in an explicit, authority-fenced startup stage, not incidental live reads; existing journal recovery inside ordinary `readCurrent()` remains unchanged until the new path is integrated. Preserve unknown accepted source evidence rather than treating runtime-artifact recovery as journal migration permission.
|
|
277
286
|
|
|
278
287
|
#### Bus/Journal Design Acceptance
|
|
279
288
|
|
|
@@ -336,20 +345,72 @@ Read-only migration probes constrain that comparison. Publishing a v2 snapshot o
|
|
|
336
345
|
- Pi `print`/`json` run modes stay passive. Inherited child sessions that share `telegram.json` but do not own the exact `pid`/`cwd` slot must not poll or call `getUpdates` unless the operator force-takes ownership.
|
|
337
346
|
- Session replacement through `reload`, `new`, `resume`, or `fork` suspends polling/watchers without releasing ownership or publishing inactivity so the next session in the same process can resume. Late shutdown cannot clear a replacement context even when its identity is reused: the captured session generation must still match. For replacements other than `/resume`, a registered follower snapshots its assigned target into a short-lived same-process handoff, stops the old receiver/heartbeat, and re-registers through the live leader without marking or replacing its Telegram thread. Local `/resume` suppresses that source-target handoff and selects the destination session's binding instead. Hard process termination cannot run graceful teardown, so stale recovery retains its restart-hint path.
|
|
338
347
|
- Live external owners require explicit takeover confirmation. Long-lived timers compare against snapshotted owner identity and stop local transport work when the slot no longer matches.
|
|
339
|
-
- `
|
|
340
|
-
- Exact ownership remains checked every second, while the durable owner heartbeat refresh runs every two seconds and becomes stale after eight seconds.
|
|
341
|
-
- `
|
|
342
|
-
- Ordinary ownership and state mutations fail closed on malformed
|
|
348
|
+
- The profile's `transport` section owns only Telegram transport control. Local extension and accepted queue state remain per Pi instance when ownership moves, but previews, final delivery, dispatch transport mutations, and delayed Bot API work fail closed until exact direct or follower authority becomes valid again.
|
|
349
|
+
- Exact ownership remains checked every second, while the durable owner heartbeat refresh runs every two seconds and becomes stale after eight seconds. Every acquisition, refresh, release, takeover, and stale recovery is one `transport`-section transaction through the shared `runtime/state.json.transaction` guard (see [Consolidated Runtime Root](#consolidated-runtime-root)); it fences stale recovery and delayed release against replacement-owner ABA and fails closed on malformed state, unverifiable ownership, contention timeout, or unsupported filesystem behavior. Publication uses private staging below `runtime/` and atomic rename.
|
|
350
|
+
- `state.json` is authoritative and private: `transport` (ownership), `workspace` (canonical Workspace/Restore evidence) and `admission` per profile. The non-canonical runtime/roster/diagnostics projection is the `runtime` section, published only by the transport owner without touching canonical sections; it and `logs.jsonl` never grant routing authority. The leader reads optional follower slot hints from it; a missing or malformed projection is ignored. Followers remain authenticated bus registrations rather than transport/Workspace/runtime writers; they may publish their process-owned admission leases.
|
|
351
|
+
- Ordinary ownership and state mutations fail closed on a malformed or foreign `state.json`; read-only ownership queries report no owner, not proof of process absence. The legacy whole-file reset is not wired for the shared envelope. A non-election leader start performs the [damaged-state reset](#damaged-state-reset-operator-approved); section-scoped recovery is not planned. Startup best-effort removes obsolete current-runtime root/session `recovery/` directories, never the pre-0.52.0 tree.
|
|
352
|
+
|
|
353
|
+
### Pre-release Module Seam Proposal
|
|
354
|
+
|
|
355
|
+
This bounded structure design is applied locally: Workspace identity has a reusable lower owner, while Thread naming rules reuse the existing `thread-naming` owner rather than creating a second naming domain. The four large effect owners stay in place; every retained move requires its own tests and shared gates. A closed dependency set or file length alone does not justify extraction: show actual cross-domain reuse or a concrete dependency-direction/cycle problem, and prefer an existing cohesive owner.
|
|
356
|
+
|
|
357
|
+
**Decision: shared value contracts under the smallest suitable owner, not domain folders or effect-kernel surgery.**
|
|
358
|
+
|
|
359
|
+
- **Workspace identity → `lib/workspace-identity.ts` (implemented).** Own session/CWD normalization, directory/session keys, local instance-slot encoding and the `TelegramWorkspaceBindingIdentity` contract. The ten-declaration closure in `threads.ts` is about 130 source lines and depends only on its own declarations plus Node crypto/path and the existing platform/CWD defaults. It needs no store, admission, transport, callback bag or reverse import. Keep algorithms, byte limits, exact key spelling, legacy keys and platform behavior unchanged. `threads.ts` imports the lower owner and retains its existing export names as compatibility reexports; the precomputed-key constructor, session-key function and key-length bound remain available to the store without recomputing or reinterpreting canonical evidence. Allocation, reservation, target selection, mutation and protection remain in Threads. Normalization is not storage-reference or custody authority.
|
|
360
|
+
- **Thread name policy → existing `lib/thread-naming.ts` (implemented).** Generated identity normalization, grapheme fallback, palette/entropy selection and distinct identity/manual-display validation share one owner with the existing manual-name dialog. The template formatter and pure title adapter use their actual value inputs, with no nominal import back to Threads. Internal rule consumers import Naming directly; old Threads exports, including the provision-request formatter signature, remain compatibility references to those same functions. Occupied-name collection, current-record policy, slot pressure and rename/provision effects stay in Threads; display projection stays in `thread-display`. Preserve random/entropy edges, palette order, limits, dialog target/scope/expiry semantics and UI-kit grammar. A separate name-policy domain buys no necessary boundary when this existing owner can provide the reusable contract.
|
|
361
|
+
- **Updates: retain worker/admission/custody together for this epic.** The public registry/execution-fence section is a possible later boundary, but its private carrier symbol and admission wrapper are coupled to worker execution. Any future extraction must move the unique carrier owner rather than copy a symbol or borrow an ended grant. A new lower module must not import `updates.ts` for its flow types; registry lifetime, consume/pass order and stale/late settlement require their own cohort. Merely splitting the worker body would create a broad callback interface without clearer ownership.
|
|
362
|
+
- **Journal: retain the transaction core.** `createJournalStoreCore` joins v1 and custody-v3 adapters with snapshot/segment acquisition, queue receipts, source references, compaction and unknown publication outcomes. Moving read/write helpers cannot split this authority. Reference-registry or codec extraction is deferred until its lifetime/type dependency closure improves the graph rather than routing callbacks back into the root.
|
|
363
|
+
- **Routing: retain the chooser/Restore operation closure.** Pending source groups, selection, one-shot issuance, receipt callbacks, cleanup and previous-world disposal share one owner. Extracting visual fragments with the same large dependency bag is not a boundary. The separate assistant-output authority block is coherent but small; no new module is justified by its size alone.
|
|
364
|
+
|
|
365
|
+
**Validation and stop boundary.** Keep the current flat DAG and composition-only `extension.ts`; avoid `types`/`utils` buckets and facade proliferation. Identity tests live with the identity owner; naming value/dialog tests share the Naming suite, while store/Restore tests remain in Threads. Explicit compatibility checks retain old exports and signatures; declaration/body equivalence and focused runtime tests guard behavior. Typecheck, focused and full tests, build/current dist, public API, Domain DAG, context and diff checks gate retained moves. Reassess after these two value-policy boundaries; if remaining splits increase interfaces or risk, finish the structural epic without forcing all four owners into smaller files. Release, reload and live-state work remain outside this proposal.
|
|
366
|
+
|
|
367
|
+
### Consolidated Runtime Root
|
|
368
|
+
|
|
369
|
+
**Layout.** The runtime root `<agentDir>/tmp/pi-telegram` (default `~/.pi/agent/tmp/pi-telegram`) has exactly two persistent root files: `state.json` and `logs.jsonl`. `sessions/<id>/` holds session-owned polling/recipient journals and custody; `attachments/` holds flat download scratch; `journals/` holds non-session service journals (`thread-cleanup` and `channel-posts`, named `<kind>.<full raw-profile SHA-256>.json`); `logs/` holds the rotated previous log; `runtime/` holds transaction guards, private staging, provisioning-recovery sidecars and IPC endpoints. Configuration stays outside the root.
|
|
370
|
+
|
|
371
|
+
**Fresh start, no migration.** Released versions wrote `tmp/telegram`; 0.52.0 starts consolidated state without importing, merging, deleting or replaying the old root. A live older-release owner there is still consulted read-only to refuse a second poller; malformed/unreadable older-owner evidence refuses acquisition rather than becoming absence. The unreleased development standalone layout is not an upgrade source either. Operator-reported Linux live smoke after removing the development root passed (clean two-file census, new Thread, cursor without history, followers, temporary tabs). Native Windows/macOS behavior is covered by the release CI matrix. Windows lacks the no-follow nonblocking open evidence that strict journal reads require: ordinary queue receipts there use the ordinary journal read, while Restore proofs, cleanup census and other strict observations fail closed.
|
|
372
|
+
|
|
373
|
+
**Envelope.** `state.json` is `{ version: 2, profiles: { [profileName]: { transport?, workspace?, admission?, runtime? } } }`. Profile names are logical keys, not filename suffixes or session identities. `transport` owns the exact leader/epoch/generation and polling-journal pointer; `workspace` owns canonical bindings/Restore/temporary facts; `admission` owns ordinary leases and destructive fences; `runtime` is a non-authoritative observation. Section owners keep their strict payload parsers and permissions; diagnostics never create routing/deletion authority.
|
|
374
|
+
|
|
375
|
+
**Kernel (Locks).** `readTelegramRuntimeState` and `mutateTelegramRuntimeStateSection` own physical authority. Reads neither create nor repair files and refuse unsupported/malformed envelopes, non-private/non-regular sources and changed physical observations; missing state is an empty envelope only when the initial observation positively reports absence. A publisher acquires one shared `runtime/<state basename>.transaction`, reads the latest envelope, supplies detached current-section/sibling observations to one synchronous reducer and replaces only its named profile/section; semantic no-ops keep file identity. Authority is rechecked before mutation, after the reducer, before writing, immediately before atomic rename and before a positive acknowledgement. Nested transactions for the same path are refused. A lost rename reply or post-publication authority loss returns `publication-unknown`; the committed fact remains and never grants replay or rollback.
|
|
376
|
+
|
|
377
|
+
**Transport.** `createTelegramLockRuntime({ statePath })` keeps acquire/election/expected-owner/heartbeat/release semantics in its profile's `transport` section, preserving sibling fields and the polling pointer through release and succession. Read-only ownership queries fail closed (no owner) on unreadable state so unrelated Pi hooks never crash; acquisition/publication still validate and refuse without replacing it. `publishStateSectionIfOwned` publishes only Workspace or runtime data under exact retained owner and caller authority with an optional `expectedScope`; it never publishes transport/admission or borrows a surrounding `commitIfOwned` grant.
|
|
378
|
+
|
|
379
|
+
**Session-bound grant.** `Locks.createTelegramOwnedStateAuthorityCapture(lock, session)` captures the exact current Pi context, session generation and owned leader epoch before the caller's first await through a structural session port (Locks does not import Lifecycle). The grant stays current only while that context/generation is current, the lock is owned for that context and the epoch is unchanged. Session replacement, same-context restart, clear, release and release-then-reacquire (a new epoch, as disconnect/connect does) revoke it; a successor captures a fresh grant and never renews the old one. Through the real Workspace store, a persist captured before a new session refuses without changing bytes while the successor publishes; an epoch-only capture would have committed that stale write.
|
|
380
|
+
|
|
381
|
+
**Workspace (Threads).** `createTelegramTopicTargetStore({ consolidated })` uses `createTelegramConsolidatedWorkspaceStorage` and the lossless `parseTelegramWorkspaceStateSection` decoder: malformed/filtered records, unsupported headers, altered normalization, unknown fields or foreign profile evidence block rather than becoming empty state; missing section means absence and whole-section removal is not admitted. Publication captures path/profile/grant before the queue await; whole-Workspace writes compare the current section with the last loaded/committed baseline inside the reducer, so a concurrent canonical change requires refresh. Transaction frames supply Restore/temporary/registration observations to existing guards without nested ownership transactions. Complete-empty journal binding keys stay serialized as `[]` so completeness survives reload. Provisioning-target recovery sidecars use `runtime/<state basename>.provision-recovery.<16-hex profile hash>.json` via `resolveTelegramWorkspaceProvisionRecoveryPath`.
|
|
382
|
+
|
|
383
|
+
**Admission.** `createTelegramWorkspaceAdmissionLedger({ path, stateProfile, profileKey, owner })` selects the shared `admission` section; the runtime binding selects exactly one `getStatePath` or legacy `getPath` identity. Admission stays process-owned: followers acquire/release exact ordinary leases while another process owns transport; destructive fences and `journal-write:custody-v3` permissions keep their predicates and one-shot issuance. Unknown fields are refused rather than dropped; only positively dead process/birth owners are pruned inside a mutation.
|
|
384
|
+
|
|
385
|
+
**Runtime projection (Status).** `createTelegramRuntimeProjectionStore` publishes the non-authoritative runtime/roster/diagnostic projection through a structural storage port (Status imports no local nominal domain), omitting `recentRuntimeEvents`. Submission captures detached content and exact profile/path/grant before the queued await; malformed runtime observations may be replaced only under fresh owner authority; unknown post-rename replies are never re-published to recover an ACK.
|
|
386
|
+
|
|
387
|
+
**Logging.** `createTelegramRuntimeDiagnosticsRuntime({ sharedFile: true })` writes one profile-labelled `logs.jsonl`. Scope resets append profile-tagged markers; the global 5 MiB threshold rotates the mixed segment to `logs/logs._prev.jsonl` under captured grant and the `runtime/logs.jsonl.transaction` guard. Event append is fail-soft and non-authoritative.
|
|
388
|
+
|
|
389
|
+
**IPC.** Bus Transport's `layout: "consolidated"` maps leader `bus.<16hex>.sock` and follower `f.<16hex>.sock` (hashed raw profile and exact recipient) under `runtime/`; Windows uses hashed native named pipes. The local server publishes a colocated private `.pt-<16hex>.sock` listener behind an atomically renamed relative logical symlink. Logical addresses stay canonical; an over-budget Unix path uses the existing private external socket shortening only at socket resolution, never for state or journal relocation.
|
|
390
|
+
|
|
391
|
+
**Validation.** Native fresh-root production tests start real leader plus follower for default and named profiles and observe one poller, the two-file root census, leader/follower publications under `runtime/`, channel success and lost-ACK issuance under `journals/` surviving a replacement extension instance without resend, rotation under `logs/`, preserved bindings and byte-identical released `tmp/telegram`. An authenticated polling callback traverses the production inactive-tab review, profile admission and strict protection observer to prepare exactly one inactive binding through `resolveTelegramServiceJournalStorage`; the cleanup journal shares `journals/` with channel records and leaves no staging files. Replacement and a repeated review retain byte-identical prepared work without issuance or deletion, and complete-empty journal keys/source tuples survive Workspace reload. These are fake-HTTP host-harness tests in one OS process, not real Pi/Telegram, whole production section-owner sequencing or native-platform acceptance.
|
|
392
|
+
|
|
393
|
+
**Shared owner-port composition.** Native property tests compose the production session-bound grant and Thread store's Workspace/Restore/runtime ports with process/birth-owned nonleader admission and actual default/named section stores on one physical root. Context, same-context generation, epoch, owner, profile and path revocation refuse both queued publications. A concurrent Workspace change refuses its whole-section CAS while independent runtime publication succeeds; neither changes transport, admission, named-profile sections, issued Restore or complete-empty references. Registration uses the existing Workspace transaction frame without nesting a shared transaction, and the nonleader lease remains releasable after transport/session changes. Transport PIDs/liveness are supplied fixtures; this proves local owner-port composition, not actual extension sequencing under those races, real multi-process/profile runtime continuity, Pi startup or platform/live acceptance.
|
|
394
|
+
|
|
395
|
+
**Production registration ownership race.** The fresh-root fixture also holds an authenticated native follower registration inside the actual leader provisioner while fake HTTP awaits a Thread-creation reply. Exact same-process owner succession changes the epoch before that reply returns. For default and named profiles separately, the late ACK cannot publish the new binding/owner, old diagnostics and teardown cannot overwrite the successor runtime/transport, and the held process/birth-owned admission lease releases. A real sibling-profile Workspace section and channel issuance bytes remain unchanged; no second creation, cleanup or model dispatch occurs. This is production composition with disposable native Linux storage and supplied Pi hooks in one OS process, not an actual Pi session change, interrupted Restore settlement, multi-process/profile continuity or platform/live acceptance.
|
|
396
|
+
|
|
397
|
+
**Production follower session race.** The same default/named-profile fixture replaces the follower context (same session ID) or renews its generation (same context object) through composed `session_start` hooks while the initial authenticated registration awaits fake-HTTP Thread creation. The independent leader retains its acknowledged remote binding and slot; the stale local attempt grants no direct-delivery authority, makes no API call on refusal and leaves the recipient journal family absent. Only a fresh acknowledged connect grants local delivery, reusing the retained binding without a second creation. Admission releases; sibling-profile Workspace, channel issuance and released storage remain exact, with no cleanup or model dispatch. This is supplied-hook production composition in one OS process, not actual Pi lifecycle acceptance, interrupted Restore settlement or an inbound execution/readiness witness.
|
|
398
|
+
|
|
399
|
+
#### Damaged-State Reset (Operator-Approved)
|
|
400
|
+
|
|
401
|
+
**A working `/telegram-connect` outranks preserving damaged runtime state.** Before a non-election leader acquisition (explicit `/telegram-connect` or session auto-start), `Locks.resetDamagedTelegramRuntimeState` takes the shared `state.json` transaction guard and validates the envelope, every profile's `transport` section and, through the composition root, every `workspace` and `admission` section. Valid state is never rewritten. Damaged JSON, an unsupported envelope, unknown sections, invalid section evidence or a non-private/non-regular file is atomically replaced with `{ version: 2, profiles: {} }`; acquisition then proceeds normally and one `lock`/`state-reset` diagnostic is recorded. Filesystem access errors are not damage and still refuse. Follower elections never reset.
|
|
402
|
+
|
|
403
|
+
The reset deliberately forgets every profile's polling cursor/journal pointer, bindings/slots, unfinished Restore/temporary facts, admission/deletion fences and Workspace evidence; no backup, import, merge or replay is attempted. Pi history, `telegram.json` and the released `tmp/telegram` are untouched. Orphaned session journals follow the normal sweep. If another live leader was using the damaged file, its own ownership checks fail closed and stop its polling; any remaining competing `getUpdates` client is handled by the existing persistent-conflict stand-down rather than by another reset. Old Telegram tabs/messages may remain and are not cleaned up. Ordinary readers and section mutations still refuse damaged state without repair; only this pre-acquisition step resets it.
|
|
343
404
|
|
|
344
405
|
### Persistence I/O Baseline
|
|
345
406
|
|
|
346
|
-
The
|
|
407
|
+
The runtime sections and log have different authority and write pressure. Preserve that distinction when optimizing them:
|
|
347
408
|
|
|
348
|
-
- `
|
|
349
|
-
- `
|
|
409
|
+
- The `transport` section is safety-critical authority. Acquire, release, takeover, stale recovery, and two-second leader lease refresh mutate it. The steady-state baseline is one cached atomic rewrite every two seconds per active profile, or 43,200 refreshes/day; one-second ownership checks are read-only. Every mutation serializes the full cross-process read/check/write through the shared `runtime/state.json.transaction` guard and replaces only that profile's section.
|
|
410
|
+
- The `workspace` section holds recovery-critical thread/capability state. Every explicit thread-store `persist()` builds a semantic snapshot, but an unchanged payload skips directory preparation, temporary-name generation, file creation, and rename after comparing JSON values independently of object-key order and ignoring `writtenAtMs`; array order, value types, and explicit null remain significant, and changed snapshots retain the full atomic replacement path. No-op saves still read and compare current disk state, rather than trusting a cache that could hide another owner's publication. Diagnostics scheduling coalesces requests across a bounded 100 ms window and writes only the `runtime` section (`persistStatus()`, skipped when the JSON value is unchanged), so an idle leader's polling snapshots never rewrite the canonical `workspace` section. Only the exact transport owner commits either section; non-owners reload current state instead of publishing.
|
|
350
411
|
- `logs.jsonl` is fail-soft observational evidence, never routing authority. Runtime events admitted in one JavaScript turn batch by captured profile path into one size check, one profile-wide file transaction, and one append while preserving event order. Batching adds no timer or shutdown-loss window; separate profiles remain isolated, and one failed group does not drop another. Scope reset and rotation retain their serialized copy/replace path. The 5 MiB value is a rotation threshold: an authorized writer rotates between batched records before the next record crosses it, so overshoot is bounded to one admitted record plus reset metadata; a writer without reset authority defers rotation to the owner.
|
|
351
412
|
|
|
352
|
-
This baseline counts write-producing code paths rather than filesystem implementation details that vary between ext4, APFS, NTFS, and network-backed home directories. Optimization evidence should compare these deterministic triggers first, then use platform smoke evidence for rename, named-pipe, crash, and cleanup behavior. Recovery-critical `
|
|
413
|
+
This baseline counts write-producing code paths rather than filesystem implementation details that vary between ext4, APFS, NTFS, and network-backed home directories. Optimization evidence should compare these deterministic triggers first, then use platform smoke evidence for rename, named-pipe, crash, and cleanup behavior. Recovery-critical fields inside `profiles.<profile>.workspace` are `bot`, `identities`, `workspaceBindings`, `reservations`, `pendingProvisions`, `syncObservations`, and `threads`; `profiles.<profile>.runtime` (runtime, live roster and diagnostics) is observational and may use bounded coalescing when authority checks remain unchanged.
|
|
353
414
|
|
|
354
415
|
Run `node --experimental-strip-types scripts/measure-workspace.mjs` for an isolated, assertion-backed Thread-store work baseline at 1, 13, and 26 bindings. It creates and removes only its own temporary fixtures; it never loads configured profiles or calls Telegram. Counters cover asynchronous filesystem calls, bytes, and JSON parse/stringify calls, not object-spread clones, synchronous existence checks, transport-owner transactions, IPC, or elapsed-time performance. The script reports aggregate counts with each row's repetition count; the following counts are per operation and were identical across these fixture sizes:
|
|
355
416
|
|
|
@@ -365,25 +426,74 @@ The first reloaded save and steady no-op saves both preserve the existing file.
|
|
|
365
426
|
|
|
366
427
|
Run `node --experimental-strip-types scripts/measure-bus.mjs` for the complementary synchronous registry baseline. At 1/13/26 entries, re-registration visits each entry once; heartbeat performs one Map get and set without scanning; target lookup visits one entry for the first target and at most the fixture size for the last or a different-chat miss; roster listing visits every entry. Returned target/protocol views are mutation-isolated from registry authority. The script counts Map calls and visited entries, not allocations or time, and restores its process-local instrumentation before exit. Two repeated runs produced identical counts. This bounded result supplies no demonstrated need for a secondary target index, shared mutable views, or persistent IPC multiplexing. Escalate to full transport/provisioning measurement only when attributable latency, event-loop blocking, or unexpectedly growing work supplies a concrete performance claim to test.
|
|
367
428
|
|
|
368
|
-
Version `0.24.0` intentionally does not read or migrate the former agent-level `locks.json`; upgrading resets Telegram ownership. Run `/telegram-connect` when a fresh owner is not elected automatically.
|
|
429
|
+
Version `0.24.0` intentionally does not read or migrate the former agent-level `locks.json`; upgrading resets Telegram ownership. Run `/telegram-connect` when a fresh owner is not elected automatically. Damaged shared-envelope handling is owned by [Consolidated Runtime Root](#consolidated-runtime-root) and its [damaged-state reset](#damaged-state-reset-operator-approved), not the legacy whole-file deletion/retry handler. Earlier releases left `recovery/` quarantine folders; startup now removes them from the current runtime root and session folders, and strict inspection ignores them.
|
|
369
430
|
|
|
370
431
|
### Threaded Mode Multi-Instance Bus
|
|
371
432
|
|
|
372
433
|
Telegram private-chat Threaded Mode is the public switch for multi-instance Telegram operation. Classic single-DM polling is the base mode. When Telegram private-chat threads are available for the bot, the bridge enables the local leader/follower bus automatically; when threads are unavailable or later disabled, the bridge returns to classic single-DM polling as a first-class mode. Before a non-owner `/telegram-connect` chooses follower registration or singleton takeover, it discards process-local status/capability projections and reads the current owner-published mode: `enabled` registers a follower without a takeover prompt, while `disabled` uses the classic confirmation flow.
|
|
373
434
|
|
|
374
|
-
Named Telegram profiles are orthogonal to Threaded Mode. The selected profile chooses the bot/session slice (`botToken`, `botId`, `botUsername`, `allowedUserId`) and scopes its durable journal cursor,
|
|
435
|
+
Named Telegram profiles are orthogonal to Threaded Mode. The selected profile chooses the bot/session slice (`botToken`, `botId`, `botUsername`, `allowedUserId`) and scopes its durable journal cursor, its `state.json` profile entry (transport, Workspace, admission, runtime), its log labels, thread/bus ownership, and leader/follower IPC endpoints; it must not change the Threaded Mode rules. Within one selected profile, leader/follower election, bus transport, thread provisioning, routing, ownership forwarding, cleanup, and runtime diagnostics behave exactly as they do for the `default` slot. A different selected profile is a parallel bot runtime: its `state.json` profile entry, profile-labelled log records, hashed service journals, thread bindings, Unix sockets, and Windows named pipes are isolated from the default profile and other named profiles while shared bridge settings remain top-level/global.
|
|
375
436
|
|
|
376
|
-
Profile reality follows three explicit storage classes. `telegram.json` shared settings and extension registries are process-global platform configuration; `profiles.default` and `profiles.<name>` bot/session fields plus observable transport/routing authority are profile-scoped; queues, active turns, ownership caches, menu state, and runtime controllers are session-local memory. Config persistence serializes cross-process writers and applies each recursive mutation delta to the latest disk snapshot, so unrelated global/profile updates do not stale-replace one another. Each profile's journal transaction independently publishes its monotonic admission cursor together with admitted work; polling never writes runtime state through config persistence. Runtime profile switching follows stop-old/commit-new ordering: reload keeps the selected identity stable, old polling/lock/bus teardown finishes while all dynamic resolvers still point at the old profile, and only then may activation expose the new token, lock key, state path, target namespace, and IPC endpoint. Downloaded attachments
|
|
437
|
+
Profile reality follows three explicit storage classes. `telegram.json` shared settings and extension registries are process-global platform configuration; `profiles.default` and `profiles.<name>` bot/session fields plus observable transport/routing authority are profile-scoped; queues, active turns, ownership caches, menu state, and runtime controllers are session-local memory. Config persistence serializes cross-process writers and applies each recursive mutation delta to the latest disk snapshot, so unrelated global/profile updates do not stale-replace one another. Each profile's journal transaction independently publishes its monotonic admission cursor together with admitted work; polling never writes runtime state through config persistence. Runtime profile switching follows stop-old/commit-new ordering: reload keeps the selected identity stable, old polling/lock/bus teardown finishes while all dynamic resolvers still point at the old profile, and only then may activation expose the new token, lock key, state path, target namespace, and IPC endpoint. Downloaded attachments are flat `kind-scope-messageId[-index][-name|.ext]` files under `tmp/pi-telegram/attachments` and are session artifacts rather than identity or routing authority. The scope is the bot username (else bot ID) for the bot's private chat, a group username or numeric ID, and for Guest Mode the remote peer's username or numeric ID, because guest message IDs belong to the peer chat. Scratch cleanup removes regular files older than 24 hours. It never age-deletes journals, ownership, state, logs, or other top-level runtime files and therefore cannot redirect live traffic or orphan immutable journal segments.
|
|
377
438
|
|
|
378
|
-
When Threaded Mode is active, the current polling owner is also the Telegram bus leader. The leader owns the local bus endpoint (Unix-domain socket on Unix-like platforms, named pipe on native Windows), polls `getUpdates`, performs direct Bot API calls, records follower heartbeats, prunes stale followers, and provisions Telegram UI thread targets through live runtime/bus state. Followers heartbeat every `1s`; the leader uses a `15s` stale grace and a `1s` prune loop so transient IPC stalls do not create false routing gaps while active forwarded updates/API calls still refresh liveness. Heartbeat pruning is silent liveness bookkeeping and does not send a Telegram-visible disconnected notice, because the common cause may be leader reload or IPC handoff rather than a dead follower. Heartbeat pruning alone preserves stored owner records and Workspace bindings. The same leader can retain bounded, non-routing observations of actually registered PID/generation/target tuples for later absence proof and retry of uncommitted preservation; the [pruning contract](./multi-instance-bus.md#follower-heartbeat-is-missed) owns cancellation, admission identity and lifetime limits. When Thread cleanup is enabled, only a subsequent OS check that confirms the exact registered PID absent may create fenced cleanup intent, serialized ahead of replacement registration. With cleanup disabled, that proof instead permits a profile-admitted, fenced non-destructive detachment: the store removes the uniquely matching owner record and records first inactivity while retaining the exact Workspace binding and its letter. Publication rechecks PID absence, runtime generation, profile, leader epoch, replacement registrations and the record snapshot; missing or ambiguous restoration identity blocks it. The Thread and its messages remain untouched, accepted-work protection remains independent, and no deleted-Thread observation is invented. Successful follower target reuse refreshes the binding and clears inactivity. Durable Workspace bindings remain restoration hints after detachment; heartbeat silence or unverifiable process evidence cannot stamp inactivity. If an authenticated live follower carries an exact target that is absent from current bindings, the leader recovers it only behind a synchronous visibility probe: success activates it, explicit stale evidence provisions a replacement, and ambiguous failure rejects registration. A carried slot is restored only when it is not already occupied. `tmp/telegram/logs.jsonl` is a session-local redacted runtime evidence stream for race debugging; it resets on extension start and runtime scope changes, and must not become routing/provisioning authority. `tmp/telegram/state.json` is an extension+bot observable/debug snapshot aligned with status diagnostics: `source: "snapshot"` and `writtenAtMs` mark it as observational, not authoritative. All instances on one Telegram profile read the same snapshot, but only the active transport lock owner persists it; followers become writers only after promotion. Status-only persistence reloads current disk bindings before serialization, preventing a stale follower/status view from erasing newer leader-owned targets. Fresh capability observations may skip redundant startup probes, but stale snapshots re-probe before suppressing bus/thread behavior. Top-level `bot` mirrors bot-wide capabilities such as thread mode, `runtime` describes process role/status, `liveRoster` mirrors followers/current targets/reservations, `diagnostics` mirrors recent status/debug signals including the latest thread-reconciler phase/counts, `threads` stores current routeable bindings, `workspaceBindings` stores profile-scoped normalized exact-`cwd` target/name/slot reuse hints, TTL-bounded reservations explain short-lived slot collision guards, and TTL-pruned `pendingProvisions` protects in-flight topic creation slots from cleanup/allocation races. Fresh provisioning writes pending state before the Bot API create call, adds the returned target to the pending record, persists a `starting` binding, then promotes it to `active` and clears pending state. If final binding persistence fails after Telegram returns a thread id, the targeted pending provision remains as cleanup/retry evidence. Once targeted pending provisions expire, they are retained for `thread-reconciler` close/delete cleanup and pending scratchpad removal after a successful cleanup apply; untargeted expired pending records can prune without cleanup because no Telegram thread id exists. Runtime events coalesce status-snapshot writes so transient bus/API/update failures remain inspectable even when the operator has not opened `/telegram-status`. The bridge must not keep a durable `telegram-targets.json` target history; stale/offline/failed thread observations are pruned instead of reused. Previous-process leader bindings that still probe alive become reservations/collision guards, not routeable active threads, so a reloaded leader can take the next free slot without duplicating the same visible tab name. The thread chat is always the private bot DM with the paired owner (`allowedUserId`). In Telegram private-chat Threaded Mode, the leader creates/reuses its own thread before polling — it is a real bound instance, not a dispatcher. Followers authenticate bus envelopes with the leader-minted capability secret stored in the active lock entry. Bot capability monitoring does not probe through the bus until the process either owns that direct lock or has completed authenticated follower registration. Leader lock entries also carry a stable `leaderEpoch` minted on acquisition and preserved across heartbeat refreshes; leader-owned cleanup/provisioning plans stamp that epoch, and Thread Reconciler apply skips destructive work if leadership has moved on before side effects run. Followers own their own Pi session state, queue, active turns, previews, menus, and lifecycle hooks, but route allowlisted, target-scoped Telegram API calls through the leader. When a follower promotes after heartbeat loss, status/state diagnostics expose only the transient `electing` lifecycle phase; stable `leader`/`follower` identity stays in the bus role so diagnostics do not duplicate role state. The TUI status bar and `/telegram-status` report `leader` or `follower` role so a registered follower is not shown as generically disconnected. Terminal status identity and the `[telegram|thread:name]` prompt label use the same target-aware current-instance resolver: registered local metadata wins over a stale shared binding for the matching target, while the binding remains a fallback for partial metadata.
|
|
439
|
+
When Threaded Mode is active, the current polling owner is also the Telegram bus leader. The leader owns the local bus endpoint (Unix-domain socket on Unix-like platforms, named pipe on native Windows), polls `getUpdates`, performs direct Bot API calls, records follower heartbeats, prunes stale followers, and provisions Telegram UI thread targets through live runtime/bus state. Followers heartbeat every `1s`; the leader uses a `15s` stale grace and a `1s` prune loop so transient IPC stalls do not create false routing gaps while active forwarded updates/API calls still refresh liveness. Heartbeat pruning is silent liveness bookkeeping and does not send a Telegram-visible disconnected notice, because the common cause may be leader reload or IPC handoff rather than a dead follower. Heartbeat pruning alone preserves stored owner records and Workspace bindings. The same leader can retain bounded, non-routing observations of actually registered PID/generation/target tuples for later absence proof and retry of uncommitted preservation; the [pruning contract](./multi-instance-bus.md#follower-heartbeat-is-missed) owns cancellation, admission identity and lifetime limits. When Thread cleanup is enabled, only a subsequent OS check that confirms the exact registered PID absent may create fenced cleanup intent, serialized ahead of replacement registration. With cleanup disabled, that proof instead permits a profile-admitted, fenced non-destructive detachment: the store removes the uniquely matching owner record and records first inactivity while retaining the exact Workspace binding and its letter. Publication rechecks PID absence, runtime generation, profile, leader epoch, replacement registrations and the record snapshot; missing or ambiguous restoration identity blocks it. The Thread and its messages remain untouched, accepted-work protection remains independent, and no deleted-Thread observation is invented. Successful follower target reuse refreshes the binding and clears inactivity. Durable Workspace bindings remain restoration hints after detachment; heartbeat silence or unverifiable process evidence cannot stamp inactivity. If an authenticated live follower carries an exact target that is absent from current bindings, the leader recovers it only behind a synchronous visibility probe: success activates it, explicit stale evidence provisions a replacement, and ambiguous failure rejects registration. A carried slot is restored only when it is not already occupied. `tmp/pi-telegram/logs.jsonl` is a session-local redacted runtime evidence stream for race debugging; it resets on extension start and runtime scope changes, and must not become routing/provisioning authority. `tmp/pi-telegram/state.json` is an extension+bot observable/debug snapshot aligned with status diagnostics: `source: "snapshot"` and `writtenAtMs` mark it as observational, not authoritative. All instances on one Telegram profile read the same snapshot, but only the active transport lock owner persists it; followers become writers only after promotion. Status-only persistence reloads current disk bindings before serialization, preventing a stale follower/status view from erasing newer leader-owned targets. Fresh capability observations may skip redundant startup probes, but stale snapshots re-probe before suppressing bus/thread behavior. Top-level `bot` mirrors bot-wide capabilities such as thread mode, `runtime` describes process role/status, `liveRoster` mirrors followers/current targets/reservations, `diagnostics` mirrors recent status/debug signals including the latest thread-reconciler phase/counts, `threads` stores current routeable bindings, `workspaceBindings` stores profile-scoped normalized exact-`cwd` target/name/slot reuse hints, TTL-bounded reservations explain short-lived slot collision guards, and TTL-pruned `pendingProvisions` protects in-flight topic creation slots from cleanup/allocation races. Fresh provisioning writes pending state before the Bot API create call, adds the returned target to the pending record, persists a `starting` binding, then promotes it to `active` and clears pending state. If final binding persistence fails after Telegram returns a thread id, the targeted pending provision remains as cleanup/retry evidence. Once targeted pending provisions expire, they are retained for `thread-reconciler` close/delete cleanup and pending scratchpad removal after a successful cleanup apply; untargeted expired pending records can prune without cleanup because no Telegram thread id exists. Runtime events coalesce status-snapshot writes so transient bus/API/update failures remain inspectable even when the operator has not opened `/telegram-status`. The bridge must not keep a durable `telegram-targets.json` target history; stale/offline/failed thread observations are pruned instead of reused. Previous-process leader bindings that still probe alive become reservations/collision guards, not routeable active threads, so a reloaded leader can take the next free slot without duplicating the same visible tab name. The thread chat is always the private bot DM with the paired owner (`allowedUserId`). In Telegram private-chat Threaded Mode, the leader creates/reuses its own thread before polling — it is a real bound instance, not a dispatcher. Followers authenticate bus envelopes with the leader-minted capability secret stored in the active lock entry. Bot capability monitoring does not probe through the bus until the process either owns that direct lock or has completed authenticated follower registration. Leader lock entries also carry a stable `leaderEpoch` minted on acquisition and preserved across heartbeat refreshes; leader-owned cleanup/provisioning plans stamp that epoch, and Thread Reconciler apply skips destructive work if leadership has moved on before side effects run. Followers own their own Pi session state, queue, active turns, previews, menus, and lifecycle hooks, but route allowlisted, target-scoped Telegram API calls through the leader. When a follower promotes after heartbeat loss, status/state diagnostics expose only the transient `electing` lifecycle phase; stable `leader`/`follower` identity stays in the bus role so diagnostics do not duplicate role state. The TUI status bar and `/telegram-status` report `leader` or `follower` role so a registered follower is not shown as generically disconnected. Terminal status identity and the `[telegram|thread:name]` prompt label use the same target-aware current-instance resolver: registered local metadata wins over a stale shared binding for the matching target, while the binding remains a fallback for partial metadata.
|
|
379
440
|
|
|
380
441
|
Fresh follower binding is manual and process-first: the operator starts another Pi process, then runs `/telegram-connect`; only then may that process allocate a profile-scoped normalized exact-`cwd` Workspace identity and cause the leader to create a Thread. A later process reopening that remembered Workspace automatically sends capability-gated restore-only admission under a live leader. The leader may reclaim, visibility-probe, or stale-replace the remembered target, but an absent binding returns quietly without creating a Thread. `/telegram-connect [profile] as=Name` supplies a unique capitalized Latin-word identity only to fresh Workspace provisioning; an existing Workspace keeps its persisted name. Telegram `/name Name` stores the owning Thread's durable `manualThreadName` and immediately applies it over the active automatic display projection; bare `/name` opens five-minute exact-target input whose next valid text is consumed before agent dispatch. Name input, cancel, and reset are consume-once; stale scope, target, message ID, expiry, and duplicate callbacks cannot mutate. Reset clears only the override and restores the current automatic projection. Leader command routing reuses its already-held profile admission for the rename body rather than recursively entering the non-reentrant Workspace gate; standalone leader renames acquire their own admission. Followers send an authenticated exact-generation `workspace-thread-rename-v1` request, and the leader owns any Bot API mutation plus durable binding persistence. Concurrent processes from one directory receive deterministic Workspace suffixes, while leader/follower roles remain transient projections over that durable identity. Telegram does not expose `/thread`, auto-spawn arbitrary unbound threads, or launch hidden follower subprocesses. In Threaded Mode, `/telegram-connect` does not offer manual takeover while a live leader exists; takeover is reserved for stale-leader election/recovery. Leadership remains an ephemeral transport role that another live follower can take over after stale heartbeat detection. A confirmed runtime transition from Threaded to Singleton stops threaded transport and suspends the process-local leader target before classic polling can accept new work; durable Workspace slot, generated name, manual display name, and binding evidence remain retained. Re-enabling Threaded Mode restores or replaces that logical binding before publishing one new live target. Already-admitted turns keep their captured destination and are never silently retargeted or duplicated.
|
|
381
442
|
|
|
382
443
|
### Unbound Thread Detection
|
|
383
444
|
|
|
384
|
-
|
|
445
|
+
For ordinary unbound prompts outside the source-bound temporary lifecycle below, Threaded Mode can expose a new thread without an existing instance binding when the owner writes in `All`. The bridge detects this during update execution: if a message from the owner has a `message_thread_id` that no instance owns, the message is routed to the unbound-thread handler instead of the leader's normal message handler. In the default runtime, this handler first reclaims the thread for the leader when the leader has no active bound thread, assigns the current leader thread identity, persists the active binding, and serves the prompt locally. If the leader already has an active thread, the handler preserves the prompt in the source Telegram thread and shows the complete forward plus replace/restore chooser. Successful forward deletes the chooser and closes/deletes the confirmed temporary source through `thread-reconciler` proof-before-delete planning and stale-epoch fencing. Successful restore always deletes the chooser, rebinds the source thread to the selected Pi instance, and closes/deletes only that instance's replaced old thread. If foreign batch forwarding partially fails, retry sends only the remaining messages before cleanup. If Telegram cannot confirm thread or chooser deletion, the chooser becomes a cleanup-only or deletion-only retry control so already-routed content never dispatches twice and no visible button expires prematurely. Unknown `forum_topic_created` service events are recorded as observations and are not destructive cleanup proof, because Telegram can deliver creation events before local provisioning/binding writes become visible across reloads. If Threaded Mode is unavailable, the message is processed normally through classic routing.
|
|
385
446
|
|
|
386
|
-
|
|
447
|
+
Unsettled plain-text choosers retain their route control while the journal source is deferred: creating a later chooser does not prune them after 30 minutes. Selected routes that still owe cleanup likewise keep their retry control. Age alone neither abandons nor dispatches a source; only the positively armed source-only lifetime below permits its exact expiry disposition. The existing 100-chooser capacity bound refuses additional allocation rather than evicting unresolved work. The production source-bound All-command path keeps its original deferred after chooser publication and uses the temporary lifecycle below. A compatibility chooser remains for callers without the required authority/store/transport composition; it retains its older publication-completion and command-expiry semantics, not the new temporary-tab guarantees. Historical orphaned originals stay held; no owner recovery path is scheduled.
|
|
448
|
+
|
|
449
|
+
The journal owns the `abandonPending` primitive for exact unclaimed v1 sources. It first writes the original entry and requested operator disposition to a private, content-addressed `<journal>.retained/abandon-<digest>.json` evidence copy, then atomically removes the active entry and publishes the existing `legacy-custody` discard tombstone in the journal. The copy alone is not proof of cancellation: the journal disposition is the commit authority. The tombstone prevents duplicate admission by current and 0.51.6 v1 readers without a schema change, fabricated execution failure, or task-completion claim. Retention survives journal compaction; archives are not executable journal sources. On POSIX, copies use mode `0600` and newly created directories `0700`; Windows uses the existing journal permission mechanism and inherited ACL boundary.
|
|
450
|
+
|
|
451
|
+
Abandonment checks exact source binding and entry evidence, caller authority at publication boundaries, and existing discard/retention agreement on retry. It refuses queued, failed, retrying, foreign, changed or absent sources, does not repair corrupt evidence, and is not exposed by the v3 custody store. Retention/publication failure leaves original authority intact unless strict read-back proves the exact tombstone committed before a later compaction failure. The worker now exposes source-bound abandonment only for a captured v1 pending entry with its exact live deferred claim, session signal, journal binding and transport/context authority. `abandonTelegramDeferredUpdate` is the admission-carrier entrypoint; raw journal cancellation is not a substitute for worker coordination. The carrier suspends its execution fence during cancellation, including forwarded clones and late completion/queue reports. A failed or unknown acknowledgement leaves that source's claim suspended for exact cancellation retry while unrelated inputs can drain. Successful abandonment releases the claim without publishing task completion; its discard tombstone prevents restart replay. An unattempted request that fails eligibility leaves dispatch authority unchanged.
|
|
452
|
+
|
|
453
|
+
Thread selection reserves the source before awaiting Workspace admission or destination lookup, and releases the reservation after routing settles. Cancellation cannot win against this in-flight reservation or an already reported queue/completion outcome, even before its late journal settlement runs. Conversely, old destination buttons refuse a suspended or abandoned source before routing effects. Eligible single-text prompt choosers now offer **⛔️ Cancel routing**, including the replace/restore submenu. Admission publishes this capability only for a captured v1 pending original, copied before handler projections can mutate execution input; capability publication is not cancellation authority. The click binds the current paired owner, source sender, profile/journal, session signal, leader epoch and exact chooser target/message under profile-wide Workspace admission. Ordinary prompt eligibility requires one supported private text projection with an exact whole-source capability; confirmed temporary tabs also permit command cancellation through their captured source. Unsupported journals, prior selection, partial forwarding, unknown issuance and cleanup-only states remain excluded. Ordinary source-only Cancel never deletes the original Telegram message or its Thread; temporary-tab cancellation has the separate conditional all-resolved cleanup contract below. After the discard acknowledgement, the chooser becomes an HTML cancellation notice with an empty keyboard; an edit failure retains the committed receipt so retry updates the UI without repeating abandonment. Uncertain storage results retain only cancellation retry, not destination/restore authority. Owner-facing recovery after restart uses the Status surface below; historical orphaned inputs remain held internally, without a historical-review menu.
|
|
454
|
+
|
|
455
|
+
Before executing a pending v1 entry, the worker uses the journal's read-only `inspectPendingRetention` port to inspect only that entry's deterministic private retention path. An intact matching copy, or an unreadable/invalid candidate at that path, parks the source in an `abandoning` claim before routing. `abandoningClaimCount` exposes the protected subset of deferred claims. Other independently verifiable inputs continue draining; repeated wake-ups do not replay the parked source. Inspection validates bounded regular-file evidence and the full entry/binding/disposition match without repairing the archive, publishing a journal revision, or treating the requested disposition as committed. A fresh authorized caller may retry the exact journal abandonment through the worker; corrupt evidence stays protected and is never silently overwritten. The package-private `inspectTelegramAbandoningUpdates` carrier port prepares this handoff: it returns at most 20 update-id-ordered protected sources per page, detached original evidence, and exact-source retry closures. Observation reads only the worker's protected claims, not archive directories or journal files; it neither proves current eligibility/archive integrity nor authorizes dispatch. Page access and retries require the captured live worker generation, journal binding, transport/context and caller authority. Retry reuses the journal CAS, cannot be retargeted by editing display metadata, and caches an acknowledged receipt for presentation retries only while authority remains current. Ordinary deferred, queued and executing inputs are not candidates. Human-owner/source-kind filtering and the recovery control surface remain routing-owned.
|
|
456
|
+
|
|
457
|
+
The leader's Status menu offers **❌ Pending cancellations** only while its active worker reports protected attempts. Routing projects up to five supported plain-text originals per review page, confined to the current paired owner/chat, journal binding, session and leader epoch. Commands, media groups, business inputs, forwarded envelopes and sources with a live selected/partially forwarded chooser are withheld. A chooser whose every originating carrier has a proven aborted signal is obsolete evidence, not a veto on a current source-only recovery capability; missing or live fences remain protective. Ordinary Cancel routing never gains this relaxation. The review holds one bounded current surface, with fresh random callback identities so even editing the same message after router recreation cannot revive an old indexed action. Refresh, navigation and generation/authority changes invalidate earlier controls. Each retry reacquires profile-wide Workspace admission, checks the exact current chooser and source, and commits through the worker capability. Acknowledged results retire matching old reroute controls and remove the recovery action without claiming task completion; failed edits retain the exact receipt for retry. Read-only review does not probe or recreate the original Thread. Corrupt evidence and unsupported sources stay protected and never fall through to agent dispatch.
|
|
458
|
+
|
|
459
|
+
This restart barrier applies once the retention copy is atomically published and to workers that implement retention inspection. A failed attempt before any copy publication has no durable cancellation intent and must not be acknowledged as cancelled. Older 0.51.6 readers respect a committed discard tombstone, but do not recognize an interrupted pre-commit retention attempt; the latter is not a downgrade-safe completed cancellation. Status recovery handles these parked attempts; it is not a general historical-pending-input recovery API.
|
|
460
|
+
|
|
461
|
+
Historical classification cannot rely on a clean v1 pending entry alone. The native integration test `Historical pending source alone cannot prove no follower acceptance` holds the acknowledgement after durable recipient admission, then revokes the source generation: reopening the source yields exactly its pre-forward snapshot, with no queue receipt, owner, input claim or provenance, while the recipient still owns admitted input. A topic-created reply marker does not distinguish those histories either. This demonstrates an information boundary, not that the reporter's particular inputs were forwarded. Recreating a chooser after startup cannot supply the missing historical evidence. Each worker generation now records all update IDs in its first fully validated journal snapshot before batching or execution. Without retention evidence, those sources cannot acquire ordinary fresh-cancellation capabilities, including through a newly published chooser or direct worker call. The baseline is not advanced on read/validation failure and is renewed for every generation; entries arriving before the first valid snapshot are conservatively included. Later admitted pending inputs retain normal cancellation, and independently protected retention attempts retain exact recovery. This fresh-cancellation fence does not establish non-delivery. Historical prior recipient acceptance remains unknown; removal of its review UI grants neither cancellation nor replay.
|
|
462
|
+
|
|
463
|
+
Production leader admission composes routing's narrow historical classifier; follower custody never enters this classifier. It selects supported private plain-text startup inputs with no currently live bound Thread (also protecting them when Threaded Mode is unavailable). It does not infer deletion or repair bindings. Eligible raw unsupported commands/media/unknown historical private-Thread originals without receipt/claim/provenance authority receive a distinct `retain` verdict regardless of whether their target is bound or Restore/temporary membership survives. This operator-approved policy stops ordinary bound startup dispatch for that subset; same-kind live arrivals stay outside the startup census and dispatch normally. Business, forwarded and known owner-bearing sources keep their existing owner paths. Holding is evidence protection, not sender authorization; it exposes no historical-review UI. Missing routing authority or unreadable Thread state fails closed. The domain-owned `shouldReviewHistoricalInput` classifier receives detached v1 pending evidence, the context and abort signal before any companion/default handler runs. Generation, context, journal binding, transport and exact source evidence are rechecked across its await; classification/read failure does not fall through to dispatch. Boolean true selects legacy `historical` review/spending; `retain` installs a separate `retained` claim, also included in `historicalClaimCount`, without a journal mutation or invented cancellation intent. Retain-only sources cannot be cold-spent, recovered through historical retry, generically cancelled, expired through an old routing clock or claimed by grouped queue outcomes. Each classifier receives detached evidence and its verdict is source-rechecked before installation. Each new generation must classify them again. Routing can also report a last-boundary historical hold if ownership changes after early classification: this suspends the carrier, refuses prior selection/queue reports, and cannot be requested retroactively after the handler returns. Its source-kind predicate receives detached original evidence through the current worker capability; a handler projection that strips forwarding/media metadata cannot manufacture eligibility. Predicate calls and their results remain generation-fenced. The native update-plan boundary recognizes only that exact carrier's acknowledged terminal hold; intentional suspension must not become a retryable execution error.
|
|
464
|
+
|
|
465
|
+
`inspectTelegramHistoricalInputs` remains a package-private bounded worker capability requiring a fresh current carrier and caller authority; it scans neither archive directories nor journal files. Ordinary abandonment still rejects historical sources, and late or grouped queue outcomes cannot steal their claims. The Historical inputs main-menu entry and historical review/Stop retrying submenu are removed. The reserved `reroutecancel:history:` prefix consumes old controls with an unavailable answer, never forwarding them to companion handlers or Pi, reopening a view, acquiring mutation admission, or abandoning a source. Held originals remain unchanged; this UI removal adds no expiry, retention intent or Thread deletion. Existing interrupted private-retention attempts still use the separate Pending cancellations review, which warns that previously accepted work may continue. Native worker and cold-router tests cover retired list, selection, confirmation, paging and refresh controls. Cross-domain IPC fixtures lose an ACK after recipient admission and prove that removal leaves uncertain source evidence and independently accepted recipient work intact. Protected-attempt recovery is tested with a supplied interrupted-retention precondition, not a retired UI grant. A production-root fixture proves that the normal menu omits Historical inputs while retained Restore originals remain held. Newly confirmed eligible prompt choosers now use the source-only lifetime described below; historical evidence never acquires a fabricated deadline. Elapsed time never proves non-delivery or authorizes Thread deletion. The operator accepted this behavior in the operator's 0.52.0 live smoke.
|
|
466
|
+
|
|
467
|
+
New private-owner prompt choosers, including grouped sources, arm a source-only 60-minute lifetime after positive publication acknowledgement. The worker captures that clock before storage contention; `journal.routingInputs.arm` atomically retains each exact v1 source's operator, `publishedAtMs`, fixed `expiresAtMs` and `waiting` phase. Refresh and restart cannot renew it. The root also uses this capability for supported acknowledged command choosers, including Telegram-provided unbound tabs and source-bound temporary tabs under the owned leader epoch; unsupported compatibility carriers retain their legacy behavior. Actual destination validation precedes `routingInputs.select` under the existing Workspace admission; the complete source cohort becomes `selected` atomically before dispatch or Restore effects. Menu browsing, invalid targets and refused Restore capability leave `waiting` intact. Source/epoch/operator/context/binding/process/session fences and post-publication readback remain mandatory; uncertain clock or selection ACK refuses issuance rather than bypassing the deadline. Selected metadata prevents cold redispatch and automatic expiry, including unknown accepted work. A failed clock publication holds the source without automatic handler retry or a made-up replacement clock; reconstruction can use only positively retained metadata.
|
|
468
|
+
|
|
469
|
+
The worker schedules one generation-fenced deadline wake, also checking retained lifetimes during normal drain and restart. Routing expiry reacquires profile-wide Workspace admission, checks current source/operator/context/leader/binding authority and refuses retained Restore or live selection/cleanup work. Only exact `waiting` sources can use the existing `abandonPending` private-copy/tombstone transaction; readback and positive disposition precede claim/control retirement, never a task-completion observer. A failed retention attempt stays protected; unsuccessful deadline wakes schedule a one-minute retry, and normal drains also recheck it. Lost removal replies reconcile the exact held original against the existing private copy and committed tombstone; absence alone cannot release it. UI edits are best-effort after ACK. No tab visibility probe, Thread deletion, target retirement, Pi stop or reset of an issued Restore grant occurs. Deadline expiry works even if the chooser/tab was manually removed; optional reliable disappearance detection is not implemented. Historical, unconfirmed, selected, queued, running and unknown issued sources remain outside automatic expiry. All peers and successor writers must understand the new v1 entry metadata before candidate reload. Native journal/producer/worker fixtures cover grouped sources, late selection, expiry boundary, restart, stale wakes/buttons, missing views, retention/clock/selection ACK faults and changed authority; they do not prove live operator acceptance.
|
|
470
|
+
|
|
471
|
+
With live bindings or a retained temporary entry, a known command or a fresh private plain-text prompt from All first enters the shared source-bound temporary-Thread path under current owner/profile/journal/context/leader authority and profile-wide Workspace admission. Threads atomically reserves a source/token intent before one `createForumTopic` attempt and records only its exact acknowledged target. The tab owns no Pi binding or A–Z slot; its presentation target is separate from the original All source. A creation error, missing target or lost publication result leaves unknown custody held, never a second creation attempt. A `created` entry can republish its chooser while its source is still pending after restart; a `creating` entry supplies no creation or dispatch grant. The acknowledged tab offers the shared Forward/Restore chooser and Cancel only when the current source has fresh private-abandonment capability. Publication defers rather than completes the original command; selection retains command semantics, and no current Pi is chosen automatically. Commands already completed by the compatibility path are not reconstructed. Unpresented expired commands keep the existing command-expiry fence; a confirmed retained tab is not evidence of expiry. Fresh threadless plain-text prompts use `New chat` with the same source-bound routing menu; fallback guidance remains when exact temporary-creation authority is unavailable. Recognized commands include built-in handlers (not only bot-menu entries), registered Telegram extension commands regardless of menu visibility, and discovered prompt-template commands. Bound-Thread and classic routing are unchanged. Local candidate composition is default with the required stores and owned epoch; there is no temporary-tab feature flag. Actual Pi/Telegram behavior was accepted in the operator's 0.52.0 live smoke; no owner-visible uncertainty recovery is scheduled.
|
|
472
|
+
|
|
473
|
+
#### Temporary Thread Multi-Input Lifecycle (Approved Design)
|
|
474
|
+
|
|
475
|
+
Temporary command tabs use the command itself as their title (for example `/start` or `/status`), without a visible internal token; fresh plain-text prompt tabs use `New chat`. Both use the same mode chooser and differ only in their title and introductory copy. A command delivered with a Telegram-provided unbound private target uses the same chooser and fresh exact-source Cancel capability; its first chooser renames that target to the command without creating another tab. A received target or successful rename alone never manufactures temporary authority. A fresh owner-authenticated private `forum_topic_created` with literal `is_name_implicit: true` supplies native creation evidence: routing retains a bounded process-local observation tied to the exact context, session generation, admission scope, operator, journal binding and executor. A later live single-source input in that exact target registers it through Threads `registerImplicitTemporaryThread` under profile admission, without calling or fabricating `createForumTopic`; the publication CAS refuses any binding, owner record, reservation, provisioning, cleanup or Restore conflict. Normal grouped membership, one-shot Forward, source disposition, last-Cancel quiet period and cleanup issuance then apply identically. Historical creation signals, context/epoch changes, manual names, string flags, foreign creators and missing observations grant nothing. The observation is consumed on registration, never reconstructed from a title or restart. Existing bindings/reservations and acknowledged temporary targets retain their protections. Their root chooser offers `Reroute…`, `Restore…`, and eligible `Cancel routing`; each mode opens its own live-target submenu without dispatching the held input. Submenus place `⬆️ Back` first to return to the mode chooser with its exact original description, retained on that pending chooser; navigation never substitutes or recomputes the root copy. Cancel appears only at the root. After cancellation, the 1-second quiet-period cleanup remains proof-gated. Private-chat temporary tabs skip the unsupported `closeForumTopic` call and issue only one `deleteForumTopic` attempt; literal positive deletion acknowledgement is still required, and uncertainty never authorizes a retry.
|
|
476
|
+
|
|
477
|
+
This multi-input lifecycle is implemented in the local candidate, not authority for live deletion. It supersedes unconditional per-chooser tab removal. The reject-only guard prevents cleanup/retirement while another known same-tab chooser remains, including unresolved cleanup-only work. `temporaryThreads.inputs` now records bounded append-only journal source groups through the unbound producer before chooser publication; duplicates are read-only, partial/foreign/overlapping groups refuse, and faults retain the source without a new chooser. Cold reads preserve this protection without process-local chooser memory. A group has no readiness or disposition flag, and an absent legacy field does not prove complete coverage. `cancelledInputs` now records exact whole known groups only after strict same-journal/operator abandonment inspection confirms committed tombstones plus matching private originals. Store publication and returned/retained acknowledgement recheck that proof and operator/executor authority; a lost reply reconciles the exact fact read-only, while missing/foreign/corrupt proof stays protective. The producer composes this into explicit Cancel before temporary cleanup or chooser retirement. Cancelled groups cannot Restore; independent sources remain untouched. More than one known group still blocks cleanup and raw retirement, including all-cancelled membership: these facts grant no deletion. Each successful Cancel retires only its own chooser. If durable membership is fully resolved by exact cancellation/completion facts and no chooser remains, a process-local quiet period starts (1 s; new input in the tab cancels it). Its single attempt, under profile admission, rechecks authority, whole-group tombstone plus retention proof, exact entry CAS, an empty strict journal census (any retained update naming the Thread, any state), absence of choosers and the normal reconciler protections. `issueTemporaryThreadCleanup` publishes canonical `cleanupIssued: true` only for an exact fully resolved entry with strict unbound canonical evidence checked inside the publication transaction; the normal target/source guards precede issuance and run again at API boundaries. The reconciler requires that exact retained issued frame and limits its plan to this tab; unrelated expired-provision cleanup never borrows the grant. Close/delete calls disable retries and transport fallback and require a positive acknowledgement. Confirmed current deletion alone permits entry retirement; skip, negative/lost reply, authority loss or post-rename publication interruption retains the marker. Cold reads classify already-issued entries as unknown, new membership refuses, and executor adoption never renews the grant. Missing evidence only refuses; an unpublished grant issues no API call and may still receive its first grant. The quiet period remains process-local, but issued uncertainty survives restart. The census cannot see updates in transit before journal append. Restore from a multi-input tab is allowed only when each other known group is durably cancelled, Forward-completed or a live unselected chooser that supports private abandonment. A previously completed Forward never supplies a new selection or cancellation grant. After positive settlement of every selected source, such siblings are abandoned with cancellation facts and their controls edited to cancelled, then `retireTemporaryThread(expected, authority, completed)` releases the entry (one newly completed selected group, all others durably cancelled or previously Forward-completed). Missing authority/proof or an unproven Restore leaves siblings pending and protected. On startup, a retained journal input recorded in a created temporary tab whose target a Workspace binding now owns is held as historical (never cancelled, discarded or routed). Live pending sources and due retry-wait sources (including startup retries) use the same fresh membership/binding predicate through `shouldHoldPendingInput` before execution, with source/context/binding/transport checks after classification awaits. The hold grants neither cancellation nor delivery and does not block unrelated inputs. Recipient custody never uses this direct-source classifier. A native production witness admits a recorded command after the worker has already processed an unrelated command, proving it cannot fall through to ordinary bound dispatch. A production retry witness keeps recorded failure metadata intact without command execution, cancellation or deletion. Claimed retries cannot schedule repeated due-time wakes; independent due and future retries remain executable. Real-IPC Forward/Restore sibling witnesses now include recipient journal drain through the real worker and follower router: only the selected message executes, source disposal is observed, and repeated controls do not replay the command. Existing message-ownership composition keeps a locally published chooser's callbacks at its leader publisher after follower rebinding; the native fixture now records publication ownership rather than relying only on target ownership. Four native delivery-reply-loss witnesses cover both routing actions (Forward/Restore), before recipient execution and after worker disposal: the leader source remains unresolved without ACK, independent sibling Cancel preserves the selected group/tab, and a late reply or repeated issued Restore click cannot settle/replay it. Explicit Forward retry after a lost ACK reproduced command duplication when the recipient worker had already disposed its journal record; stable delivery identity alone was not terminal deduplication. The follower's bounded process-local delivery window now acknowledges such a retry without re-execution while it retains that delivery. Ordinary Forward from a temporary tab first publishes a durable per-group `forwardedInputs` issuance fact in the canonical Threads owner under current authority, before either local command handling/prompt preparation/queue admission or follower RPC. Only positively unpublished issuance may receive its first grant; a published fact (even if its reply is ambiguous) is never retried, including after reload, restart or leader replacement. Local repeated selection refuses the retained grant before handler or queue re-entry. Captured All-tab provenance and acknowledged typed-input membership remain protective if the metadata reader disappears; unavailable evidence never falls back to unguarded generic dispatch. Restore retains its separate canonical routing grant. Native local fixtures interrupt source completion before/after commit, prompt preparation and issuance publication/authority; issued sources stay nonterminal, independent siblings remain cancellable and cold restart never manufactures completion or another attempt. A process-local `foreignForwardIssued` mirror gates the live chooser. An issued group is refused by repeat Forward, Cancel, expiry and Restore (the store also rejects them), keeps sibling accounting protective and is resolved only by a positive `completedInputs` fact; independent siblings remain operable and a restarted chooser cannot resend. Issuance is not proof of delivery, and recipient-side deduplication is limited to the bounded process-local delivery window; an operator-visible exit for a permanently unknown issued group, generic journal forwarding and non-temporary reroute retries are unchanged and not scheduled. Canonical Restore keeps its existing durable grants. Mixed native witnesses prove that a completed Forward allows Restore of another group without replay/recancellation, while an unknown Forward prevents sibling Restore before an intent is created. Prompt Restore leaves a command sibling pending at queue admission, then privately cancels it only after exact scoped worker receipt disposition. Independently Forwarded queued input is neither unassigned nor cancellation-capable: either receipt may dispose first, the other retains custody, and only both positive dispositions release temporary membership while keeping the bound tab/slot. An ordinary queued ACK emits the existing source-completion hint only after whole acknowledged removal and fresh context, owner, generation and originating journal-binding checks; it creates no immutable Restore marker. A published independent Forward group fact wakes pure inspection of already settled Restore only after its Workspace admission releases. The store refuses both Forward facts and cancellation for any source overlapping a retained Restore grant, regardless of target; malformed partial selected groups stay protective. Native ordinary prompt Forward plus sibling Cancel also delays deletion until receipt disposition and fresh all-resolved cleanup. Pi handoff is supplied by the fixture, not observed model/task completion. The retained All-command fixture registers 84 scenarios across creation, Forward, Cancel, cleanup/membership and Restore, including full slots, publication faults, sibling protection, native Unix IPC and recipient worker drain through the real non-reentrant Workspace gate. Full-slot follower cases hold a real Unix response before apply, after apply, and after read-only inspection until client timeout. Fresh same-session registration/context generations reconcile only through inspection; late replies never publish readiness, repeat apply or dispatch, cancel siblings, release independent queued recipient work or delete the tab. Successful continuation retains exact scoped source-disposition proof. A separate cold-successor witness loads fresh stores and a journal binding after an issued command lost acceptance authority, then starts a fresh context/generation/epoch under supplied same-session owner publication. Original command and sibling stay pending, spent routing/canonical facts remain unchanged, and stale Restore/Forward/Cancel controls cannot replay or dispose them. No chooser is reconstructed: the protective hold is proven, not a usable cold recovery surface. Pi effects and startup registration remain supplied adapters/preconditions; actual Pi startup and live behavior were accepted in the operator's 0.52.0 live smoke. Forward completes only its own input: no Forward removes the tab, the worker's completion ACK records a durable `completedInputs` group (process-local accumulation across the group's sources), and when every known group is cancelled or completed the delayed, fully rechecked cleanup removes the tab once. Cancelled and completed groups are disjoint, cannot Restore, and a completed Forward queued prompt holds the tab until its receipt completes. The tab chooser discloses removal only when no other input remains; a failed removal no longer edits the original chooser. Source-only prompt Cancel remains unchanged. The candidate's temporary-tab chooser is the default under the owned leader epoch, not a configurable feature flag. Upgrade all prospective canonical readers/writers before live activation; the metadata (including `forwardProtocol`, `forwardedInputs` and `cleanupIssued`) is not downgrade-safe through older snapshot writers, which could drop issuance facts.
|
|
478
|
+
|
|
479
|
+
- `Membership`: A positively source-linked disposable temporary Thread owns no Pi binding or A–Z slot and may contain multiple independent unassigned inputs, each with its own routing controls. Track exact journal source/group membership, including additional inputs, rather than treating the first chooser or a process-local count as the whole Thread. Serialize admission, selection, cancellation, Restore and cleanup through the existing Workspace admission and mutation owners; unreadable, incomplete, selected, accepted, running or unknown-issued evidence blocks deletion.
|
|
480
|
+
- `Per-input Cancel`: Privately retain the exact original and positively commit source-only cancellation before acknowledging `Routing cancelled` and retiring its route controls. Other unassigned inputs retain their choosers and custody; cancelling one input neither delivers nor cancels siblings. No separate Cancel-and-delete action or extra confirmation dialog is required, but the chooser must disclose the conditional whole-tab deletion consequence.
|
|
481
|
+
- `Last-resolution cleanup`: Only an explicit Cancel or positive completion of an explicitly issued Forward that resolves the final known group may schedule deletion of the entire still-temporary Telegram tab, including all its messages. The operator-approved grace is 1 second (1,000 ms), superseding the earlier 2–3-second design. The current local candidate already matches it without a runtime change or production override. For Last Cancel, scheduling follows positively recorded whole-group cancellation and successful retirement of the final chooser, not the initial button tap. The timer establishes earliest eligibility for one proof-gated cleanup attempt, not an exact deletion time: Workspace admission, fresh protection checks and Bot API latency may delay or refuse it. Actual client timing was accepted in the operator's 0.52.0 live smoke. Mirrored native witnesses use shortened injected delays, not a production-default or Telegram-client timing measurement. This grace is presentation timing, not proof of emptiness or non-delivery. New input or a changed binding/authority cancels the scheduled cleanup. Immediately before issuance, reacquire current Workspace admission, recheck exact operator/profile/session/leader/source/target authority, positive disposable-tab provenance, complete source dispositions and all Thread protection, then publish the canonical one-shot temporary cleanup grant. A restored/bound Thread or any unresolved/unverifiable work remains protected. TTL, startup, silence and a zero chooser count do not manufacture this last-Cancel grant. Failed/unknown deletion cannot undo cancellation or cause automatic repeat issuance.
|
|
482
|
+
- `Forward`: Forward only the selected input and preserve independently held siblings. Its existing source-tab cleanup cannot erase another unassigned or protected input; per-input completion is not whole-Thread deletion proof.
|
|
483
|
+
- `Successful Restore`: Preserve this tab as the selected instance's bound Workspace Thread and keep its slot; only the selected input is delivered. Cancel the exact other still-unassigned inputs with private retention, positive source-only disposition and invalidated controls, without delivering them to Pi or physically removing the restored tab. Already selected, queued, running and unknown-issued work is outside sibling cancellation. Failed or unknown Restore does not cancel siblings. Composition must establish the exact sibling set under serialized membership and prevent held/partly-cancelled originals from falling through to normal bound-Thread execution after relocation; a retention/ACK fault preserves unresolved sources and cannot roll back independently accepted work.
|
|
484
|
+
- `Validation and activation`: Native witnesses must cover two/three inputs, grouped sources, different cancellation orders, new admission during the grace, concurrent selection/Forward/Restore, stale controls, incomplete membership, retention/ACK faults, restart and one-shot unknown deletion. Operator-controlled live acceptance must explicitly authorize disposable test-tab deletion; it passed in the operator's 0.52.0 live smoke.
|
|
485
|
+
|
|
486
|
+
#### Restart As A New World (Operator-Approved, Implemented Locally)
|
|
487
|
+
|
|
488
|
+
A Pi process restart starts a new routing world. Unfinished routing from the previous process is not restored, reconstructed or announced:
|
|
489
|
+
|
|
490
|
+
- `Spent prompts`: Pending prompts that Pi never accepted into its queue — unselected, chooser-held, temporary-tab or Restore-held, including an issued but unconfirmed Forward — are spent: removed from pending custody without Pi delivery, a private retained copy, a notification or task completion. The polling cursor already prevents Telegram redelivery; spending is disposition, never replay. Implemented: the leader worker's `spendHistoricalInput` removes startup-census pending sources that carry a routing input or that the domain classifier marks boolean true, under current authority, through ordinary removal without completion observers. A distinct retain-only verdict takes precedence over an old routing clock and spending; unsupported protected originals remain unchanged. Retry-wait sources, interrupted private abandonments, queued receipts and live arrivals keep their existing holds; unknown classification spends nothing. Also implemented: the leader's startup hint, fenced by the owned polling generation, calls routing `forgetPreviousWorld`. Under profile admission it atomically removes this operator's Restore intents and temporary entries whose executor belongs to another runtime instance (a reload is a new instance), then makes one non-idempotent `deleteForumTopic` attempt per previously classified disposable created tab still unreferenced by bindings, active records, temporary entries or Restore intents, and only after a fresh strict source census returns complete-empty. Retained, unreadable or unavailable source evidence blocks that deletion without reconstructing the forgotten intent.
|
|
491
|
+
- `Old temporary tabs`: An unbound tab with positive disposable provenance gets one silent deletion attempt. Failure or an unknown outcome leaves the tab without retry.
|
|
492
|
+
- `Unfinished Restore`: The intent is forgotten. Already committed canonical binding changes stay as they are, without rollback.
|
|
493
|
+
- `Old controls`: Choosers and buttons from the previous process answer `⌛ Routing choice expired.` (implemented as `TELEGRAM_ROUTING_CHOICE_EXPIRED`, shared with the warm expiry edit).
|
|
494
|
+
- `Untouched`: Committed Workspace tabs and bindings, prompts already accepted into the Pi queue (exact receipts), follower custody, and in-process `/new` succession, which is not a restart.
|
|
495
|
+
|
|
496
|
+
The user recreates a temporary tab and restores again when needed. This superseded the earlier cold policy: working-tab attestation, held-source inspection/scope capabilities and strict per-group cold disposition/finalization were removed. The historical classifier, temporary-tab hold, one-shot startup hint and polling-generation fence remain as inputs to spending and forgetting.
|
|
387
497
|
|
|
388
498
|
The routing identity split is deliberate:
|
|
389
499
|
|
|
@@ -415,9 +525,9 @@ All inbound updates are gated by the configured authorized user id.
|
|
|
415
525
|
|
|
416
526
|
Here, **durable** means recovery across ordinary process exit, crash, kill, and replacement after a successful atomic rename is visible to the filesystem. It does not promise survival across host, kernel, filesystem, storage-device, or power failure: journal and offset publication do not call `fsync`/`fdatasync`, and parent directories are not flushed. A host-level failure may therefore lose a recently acknowledged rename despite correct process-level ordering. Operators requiring that stronger boundary must place the agent directory on storage with an independently managed durability/backup policy; `0.28.0` must not be described as power-loss durable.
|
|
417
527
|
|
|
418
|
-
The profile-scoped journal separates transport progress from semantic progress.
|
|
528
|
+
The profile-scoped journal separates transport progress from semantic progress. Polling and local leader/classic admission use the owners-named journal, normally `tmp/pi-telegram/sessions/<id>/inbox[.<profile>].json`; active follower recipient custody uses `sessions/<id>/journal.<recipient hash>[.<profile>].json`. Existing root custody and missing-session compatibility are retained, not migrated. See [Session-Owned Journal Storage](./multi-instance-bus.md#session-owned-journal-storage) for path selection, succession and sweeping. The selected post-v1 storage design is one revisioned compacted snapshot plus immutable atomic transaction segments beside it. Existing v1 files load as implicit revision `0`, while positive snapshot revisions are explicit. Immutable revision segments publish privately and atomically under the existing journal transaction lock; exact repeats are idempotent, while gaps and conflicting duplicate revisions fail closed. Each segment carries one complete mutation, and readers reconstruct ordered upserts, removals, and operator-disposition state only from revisions newer than the snapshot. Malformed or gapped segments, filename/revision disagreement, and foreign journal identity fail closed. Compatibility recovery for the former broad temp-cleanup bug rebuilds a missing snapshot when its complete revision-1 segment chain removes known base authority before any upsert and reconstructs to an empty journal, and repairs a revisionless snapshot when the first surviving segment supplies its exact positive predecessor revision and the reconstructed tail validates. If repair fails, the transaction-locked loader follows the operator-approved corruption policy: it deletes the damaged segment directory first, then atomically replaces the snapshot with a fresh empty private journal (a non-file snapshot path is deleted), records an informational recovery event listing the deleted paths, and continues startup. Damaged inputs are lost by design; no `recovery/` quarantine is created. Deleting segments first prevents stale segment replay beneath a fresh revisionless snapshot. A crash or failed publication can leave an absent snapshot or the old damaged regular snapshot; deleted segments are not restored, and a later read may reset again. Private `.retained` originals are not touched by this reset. After the initial snapshot, append, batch completion, queue receipt/owner/handoff, retry/terminal, recovery, and operator dispositions publish only changed upserts/removals and disposition replacement in one segment. This avoids rewriting retained raw updates during completion-heavy drains without splitting exact queue, failure, recovery, or disposition transactions.
|
|
419
529
|
|
|
420
|
-
Compaction runs under the journal transaction lock when either 256 unapplied segments or 4 MiB of segment bytes is reached. It publishes the complete private (`0600`) snapshot at revision `R` before best-effort deletion of segments `<= R`; failed cleanup leaves redundant segments that readers ignore. Interrupted cleanup therefore leaves either an older snapshot plus newer authoritative segments or a newer snapshot plus harmless redundant older segments. Revision gaps, conflicting duplicates, malformed segments, and identity mismatches fail closed. The logical reconstructed journal and aggregate unapplied segment bytes are independently bounded at 10,000 entries and 32 MiB as applicable; rejected growth publishes neither snapshot nor segment bytes. Compaction may temporarily require exactly one private complete snapshot of at most 32 MiB. Capacity pauses polling
|
|
530
|
+
Compaction runs under the journal transaction lock when either 256 unapplied segments or 4 MiB of segment bytes is reached. It publishes the complete private (`0600`) snapshot at revision `R` before best-effort deletion of segments `<= R`; failed cleanup leaves redundant segments that readers ignore. Interrupted cleanup therefore leaves either an older snapshot plus newer authoritative segments or a newer snapshot plus harmless redundant older segments. Revision gaps, conflicting duplicates, malformed segments, and identity mismatches fail closed. The logical reconstructed journal and aggregate unapplied segment bytes are independently bounded at 10,000 entries and 32 MiB as applicable; rejected growth publishes neither snapshot nor segment bytes. Compaction may temporarily require exactly one private complete snapshot of at most 32 MiB. Capacity pauses polling without deleting or resetting valid authority. The separately approved slotless session-family sweep can delete valid recipient families; it is not a capacity-recovery or settlement path. Only a history that cannot be reconstructed safely uses the delete-and-reset fallback above.
|
|
421
531
|
|
|
422
532
|
`pending` entries remain immediately executable while raw interception, routing, or grouping is incomplete. Execution failures become `retry-wait` with durable attempt count, next eligible time, failure class, bounded summary, and latest failure time, except that an exact HTTP 400 stale/deleted Telegram thread error carrying its proven request `{chatId, threadId}` terminally settles the currently executing source after shared binding invalidation; follower settlement remains idempotent when the leader already persisted that stale binding. The `failed` state remains schema-compatible only for legacy candidate journals and is converted to automatic retry during lifecycle startup. `queued` entries carry exact prompt/control receipts plus the acquiring Pi runtime instance, OS pid/birth identity, session generation, acquisition id, and acquisition time. Queueing alone is never completion.
|
|
423
533
|
|
|
@@ -433,7 +543,7 @@ Queued semantic authority has no elapsed-time lease. A timeout cannot prove eith
|
|
|
433
543
|
|
|
434
544
|
The initial `offset: -1` cursor bootstrap is allowed only when both cursor and journal are absent or empty. Thereafter process-level ordering is journal atomic rename → one monotonic offset atomic rename → worker signal. Failure before journal publication leaves the offset unchanged; failure after journal publication but before offset publication permits Telegram redelivery and journal dedupe; failure after offset publication but before worker signal replays from the journal on restart. Queue-owner, retry, terminal, handoff, and completion transitions use the same journal publication primitive and therefore share this process-crash boundary. The final completion window is at-least-once, so replay-sensitive external effects must use `update_id` or the stable delivery id as an idempotency key.
|
|
435
545
|
|
|
436
|
-
Threaded Mode forwarding is a two-journal handoff. Only peers that mutually advertise protocol v1 and `durable-follower-admission-v1` may route or become election-eligible. The follower validates its exact binding and registration generation, durably appends the source-bound delivery, and only then returns the exact receipt. The leader classifies each attempt as `accepted`, `retryable`, or `terminal-rejected` with its delivery identity and failure class; only `accepted` with the expected `deliveryId` and `sourceUpdateId` may complete leader journal authority. Missing, negative, stale-generation, or mismatched-receipt acknowledgements remain durable, and a callback error answer is only an operator-facing side effect.
|
|
546
|
+
Threaded Mode forwarding is a two-journal handoff. Only peers that mutually advertise protocol v1 and `durable-follower-admission-v1` may route or become election-eligible. The follower validates its exact binding and registration generation, durably appends the source-bound delivery, and only then returns the exact receipt. The leader classifies each attempt as `accepted`, `retryable`, or `terminal-rejected` with its delivery identity and failure class; only `accepted` with the expected `deliveryId` and `sourceUpdateId` may complete leader journal authority. Missing, negative, stale-generation, or mismatched-receipt acknowledgements remain durable, and a callback error answer is only an operator-facing side effect. The follower's bounded [delivery replay window](./multi-instance-bus.md) acknowledges a repeated `deliveryId` without re-execution.
|
|
437
547
|
|
|
438
548
|
Delivery ids derive only from envelope kind, source `update_id`, and stable recipient binding. Live registration generation remains a separate attempt fence. Message ownership carries the stable binding and rebinds to its current authenticated follower registration after replacement. `ownership.getForwardOwnership()` projects the protocol identity from the exact matching live instance, registration generation, and binding on every lookup; protocol is not cached with message history. The same port serves messages, edits, callbacks, and reactions, including callbacks without a Thread ID. If a cache lookup returns a foreign record but no matching durable-capable protocol can be projected, that record remains foreign and fails forwarding validation rather than falling through to local execution. The final bus validator independently rechecks generation, binding, and complete protocol identity before transport. Lost acknowledgements therefore retain the same delivery identity; only the exact durable receipt completes the source. Package build skew is allowed only while protocol version and capabilities remain compatible.
|
|
439
549
|
|
|
@@ -444,13 +554,13 @@ The canonical update transition contract is:
|
|
|
444
554
|
- `pending → executing`: the generation-local worker selects an unclaimed source; `executing` is a runtime phase, not a separately persisted entry state.
|
|
445
555
|
- `executing → completed | queued | pending | retry-wait`: exact local completion removes the entry, queue admission persists its receipt, deferred grouping retains replay authority, and every execution failure persists retry evidence.
|
|
446
556
|
- `retry-wait → executing`: only after `nextRetryAtMs`; repeated signals before eligibility do not execute the entry. Automatic retries continue indefinitely with exponential `1s → 2s → 4s → 8s → 16s → 32s → 60s` delay capped at `60s`, while later independent updates continue draining.
|
|
447
|
-
- Legacy `failed → retry-wait`: startup atomically resumes terminal entries written by earlier `0.28.0` candidates.
|
|
557
|
+
- Legacy `failed → retry-wait`: startup atomically resumes terminal entries written by earlier `0.28.0` candidates. Ordinary execution/retry policy never silently discards valid inbound authority and exposes no Pi command for manual retry/discard. Approved damaged-history reset and disposable-session sweeping are separate storage-loss policies, not successful execution or receipt settlement.
|
|
448
558
|
- `queued → offered → staged → queued`: only the exact persisted donor may offer or cancel a live handoff; an offer preserves donor ownership but freezes ordinary settlement and recovery. Authenticated bounded IPC stages one exact payload/receipt outside the live queue. Exact recipient acceptance mints a fresh acquisition, reconstructs local ownership, removes donor work, then publishes recipient dispatch readiness.
|
|
449
559
|
- `queued → completed`: only the exact persisted owner receipt may complete or discard queued sources; generic completion rejects queued state. Process-birth-proven owner death atomically discards the complete unoffered session-owned receipt without replay; live, unverifiable, or offered owners remain queued.
|
|
450
560
|
|
|
451
561
|
The worker executes at most 64 eligible entries from one validated journal snapshot, commits ordinary completions through one journal transaction, then yields through a generation-checked event-loop boundary. Retry, queue, or prior-generation boundaries first flush completed ids and force a fresh snapshot, preserving exact state-transition atomicity without per-entry parse/rewrite churn. A deterministic 2,048-entry stress gate requires exactly 32 completion publications, 33 reads including the final empty snapshot, continued 1ms timer progress, and less than 250ms maximum observed heartbeat delay. Byte-capacity tests cover failed and retry-wait diagnostics, queue receipt/owner and handoff metadata, and operator dispositions; every rejected growth leaves the prior authority bytes unchanged. It still scans later independent entries after retry or terminal persistence. An unresolved reaction remains a queue-mutation dependency even in `retry-wait` or `failed`, but dispatch checks that dependency against the candidate queue item's exact chat and source message ids instead of globally blocking unrelated targets. Successful replay or an exact discard disposition releases the dependency. Worker state, debug status, state snapshots, and redacted runtime events expose journal depth, retry/terminal counts, the next retry, latest terminal identity, copyable operator commands, and the exact first foreign queued owner identity (instance, PID/birth, session, and acquisition) when semantic authority belongs to another process.
|
|
452
562
|
|
|
453
|
-
The journal is the sole polling/admission authority. Each atomic journal revision publishes the admitted batch and monotonic `acceptedThroughUpdateId` together; cursor-only initial synchronization uses an empty batch revision. Existing config cursors are transferred once before polling: journal publication precedes config removal, restart retries are idempotent, established journal authority never regresses, and an unprovable non-empty journal fails closed. Upgrades create journals lazily before the first post-upgrade offset advance. A bot/profile identity change with unresolved authority fails closed. Once reconstructed authority is empty, the next read atomically rebinds profile and bot identity under the journal transaction and removes redundant old-identity segments best-effort; stable-`botId` token rotation remains valid even with entries. Downgrading below `0.37.0` with a cursor-schema journal is unsafe because an older runtime cannot recover `acceptedThroughUpdateId` and could repoll admitted updates.
|
|
563
|
+
The journal is the sole polling/admission authority. Each atomic journal revision publishes the admitted batch and monotonic `acceptedThroughUpdateId` together; cursor-only initial synchronization uses an empty batch revision. Existing config cursors are transferred once before polling: journal publication precedes config removal, restart retries are idempotent, established journal authority never regresses, and an unprovable non-empty journal fails closed. Upgrades create journals lazily before the first post-upgrade offset advance. A bot/profile identity change with unresolved authority fails closed. Once reconstructed authority is empty, the next read atomically rebinds profile and bot identity under the journal transaction and removes redundant old-identity segments best-effort; stable-`botId` token rotation remains valid even with entries. Downgrading below `0.37.0` with a cursor-schema journal is unsafe because an older runtime cannot recover `acceptedThroughUpdateId` and could repoll admitted updates. After rebuilding the checkout, run `node scripts/check-downgrade.mjs [agent-dir]`; the conservative older-schema validator rejects that retained authority even when entries are drained. It inspects flat snapshots and canonical session-folder `journal.*` and `inbox[.<profile>]` snapshots plus segment tails under shared directory/file/byte limits. Noncanonical directory names, symbolic links, orphan segments and private retention refuse inspection instead of being ignored; legacy `recovery/` folders hold no journal authority and are skipped. It never modifies or compacts files. Stop all participants before relying on the result: this journal-only advisory check is not writer closure, a strict whole-namespace recensus, proof of coordinator compatibility or permission to downgrade, replay or delete. The pre-0.52.0 `tmp/telegram` tree remains untouched. Current-runtime corruption recovery is the distinct guarded delete/reset policy above; unsupported schemas and saved `telegram.json` stay preserved.
|
|
454
564
|
|
|
455
565
|
Polling and inbound-worker diagnostics remain separate so an executing, deferred, locally queued, foreign-queued, or blocked journal head cannot masquerade as a stalled `getUpdates` request.
|
|
456
566
|
|
|
@@ -510,6 +620,10 @@ One monotonic session generation also fences agent/tool/message events, compacti
|
|
|
510
620
|
|
|
511
621
|
For a configured Rich response with final text and exactly one supported queued PNG/JPEG, MP4, or MP3 artifact, queue orchestration asks `outbound-attachments` for one reply-anchored multipart Rich result before finalizing ordinary text. A successful result clears the preview, records exact message ownership, and suppresses duplicate text/file delivery. A known-safe rejection returns to the established paths; an ambiguous send stops the turn without fallback or replay. HTML mode, multiple or unsupported files, Guest Mode, and all voice-policy outputs bypass this optimization.
|
|
512
622
|
|
|
623
|
+
#### Queue Lifetime
|
|
624
|
+
|
|
625
|
+
The prompt queue is session-local, not a restart-persistent inbox. Waiting prompts are discarded on shutdown and dead-owner cleanup, and dispatch stays connected-local as described above. The operator excluded restart-persistent prompt storage from 0.52.0 and future plans to avoid unnecessary complexity; it is cancelled, not deferred. Operator rule before any reload or restart: wait for the queue to drain, or knowingly accept losing the waiting work and re-send what still matters afterwards. Nothing is preserved or replayed automatically. Existing durable admission, receipt ownership, custody and anti-replay protections remain unchanged and do not promise reconstruction of the waiting prompt queue.
|
|
626
|
+
|
|
513
627
|
### Controls And Menus
|
|
514
628
|
|
|
515
629
|
Telegram controls execute through command/callback domains, not by entering the normal prompt queue unless they intentionally create a prompt turn. Built-in read-only menu commands are admitted once required local state mutation finishes: first-user pairing still persists before `/start` is accepted, while menu rendering and BotFather command synchronization run as context-fenced best-effort effects with diagnostic failure sinks. Their unresolved Telegram calls therefore cannot retain the durable polling offset or prevent the next `getUpdates` request. Detached effects, deferred dispatch/watchdog, typing, and diagnostics callbacks contain primary and diagnostic failure; stale typing context is ignored, while snapshot publication serializes one write plus one retained coalesced rerun. Raw companion handlers still run before durable built-in routing and should return quickly even though their execution no longer retains polling.
|
|
@@ -535,6 +649,8 @@ Queue and menu mutations are reachable through Telegram updates handled by the c
|
|
|
535
649
|
|
|
536
650
|
Manual `/compact` requires inline confirmation because accidental taps are disruptive. Confirmed manual compaction and auto-compaction both set the bridge compaction flag, block queued prompt dispatch, retain that flag in explicit diagnostics, and clear it on native compact completion or failure, timeout fallback, or session shutdown. Pi owns its terminal compaction lifecycle; pi-telegram keeps `Active` scoped to Telegram-owned work and otherwise preserves the stable connected/leader/follower role. Mid-run threshold compaction reports notices in place between tool output and the next assistant response; compaction observed after terminal assistant output waits for final Telegram delivery so transport chronology matches the terminal.
|
|
537
651
|
|
|
652
|
+
The five-minute observer timeout releases the local compaction flag and observer-owned typing and requests deferred queue dispatch; it is not Pi completion, cancellation or failure. Pending automatic terminal-notice and Activity correlation survive that timeout so a later native success/failure/cancellation still reports once. A new compaction supersedes the prior correlation; session shutdown discards it. Existing session and transport fences still govern publication.
|
|
653
|
+
|
|
538
654
|
Native typing during compaction follows connected-instance activity rather than terminal status:
|
|
539
655
|
|
|
540
656
|
- Confirmed manual `/compact` starts a native `typing` keepalive in the command target and stops it on completion/failure.
|
|
@@ -612,6 +728,8 @@ The bridge does not mirror arbitrary `ctx.ui.confirm/input/select/custom` prompt
|
|
|
612
728
|
|
|
613
729
|
## Diagnostics And Operational Behavior
|
|
614
730
|
|
|
731
|
+
Deferred diagnostics snapshots are session-owned. Each 100 ms coalesced request captures the exact context/session generation before its timer or queued microtask; a successor requires a fresh request. Shutdown synchronously suspends requests, cancels the timer and drops predecessor reruns, then drains any started publication before cleanup can proceed. Fencing occurs before status projection, not only before publishing the runtime section: its cursor getter uses a journal reader that can create transaction guards or recover a missing snapshot. Already-dequeued callbacks cannot clear a successor timer or recreate retired journal families. Explicit status rendering stays immediate, while its best-effort snapshot request shares the same owned scheduler. The session scope is structural; Status stays independent of Lifecycle and Locks, and neither diagnostics nor its drain grants transport, storage recovery or routing authority. Deterministic removed-directory and snapshot-with-segments regressions guard fixture isolation without sleeps, deletion retries or cursor relaxation; native fixtures are not live or cross-platform acceptance.
|
|
732
|
+
|
|
615
733
|
Status rendering distinguishes connected, active, dispatching, queued, tool-running, model-switching, and compacting states; the Telegram status menu gives compaction precedence over generic active or pending work. Its compact Tokens row shows only input and output totals, while the adjacent Cache row groups `R` cache-read tokens, `W` cache-write tokens, and `CH` for the latest assistant request's cache-read share of prompt tokens rather than a misleading cumulative-session ratio; the labels remain distinct from companion-provided usage limits. Observed automatic compaction sends the same start and completion notices as the manual command without duplicating notices for command-owned compaction. If no terminal hook arrives, the current observation times out after five minutes: it releases only observer-owned status/typing, records a diagnostic, and requests a guarded queue-dispatch recheck. Timeout is not proof that Pi compaction completed or was cancelled and emits no invented terminal notice. Superseded timeout callbacks and stale-context terminal hooks cannot abandon or cancel a newer observation. If a queue mutation removes the last waiting item while Telegram-owned work still has running tools, status remains active instead of degrading to connected.
|
|
616
734
|
|
|
617
735
|
Queue reaction behavior, lane-tail transitions, Keep/Skip independence, multi-reaction precedence, and the Bot API reaction-removal limitation are defined in [Priority, Reactions, Keep, and Skip](#priority-reactions-keep-and-skip). Reaction changes first flush a matching delayed text or media group so the governed turn exists before mutation, and dropping marked heads cannot leave status permanently queued.
|