Skip to content
mossaabPublic

About

No description, website, or topics provided.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

ofx

A command-line console for OpenFox: inspect, diagnose, maintain and act on sessions without going through the web UI.

ofx reads OpenFox's SQLite event log directly (read-only), and uses OpenFox's own HTTP API for anything that writes. Nothing about it is specific to one incident or one instance: it is the operator console for any OpenFox installation, and it keeps working when the web UI does not — a runaway session, a stalled state load, an unreachable API.

What it is for

Need Commands
Health check, on a terminal or from cron status (informational), diagnose / watch — exit 0/1/2 = ok/warn/critical
Session inventory: which session holds what list, list --all
See what is running right now, and where it stands live
Explain where the time and the megabytes go inspect, plan
Steer a session without the UI stop, pause, resume, continue
Create, remove, archive, migrate a session new, rm, export, import
Database care: coherent backup, offline VACUUM backup, vacuum
Service lifecycle and logs service start|stop|restart|status|install|upgrade, logs [-f]
Keep the numbers redirect any command to a file — see Your own reports and diagnostics

Every command is a plain stdout writer with a documented exit code, and all the read-only ones need no credentials, so ofx composes with your shell, your cron and your monitoring instead of replacing them.


Why the event log gets out of hand

OpenFox stores every session as an append-only event log in SQLite. Two design properties make that log grow far faster than the work it describes:

  1. tool.preparing is written once per streamed fragment, each carrying the accumulated JSON arguments. So a tool call whose arguments end up 29 KB long emits thousands of events whose payloads are progressively longer prefixes of the same string. The write cost is quadratic in the argument size.
  2. turn.snapshot is only written when a whole turn resolves. A turn that runs for hours writes no snapshot and triggers no cleanup for its entire duration.

The read side then turns that into a stall. getEvents() is called without a fromSeq bound, so it re-reads the entire journal; and because better-sqlite3 is synchronous, that read blocks the main thread — including the HTTP server.

Measured on a real incident (OpenFox 2.0.160, one session):

Metric Value
Session event log 111,808 events / 210 MB, a single turn.snapshot
Cost of one state load 876 ms, re-reading 203 MB
getLatestSnapshot() ~32–45 ms, walking 83,337 index entries to find one row
getEvents() with fromSeq vs without 747 ms vs 877 ms — the snapshot was too old to help
tool.preparing events for 243 real tool calls 59,913 — a 864× inflation, 203 MB of the 210 MB
Fragments written for a single tool call 6,473 — 84 MB stored where 28 KB would do

These numbers are why ofx reports lag — the bytes accumulated since the last snapshot — rather than counting events, and why inspect measures the real state-load cost instead of inferring it. The measurements come from one instance (see Background); the mechanism is the same on every OpenFox installation, and it is what the tool is built to make visible, prove with numbers, and act on safely.


Requirements

  • Linux (reads /proc, uses systemctl for the service lifecycle)
  • Node.js — the same interpreter that runs OpenFox. better-sqlite3 is a native module, so an ABI mismatch makes require() fail. The ofx launcher finds the right interpreter automatically from the running OpenFox process.
  • OpenFox installed at /opt/openfox (the default; overridable via OPENFOX_DIR)
  • Optionally: a systemd unit named openfox

ofx has no npm dependencies. It reuses the better-sqlite3 that OpenFox already ships.


Install

git clone <this-repo> ~/projects/ofx
cd ~/projects/ofx
./install.sh

install.sh symlinks ofx into a directory on your PATH (default ~/.local/bin), checks that the native module resolves, runs a smoke test, and explains how to authenticate. It is idempotent and does not copy files.

./install.sh --prefix /usr/local/bin   # install elsewhere
./install.sh --uninstall               # remove the symlink

Quick start

ofx status              # service, process, API, database — one screen
ofx list                # sessions, sorted by snapshot lag
ofx inspect --hot       # the worst session, in detail
ofx plan --hot          # what a compaction would do (dry-run, changes nothing)

Everything above is read-only and needs no credentials.

To act on a session, authenticate once:

ofx auth login          # masked password prompt -> token stored with mode 600
ofx stop --hot          # stop the running turn of the worst session
ofx export --hot        # archive it to a file first
ofx compact --hot --apply --restart

Commands

Read-only — no credentials, no writes

Command What it does
status systemd unit, processes, API reachability, DB size/WAL/holders, session lag
list [--all] sessions with event count, payload size, snapshot count, lag
inspect <id|--hot> [--fast] per-type breakdown, snapshot positions, real state-load cost
plan <id|--hot> dry-run of a consolidation: eligibility, what survives, what is lost
live [--interval N] [--min N] live dashboard of running sessions: step, elapsed time, token speed, context, lag
watch live journal growth rate
diagnose full health report with thresholds, exit 0/1/2
backup [--out DIR] [--keep N] coherent DB backup via SQLite's online backup API
vacuum [--dry-run] [--yes] checkpoint + VACUUM (server must be stopped)

Live dashboard: ofx live

ofx live draws the sessions that are running right now — one card each: workflow step, elapsed time, context occupancy, token speed, tokens used and snapshot lag — and, when fewer than two are active, fills the screen with up to two of the most recently finished sessions (duration, final step, final context size, outcome).

ofx live                 # every 2 s, until Ctrl+C
ofx live --interval 1    # faster samples
ofx live --min 0         # running sessions only
ofx live --items         # expand criteria and findings, one line each
ofx live --once          # one screen, for a log

Everything is read-only, and the database connection is opened and closed on every sample: a dashboard left open for hours never pins a WAL read snapshot, so it never delays a checkpoint. A sample was measured at 2–10 ms on a 1.4 GB database — the lag is accounted incrementally and turn.snapshot payloads are never parsed in the loop.

The frame follows the terminal width — there is no upper cap, so a wide terminal gets a wide dashboard: the RECENT table gains columns and a wider STEP. The width is re-read on every sample, so resizing the window is picked up on the next one, and 60 columns is the floor below which the cards cannot stay readable.

A session waiting for you

A session blocked on a human is the one thing the dashboard must not let you miss: it is listed first, its rail and badge turn red, the glyph alternates between ◆ and ◇ every 500 ms, and the second line says what is awaited. The pulse is bounded to the first minute — an older wait stays red and static, so an abandoned question (or a session whose process died while a question was pending) cannot animate the screen forever, and the waiting Ns counter keeps counting between two samples.

  ▌ ◆ WAITING  3f2c1a9e  Fix the login redirect
  ▌     Build & Verify › Implement · build                        waiting 12s · question
  ▌ ◆ WAITING  7b4d0c21  Add the CSV export
  ▌     Build & Verify › Finalize · verification                  waiting 4s · access
Last event Meaning
chat.done with reason waiting_for_user a question is pending — OpenFox writes it immediately after chat.ask_user, so this is the state that is actually observable
chat.ask_user a question is pending, in the instant before that chat.done
path.confirmation_pending an access request on a path, waiting for approval
path.confirmation_responded answered — no longer waiting

The blink redraws the frame already in memory: the sample cadence, and the single database open/close per sample, are unchanged. It is disabled when stdout is not a color terminal (--no-color, NO_COLOR, redirected output), which always gets the "on" phase — a logged sample stays stable and grep-able.

Acceptance criteria and review findings

OpenFox writes the build acceptance criteria and the review findings as metadata.set events (keys criteria and review_findings), rewriting the whole list each time one point changes. They survive cleanupOldEvents(), so they are visible for finished sessions too. ofx live shows one glyph per point, i.e. the list exactly as it is validated, one item at a time:

  ▌     criteria  ✓✓◐✗··✓◐  4/8 done   1 failed · 2 in progress · 3 passed · 2 pending
  ▌     review    ✓○–        2/3 done   1 open · 1 resolved · 1 dismissed
Status Glyph Meaning
pending · not started
completed ◐ implemented, not verified yet
passed / failed ✓ / ✗ verified during the review
open ○ review finding not addressed
resolved / dismissed ✓ / – finding closed

ofx live --items expands each list, one line per point with its id and description. The finished-session table gets a CHECKS column (criteria · findings ratios) when the terminal is wide enough.

Two limits come from OpenFox's own data model, and ofx live states them instead of inventing numbers: sessions.total_tokens_used is not maintained (0 on every session) and cleanupOldEvents() purges the journal of a finished session. Token totals for sessions that already ended are therefore shown as -. What survives is the final context size (turn.snapshot.contextState), the workflow step, the duration and the outcome.

Actions — via the HTTP API, token required

Command Endpoint
stop <id> POST /api/sessions/:id/stop
pause <id> / resume <id> POST /api/sessions/:id/pause / resume
continue <id> POST /api/sessions/:id/continue
export <id> [-o FILE] GET /api/sessions/:id/export — writes nothing to the session
import <FILE> --project <id> POST /api/sessions/import — creates a new session
new --project <id> [--title T] POST /api/sessions
rm <id> [--yes] DELETE /api/sessions/:id — definitive

Export / import, and a hard limit

An export contains everything needed to recreate the session: the source metadata, the cached layout, the messages, and the raw event log. ofx export is read-only; ofx import creates a new session and leaves the original alone.

The import path has a ceiling. OpenFox caps HTTP request bodies at 75 MB:

app.use(express.json({ limit: "75mb" }));   // dist/chunk-2J6OW67H.js:8608

A session whose export exceeds that can be exported but not re-imported. ofx import measures the file before sending and refuses with an explanation, rather than letting the server fail with a bare HTTP 413.

On the instance this tool came from, one session reached 80,367 events / 64.5 MB and produced a 76.3 MB export — already past the limit, and still growing. An archive taken too late is a one-way file. Export while the session is still small, and keep an eye on ofx list.

Credentials

Command What it does
auth login masked password prompt, exchanges it for a token, verifies it, stores it 600
auth set-token paste an existing token
auth status which credential source would be used, and under what permissions
auth check verify the credentials against the API
auth clear delete the stored token

Resolution order: OPENFOX_TOKEN → token file → OPENFOX_PASSWORD.

Service lifecycle

Command What it does
service start|stop|restart|status drive the openfox systemd unit
service install [version] destructive — rm -rf /opt/openfox then npm install -g
service upgrade destructive — stop, reinstall, start
logs [N] [-f] journalctl -u openfox, falling back to the log file

service install and service upgrade execute a sudo rm -rf on the install directory. They require an interactive confirmation that cannot be bypassed, and install refuses to run while the service is active.

Compaction

ofx compact <id>                      # dry-run: exactly like `plan`
ofx compact <id> --apply              # backup, then stop the session via the API
ofx compact <id> --apply --restart    # …then restart the service

ofx does not implement compaction. Consolidation is OpenFox's own code (EventStore.consolidateSession) and runs automatically at server startup. Replaying its fold from the outside would risk corrupting a session. ofx verifies eligibility and orchestrates the restart instead.


Safety model

Three independent tiers, in increasing order of consequence:

1. Read-only. list, inspect, plan, status, backup, watch, live, diagnose open the database with readonly: true and PRAGMA query_only = true. No writes, no locks held, no kill.

2. Writes through the API. stop, pause, resume, continue, new, rm, export. OpenFox keeps snapshotCache and promptsCache in memory and only invalidates them on writes that pass through it. Writing to SQLite from outside would leave those caches stale — which is precisely the failure mode of the original incident. So ofx never does that.

3. Offline writes. Only compact --apply, and only after a mandatory backup that aborts the whole operation if it fails.

Additional guarantees:

  • compact runs its pre-checks before the backup, so it never writes a 700 MB backup for an operation that cannot proceed.
  • auth login verifies the token against the API before writing it to disk. A stored token that does not work fails later and elsewhere.
  • The token file is created via O_EXCL with mode 600 and then renamed, so it is never readable by anyone else, not even momentarily.
  • Masked prompts read in raw terminal mode and refuse to run when stdin is not a TTY. A secret cannot be piped in.

Why it is fast where OpenFox is slow

ofx reads with set-based queries instead of per-session correlated subqueries: on a 232-session database, list issues a handful of grouped queries against events, not 232 × N lookups. And it only ever reads the columns it needs.

The measurement that matters is not ofx's own speed, though — it is that the numbers it prints explain a 210 MB regression that was invisible from inside the UI.


What it deliberately does not do

  • ofx message. POST /api/sessions/:id/message calls queueMessage(), which only pushes into an in-memory Map. Nothing starts a turn from HTTP: /chat and /continue are validation stubs that return {accepted:true} without doing anything. Turns are started by a WebSocket client message. A message command over HTTP would queue into a structure nothing drains, and report success: true while doing nothing. Shipping that would be worse than not shipping it.
  • POST /api/sessions/:id/truncate. This cuts the conversation with deleteEventsAfterSeq. It is not a compaction, and it is destructive. Use the UI.
  • Provider, MCP and config endpoints. These are settings. They belong in the UI, and exposing them on the command line multiplies the ways to break a working setup.

Background

ofx was written while working a real incident — an OpenFox instance pinned a CPU core for 9h30 and became unstoppable from its own UI. That incident shaped the tool's non-negotiables (read-only by default, pre-checks before expensive writes, no compaction of its own, no write that bypasses OpenFox's in-memory caches), but it is not the scope: the same commands are what you use to keep an instance healthy, to size it, to archive or migrate a session, and to answer "why is this disk filling up". The numbers in Why the event log gets out of hand were measured on that instance; ofx inspect and ofx diagnose are what made them visible in the first place.

The full analysis — incident narrative, bug report, prevention plan, maintenance runbook and the scripts that regenerate those numbers — is not part of this repository. It lives in docs/, which .gitignore excludes on purpose: it describes one specific instance (host metrics, session sizes, project names, operator notes), and it stays on the machine that produced it.

Your own reports and diagnostics

This repository ships the tool only — no instance data, no logs, no reports. Anything you measure is yours, and the git-ignored docs/ directory is the natural place to keep it:

mkdir -p docs
ofx status               > docs/status-$(date +%F).txt
ofx list                 > docs/sessions-$(date +%F).txt
ofx inspect --hot --fast > docs/hot-session-$(date +%F).txt
ofx live --once          > docs/live-$(date +%F-%H%M).txt
ofx diagnose             > docs/health-$(date +%F).txt
ofx watch --samples 10 --interval 30 > docs/growth-$(date +%F).txt

Every command writes plain lines to stdout and gates its colors on isatty, so the output is safe to redirect to a file or to pipe into another tool.

ofx diagnose doubles as a monitoring probe: exit 0 = healthy, 1 = a warning threshold is crossed, 2 = critical — ofx diagnose >/dev/null; echo $?.

Because docs/ is ignored, a report can never be committed by accident. That matters: a report quotes session titles, sizes and paths.


License

MIT — see LICENSE.

About

No description, website, or topics provided.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages