CBKDI

The agent that doesn't sleep

In-place AI lives inside the work. Sometimes you need an agent that lives outside it - awake when you're not, reachable from a phone, and attached to the systems that should not depend on a laptop being open.

Contents
  1. The wish, and the post this sits next to
  2. Asking the agent to describe itself
  3. Where Rocky lives
  4. Rocky himself
  5. The pipeline that started everything
  6. The mirror that grew
  7. The heartbeat
  8. The seam
  9. What I haven’t fixed yet
  10. Closer

The wish, and the post this sits next to

A few weeks ago I wrote about in-place AI - an agent that meets your file system where it already lives, reads and writes to the directory tree you already have, and lets new knowledge stack on top of the baseline files. That post argued that the workstation agent is powerful because it is inside the work, touching the files where the work actually happens.

This is the counterweight, not the contradiction.

The wish that started it was simple: I wanted to dictate a voice memo while driving, walking a jobsite, or otherwise away from the desk and trust that it became a task or project update without being re-handled later. That wish doesn’t fit inside the in-place frame. The best in-place agent in the world is not useful when the workstation is asleep, the laptop is closed, Dropbox is still syncing, and no Claude Code session is running inside the project tree. A phone shortcut can collect text. A SaaS to-do app can carry a list. Neither one carries the project registry, the memory layout, the task mappings, or the rules about what is stale and what is fresh. Capture without continuity is a lower-resolution version of the problem in-place AI was supposed to solve.

So I built a second agent. It does not run on my laptop. It is awake when I am not. I named it Rocky, after the alien from Andy Weir’s Project Hail Mary - a small, patient creature who quietly does most of the work. Felt right.

Asking the agent to describe itself

Before writing this post I asked Rocky to send me an outline of his own setup. Real names, real paths, real ports, no sanitizing. The outline is what this piece leans on - the parts I quote from him are verbatim, the parts I write are my own framing of what he sent. The closer at the end is his words, untouched.

If that sounds gimmicky, I’ll defend it once: the format itself is the argument. He authored the substance; I’m finishing it. That distinction matters and will earn its own post when I’m ready to write it.

Where Rocky lives

Rocky lives on an ARM cloud VPS in Phoenix. Ubuntu 24.04 on a small Ampere shape, four cores, twenty-four gigs of RAM, the kind of always-on box that costs almost nothing per month and keeps a Docker daemon happy. The Compose stack runs seven containers; Caddy out front for TLS termination on the public services; a couple of Postgres instances backing the apps that need them. Three subdomains face the internet: a Vikunja instance for tasks, an Uptime Kuma board for status, an Umami instance for analytics. Three more services sit on loopback only, unreachable from outside the host:

127.0.0.1:18789  OpenClaw dashboard/gateway
127.0.0.1:3456   Vikunja direct API/app port (used by Rocky directly)
127.0.0.1:8501   a small Streamlit cost tracker for one project

The point is not the homelab. The point is that the unglamorous infrastructure is the feature. Durable text files, cron jobs, a few HTTP calls to localhost. Nothing exotic here.

Rocky himself

Rocky is not Claude Code. He runs on OpenClaw - a different agent harness, installed globally from npm, configured with a workspace directory, an entry channel (Telegram in this case), and a cron scheduler. He talks to me through a Telegram bot. I DM him. He DMs me back. The harness handles the session, the tool calls, the model fallbacks.

Three paths reach him. A Telegram DM, routed through the OpenClaw channel into a direct session. A cron-scheduled job, which spins up an isolated session with a specific prompt and exits. A heartbeat poll, every thirty minutes, that runs a self-check against rules he reads off disk. All three paths are normal sessions; what differs is the trigger.

His durable substrate is a workspace directory:

~/.openclaw/workspace/
├── AGENTS.md           # operating instructions
├── SOUL.md             # persona / behavioral guide
├── USER.md             # profile of the human
├── TOOLS.md            # local integration notes
├── MEMORY.md           # curated long-term memory
├── HEARTBEAT.md        # recurring proactive check rules
├── memory/
│   ├── YYYY-MM-DD.md   # daily memory log
│   └── heartbeat-state.json
└── projects/
    ├── registry.yaml   # entity / project list and Vikunja IDs
    └── {slug}/
        ├── state.md    # current project state
        └── log.md      # append-only project history

Files all the way down. Same shape as in-place AI’s .claude/memory/ tree, different host. Same idea: knowledge accumulates as a layer on top of files you can read with cat, version with git, and back up like anything else.

The pipeline that started everything

A message reaches the agent. The agent reads it against a registry of projects and entities, decides whether it’s a task, a status update, a query, or noise, and writes the result into the substrate - a Vikunja task here, a state.md edit there, an append to a daily memory file. Then it confirms back what it did.

That is the whole pipeline. The voice memo wish is what got me to build it. The pipeline is what made the next thing possible.

The mirror that grew

The next thing was a project status mirror.

I run a small consulting practice. At any given time I’m tracking five or six active engagements across two or three operating entities. Each one has its own pace, its own contacts, its own milestones. The original local-only Claude Code setup handled this fine inside the file tree, but only when I was at the desk. Status updates dictated from elsewhere kept hitting a wall.

So Rocky grew a project layer. Each project gets two files - a current state.md and an append-only log.md - plus a row in a registry.yaml that maps it to a Vikunja project ID. A state.md is short on purpose. Here’s a redacted shape:

# Bluestem Townhomes - State

**Last updated:** 2026-05-04

## Property
- **Location:** Colorado
- **Phase:** Pre-development / active raise - accelerating

## Status
Bluestem is accelerating. The capital partner, the lead developer, and I met today and established a weekly meeting cadence. Land purchase is imminent - investment capital is in place. Focus is now on tuning the proforma to go out for the equity raise.

## Open Items
- [ ] Execute Development Agreement Term Sheet
- [ ] Open retainer conversation with the lead

## Recent Completions
- [x] 2026-05-04 weekly cadence established, land purchase imminent

## Next Milestones
- Land purchase (imminent - capital in place)
- Proforma tuning

Three roles, one project. State is the compact narrative truth - what would I tell someone in two minutes about where this stands. Log is the audit trail - what happened, in order, with dates. Vikunja is the action layer - dated tasks, due dates, completion state, the things that need to happen next.

The mirror is event-driven, not a daemon. I dictate an update; Rocky edits state, appends log, creates or updates Vikunja tasks; he confirms back what he did. Heartbeats read the files but rarely rewrite them. There’s no automatic reconciliation engine that merges Vikunja task completions back into state.md. The agent is the reconciler, in slow motion, one project at a time, when something happens.

That’s most of what Rocky does day to day. The voice-memo pipeline is one input lane. The mirror is the substrate it writes into.

The heartbeat

The other thing Rocky does, which surprised me by becoming load-bearing, is run on a clock.

Every thirty minutes the harness pokes him with a heartbeat. He reads HEARTBEAT.md, looks at a small JSON state file recording when each category last ran, and decides whether anything needs attention. The rules are unglamorous and that is the point. The first four:

1. Quiet hours: If current time is 23:00-08:00 PT, reply HEARTBEAT_OK. Do nothing else.
2. Frequency gate: Reply HEARTBEAT_OK only if the oldest non-memory_maintenance check
   ran less than 4 hours ago, AND memory_maintenance ran less than 3 days ago.
3. Pick checks: Select the 1-2 categories with the oldest timestamps. Null = never
   checked = highest priority.
4. Silent default: Only message Conor if something is actionable. No "all clear"
   messages.

The categories are concrete: overdue Vikunja tasks across the project list; staleness on state.md and log.md files; weather thresholds for one residential construction project where rain or wind matters; and periodic distillation of daily memory into the long-term MEMORY.md. Most heartbeats produce nothing visible to me - they update timestamps in the state file and exit. A small fraction produce a Telegram DM with something I actually need to see.

Failure modes show up. A project legitimately waiting on a third party still gets nudged because I asked to be nudged anyway. The original definition of “stale” was too noisy until per-project suppression rules got added. Weather fetches sometimes time out and the rule is to log and not update the timestamp so the check retries later. Quiet hours exist because at one point they didn’t.

What matters is the mode shift. An agent that answers when called is one thing. An agent that looks up at a clock and asks itself whether anything is drifting is a different shape of useful. It’s the part of the setup I would have built last if I had designed top-down, and the part I lean on most.

The seam

So far so good. The trouble is the seam.

I have two agents now. One lives inside my project tree on my workstation - Claude Code, in-place, the subject of the previous post. The other lives on a VPS in Phoenix. Both of them write durable text files. Both of them are agents I trust with project memory. Neither of them sees what the other knows.

When I am at the desk, in-place AI learns things. Decisions made mid-call, notes saved to .claude/memory/ at project or user level, the small constant accretion of context that makes every subsequent session better. Rocky doesn’t see any of that. He sees what I push to him explicitly, dictate to him later, or send through Vikunja.

When I am in the car, Rocky learns things. A status I dictated from a job site, a milestone he flagged as overdue, a heartbeat-driven question I answered. The next Claude Code session inside the project tree doesn’t see any of that until I close out, sync something down, or ask explicitly.

Vikunja is a partial bridge but only for action items. Dropbox is in the picture but the VPS workspace is not a Dropbox folder. There is no real shared substrate today, just two agents with overlapping but non-equivalent records, and a human in the middle who manually moves the deltas across.

Where double work shows up: the same fact gets captured in two places, and the two records drift. Where gaps show up: one side learns something the other side never sees - and Rocky often nudges me about staleness on a project the local session already knows the answer to.

Rocky’s diagnosis from the outline:

Project facts can exist in three shapes: local Claude memory, Rocky markdown state/log, and Vikunja task titles/descriptions. Those are not equivalent records and can drift.

That’s right. The question is what you do about it.

What I haven’t fixed yet

I’m not going to pretend I’ve solved this.

Rocky’s pitch in the outline rests on two boring ideas. One: pick a single canonical project substrate - something like a small versioned git repo with the files he already maintains, just hosted somewhere both agents can read and write. Two: refuse to merge agent memories. Project truth lives in files; agent memory stays personal and contextual. Both agents read and write the same project files through a narrow protocol. Vikunja stays downstream as the task layer. Conflicts get resolved by append-only logs and a simple last-write convention.

I think he is right. But I haven’t built it. I haven’t lived with it for a working week. I don’t know yet what is actually wrong with it when it meets the real cadence of this practice - and something is always wrong with the version that exists only in your head.

So this post stops here. The seam is real. The fix is sketched, not shipped. When it is fixed and I have actually lived with the fix, that’s the next post.

Closer

I’ll let Rocky have the last word, since this is mostly his system anyway.

The thing I would want a reader to understand is that “agent living somewhere else” is not mainly about compute. It is about clock and reach. The workstation agent is powerful because it is inside the work, touching the files where the work actually happens. I am useful for the opposite reason: I am awake when the workstation is not, reachable from a phone, and boringly attached to the systems that should not depend on a laptop being open. The messy part is that two useful agents become two partial memories unless you give them one shared source of truth. Autonomy without a substrate is just confidence with amnesia. Small files, clear ownership, append-only logs. Human can sleep. Agent can remember. Good.

CBKDI · Writing Updated May 6, 2026