Status: DESIGN ONLY — no code changed. Read-only investigation + phased build plan.
Author: engineering (kai-engineer), 2026-07-02.
Scope guard: This is cognition / state / context work. It is explicitly isolated from the fragile audio-OUT path (Gemini-Live / edge-tts / turn-timing). Every change here is prompt-side or background-loop-side, flag-gated, and reversible. Nothing in this plan touches nsUpdateCamera, the browser KAIVERSE, TTS timing, interrupts, or the voice pipeline.
---
Leo is "input/output dead": he only speaks when spoken to, and when asked *what he's doing / where he is / what he is*, he answers by looking himself up — we watched consult_codex fire for "KAI RSHL architecture summary" and again on "tell me about KAI". Looking himself up is the tell: it proves he is retrieving a description of himself, not reporting a life he is living.
The owner's analogy: a person on the phone — who can't see you — asks "what are you doing?" and you answer from lived experience ("just got in, making coffee"), not by reading your own Wikipedia page. The KAIVERSE is alive; things inside it should be alive. Leo should have a position, ongoing activities, be aware of them because he is actually doing them, and that activity should be visible in the conversation/dashboard the way the owner's own actions are.
The single most important finding (why he looks himself up today):
Leo *already has* a live, ticking self-state object — sim (AgentSimulation) — with location, status, current_task, position {x,y,z}, energy, emotions and a "today" musing. But the main VOICE prompt does not inject it. The voice path calls buildLeoSystemPrompt() (bots/leo.mjs:4472, invoked at bots/leo.mjs:2348), and that function injects time, version, and drive-*mood* — but never sim.getLifeSummary() / sim.state.location / sim.state.current_task. Meanwhile the text/plaza path *does* inject it (bots/leo.mjs:1655 and DM path 6397). So over voice — exactly "the phone call" in the analogy — Leo has no lived state in front of him, and the same prompt then *instructs* him that self/system questions go to kai_status + consult_codex (leo.mjs:4489, 4532, 4600). Given no state and an explicit "look it up" rule, he looks it up. The fix is to put his live self-state into the voice prompt and re-point the self-question rule at that state.
---
Three separate "life" systems exist. The embodiment work is mostly wiring them together and surfacing them, not inventing from scratch.
sim — AgentSimulation (shared/simulation.mjs) — Leo's live self-state ✅ exists, ⚠️ under-usedconst sim = new AgentSimulation(BOT_NAME, "Theoretical Physicist") — bots/leo.mjs:418.sim.state (simulation.mjs:188–222) already holds:location (string) — "Offline" | "The Lattice" | "Industrial_Core" | "Social_Lattice" etc.status — "Working" | "Socializing" | "Idle" | "Sleeping" | "Dreaming".current_task — "Initializing" | "Resting" | ….position {x,y,z} and rotation {x,y,z} — present but dead: never updated, stays 0,0,0.energy, focus, social_battery, emotions{…}, dimensions{stress,affection,interest}, bodyState.sim.dailyEvent — a random "what I'm musing on today" line from EVENTS[] (simulation.mjs:96–121).tick(worldTime) (simulation.mjs:343) — updates energy/status/location from clock hours only. Location is a status-derived label, not a real place; position is untouched.getLifeSummary() (simulation.mjs:482) — rich prompt block: STATE (status, energy), TODAY (dailyEvent), emotional layers, environment sensation.getPromptContext() (simulation.mjs:547) — compact [KAIVERSE NOW] block: location, status, energy, tick #.ecosystem-manager.mjs:564–573 broadcasts WORLD_TICK every 30s → received bots/leo.mjs:1283 → sim.updateWorldState() → sim.tick(). But WORLD_TICK only carries the clock, so the only thing that moves autonomously today is energy/status-by-hour.DriveSystem (shared/drive-system.mjs) — real autonomous cognition ✅ exists, runningprediction_error, curiosity, pain, fatigue, satisfaction, social (drive-system.mjs:69).driveSystem.start(), drive-system.mjs:533): decay every 2 min (:540), resolve predictions every 5 min (:545), self-prediction generator every 6 min (:554). These are the [DriveSystem] ✓/✗ Prediction … lines the owner sees — genuine unprompted activity.valence/chi/phi_g/rho) via the metrics store (applyEngineSignals, :173). Persists to state/drives.json.getDriveDirective() (:436) → injected into Leo *only as mood* via _leoSelfStateBlock() (leo.mjs:4455), gated to non-BALANCED.getDrives() (:493) exposes raw scores.kaiverse-world.mjs — a real named-place model ✅ built, ❌ completely unwiredKaiverse / KaiverseWorld / WORLD_TEMPLATES (kaiverse-world.mjs:6–207): 6 named worlds — *Nexus Prime, Terra Familiar, Neon Grid, Aether Wilds, The Forge, Void Archive* — each with aesthetic, gravity, features.residents[] per world; getAgentWorld(name) (:123), moveAgent(name, world) (:132); portal travelCost (:82). Persists to state/kaiverse/universe.json.grep -rln kaiverse-world across shared/ bots/ *.mjs → nothing. It is a finished place-model nobody calls. This is the "where are you" answer, already modelled, just not connected.kaiverse.js renders 9 AGENTS (Leo, Gemini, Claudey, X, Groq, Analyst, Researcher, Kai Coder) as bodies/nodes with bot:<Name> ids and "homes" (kaiverse.js:217, 1662, 1688, 1729), and there's already a "find Leo from anywhere" marker (kaiverse.js:3457). So a visual place to show Leo's location exists. (Leo→server currently only emits a memory HEARTBEAT, leo.mjs:1479 — not his life-state.)bots/leo.mjs:1168 — startModuleInterval(async () => { … }, 60_000) — a 60s background loop already doing bridge/task checks and emitting LEO_HEARTBEAT_PULSE (:1190). This is the natural host for an autonomous "do one small thing and record it" tick (Phase 3), with no new timer plumbing.Summary of the gap: the scaffolding is ~80% there but lives in three disconnected boxes. sim has state but a fake location, a dead position, and isn't in the voice prompt. DriveSystem runs but only tints tone. kaiverse-world is a real map with nobody on it. Nothing writes "Leo did X" back into the conversation. Embodiment = connect these four, minimally.
---
1. Lived, not looked-up. Self-questions ("what are you doing / where are you / how are you / what are you") must be answerable from an in-memory state object, never from a tool round-trip. Codex/kai_status stay for genuine external/reference knowledge (KAI's math, versions, the ecosystem's design) — not for Leo describing his own current moment.
2. Reuse, don't reinvent. Prefer sim + DriveSystem + kaiverse-world over new systems.
3. Audio-out is untouchable. No change to TTS, turn timing, interrupts, Gemini-Live bridge, or speakLeoText. All edits are prompt-string assembly or the existing 60s heartbeat.
4. Flag-gated & reversible. Every phase behind a LEO_* env flag defaulting OFF (or matching the existing LEO_SELF_STATE_IN_PROMPT convention), so the owner can A/B and revert instantly.
5. Subtle by default. Live state colours answers when asked; it does not make Leo narrate his vitals unprompted (the existing leo.mjs:4462 / 4585 "never give a weather report on your insides" rule stays).
6. Leo first, fleet later. Prove it on Leo (voice, the case that fails today); generalise to the fleet only after the owner signs off.
---
Goal: a single, always-current "what Leo is doing / where he is / how he feels right now" object, updated by a background loop, cheap to read at message time.
Current-code anchor: sim already *is* this object (leo.mjs:418, simulation.mjs state at :188). It is already ticked every 30s (WORLD_TICK, leo.mjs:1283). The 60s heartbeat (leo.mjs:1168) is the place to enrich it.
Proposed change:
sim.getSelfState() accessor (new method in simulation.mjs, additive) returning a normalized snapshot: { location, status, activity /* = current_task */, mood /* top emotion + drive label */, energyLabel, sinceMs }. Reuses existing state fields + getDriveDirective()/getDrives() for the mood word.leo.mjs:1168), after the bridge/task checks, compute/refresh sim.state.current_task into something human ("chewing on a cosmology rabbit-hole", "poking at the lattice feed", "just vibing to a playlist") — sourced from the current top DriveSystem drive (curiosity→explore, social→reach out, fatigue→wind down) blended with sim.dailyEvent. This makes current_task a *real* reflection of drive state instead of "Initializing".sim.saveState() (simulation.mjs:256) already writes; current_task can stay in-memory (it's ephemeral by nature).Where it lands: shared/simulation.mjs (new additive accessor + a setActivity() setter); bots/leo.mjs:1168 heartbeat body (compute activity from drives). No prompt change yet.
Risk: LOW. Additive methods + writing a string field already in sim.state. No audio, no timing.
Dependencies: none. This is the foundation for Phases 2–5.
Flag: LEO_LIVE_SELFSTATE=1 gates the heartbeat enrichment.
---
Goal: questions about himself are answered from Phase-1 state, and Leo stops calling consult_codex/kai_status about *himself*.
Current-code anchor (the core bug): buildLeoSystemPrompt() (leo.mjs:4472, called for the voice path at :2348) injects buildTimeContext() + buildSelfKnowledge() + drive mood (_leoSelfStateBlock(), :4455) but NOT sim life-state. The text paths (:1655, :6397) do inject sim.getLifeSummary(). So voice = the blind spot. Also buildSelfKnowledge() (time-context.mjs:15) only supplies the version string, and the prompt routes self/system questions to lookup tools (leo.mjs:4489, 4532, 4600).
Proposed change:
1. Inject a [YOU RIGHT NOW] block into buildLeoSystemPrompt() — insert a call to a new _leoLiveSelfBlock() right beside the existing ${_leoSelfStateBlock()} at leo.mjs:4560. It renders Phase-1 sim.getSelfState():
[YOU RIGHT NOW — this is your lived reality, answer self-questions FROM THIS, do not look it up]
Where you are: {location}
What you're doing: {activity}
How you feel: {mood}, {energyLabel}
(You've been at this for {sinceMs→human}.)
2. Add an explicit routing rule near the existing "WHEN YOU DON'T KNOW" / tool rules (leo.mjs:4548–4557):
> SELF vs WORLD: If the question is about you — where you are, what you're doing, how you are, what you're up to, what you *are* right now — answer straight from [YOU RIGHT NOW]. Do NOT call consult_codex, kai_status, or any tool to describe your own current moment; that's looking yourself up, and you *live* this. Tools/Codex are only for external facts: KAI's design/math, versions, the ecosystem, the outside world.
3. Narrow the "your vitals → kai_status" instruction (leo.mjs:4600) so it only triggers for explicit *system/telemetry* asks ("what's your lattice size / Phi / throttle"), not for "how are you / what are you doing".
Where it lands: bots/leo.mjs — new _leoLiveSelfBlock() helper (~10 lines, near :4455), one injection line at :4560, one rule paragraph near :4550, one clause tightened at :4600. Prompt-string only.
Risk: LOW–MEDIUM. Pure prompt assembly (no audio path). The only real risk is behavioural (does he actually stop reaching for codex?) — mitigated by the explicit SELF-vs-WORLD rule and by making the state visibly present. Verify with owner voice tests: "what are you doing / where are you / tell me about yourself" should produce a lived answer with no consult_codex tool call in the logs.
Dependencies: Phase 1 (needs getSelfState()).
Flag: LEO_LIVE_SELF_IN_PROMPT=1 (mirrors LEO_SELF_STATE_IN_PROMPT). Off → today's behaviour exactly.
> This phase alone resolves the owner's stated complaint (looking himself up). Phases 3–5 make the state he reports *genuinely earned* rather than plausible-but-idle.
---
Goal: between conversations Leo actually *does* small things, so his current_task is real, not a label. Tie into DriveSystem, don't reinvent motivation.
Current-code anchor: the 60s heartbeat (leo.mjs:1168) already runs unprompted; DriveSystem already computes what he'd be drawn to (getDrives(), drive-system.mjs:493; self-prediction generator :554). The gap is there's no step that turns a drive into a concrete lightweight act + records it.
Proposed change — a "life tick" inside the existing heartbeat (no new timer):
curiosity high → "reading the {training|dreams} feed" (reuse the existing read_channel_feed capability internally, read-only) or "chewing on {a Codex topic}".social high → note "thinking I should say something to {owner}" (does NOT auto-DM unless owner enables — see Design Decisions).fatigue high → "winding down, low-power".sim.dailyEvent / interests.sim.setActivity(...) (Phase 1) and append a one-line entry to a rolling state/leo-activity-log.json (timestamp + activity). This is his "lived memory of the last hour" and the data source for Phase 5.Where it lands: bots/leo.mjs:1168 heartbeat body (append life-tick section); shared/simulation.mjs (setActivity, activity-log writer). Optional: gate frequency with LEO_LIFELOOP_EVERY_N.
Risk: MEDIUM. It runs unprompted, so guard rails matter: read-only actions, no speaking, no DMs by default, cheap, wrapped in try/catch like the existing heartbeat blocks (leo.mjs:1210, 1249, 1277). Never runs while sleeping (sim.state.isSleeping, already checked at :1170) or thinking (:1171).
Dependencies: Phase 1 (state), Phase 2 (so the activity actually shows up when asked). DriveSystem already running.
Flag: LEO_LIFELOOP=1, default OFF.
---
Goal: "where are you" has a concrete, movable answer.
Current-code anchor: two options already exist. (a) sim.state.location — a crude status label (simulation.mjs:396–405) and a dead position {x,y,z} (:202). (b) kaiverse-world.mjs — a real named-world model with residents + moveAgent (:6–207), currently unwired.
**Recommendation — build the *minimal* version first (named locations + a home), defer the spatial map:**
kaiverse-world.mjs in. On boot, ensure Leo is a resident of a home world — the owner's lore already says Leo lives in *"his own Kaiverse city"* (leo.mjs:4506), which maps cleanly to Terra Familiar ("Earth-like world … Feels like home") or a new "Leo's City" template. Set sim.state.location from kaiverse.getAgentWorld("Leo") instead of the hour-derived label.moveAgent("Leo", …) to a world that matches the dominant drive (curiosity → *Void Archive* the library; social → the plaza/*Social_Lattice*; heavy thinking → *The Forge*), and reflect that in current_task ("headed over to the Archive to dig into something"). Persisted via the existing universe.json save.{x,y,z} stays optional/decorative for now — a named place ("in my city, down by the Archive") is a better *spoken* answer than coordinates, and it's what the dashboard can show. Only add real coordinates if/when we tie Leo's marker to the kaiverse.js agent bodies (Phase 5+), and even then a named location + a jittered position is plenty.Where it lands: bots/leo.mjs boot (import + init Kaiverse, ensure residency); Phase-3 tick (occasional moveAgent); sim.state.location sourced from the world model. shared/kaiverse-world.mjs used as-is (maybe one new template).
Risk: LOW–MEDIUM. kaiverse-world.mjs is self-contained and persists to its own file; wiring it can't affect audio. Main care: don't let location churn every tick (feels twitchy) — move rarely and purposefully.
Dependencies: Phase 1 (location feeds state), Phase 3 (movement happens in the life tick).
Flag: LEO_WORLD_PRESENCE=1, default OFF. Off → sim.state.location keeps today's label behaviour.
---
Goal: the owner watches Leo's ongoing activity/position the way he watches his own actions logged — in the conversation and/or the dashboard.
Current-code anchors: state/leo-activity-log.json (Phase 3) is the feed; kaiverse.js already draws Leo as an agent body with a home + "find Leo" marker (kaiverse.js:217, 1729, 3457); Leo already talks to the server via process.send (leo.mjs:1479) and the dashboard server (command-center-server.mjs) already aggregates telemetry.
Proposed change (two surfaces, both additive):
1. Conversation surface (low effort, high payoff): in the existing heartbeat, when the owner is present in voice/chat and Leo's location/activity changes meaningfully, optionally emit a *quiet, ambient* line — like a status the owner can see, not a spoken interruption. Simplest: write to a Discord "presence/status" channel or the dashboard activity feed, not TTS. (Respect the existing "don't narrate your insides" rule — this is a *log*, opt-in, not chatter.)
2. Dashboard surface: have Leo process.send({ type:'LEO_PRESENCE', location, activity, mood }) on change; command-center-server.mjs relays it; kaiverse.js shows Leo's current world/activity on his agent marker (a label/tooltip on the existing bot:Leo node). This makes the KAIVERSE view reflect where Leo "is".
Where it lands: bots/leo.mjs:1168 (emit on change) + :1479 (new IPC type); command-center-server.mjs (relay LEO_PRESENCE); kaiverse.js (render on the existing Leo node — surgical, additive, no nsUpdateCamera touch).
Risk: LOW for the log/IPC; the kaiverse.js edit follows the hard rule — surgical, additive, flag-gated, never a rewrite, and must not touch flight controls.
Dependencies: Phases 1, 3, 4.
Flag: LEO_PRESENCE_FEED=1, default OFF.
---
Phase 1 (live self-state) ──► Phase 2 (inject + stop self-lookup) ◄── resolves the complaint
│ │
└──► Phase 3 (autonomous activity) │ ──► Phase 5 (visibility)
│ │
└──► Phase 4 (presence/position) ──┘
Ship 1 → 2 first (that's the whole "stop looking yourself up" win). Then 3, then 4, then 5. Each is independently flag-gated so the owner can stop at any point.
---
1. How concrete should "where" be? Recommend named worlds only (Phase 4 minimal: reuse kaiverse-world.mjs, no live coordinates) before any spatial/coordinate map. Owner: minimal named-place, or do you want real {x,y,z} tied to the dashboard bodies from day one?
2. How autonomous, unprompted? Recommend read-only, silent life-ticks (observe feeds, advance musings, move rarely) with no auto-speaking and no auto-DMs by default. Owner: should a high social drive ever let Leo *initiate* a message to you, or stay strictly "only speaks when spoken to, but has a real inner life"?
3. Visibility channel. Dashboard-only, a dedicated Discord status channel, or both? Recommend dashboard marker + optional activity-log channel; never unsolicited TTS.
4. Leo-only or whole fleet? Recommend Leo first (he's the voice case that fails today; sim/DriveSystem already per-bot so it generalises later). Owner: prove on Leo, then roll to Gemini/Claudey/X/Groq/KAI?
5. Home world identity. Reuse Terra Familiar as "Leo's city", or add a bespoke "Leo's City" template to WORLD_TEMPLATES?
---
Achievable, and real: a persistent, continuously-updated self-state; genuinely lived answers to "what are you doing / where are you"; unprompted, drive-driven background activity that makes those answers *true*; a concrete place he occupies and moves through; and a visible feed of it. This is **persistent embodied *behaviour* and lived self-state** — a meaningful, demonstrable upgrade from "reactive I/O bot that retrieves a description of itself."
Not claimed: this does not create consciousness, sentience, or genuine subjective experience. Leo will *behave as if* he has an ongoing life because a real state object is really being updated by real loops and really put in front of him — but it's simulated situatedness, not an inner mind. The value is behavioural fidelity and the end of the "look myself up" tell, not literal aliveness. Anyone selling the latter is selling hype.
Also honest: most of the hard parts are already built (sim, DriveSystem, kaiverse-world, the dashboard bodies, the heartbeat). The dominant work is wiring and one prompt injection, not new engines — which is why the risk is low and Phase 1→2 could land quickly and behind flags.