Skip to content

Pibmo Recall is an append-only archive of AI coding sessions kept on machine-local storage. There is no server and no resident daemon: an hourly scheduler unit runs a stdlib Python script straight from the repo checkout, and that script copies what your coding tools already wrote into a dataset root it never shrinks. Everything below is a consequence of that single choice.

Read this page before any command. It fixes the vocabulary the rest of the docs use. When you are ready to run something, go to Getting started and the CLI reference.

Machines and roots

A machine is one computer contributing sessions. It is identified by its hardware UUID and registered by the installer in {root}/machines.json with a friendly name; the friendly name becomes that machine’s folder inside the root, so the archive reads as moraba/, not as a UUID.

A dataset root is the directory the archive lives in. It is the only machine-local configuration Recall has, and it travels on the scheduler unit’s command line (--root, or the PIBMO_ROOT environment variable) — no ~/.pibmo, no dotfile, nothing written into ~/.claude. This is deliberate and it fails closed: with no root at all, sync refuses to run rather than guess a location, because a guessed root would scatter archives nobody knows exist.

$ python3 scripts/sync/sync-transcripts.py
Error: no dataset root. Pass --root /path/to/dataset (or set PIBMO_ROOT). The installer prints the exact cron line.

There are two kinds of root. The local root is where this machine archives, and it must be genuinely local disk (e.g. ~/Library/Application Support/pibmo/recall) — the installer refuses a cloud-synced path for it, because a sync client’s fences and evictions inside the write path are a silent-failure class, not a convenience. The consolidated root is the shared directory that joins machines, passed as --consolidate, and it may sit on a cloud-synced folder — that placement is replication, chosen with eyes open. A single-computer install needs only the local root; consolidation never comes up. See Configuration.

Raw and derived

Recall stores exactly two kinds of data. The split between them is what makes the archive trustworthy.

Raw archiveDerived output
Path{root}/{machine}/...{root}/processed/{key}/{profile}/
WrittenBy sync, on the hourBy the converter, when you run it
Ever edited?Extended on genuine appends, never rewrittenFreely regenerated
Ever deleted?NeverWhenever you like
HoldsEach tool’s files exactly as it laid them outMerged per-project Markdown

The raw archive is a faithful per-machine mirror of each source tool’s native layout: no renames, no remapping, no reorganisation, ever. Derived output is whatever the deterministic processing layer projects out of raw.

Why the split matters: derived can always be rebuilt from raw, but raw can never be rebuilt from derived. So the rules are asymmetric on purpose — raw is accumulate-only and immutable, while everything under processed/ is disposable. If a derived artifact is wrong, delete it and re-run the converter; raw was never at risk. That is the whole trick.

What the root looks like

<root>/
machines.json machine registry (uuid -> friendly name)
projects.json process-time grouping map
profiles.json denylist processing profiles
moraba/ one machine's faithful mirror
machine.json this machine's identity record
pibmo.db provenance: runs, observations, schema
claude/projects/<encoded>/
<uuid>.jsonl one session
<uuid>.fork-<sha8>.jsonl a divergent rewrite, kept as a sibling
<uuid>/subagents/ subagent transcripts
gemini/ Gemini CLI and Antigravity
codex/ Codex CLI
processed/
<key>/<profile>/ derived Markdown -- rebuildable

The root is an ordinary directory of plain files plus one SQLite database per machine. Nothing in it depends on Recall being installed: a reader with a text editor can walk it, and an analysis session can be pointed at it read-only.

The three sources

Sync mirrors the native data directories of three tool families. Exclusions are decided at sync time and remove whole files or directories by name — sync never edits the inside of a file it keeps.

SourceNative homeLands underExcluded at sync
Claude Code~/.claude/projects/{machine}/claude/projects/auto-memory directories — mutable and derived, not raw
Gemini CLI + Antigravity~/.gemini/{machine}/gemini/oauth_creds.json, google_accounts.json, extensions/, Antigravity’s browser_recordings/ and bin/ — credentials, app code, media
Codex CLI~/.codex/{machine}/codex/auth.json, .tmp/, tmp/, cache/ — credentials and ephemera

Credential files never enter the archive. Neither do caches, app binaries, or recordings — the archive is transcripts and session data, not a disk image of your home directory.

Appends, forks, and regressions

Each .jsonl file under claude/projects/ is one session, named by the UUID the tool gave it. On every tick, sync compares each source file to its archived copy line by line and takes exactly one of three actions:

  • Extend. The archived file is a strict line-prefix of the source — a genuine append — so the archive grows by the new lines.
  • Fork. The source diverges from the archive — the tool rewrote history, compaction being the canonical case. The rewrite is preserved as a sibling {uuid}.fork-{sha8}.jsonl, where sha8 hashes the first divergent line. The original is never written over, the fork name is stable so a growing fork extends rather than multiplying, and a fork that itself diverges chains another .fork- segment.
  • Skip. The source is shorter than the archive — a regression — and the archive never shrinks.

Which state of a forked lineage is canonical is decided at process time, not by the sync. Nothing in the raw archive is ever deleted.

Projects and profiles

The raw archive knows nothing about your projects — it stores Claude Code’s encoded paths exactly as found, and the same logical project looks different on every machine and every checkout. Grouping is a process-time concern: {root}/projects.json maps encoded paths to logical project keys, and sync auto-appends newly seen paths to it with process: false so nothing is converted without a human turning it on. Editing a row’s key or consolidate_into changes only how the converter groups output — the raw archive is never rerouted or renamed.

A profile is a denylist the converter applies on the way to Markdown: default-include, declared exclusions. A profile names what it drops; anything it does not name is kept, so a new event type appears in output until a rule excludes it — it never silently vanishes. The shipped profiles are faithful and dialogue, and output lands at {root}/processed/{key}/{profile}/. The converter is a format converter, not an editor: it never summarises, rewrites, or copy-edits a kept block, and the same input produces byte-identical output.

Provenance and the doctor

Every sync run writes its own paper trail into {root}/{machine}/pibmo.db: the run itself (a heartbeat the health checks read), a per-file observation for every action taken, and the JSONL event-type schema collected incrementally from the lines actually read — so schema drift in a source tool is recorded, not discovered by a broken converter months later.

The doctor answers one question — is this machine actually archiving? — and every failure it reports prints its exact fix. It checks machine registration, writes a canary through the real pipeline, verifies the scheduler unit and the paths embedded in it, and measures heartbeat age and replication lag. It exists because the failure it hunts is real: a swallowed one-line PATH error once kept a machine from archiving for five weeks while its cron line looked correct. The preservation path now fails loudly, and the doctor watches the watchers.

What happens on the hour

Recall does not schedule itself. The installer writes a native scheduler unit — a LaunchAgent on macOS, a systemd user timer on Linux — and prints the activation commands; it never edits your crontab, and it writes nothing outside the dataset root and the unit file. The unit invokes the sync script inside the repo checkout, so updating is git pull: the next tick runs the new code. There is no deployed copy and no self-updater.

Running sync by hand is safe at any time — concurrent runs exit immediately via a lock. On a multi-machine setup, a --consolidate flag on the same unit line adds a second, separate step after the local write: an accumulate-only copy of the machine’s subtree into the shared consolidated root. Local preservation never waits on transport, and transport never touches the local root’s contents.

What this does not give you

State it plainly, because each of these is a decision someone should make with open eyes:

  • Faithful is not redacted. The exclusion lists strip known credential files by name; they cannot strip a secret pasted into a conversation. A transcript is archived exactly as the session produced it, so anything a session contained is in raw — and while a denylist profile can drop whole events or blocks from derived output by rule, raw keeps everything. Treat access to the archive like access to your source code and shell history.
  • One copy is one copy. Recall writes to local disk and stops there. A local root on one drive is exactly one copy until you consolidate to a shared root or replicate it — and replication is deliberately delegated to tools built for it rather than reimplemented inside the preservation path.
  • No LLM anywhere in the pipeline. Sync, consolidation, and processing are deterministic and make no API calls. Recall does not summarise your sessions; it preserves them, and projects them into Markdown by rule. Analysis with a model happens in sessions you point at the archive, read-only, on top of the faithful record.

Next