# Services Architecture This file documents the local service shape on `ballbox-first`, with emphasis on terminal persistence for `pi` and tmux. ## Goal Keep `pi` usable as an interactive TUI while making tmux only a persistence layer. Desired properties: - `pi` runs inside tmux. - Closing a terminal client does not kill `pi`. - tmux does not become the primary UI. - Mouse scroll works for history without moving the `pi` cursor. - Color rendering stays correct inside tmux. ## Current tmux model ### tmux config `~/.tmux.conf` is intentionally persistence-first: - `mouse on` - `history-limit 100000` - `set-clipboard on` - custom prefix: `C-\\` - status bar off - dangerous interactive defaults unbound - copy mode uses vi keys - wheel scroll enters/uses copy mode - explicit detach/reload bindings only ### Important tmux bindings - Prefix: `Ctrl-\\` - Detach: `Ctrl-\\` then `D` - Reload config: `Ctrl-\\` then `r` - Scroll/history: - wheel up enters copy mode or scrolls within it - wheel down scrolls or exits copy mode ### Why tmux stays minimal tmux is not used for pane/window management here. It is only the durable PTY container for the running `pi` process. That means: - no pane splitting - no tab/session UI workflow - no mouse-driven tmux navigation - no session picker dependency for normal `pi` use ## `pitmux` `~/.bashrc` defines `pitmux()`. Purpose: - start `pi` inside a tmux session - keep the session attachable later - keep color output correct inside tmux ### Current launch behavior `pitmux` forces these env values for the `pi` process: - `TERM=tmux-256color` - `COLORTERM=truecolor` - `TERM_PROGRAM=` This avoids the brown background / palette mismatch that appeared when `pi` ran inside tmux with the host terminal's `TERM_PROGRAM` leaking through. ### Usage ```bash pitmux pitmux pi-1 ``` If no name is passed, `pitmux` creates a timestamped session name like: ```text pi-20260421-165900 ``` ### Launch shape `pitmux` runs: ```bash env TERM=tmux-256color COLORTERM=truecolor TERM_PROGRAM= tmux new-session -A -s pi ``` ## `pi` resume helper `~/.bashrc` also defines `piresume()`. That wrapper is separate from tmux. Purpose: - run `pi` normally - after exit, print the latest session id hint - help resume with: ```bash pi --session ``` ## Native detached batch jobs and loops for subagents For simple non-interactive `pi` automation, use native shell helpers instead of extension-based side-agent orchestration. Tool split: - `pi-loop`: multiple sequential passes over the same task prompt in the same working tree - `pi-job-*`: one detached inspectable batch run with explicit lifecycle commands - `ralph-loop-run`: blocking sequential multi-agent loop with file-based shared state between iterations Choose `pi-loop` when you want later sessions to reinforce or refine earlier work. Choose `pi-job-*` when you want one background run with explicit `check`, `wait`, and `stop` controls. Choose `ralph-loop-run` when you want strict step-by-step orchestration, hardcoded runner logs, and control returned only after the full sequence finishes. Observed fit from local runs: - strong fit for bounded multi-pass repo sweeps with a concrete checklist and explicit goal gates - especially good when each pass should inherit precise state from the previous one through `TASKS.md` and `STATE.md` - worked well for Ballbox accessibility/hardening loops where each iteration made a small verified change, updated the shared files, and handed off the next narrow step - especially valuable when the tail of the loop acts as validator, with permission to do bounded self-repair and then run one final revalidation pass - less useful for one-shot changes, broad discovery, or tasks that benefit more from parallel branching than serialized handoff - fails badly if the loop is treated as "N passes no matter what" without binary success checks, feature-state tracking, and no-progress escalation Shared shape: - exact existing `cwd` is chosen by caller - non-interactive `pi` execution with explicit provider/model defaults - durable logs and artifacts on disk - no cron, no background daemon, no extra service layer ### `pi-loop` Use `pi-loop` for repeated passes on one task. Behavior: - caller sets iteration count and base prompt - each iteration prepends loop context including current iteration number - each iteration can include tail output from the previous iteration - each iteration runs in the same working tree so repo state carries forward - run artifacts are stored under `/home/sebas/runtime/pi-loop-runs//` See: `docs/pi-loop.md` ### `pi-job-*` Use `pi-job-*` for one detached inspectable batch run. ### `ralph-loop-run` Use `ralph-loop-run` for a blocking multi-iteration orchestration loop where each new `pi` agent must communicate through files in a shared run directory. Behavior: - one run directory under `/home/sebas/runtime/ralph-loops//` - runner seeds `TASKS.md` and `STATE.md` - each iteration gets a fresh non-interactive `pi` invocation - agents read and update only the shared run files for handoff - runner logs hardcoded steps in `runner.log` - caller gets control back only after all iterations finish - no tmux and no detached process model by default Use it when: - the task can be decomposed into narrow sequential passes - the next best step should be chosen by the previous pass, not by a static up-front plan - you want a durable audit trail of iteration-by-iteration progress and validation - you want fresh-agent resets between passes so drift stays low - you want the loop to end with validator-driven closure instead of a plain final summary Avoid it when: - one direct pass is enough - the task needs concurrent branches or side investigations - the handoff overhead would exceed the value of the extra passes Default completion pattern: - before the loop starts, lock intent gates with the user when they are unclear. Intent gates include the real objective, exclusions, accepted tradeoffs, and what counts as done. - open passes continue until a goal gate is reached: `done`, `blocked_with_reason`, or `needs_user_decision` - do not treat a fixed number of passes as the main completion rule; pass caps are safety limits, not success definitions - technical checklists and feature-state tracking may be derived by the agent only after intent gates are clear - reserve the tail for validator by default - if validator finds bounded fixable gaps, let it repair them - after repair, require one extra revalidation pass - stop only when revalidation passes cleanly, scope/risk blocks self-repair, or the loop ends in explicit blocked/user-decision state Required loop artifacts: - binary checklist for visible success criteria - explicit feature states for every visible feature: - `real_working` - `real_read_only` - `blocked_hidden` - `unknown_do_not_show` - explicit state markers in `STATE.md`: - `candidate_done` - `validator_status` - `repair_applied` - `revalidation_status` No-progress rule: - a pass counts as material progress only if it changes at least one of: - code - state - evidence - risk level - unblock path - if a pass makes no material progress, the next pass must not continue as usual - it must instead: - replan - switch tools/approach - or exit to `blocked_with_reason` / `needs_user_decision` Validator authority: - validator should fail immediately if a visible business feature is backed by fake/local placeholder data presented as real - validator may prune visible scope instead of allowing half-real features to remain exposed - unsupported or unconfirmed features should be hidden/disabled, not left ambiguous Easy verification later: - final `STATE.md` should explicitly say whether the closing pass was `validator`, `repair`, or `revalidation` - `STATE.md` should also show the goal gate outcome and the validator markers above - if repair happened, final `STATE.md` should also state that validator found fixable gaps - `runner.log` should show whether the loop ended directly after validation or after a repair-plus-revalidation tail - if those markers are missing, treat the loop as not having followed the default validator pattern Desired shape: - one initial prompt only - `pi` runs non-interactively (`pi --provider openai-codex --model gpt-5.4-mini --thinking low --no-extensions -p` by default) in a detached background process - output is written to a durable log file - parent orchestrator can poll state, tail logs, wait, or stop the process - no tmux dependency for one-shot jobs ### Runtime layout Use `/home/sebas/runtime/pi-jobs` as runtime storage. Per job: ```text /home/sebas/runtime/pi-jobs/jobs// meta.env prompt.txt output.log status pid exit_code started_at finished_at ``` ### Process model - `pi-job-start` creates the job directory and launches a detached runner with `nohup`. - the runner executes `pi -p` in the requested directory with explicit provider/model defaults to avoid accidental fallback to unauthenticated providers. - the runner writes status files and appends full stdout/stderr to `output.log`. - `pi-job-check` reconciles file status with `pid` liveness and prints recent log lines. - `pi-job-wait` is a simple poll loop over status files plus `pid` liveness. - `pi-job-stop` sends `TERM`/`KILL` to the recorded `pid` and marks the job as stopped. ### Why this path This keeps the batch layer native and inspectable: - detached process for lifetime - shell scripts for control - plain files for logs and metadata Use tmux only for interactive persistent `pi` sessions (`pitmux`, Telegram service), not one-shot batch jobs. Default batch overrides: - `PI_PROVIDER` defaults to `openai-codex` - `PI_MODEL` defaults to `gpt-5.4-mini` - `PI_THINKING` defaults to `low` No interactive key injection workflow is assumed beyond the initial prompt. ## Dedicated Telegram `pi` service For an always-reachable Telegram agent, run one dedicated `pi` instance under a user systemd service and keep tmux as the PTY container. Desired shape: - one fixed tmux session: `pi-telegram` - one fixed working directory: `/home/sebas` - one fixed config dir: `/home/sebas/pi-config` - service restarts on boot via user systemd - service loop recreates the tmux session if `pi` exits - Telegram ownership stays on that one instance; use `/new` there when you want a fresh conversation ### Runtime pieces - launcher loop: `/home/sebas/pi-config/bin/pi-telegram-service` - user service: `~/.config/systemd/user/pi-telegram.service` - tmux session name: `pi-telegram` - Telegram extension source pinned locally via settings package path: `/mnt/rpi/dev/pi-telegram-sebas` ### Behavior - first-time setup still needs one manual `/telegram-connect` inside that dedicated `pi` session - after that, normal conversation and `/new` stay inside the same long-lived instance - `/new` is provided by the local pinned `pi-telegram-sebas` fork, not upstream npm package state - after reboot, the service relaunches the same tmux-backed `pi` process from the same `cwd`, which matches the Telegram bridge auto-resume model ## Wi‑Fi tracker service A lightweight Wi‑Fi/LAN presence tracker now runs on this machine and publishes a small dashboard through nginx. Scope of this service: - collect device presence visible from this Pi on the local LAN - keep an append-only JSONL activity log - maintain per-device aliases set from the web UI - publish a derived summary for dashboard rendering - expose a tiny local API for alias CRUD Non-goals: - this is not exact Wi‑Fi association telemetry from the router/AP - no packet payload capture - no DPI - no traffic-content logging ### Current model This Pi is not the AP/router controller for the home network. So the tracker uses LAN presence observation: - network sweep over the local subnet - neighbor table (`ip neigh`) snapshot - reverse DNS lookup when available - router HTML probe for basic vendor/model identification This yields: - IP - MAC - hostname sometimes - first seen / last seen - online/offline transitions inferred from snapshots It does not yield: - exact connect/disconnect at Wi‑Fi layer - SSID/BSSID - RSSI - AP roaming details ### Runtime pieces Collector and derived data: - script: `/home/sebas/pi-config/bin/wifi-presence-log` - raw log: `/home/sebas/runtime/agent-logs/wifi-presence/activity.jsonl` - state: `/home/sebas/runtime/agent-logs/wifi-presence/state.json` - aliases: `/home/sebas/runtime/agent-logs/wifi-presence/aliases.json` - fingerprints/evidence: `/home/sebas/runtime/agent-logs/wifi-presence/fingerprints.json` - alert prefs: `/home/sebas/runtime/agent-logs/wifi-presence/alert-prefs.json` - inventory ever-seen: `/home/sebas/runtime/agent-logs/wifi-presence/inventory.json` - dashboard summary: `/var/www/wifi-tracker/summary.json` - service note: `/home/sebas/pi-config/docs/wifi-tracker.md` Scheduling: - service: `~/.config/systemd/user/wifi-presence-log.service` - timer: `~/.config/systemd/user/wifi-presence-log.timer` - cadence: every 2 minutes Dashboard/API: - static UI: `/var/www/wifi-tracker/index.html` - API server: `/home/sebas/pi-config/bin/wifi-tracker-api.py` - API service: `~/.config/systemd/user/wifi-tracker-api.service` - health endpoint: `/wifi-tracker/api/health` - alert prefs endpoint: `/wifi-tracker/api/alert-prefs` - nginx route: `/wifi-tracker/` - nginx API route: `/wifi-tracker/api/` - portal link added on `/` ### Data flow 1. `wifi-presence-log.timer` triggers the collector. 2. Collector sweeps the subnet and reads neighbor state. 3. Collector appends JSONL events to `activity.jsonl`. 4. Collector updates `state.json` and rebuilds `summary.json`. 5. Web UI fetches `/wifi-tracker/api/summary`. 6. Alias writes go to `/wifi-tracker/api/aliases` and persist in `aliases.json`. 7. Alias writes also patch `summary.json` immediately for UI feedback. 8. Collector runs lightweight fingerprint heuristics and may write descriptive ` [from AI]` aliases for previously unnamed devices. 9. Fingerprint evidence is written to `fingerprints.json`. 10. Next collector cycle recomputes the full summary, activity buckets, and per-device presence timeline segments derived from raw snapshots. 11. An ever-seen inventory is updated so historical devices remain visible even after they disappear from current state. ### Event types in `activity.jsonl` - `cycle_start` - `router_probe` - `device_snapshot` - `device_online` - `device_offline` - `cycle_end` ### Dashboard behavior Current UI supports: - current device list - alias input per MAC - recent online/offline events - per-device activity chart with ranges: - hour - day - week - health indicator - per-device alert on/off toggle - fingerprint/evidence table - presence timeline graph with blue connected segments and offline hysteresis - explanatory text in the page itself Activity graphs are derived from approximate sessions rebuilt from repeated `device_snapshot` observations. Buckets represent estimated hours of presence, not traffic volume. ### Operational notes - Alias changes now patch the current summary immediately, then get recomputed on the next collector cycle. - Auto-generated aliases are marked with ` [from AI]` and should not overwrite manual naming. - Fingerprint evidence is persisted separately so guesses can be audited and corrected. - If better fidelity is needed later, preferred upgrade path is router/AP integration rather than local sniffing from this Pi. - Raw logs live under `/home/sebas/runtime/agent-logs` as part of the canonical runtime plane. - Raw JSONL rotates automatically with a small rolling retention window. ## Shell / web service history ### Removed - `code-server` at `/cli` - `ttyd` - `dtach` - `pi-dashboard` Reason: they did not match the target shape or they introduced too much UI mismatch. ### Current direction The current preferred path is: - local terminal or SSH - `pitmux` for persistence - tmux only as a PTY container - `pi` as the interactive TUI ## Operational rules - Do not use tmux as the main UI. - Do not depend on `Ctrl+D` as a detach action; it exits the shell and can kill the session. - Use tmux detach explicitly. - Keep browser/web exposure out of the loop unless a future tool actually needs it. ## Open questions - Whether `pi` can eventually run with a better PTY/session wrapper than tmux. - Whether a future tool can expose the same persistent TUI model without tmux. ## Relevant files - `~/.bashrc` - `~/.tmux.conf` - `/home/sebas/pi-config/AGENTS.md`