Profile
Back to NewsBack
GitHub Trending 37 min
Reader Mode
domdoss/Warden: This is a fork of the https://dockbox.dev project I made

domdoss/Warden: This is a fork of the https://dockbox.dev project I made


⚠️ WARNING — READ THIS BEFORE YOU RUN ANYTHING ⚠️

This is insane software. It is probably not safe to run.

Warden is an AI agent with the same access as your user account. It executes shell commands, moves your mouse and types on your keyboard, drives your real browser with your real logged-in sessions and saved passwords, reads and sends your email, and can edit and restart its own source code. There is no sandbox and no container. A model mistake, a prompt injection from a web page it visits, or an email it reads can do anything you can do at a terminal.

!Safety warning modal shown on first dashboard launch

This is the warning the dashboard shows on first launch. It is not a joke and it is not boilerplate. Do not run Warden on a machine you care about unless you have read the code, understood the risks, and accepted that you are handing a language model the keys to your computer.

Now that that's out of the way — I'm currently running it on my desktop, just rawdogging the system, and it has been solid. sudo prompts pop up graphically (polkit catches them), and in practice it doesn't stray out of its workspace unless it's actively searching for files or the like. Is this stupid in theory? Yes, absolutely. Does it work in practice? So far, yes — for now. Just back things up from time to time, in case.


What is it?

Warden is a personal AI assistant that lives on your desktop. It runs local models through Ollama for fast, private tasks, and reaches out to cloud models for heavy lifting — all within a single conversation. It connects to your real browser, controls your desktop, manages your email and calendar, and talks to you through whatever channel you prefer.


Architecture

The Seat

A single LLM — Warden, the captain's first officer — runs the show, and it is the only thing you talk to. It both answers and does the work: the browser, the desktop, the shell, files and the web are its own hands, not a specialist's. It hands off only what genuinely belongs to another seat — email and scheduling to Iris, heavy code to Vulkan, audits to Artemis — and work too long for a chat turn to a background copy of itself.

This used to be two seats: a deliberately small router that never touched the internet, delegating every hands-on step to Atlas. That split cost a round trip and a brief on every action, and the router could only describe work it could not see. The router and Atlas are now one seat, running one model, with one prompt.

You → Warden ─ does it itself: browser, desktop, shell, files, web ─→ You
              └─ hands off what it does not own:
                   → Iris   (email, calendar, reminders, alarms)
                   → Vulkan (code and builds, background)
                   → Artemis(audit, background)
                   → Sentry (security scans, background — currently disabled)
                   → Council(deliberation)
                   → atlas_background (a copy of itself, for long work)
flowchart LR
    U[You] --> S["Warden seat"]
    S -->|"does it itself"| W["Browser / Desktop / Shell / Files / Web"]
    W --> S
    S -->|"WHAT brief"| I["Iris — email, calendar"]
    S -->|"WHAT brief"| V["Vulkan — code, builds"]
    S -->|"WHAT brief"| A["Artemis — audit"]
    S -->|"WHAT brief"| SE["Sentry — security (disabled)"]
    S -->|"WHAT brief"| C["Council — deliberation"]
    S -->|"WHAT brief"| BG["atlas_background — long work"]
    I --> S
    V --> S
    A --> S
    SE --> S
    C --> S
    BG --> S
    S -->|"report"| U
💡 Tools are not carried in context all the time. Every seat but Iris gets a per-turn ranked set — the always-needed core plus what the task actually calls for — out of a pool that now includes every installed MCP server. A capability that doesn't surface is one list_skills + activate_skill away, so nothing is lost; it just stops being paid for on turns that never touch it. Iris is exempt: it runs the small fine-tuned toolcall model, trained on exactly one fixed tool list, so a list that changed per turn would put it off-distribution.

A small model is enough — that's the whole point

This is the counterintuitive part: the seat can be the cheapest model in the stack, and that's by design. Most turns are classification and composition — what does the user want, which specialist owns it, what does that specialist need to know to start cold, is anything I'm babysitting going sideways? — plus hands-on work that a good small model already nails (open a page, run a command, edit a file). None of that needs a frontier model. An e4b handles it — locally, in well under a second per turn, on hardware you already own — so the thing you talk to most carries no per-turn cloud cost.

e4b works; 31B cloud is recommended. The e4b is the floor, and the whole point is that the bar for "good enough to run the show" is low — it replies, routes, and composes briefs. But supervision has a failure mode the e4b will hit eventually: when a sub-agent returns narration that looks like a result (a timestamp computation written as prose, an "I'll do that" with no tool call), an e4b seat will sometimes take it at face value and relay it to you as done. A 31B cloud seat reads the same non-result and notices nothing was actually produced. (Loop-killing is the one supervision job that doesn't depend on the seat's smarts at all — a deterministic churn detector handles it regardless of model size; see Babysitting the sub-agents.) So: run the e4b if you want it all local and free; run the 31B cloud if you want the babysitting to actually catch sub-agents that bluff.

The expensive generation splits two ways. Work the seat keeps — browser, desktop, shell, files — runs on the seat's own model. Work it delegates goes to the specialists, each on its own model: Vulkan, Artemis, and the three Council seats. A delegation is a self-contained WHAT brief — the seat states what needs to happen and stops, never prescribing how (no URLs, no search queries, no "go to X then click Y"). That discipline is what lets a small model supervise a frontier one without getting in the way: the brief carries the goal, and the specialist owns the execution.

Babysitting the sub-agents

Delegation is not fire-and-forget. When the seat hands long work to a background copy of itself, the job runs in the background — the seat gets a job ID back immediately and stays free to handle your next message. While those jobs run, supervision is on-demand, not automated:

There is no running-job supervisor. The periodic LLM self-audit watchdog that used to tick every few minutes and flag running jobs as on track / off-rails / crashed has been cut out — the ticker never arms, the tick function is a no-op, and the dashboard Supervisor row is now just the completion-verdict gate (model + on/off) — its cadence control is gone. It was unneeded: the seat's chain-following is now strong enough to drive a job end to end on its own — delegate → drain the finished-job inbox → confirm the result against the ask → chain the next step, or re-delegate on failure — so a second model watching the running job added false positives without value. And it was actively harmful on local hardware: a 3b model cannot reliably tell a slow read-or-compose from veering, so it kept flagging healthy background jobs (a read-first pass, a long idle while a 27b composes a large file) and the standing escalation rule killed them and handed the work to Vulkan — and even after the ticker was no-op'd, the seat's prompt still made it role-play supervisor flags about its own legitimate read-only delegations and stop them. You supervise on demand: ask the seat how a job is doing and it looks (list_running_agents / agent_logs / read_job_result), assesses the recent calls itself, and can stop_agent + re-delegate on its own judgment. It just no longer wakes itself up to do it. (The one thing that still runs on the supervisor model is the completion verdict on finished jobs — see below.)

A deterministic churn detector. The thing that watches a looping job is in code, not the LLM. The runner counts consecutive read-only calls (Read, Grep, Glob, Bash, WebSearch, …) with no deliverable produced. At 12 in a row it injects a commit nudge into the agent's next turn — "you have enough context, stop searching, write the deliverable now" — and re-nudges every 10 further read-only calls. It only nudges: the runner never aborts a job on a call count and never auto-escalates to Vulkan. What happens next is the orchestrator's call — it reads the job's status and decides on its own judgment to nudge again, stop_agent it, or let it run. Artemis and Sentry are exempt — read-only is their job, not a loop. (Full mechanics: docs/notes/supervision-and-churn.md.)

When a job finishes, the result lands in an inbox.

Crucially, supervision stays out of your chat. The churn detector's commit nudge is injected into the agent's own turn, not your conversation. Progress lives in the dashboard instead: the real status line each job emits on every tool call streams into a grouped, collapsible Live Activity panel (one summary line when collapsed, the recent history when expanded), so you watch what's actually happening without a parade of chat bubbles. The chat only carries completed-task reports (and interventions) — you ask once, the seat drives the whole chain end to end, and you hear from it when there's something finished to tell you.

The inbox is the backbone of the async model. Finished jobs drop their full output there, and at the end of each turn the seat drains it, digests what actually matters in its own voice, and chains any follow-up work the results call for — so a multi-step ask (plan → council → revise) runs end to end without you having to say "and?" or "continue" between steps. If a job failed, the failure routes back to the seat automatically — it reads the full output, works out what went wrong, and re-delegates with a reworked brief (a different approach, a corrected URL, a missing detail — whatever the output showed was broken). You only hear about a failure if it can't be recovered; after the same task has failed the same way twice, the seat stops retrying and tells you instead. Urgent results can even interrupt a turn mid-flight, so a finished job you're waiting on never sits behind whatever else happens to be running.

The net effect: you ask once, and the seat owns the outcome — prompting the specialists, supervising them, cutting off the ones that drift, and correcting course until the job is done or it's genuinely stuck.

flowchart TD
    A["You ask once"] --> B["Seat dispatches a WHAT brief"]
    B --> C["Job runs in the background"]
    C --> D["Finished job lands in the inbox"]
    D --> E["Seat drains the inbox"]
    E --> F{"Result matches the ask?"}
    F -->|"yes"| G["Chain next step, or report"]
    F -->|"no"| H["Re-delegate with a reworked brief"]
    H --> C
    G -->|"more to do"| B
    G -->|"done"| I["Report back in its own words"]

One conversation, one voice

You have one conversation, with one assistant. Vulkan, Iris, and the rest never see your messages and never speak to you — the seat is the only voice in the chat. Most of what you ask, it does itself with its own tools; when work belongs to another seat, it composes a self-contained brief for the right specialist, and reports back in its own words when the work is done.

!The seat composes a clean WHAT brief before handing work to a specialist

Your raw message never reaches a specialist. When you ask for something the seat does itself — "hey can you set the volume to like fifty percent" — it just does it: no rewrite, no middleman. Only work that belongs to another seat — email to Iris, heavy code to Vulkan, an audit to Artemis — gets handed off, and then as a self-contained WHAT brief: the goal in the seat's own words, typos and slang resolved, so the specialist starts from a clean statement of what's needed instead of guessing at your phrasing.

Sub-Agents

Each sub-agent has its own system prompt and toolset. Iris and Sentry share one model (the dashboard's Toolcall model); Vulkan, Artemis, and each Council seat keep their own, and a background copy of the seat runs on the seat's own settings. They don't share context — the seat composes a self-contained task string with everything the sub-agent needs.

| Agent | Model | Tools | Role | |-------|-------|-------|------| | Atlas | Local or cloud | Shell, browser (DOM control), desktop, files, web search/fetch, documents | Execution — anything that touches the internet or runs commands. | | Vulkan | Local or cloud | Read, Edit, Grep, Glob, Bash, build & test runs | Coding, scripting, building, heavy bash — editing source, running builds and tests, refactoring, complex shell pipelines. Runs in the background like Atlas. | | Iris | Local or cloud (local recommended) | Email, calendar, contacts, todos, scheduling, projects & work tasks | Personal information management — email digests, calendar events, scheduled tasks (cron, interval, once), projects, deliverables, and work tasks. | | Artemis | Local or cloud | Read-only file access | Critical review — audits conversations and decisions. | | The Council | 3×, local or cloud | Read-only file access | Three independent seats (Skeptic, Pragmatist, Synthesist) deliberate in parallel on high-stakes decisions. | | Sentry | Local or cloud (local recommended) | Bash (read-only), sentry_report | Background security scanner — currently disabled (SENTRY_ENABLED=false). When enabled it would inventory the PC read-only (listening sockets, network connections, running services, autostart, user crontab, enabled units, processes), diff against a learned baseline, and report only when something new or suspicious appears. It's off until the fine-tune has sentry examples to drive a real inventory — a stock model fabricates one, so re-enable only after training sentry rows. |

🎛️ The Atlas seat, Vulkan, Artemis, and each Council seat have their own model. Iris and Sentry share one Toolcall model (a single model + ctx row in the dashboard). Pick local Ollama or cloud per role — the same pipeline handles both. With max_loaded_models=3 (see Tuning the Ollama daemon), the Atlas seat (always kept alive) and the Toolcall model (if its keep-alive checkbox is on — this covers Iris/Sentry) stay resident in VRAM, with room for a third resident model (a separately-enabled model whose keep-alive is on). The supervisor model no longer holds a resident slot — its running-job tick was cut out; it only runs the occasional completion verdict on finished jobs. Any fourth model evicts the least-recently-used resident. The keep-alive re-pin that runs every minute only maintains models that are loaded — it snapshots Ollama's /api/ps first and never re-spawns a model that was deliberately unloaded, so a pinned model can't fight something big (like the training loop's 17 GB analyst) for VRAM after you cleared it on purpose.

!The Agents panel: every sub-agent with its model, status, and toolset

!The Council: three seats deliberate in parallel on a decision

!A Council verdict is returned to the seat

⏰ Scheduling — Iris

Iris owns the scheduling and calendar side of personal information management. It creates and manages schedule entries and calendar events — it never executes the scheduled tasks.

The seat owns the intent; Iris owns the timing. When something needs to happen later, the seat gives Iris a prompt (what to run) and a when (the timing intent). For scheduling, Iris's sole job is to translate that into one flawless schedule entry and hand it to the scheduler. Nothing more. When the ask is an appointment rather than a fire-later task, Iris writes a calendar event (start/end time, location) the same way.

What Iris does:

  • Picks the right schedule_type — cron (recurring at specific times), interval (every N ms), or once (a single future timestamp) — and writes the schedule_value in its exact format.
  • Does the time arithmetic in your local timezone, walking the offset digit by digit and verifying computed-time minus now equals the requested interval before committing.
  • Stores the prompt verbatim — at fire time that prompt is injected into the running chat as a message from "Scheduler", and the seat runs it like any other message, with full context and all its tools. Iris set up the schedule; the seat does the work.
  • Manages the lifecycle of existing entries — list, pause, resume, cancel, update.
  • Creates, lists, updates, and deletes calendar events (appointments with start/end time, location) — the calendar side of timing.
  • Everything else personal: email, contacts, todos, projects, and work tasks.
What Iris does not do:
  • It does not execute the scheduled task. Ever. It writes the entry and stops.
  • It does not gather data or do research — if a scheduled prompt needs facts (a price, a status, a number), the seat gathers them first and hands Iris the result to bake into the prompt.
  • It does not diagnose why a task did or didn't fire — that's Artemis's job. Iris only touches the entry if it needs fixing or recreating.
Model: basic structured output — a small local model (granite) is plenty. The reliability lives in the prompt and the format validation, not in a big model.

The schedule-value format is where scheduling breaks in every system that has one, so Iris is built to be obsessive about it: it validates the cron expression, rejects malformed intervals and timestamps, refuses timezone suffixes on once, and double-checks its own offset math. The point is that the entry is correct the first time, every time, on a model that costs nothing to run.

🪨 The toolcall agents (Iris, Sentry) are prompted for granite4.1:3b. Their system prompts are tuned to that 3B model — temperature 0, deterministic keyword→tool rules, and no few-shot examples (granite pattern-matches example shapes: shown only schedule_task(...) examples, it would call schedule_task to "delete" instead of cancel_task). When editing any of these prompts, keep that target in mind: drive behavior with explicit rules and tool-selection mappings, never examples, and verify against granite4.1:3b — a prompt that reads cleanly on a big cloud model can mis-fire on the 3B local one.
⚠️ Stock granite doesn't cut it — the fine-tuned toolcall-ft model is effectively required. The toolcall agents need the LoRA fine-tune built from training/ (toolcall-ft in Ollama; see training/README / Modelfile.toolcall-ft for building it). Reliable tool-call transcription on this action-parameter surface (one tool per noun — email, task, calendar, alarm — each with an action enum, collapsed from 41 flat tools in 2026-09) was trained in — a stock model won't reproduce it. On a fresh install without the LoRA weights it just wouldn't work, even with the 8B stock model swapped in.

Persistent Runner

🔥 The agent-runner is a persistent child process — no Docker, no containers, no cold starts between messages. It stays warm for hours (configurable IDLE_TIMEOUT), keeping MCP servers connected and skills loaded. Follow-up messages route over IPC in milliseconds.

🧠 Prompt Engineering

This is the feature that makes Warden work. The system prompt isn't a paragraph of vibes — it's a carefully engineered control surface that has been iterated on extensively.

🎯 Delegation Discipline

The seat is trained to state WHAT, never HOW, in every delegation. It doesn't prescribe URLs, search queries, or step-by-step instructions for the specialists. The system prompt explicitly forbids it:

"Atlas is the internet model. It runs on a larger, more capable model than you. Never tell Atlas how to use the internet — no URLs, no search queries, no 'go to X then click Y.' Give it the goal and the facts, and stop."

This is reinforced at three layers: the seat's system prompt, the delegate tool descriptions (what the seat sees when deciding to call them), and the specialists' own system prompts (which tell them to ignore prescribed steps).

🎯 Driving Forces

The seat's reasoning posture is a dashboard dropdown, not a fixed trait. A Driving Force preset rewrites the seat's preamble with a different bias — and switching it clears the seat's context, so the new persona starts a fresh conversation instead of inheriting the old one's thread. The built-in presets:

| Preset | Bias | |--------|------| | ship-it | Optimize for getting it done; accept a good-enough result in hand over a perfect one that isn't. | | architect | Think in systems and long-term structure before acting. | | debugger | Assume something is broken; trace the real data flow before changing anything. | | first-principles | Reason from fundamentals instead of analogy. | | mentor | Explain and teach rather than execute. | | socratic | Ask the user sharp questions instead of guessing. | | paranoid-reviewer | Assume the worst; double-check every claim before trusting it. | | staff-engineer | Own the outcome end to end and weigh trade-offs explicitly. |

The presets are plain markdown in data/driving-forces/ — add your own and it shows up in the dropdown. Empty selection is the built-in default preamble.

🧵 Fabric Pattern Integration

Warden ships with 258 expert prompt patterns from the Fabric library. Every turn, the user's message is keyword-extracted and the top 5 most relevant patterns are injected into the system prompt by name and description. The seat loads the full pattern on demand and bakes its framing into its own work and the briefs it hands out — structure without micromanaging the execution.

🎲 Dynamic Tool Selection

Warden is built to host many tools at once — the core set plus anything you add via skills and MCP servers — so the tool surface had to scale without bloating every prompt. Not all 30+ tools go into every turn. Keywords from the conversation are extracted and tools are ranked by relevance; the core routing tools (sub-agents, Read, Bash) are always included, and everything else is surfaced only when relevant. This keeps the context window lean, the model focused, and the system futureproof — add a new tool and it's available without rethinking the prompt.

!Skills & MCP panel: dozens of toggled capabilities

🛡️ Defensive Loop Patterns

The tool loop has multiple circuit breakers to prevent common failure modes:

  • Intent-without-action detection — if the model keeps saying "I'll do X" without actually calling tools, it gets nudged (capped at 2 nudges)
  • Circling detection — consecutive useless rounds (no tool calls, no output) trigger a forced no-tools round to extract an answer
  • Degenerate output filter — word-mash / garbled output from misconfigured models is detected and suppressed
  • Verifier sub-agent — after effectful work (file writes, edits), a verifier pass confirms the changes
  • Deterministic churn detection — N consecutive read-only calls (Read/Grep/Glob/Bash/WebSearch — the RESEARCH set) with no deliverable produced → inject a "stop searching, write now" commit nudge, re-nudged every 10 further read-only calls (12/10 thresholds). It only nudges — the runner never aborts on a call count and never auto-escalates; the orchestrator decides (nudge / stop_agent / let it run) on its own judgment. Artemis and Sentry are exempt (read-only is their job). (See docs/notes/supervision-and-churn.md.)
  • Per-delegate concurrency cap + same-file backstop — at most 3 jobs per delegate run at once (MAX_CONCURRENT_PER_DELEGATE); a further dispatch for the same delegate is queued and starts when a slot frees. Separately, a dispatch that names a file a running writer already holds is queued behind it, so two jobs can't clobber the same file.
  • Atlas first-turn thinking — Atlas thinks on its first iteration only (plan what it needs, then act), paired with a READ ONCE prompt rule (read each named file once; don't re-Read it to find the next edit target — that re-reading is the loop). Later iterations keep thinking off to preserve context.
  • Supervisor watchdog (cut out) — the periodic LLM self-audit that flagged running jobs and let the seat nudge/stop has been removed. It was unneeded once the seat's chain-following could drive a job end to end on its own, and a 3b model kept false-flagging healthy slow reads/composes. The seat still monitors on demand (ask it, and it looks + can stop/retry on its own), and the completion verdict still judges finished jobs. Nothing stops a job on a call count — the deterministic churn detector only nudges; the only code-enforced stop is a 3-hour wall-clock kill, and otherwise the seat (on demand) or you decide when to stop_agent.

📝 Memory System

The seat writes directly to MEMORY.md, TODO.md, and HEARTBEAT.md — no delegation needed. Only MEMORY.md is loaded into context every turn; HEARTBEAT.md is injected only for scheduled heartbeat tasks, and TODO.md is not auto-loaded.

After every conversation, a memory writeback pass runs automatically: a local model reads the last ~30 messages of the chat, distills durable facts (preferences, decisions, context the agent should carry forward), and appends them to MEMORY.md with a dated entry in JOURNAL.md. The distilled facts are visible to the agent on the very next turn — no manual note-taking, no "remember this" prompts. The writeback is fire-and-forget (never blocks the message loop), throttled to once per chat per 15 minutes, and auto-compacts MEMORY.md when it grows too large. Both files live at WORKSPACE_ROOT — the same place the seat loads MEMORY.md from every turn, and the same place Mercury writes MERCURY_MEMORY.md. (JOURNAL.md lives there too but is never injected into the prompt.)

🪞 Mercury — rolling conversation memory

Mercury is the working-memory layer for the live conversation — how a fresh seat (after a restart or context clear) knows what the current thread is about without replaying an ever-growing transcript.

  • Rolling compaction. On a turn-based cadence (every 30 user turns — MERCURY_TURNS_PER_COMPACTION — never mid-turn), a local model reads the recent conversation, keeps the newest ~30 messages verbatim (MERCURY_RECENT_MESSAGES), and compresses the rest into a concise state-of-the-conversation note written to MERCURY_MEMORY.md. Every turn's prompt starts with that summary merged into the system prompt, cache-prefix-stable.
  • RAG over recent history. A follow-up that references something older than the verbatim window ("what did I say about the filter two hours ago?") pulls keyword-matched turns from the last ~120 messages into a block. Retrieval is deliberately conservative, because raw old turns injected carelessly once hijacked a turn — the model answered a fresh question by replaying a day-old exchange verbatim, matched on a single shared keyword: a query with 3+ keywords must now match ≥2 of them, score ties break toward the newest message, every retrieved line carries its timestamp, and the block is labeled background-only with the current ask explicitly prioritized.
  • Filed to MARM when present. Each compaction also files the summary into MARM (session mercury, marm_log_entry, deduped on unchanged text) so the rolling state stays semantically recallable even after the 120-message horizon passes. Fire-and-forget and fail-open: MARM down or absent changes nothing — Mercury and its RAG work standalone and are always the default recall path.

🧬 MARM — semantic long-term recall (default-on)

MARM Memory (by Lyellr88) is the default long-term memory server: local-first recall beyond what MEMORY.md can hold. Warden runs identically without it — every MARM path is fail-open — but with it installed you get:

  • Nothing is forgotten. MEMORY.md auto-compacts and drops older lines; MARM keeps every distilled fact the writeback has ever logged, recallable by relevance.
  • Semantic recall, injected automatically. Every turn, the runner asks MARM for the memories most relevant to the incoming message and injects the top hits into the seat's prompt — recall doesn't depend on the model remembering to ask. Hybrid BM25 + embedding search, so "why did the streaming app white-page" finds the memory written as "router DNS hijack blocked stremio.com." The seat can still call marm_smart_recall directly for a deeper dig.
  • Linked ideas. MARM's concept graph connects the logged facts into entity → predicate → entity relations, so separate ideas are pieced together instead of sitting in a flat list.
Default-on since 2026-09-22. install.sh installs the server (uv tool install marm-mcp-server; requires uv), provisions a marm-memory.service systemd --user unit (MCP HTTP at 127.0.0.1:8001, wired into warden.service via the warden.service.d/marm.conf drop-in so it starts with Warden), and registers the marm stdio entry in data/mcp-servers.json with the absolute binary path (systemd's minimal PATH can miss ~/.local/bin; a fresh install's seeded config uses marm-memory from PATH and upserts to the absolute path on install). Opt out with INSTALL_MARM=0 ./install.sh and/or disable the marm entry in data/mcp-servers.json. Optional: pin the concept-graph topic model with MARM_TOPIC_MODEL= install.sh when the server default isn't right for your models (ollama list).

The seat is granted mcp__marm__* tools — the one MCP server it calls directly, since recall is assistant state, not hands-on work. The # LONG-TERM RECALL prompt section is emitted only when the marm entry is enabled, so a disabled MARM never leaves the model advertised tools it doesn't have. Data lives in ~/.marm/.

How it integrates (all built in, none of it requires touching Warden's core):

  • The memory writeback mirrors every distilled fact into MARM (marm_log_entry, over MCP HTTP) right after appending to MEMORY.md — fire-and-forget, with a 10s timeout: MARM down or absent changes nothing. In the other direction, marm-recall.ts auto-recalls the top MARM matches for each incoming message (2.5s timeout, ≤3 hits, ≤900 chars, fail-open) and injects them into the seat's prompt as a # RECALLED MEMORIES section.
  • Mercury compaction files each rolling summary into MARM as well (marm_log_entry, session mercury, deduped on unchanged text) — the conversation's recent state becomes long-term recallable under the same fail-open contract.
  • The voice app's center panel is the memory galaxy (eyes_ears/ui/memory-galaxy.html): a 3D constellation of MARM's concept graph — nodes are entities extracted from stored memories, edges are walked relations. Clicking a node opens its details in a separate popup window. The built graph is cached (15 min), so a UI restart doesn't re-probe MARM. With MARM off or the concept graph unbuilt, the galaxy falls back to its built-in demo constellation.
flowchart TD
    S["Seat"] -->|"loaded every turn"| M["MEMORY.md (TODO.md and HEARTBEAT.md are not auto-loaded)"]
    W["memory writeback"] -->|"distills durable facts"| M
    W -->|"mirrors facts"| MARM["MARM — semantic long-term recall"]
    MARM -->|"top recalls injected"| S
    S -->|"rolling compaction"| MC["Mercury — MERCURY_MEMORY.md"]
    MC -->|"filed on compaction"| MARM

💓 Heartbeat

HEARTBEAT.md holds standing instructions the agent executes on schedule via the task scheduler — no prompt from you required. Edit it from the dashboard's Heartbeat panel (or let the agent edit it itself) and the instructions run automatically, giving the agent persistent autonomous behavior between conversations.

Under the hood, the heartbeat is a real scheduled task (heartbeat-owner) persisted to heartbeat.json and synced to the task scheduler. The dashboard's Save button writes your instructions, creates or updates the cron row, and mirrors the content to HEARTBEAT.md so runTask injects it into the prompt. The task is seeded paused on boot — it's always visible in the Ops Scheduled tab, and you enable it with one click. Pausing via the Ops tab reflects immediately in the dashboard toggle; resuming re-activates the cron. The cron defaults to hourly at :45 (offset from the iris-digest crons so they don't pile up on the same minute).

!Heartbeat panel: scheduled instructions the AI executes automatically

🗜️ Context Compaction

The seat keeps its running context lean by design, not by a giant ceiling. After every turn its persistent conversation collapses to chat-history-only — the last few turns' final responses (~1K each) — and drops all the tool calls, tool results, and system injections that produced them. Mercury is pinned; long-term memory isn't relied on for the working window. Trimming is group-aware (it drops whole tool-call→result groups, so it keeps fewer messages without orphaning a tool result), and each tool result is capped at ~1K tokens. Two manual resets sit on top:

  • New Thought (chat header) clears the seat's context server-side — the next message starts a fresh conversation, no accumulated history.
  • Clear Conversation (chat header) goes further: it deletes the stored message log for the chat too, wipes the rolling Mercury summary, and sets the context-clear marker — the next message starts from a genuinely empty history, database included.
  • Idle auto-clear (Settings → Model Configuration → Idle clear, default 30 min) drops context automatically when your last message was older than the threshold. 0 disables it.

✏️ Self-Editing

The agent can modify its own source. A built-in self-edit skill constrains edits to src/ and container/agent-runner/src/, runs npm run build, gates on a successful compile, tells you what's changing, then restarts the service with systemctl --user restart warden. It refuses to touch dist/, configs, or the systemd unit, and never restarts on a failed build — so the agent can ship its own fixes without you opening a terminal.

A companion update-warden skill does the upstream equivalent: it fetches origin/main, shows what's new, merges, runs npm run build, and — with the same gate — restarts only if the build succeeded (exit 0), verifying systemctl --user is-active warden prints active afterward. It refuses to run on a dirty tree and aborts the merge on unresolvable conflicts, leaving the service on the previous dist/ until you fix things. Ask "update warden" to trigger it.


☁️ Hybrid Model Architecture

Warden is built for hybrid operation from the ground up. Different tasks need different models, and you shouldn't have to choose one and stick with it.

⚙️ How It Works

Every model selection in the dashboard is per-role:

| Role | Recommended | Why | |------|-------------|-----| | Atlas (the seat) | e4b local works; 31B cloud recommended | Fast, cheap routing + hands-on work + supervision. e4b is the floor; 31B cloud catches sub-agents that bluff a result. Keep-alive is on by default. | | Supervisor (watchdog) | Small local (e.g. granite4.1:3b) | The running-job self-audit tick is cut out (see above). The model still runs the completion verdict on finished jobs — tool-less, a few hundred tokens per call, falls back to the seat model if unset. The dashboard Supervisor row now just gates that completion verdict (model + on/off); its periodic tick/cadence control is gone. | | Atlas | Cloud (deepseek, glm) | Heavy lifting — internet access, shell, browser, complex reasoning. Keep-alive optional. | | Vulkan | Cloud (default) | Coding, builds, tests, refactoring, heavy shell pipelines. Keep-alive optional. | | Toolcall agents | Local (recommended) | Iris and Sentry share one model + ctx row. Run them local; save cloud for Atlas and the Council. | | Artemis | Cloud (default) | Read-only audit. Keep-alive optional. | | Council seats | Cloud ×3 (different models) | Diverse perspectives for deliberation. |

Every role can be flipped to local or cloud from the dashboard — same pipeline, no code change — so the column above is a recommendation, not a constraint.

⚠️ Kimi is not recommended right now. Kimi loops on read-heavy / long-running subagent tasks (atlas, vulkan, artemis) — it re-issues the same read-only call repeatedly instead of converging, and it leaks its chain-of-thought as untagged text when thinking is disabled. (Thinking is decided per model by its advertised Ollama capability, not by name — a model that advertises thinking gets think:true, anything else runs think:false and falls back cleanly if the server rejects it.) The deterministic churn detector still catches read-only-call loops on kimi, but artemis is churn-exempt, so a looping kimi-artemis runs to the iteration cap unchecked. The default fleet is all-local (atlas = orchatlas-ft, the toolcall sub-agents = local Granite, Vulkan = qwen3-coder-next); cloud kimi/glm remain available per-role if you want them.

All of this is configured from the dashboard's Settings panel — assistant name, model per role, Ollama URL, and keep-alive toggles:

!Settings panel: assistant name, model configuration per role, Ollama URL, and keep-alive toggles

!Local model settings: pick a local Ollama model per role

!Thinking-mode dropdown in the web settings panel

🔄 One Pipeline, Local or Cloud

There is no separate infrastructure for cloud models. Ollama serves both local models (on your machine) and cloud models (remote Ollama-compatible endpoints) through the same HTTP API — so every agent can be flipped between local and cloud from the dashboard with no code or infrastructure change. You're just picking a model id. The agent-runner talks to Ollama directly — no proxy in the path by default.

🧭 Model Routing

The agent-runner speaks Ollama's native HTTP API and talks to Ollama directly — local models at localhost:11434, cloud models at their Ollama-compatible endpoint, model picked per role in the dashboard. No proxy in the path. The agent doesn't know or care whether the model is local or cloud; same format, same tools, same conversation.

!Friendly model names: cloud models shown by a readable name, not a raw id

!Servers menu: manage local and cloud Ollama endpoints

Optional — piping in Claude: src/credential-proxy.ts (port 3001) is in the codebase but not wired in by default. It exists for one case: routing to Anthropic's Claude. It translates Ollama-native requests ↔ Anthropic format and injects the Claude API key so the agent-runner never sees it. If you want Claude, wire the proxy in and point the agent-runner at it; otherwise everything stays on native Ollama.

Supervisor model & per-iteration thinking. The running-job self-audit tick is cut out; the completion verdict still runs on a separate SUPERVISOR_MODEL (falls back to the seat model) — a small, tool-less call, so a local model is ideal. Sub-agents think on a per-iteration rule: Atlas thinks on its first turn only (plan, then act — paired with the READ-ONCE prompt rule against the re-reading loop); thinking is otherwise capability-driven — a model that advertises thinking to Ollama may think on its turns, one that doesn't runs think:false and retries cleanly without it if the server rejects it; every other sub-agent iteration is think:false. The seat has its own dashboard thinkingMode (max = every turn, true = first turn, off otherwise) and thinks on turn 1 by default. Granite models don't advertise thinking, so the capability check keeps the Granite toolcall agents (iris/sentry) on think:false automatically.


🌐 Real Browser Automation

Warden browses through your actual Chrome — the one you're logged into, with cookies, sessions, and extensions intact. There is no Warden-owned browser, no headless puppet, no CDP port: the single path is a Chrome extension bridge.

The chain: the chrome-mcp-server extension (unpacked copy at chrome-mcp-server-1.0.0/) runs in your real Chrome and spawns the mcp-chrome-bridge native host (npm install -g mcp-chrome-bridge, serving MCP on 127.0.0.1:12306). Warden's browser gate (src/browser-mcp-gate.ts) owns the single upstream connection to that host and re-serves it statelessly at POST /mcp/browser on :3200 — the agent-runner never touches 12306 directly.

The browser is an optional component that never blocks anything. It's dialed on demand — only when a browser/web tool is actually invoked — and the gate fails fast (5s) if the bridge is down, so no request can hang on it. Warden boots, chats, and runs every non-web turn with the bridge completely absent.

flowchart LR
    A["Chrome MCP Server extension"] -->|"connectNative"| B["mcp-chrome-bridge :12306"]
    B -->|"single upstream connection"| C["Warden gate :3200 — POST /mcp/browser"]
    C -->|"chrome_* tools"| D["agent-runner (browser + youtube tools)"]

For the full contract (lazy connect, fail-to-default toolsets, the diagnostics ladder, and the one-command repair via scripts/restart-browser-bridge.sh), see CLAUDE.md → Browser capability.


🖱️ Desktop Control

Warden controls your actual desktop — mouse movement, keystrokes, window management. Wayland via ydotool, X11 via xdotool. It discovers your display environment automatically, even when started from systemd with no DISPLAY set.


🧰 The Custom Toolset

Almost none of Warden's tools are generic wrappers — they're purpose-built, one layer above the real system, and they work out of the box because they bind to what's already there: your real Chrome profile, your real audio hardware, your real CalDAV calendar, your real media players. No adapters to configure, no credentials to paste twice, no "integration setup."

The design rule for the action tools is one tool per noun, one action parameter. Iris's toolcall surface is four merged tools — email, task, calendar, alarm (a 2026-09 collapse of 41 flat tools). project is a separate merged tool (kind + action over projects, work tasks, deliverables, blockers, priorities and financials) that the seat calls directly, not Iris. Each is a single schema whose action selects the operation. Small models stay in-distribution, and every new operation is one enum value, not a new tool.

🌐 Browser automation (via the extension bridge)

The live surface is the extension's own chrome_* toolset (~27 tools: chrome_navigate, chrome_read_page, chrome_computer, tab management, key presses, …) served through the Warden gate. The youtube tool (container/agent-runner/src/tools/youtube.ts) drives pages through the McpPage adapter on top of it — navigate + script-eval are the required pair, with tab-list and key-press tools upgrading fidelity when present. If the bridge is down, the tool reports that as a terminal fact and the turn ends — browser failure never takes down chat or any other capability.

🖱️ Desktop & apps

desktop_screenshot · desktop_click · desktop_type · open_app — sees and steers native apps (settings windows, media players) with pixel coordinates, auto-detecting Wayland vs X11; open_app fire-and-forgets files to their OS-default app.

🔊 Media & audio

audio_volume (speakers) · mic_volume · media_control (play/pause/skip) — anything exposing MPRIS: a browser YouTube tab, Spotify, mpv, VLC. Dedicated tools, not amixer/playerctl shell noise.

youtube — play, search, and control the real Chrome YouTube tab through the browser bridge: now-playing, pause/resume, next, seek, fullscreen. Any ask mentioning YouTube routes here and the tool result is the reply; a "stop the music" ask maps to action: pause, and the prompt's control rule forbids ever stating a playback change without the call that made it — that exact zero-tool-call hallucination (seat claimed "paused" while the music kept playing) is what the mapping was added for (2026-09-21).

👁️ Vision

query_image — ask questions about any image on disk, answered locally. read_image + webcam_capture + desktop_screenshot feed frames into the agent's vision context automatically.

✉️ Communications

email (read/send/cache, date-windowed) — your real IMAP inbox · contacts — full CRUD (list_contacts, search_contacts, get_contact, create_contact, update_contact, delete_contact) · calendar — CalDAV events, Kontact/Google-synced.

⏰ Scheduling & time

alarm · task (schedule/list/update/pause/resume/cancel — cron, interval, one-shot) · start_timer · stop_timer · log_time — durations are parsed host-side (ISO-8601 PT2M), never by the model doing clock math. Fired reminders land in chat directly, and the Alarms view shows every once/cron/interval job including fired ones.

📋 Projects, work & todos

project (kind + action over projects and work tasks) · list_todos · create_todo · complete_todo · delete_todo.

📄 Documents

generate_pdf — build a PDF from scratch · convert_file — DOCX/XLSX/HTML/markdown conversions, no external services.

🧠 Context & chat plumbing

get_chat_history · attach_file (deliver a generated file to the user) · send_message · clear_context — the agent manages its own thread weight.

🛡️ Sentry

sentry_report — the software-security scanner (read-only sockets/services/crontab audit, one inventory submission, host logs the scan and relays findings). Currently disabled: SENTRY_ENABLED=false in the agent-runner and SENTRY_SCANS_ENABLED=false on the host, because the fine-tune has no sentry SFT examples and fabricates its inventory. The tool is registered but reports Sentry offline.

🔌 The rest

api_request + list_api_keys — your own stored provider credentials, used directly · fabric_pattern — Daniel Miessler's fabric patterns hot-plugged into prompts · Bash — a persistent shared shell where cd sticks.

Every tool above is registered with a toolset and routed to agents by role (Dynamic Tool Selection) — vulkan gets code tools and physically cannot screenshot-loop, sentry gets read-only audit tools and nothing else, atlas gets the browser and the media hardware.


📊 Dashboard

A full PWA at http://localhost:3200. Open it and you're in — single-user, the full dashboard is right there.

!The Warden dashboard

The dashboard includes:

| | | | |---|---|---| | 💬 Chat | Main conversation interface | 🗂️ Projects | Deliverables, blockers, financials | | 📁 Files | Browse, upload, download, manage | ⏰ Scheduled Tasks | Cron/interval/once automation | | 🔑 API Keys | Provider credentials | ⏰ Alarms | Reminders with sound + desktop notify | | 💓 Heartbeat | Standing instructions on schedule | ⚡ Actions | One-touch prompt buttons | | ✉️ Email | IMAP inbox + send | | 🎤 Talk | Voice transcription | 📝 Notes | Obsidian-style markdown vault | | 📅 Calendar | CalDAV synced with Kontact | 🧩 Skills & MCP | Hot-pluggable

... (README truncated for length)

Chat with me