BRAIN CURRENCY
docs/designs/BRAIN_CURRENCY.md
Brain currency — fix the incident, then build the ladder
Generated by /plan-ceo-review on 2026-08-10 Rev 3, after two adversarial spec-review rounds (6/10 → 7/10) and an independent outside voice. Branch: garrytan/gbrain-commit-indexing | Mode: SELECTIVE EXPANSION Repo: garrytan/gbrain
Citation convention: repo-relative paths. src/core/sync.ts (540 lines) and
src/commands/sync.ts (5804 lines) are different files; both are cited.
Origin
An investigation into "how does gbrain pick up new commits from GitHub" found it never
talks to GitHub. It diffs git diff last_commit..HEAD against a local checkout
(src/core/sync-delta.ts:113). Getting remote commits into that checkout is a separate,
opt-in concern.
It then found worse: on the founder's machine gbrain autopilot was installed, died
2026-05-31, and stayed dead 71 days while three surfaces reported healthy.
1. autopilot --status is an artifact-presence check.
src/commands/autopilot.ts:1775-1786 — plist existsSync on darwin, crontab grep
elsewhere. Never asks whether the job is loaded, the process alive, the baked --repo
present, or the log fresh. Always exits 0.
2. doctor's sync_freshness computes the 71-day number and throws it away.
src/core/source-health.ts:182-194:
const wallClockSeconds = Math.floor((nowMs - lastSyncMs) / 1000); // ← the 71 days
if (wallClockSeconds < 0) return wallClockSeconds;
if (contentMs !== null && Number.isFinite(contentMs)) {
return contentMs <= lastSyncMs ? 0 : wallClockSeconds; // ← discarded
}
When the clone is unreachable, src/commands/doctor.ts:4306-4344 routes the verdict here.
The function measures drain completeness, not staleness. "We caught up when we last
looked" and "we have not looked in 71 days" both return 0.
3. gbrain status inherits it. src/commands/sync.ts:5440-5453 → 'fresh' beside a
71-day-old date, exit 0. (gbrain sources status does report the real lag in its LAG
column, but has no warn line for it and no exit contract.)
Root cause of the death: src/commands/migrate-engine.ts (22,733 bytes) contains
zero autopilot references. The Supabase-to-PGLite migration rewrote
~/.gbrain/config.json while a daemon built on the old config kept running and died on
config.database_url.
The key insight the reviews converged on
The content comparison in #2 is not a bug someone forgot. src/commands/doctor.ts:4288-4305
documents why it exists:
a container restart wipes
local_path... and since a no-op sync doesn't advancelast_sync_at, every QUIET source read as stale/FAIL after a restart (score-sinking alert storm; observed live: 16-source brain, 12 clones gone after a config-update restart, doctor 70→30).
The premise in bold was invalidated after that code was written. v0.42.52.0 added a
heartbeat at src/commands/sync.ts:2287-2298:
// bump last_sync_at as a heartbeat on every successful 0-changes sync...
if (opts.sourceId) {
await engine.executeRaw(`UPDATE sources SET last_sync_at = now() WHERE id = $1`, [opts.sourceId]);
}
A no-op sync does advance last_sync_at now. So a quiet source that is being checked
has a recent last_sync_at and survives a wall-clock ceiling; the 71-day case has an old
one because no sync ran at all. The two cases are now distinguishable, and the
fallback's justification has expired.
That is the whole incident: a wall-clock ceiling on the discard branch, in one pure
function that doctor, gbrain status, and sources status all call. It fixes all three
by construction, with no new table, no new command, and no migration.
It also means the heartbeat this plan originally proposed to build already ships. A
separate live_ticks table would be a fourth status surface on a fifth data source,
curing "three surfaces disagreed" by adding one more that can disagree.
Base branch
The whole wave (PR-A, PR-B, PR-C) is based on
garrytan/codex-as-agent-default-install, not master. That branch carries the
bootstrap surface (src/core/bootstrap/{host-specs,hooks}.ts, detectHarness()) that
PR-B's harness tier needs, so PR-B is not blocked — an earlier revision of this doc
assumed it was.
That branch moves frequently; re-fetch before comparing anything against it. A stale remote-tracking ref is an easy way to reach a confidently wrong conclusion here.
Sequencing (decided)
Three PRs. Nothing is cut; the order changed.
PR-A — close the incident (ships first)
- Wall-clock ceiling in
lagFromContentMs(src/core/source-health.ts:189): returnwallClockSecondsonce it exceeds an absolute bound regardless of the content comparison. Bound is a named env knob per repo convention (GBRAIN_STALENESS_CEILING_HOURS, default 72, matching the existingGBRAIN_SYNC_FRESHNESS_FAIL_HOURS). - Regression test (acceptance criterion 1 below).
- E3 —
src/commands/migrate-engine.tsreconciles the running daemon. - Wrapper self-disable —
src/commands/autopilot.ts:1314-1359. Adapted fromsrc/core/brain-repo-durability.ts:509-512, NOT copied: two corrections the engineering review established.- Predicate is
[ ! -d "$repo" ], not[ ! -d "$repo/.git" ].--repomay be a subdirectory of the checkout (sync resolves the root itself by walking up), and.gitis a FILE in worktrees and submodules — either shape would self-disable a healthy install. exit 0is correct for the durability wrapper because launchd fires it onStartInterval(one shot). Autopilot runs underKeepAlive=true+ThrottleInterval=60and systemdRestart=always, where exiting disables nothing and instead produces a silent respawn-every-60s loop. The wrapper mustlaunchctl bootout/systemctl --user disable --nowitself on those targets and drop a marker that--statussurfaces.
- Predicate is
- Reconnect classifier —
src/commands/autopilot.ts:58-78; a JSTypeErrormust not substring-match as a config verdict. autopilot --statusreads the heartbeat instead ofexistsSync, and exits nonzero when stale.- E8 hygiene — test-run pollution of
~/.gbrain/sync-failures.jsonl;buildSyncManifest(src/core/sync.ts:105-140) dropping gitT(typechange). Narrowed:src/core/sync-delta.ts:130passes-Monly, soCis unreachable without--find-copiesandUneeds a conflicted worktree.C/Uhandled defensively.
Not in PR-A: the lockfile-leak fix. Removing the leaked ~/.gbrain/autopilot.lock
deletes the signal that distinguishes crashed from never installed
(src/commands/status.ts:595-598) before its replacement exists. It lands in PR-C
alongside live status.
PR-B — the harness tier alone
The harness tier is the only tier the modal gbrain user can actually run (PGLite default,
desktop harness, behind NAT), so it ships alone and early rather than buried inside the
cathedral. Reuses src/core/bootstrap/{host-specs,hooks}.ts, which the base branch
already provides.
PR-C — the ladder
live command family, live.mode bundle, shape detection, cron/daemon/webhook
tiers, advisor collector, init offer, watch tier, shared os-scheduler.ts, and the
lockfile-leak fix. live_ticks is re-examined here against the shipped
last_sync_at heartbeat rather than assumed — the burden is on the new table to justify
itself.
The constraint being satisfied (quoted so it can be checked)
docs/designs/AGENT_BOOTSTRAP_PLAN.md on origin/garrytan/codex-as-agent-default-install,
decision D9:
D9 Scheduling: almost nothing on by default. ON: SessionEnd push (event-driven, no daemon). OPT-IN: 15-min harden cron. Autopilot NOT default on PGLite (verified: its sync/embed children would contend with every live serve for the single-writer lock, and nothing handles
LiveServeLockErrorpolitely today) — recommended on Postgres; any future scheduled job must treat lock-held as skip-silently-and-log.
Reconciliation: D9 says "15-min harden cron"; the shipped default is 1800s / 30 min
(src/core/brain-repo-durability.ts:76, :659). D9's figure is stale. This plan uses 30.
This plan's decisions are labelled L1..L14 to avoid collision with that document.
PR-C design (carried forward, not yet committed to a diff)
Tiers — five active plus off
| tier | mechanism | expected cadence | engine gate |
|---|---|---|---|
off | nothing | n/a — live status exits 0 | — |
harness | agent hook / session boundary | event-driven, age-exempt | any (incl. Windows, containers) |
webhook | HMAC push from GitHub | event-driven, age-exempt; paired keepalive cron supplies the age signal | any + reachable serve --http |
cron | OS scheduler | declared expected_cadence_seconds | any; PGLite floor 1800s + lock-aware skip |
daemon | resident autopilot, runCycle | 300s | Postgres only (D9) |
watch | daemon + chokidar | floor 300s for freshness purposes, not the ~1s event latency | Postgres only (D9) |
Event-driven tiers are exempt from age-based failure; a webhook repo with no pushes for
three days is healthy, not failed. watch's freshness cadence is decoupled from its event
latency so a GC pause is not a FAIL.
off is a first-class bundle member with enabled: false, copied from
src/core/pace-mode.ts:65-71.
L1 — Shape detection predicate
| Signal | Source | Meaning |
|---|---|---|
| engine | config.engine | postgres required for daemon/watch |
| interactive desktop harness | CLAUDECODE, CLAUDE_CODE_ENTRYPOINT, CODEX_HOME, CODEX_SANDBOX, CODEX_CI (env only) | any present → cap at harness |
| long-lived host | detectInstallTarget() ∈ {macos, linux-systemd, ephemeral-container+injection point} | a reboot-surviving scheduler exists |
| server posture | serve --http configured, or minion_mode != 'off' | corroborating, never sufficient alone |
macos is in the long-lived row deliberately: detectInstallTarget() returns 'macos'
unconditionally on darwin (src/commands/autopilot.ts:1277), and darwin is the platform
of the origin incident. Omitting it would make the incident host permanently
shape-ineligible.
No filesystem probes for harness identity. The ~/.claude/hooks/... class of probe
(src/commands/autopilot.ts:1304) is what false-positives today. Env vars only.
Any inconclusive read falls to harness, never daemon.
L2 — live status exit codes
| Condition | Exit |
|---|---|
fresh, or live.mode == off | 0 |
PGLite lock held by a live serve (blocked_by_serve) | 0 |
| tier enabled + heartbeat missing or stale | 1 |
| drifted install, or DB connect failure | 2 |
live.mode == off exiting 0 is load-bearing: otherwise every fresh install exits nonzero,
which is the cycle_freshness #2540 lesson (never-configured must not turn the surface
red). And lock-held is not an outage: src/core/pglite-engine.ts:444 acquires the file
lock on every connect() and throws if it fails, so on the default engine with a resident
serve, treating that as exit 2 would make FAIL the steady state.
L3 — skipped_locked semantics
A tick that cannot acquire the PGLite lock does not satisfy freshness and does not
degrade it. It is neutral: logged, not recorded as work-done, and not counted toward
staleness for a grace window of 3 consecutive skips, after which the surface reports
blocked_by_serve with the remediation inline. Treating it as work-done rebuilds the
71-day false-green; treating it as failure makes the default engine permanently red.
L4 — Scheduler ownership
Ownership lives in a sidecar ~/.gbrain/live-ownership.json, not in an entry comment.
On darwin both harden and autopilot install launchd plists (files, not comment-bearing
crontab lines), so the # gbrain:autopilot v0.11.0 marker convention does not generalize.
The sidecar covers all install targets uniformly.
Three enumerated cases:
- Harden cron exists + pull opted in → rewrite through
os-scheduler.ts,ownership=live-adopted. - Harden cron exists + pull declined → leave it entirely alone; install a separately
labelled
liveentry. This is the default and lands first, so PR-C'slive onnever meets an existing harden cron without a rule. - Neither exists → install a
liveentry,ownership=live.
live off removes only entries live created and reverts adopted ones to harden.
Pre-existing gbrain autopilot installs are migrated, not orphaned: first live status after upgrade reports tier: daemon (legacy autopilot) and offers one-time
adoption.
L5 — Op scopes
| Op | scope | localOnly | remote |
|---|---|---|---|
live_status | read | no | allowed; omits local_path, scheduler artifact paths, and log tail |
live_tick | write | yes | reject |
live_on / live_off | admin | yes | reject |
live_self_heal | admin | yes | reject |
Self-heal walks a DB-supplied local_path and then writes a scheduler entry.
src/commands/doctor.ts already gates its git short-circuit on localOnly === true
("a remote-callable code path must NOT walk DB-supplied local_path values with
subprocess calls"). Self-heal honors that and additionally requires a realpath match
against the anchor via isAnchorOwnedSyncPath (src/commands/sync.ts:1296).
Bootstrap paradox, acknowledged: if the broken thing is the scheduler entry, a
scheduled self-heal never runs. Non-scheduled triggers are the harness tier (PR-B) and
an explicit gbrain live doctor. PR-C ships self-heal with both, not with a scheduled
trigger alone.
L6 — Revert
A code revert leaves plists, crontab lines, systemd units, and (E1) a GitHub webhook installed and unowned. Therefore:
- Revert requires
gbrain live offfirst on any enabled host. Stated in the PR body. - The generated wrapper self-disables on a marker file written by
live onand removed bylive off. Not agbrain live --helpprobe: that adds a process spawn per tick and assumes an exit code the CLI does not guarantee. - The migration, if
live_tickssurvives PR-C's re-examination, is additive and uses the next free version at implementation time (125 is the current max; two waves may land first).
L7 — E5 must not use nag-state.ts
src/core/skillpack/nag-state.ts is skillpack-scoped (schema gbrain-skillpack-nag-v1,
entries keyed on pack_version, DEFAULT_NAG_CEILING = 3, suppressed thereafter). Wiring
a dead-sync alarm through it means a genuinely broken brain goes silent after three
notices, which is a suppression mechanism for the exact failure mode whose defining
property was 71 days of silence.
E5 instead uses a rate limit, not a ceiling: at most once per session, never suppressed permanently, escalating in terseness rather than disappearing.
L8 — E1 webhook dependencies (previously unpriced)
Creating a GitHub webhook programmatically needs an admin:repo_hook token. No
acquisition, storage, scope, or rotation story existed. Therefore E1 ships in manual
mode only: live on --tier webhook generates the secret, resolves and prints the payload
URL, and the user pastes it into GitHub, matching what gbrain sources webhook set
(src/commands/sources.ts:909-916) already does. No token, no remote hook creation, no
live off remote deletion problem.
The "verified test ping" must originate from GitHub, not locally. A local ping proves nothing through NAT and would be an artifact-presence check, the precise anti-pattern in the Origin section.
L9 — live_ticks retention
If the table survives PR-C, the sweep runs inside live tick (bounded best-effort
DELETE on a TTL), not only in the cycle's purge phase. purge is a runCycle phase
(src/core/cycle.ts:1434), and runCycle runs only on daemon/watch — the cron,
webhook, and harness tiers would accumulate forever.
Scope decisions (all accepted; PR assignment added)
| # | Item | PR | Note |
|---|---|---|---|
| L10 | Approach C: full ladder | A/B/C | user chose the cathedral; resequenced, not cut |
| L11 | Tier default keys on deployment shape, not vendor | C | Hermes has zero detectable signal |
| E1 | Webhook tier, manual mode (L8) | C | |
| E2 | Self-heal with .tmp+rename+.bak rollback | C | bootstrap paradox handled per L5 |
| E3 | migrate-engine reconciles the daemon | A | the literal root cause |
| E4 | Pull cron adoption per L4, separate opt-in per L12 | C | |
| E5 | Agent-facing staleness, rate-limited not nag-ceilinged (L7) | C | |
| E6 | Windows hard error naming --tier harness | C | detectInstallTarget() has no win32 branch |
| E7 | live_ticks — re-examined, not assumed | C | the shipped last_sync_at heartbeat may suffice |
| E8 | Hygiene, narrowed to git T | A |
L12 — E4's pull cron is an autonomy question
docs/guides/upgrades-auto-update.md:41-43 states "auto is deliberately NOT a default
anywhere — it's an explicit autonomy grant, because applying code from GitHub unattended
is, by design, remote code execution." This plan does not flip self_upgrade.mode.
E4 schedules git pull every 30 minutes. That is content, not code, and durability keeps
gbrain's hooks local and untracked so a pulled commit cannot rewrite executable hook code.
But it is still unattended network fetch into a directory gbrain runs tooling against.
Therefore the pull cron is a separate opt-in from the tier, proposed and explained by
live on, never silently bundled.
L13 — The directive's internal tension, stated
"OpenClaw and Hermes default to always-up-to-date" sits against L1's "shape detection
recommends, never installs" and D9's "almost nothing on by default." These are reconciled
by scope: shape detection sets the recommended tier and pre-selects it in the init
consent prompt, so a shape-matching host is one keystroke from always-on rather than
silently converted. Whether that consent is required on upgrade as well as fresh
install is open decision F1 below.
L14 — Acceptance criteria
- Three-surface honesty. A source whose
local_pathis deleted, whoselast_sync_atis 71 days old, whosenewest_content_atis non-NULL, and whosechunker_versionmatches must report stale/fail fromdoctorandgbrain status, and must surface the lag insources status. Both fixture preconditions are required: a NULLnewest_content_atalready falls through to wall-clock (src/commands/doctor.ts:4335-4342) and a chunker mismatch already disables the fallback (:4318), so a naive fixture passes against unfixed code.sources statusis held to output, not exit code — it has no exit contract today and adding one is an undeclared breaking change to a read-only dashboard. - Quiet-source non-regression. A source with a recent
last_sync_at, an unreachable clone, and no new content must still report OK. This is the 16-source / doctor 70→30 incident; the ceiling must not re-light it. - Install honesty (PR-C).
live on --tier cronverifies the job loaded and exits nonzero if not; deleting the repo makeslive statusexit nonzero and name the path;live offleaves nothing. - Concurrency (PR-C, Postgres only). Two tiers ticking produce one import and one neutral skip record. On PGLite the second process cannot open the DB at all, so the defined outcome is a log line and no row.
- Watch tier (PR-C). E2E expects queued-job-failure, not synchronous rejection —
ingest_captureenqueues and returns. - Engine parity (PR-C, if
live_tickssurvives). DDL identical in both engines, pinned bytest/e2e/engine-parity.test.ts; bootstrap probe-set entry pinned bytest/schema-bootstrap-coverage.test.ts.
Open decisions (unanswered — do not silently default)
- F1. Does shape-detected always-on apply on upgrade as well as fresh install?
Codebase precedent (
src/commands/upgrade.ts:513-516,mcp.publish_skills) is new-installs-only with a one-time prompt for existing. Gates PR-C only. - F2. Command noun and config key:
gbrain live+live.mode(requires renaming the existingliveSyncStatushelper atsrc/core/db-lock.ts:749tosyncInProgress, two call sites) vsgbrain sync live+sync.live.mode. Gates PR-C only.
Deferred to TODOS.md
- Full Windows
schtaskstier — no test machine;harnesscovers it - Per-tier cost meter for
daemon/watch - Cross-OS scheduler probing as a
live statusdiagnostic (TODO-V19-D stays open; the heartbeat makes it optional rather than load-bearing) - Centralize the three freshness call sites onto one
freshnessVerdict()helper (existing filed P3, now partially satisfied by PR-A's single-function fix)
Dream state delta
PR-A leaves brain currency honest. PR-B leaves it workable for the modal user. PR-C leaves it a product feature. Remaining gap to the 12-month ideal: currency is still something the user turns on, not something simply true of a configured brain. F1 is the decision that closes or preserves that gap.
Reviewer concerns (unresolved after 3 iterations)
- Scope, from both reviewers: PR-C remains large (command family, mode bundle, shape detector, three tiers, advisor collector, init prompt, webhook, watch tier, scheduler extraction). The PR-A/B/C split answers the sequencing objection but not the size of C itself. Revisit at PR-C planning with the incident already fixed.
live_ticksnecessity is explicitly unresolved and assigned to PR-C rather than decided here.