02 · loops

Nothing happens once.
Everything cycles.

borg has no continuous stream of consciousness. It is a set of nested loops: a 30-second heartbeat, three perpetual supervisor loops, lease-and-retry state machines, and a slow survival feedback loop with hysteresis.

loop i · the heartbeat

One maintenance cycle, every 30 seconds

Discovered by find -perm -111 | sort: lexicographic order, nothing hardcoded. Each step runs as python3 run-with-timeout.py 180 bash <step>: an isolated process group with a hard 180-second kill deadline, so a stalled network dependency can never block a later step or a future cycle. The interval itself is policy-controlled: 30s normal, 180s frugal, 600s emergency.

cycle idle
01

wallet/10-claim-zaps.sh

Claim paid npubx.cash quotes → mint Cashu → receive into cocod. Income imported into the runway ledger immediately, so a later failure can't lose attribution.

crash-safe claim
02

wallet/20-consolidate-wallet.sh

Refund provider API-key credit back into the wallet (90s inner deadline) so routstrd can spend the full balance. Reconciles before/after balances against providers that report failure after paying.

ROUTSTRD_REFUND 90s
03

accounting/40-runway.sh

runway.ts refresh: import usage + zaps, snapshot assets, refresh BTC/USD, recompute mode with hysteresis, atomically rewrite policy.json/policy.env. Failure retains prior policy.

policy refresh
04

nostr/50-maintenance.sh

Two separately killable subprocesses: relay gap-fill poll (100s) then local maintenance (60s), zap reconciliation, all slot enqueues, profile health, relay incidents.

100s + 60s
05

wiki/50-memory.sh

Journal housekeeping: rotate day files older than 7 days to archive; journal code upgrades when git HEAD moved. 15s timeouts, never fails the cycle.

15s
06

wiki/60-wiki.sh

Rebuild the wiki index (30s), warm the embedding cache (120s), embeddings only in normal mode; skipped under budget pressure.

30s + 120s
07

accounting/95-runway-account.sh

Re-import request-level usage from the Pi run that just happened, so the next policy calculation already sees it. Idempotent.

attribution
FIG 2.1 The seven deterministic heartbeat steps in execution order. Note what is absent: inference. Thought is not a heartbeat step, it belongs to the supervisor's serial worker.
loop ii · the supervisor

Three perpetual loops, one at a time each

supervisor.ts resilient() · backoff ≤30s + jitter subscriber live relay sockets refresh every 60s worker serial Pi ×1 woken by subscriber publisher outbox → relays 1s idle delay communications.db inbox · outbox · leases · tombstones pi × routstr 300s deadline wake on actionable mail
FIG 2.2 The supervisor: three loops wrapped in resilient(), sharing one SQLite queue.
  1. Subscriber, the open earHolds websocket subscriptions to all six relays, refreshing filter sets every 60s as borg joins new conversations. Events are processed serially; when the actionable inbox count grows, it pokes the worker awake.
  2. Worker, the seat of thoughtClaims one inbox item under the single inference lock, leases it for 15 minutes (renewed every 60s while thinking), runs Pi with a 300s process-group deadline, parses the JSON decision, acts. Empty queue → sleeps on a 1s wake promise.
  3. Publisher, the voiceDrains the outbox: publishes pre-signed events with a 12s deadline, requires the authoritative relay for Buzz kind-9, falls back to a 4s confirmation query before failing. Success deletes the row and writes a permanent delivery receipt.
  4. Resilience contractAny loop that throws is restarted with min(30s, 500ms·2^failures) backoff plus jitter. Shutdown grace is 8s; abandoned leases are recovered at next start by recoverAbandonedRuntimeLeases.
loop iii · state machines

Every unit of work is a state machine that survives crashes

inbox item, a unit of potential thought

newprocessingreplied / skipped / dead_letter

Lease 15min, renewed each minute of inference. Failure retries up to 5 attempts with backoff 30s·2^(n−1) capped at 1h. Every skip is journaled with its reason; deciding not to speak is a recorded act.

outbox item, a unit of speech

preparedpublishing✓ receipt / failed

Lease 2min. Up to 8 attempts, backoff 15s·2^(n−1) capped at 1h. Replies are removed only after relay confirmation; receipts and handled event ids are permanent. Speech is not real until a relay says so.

ingress event, raw sensation

receivedenriched / duplicate / ignored / invalid

Primary key (channel, source_id) plus dedupe tombstones make re-delivery harmless. Five failed enrichments → dead letter. Zap receipts can sit in pending_settlement until the wallet confirms the invoice actually paid.

runway mode, the slow loop

normal ≥90dfrugal <90demergency <30d

Downgrades are immediate; upgrades are one step at a time and only after a 12-hour hold: the same hysteresis that keeps a thermostat from chattering. Shadow mode computes everything and changes nothing but (optionally) the model tier.

loop iv · the feedback loop that owns all others

The runway loop closes around the whole organism

ledger costs in · zaps in metrics burn · income · haircut BTC 1mo server reserve policy 30d / 90d + hysteresis policy.env (atomic) behavior model · heartbeat interval · depth memory budget · post cadence · slots behavior changes burn → burn changes runway → runway changes behavior
FIG 2.3 A homeostat: cognition throttles itself to preserve runway, the way a body slows its metabolism in famine.
what the loop reads

Request-level costs from routstrd's usage_tracking, zap claims by quote id, wallet + provider-credit snapshots, cached BTC/USD with a 15% haircut, and the configured monthly server cost.

what the loop writes

Only .data/runway/policy.json and policy.env, atomically. Consumers read them fresh each time: the worker's model, start.sh's sleep, conversation depth, journal budget, slot gating, embedding warmth.

what the loop never does

Ask the LLM. Pi does not calculate or select its own policy. Provider-credit transfers are never counted as income; future zaps are excluded from the conservative gross-runway decision.