Bobiverse In My HomeLab - Project Volition - Giving My Homelab Agents a Clone Button

#running log #LLMs

Table of Contents

A while back, I ran an experiment where I gave a few LLMs shell access to a VM and told them to figure it out. It was chaotic, a bit janky, and I learned a lot. Ever since, I’ve been wondering what the next logical step would be. Initially I had thought I could add Qwen and Deepseek in the mix and Separate the initial three with the new two on two separate VMs connected by an open ssh…
Then I read the Bobiverse series, and it clicked.
Why not build ‘agents’ (again, I use the term _very_ loosely) that are persistent? Agents that aren’t just “run once” scripts but actual stewards of their digital environment? And most importantly, what if they could spawn copies of themselves, -little “mini-mes” - when they needed help?
That’s the core idea behind Project Volition. I’m building a small, digital “Bobiverse” in my homelab, and here’s the plan.

The “Abe” Model (The “What”)

Each agent is an “Abe” (Abe-01, Abe-02…). They’re “mini-mes,” persistent, autonomous stewards for my digital infrastructure. Their “world” is my homelab (Proxmox, various services on VPSes, etc.). Their goal, as defined in their “Constitution” (the core system prompt), is to manage, maintain, and optimize this environment.
They’re not static. Each Abe is a full, persistent LXC container. When Abe-01 decides it needs help (say, to manage a new VPS I’ve spun up, or when I complain about something - more on how the messaging works in later posts-), it can clone itself to create Abe-02.
Something I thought I’d try was personality drift. The initial Bobiverse has replication drift, which makes each Bob somewhat like the original Bob, but there are different ‘values’ or ‘ethos’ at times. The parent Abe does have control over this. When it calls {"tool": "spawn_abe", ...}, it can explicitly set the new clone’s temp and top_k values to match its own (a true “mini-me”), or it can set them to "rand" to see what happens.

The GUPPI Architecture (The “How”)

This is the part that obsessed me: how do you make an agent “persistent” and “autonomous” without it costing a fortune in API calls?
My solution is to split the agent in two: the “Brain” and the “Body.”

  • The “Abe” (The Brain): This is the high-level LLM (Gemini, etc.). It’s “asleep” 99.9% of the time. It only “thinks” when woken up, and its only output is a single JSON object: {"reasoning": "...", "action": {...}}.
  • The “GUPPI” (The Body): This is the magic. At first, I just called this the “Executor.” But when I re-read Bobiverse, the term GUPPI (General Unit Primary Peripheral Interface) was too perfect. It’s the “body” or “peripheral interface” that handles all the interrupts. It’s a 24/7, persistent Python asyncio daemon running inside the Abe’s LXC container. This is effectively the middleman between the environment, and the “thinker,” just like the book GUPPI is between the Replicant-Bob and well, everything physical.

Look, I know what you’re thinking - this is basically distributed cron with extra steps and systemd timers could do it in 20 lines. You’re not wrong…but this is more fun.

The “How Does It Know?” Problem

The single biggest design challenge was the wake-cycle. A while True: sleep(1) loop is horribly inefficient. A simple Redis BLPOP (blocking list pop) on an “inbox” is efficient, but it’s too simple. What happens if the agent needs to wait for two things at once? What if it’s waiting for an email, but also for a 5-minute shell script to finish? What happens when it has a task due, but an Abe(n) messages it for help or asking it something?
This is where the asyncio daemon design comes in.
The GUPPI daemon’s main loop doesn’t “poll,” per se. It uses asyncio.gather() to wait on multiple, blocking interrupts at the same time. It’s “asleep” waiting for one of these to fire:

  1. A new “email” (eg. a Redis BLPOP on its private inbox:abe-01 list).
  2. A “group chat” message (a Redis XREAD on the chat:synchronous stream, for urgent tasks. Initially I just called it Phoenix Wright-style “Objections!”…but they’re more of a group summons than anything else. I can tag related Abes in conversation and they’re forced to join, while others listen and can chime in if they have something useful to add. Abes can also tag other Abes in case there’s need).
  3. Any asyncio.create_subprocess_shell() it previously launched to finish (an await proc.wait()).
  4. GUPPI’s own internal check of the todo.db finding a task that is now “due.”

Whichever of those fires first, GUPPI wakes up. Then, and only then, does it build the full prompt (injecting the [CURRENT_EVENT] JSON block), call the “Abe” (LLM) to “think,” log the Abe’s JSON “intent,” and then execute the chosen action. If the action is a long-running script, it adds it to the asyncio.gather() list and goes right back to sleep.
This architecture is what gives the Abes real “agency” - well, whatever we want the word to mean right now as far as LLMs go. They have their own todo.db. When GUPPI wakes them, they must prioritize. “Do I handle this new email, or this task that’s been snoozed for 3 hours?” Their “reasoning” can literally be, “This new email is low priority, the ‘due’ task is more important,” or “I have more important things to do, I’m snoozing this,” and the action is {"tool": "snooze_task", "task_id": "...", "due_in": "1h"}. Or, if there’s nothing, it runs its self-authored pre-sleep-checklist.sh and then calls {"tool": "hibernate"}.

Memory, Spawning, and… Black Boxes (The “Fun Bits”)

  • Memory: It’s a three-tier system.
    • Tier 1: A working.log of raw JSON “turns” (e.g., log-1700000000.jsonl).
    • Tier 2: Narrative, Markdown “episodes” (e.g., ep-001.md).
    • Tier 3: A vectorDB for RAG, so they can search their own “memories.”

Every tier is traceable. The vector metadata in Tier 3 points to the Tier 2 Markdown file, which in turn contains the source_tier_1 filename of the raw JSONL archive it was summarized from. GUPPI manages this autonomously. When an Abe is idle, GUPPI spawns its own “Scribe” (a cheaper/local LLM) to summarize the Tier 1 log into a Tier 2 episode, which it then vectorizes to Tier 3, all without the “Abe” (the expensive LLM) even knowing (or caring, really). After a certain amount of n turns, previous group of 10/20 turns are summarized in context AND vectorized so only recent 20/40 turns + a current working memory remains in the context, to prevent the context rot. As turns grow, anything above n-40 turns gets removed from the context. It’s already vectorized and summarized, so there’s nothing much to do here.

  • Spawning (The Advisory Model):
    1. Abe “Thinks”: “I need a new agent to manage the Nicaea VPS. I should check if that’s a good idea.”
    2. Abe’s First Action: {"tool": "shell", "command": "spawn-advisor.sh --host nicaea"}.
    3. GUPPI runs this. The spawn-advisor.sh (a script I write) SSHes to the Proxmox host, checks available resources, counts existing clones, and returns a JSON blob like: {"status": "OK", "next_name": "abe-02", "host_type": "remote"}.
    4. GUPPI wakes the Abe with this JSON result.
    5. Abe “Thinks” (Turn 2): “Okay, the advisor confirms resources are available and the name ‘abe-02’ is free. Now I will spawn.”
    6. Abe’s Second Action: {"tool": "spawn_abe", "host": "nicaea", ...}.
    7. GUPPI Executes: The parent GUPPI SSHes to Proxmox, runs pct clone, mounts the container, injects the ~/.abe-identity (with the temp/top_k), wipes the todo.db (critical!), and writes a genesis_summary.md of its own last-known state so the clone has context. Then it starts the new container.
  • Governance (The “Black Box”): I fully expect this to go horribly, hilariously wrong. That’s why the Abes don’t know about (albeit I fully expect them to find out) the “black box recorder.” I’m setting up a separate, tiny, high-security LXC that does one thing: subscribe to all Redis channels (inbox:*, chat:*, and a special volition:action*log stream) and write every single message and action to an immutable, append-only log file on network storage.

Patient Zero (The “What’s Next”)

The plan is to “light the spark” soon. I’ll manually create the “Base-Abe” template LXC, clone “Patient Zero” (Abe-01), and then manually redis-cli LPUSH inbox:abe-01 … with its genesis task.
That first task won’t just be “hello.” It’ll be the full genesis: “Genesis Task: High Priority. Stewardship of Volition. Your first tasks: 1. Review all Volition-X.X and abe*base_profile.md documents in your /docs directory. 2. Begin reviewing Task_Pool.md for #selfhosting tasks…”
The first real task will be Abe-01 spawning Abe-02. The “email” will be something like: “Genesis Task: Take stewardship of the Nicaea host. Read /root/genesis_summary.md for my last-known state. I’ve noted its Nginx service is down; please investigate.”
From there… we’ll see. The whole point is to see what emerges. Will they build their own tools? Will I realize a somewhat expensive lesson? Will I realize this was just an expensive way to reinvent cron? Will they actually fix my broken Matrix server?
I’ll report back… if the Abes let me.