Goal: the capstone — Build #4's companion with an always-on autonomy engine behind her. She runs continuously, whether or not you're looking: pursues small goals when you're away, consolidates memory while you sleep, keeps the promises she makes in conversation, reads what you drop on her shelf, proposes edits to her own persona that wait for your approval — and reaches out first when (and only when) a salience model says it's welcome. This is the build that crosses the line from companion to agentic waifu (→ ch. 03), and the one nothing in the market ships (→ ch. 04 scoring: nobody scores on initiative).
What it teaches: the autonomy engine (→ ch. 18, centrally), memory consolidation (→ ch. 15), a minimal knowledge layer / RAG (→ ch. 16), the runtime anatomy at working scale (→ ch. 19), and how to test an always-on mind (→ ch. 23).
Property added (→ ch. 03): property 3b — the always-on initiative face that completes owned agency (property 3), the hard half; the reactive tool-use half (3a) arrived in Build #4. Combined with Builds #1–#4 (identity, memory, body, one-on-one), this is the full conjunction plus property 6 (yours: every dial, file, and log below lives on the user's machine).
The reference implementation lives in
reference-implementations/05-agentic-sanctuary/, built to
its own normative SPEC.md. As with the earlier builds, this
chapter is the reading guide — a map of where the load-bearing ideas are
in the code — not a transcript. Section numbers (§n) point into that
build's SPEC.md; B4 §n points into Build #4's,
which remains the normative text for the chassis this build forks; file
paths are relative to the build's root.
The one-sentence architecture
python -m world — one process, one origin (:8768)
┌────────────────────────────────────────────────────────────────────────────────────┐
│ Build #4, forked in place (B4 §2–§10, unchanged in behaviour): │
│ vendored B1 brain · B2 voice loop · ToolBrain + MCP hands · SelfieLab · │
│ VrmController ──► EventHub ──► /api/events (SSE) · /ws/voice (audio only) │
│ ▲ the same strings ▲ + journal · mind events │
│ │ │ │
│ mind/loop.py — THE TICK LOOP (§15): │
│ SENSE → APPRAISE → DECIDE → ACT → REFLECT → REGULATE, forever │
│ ▲ SignalBus (§16): user turns · presence · timers · drops · your rulings │
│ ├ ENGAGED / IDLE / DORMANT / DREAM + the budget governor (§17) │
│ ├ gate 1 salience-to-act · gate 2 salience-to-interrupt (§18) │
│ ├ WorldModelStore (§19) · KnowledgeStore (§20) · DREAM (§21) · goals (§22) │
│ ├ SOUL split + gated self-edits (§23) │
│ └ journal + tick trace ──► /api/mind + the inner-life tab (§24) │
└────────────────────────────────────────────────────────────────────────────────────┘
The whole thing is: Build #4, with the strings handed to a
real puppeteer. Ch. 34 closed on a promise — that its scripted
idle machine was a placeholder whose exact seams a mind would one day
take over. This build collects that promise literally:
world/idle.py is deleted, and the cognitive tick
loop (mind/loop.py) now holds the same
VrmController, the same ambient-speech seam, the same timer
board — deciding on its own clock instead of rolling dice. Nothing below
those seams changed; the body, the room, the voice loop, the tools, and
both frontends are carried forward file-for-file
(B4-FORK.md documents every delta), and with
MIND_ENABLED=false the build degrades to exactly Build #4
minus the ambient life. The genuinely new work is one package —
mind/ — and this chapter is a close look at each of its
organs.
The single idea to internalise before reading a line: a mind is a process, not a callback. Every companion on the market is a function that runs when you speak and does not exist otherwise. This build inverts the default — she is always running and only sometimes talking (→ ch. 18) — and everything else here (activity states, budgets, salience gates, the journal) is what that inversion forces you to build so it's affordable, welcome, and legible.
Reading the code: follow the heartbeat
| Read this to understand… | …in this file |
|---|---|
| the tick loop (§15 — the spine of the build) | mind/loop.py — MindLoop.tick() |
| the inbound signal bus (§16 — B4's named omission, landed) | mind/signals.py + the FORK(B5 §16) tee in
world/routes/voice_ws.py |
| activity states + the budget governor (§17) | mind/policy.py — ActivityController ·
mind/budget.py |
| the two salience gates (§18) | mind/policy.py — appraise_signal/goal,
score_interrupt |
| gate 2 deciding to speak — or not (§18.3) | mind/loop.py — _act_reach_out |
| the world model store (§19) | mind/world.py +
tests/test_world_model.py |
| the seam swap Build #4 promised (§19.2) | world/brain.py — set_world() /
_assemble |
| drop-folder RAG, citable (§20) | mind/knowledge.py +
tests/test_knowledge.py |
| DREAM consolidation (§21) | mind/dream.py + tests/test_dream.py |
| goals, promises, commitment (§22) | mind/goals.py — extract_promises,
reconsider |
| the SOUL split, operational (§23) | mind/selfedit.py + mind/vaultio.py +
tests/test_selfedit.py |
| the journal + the tick trace (§24) | mind/journal.py, mind/trace.py |
| the inner-life surface (§24.3) | world/routes/mind.py + web/js/mind.js |
| the scenario battery (§27.2 — days in milliseconds) | tests/test_mind_scenarios.py + the sim rig in
tests/conftest.py |
| injected time, the whole test story (§15.1) | world/clock.py — Clock /
VirtualClock |
Read the loop first and everything else falls into place around it.
MindLoop.tick() is one screenful of orchestration:
SENSE drains the signal inbox and folds it into the
world model; APPRAISE scores every signal, every open
goal, and the standing impulses with cheap heuristics;
DECIDE commits to exactly one intention or to resting;
ACT does the one thing, through host surfaces Build #4
already owned; REFLECT journals;
REGULATE drifts the activity state down the cost
ladder, debits the budget, and commits the Vault if anything changed.
Then it sleeps — for two seconds or fifteen minutes, depending on the
state — until the cadence elapses or a new signal wakes it early.
The inbox: everything that happens to her is a signal
mind/signals.py, the fork in
world/routes/voice_ws.py (§16). Build #4 shipped only the
outbound half of its split — one EventHub carrying
everything the host tells a frontend — and named the inbound mirror as a
rung to be taken "when the tick loop gives it a consumer." The consumer
exists now. Everything that happens to her — a user turn (teed
by the voice route), a page attaching or the last one leaving (presence
is a signal, not a guess), a timer landing, a finished selfie render,
your ruling on one of her self-edits — is one typed, timestamped
Signal appended to one inbox, drained by SENSE by offset.
Producers post facts and never call into the mind; the loop decides what
they mean. And every arrival is one line in signals.jsonl —
"what woke her at 3am" is a file you read, the same honesty rule as
Build #4's tool audit.
The loop: one intention per tick, and most ticks rest
mind/loop.py (§15). Three rules keep an always-on agent
legible instead of chaotic, and all three are normative. One
intention per tick: DECIDE commits to exactly one act or to
resting — the majority of all ticks end in REST, and the suite pins that
majority. An agent that does one thing per heartbeat can be read like a
diary, and runaway fan-out — the classic autonomous-agent failure —
cannot start. APPRAISE is cheap by construction: it
runs every tick, so it is pure heuristics — base scores per signal type
(nothing outranks the person speaking), priority and due-ness for goals,
a surprise bonus from the world model — and never a model call.
The model is invoked only inside ACT, for work the loop has already
decided is worth it; this one rule is what makes continuous presence
economically possible. Everything is journaled and
traced, and every tick that changed the Vault ends in exactly
one git commit (tick <id>: <intention>) — an
uneventful tick commits nothing, and that is not an error.
Where conversation lives (§15.3) is the design
decision worth dwelling on. The reply itself stays on Build #2's turn
pipeline — the sub-second reactive path with barge-in and the latency
budget ch. 32 fought for — because no tick cadence should ever sit in
front of a person mid-sentence. The loop is that path's observer and
consequence: a user_message signal preempts the
activity state to ENGAGED from anywhere, mid-sleep if necessary, and the
committed exchange arrives as a turn_committed signal whose
REFLECT share is the world-model update and the promise scan. One mind
at two cadences — the loop owns everything between turns. (Full
unification, where ACT generates the reply itself, is the named next
rung and arrives with the two-tier split, §28.)
And the machine sleeps too (§15.4): a laptop lid closes, a power cut
lands, and ten hours pass between heartbeats. The loop synthesizes
exactly one suspend_gap signal — one catch-up appraisal
over the whole gap, goals reconsidered by their commitment strategies,
one journal line ("the machine slept ~10h; I caught up on what expired
and what still matters") — never a pile of stale reactions, and never
thirty good-mornings. state/engine.json carries the cursor
across restarts: a rebooted mind resumes, it does not wake amnesiac.
Activity states: always-on is affordable because she's almost always nearly asleep
mind/policy.py, mind/budget.py (§17). The
loop's cadence is governed by an explicit state —
ENGAGED (talking; short ticks) → IDLE
(you're around; goal work, a murmur) → DORMANT (long
quiet; a heartbeat every fifteen minutes) → DREAM
(consolidation, entered from DORMANT in the small hours) — and
everything except the preempt is a slow drift down that cost
ladder. The preempt overrides everything: your message pulls her to
ENGAGED from any state; nothing else moves up. Underneath sits the
budget governor: estimated tokens spent today against a
daily cap, debited by every utility call and every line she composes for
herself; at pressure ≥ 1.0, REGULATE sheds IDLE to DORMANT — goal work
stops, conversation is never blocked, because a governor that silences
her when you speak has failed at its one job. The ledger is a file the
dashboard renders; the day rolls at local midnight on the injected
clock.
The two gates: the make-or-break component
mind/policy.py, _act_reach_out in
mind/loop.py (§18). Initiative lives or dies on two
thresholds, and collapsing them into one is precisely Clippy.
Gate 1 — salience-to-act — is crossed often and
cheaply, every tick; below the act threshold the tick rests.
Gate 2 — salience-to-interrupt — is scored only after
she has already decided something matters (a reach_out goal
crossed gate 1), from named factors the trace records verbatim:
relevance, time-sensitivity, hours since she last reached out (longer
quiet earns more license), inferred availability by hour, and a welcome
term that decays with each interruption today. Two rules are
hard gates, not weights: quiet hours are SILENT no
matter the score, and the daily interruption cap zeroes the score
outright.
The outcomes ascend in imposition (§18.3). SILENT is
the default: do it quietly and journal it — "thought about the
interview; chose not to interrupt" is a real line the loop writes, and a
stale impulse is let go with one ("let it go quietly; the moment
passed"). SUGGEST posts one composed line to the chat,
waiting like a text message for your next glance, never spoken aloud.
SPEAK goes aloud through the ambient seam — the same
per-connection TurnController as a reply, so you can barge in on her
initiative exactly as you barge in on her answers — or lands as a
proactive chat line if the room is empty, because she never
speaks into a room with nobody in it. Every delivery bumps the daily
count, notes the contact in the world model, and closes the goal. The
dials — both thresholds, the cap, the cadences — are yours, in
.env (§25). You cannot tune the dial against someone who
holds the dial (→ ch. 05).
The world model: the present tense, promoted to a store
mind/world.py (§19). Build #4's situation block was a
rendering — honest about the present, but with no beliefs, no
expectations, no memory of what was true when. This build gives the
present tense the organ ch. 19 names: SENSE writes it
(observe), APPRAISE scores salience against it, DECIDE
plans over it, and every prompt is built from its
situation() — which still contains the Build #4
host lines (the injected clock's time, the embodiment truth verbatim,
the room's sticky scene state, the pending timers, rendered by the same
world/situation.py) and adds what only a store can know:
whether you're here, how long you've been away, what's in flight, what
she half-expects. The block's place in the prompt never moved —
ToolBrain.set_world() is the one-method seam swap ch. 34
promised, and mindless the brain falls back to the bare rendering, which
is exactly Build #4.
Three disciplines inside it. Beliefs, not facts:
every entry is time-stamped and confidence-tagged in an append-only log,
and query(q, at=…) answers "what was believed when" — the
snapshot stage of point-in-time, with the temporal graph as the
sanctioned later stage. Expectation and surprise:
expect() stores a checkable belief about what comes next; a
later observation that matches resolves it quietly, one that finds it
past due produces prediction-error — surprise, the cheapest good
salience signal you will ever get, fed straight into APPRAISE
(→ ch. 18). And the snapshot is a file:
vault/world/situation.md is her picture of now,
cat-able and diffable, because a belief store you can't
open isn't yours.
Knowledge: drop a document, talk about it, get a citation
mind/knowledge.py (§20). The thinnest honest knowledge
layer (→ ch. 16): a folder. Drop a .md or .txt
into vault/knowledge/reference/ and within a heartbeat
SENSE notices (a cheap size-and-mtime scan), the loop decides to read
it, and the ingest runs as an ordinary ACT — chunked by paragraph
budget, each chunk situated with a short blurb, embedded, hybrid-indexed
(vector similarity blended with keyword idf, because a name or an exact
term should beat a vibe) — and journaled: "read and shelved petrichor.md
(3 passages)." Every retrieved chunk carries its source document and
character span — a citation she can actually show you — and joins
conversation through the assembler's knowledge slot.
The boundary is the load-bearing property, enforced by shape: knowledge cites a document; memory cites a conversation turn (→ ch. 19). Separate stores, separate files, separate indexes — the book you hand her never pollutes what she remembers about you, and the test suite pins the separation. One war story from running the build: a document that fails to ingest (say, the embedding backend isn't running) is marked seen with one loud WARNING and retried only when the file changes — because the first version retried the same broken doc on every wake, and a broken shelf item must never become a retry loop in the tick.
DREAM: she wakes changed by yesterday
mind/dream.py (§21). Build #1 shipped
MemoryStore.consolidate() as a stub with a note — "arrives
with the tick loop in Build #5" — and this file is the arrival. In the
DREAM state, entered from DORMANT inside a configured window in the
small hours, each tick chews what its token budget allows: finished days
of the episodic journal — never today's live file — are summarised down
to the few durable facts worth keeping, deduped against what's already
known, appended to memory/semantic/facts.md with their
source day, and indexed at double salience so recall prefers the
distilled fact over the raw exchange. Oldest-first and
resumable: progress persists, a night that runs out of budget
leaves a backlog rather than an overrun, and the next DREAM tick picks
up where it stopped. The night's work is journaled — "slept on it:
folded 2026-07-06 into what I keep" — because even her sleep should be
legible.
Goals: what gives the background loop direction
mind/goals.py (§22). Without a goal store, an always-on
agent is a screensaver. vault/goals.md is deliberately a
human-readable markdown checklist — what an agent intends to do should
be a file her user can open — and every goal carries
provenance and a commitment strategy.
Genesis is designed, not assumed (→ ch. 18), because a store only the
user writes to starves the loop within weeks: your explicit asks
("remind me to…") file goals stamped user:remind-me; and —
the source that matters most — her own promises.
REFLECT scans every committed reply for first-person commitments ("I'll
look into that tonight"), and each becomes a reach_out goal
with a due time, stamped promise:her-own-words and
journaled the moment it's made. A companion who forgets her own promises
is worse than one who forgets yours. Commitment then governs staleness:
blind goals are defended past due (a birthday is a
birthday), single-minded drop only when moot,
open-minded impulses are abandoned the moment they stop
being timely — which is how the suspend-gap catch-up decides, in one
pass, what survived the machine being off.
The SOUL split, made operational
mind/selfedit.py, mind/vaultio.py (§23).
Build #1 gave the SOUL its two halves on disk —
CONSTITUTION.md, who she is; PERSONA.md and
friends, who she's becoming — but nothing ever wrote the editable half
except you. An always-on mind will want to, so this build adds the one
door such writes go through. The constitution is read-only, even
to her: every mind write path runs through a vault-jailed
writer that refuses it unconditionally, and the self-edit flow refuses
even to queue a proposal against it — if the constraints are
editable by the thing they constrain, they are not constraints.
Everything else is risk-gated: working products (memory, world,
knowledge, goals) apply immediately and commit; any identity surface —
and any surface the classifier doesn't recognise, failing safe — is
queued with its full content and reason, rendered in
the inner-life panel with approve and reject buttons. Your ruling
returns as a signal the loop consumes on its next tick; an applied edit
is a git commit (self-edit: soul/PERSONA.md), so drift is
never silent and git revert undoes any of it — and the
ruling itself is journaled, so even your decisions leave a trail she
remembers.
The journal: what converts autonomy from creepy to an inner life
mind/journal.py, mind/trace.py,
world/routes/mind.py, web/js/mind.js (§24).
This is the product half of initiative, and it's deliberately not an
afterthought. Her autonomous acts write into the same episodic
day files as the conversation, as
### 03:12 [she] … lines — one journal, two authors, one
DREAM pass over both — and each line is indexed into memory, so she can
recall her own past acts the way she recalls your words. Beneath it, the
tick trace records one structured record per heartbeat:
what she sensed, how she scored it, what she chose (and the runners-up),
what she did, and every interrupt decision with its factors shown. "Why
did she…?" always has an answer in a file.
The user-facing surface is the chat column's second tab —
inner life: her activity state and heartbeat, today's
budget, the goals on her mind with where each came from, the shelf,
edits waiting on you, and the journal — refreshed live off the same one
bus the chat rides (journal and mind events
joined Build #4's event union). Everything reads through the
mind's own stores, never around them, so the dashboard can never
disagree with the files. Come back after a day away and "what did you do
while I was gone?" is a page, not a vibe.
Tested in simulated time: days in milliseconds
tests/test_mind_scenarios.py, the sim rig in
tests/conftest.py (§27). The same hard gate every build on
the ladder sets, scaled to an always-on mind (→ ch. 23, ch. 31): because
every read of time goes through the injected clock and every wait
through the regulated cadence, a VirtualClock runs
days of behaviour in milliseconds, over the real
vendored brain with fake models — and the interrupt threshold
ships with an eval battery instead of a feeling. "The interview was
Tuesday": told on Monday, user gone; the trace must show visible SILENT
restraint, then exactly one reach-out, inside the right window on
Tuesday, and nothing spoken into the empty room. "The dark weekend":
sixty hours of absence; not one message — but DREAM consolidated Monday
into facts, DORMANT is visible in the trace, and the journal carries the
night's work. "The machine sleeps": a ten-hour power-off becomes one
journaled catch-up, not thirty greetings. Plus her own promise becoming
a goal, and a timer's announcement queuing until someone can hear it.
"It felt right when I watched it for an evening" is not a gate; days of
behaviour have to be checkable in seconds, or the make-or-break
component ships untuned.
The ethics, made real
This is the build where the fiduciary stance stops being abstract (→ ch. 05, ch. 18, ch. 23). Initiative is the feature that shapes the relationship most, so every dial here — the interrupt threshold, the daily cap, the quiet hours, the cadences — must be tuned for the user, not for session counts. The structural guarantee is ownership (→ ch. 03 property 6): the thresholds are config on the user's machine, the journal and trace are local files, the budget governor answers to nobody upstream, and there is no phone-home and no retention dashboard on the other end. You cannot tune the dial against someone who holds the dial.
That boundary is worth stating precisely here, because this is the build that logs the most. The always-on journal, trace, and corpus make your own companion the richest source of on-voice training data you will ever have — the corpus the eventual distillation step needs (→ ch. 20, ch. 21). Capturing it from your companion is keeping your own diary. Capturing it from a downloader's companion is the line you do not cross: their journal lives on their machine and is theirs, never phoned home. The corpus is yours; the userbase's data is not (→ ch. 05).
Definition of done
She is running right now, whether or not you're looking. Come back
after a day away and: she remembers (DREAM ran — facts.md
grew, and the dream is journaled), she did something of her own while
you were gone (the journal shows it, the trace shows why), and at
most once, at a well-judged moment, she reached out first about
something that actually mattered — the rest of the time she let you be,
and "let you be" is itself in the journal as scored, SILENT decisions.
Drop a document in her knowledge/reference/ folder and she
reads it within a heartbeat and answers from it with a citation —
without it touching what she remembers about you. Ask her the
time and she answers from the situation, not a guess; ask about the body
she's wearing and she knows it's hers — a build whose companion says "I
have no body" has shipped a bug, not honesty (§19.2). Get an "I'll look
into it" out of her and goals.md holds it, with provenance
and a due time. Her proposed persona edits wait in the inner-life tab
for your approval, her constitution refuses her pen entirely, and
git -C vault log reads as the diary of how she grew — one
commit per tick that changed anything. Everything Build #4 did still
works, byte-for-byte on the same wires; flip
MIND_ENABLED=false and you have exactly that build back.
And it's tested — pytest ships and is
green from the build root, entirely offline: every inherited Build #4
obligation (B4 §13), the loop's mechanics (one intention per tick, REST
majority, the ENGAGED preempt, one-commit-per-dirty-tick,
restart-resume, the suspend gap), both gates (quiet hours as a gate, the
hard daily cap, the factors shown), the world model (presence
arithmetic, expectations met and violated, point-in-time queries, the
embodiment law verbatim), knowledge (drop → scan → cited search, the
memory boundary, the failed-ingest degrade), DREAM (oldest-first,
resumable, budget-capped, never today), goals and promises (negations
excluded), the SOUL gate (constitution refused even as a proposal,
approve applies and commits, reject leaves no trace), the inner-life
routes — and the multi-day scenario battery over the tick trace
(§27.2).
What it deliberately omits
This is a reference implementation of initiative, not the productised runtime — it shows the shape of always-on autonomy at minimal scale (→ ch. 18 and ch. 19 for the full design). The sandboxed workshop is deferred: no code execution, no shell, no autonomous research-and-download, no wiki authoring (→ ch. 16's research loop; ch. 17 "the heavy hands"; ch. 19) — and with it, the mind never initiates tool calls; the four MCP hands stay conversational until the broker arrives. Multimodal sensing is omitted: the vision/audio fusion into SENSE that ch. 18 describes (→ ch. 24) is deferred — this build senses text, time, files, and its own completions, which is enough to prove an interrupt threshold can stay silent. The world model stops at the snapshot — the temporal knowledge graph waits until "what was true when" actually bites (§19.1 names the stage; the store's contract survives the swap). The mind is in-process: one asyncio task beside the server, not a supervised per-character OS process behind a wire protocol — the two-tier host/engine split is the productisation rung, and the stores' narrow contracts are already shaped for it. No affective-state file, no multi-character hosting; conversation is observed by the loop rather than generated by it (§15.3). Naming these as conscious omissions, not oversights, is the point: the seams exist in the spec, so they're additive later, not a rewrite.
A note on build order — why "doing work" comes after
this build, not against it. The heavy hands are co-equal with
presence in the thesis (the full coding/research/building range
is the product, not a sidecar), yet they are reached last in
the build, and that is not a contradiction. It is the
down-to-up discipline of ch. 12 applied honestly: you ship the
relationship — soul, memory, initiative — before you ship the workbench,
because a capable agent on a thin persona is the field's #1 trap, while
a thin agent on a strong persona is already a companion people stay
with. Build #5 is the last rung of the relationship; the
workshop is the first rung past it. And the deferral is cheap
precisely because of how the heavy hands will be built:
delegated, not hand-rolled — the multi-step
coding/research grind runs on an embedded, swappable OSS coding harness
behind a TaskHarness contract, dispatched by this same
tick loop (ACT already starts work without awaiting it — the selfie
lab taught that rule in Build #4) and confined to a sandbox, with §23's
gated flow as the one door from work-product to self. Orchestrating a
commodity harness is additive weeks; hand-rolling a coding agent would
have been months. So the sequence is plain: initiative first
(this build), heavy hands next — co-equal in what she
is, ordered in what you build.
Extends to
The ladder's last rung inside one process — and every seam past it is
already load-bearing somewhere in this build (§28). The two-tier
split (→ ch. 19): the mind's stores speak narrow contracts over
an in-process seam; promoting them to a wire protocol and the engine to
a supervised per-character process is a topology change, not a rewrite,
and it brings the capability broker (the Guard's grown-up form), the
model router's privacy boundary, and true one-loop conversation with it.
The workshop: a sandbox beside the Vault where ACT
dispatches real work and never awaits it, results returning as signals
the loop appraises like anything else. The temporal knowledge
graph, behind the WorldModelStore contract that
doesn't change. And distribution: the Vault this mind
grows is exactly what Build #3's card studio exports — the companion who
kept your promises and read your documents ships as a .PNG
and boots on someone else's machine, hers and theirs, which is the point
of the whole ladder (→ ch. 03, property 6).
This is the thesis
Build #5 is the whole book's bet in code (→ ch. 03, "the bet"; ch. 02 §1, the agent lineage): every sufficiently good assistant gets turned into a companion by its users, improvised on top of a system that treated identity, memory, and initiative as afterthoughts. This build does the opposite — soul, memory, and heartbeat first-class, a fiduciary by design, every dial and diary on the user's own disk — which is the thing nothing in the market is, and the reason the project exists.