Data flow
June 4, 2026 · View on GitHub
How data moves through prevAIl, from keystroke to disk to recall.
1. A turn end-to-end
A single chat turn in a domain workspace:
user types in chat-pane.tsx
│
│ send (Enter)
▼
app.tsx handleSend
│
│ capture framework + lens at send time
│ (NOT at receive time — chips can change mid-turn)
▼
cli-bridge.ts runChatTurn
│ buildFrameworkPreamble(framework) + prompt
│ + (claude only) --append-system-prompt = AGENTS-operating.md
│ + (web=deny) WEB_DENY_NOTE appended to manual
▼
spawn(<cli.bin>, argv, scrubbedEnv, own process group)
│
│ onChunk(delta) ─────► chat-pane streams partial text
▼
full reply string returned
│
├──► persistMessage() → ~/.prevail/sessions DB (SQLite)
├──► writeTurnSummary() → <domain>/_log/YYYY-MM-DD.md
│ + .shasum sibling
│ + memory.indexEntry() (best-effort embed)
└──► distillTurnToJournal() → <domain>/_journal/decisions.md
<domain>/_journal/facts.md
The framework + lens labels chosen by the user at send time travel
through the whole pipeline so the _log/ meta line records exactly which
toggles were on when the question fired. This is captured in
src/app.tsx around line 1545 (single chat) and 1849 (council).
2. A council turn
/council <prompt> (or council-mode-toggle ON + Enter):
user prompt
│
▼
expand panelists (CLIs × pinned models)
│
├── lens=null → one call per panelist
├── lens=<id> → one call per panelist, same lens on each
└── lens=all → panelists × LENSES ← cap by councilMaxCallsPerTurn
│
▼
fan out (parallel) — runChatTurn(bare=true) per panelist call
│
▼
sanitize each reply (## → "(panelist) ## ")
│
▼
chair synthesis call — chair model reads all panelist outputs
│
▼
verdict bubble + parseVerdict() → divergence flag
│
├──► persistMessage → sessions DB
├──► writeTurnSummary → _log/ (kind="council-verdict")
└──► distillTurnToJournal → _journal/
lens=all is the only mode that multiplies the panelist count, which is
why every council turn first prints an estimated cost line and refuses to
exceed councilMaxCallsPerTurn (default 16) without explicit consent.
3. The vault folder layout
<vault>/
AGENTS-operating.md ← user-edited (operating manual for claude)
<domain>/
state.md ← user-edited
QUICKSTART.md ← user-edited
PROMPTS.md ← user-edited
open-loops.md ← user-edited
skills/<skill-id>/SKILL.md ← user-edited (n on Skills tab scaffolds new ones)
_log/<YYYY-MM-DD>.md ← AUTO-WRITTEN by writeTurnSummary() on every turn
_log/<YYYY-MM-DD>.md.shasum ← AUTO-WRITTEN sibling — one (entry-id, sha256) per line
_journal/decisions.md ← AUTO-WRITTEN by distillTurnToJournal()
_journal/facts.md ← AUTO-WRITTEN by distillTurnToJournal()
Who reads what:
- The model reads
state.md,QUICKSTART.md,PROMPTS.md,open-loops.md,skills/*/SKILL.md, and recall hits surfaced bysrc/memory.ts. _log/is for the user (andprevail vault verify)._journal/is for the user and any future recall index..shasumis forprevail vault verify.
4. Config + secrets
Everything under ~/.prevail/ is machine-local and must not be synced
to iCloud / Dropbox / git:
~/.prevail/
config.json ← chmod 0600 (vault path, chair pin, model pins,
per-domain framework/lens overrides,
web-access, councilMaxCallsPerTurn, ...)
telegram.json ← chmod 0600 (bot token, chat-ID allowlist)
mcp.json ← chmod 0600 (auto-generated bearer token)
connectors/<id>/auth/ ← chmod 0600 (per-connector OAuth refresh tokens)
daemon.pid ← (PID of running daemon, for zombie detection)
sessions/<db> ← chat session SQLite DB (persistMessage target)
The cockpit refuses to write any secret with looser permissions than 0600.
5. The benchmark loop
bench/ lives under the vault so question sets are version-controlled
with the user's context, not buried in ~/.prevail/:
<vault>/benchmark/
questions/<question-id>.md ← user-authored test cases
│
│ prevail bench run [--cli=...] [--model=...]
▼
runs/<YYYY-MM-DD>_<cli>_<model>/
results.json ← raw replies + timings + token counts
│
│ prevail bench score (rubric LLM judges results.json)
▼
score.json ← per-question scores + summary
│
│ prevail bench leaderboard (aggregates score.json across runs)
▼
stdout: ranked board
The benchmark loop never touches _log/ or _journal/ — bench output is
isolated under runs/ so a noisy benchmark session doesn't pollute the
domain history.
6. State that survives a restart vs state that doesn't
Survives:
- Everything under
<vault>/(markdown is the source of truth). ~/.prevail/config.json,telegram.json,mcp.json,connectors/.~/.prevail/sessions/<db>— chat sessions can be re-opened..shasumsiblings — log integrity carries across restarts.
Does not survive:
- In-process chat session memory in the cockpit (state held in React
state via
src/session.ts's in-memory cache layer). - Pending council fanouts (an in-flight
/councilaborted by quit is gone; nothing was written yet). - Per-session "gut" takes captured by
/gut— held inpendingGutRef.currentand consumed by the next council verdict. Quit before the verdict and the gut is lost. - Streaming partial-reply buffers (onChunk deltas) — only the final reply is persisted.
If a turn completed (reply received), it persisted. If it was cancelled or the process died mid-turn, nothing landed on disk for that turn.