Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ The core product loop is mobile continuation: start or supervise work in the ter
- The bridge is session-local, paired to one allowed Telegram user, and owns a local queue aligned with Pi lifecycle hooks
- The Telegram lock owns external Telegram transport, not local Pi execution. Local extension state and queue runtime are per Pi instance: losing exact `/telegram-connect` ownership must not clear accepted queue state or stop local Pi dispatch, but preview, final delivery, attachments, menus, and other Bot API mutations fail closed once another runtime owns transport. A replacement owner must never receive stale direct mutations merely to preserve old-runtime Telegram output. Bus leader lock entries carry a stable `leaderEpoch` minted on acquisition and preserved across heartbeat refreshes; leader-owned Thread Reconciler cleanup/provisioning actions stamp that epoch and skip destructive side effects if current leadership differs.
- The Telegram lock is always the transport ownership lock. Every shared `locks.json` mutation must serialize the complete cross-process read/check/write transaction, preserve unrelated registry keys, and fail closed on transaction contention; atomic payload replacement alone does not make ownership linearizable. Refresh, release, takeover, and irreversible leader work must additionally fence against the exact acquired owner and leader epoch. In classic/private-chat mode, transport ownership is the whole bridge ownership and `/telegram-connect` from another live Pi should offer the classic takeover confirmation. In Threaded Mode, transport ownership is the bus leader role and `/telegram-connect` from another live Pi should register as follower instead of offering takeover while the leader is live. If follower registration is not applicable because Threaded Mode is disabled or unavailable, fall back to the classic takeover path.
- Shared filesystem transactions publish a fully initialized private guard directory containing one exact generation-specific `owner.<generation>.json` through same-parent atomic rename. The owner filename and payload generation must match so stale observation cannot rename replacement metadata. Release verifies the owner and atomically renames the stable guard away before cleanup. Directory recovery first claims owner metadata inside the still-occupied guard so a crashed recoverer can itself be replaced without exposing the stable path; transient guard moves and rollback both retry, while inactive process-global claim generations remain recoverable after failed restoration. If secondary recovery cleanup fails after publishing a replacement main guard, release that exact replacement before propagating the failure. Legacy regular-file recovery remains separately serialized through recoverable directory guards. New guards must not depend on hard links, direct file rename-overwrite, incomplete create-then-write state, or age-only recovery. Legacy regular-file guards may be read only for bounded upgrade recovery; malformed or unsupported guards fail closed.
- Telegram profiles are parallel bot/runtime realities, not merely alternate credentials. Shared bridge settings remain global, but any lock, IPC endpoint, state snapshot, diagnostics log, thread/bus binding, runtime cache, ownership map, or operator-visible status surface that could mix bot identities must be deliberately classified as global, session-local, or profile-scoped. Preserve default-profile legacy paths/keys for compatibility, and give named profiles isolated observable surfaces unless there is an explicit top-level shared-config reason not to.
- Telegram private-chat Threaded Mode is the public switch for the local multi-instance bus. There is no separate public `telegram.json` bus switch: classic/private-chat singleton DM behavior is the base product mode, and when Telegram reports Threaded Mode available for the bot the bridge enables leader/follower bus behavior. If Telegram reports Threaded Mode unavailable or thread operations prove it unavailable, the bridge returns to classic singleton behavior as a first-class mode. During a live Threaded Mode downgrade, the current transport leader keeps the singleton classic polling role and followers disconnect instead of taking over or offering takeover while active thread bindings prove this is a downgrade from a live bus organism. Do not inject slot/thread rename guidance in classic mode. Blocked instances should not silently register as followers unless the effective runtime mode is Threaded Mode.
- In Threaded Mode, exactly one live leader owns `getUpdates` and direct Telegram Bot API calls for one bot token. `/telegram-connect` means join the local Telegram organism: if no live leader exists the instance becomes leader; if a live leader exists the terminal-visible instance registers as follower; ordinary connect must not offer manual takeover while the leader heartbeat is live. Leadership is an ephemeral transport role, not permanent ownership by the first terminal-visible Pi instance. Followers must be started by the operator as real Pi processes, register over the local bus, heartbeat, receive only their routed inbound updates, and send outbound replies/previews/files/chat-actions through the leader transport; Telegram must not spawn hidden Pi processes, launch follower subprocesses, or expose a slash command that creates a new local Pi instance. If a leader reloads while followers stay alive, followers must treat rejected/unknown/unauthorized heartbeats as a registration-loss signal and optimistically re-register with the currently active leader before considering promotion; after a short grace window without successful re-registration, a live follower may promote only through an atomic expected-owner transition after the exact observed leader lease becomes stale or inactive. An unreachable IPC endpoint does not authorize replacing a still-live lock owner; remain disconnected/retrying rather than creating split-brain polling. Terminal status must not remain `follower` while the leader registry can no longer route that follower's thread. Heartbeat pruning is liveness bookkeeping, not permission to delete, mark offline, or send a Telegram-visible disconnected notice for a follower's thread; preserve active follower tabs/bindings across transient reload/reconnect incidents and reserve destructive teardown/offline transitions for explicit disconnect or confirmed reconciliation cases. If the leader dies, a live follower should eventually be able to take over through stale-leader election/recovery so the local multi-instance organism keeps running. Explicit `/telegram-disconnect` is a manual thread teardown: a leader deletes its own bound thread before releasing transport ownership, while a follower asks the leader to delete its assigned thread through scoped bus API before unregistering. Status is reactive to the effective runtime role and bot capability: classic/non-thread mode shows ordinary `telegram connected`, active bus leadership shows `telegram leader`, and registered followers show `telegram follower`; stable multi-instance truth belongs in the bus role (`leader`/`follower`), while lifecycle is reserved for transient recovery state such as `electing` and must not duplicate stable roles with labels like `leader-active`. When leadership or Threaded Mode availability changes at runtime, the status line and `/telegram-status` must converge promptly instead of preserving a stale role label.
Expand Down
30 changes: 29 additions & 1 deletion BACKLOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,34 @@
# Project Backlog

_This backlog tracks only open release-relevant work: live runtime verification, evidence-gated Telegram client follow-ups, and upstream Pi API blockers. Completed outcomes and validation evidence belong in `CHANGELOG.md`, not in this queue._
_This backlog tracks only open release-relevant work: hotfixes, live runtime verification, evidence-gated Telegram client follow-ups, and upstream Pi API blockers. Completed outcomes and validation evidence belong in `CHANGELOG.md`, not in this queue._

## P0 — Termux-Compatible Filesystem Transactions (`0.22.1`)

Context: issue #131 proves that Android/Termux rejects the hard-link publication used by the `0.22.0` transaction guard, causing Pi to exit during JSONL initialization. A direct file-rename fallback is unsafe because rename replaces an existing destination and can admit multiple transaction owners. The hotfix keeps per-resource serialization but publishes a fully initialized, non-empty guard directory atomically.

Open work:

- [x] Replace staged-file/hard-link publication with a staged guard directory containing one private generation-specific owner file, then atomically rename that non-empty directory to the stable per-resource transaction path.
- [x] Keep collision-resistant owner generation plus PID and acquisition time, exact-owner verification, serialized stale recovery, and fail-closed malformed-guard behavior.
- [x] Release guards by exact-owner atomic rename away from the stable path before recursive cleanup; support stale legacy file guards left by `0.22.0` during upgrade.
- [x] Ensure JSONL append/rotation transaction failures remain diagnostics-only and cannot produce an unhandled rejection that terminates Pi.
- [x] Add deterministic regressions for directory publication, exact release, legacy and directory stale recovery, malformed guards, simultaneous acquisition, concurrent recovery, and swallowed diagnostics failures.
- [x] Update lock/diagnostics documentation and durable transaction contracts for the staged-directory protocol and fail-soft JSONL behavior.
- [x] Run implementation tests, typecheck, strict Domain DAG, ABCd, audit, package dry-run, invariants, and `git diff --check`.
- [x] Run an independent four-lens concurrency/filesystem review of directory publication, contention classification, stale recovery, exact release, legacy migration, and diagnostics containment; preserve its `NOT READY` evidence for the reproduced abandoned-recovery-guard deadlock.
- [x] Replace directory stale recovery with an internal exact reclaim marker that remains recoverable after claimant death; preserve live recovery guards and recover abandoned new-directory and legacy-file recovery guards with deterministic regressions.
- [x] Prove fail-soft diagnostics in a child process that emits one failing record under `--unhandled-rejections=strict`, independently from later queue recovery.
- [x] Run a clean follow-up review and preserve its `NOT READY` evidence for the reproduced same-process reclaim stall after a transient guard-rename failure.
- [x] Retry transient reclaim renames, roll the exact marker back on exhaustion, and treat inactive process-global reclaim generations as recoverable even when rollback itself fails; cover same-process reacquisition in both cases.
- [x] Run a final four-lens review and preserve its `NOT READY` evidence for peer-visible marker starvation and leaked replacement ownership after failed abandoned-recovery cleanup.
- [x] Retry rollback renames for peer-visible recovery, release exact newly recovered main ownership when secondary cleanup fails, and cover both failure sequences with deterministic same-process and child-process regressions.
- [x] Run one bounded final reviewer and preserve its `NOT READY` evidence for a reproduced stale-observation ABA race that let a delayed recoverer claim a replacement generation.
- [x] Bind each new directory owner generation into its unique `owner.<generation>.json` path and require filename/payload generation agreement, so delayed recovery receives `ENOENT` instead of renaming replacement metadata; cover the exact interleaving and repeat the child-process recovery barrier 20 times.
- [x] Obtain a final clean `READY` verdict and resolve every remaining release-blocking local finding.
- [x] Prepare `0.22.1` version metadata after review readiness, then rerun the complete release gate against the final package contents.
- [ ] Ask the issue #131 reporter to verify Termux startup, config/log writes, `/telegram-connect`, and reload after the hotfix becomes available; retain this as post-release environment evidence rather than a local release blocker.

Done when: pure Node filesystem operations provide exactly-one transaction ownership without hard links; old, replacement, and failed-recovery owners cannot delete or strand each other's guards; diagnostics failures cannot terminate Pi; independent review returns local `READY`; final release gates pass; and the post-release Termux verification request remains explicit until reporter evidence arrives.

## P1 — Promoted Follower Reload Evidence

Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## Unreleased

## 0.22.1: Termux-Compatible Filesystem Transactions

- `Filesystem Transactions`: Replaced hard-link guard publication with private staged directories containing complete exact-owner metadata, atomically published each non-empty guard by same-parent rename, and atomically renamed exact-owned guards away before recursive cleanup. Bound each owner generation into a unique filename so delayed stale observations cannot claim replacement metadata, retained exact stale recovery through recoverable internal claimant markers, retried reclaim and rollback moves without peer-visible stalls, released newly recovered ownership when secondary cleanup fails, kept fail-closed malformed-state handling, and bounded legacy regular-file guard recovery. Impact: shared Telegram state no longer requires the hard-link operation rejected by Android/Termux, while exactly-one cross-process ownership remains locally enforced; live Termux confirmation remains post-release evidence.
- `Diagnostics`: Contained synchronous and queued JSONL persistence failures inside the diagnostics boundary while preserving later queued records after a failed append or rotation. Impact: an unavailable diagnostics path cannot terminate Pi through an unhandled rejection or permanently poison subsequent runtime evidence.

## 0.22.0: Concurrency And Runtime Ownership Hardening

- `Composition Root`: Reduced `index.ts` from 1,534 to 1,077 lines by moving transport generations, Threaded Mode orchestration, bus request identity, thread/status projections, exact message ownership, sync/provisioning state, session lifecycle sequencing, diagnostics persistence, Delivery policy, inbound target authority, follower forwarding, retry defaults, and leader identity state into their owning flat domains. The entrypoint now retains high-level runtime construction, narrow live-port wiring, and extension registration; direct one-to-one adapters remain only where another wrapper would hide rather than clarify composition. Impact: release-critical ownership and recovery policy can be tested beside its domain instead of accumulating in the extension bootstrap.
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ The repository uses a **Flat Domain DAG**:
- `outbound-markup`: top-level assistant action comment parsing, attribute parsing, voice reply planning, and preview/delivery stripping.
- `outbound`: outbound text transformations, voice/button artifact delivery, and generated callback actions.
- `outbound-attachments`: `telegram_attach`, queued outbound files, stat/limit checks, and photo/document delivery classification.
- `status` / `logs`: status bar/status-message rendering, queue-lane summaries, the structural redacted event ring, profile-aware JSONL scope/reset/append behavior, exact-owner destructive commits, status snapshot scheduling, and grouped diagnostics. `status` remains a structural leaf; `logs` composes filesystem evidence with status projections.
- `status` / `logs`: status bar/status-message rendering, queue-lane summaries, the structural redacted event ring, profile-aware JSONL scope/reset/append behavior, exact-owner destructive commits, fail-soft synchronous and queued diagnostics persistence, status snapshot scheduling, and grouped diagnostics. `status` remains a structural leaf; `logs` composes filesystem evidence with status projections and contains every persistence failure so diagnostics cannot terminate or poison the runtime queue.
- `lifecycle` / `prompts` / `prompt-templates` / `pi`: session-generation fencing and start/shutdown assembly across Queue, grouped input, Delivery, polling, capability monitor, watchdog, and follower refresh; Pi hook registration; Telegram prompt guidance; prompt-template discovery/expansion; and centralized direct Pi SDK imports.
- `command-templates`: shell-free command-template helpers, composition expansion, placeholder substitution, executable resolution, warnings, and retry/timeout semantics.

Expand Down
Loading
Loading