Skip to content

Troubleshooting

Each entry below gives the exact message Recall prints, why it happens, and how to clear it. Messages are quoted verbatim from the tools; only machine-specific paths have been shortened for readability — /path/to/root for the local dataset root, ~/code/pibmo-recall for the repo checkout, moraba for this machine’s friendly name, matching the defaults in Getting Started.

Most of these failures are exactly what the doctor exists to catch, so run it first — every failing check prints its own remediation, and the entries below expand on the signatures:

Terminal window
python3 ~/code/pibmo-recall/scripts/sync/doctor.py --root /path/to/root

Error: no dataset root

Symptom

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

on any manual run without --root. The processing layer fails the same way with its own wording: Error: no dataset root. Pass --root (or set PIBMO_ROOT).

Cause

The dataset root is the only machine-local configuration Recall has, and it deliberately lives nowhere except the scheduler unit / cron line and the PIBMO_ROOT environment variable — no ~/.pibmo, no dotfile to fall back to. A manual run therefore has to be told where the archive is; guessing a path would risk creating a second archive somewhere wrong.

Fix

Read the root off the installed unit, then pass it:

Terminal window
# macOS -- the LaunchAgent carries the root:
grep -A 1 -e '--root' ~/Library/LaunchAgents/com.pibmo.recall.sync.plist
# Linux -- the systemd service does:
grep ExecStart ~/.config/systemd/user/pibmo-recall-sync.service
# crontab fallback:
crontab -l | grep sync-transcripts
Terminal window
python3 ~/code/pibmo-recall/scripts/sync/sync-transcripts.py --root /path/to/root -v
# or, for the rest of the shell session:
export PIBMO_ROOT=/path/to/root

machines.json not found

Symptom

Terminal window
$ python3 ~/code/pibmo-recall/scripts/sync/sync-transcripts.py --root /path/to/root -v
Error: /path/to/root/machines.json not found.
Run install.sh first to register this machine and seed the shared config.

Cause

{root}/machines.json is the installer-owned machine registry every dataset root carries. The sync refuses to run against a bare directory rather than silently seeding a new root at whatever path was typed — a mistyped --root would otherwise scatter archives across the disk. Either the path is wrong (or the volume unmounted), or install.sh has never run on this machine.

Fix

If the root exists elsewhere, fix the path (find the real one on the unit line as in the previous entry). If this is a genuinely new machine, run the installer:

Terminal window
cd ~/code/pibmo-recall && bash install.sh

It proposes the platform-default local root, registers the machine, seeds machines.json / projects.json / profiles.json, and writes the scheduler unit. It is idempotent — re-running is safe. On a single computer it is two prompts and Enter.

is not registered in machines.json

Symptom

Terminal window
$ python3 ~/code/pibmo-recall/scripts/sync/sync-transcripts.py --root /path/to/root -v
Error: machine 6b9d21c4... is not registered in /path/to/root/machines.json.
Run install.sh on this machine first.

Cause

Registration maps this machine’s hardware UUID to a friendly name — the folder its subtree lives under. Under the install-first model the sync never auto-creates a machine folder and never invents a name: an unregistered machine writing into a shared root would grow anonymous UUID folders nobody can attribute. Registration is install.sh’s job, so the sync fails fast instead.

Fix

Run the installer on this machine and point it at the same root; registering is exactly what it does, and an existing root’s shared config is kept, not overwritten. The doctor reports the same condition as:

Terminal window
FAIL registered 6b9d21c4... is not in machines.json
fix: run install.sh on this machine to register it

Installer refuses a cloud-synced local root

Symptom

Terminal window
$ bash install.sh
...
Local dataset root (this machine's archive) [/Users/you/Library/Application Support/pibmo/recall]: ~/Dropbox/Datasets/pibmo
REFUSED as the local dataset root: ~/Dropbox/Datasets/pibmo
- path component 'Dropbox' looks cloud-synced (dropbox)
Risks of a cloud-synced local root (the 2026-06/07 fleet outage class):
- schedulers can be permission-fenced off the path (macOS TCC denies
launchd and cron access to ~/Library/CloudStorage) -> total, silent outage
- the cloud client can evict files to dataless placeholder stubs the
sync cannot read
- client rewrites and conflict copies violate append-only preservation
- when the client is not running, nothing is preserved at all
The local root must be a plain local path -- a hard rule of the
preservation design. A cloud-synced folder is welcome as the CONSOLIDATED
root instead -- that is replication's job, not preservation's.
Detection is heuristic: re-run with --force to override eyes-open.
Aborting. Re-run and press Enter to accept the proposed default, or
override the refusal with: bash install.sh --force

Cause

Preservation is local-first: the machine-local root must sit on a path no cloud client, permission fence, or network can deny. The risks in the message are outage classes observed in production on the author’s own fleet, not hypotheticals. Symlinks are resolved before classification, so a local-looking alias into a cloud folder is still caught. Detection is heuristic by design — custom cloud-folder locations are not all detectable — which is why the refusal pairs with an escape hatch instead of pretending to be airtight.

Fix

Press Enter to accept the proposed platform default (macOS: ~/Library/Application Support/pibmo/recall; Linux/WSL: ~/.local/share/pibmo/recall), and give the cloud folder as the consolidated root when the installer asks about multi-machine destinations. That is where a cloud-synced folder belongs: written by the transport step, replicated by the vendor’s own client. If you really do want a cloud-synced local root, bash install.sh --force proceeds eyes-open. See Concepts for the local / consolidated split.

sync skipped (already running)

Symptom

Terminal window
$ python3 ~/code/pibmo-recall/scripts/sync/sync-transcripts.py --root /path/to/root
[2026-07-22 10:00] sync skipped (already running)

Cause

Every run takes a non-blocking exclusive lock keyed by the dataset root (a file under /tmp; PIBMO_LOCK_DIR overrides), so two syncs can never interleave writes into the same archive. The skip exits 0 immediately: an hourly tick overlapping a long manual run — or the reverse — is the normal case, and the run already in flight is doing the same job.

Fix

Nothing, usually — the next tick picks up whatever this one skipped. If every run skips, something genuinely holds the lock: check ps aux | grep sync-transcripts. Environment failures around the lock are loud rather than masquerading as “already running” forever:

Terminal window
$ python3 ~/code/pibmo-recall/scripts/sync/sync-transcripts.py --root /path/to/root
Error: cannot open lock file /tmp/pibmo-sync-9f3a71c2.lock: [Errno 13] Permission denied: '/tmp/pibmo-sync-9f3a71c2.lock'

means the lock file is owned by another user (two accounts syncing the same root) or the lock directory is unwritable; point PIBMO_LOCK_DIR at a directory this user can write.

no sync has ever recorded a run

Symptom

Terminal window
$ python3 ~/code/pibmo-recall/scripts/sync/doctor.py --root /path/to/root
Pibmo Recall doctor -- machine 'moraba', root /path/to/root
OK root /path/to/root/machines.json readable (2 machine(s) registered)
OK identity 6b9d21c4...
OK registered 'moraba'
OK writable canary ok under moraba/logs/
OK unit 1 unit file(s) current
OK unit-cmd embedded interpreter and script exist
FAIL heartbeat no sync has ever recorded a run in /path/to/root/moraba/pibmo.db
fix: run one by hand to prove the pipe:
python3 /path/to/pibmo-recall/scripts/sync/sync-transcripts.py --root "/path/to/root" -v
then activate (idempotent; bootout first when re-writing an active unit):
WARN log no /path/to/root/moraba/logs/cron.log yet
fix: expected after the first scheduled run; manual -v runs print to the terminal instead
Summary: 6 ok, 1 warn, 1 fail

Cause

Every sync run — scheduled or manual — records a heartbeat row in {root}/{machine}/pibmo.db. No row at all means no sync has ever started here: the normal state right after install (before the first manual run or the first scheduled tick), and also the state a written-but-never-activated unit leaves behind indefinitely.

Fix

Exactly the printed one: run a sync by hand to prove the pipe end to end, then activate the unit so the hourly ticks take over.

Terminal window
python3 ~/code/pibmo-recall/scripts/sync/sync-transcripts.py --root /path/to/root -v
# macOS:
launchctl bootout gui/$UID com.pibmo.recall.sync 2>/dev/null || true
launchctl bootstrap gui/$UID "$HOME/Library/LaunchAgents/com.pibmo.recall.sync.plist"
# Linux:
systemctl --user daemon-reload
systemctl --user enable --now pibmo-recall-sync.timer
loginctl enable-linger $USER # headless: keep the user manager alive across logouts

the scheduler IS firing but runs die before recording

Symptom

Terminal window
$ python3 ~/code/pibmo-recall/scripts/sync/doctor.py --root /path/to/root
...
FAIL heartbeat last completed sync 2026-07-19T09:00:14+00:00 (49.2h ago)
fix: the log is FRESH (0.2h) while the heartbeat is not -- the scheduler IS firing but runs die before recording.
Read the error: tail "/path/to/root/moraba/logs/cron.log"
...

Cause

A heartbeat is healthy up to 26 hours old (--max-age-hours adjusts) — an hourly schedule plus a laptop that slept through a day. Past that, the doctor separates two failure shapes by comparing the heartbeat against the log file’s age. A fresh log with a stale heartbeat means launchd / systemd is delivering ticks and every run is dying before its first database write. Because diagnostics are never swallowed (an invariant of the codebase), the actual error is sitting at the end of the log. This signature is how a real outage on the author’s fleet was finally diagnosed — a scheduler that fired every hour for weeks while every run died on a PATH problem — and it is the class the doctor was built to catch.

Fix

Read the log; the last lines name the failing stage:

Terminal window
tail "/path/to/root/moraba/logs/cron.log"

When the log is stale too, the same check prints a different fix line — scheduler is not delivering runs (max healthy age 26h). followed by the activation commands: the unit was never activated, was booted out, or (Linux) the user manager died at logout. Re-activate as in the previous entry.

unit-cmd references a missing script (the repo moved)

Symptom

Terminal window
$ python3 ~/code/pibmo-recall/scripts/sync/doctor.py --root /path/to/root
...
FAIL unit-cmd com.pibmo.recall.sync.plist references missing /path/to/old-checkout/scripts/sync/sync-transcripts.py
fix: the repo or interpreter moved -- re-run install.sh from the current location, then re-activate:
Activate (idempotent; bootout first when re-writing an active unit):
launchctl bootout gui/$UID com.pibmo.recall.sync 2>/dev/null || true
launchctl bootstrap gui/$UID "/Users/you/Library/LaunchAgents/com.pibmo.recall.sync.plist"
If a sync destination crosses a vendor cloud mount (e.g. a
consolidated root under ~/Library/CloudStorage), the first launchd
run may raise ONE macOS consent prompt for python3 -- grant it once.
...

Cause

The unit embeds absolute paths to the interpreter and to sync-transcripts.py in this checkout. There is no deployed copy: the unit points at the repo, which is why updating is just git pull — the next tick runs the new code. The flip side is that moving, renaming, or re-cloning the checkout leaves a byte-perfect unit firing into a dangling path forever, with no process left alive to write an error anywhere. The doctor checks that both embedded paths still exist; this exact class once took one of the author’s machines dark for days before the check existed.

Fix

Re-run install.sh from where the repo now lives, then re-activate with the printed commands. Two sibling failures of the same check are handled the same way: FAIL unit not installed: ... and FAIL unit differs from a fresh render: ... (the unit’s inputs changed since install). The standalone check gives the same verdict for scripts:

Terminal window
$ python3 ~/code/pibmo-recall/scripts/sync/scheduler.py --check --root /path/to/root --machine moraba
STALE: /Users/you/Library/LaunchAgents/com.pibmo.recall.sync.plist: differs from a fresh render

and prints OK: 1 unit file(s) current when clean.

cannot write under the machine subtree (a permission fence)

Symptom

Terminal window
$ python3 ~/code/pibmo-recall/scripts/sync/doctor.py --root /path/to/root
...
FAIL writable cannot write under /path/to/root/moraba/logs: [Errno 1] Operation not permitted: '/path/to/root/moraba/logs/.doctor-canary'
fix: a permission fence is blocking the sync (the moraba TCC class) -- keep the local root on an ungated local path, per the local-first design; if this is a vendor-mount consolidated root, grant the scheduler's interpreter disk access once
...

Cause

The doctor proves writability with a single canary file written and deleted under {machine}/logs/ — it never touches the raw archive. Operation not permitted (as opposed to Permission denied) is the macOS signature of a TCC privacy fence: launchd and cron are denied paths like ~/Library/CloudStorage wholesale, and the denial is invisible until something tries to write. A local root behind such a fence means the scheduled sync archives nothing while looking perfectly configured — the outage class the local-first architecture exists to prevent.

Fix

Keep the local root on the ungated platform default; the installer refuses cloud-synced paths for exactly this reason (see the refusal entry above). If the fenced path is a vendor-mount consolidated root — permitted, eyes-open — the first launchd run may raise one macOS consent prompt for python3: grant it once, and transport resumes on the next tick.

The consolidated root is unreachable or lagging

Symptom

Terminal window
$ python3 ~/code/pibmo-recall/scripts/sync/doctor.py --root /path/to/root --consolidate /path/to/consolidated
...
FAIL consolidated /path/to/consolidated unreachable
fix: mount/attach the consolidated root, or fix the --consolidate path on the unit line
...

or, when reachable, staleness measured rather than assumed:

Terminal window
WARN consolidated this machine's replicated heartbeat 2026-07-19T09:00:14+00:00 (53.1h behind local)
fix: transport is lagging -- check the tool that moves this subtree (vendor client / rclone / pull job)
WARN fleet seneca: heartbeat 322.6h old
fix: that machine (or its transport) has gone dark -- run the doctor there

Cause

Replication is delegated: Recall pushes this machine’s subtree into the consolidated root as a transport step and leaves cloud replication to the tool that owns the folder — the vendor’s client, rclone, a pull job. The doctor therefore measures outcomes, not mechanisms: it compares each machine’s replicated heartbeat at the destination against the local one, and complains past 48 hours behind. unreachable means the path does not exist right now: an unmounted volume, a stopped cloud client serving online-only placeholders, or a typo on the unit line.

Fix

Per the messages: mount the volume or start the client; fix the --consolidate path by re-running install.sh (it rewrites the unit); for lag, check the transport tool that moves this subtree. The fleet rows list every other machine’s subtree at the destination by staleness — they tell you which machine to go run the doctor on, since only that machine can see its own scheduler. See Architecture for the replication tiers.

The session banner warns Archive: ⚠ STALE

Symptom

Terminal window
=== Session context ===
Time: 2026-07-22T09:14:03+10:00
Machine: moraba (6b9d21c4…) [macOS] host=moraba.local
...
Archive: ⚠ STALE — last sync 49.2h ago (max 26)
fix: python3 /path/to/pibmo-recall/scripts/sync/doctor.py --root "/path/to/root"
Session: 38c37a77-... (startup)
=======================

Cause

The optional SessionStart banner watches the watchers with no configuration of its own: it discovers the dataset root from the installed unit / cron line, reads the last heartbeat from {root}/{machine}/pibmo.db, and warns when it is older than 26 hours. Routine stays one quiet line — Archive: OK — last sync 0.4h ago (via com.pibmo.recall.sync.plist) — trouble gets a second, indented fix line. Machines with no schedule print no Archive: line at all. Sibling warnings from the same check: ⚠ this machine is not registered in /path/to/root/machines.json (fix: run install.sh on this machine) and ⚠ no sync has ever recorded a run. A liveness check must never break a session start, so errors inside the check are surfaced in the line itself rather than thrown.

Fix

Run the printed doctor command. Every doctor failure prints its exact remediation, and the specific signatures are the entries above — most often the stale-heartbeat pair. The warning clears on the first sync that records a fresh heartbeat.

ModuleNotFoundError: No module named 'yaml' on processing

Symptom

Terminal window
$ python3 ~/code/pibmo-recall/scripts/process/process_root.py --root /path/to/root
Traceback (most recent call last):
File "/path/to/pibmo-recall/scripts/process/process_root.py", line 53, in <module>
from process.profile_convert import ProjectRow, run_profiles # noqa: E402
...
File "/path/to/pibmo-recall/scripts/process/frontmatter.py", line 14, in <module>
import yaml
ModuleNotFoundError: No module named 'yaml'

Cause

The sync, hook, and doctor layer is deliberately pure Python stdlib — preservation must not depend on pip. The processing layer is the one exception: its emitters write YAML frontmatter and index files, so process_root.py needs PyYAML, and the interpreter that ran it does not have it.

Fix

Run the processor with an interpreter that has PyYAML — on macOS the repo’s documented choice is the system interpreter, which ships it — or install the dependency into yours:

Terminal window
/usr/bin/python3 ~/code/pibmo-recall/scripts/process/process_root.py --root /path/to/root
# or:
python3 -m pip install pyyaml

Only the processing layer needs this; the sync and the doctor stay stdlib-only by design.

Running the processor against a root that has never synced fails differently: Error: no machine/claude/projects/encoded folders found under /path/to/root (or --key matched nothing). — the archive is empty (run a sync first), or every row was filtered out by --key.