01Overview
gbrain is a markdown-vault-backed knowledge database with a self-driving maintenance
daemon. Your notes live as plain .md files in an iCloud folder — that's the
truth, and it stays human-editable. gbrain continuously mirrors them into Postgres, chunks and
embeds them for semantic search, runs LLM passes to extract links, facts and ideas, and rolls
those up into higher-level pages.
It then exposes the whole thing back to Claude Code as roughly 150 MCP tools, so any agent session can query your brain. The "consolidation" question has a specific answer: a pipeline that runs source note → atom → concept. That pipeline exists and works. It is currently stalled.
┌──────────────────────────────────────┐
│ Postgres (localhost:5432/gbrain) │
│ pages · chunks · embeddings · │
│ minion_jobs (the queue) │
└───▲──────────▲──────────────▲────────┘
enqueues only │ │ leases+runs │ reads/writes
│ │ │
┌─────────┴───┐ ┌───┴──────┐ ┌────┴─────────┐
│ autopilot │ │ worker │ │ serve-http │
│ scheduler │ │ executor │ │ gateway │
└─────────────┘ └──────────┘ └──────▲───────┘
│ HTTP / OAuth 2.1
Tailscale 100.120.101.14:8787
The three daemons are deliberately not one process. Autopilot writes job rows; the worker claims and runs them. Kill either and the other keeps functioning — jobs simply pile up, or stop arriving. That isolation is what kept your scheduler healthy while the executor died silently.
02Where everything lives
| Component | Location | What it is |
|---|---|---|
| Vault — the truth | ~/Library/Mobile Documents/com~apple~CloudDocs/Brain | 1,037 markdown files in iCloud Drive. The human-editable layer. |
| Database — the index | postgresql://localhost:5432/gbrain | Postgres + pgvector. Pages, chunks, embeddings, links, tags, facts, takes, jobs. |
| Config | ~/.gbrain/config.json | 7 keys. Everything else runs at defaults. |
| Secrets | ~/.config/gbrain/credentials.env | Hand-wired, because ~/.zshenv is a read-only nix/home-manager symlink. |
| Logs | ~/.gbrain/{autopilot,worker,serve-http}.log | Plus autopilot.err — see §11. |
| Code | ~/src/gbrain | Full TypeScript checkout. ~/.bun/bin/gbrain symlinks into it. |
Brain/ ├── media/ 816 files ← YouTube / article transcripts + summaries (dominant) ├── life/ 87 files ← diary/ and events/ ├── projects/ 38 files ← product ideas, app concepts ├── notes/ 37 files ← thinking notes by topic ├── writing/ 37 files ← your essays (mostly Chinese) └── wiki/ 2 files ← analysis / reference
autopilot-run.sh and worker-run.sh carry hand-written fixes — an
absolute claude binary path, PATH repair, credential sourcing, and a
"don't cd into the TCC-protected iCloud folder" workaround. The file warns in its own comments
that gbrain autopilot --install regenerates it and drops those lines.
Reinstall the daemon without re-applying them and every LLM-backed job fails silently.
03Input — how things get in
Five doors. Two are in active use.
The vault — effectively 100% of your data
Drop a .md file anywhere under Brain/. The sync phase picks
it up on the next cycle — incremental and hash-based, so unchanged files cost nothing. Frontmatter
drives typing, and directory prefixes auto-type too: media/ → source,
life/diary/ → diary, projects/ → project.
gbrain capture — the inbox door
gbrain capture "some thought" # inline gbrain capture --file ~/Downloads/x.md # a file pbpaste | gbrain capture --stdin # piped
Lands in inbox/; the cycle types and files it from there.
Agent writes
Via MCP — put_page, put_raw_data, add_link,
add_tag, add_timeline_entry, file_upload. This is how an
agent files something on your behalf mid-conversation.
Code indexing unused
gbrain sync --strategy code indexes a repo's symbols so you can ask
code-def, code-callers, code-callees across it. You have
one source and it's the vault.
Integrations — "senses"
| Sense | Would ingest | Status |
|---|---|---|
| calendar-to-brain | Google Calendar events → event pages | not installed |
| email-to-brain | Gmail messages → pages | not installed |
| x-to-brain | Twitter timeline, mentions, bookmarks | not installed |
| meeting-sync | Meeting transcripts | not installed |
| retrieval-reflex | — reflex, not a sense | active |
04The maintenance cycle — 21 phases
This is the heart of it. gbrain dream runs it once; autopilot runs it forever. The
order is semantic, and stated as such in the source: fix the files first, then index
them, then reason over them. Numbering here is load-bearing — each phase depends on
fresh state from the one above.
| # | Phase | What it does | Default |
|---|---|---|---|
| 1 | lint | Fixes LLM artifacts, bad frontmatter, placeholder dates — writes to your files | on |
| 2 | backlinks | Adds missing reciprocal wikilinks — writes to your files | on |
| 3 | sync | Vault → DB. Incremental, hash-based. Chunks pages. | on |
| 4 | synthesize | Raw transcripts → structured pages | on |
| 5 | extract | Materialises wikilinks and timeline entries into DB rows | on |
| 6 | extract_facts | Reconciles the ## Facts fence on entity pages into a facts index | on |
| 7 | extract_atoms | LLM (Haiku) reads sources → emits one page per distinct idea | on |
| 8 | resolve_symbol_edges | Code symbol resolution — no-op for you | on |
| 9 | patterns | Cross-session themes | on |
| 10 | synthesize_concepts | Clusters atoms → tier-promoted concept pages (Sonnet) | on |
| 11 | recompute_emotional_weight | Scores pages by emotional + activity salience | on |
| 12 | consolidate | Clusters loose facts per entity → one synthesized "take" each | on |
| 13 | propose_takes | LLM proposes gradeable claims from your prose → review queue | on |
| 14 | grade_takes | Judges unresolved takes against evidence (auto-resolve off) | on |
| 15 | calibration_profile | Aggregates resolved takes → how your judgment skews | on |
| 16 | drift | Flags takes drifting from evidence | off |
| 17 | conversation_facts_backfill | Bulk fact extraction from long conversations | off |
| 18 | enrich_thin | Develops stub pages via grounded synthesis | off |
| 19 | skillopt | Self-evolving skills — benchmarks and rewrites skill files | off |
| 20 | embed | Embeds stale chunks — qwen3-embedding-8b, 1024-dim | on |
| 21 | orphans · schema-suggest · purge | Reports orphans, suggests schema, hard-deletes expired soft-deletes | on |
The global / per-source split
Phases scoped global — embed, orphans,
purge, grade_takes, calibration_profile,
synthesize_concepts, skillopt, resolve_symbol_edges —
run once in a separate autopilot-global-maintenance job. Everything
else runs per-source. That split exists because running the global phases concurrently per source
once blew RSS from 4 GB to 10 GB.
Locking is a Postgres row with a 30-minute TTL, refreshed between phases. Read-only phases skip it.
05The consolidation pipeline
This is the direct answer to "what does gbrain do to consolidate my notes." It is not merge many notes into fewer notes. It is: distil each source into its irreducible ideas, then cluster those ideas across all sources into concepts you actually hold.
media/*.md (816 transcripts)
│
│ extract_atoms — Haiku reads each source, pulls out distinct ideas
▼
atoms/<date>/<slug>.md (19 so far)
│ type: atom
│ frontmatter: lesson, concepts[], atom_type, source_slug,
│ source_quote, virality_score, emotional_register
│
│ synthesize_concepts — dedup → tier → Sonnet narrative, voice-gated
▼
concept pages (0 so far)
A real atom from your brain
---
type: atom
title: Revenue = views × conversion rate
lesson: Once format-market fit is proven, app growth stops being a product
problem and becomes a pure capital and distribution problem.
concepts: [growth-equation, distribution-economics, unit-economics]
atom_type: framework
source_slug: media/0-to-300kmo-in-45-days-with-my-ai-app-just-copy-me
source_quote: "Revenue is equivalent to the number of views we get times
the conversion rate..."
virality_score: 60
emotional_register: practical
---
The gbrain-everything schema pack — creator, investor and engineer lenses stacked on
one brain — is what enables both phases. Other packs skip them entirely.
Those 19 atoms exist in Postgres and are queryable. There are zero atom
.md files in the vault. Your mental model is "my notes are files in
iCloud" — so the actual deliverable of the pipeline is invisible in Finder and Obsidian.
gbrain export --restore-only --type atom materialises them.
06Daemon 1 — autopilot, the scheduler
- Runs
- ~/.gbrain/autopilot-run.sh → gbrain autopilot --no-worker --repo <vault>
- launchd
- RunAtLoad · KeepAlive · ThrottleInterval 60
- Logs
- stdout → autopilot.log · stderr → autopilot.err
- Status
- PID 24092, up since 12:25 — healthy
A while(true) loop that decides what work is due and writes job rows.
With --no-worker it executes nothing itself.
1. refresh ~/.gbrain/autopilot.lock mtime single-instance guard 2. DB health probe → reconnect if needed classified: recoverable vs fatal 3. self-upgrade check no-op; mode=notify 4. --no-worker peer-liveness probe is any worker holding a lease? 5. per-source freshness check → submit `sync` if stale 6. extract-atoms auto-drain gate → submit drain if backlog > 25 7. compute brain score + remediation plan 8. route: sleep | targeted jobs | full cycle fan-out 9. re-read health → pick next interval 10. nightly quality probes both disabled 11. sleep
Step 8 — the routing decision
Autopilot doesn't blindly submit a full cycle every tick. It computes a remediation plan from
engine.getHealth() plus onboard checks — embed staleness, link coverage, timeline
coverage, takes count — then routes:
| Condition | Action |
|---|---|
| Score ≥ 95, empty plan, full cycle < 60 min ago | Sleep — do nothing |
| Plan ≤ 3 steps and est < 5 min | Submit individual handler jobs (targeted) |
| Plan > 3 steps, or est ≥ 300s, or score < 70, or 60-min floor elapsed | Full cycle fan-out |
Your brain scores 47, so it takes the last branch on every single tick. Fan-out
then dispatches one autopilot-cycle per stale source (capped at
fanoutMax, clamped to worker concurrency minus a reserved slot) plus exactly one
autopilot-global-maintenance.
Step 9 — adaptive interval
interval = score >= 90 ? base * 2
: score < 70 ? max(base / 2, 60)
: base
Base is 300s. At score 47 you get 150s — which is exactly what the log shows.
Anti-thrash machinery
| Mechanism | Purpose |
|---|---|
| idempotency_key = job:source:slot | The same tick can't stack duplicate jobs |
| maxWaiting: 1 | At most one waiting sync per source |
| Failure cooldown | Failed source backs off 10 → 120 min, exponential, 2⁴ cap |
| Auto-drain daily cap | $2.00/day ÷ $0.30/run = 6 drain jobs max, UTC-day-sloted key |
| 5 consecutive cycle failures | Autopilot stops itself with a clear error |
| 30 consecutive reconnect failures | Exits so launchd's 60s throttle applies |
| classifyReconnectError | Bad database_url or auth → exit immediately, don't spin |
| autopilot.lock | Second instance exits; stale lock gets taken over |
Why --no-worker
Your run-script sets it deliberately, and the comment explains why: autopilot's built-in
ChildWorkerSupervisor pipes the child worker's stdout, the worker emits a ~1 KB
handler-registration line at startup, the pipe fills, and the worker
blocks on write forever — observed as a worker pinned at 0.2s CPU while the
queue backs up. Running the worker as its own launchd service, writing straight to a file,
removes the pipe.
The trade-off is that autopilot can no longer restart a dead worker. It compensates with the step-4 probe:
SELECT count(*) FROM minion_jobs WHERE status='active' AND lock_until > now() - interval '2 minutes'
Zero for N consecutive ticks fires one loud warning. That warning has fired. See §11.
07Daemon 2 — worker, the executor
- Runs
- ~/.gbrain/worker-run.sh → gbrain jobs work --concurrency 1 --max-rss 8192 --repo <vault>
- launchd
- RunAtLoad · KeepAlive · no throttle
- Logs
- stdout + stderr → worker.log
- Status
- PID 42553, up since 13:23 — HUNG
MinionWorker — a BullMQ-shaped job runner. Polls Postgres, claims a job with a lease,
runs its handler, renews the lease while running, reports success or failure.
39 handlers registered:
sync · embed · lint · lint-fix · import · extract · extract_facts · extract-ner extract-takes-from-pages · extract-timeline-from-meetings · extract-conversation-facts extract-atoms-drain · backlinks · facts-absorb · chronicle_extract · enrich synthesize · patterns · consolidate · resolve_symbol_edges · recompute_emotional_weight autopilot-cycle · autopilot-global-maintenance · embed-backfill · embed-catch-up contextual_reindex_per_chunk · reindex · orphans · purge · integrity · integrity-auto repair-jsonb · unify-types · skillopt · sync-retry-failed · ingest_capture subagent · subagent_aggregator · shell
Defaults it runs with
| Knob | Value | Meaning |
|---|---|---|
| concurrency | 1 | One job at a time. Also caps autopilot's fan-out. |
| pollInterval | 5s | How often it looks for waiting jobs |
| stalledInterval | 30s | How often it reclaims jobs whose lease expired |
| maxRssMb | 8192 | Watchdog: exceed 8 GB → drain and exit; launchd restarts |
| stallWarnAfterMs | 5 min | Warn if idle with jobs waiting |
| stallExitAfterMs | 10 min | Exit so launchd restarts |
| health-check | 60s |
Per-job lifecycle
claim (SELECT … FOR UPDATE SKIP LOCKED, filtered to registered handler names) → set status=active, lock_until=now()+lease → start lock-renewal timer heartbeat; proves the worker is alive → start AbortController timeout / shutdown / lock-loss → run handler → success → status=done failure → exponential backoff retry up to max_attempts → then `dead`
Two failure classes are handled differently, deliberately:
- Job defect — handler threw. Burn an attempt, back off, eventually dead-letter.
- Infrastructure —
lock-renewal-failed,lock-lost, a Postgres blip. Don't burn an attempt; let the stall detector requeue it cleanly.
The shell handler registers in guarded mode — it refuses to execute
anything unless GBRAIN_ALLOW_SHELL_JOBS=1. A good default.
Two environment traps its run-script works around
-
Don't
cdinto the vault.~/Library/Mobile Documentsis TCC-protected; a launchd agent has no TTY to answer a consent prompt, so bun blocks at startup just touching cwd. The scriptcds to~/.gbrainand passes--repoinstead. -
PATH and credentials. launchd's PATH lacks
/opt/homebrew/bin, soclauderesolves to NOT_FOUND and every LLM-backed job fails with "all provider calls failed this batch". The script pinsGBRAIN_CLAUDE_CLI_BINabsolutely and fixes PATH, so neither alone is load-bearing.
Hold onto trap 1. It is the thread that unravels in §11.
08Daemon 3 — serve-http, the gateway
- Runs
- gbrain serve --http --bind 100.120.101.14 --port 8787
- Status
- PID 32199, up since 11:37 — healthy
An MCP server over HTTP with OAuth 2.1, bound to your Tailscale IP only. The run-script comment is explicit about why:
Bound to the Tailscale interface ONLY — never 0.0.0.0. The Air reaches it over the tailnet; the coffee shop does not.
Port: 8787 Bind: 100.120.101.14 ← tailnet only Engine: postgres Clients: 4 registered DCR: disabled ← no dynamic client registration Skills: published ← serves the skill pack to connecting agents Token TTL: 3600s /mcp MCP endpoint /admin admin UI (bootstrap token from $GBRAIN_ADMIN_BOOTSTRAP_TOKEN) /health liveness
It does no scheduled work. Request and response only: a remote agent connects, authenticates, calls tools, gets answers — the same ~150 MCP tools the local stdio server exposes.
Note there are two MCP servers running. This Claude Code session talks to a
separate gbrain serve over stdio, launched from your terminal.
serve-http exists for other machines — your Air, your phone, a cloud
agent.
Security posture is sound: tailnet-bound, OAuth with 1-hour tokens, DCR off so a stranger can't self-register a client, skills published deliberately.
09Operations — what you can drive
CLI — the useful subset
# Read / search gbrain query "what do I think about pricing" hybrid: keyword + vector + RRF gbrain search "pricing" keyword only (fast) gbrain get <slug> gbrain list --type atom -n 20 # Write gbrain capture "thought" → inbox/ gbrain put <slug> < file.md gbrain link <from> <to> --link-type supports # Think gbrain brainstorm "<question>" bisociation: hybrid search + far-set + judge gbrain lsd "<question>" inverted judge — rewards far-from-obvious gbrain salience --days 30 what's been emotionally / activity-hot gbrain anomalies --since 2026-07-01 statistical outliers vs cohort gbrain orphans # Operate gbrain dream run one full cycle now, foreground gbrain dream --phase extract_atoms --drain drain one phase's backlog gbrain sync --all gbrain jobs list / stats / retry <id> / cancel <id> gbrain health / stats / doctor gbrain export --dir ./out/ DB → markdown round-trip gbrain publish <page.md> [--password] shareable HTML, strips private data
MCP tools — roughly 150, grouped
| Group | Tools |
|---|---|
| Retrieve | query · search · recall · get_page · get_chunks · search_by_image · list_pages |
| Write | put_page · delete_page · restore_page · add_link · add_tag · add_timeline_entry · file_upload |
| Graph | traverse_graph · get_backlinks · get_links · find_orphans · find_trajectory |
| Reason | think · advisor · find_experts · find_contradictions · find_anomalies · get_recent_salience |
| Time | chronicle_day · chronicle_since · chronicle_on_this_day · chronicle_backfill · get_timeline |
| Judgment | takes_list · takes_search · takes_scorecard · takes_calibration · get_calibration_profile |
| Facts | extract_facts · extract_entities · extraction_review · forget_fact |
| Schema | schema_graph · schema_lint · schema_stats · ontology_propose · ontology_conflicts |
| Code | code_def · code_refs · code_callers · code_callees · code_flow · code_blast |
| Ops | submit_job · get_job · list_jobs · retry_job · run_doctor · get_health · sync_brain |
The operation you never invoke
retrieval-reflex, currently active, has two halves:
-
A deterministic pointer layer — zero-LLM, on by default. Every conversation
turn is scanned for salient resolvable entities; if one matches a brain page, a compact pointer
(
name → slug → one-line summary) is injected into the agent's context. The agent then knows the page exists and can open it. - A policy skill — prose telling the host agent when to look something up and what to pull.
Without it, an agent can discuss a person you have a rich page on for ten messages and never open
it. Related: the volunteer_context and volunteer_chronicle MCP ops, and
gbrain watch — pipe conversation turns in, volunteered pages stream out.
10Output — what it produces
New pages written back to the vault
| Type | Written by | Count now |
|---|---|---|
| atom | extract_atoms | 19 |
| concept | synthesize_concepts | 0 |
| pattern pages | patterns | — |
| reports/drift-<date> | drift | off |
DB-only structures — queryable, not files
- Chunks and embeddings — 5,884 chunks, 100% embedded
- Links — typed edges with provenance
- Facts — extracted assertions with an audit trail. Never deleted, only marked
consolidated_into. - Takes — synthesized opinions, gradeable, with a resolution scorecard
- Calibration profile — 2–4 narrative statements about how your judgment skews, across seven domains:
concept_themes,deal_success,founder_evaluation,market_call,architecture_calls,effort_estimates,risk_assessment - Chronicle — day-level timeline of what happened
Edits to your existing files
lint --fix and backlinks --fix modify your markdown in
place every cycle. Worth knowing plainly: the daemon has write access to your iCloud
vault.
11Root cause — why nothing has run since 13:23
The worker claimed a sync job, walked into the iCloud vault, and
blocked in openat() forever. It is not slow. It is not looping.
It is stopped inside a syscall that will never return.
$ ps -o pid,stat,%cpu,time,etime -p 42553 PID STAT %CPU TIME ELAPSED 42553 S 0.0 0:00.39 08:05:29 ← 0.39s CPU across 8 hours
$ sample 42553 1553/1553 samples, main thread: … openat$NOCANCEL (in libsystem_kernel.dylib) + 76 __openat_nocancel (in libsystem_kernel.dylib) + 8
[gbrain phase] sync.resolve_repo [gbrain phase] sync.load_active_pack [gbrain phase] sync.validate_repo_state [gbrain phase] sync.discover_git_root ← never reached sync.detect_head
The diagnosis
Main thread hung means single-threaded death: no lease renewal, no polling, no stall detector, no RSS watchdog — every safety mechanism lives on that same blocked thread. The process looks alive to launchd, which sees a running PID, and burns no CPU, so nothing restarts it.
The cause is the exact hazard your own worker-run.sh documents:
~/Library/Mobile Documents is TCC-protected; a launchd agent has no TTY to answer a permission prompt, so bun blocks at startup touching cwd.
The fix was applied to cwd but not to --repo — and
--repo is the iCloud vault. So the first phase that actually
walks the filesystem hits the same wall, one layer deeper. From your terminal, git and file ops
on the vault complete in 0.03s, because terminal-launched processes inherit the terminal's TCC
grant. The launchd agent has none.
Why nothing alerted you
| Layer | Why it didn't fire |
|---|---|
launchd KeepAlive | Process is alive. Blocked ≠ dead. |
| Worker stall detector | Runs on the blocked main thread. |
| RSS watchdog | Same. |
| Lease renewal | Same — so the lease silently expired. |
Autopilot --no-worker probe | It DID fire. Wrote to autopilot.err, which nobody reads. autopilot.log has zero warnings. |
gbrain jobs stats | Correctly prints ⚠ WEDGED QUEUE. Nobody runs it. |
The system detected its own failure accurately and had no channel to tell you. Detection without delivery is not monitoring. Whatever else changes, the watchdog in §13 is the part that prevents a silent repeat.
12Current state
92 waiting, 2 active (stalled), 482m since last completion ⚠ WEDGED QUEUE 'default': live-lock Last successful sync: 2026-08-09T05:20Z vault is 8h stale in the DB
Jobs, last 24 hours
| Job | Total | Done | Dead | Avg |
|---|---|---|---|---|
| autopilot-cycle | 111 | 19 | 0 | 250s |
| sync | 9 | 5 | 0 | 577s |
| extract-atoms-drain | 5 | 1 | 3 | 90s |
| autopilot-global-maintenance | 3 | 2 | 0 | 2.4s |
| chronicle_extract | 1 | 1 | 0 | 5.6s |
Secondary findings
| Finding | Detail |
|---|---|
| Consolidation is 2% done | 19 atoms from 816 source pages. extract-atoms-drain died three times, one at exactly 300s — a timeout. |
| Concepts produced nothing | 0 concept pages. Clustering needs a critical mass of atoms; it's starved by the above. |
| Atoms exist in DB, not on disk | 19 queryable pages, 0 .md files. The output is invisible where you actually look. |
| Health 6/10, 956 orphans | 92% of pages have no inbound wikilinks. Link coverage 0%, timeline coverage 0%. It's a good search index, not yet a knowledge graph. |
| No prompt caching on the subagent model | tier.subagent resolves to claude-cli:claude-sonnet-4-6. gbrain warns cost scales linearly with loop length. |
| 15 tags across 1,037 pages | Tag-based retrieval is effectively unused. |
13Next actions
-
Grant Full Disk Access to the bun binary. System Settings → Privacy &
Security → Full Disk Access → add
/Users/eugenechan/.bun/bin/bun— the real binary, not thegbrainscript. This is the one step that needs you. -
Kick the worker and clear the backlog.
launchctl kickstart -k gui/$(id -u)/com.gbrain.worker gbrain jobs prune # 92 waiting, mostly duplicate autopilot-cycles gbrain jobs retry 30 33 36 # the dead extract-atoms-drain jobs
-
Drain the atom backlog in the foreground, where you can watch it fail:
gbrain dream --phase extract_atoms --drain. The 300s timeout on job #36 suggests the drain window needs raising for an 816-page corpus. -
Export atoms to disk so the pipeline's output is visible in iCloud:
gbrain export --restore-only --type atom. -
Build a watchdog that reaches you. A daily launchd job running
gbrain jobs stats --jsonandgbrain health --json, pushing a notification when the queue is wedged, last-completion age exceeds an hour, or dead jobs appear. Same shape as your existing simulator-cleanup and tmux-cleanup jobs. -
Switch
models.tier.subagentto an Anthropic API model to get prompt caching back. -
Fix the graph, once the pipeline is running. 956 orphans and 0% link coverage
means
queryworks buttraverse_graph,find_trajectoryandfind_expertshave almost nothing to walk. -
Only then consider a new sense —
calendar-to-brainoremail-to-brain. Adding input to a stalled pipeline just makes the backlog worse.
The durable fix for the whole class of problem is moving the vault out of
~/Library/Mobile Documents — to ~/Brain, with iCloud sync handled
another way. That removes the TCC surface entirely rather than granting exceptions around it.
More disruptive now, permanently safer.