add system overview: how and why the pieces fit together
This commit is contained in:
parent
e638376a1f
commit
c700721806
@ -6,6 +6,425 @@ production setup that's been running since early 2026.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## How This System Evolved
|
||||||
|
|
||||||
|
This section explains what we built, why, and how the pieces fit together. The
|
||||||
|
rest of the document has the specific prompts and schemas — this part gives you
|
||||||
|
the big picture so you understand the design decisions.
|
||||||
|
|
||||||
|
### The Starting Point
|
||||||
|
|
||||||
|
OpenClaw gives you a persistent workspace directory and the ability to run
|
||||||
|
tools (shell, web, browser, messaging). Out of the box, the agent wakes up
|
||||||
|
fresh every session with no memory of what happened before. The core challenge
|
||||||
|
is: **how do you turn a stateless LLM into a stateful personal assistant that
|
||||||
|
knows who you are, where you are, what you need, and what it was working on?**
|
||||||
|
|
||||||
|
The answer is files. The workspace IS the agent's brain. Every piece of state,
|
||||||
|
every rule, every memory lives in files that the agent reads on startup and
|
||||||
|
updates as things change.
|
||||||
|
|
||||||
|
### The Workspace Files — Separation of Concerns
|
||||||
|
|
||||||
|
We started with one big AGENTS.md that had everything: rules, procedures,
|
||||||
|
medication instructions, git workflow, travel preferences. It worked at first
|
||||||
|
but quickly became unwieldy. The context window was getting eaten by
|
||||||
|
instructions the agent didn't need for most tasks.
|
||||||
|
|
||||||
|
The solution was **factoring out rulesets into focused files:**
|
||||||
|
|
||||||
|
- **AGENTS.md** — the master file. Responsibilities, session startup
|
||||||
|
procedure, high-level rules. Think of it as the table of contents that
|
||||||
|
points to everything else.
|
||||||
|
- **SOUL.md** — personality, tone, values. Separated because it's
|
||||||
|
philosophical, not procedural. Also: it's fun to let the agent evolve this
|
||||||
|
one over time.
|
||||||
|
- **USER.md** — info about the human. Timezone, communication preferences,
|
||||||
|
basics the agent needs every session.
|
||||||
|
- **MEMORY.md** — curated long-term memory. Only loaded in private sessions
|
||||||
|
(security — prevents leaking personal context in group chats).
|
||||||
|
- **HEARTBEAT.md** — what to check on periodic heartbeat polls. Kept small
|
||||||
|
intentionally to minimize token burn on frequent checks.
|
||||||
|
- **TOOLS.md** — environment-specific notes (hostnames, device names, channel
|
||||||
|
IDs). Separated from skills because this is YOUR setup, skills are shared.
|
||||||
|
|
||||||
|
Then the procedural rulesets got their own files too:
|
||||||
|
|
||||||
|
- **`memory/medications-instructions.md`** — full medication protocol. Dosages,
|
||||||
|
schedules, safety rules, overdose prevention logic. Only loaded when
|
||||||
|
medication tasks come up.
|
||||||
|
- **`memory/checklist-pr.md`** — PR quality gate. What to verify before pushing,
|
||||||
|
after sub-agent pushes, before assigning to the human for review.
|
||||||
|
- **`memory/checklist-flights.md`** — flight time verification, prep block
|
||||||
|
creation, landing checklist triggers.
|
||||||
|
- **`memory/checklist-medications.md`** — pre-action verification before
|
||||||
|
reporting medication status or sending reminders.
|
||||||
|
- **`memory/checklist-messaging.md`** — rules for every outgoing message: URL
|
||||||
|
verification, timezone conversion, status claim verification.
|
||||||
|
- **`memory/landing-checklist.md`** — post-flight procedures (update location,
|
||||||
|
timezone, check meds, sync calendar).
|
||||||
|
|
||||||
|
The key insight: **MEMORY.md has a "checklists" section at the very top** that
|
||||||
|
says "before doing X, read file Y." The agent reads MEMORY.md at session start,
|
||||||
|
sees the checklist index, and knows which file to consult before any action.
|
||||||
|
This way the detailed instructions aren't always in context — they're loaded
|
||||||
|
on-demand.
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## ⛔ CHECKLISTS (read the relevant file before acting)
|
||||||
|
|
||||||
|
- **Any PR/code work** → `memory/checklist-pr.md`
|
||||||
|
- **Any message to the user** → `memory/checklist-messaging.md`
|
||||||
|
- **Medications** → `memory/checklist-medications.md`
|
||||||
|
- **Flights/travel** → `memory/checklist-flights.md`
|
||||||
|
```
|
||||||
|
|
||||||
|
### The Daily Context State File
|
||||||
|
|
||||||
|
This was probably the single most impactful addition. It's a JSON file
|
||||||
|
(`memory/daily-context.json`) that every session reads on every message. It
|
||||||
|
tracks the current state of the human: where they are, what timezone they're
|
||||||
|
in, whether they're sleeping, when they last took meds, when they last sent a
|
||||||
|
message.
|
||||||
|
|
||||||
|
Why JSON instead of markdown? Because it's machine-readable. The agent can
|
||||||
|
update individual fields programmatically, and the structure is unambiguous.
|
||||||
|
Markdown is great for instructions; JSON is great for state.
|
||||||
|
|
||||||
|
The daily context file means:
|
||||||
|
- The agent always knows the human's timezone (critical for a frequent
|
||||||
|
traveler)
|
||||||
|
- Medication reminders fire based on actual timestamps, not guesses
|
||||||
|
- Sleep predictions inform when to send alerts vs stay quiet
|
||||||
|
- Location tracking happens automatically
|
||||||
|
|
||||||
|
### The Two-Tier Memory System
|
||||||
|
|
||||||
|
Daily files (`memory/YYYY-MM-DD.md`) are raw logs — what happened each day.
|
||||||
|
Topic tables, decisions, lessons, notes. Think of these as a daily journal.
|
||||||
|
|
||||||
|
MEMORY.md is curated long-term memory — the distilled essence. Standing rules,
|
||||||
|
key facts, lessons learned. Think of this as what a human would "just know"
|
||||||
|
about their life.
|
||||||
|
|
||||||
|
During heartbeats, the agent periodically reviews recent daily files and
|
||||||
|
promotes significant items to MEMORY.md, and removes stale info. It's
|
||||||
|
explicitly modeled on how human memory works: raw experience gets processed
|
||||||
|
into lasting knowledge, and irrelevant details fade.
|
||||||
|
|
||||||
|
The security model: MEMORY.md is only loaded in private (1:1 DM) sessions.
|
||||||
|
In group chats, the agent works from daily files and daily-context.json only.
|
||||||
|
This prevents personal context from leaking into shared channels.
|
||||||
|
|
||||||
|
### Medication Tracking
|
||||||
|
|
||||||
|
This is a safety-critical system. The design is deliberately paranoid:
|
||||||
|
|
||||||
|
- **CSV as source of truth.** Not the daily-context boolean, not the agent's
|
||||||
|
memory — the CSV log file is authoritative.
|
||||||
|
- **Double-verification before any action.** The daily context has a
|
||||||
|
`hasTakenDailyMedsToday` boolean AND a `dailyMedsTimestamp`. A midnight
|
||||||
|
cron resets the boolean. But if the human is in a timezone ahead of the
|
||||||
|
server, the reset happens at the wrong time. So the rule is: always verify
|
||||||
|
the timestamp falls on today's date in the human's timezone. The boolean
|
||||||
|
is a convenience hint, never the source of truth.
|
||||||
|
- **Interval medications anchored to actual doses.** Some meds are "every 5
|
||||||
|
days" or "every 14 days." The next dose date is calculated from the last
|
||||||
|
actual dose timestamp, not from the intended schedule. If a dose is missed,
|
||||||
|
the next date is unknown until the missed dose is taken and logged.
|
||||||
|
- **Overdose prevention.** The agent blocks logging if it detects a same-day
|
||||||
|
duplicate batch. This is the highest-priority safety rule — a duplicate
|
||||||
|
daily batch of certain medications could cause cardiac arrest.
|
||||||
|
- **Escalating alerts.** If daily meds are >26h since last dose, the agent
|
||||||
|
escalates aggressively — ntfy push notification if chat messages go
|
||||||
|
unacknowledged.
|
||||||
|
|
||||||
|
The medication instructions file is loaded on-demand (not every session),
|
||||||
|
keeping context costs low for non-medication conversations.
|
||||||
|
|
||||||
|
### Sleep Tracking
|
||||||
|
|
||||||
|
The agent infers sleep from activity gaps rather than requiring explicit
|
||||||
|
"I'm going to sleep" statements. On every message, it checks: was there a
|
||||||
|
gap since the last message that overlaps with the predicted sleep window? If
|
||||||
|
so, log sleep start = last activity before gap, wake = first activity after.
|
||||||
|
|
||||||
|
Activity isn't just chat messages — it includes Gitea commits, comments,
|
||||||
|
scheduled flight departures, and any other observable actions.
|
||||||
|
|
||||||
|
Sleep predictions drift over time (the human in this setup tends to sleep
|
||||||
|
~30min later each day), so the agent tracks the trend and extrapolates.
|
||||||
|
Caffeine intake adjusts predictions forward. The predictions feed into:
|
||||||
|
- When to send alerts vs stay quiet
|
||||||
|
- Sitrep "sleep conflict" warnings (appointment during predicted sleep)
|
||||||
|
- General awareness of the human's schedule
|
||||||
|
|
||||||
|
### Location & Timezone Tracking
|
||||||
|
|
||||||
|
The agent always knows where the human is. This matters because:
|
||||||
|
- Times must always be displayed in the human's local timezone, not the
|
||||||
|
server's timezone
|
||||||
|
- "Today" and "tomorrow" mean different things in different timezones
|
||||||
|
- Medication timing needs to account for timezone changes
|
||||||
|
- Flight prep needs local airport info
|
||||||
|
|
||||||
|
The landing checklist (triggered automatically after every flight) updates
|
||||||
|
location, timezone, nearest airport, and lodging in the daily context file.
|
||||||
|
It also checks if any cron jobs have hardcoded timezones that need updating.
|
||||||
|
|
||||||
|
### The Gitea Notification Poller
|
||||||
|
|
||||||
|
OpenClaw has heartbeats, but those are periodic (every ~30min). For Gitea
|
||||||
|
issues and PRs, we wanted near-realtime response. The solution: a tiny Python
|
||||||
|
script that polls the Gitea notifications API every 2 seconds and wakes the
|
||||||
|
agent via OpenClaw's `/hooks/wake` endpoint when new notifications arrive.
|
||||||
|
|
||||||
|
Key design decisions:
|
||||||
|
- **The poller never marks notifications as read.** That's the agent's job
|
||||||
|
after it processes them. This prevents the poller and agent from racing.
|
||||||
|
- **It tracks notification IDs, not counts.** This way it only fires on
|
||||||
|
genuinely new notifications, not re-reads of existing ones.
|
||||||
|
- **The wake message tells the agent to route output to Gitea/Mattermost,
|
||||||
|
not to DM.** This prevents chatty notification processing from disturbing
|
||||||
|
the human.
|
||||||
|
- **Zero dependencies.** Just Python stdlib (`urllib`, `json`, `time`). Runs
|
||||||
|
anywhere.
|
||||||
|
|
||||||
|
Here's the full source:
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Gitea notification poller.
|
||||||
|
Polls for unread notifications and wakes OpenClaw when the count
|
||||||
|
changes. The AGENT marks notifications as read after processing —
|
||||||
|
the poller never marks anything as read.
|
||||||
|
|
||||||
|
Required env vars:
|
||||||
|
GITEA_URL - Gitea instance URL
|
||||||
|
GITEA_TOKEN - Gitea API token
|
||||||
|
HOOK_TOKEN - OpenClaw hooks auth token
|
||||||
|
|
||||||
|
Optional env vars:
|
||||||
|
GATEWAY_URL - OpenClaw gateway URL (default: http://127.0.0.1:18789)
|
||||||
|
POLL_DELAY - Delay between polls in seconds (default: 2)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import urllib.request
|
||||||
|
import urllib.error
|
||||||
|
|
||||||
|
GITEA_URL = os.environ.get("GITEA_URL", "").rstrip("/")
|
||||||
|
GITEA_TOKEN = os.environ.get("GITEA_TOKEN", "")
|
||||||
|
GATEWAY_URL = os.environ.get("GATEWAY_URL", "http://127.0.0.1:18789").rstrip(
|
||||||
|
"/"
|
||||||
|
)
|
||||||
|
HOOK_TOKEN = os.environ.get("HOOK_TOKEN", "")
|
||||||
|
POLL_DELAY = int(os.environ.get("POLL_DELAY", "2"))
|
||||||
|
|
||||||
|
|
||||||
|
def check_config():
|
||||||
|
missing = []
|
||||||
|
if not GITEA_URL:
|
||||||
|
missing.append("GITEA_URL")
|
||||||
|
if not GITEA_TOKEN:
|
||||||
|
missing.append("GITEA_TOKEN")
|
||||||
|
if not HOOK_TOKEN:
|
||||||
|
missing.append("HOOK_TOKEN")
|
||||||
|
if missing:
|
||||||
|
print(
|
||||||
|
f"ERROR: Missing required env vars: {', '.join(missing)}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def gitea_unread_ids():
|
||||||
|
"""Return set of unread notification IDs."""
|
||||||
|
req = urllib.request.Request(
|
||||||
|
f"{GITEA_URL}/api/v1/notifications?status-types=unread",
|
||||||
|
headers={"Authorization": f"token {GITEA_TOKEN}"},
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||||
|
notifs = json.loads(resp.read())
|
||||||
|
return {n["id"] for n in notifs}
|
||||||
|
except Exception as e:
|
||||||
|
print(
|
||||||
|
f"WARN: Gitea API failed: {e}", file=sys.stderr, flush=True
|
||||||
|
)
|
||||||
|
return set()
|
||||||
|
|
||||||
|
|
||||||
|
def wake_openclaw(count):
|
||||||
|
text = (
|
||||||
|
f"[Gitea Notification] {count} new notification(s). "
|
||||||
|
"Check your Gitea notification inbox via API, process them, "
|
||||||
|
"and mark as read when done. "
|
||||||
|
"Route all output to Gitea comments or Mattermost #git/#claw. "
|
||||||
|
"Do NOT reply to this session — respond with NO_REPLY."
|
||||||
|
)
|
||||||
|
payload = json.dumps({"text": text, "mode": "now"}).encode()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
f"{GATEWAY_URL}/hooks/wake",
|
||||||
|
data=payload,
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {HOOK_TOKEN}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
method="POST",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=5) as resp:
|
||||||
|
status = resp.status
|
||||||
|
print(f" Wake responded: {status}", flush=True)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(
|
||||||
|
f"WARN: Failed to wake OpenClaw: {e}",
|
||||||
|
file=sys.stderr,
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
check_config()
|
||||||
|
print(
|
||||||
|
f"Gitea notification poller started (delay={POLL_DELAY}s)",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
last_seen_ids = gitea_unread_ids()
|
||||||
|
print(
|
||||||
|
f"Initial unread: {len(last_seen_ids)} notification(s)", flush=True
|
||||||
|
)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
time.sleep(POLL_DELAY)
|
||||||
|
|
||||||
|
current_ids = gitea_unread_ids()
|
||||||
|
new_ids = current_ids - last_seen_ids
|
||||||
|
|
||||||
|
if not new_ids:
|
||||||
|
last_seen_ids = current_ids
|
||||||
|
continue
|
||||||
|
|
||||||
|
ts = time.strftime("%H:%M:%S")
|
||||||
|
print(
|
||||||
|
f"[{ts}] {len(new_ids)} new notification(s) "
|
||||||
|
f"({len(current_ids)} total unread), waking agent",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
wake_openclaw(len(new_ids))
|
||||||
|
last_seen_ids = current_ids
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
```
|
||||||
|
|
||||||
|
Run it as a background service (launchd on macOS, systemd on Linux) with the
|
||||||
|
env vars set. It's intentionally simple — no frameworks, no async, no
|
||||||
|
dependencies.
|
||||||
|
|
||||||
|
### The Daily Diary
|
||||||
|
|
||||||
|
Every day gets a `memory/YYYY-MM-DD.md` file. The agent appends a topic
|
||||||
|
summary to a table after every meaningful conversation:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Topics
|
||||||
|
|
||||||
|
| Time (TZ) | Topic |
|
||||||
|
| ---------- | ------------------------------------------------- |
|
||||||
|
| 14:30 | Discussed PR review workflow |
|
||||||
|
| 16:00 | Medication logged |
|
||||||
|
| 18:45 | Flight prep blocks created for tomorrow's flight |
|
||||||
|
```
|
||||||
|
|
||||||
|
This serves multiple purposes:
|
||||||
|
- Any session can see what's been discussed today without loading full
|
||||||
|
conversation history
|
||||||
|
- The agent can use `sessions_history` to get details on a specific topic
|
||||||
|
if needed
|
||||||
|
- During memory maintenance, the agent reviews these to decide what's worth
|
||||||
|
promoting to MEMORY.md
|
||||||
|
- It's a simple audit trail of what happened
|
||||||
|
|
||||||
|
### The Requirement Capture Rule
|
||||||
|
|
||||||
|
One of the most important rules: **every single requirement, preference, or
|
||||||
|
instruction the human provides MUST be captured in a file immediately.** Not
|
||||||
|
"mentally noted" — written to disk. Because the agent wakes up fresh every
|
||||||
|
session, a "mental note" is worthless. If it's not in a file, it didn't
|
||||||
|
happen.
|
||||||
|
|
||||||
|
This applies to everything: project rules ("no mocks in tests"), workflow
|
||||||
|
preferences ("fewer PRs, don't over-split"), corrections, new policies.
|
||||||
|
Immediate write to the daily file, and to MEMORY.md if it's a standing rule.
|
||||||
|
|
||||||
|
### PII-Aware Output Routing
|
||||||
|
|
||||||
|
A lesson learned the hard way: **the audience determines what you can say,
|
||||||
|
not who asked.** If the human asks for a medication status report in a group
|
||||||
|
channel, the agent can't just dump it there — other people can read it. The
|
||||||
|
rule: if the output would contain PII and the channel isn't private, redirect
|
||||||
|
to DM and reply in-channel with "sent privately."
|
||||||
|
|
||||||
|
This is enforced at multiple levels:
|
||||||
|
- AGENTS.md has a warning banner at the top
|
||||||
|
- The checklist system catches it before action
|
||||||
|
- Channel-specific rule files (like our `memory/JAMES_CHAT.md`) define what's
|
||||||
|
shareable per-channel
|
||||||
|
|
||||||
|
### Sub-Agent Isolation
|
||||||
|
|
||||||
|
When spawning coding sub-agents for PR work, each one MUST clone to a fresh
|
||||||
|
temporary directory. Never share git clones between agents — dirty working
|
||||||
|
directories cause false CI results, merge conflicts, and wrong file state.
|
||||||
|
The rule: `cd $(mktemp -d) && git clone <url> . && ...`
|
||||||
|
|
||||||
|
### The Heartbeat System
|
||||||
|
|
||||||
|
OpenClaw polls the agent periodically with a configurable prompt. The agent
|
||||||
|
reads HEARTBEAT.md and decides what to do. We keep HEARTBEAT.md small
|
||||||
|
(focused checklist) to minimize token burn on these frequent checks.
|
||||||
|
|
||||||
|
The heartbeat handles:
|
||||||
|
- Gitea inbox triage (check for new assignments)
|
||||||
|
- Flight prep block creation (look ahead 7 days)
|
||||||
|
- Open project review (can I take a step on anything?)
|
||||||
|
- Workspace sync (commit and push changes)
|
||||||
|
- Periodic memory maintenance
|
||||||
|
|
||||||
|
State tracking in `memory/heartbeat-state.json` prevents redundant checks
|
||||||
|
(e.g., don't re-check email if you checked 10 minutes ago).
|
||||||
|
|
||||||
|
The key output rule: heartbeats should either be `HEARTBEAT_OK` (nothing to
|
||||||
|
do) or a direct alert. Work narration goes to a designated status channel,
|
||||||
|
never to the human's DM.
|
||||||
|
|
||||||
|
### Putting It All Together
|
||||||
|
|
||||||
|
The system works as a loop:
|
||||||
|
|
||||||
|
1. **Session starts** → read SOUL, USER, daily-context, today's daily file
|
||||||
|
2. **Message arrives** → check daily-context for state changes (sleep gap?
|
||||||
|
location change? meds overdue?), respond to the message, update state
|
||||||
|
3. **Heartbeat fires** → check inbox, projects, flights, sync workspace
|
||||||
|
4. **External event** (Gitea poller wake) → process notification, respond
|
||||||
|
via appropriate channel
|
||||||
|
5. **Session ends** → state persists in files for next session
|
||||||
|
|
||||||
|
The files are the continuity. The agent is stateless; the workspace is not.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
1. [Workspace Bootstrapping](#workspace-bootstrapping)
|
1. [Workspace Bootstrapping](#workspace-bootstrapping)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user