Memory & Capability-Fallback Audit — 2026-04-18
Pod 36. Two related questions from Robert:
- Does snappy-os handle memory?
- What happens when a capability the agent expects is missing (browser, API, gateway, env)?
Part A — Memory
Two distinct memory systems on this machine
1. Per-project auto-memory (owned by Claude Code harness, NOT snappy-os):
Location: ~/.claude/projects/<project-slug>/memory/*.md
Current state on this machine (Robert's dev box):
| Project dir | Memory files |
|---|---|
-Users-robertboulos-projects-rayFridayUpdate | 34 (MEMORY.md index + 33 feedback/project files) |
-Users-robertboulos-projects-snappyClassroomContent | 41 |
-Users-robertboulos--claude | 15 |
-Users-robertboulos | 5 |
-Users-robertboulos-projects-snappy-os | 0 (empty) |
| other 6 project dirs | no memory/ subdir |
The rayFridayUpdate/memory/MEMORY.md index is 31 lines, each a bullet pointing at a feedback/project file. Auto-memory is strictly per-project per-machine — nothing in snappy-os reads or writes these files at runtime. The skool-partner-cta.md skill is the only file that even mentions the path (state/skills/skool-partner-cta.md:124).
2. Snappy-os's own memory — the state/log/*.ndjson append-only logs:
Top 5 log files by size:
6.6M symlink-runs.ndjson
896K lint-sync-integrity-gateway.ndjson
428K lint-symlink-check.ndjson
268K frictions.ndjson
260K migrations.ndjson
176K evals.ndjson
144K sync-events.ndjson
These ARE the memory: every skill run logs to chain.ndjson, every eval to evals.ndjson, every defect to frictions.ndjson. The PID loop (state/lint/pid-trends.ts) reads them to decide what regenerates next.
3. Loader rewrites as slow memory. When a loader falls short, ~/.claude/logs/snappy-os-loader-feedback.log gets a line, and the PID loop promotes the rule into a skill's Critical Rules (program.md "per-turn loader sidecars" section). Memory that modifies the system itself.
The memory-consolidation skill (state/skills/memory-consolidation.md)
- Prose skill, graduation: prose, eval: auto.
- Cron daily 6 AM. Reads all
.mdfiles in a memory dir, dedupes, prunes stale references, merges, writes back. Appends one eval row. - Ambiguous: the skill page says "project memory directory" without specifying which one. The loader (
memory-consolidation.agents.md) confirms stale = "references things that no longer exist."
Cross-machine memory
state/log/*.ndjsonIS synced cross-machine viabin/cli.js push→ DO Spaces → manifeststate/log/sync-manifest.json. Verified:grep -c "memory" sync-manifest.json= 5 matches (thememory-consolidationskill files + codex shims). The auto-memory~/.claude/projects/*/memory/*.mdtree is NOT in snappy-os sync scope — it is Claude-Code-harness-local.- Cross-tenant PID promotion reads
state/log/evals.aggregate.ndjsonand promotes rules when >=3 tenants score a rev >=0.85 across >=5 runs (program.md "Sync" section). Memory flows machine → DO → other machines via aggregates, not raw markdown.
NOT persisted
- The running conversation / tool-use trace. No memory of prior Task subagent outputs except what logs captured.
- Auto-memory in
~/.claude/projects/is per-machine. If Robert's laptop dies, the 34 rayFridayUpdate memory files are gone unless he has~/.claude/in Time Machine. - No hooks write into auto-memory automatically. It's updated only when Claude decides a line belongs there (memory-tool driven).
Part B — Capability fallback
Dependency inventory
| Dependency | Detection? | Degrade strategy | Files |
|---|---|---|---|
.env.cache | yes (state/lib/env.ts:28) | console.error + return {}; env(key) then throws with a clear "Create .env.cache at project root or symlink" message | state/lib/env.ts:49-56 |
| Gateway (skills.snappy.ai) | yes (curl --max-time 3, content length check) | Falls back to local repo ~/projects/snappy-os/state/skills/ — hook still works offline | ~/.claude/hooks/snappy-os-inject.sh:57-74 |
| HTTP 5xx / retries | yes (built-in retry loop) | Retry N times with delay, then throw | state/lib/http.ts:21-36 |
Browser (agent-browser CLI → Playwright) | partial — see below | partial | state/lib/browse.ts, linkedin-fetcher.ts, skool-fetcher.ts |
| Anthropic/OpenRouter API keys | yes (via env("ANTHROPIC_API_KEY")) | env() throws missing-credential error; no rate-limit handling in dispatch.ts beyond provider default | state/lib/dispatch.ts:18-160 |
Browser-dependent skills: 30+ files reference browser/playwright/agent-browser
Production entry point for browser is state/lib/browse.ts (320 lines) wrapping the agent-browser homebrew CLI (which bundles Playwright: ~/Library/Caches/ms-playwright/chromium-* present on this box).
Preflight pattern — inconsistent:
state/lib/skool-fetcher.ts:56— HAS preflight:if (!existsSync(AUTH_STATE)) { console.warn(...); return []; }— graceful.state/lib/linkedin-fetcher.ts:33comment says "Returns empty array + console.warn on auth failure" — graceful for auth, but does NOT check whetheragent-browserbinary is installed before callingnavigate(). A machine without Playwright crashes atexecSyncinbrowse.ts:26.state/lib/browse.ts— NO check foragent-browserin PATH.runBrowser()callsexecSync("agent-browser …"); if missing, throwsError: command not foundwith no degrade path.state/lib/sweep.ts:842— lazy-imports the browser fetchers, wraps each inrunFetcher()which catches errors per-channel (sweep.ts:830-834). So if LinkedIn/Skool fail, Slack/Gmail still succeed and the per-channel error is returned inerrorsmap. This is the graceful-degrade seam for the whole system.
Verified: which agent-browser returns /opt/homebrew/bin/agent-browser on this machine. Preflight failures would manifest on a Joe-machine without Homebrew or without agent-browser installed.
Runtime/CLI absence (well-handled)
state/lib/runtime.ts has the best pattern in the repo: which(bin) check + explicit mode: "absent" return (lines 37-44, 89-91, 101-103, 149-150, 154-157). Every runtime wrapper (claude, codex, gemini, openclaw, cursor, windsurf) preflights via command -v and returns { mode: "absent", exit: 127, stderr: "<bin> not in PATH" } without crashing. This is what browser should look like.
Graceful-degrade count
grep fallback|degrade|offline|preflight|isAvailable across state/lib/ + state/bin/: 25 files match. Majority are file-existence checks (existsSync) returning [] or default. No central "capability registry."
No live probe possible for inbox-sweep --dry-run
state/bin/inbox-sweep/run.ts does not exist (skill is prose; no sidecar yet). Task asks for a probe; skipping rather than inventing a false command.
Gaps (concrete, with paths)
state/lib/browse.ts:26—runBrowserhas noagent-browserbinary preflight. A Joe-machine without Homebrew crashes with "command not found" instead of a clean degrade. Fix: copy thewhich(bin)pattern fromstate/lib/runtime.ts:37-44, return{ ok: false, reason: "agent-browser not in PATH" }and let callers skip gracefully.
state/skills/memory-consolidation.md— does not specify which memory directory to read. On a multi-project machine (11.claude/projects/dirs here), the cron either hits one hardcoded path or iterates without guidance. The skill also ignores snappy-os's ownstate/log/*.ndjsonmemory entirely — consolidation is scoped to markdown files only, so the 6.6MBsymlink-runs.ndjsonnever gets pruned.
- No
ANTHROPIC_API_KEYrate-limit / quota degrade instate/lib/dispatch.ts.dispatch.ts:160readsenv("OPENROUTER_API_KEY"). Provider 429s propagate as thrown errors — no retry, no fallback to a secondary provider, no cost-cap check before dispatch. Every orchestrator relying on dispatch crashes when the quota hits; nothing in the eval log captures "skipped because quota exceeded" vs "failed for real."
Verdict
Memory: snappy-os has TWO memory systems. state/log/*.ndjson is the machine's own append-only memory, synced cross-machine via the gateway. ~/.claude/projects/*/memory/*.md is Claude-Code-harness auto-memory, per-machine only, and NOT synced by snappy-os. The memory-consolidation skill only touches the second kind — the ndjson logs grow unbounded (see 6.6MB symlink-runs).
Capability-fallback: the shape is right (env → explicit throw; gateway → local fallback; sweep → per-channel error isolation; runtime.ts → which preflight). Execution is uneven: browse.ts trusts agent-browser exists, dispatch.ts trusts provider quota exists, memory-consolidation doesn't know which dir. Fix the three gaps above and the system degrades cleanly on a bare Joe-machine.