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:
python3 ~/code/pibmo-recall/scripts/sync/doctor.py --root /path/to/rootError: no dataset root
Symptom
$ python3 ~/code/pibmo-recall/scripts/sync/sync-transcripts.py -vError: 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:
# 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-transcriptspython3 ~/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/rootmachines.json not found
Symptom
$ python3 ~/code/pibmo-recall/scripts/sync/sync-transcripts.py --root /path/to/root -vError: /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:
cd ~/code/pibmo-recall && bash install.shIt 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
$ python3 ~/code/pibmo-recall/scripts/sync/sync-transcripts.py --root /path/to/root -vError: 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:
FAIL registered 6b9d21c4... is not in machines.json fix: run install.sh on this machine to register itInstaller refuses a cloud-synced local root
Symptom
$ bash install.sh...Local dataset root (this machine's archive) [/Users/you/Library/Application Support/pibmo/recall]: ~/Dropbox/Datasets/pibmoREFUSED 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 allThe local root must be a plain local path -- a hard rule of thepreservation design. A cloud-synced folder is welcome as the CONSOLIDATEDroot 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, oroverride the refusal with: bash install.sh --forceCause
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
$ 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:
$ python3 ~/code/pibmo-recall/scripts/sync/sync-transcripts.py --root /path/to/rootError: 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
$ python3 ~/code/pibmo-recall/scripts/sync/doctor.py --root /path/to/rootPibmo 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 insteadSummary: 6 ok, 1 warn, 1 failCause
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.
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 || truelaunchctl bootstrap gui/$UID "$HOME/Library/LaunchAgents/com.pibmo.recall.sync.plist"
# Linux:systemctl --user daemon-reloadsystemctl --user enable --now pibmo-recall-sync.timerloginctl enable-linger $USER # headless: keep the user manager alive across logoutsthe scheduler IS firing but runs die before recording
Symptom
$ 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:
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
$ 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:
$ python3 ~/code/pibmo-recall/scripts/sync/scheduler.py --check --root /path/to/root --machine morabaSTALE: /Users/you/Library/LaunchAgents/com.pibmo.recall.sync.plist: differs from a fresh renderand prints OK: 1 unit file(s) current when clean.
cannot write under the machine subtree (a permission fence)
Symptom
$ 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
$ 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:
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 thereCause
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
=== Session context ===Time: 2026-07-22T09:14:03+10:00Machine: 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
$ python3 ~/code/pibmo-recall/scripts/process/process_root.py --root /path/to/rootTraceback (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 yamlModuleNotFoundError: 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:
/usr/bin/python3 ~/code/pibmo-recall/scripts/process/process_root.py --root /path/to/root# or:python3 -m pip install pyyamlOnly 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.
Related
- Overview and Getting Started
- Configuration —
machines.json,projects.json, and the unit line - CLI Reference — every script and flag
- Architecture and Concepts
- FAQ