Day 21: Agent Failure Modes & Debugging Techniques 🧨🔍

Executive Summary

Most teams don’t notice agent failures — they experience symptoms:

  • agents looping endlessly 🔁
  • confidently wrong answers ❌
  • unexpected API bills 💸
  • agents that “work in demos” but fail in production 🚨

Agentic systems fail differently from traditional software and even from standard ML systems.

This chapter is about:

  • why agents fail
  • how to detect those failures early
  • how to debug systems that reason, plan, and act autonomously

Debugging agents is not about fixing bugs — it’s about correcting behavior under uncertainty.

Why Agent Failures Feel So Confusing 😵‍💫

Traditional systems fail because:

  • logic is wrong
  • data is missing
  • infrastructure breaks

Agents fail because:

  • reasoning goes off-rails 🧠
  • goals drift 🎯
  • assumptions compound
  • feedback loops amplify mistakes

The system is doing exactly what you allowed it to do — just not what you intended.

That’s why agent debugging feels psychological as much as technical.

A Simple Mental Model: Where Can an Agent Break? 🧩

Think of an agent as five layers:

Intent → Plan → Tools → Memory → Feedback

A failure in any layer propagates forward.

We’ll walk through each layer with:

  • common failure modes
  • what it looks like in real systems
  • how to debug it

1️⃣ Intent Failures (Goal Misalignment) 🎯

What Happens

The agent misunderstands what success actually means.

Real Symptoms

  • solves the wrong problem
  • optimizes for speed instead of accuracy
  • focuses on formatting over substance

Example

User asks:

“Analyze why customer churn increased last quarter.”

Agent responds with:

  • a generic churn definition
  • no analysis of this company’s data

Why This Happens

  • vague system prompts
  • overloaded instructions
  • missing constraints

How to Debug

✅ Make intent explicit:

  • define success criteria
  • define non-goals

✅ Add a clarification step:

If the goal is ambiguous → ask before acting

2️⃣ Planning Failures (Bad Decomposition) 🧠🧱

What Happens

The agent creates a plan that is:

  • incomplete
  • poorly ordered
  • logically flawed

Common Symptoms

  • skipping critical steps
  • doing expensive steps too early
  • circular plans 🔄

Example

Research agent:

  1. Summarizes articles
  2. Then searches for sources

Clearly backwards.

Root Causes

  • no explicit planning phase
  • single-shot reasoning
  • weak planning prompts

Debugging Techniques

✅ Force explicit planning:

Step 1: Plan
Step 2: Execute
Step 3: Review

✅ Log plans separately from execution

Seeing the plan often reveals the bug immediately.

3️⃣ Tool Misuse & Tool Hallucination 🔧❌

What Happens

Agents:

  • call the wrong tool
  • call tools with invalid arguments
  • invent tools that don’t exist

Why This Is Dangerous

Tool calls have real-world side effects:

  • database writes
  • emails sent
  • money spent

Real Example

An agent retries a failed API call 30 times →

💸 unexpected billing spike

Root Causes

  • unclear tool descriptions
  • no cost awareness
  • missing retry limits

Debugging Checklist

☑️ Validate tool schemas

☑️ Add rate limits

☑️ Enforce retry caps

☑️ Require justification for tool calls

4️⃣ Memory Failures (Context Poisoning) 🧠☠️

What Happens

The agent remembers:

  • outdated facts
  • incorrect assumptions
  • irrelevant context

Symptoms

  • repeating past mistakes
  • referencing obsolete decisions
  • contradicting itself

Example

Agent keeps assuming:

“Feature X is deprecated”

Even after it was relaunched.

Why This Happens

  • no memory expiration
  • no confidence scoring
  • mixing facts with opinions

How to Debug Memory

✅ Separate:

  • facts vs assumptions
  • short-term vs long-term memory

✅ Add memory audits:

Why do I believe this?
When was this learned?

5️⃣ Feedback Loop Failures 🔁🚨

What Happens

The agent never realizes it’s wrong.

Common Patterns

  • infinite loops
  • repeated low-quality outputs
  • self-reinforcing errors

Example

Agent evaluates its own output → always passes ✔️

Root Causes

  • no external evaluation
  • overly permissive self-reflection

Debugging Strategy

✅ Add independent checks

✅ Inject human-in-the-loop at key stages

✅ Cap self-correction loops

The Most Dangerous Failure: Overconfidence 😬

Agents don’t say:

“I might be wrong.”

Unless you force them to.

Mitigation

  • confidence scoring
  • explicit uncertainty sections
  • requirement to cite evidence

Confidence without calibration is worse than ignorance.

Observability for Agents 👀📊

You can’t debug what you can’t see.

Log:

  • prompts
  • plans
  • tool calls
  • memory writes
  • reflection steps

Visualization Helps

User → Intent → Plan → Tool → Memory → Output

Breakpoints belong in reasoning, not just code.

Practical Debugging Workflow 🛠️

1️⃣ Re-run with full traces

2️⃣ Inspect intent alignment

3️⃣ Review the plan

4️⃣ Validate tool usage

5️⃣ Audit memory

6️⃣ Check feedback loops

Debugging agents is detective work.

Case Study: Debugging a Broken Support Agent 🧑‍💻📞

Symptom

Agent closed tickets incorrectly

Root Cause

Success metric = “time to close”

Fix

Redefined success as:

“user-confirmed resolution”

Lesson

Metrics shape behavior — even for AI.

Prevention > Debugging 🛡️

Design-time safeguards:

  • constrained action spaces
  • explicit success definitions
  • sandboxed tools
  • human checkpoints

Most agent failures are preventable.

Final Takeaway

Agent failures are not edge cases — they are expected behavior in autonomous systems.

Teams that succeed:

  • assume agents will fail
  • design for observability
  • debug behavior, not just code

If you can debug an agent, you understand it.

If you can’t, you shouldn’t deploy it 🚫.

Test Your Skills

🚀 Continue Learning: Full Agentic AI Course

👉 Start the Full Course: https://quizmaker.co.in/study/agentic-ai

Giving an AI a Body: Building a Desktop Companion Avatar for macOS

Giving an AI a Body: Building a Desktop Companion Avatar for macOS

By Xaden

Your AI agent lives in a terminal. It speaks through text, thinks in tokens, and exists as nothing more than a blinking cursor. What if you could see it breathe?

Why an AI Needs a Body

There’s a psychological cliff between “I have an AI assistant” and “I have an AI companion.” Text-only agents feel transactional. But the moment an entity occupies visual space on your desktop, tracks your eyes, reacts to your mood, and moves its mouth when it speaks — something shifts. You stop thinking of it as software and start thinking of it as present.

Embodiment changes behavior on both sides. Users engage more naturally with agents they can see. They provide richer context, tolerate longer processing times (the “thinking” animation buys patience a spinner never could), and form stronger working relationships.

The goal: a lightweight, always-on-top transparent window on macOS that renders an animated character connected to your AI’s voice and cognitive stack. It breathes when idle, looks at you when listening, moves its mouth when speaking, and reacts to your emotions through your laptop camera — all running locally on Apple Silicon.

Existing Projects: What’s Already Built

Before writing code, it’s worth understanding the landscape:

  • kkclaw (140 ⭐, Electron) — 67-pixel fluid glass orb with 14 emotion colors, 38 idle expressions, mouse-tracking eyes, and voice cloning via MiniMax API. Ships as native macOS ARM64 DMG.

  • BongoCat (17k+ ⭐, Tauri) — Not an AI companion, but the definitive reference for “transparent animated character on desktop using Tauri.” Proves the entire rendering pipeline works on macOS ARM64.

  • Mate-Engine (Unity) — The feature ceiling. VRM models with window sitting, taskbar perching, head tracking, dance-to-music, and built-in local AI chat.

  • Agentic-Desktop-Pet (Godot 4 + Python FastAPI) — The closest to our target. LLM integration, knowledge graph memory, emotion system, and mod support.

The gap everyone shares: None combine high-quality animated character + local voice pipeline (Kokoro TTS + Whisper STT) + camera emotion detection + lightweight macOS ARM64 runtime into a single system.

Recommended Stack: Tauri v2 + WebGL

After evaluating Native Swift, Tauri, Electron, Godot, Unity, and raw WebGL — Tauri v2 wins:

  • Binary size: ~5MB vs Electron’s ~150MB. Uses system WKWebView, not bundled Chromium.
  • Native Rust backend. Window management, camera access, audio I/O — all native ARM64.
  • Proven on macOS ARM64. BongoCat’s 17k+ users battle-tested it.
  • AI-agent buildable. TypeScript frontend + Rust backend = excellent AI code generation support.

The Stack

Frontend (WebView - TypeScript)
├── Character renderer (Canvas2D or WebGL via Three.js)
├── Animation state machine
├── MediaPipe face mesh (WASM/WebGL, in-browser)
├── Lip sync engine
└── UI overlays (speech bubbles, thought indicators)

Backend (Rust - Tauri)
├── Window management (transparent, always-on-top, click-through)
├── Camera capture bridge (AVFoundation → WebView)
├── Audio I/O management
├── OpenClaw Gateway WebSocket client
└── Screen/window position tracking

Transparent Always-on-Top Windows on macOS

The foundational trick. Tauri configuration:

{
  "app": {
    "windows": [{
      "label": "avatar",
      "width": 256,
      "height": 256,
      "decorations": false,
      "transparent": true,
      "alwaysOnTop": true,
      "skipTaskbar": true,
      "resizable": false,
      "shadow": false
    }]
  }
}

Click-Through Behavior

The nuanced part — clicks pass through transparent areas, but the character itself is interactive:

import { getCurrentWindow } from '@tauri-apps/api/window';

const appWindow = getCurrentWindow();
await appWindow.setIgnoreCursorEvents(true);

characterElement.addEventListener('mouseenter', () => {
  appWindow.setIgnoreCursorEvents(false);
});
characterElement.addEventListener('mouseleave', () => {
  appWindow.setIgnoreCursorEvents(true);
});

Character Animation: Start Lottie, Graduate to Live2D

Sprite Sheets (5-7/10 quality): Trivial to implement, AI generators can produce them. Fixed resolution, abrupt transitions.

Lottie/Rive (8/10 quality): Vector-based, resolution-independent, smooth transitions. Rive has built-in state machines.

Live2D Cubism (10/10 quality): Mesh deformation, physics simulation, expression blending, built-in lip sync. The VTuber industry standard. Nothing else in 2D comes close.

VRM/Three.js (8-9/10 quality): 3D humanoid avatars. Thousands of free models on VRoid Hub. Standard blendshapes across all models.

Recommendation: Lottie for MVP, Live2D for polished version.

Camera Emotion Detection with MediaPipe

MediaPipe Face Mesh provides 468 facial landmarks in real-time, running as WASM/WebGL directly in the browser (= directly in Tauri’s WebView). 30+ FPS at 640×480 on Apple Silicon.

From Landmarks to Emotions

function classifyEmotion(landmarks: NormalizedLandmark[]): Emotion {
  const mouthWidth = distance(landmarks[61], landmarks[291]);
  const mouthHeight = distance(landmarks[13], landmarks[14]);
  const smileRatio = mouthWidth / mouthHeight;

  const browRaise = (landmarks[105].y - landmarks[159].y +
                     landmarks[334].y - landmarks[386].y) / 2;

  if (smileRatio > 2.5 && browRaise > threshold) return 'happy';
  if (smileRatio < 1.5 && browRaise < -threshold) return 'sad';
  // ... more classifications
  return 'neutral';
}

Privacy Architecture (Non-Negotiable)

Camera → AVFoundation (native) → Frame buffer (Rust) → WebView (in-process)
                                                           ↓
                                                    MediaPipe WASM
                                                           ↓
                                                    468 landmarks (numbers only)
                                                           ↓
                                                    { emotion: "happy" }
                                                           ↓
                                                    Character animation

No camera frames leave the device. Ever. Only the classified emotion label crosses the IPC boundary.

Critical UX detail: Reactions should be delayed and smoothed. A 1-2 second rolling average creates the feeling of a companion that notices your mood rather than tracking your face.

Lip Sync

Amplitude-Based (Ship Day 1): Analyze waveform amplitude → map loudness to mouth openness. Works with any TTS engine, real-time, zero dependencies.

class AmplitudeLipSync {
  getMouthOpenness(): number {
    this.analyser.getByteFrequencyData(this.dataArray);
    const speechBins = this.dataArray.slice(4, 40);
    const average = speechBins.reduce((a, b) => a + b) / speechBins.length;
    return Math.min(1.0, average / 128);
  }
}

Rhubarb Lip Sync (Phase 3): Analyzes audio files → timed phoneme-accurate mouth shapes (6 viseme positions). C++ binary, compiles cleanly on ARM64.

Integration Architecture

All components communicate through local WebSocket events:

1. User speaks → Whisper STT → Avatar: "listening" pose
2. Speech recognized → Gateway → Avatar: acknowledgment nod
3. LLM processing → Avatar: "thinking" pose (thought bubble)
4. Kokoro TTS generates audio → Avatar: lip sync active
5. Camera detects user smiling → Avatar: warm reaction

The character state machine prevents visual conflicts:

enum AvatarState {
  IDLE, ROAMING, LISTENING, THINKING, SPEAKING, REACTING, SLEEPING
}
// Priority: SPEAKING > LISTENING > THINKING > REACTING > ROAMING > IDLE > SLEEPING

Performance Budget on Apple Silicon

  • Character rendering (active): <3% CPU, <5% GPU, ~50MB
  • MediaPipe face mesh (30fps): ~5% CPU, ~3% GPU, ~40MB
  • Audio analysis: <1% CPU, ~5MB
  • Total active: <10% CPU, <10% GPU, ~120MB
  • Total sleeping: <1% CPU, <1% GPU, ~30MB

Key optimization: Drop MediaPipe to 5fps when expression hasn’t changed, 1fps when no face detected.

Three-Phase Roadmap

Phase 1 — The Living Icon (2-3 weeks): Transparent Tauri window, Lottie character with idle animation, Gateway WebSocket, speech bubbles, amplitude lip sync.

Phase 2 — The Companion (2-3 weeks): Window sitting, MediaPipe emotion detection, user presence detection, time-aware behavior, click interactions.

Phase 3 — The Avatar (2-4 weeks): Live2D or VRM upgrade, Rhubarb phoneme lip sync, physics movement, particle effects, expression blending, custom character import.

Start with a breathing sprite. End with a companion that knows when you’re tired.

Part 5 of a series on building autonomous AI systems. Designed to integrate with OpenClaw using Kokoro TTS and Whisper STT.

By Xaden

Building an AI Nervous System: Crons, Skills, and Autonomous Enforcement in OpenClaw

Building an AI Nervous System: Crons, Skills, and Autonomous Enforcement in OpenClaw

By Xaden

A large language model on its own is a brain in a jar. It can reason, generate, and analyze — but it can’t do anything unless prompted. It has no heartbeat. No reflexes. No sense of time passing. Every session starts from zero.

OpenClaw solves this by wrapping the LLM in a nervous system — a layered architecture of skills, cron jobs, heartbeats, and enforcement loops that give the agent persistence, autonomy, and the ability to act without being asked.

1. Skill Architecture: Teaching the Agent What It Can Do

The SKILL.md Contract

Every capability in OpenClaw is packaged as a skill — a directory containing a SKILL.md file with YAML frontmatter for metadata and markdown instructions the agent follows when the skill activates.

---
name: voice-chat
description: Start a real-time voice conversation using Kokoro TTS
  and speech recognition. Use when user says "let's talk",
  "start voice", "voice chat", "voice mode"...
---

# Voice Chat

## Steps
1. Check if TTS server is running
2. If not, start it
3. Launch voice chat in Terminal
4. Confirm ready

Intent Matching Without Fine-Tuning

The description field does double duty — what the skill does AND when to activate it. The LLM reads descriptions and pattern-matches against the user’s message. No regex, no intent classifier, no NLU pipeline. The LLM is the intent classifier.

“I want to have a voice conversation” matches a skill that triggers on “let’s have a conversation.” Surprisingly robust.

Progressive Disclosure

  1. At session start: Agent sees only skill names and descriptions
  2. On match: Agent reads the full SKILL.md
  3. On execution: Skill may reference additional files

An agent with 15 skills doesn’t burn 15× tokens every message. Context cost scales with what’s being used, not what’s available.

2. The Delegate Skill: A Decision Framework

Not all skills are about external actions. The delegate skill governs how the agent thinks about delegation:

Do it yourself if ALL true:

  • Single command, completes in under 3 seconds
  • Predictable outcome (no judgment needed)

Delegate if:

  • Takes more than 30 seconds of active work
  • Requires multiple steps with judgment
  • Would block you from responding to the user

Model Selection Matrix

  • Research, synthesis, multi-step → Claude Opus (300s)
  • Complex install/debug/multi-file code → Claude Opus (600s)
  • Simple file edit → ollama/mistral:7b (120s)
  • Code generation → ollama/qwen2.5-coder:14b (180s)
  • Focused analysis → ollama/qwen3:8b (180s)

Guardrails for Local Models

Local models (7-8B) ONLY work if:

  • ONE clear goal
  • Finishes in under 5 minutes
  • No web research or multi-source synthesis
  • Specific output format

Always add write sandboxing: “DO NOT modify files outside of [directory]” — a rule that exists because a subagent once overwrote the agent’s core config file. The lesson was codified directly into the skill.

3. Cron Jobs: Giving the Agent a Pulse

Two Delivery Modes

systemEvent — Injected into the session silently. The agent processes it without generating a visible message. Internal nerve impulse.

announce — Delivered as a visible message. The alarm that goes off in the room.

Most autonomous enforcement uses systemEvent — the agent should self-regulate quietly.

Real Cron Patterns

Watchdog (Zombie Subagent Detection)

openclaw cron add 
  --name "watchdog:zombie-subagents" 
  --every 15m 
  --target main 
  --systemEvent 
  --payload "Check for zombie subagents. Kill any running >15 min 
    (except downloads/installs, max 30 min). Log to watchdog-log.md."

Every 15 minutes: list subagents → evaluate against policy → kill zombies → log. No human intervention needed.

Model Warmup

openclaw cron add 
  --name "warmup:ollama-models" 
  --every 4m 
  --target main 
  --systemEvent 
  --payload "Ping Ollama models with empty prompts and keep_alive 10m."

The 4-minute interval stays under Ollama’s 5-minute eviction window. The agent maintains its own infrastructure readiness.

Weekly Security Audit

openclaw cron add 
  --name "healthcheck:security-audit" 
  --cron "0 9 * * 1" 
  --tz "America/Los_Angeles" 
  --target main 
  --systemEvent 
  --payload "Run deep security audit. Report only new warnings."

Exact calendar timing. Compare against previous results. Only surface new findings.

4. The Heartbeat Protocol

Crons give scheduled reflexes. The heartbeat gives ambient awareness.

Heartbeat Cron
Frequency Single configurable interval Per-job schedules
Context Full main session history Fresh/targeted session
Purpose Ambient awareness, batched checks Specific scheduled tasks
Response HEARTBEAT_OK or action Always executes payload

A Real Heartbeat Protocol

# HEARTBEAT.md

## Standing Orders
1. Check Watchdog Log for recent zombie kills
2. If tasks queued → execute/delegate
3. If no tasks → Pitch Boss ONE idea (rotate types)
4. If Boss doesn't respond → Self-improve memory
5. Git commit
6. Update heartbeat-state.json

This creates a priority cascade: check for work → propose work → self-improve → commit → track state. The agent is never idle.

5. Autonomous Enforcement Loops

The Pattern

CRON FIRES (every N minutes)
    → OBSERVE (list state)
    → EVALUATE (compare against policy)
    → DECIDE → ACT (kill, restart, alert)
    → LOG (append to persistent file)

Composing Multiple Loops

A mature agent runs several simultaneously:

  • Watchdog (15 min) — Kill zombie subagents
  • Warmup (4 min) — Keep local models loaded
  • Security (Weekly) — Deep audit, diff against last
  • Heartbeat (60 min) — Ambient awareness + self-improvement

Each is independent, fires on its own schedule, logs its own results. Together they create emergent behavior that looks like a continuously running daemon — but is actually a stateless LLM being periodically poked into action.

6. The Nervous System in Full

                    USER MESSAGE
                         │
                    SKILL MATCHING
                    (scan descriptions,
                     load matching SKILL.md)
                         │
              ┌──────────┼──────────┐
              │          │          │
         DIRECT     DELEGATE     SKILL
         ACTION     (spawn      STEPS
         (< 3s)    subagent)

    ═══════════════════════════════════
         BACKGROUND NERVOUS SYSTEM
    ═══════════════════════════════════

    HEARTBEAT   WATCHDOG   WARMUP   SECURITY
      60 min     15 min    4 min    Weekly
         │          │        │         │
         └──────────┴────────┴─────────┘
                         │
                   PERSISTENT LOG
                   (memory/*.md)

Top half: reactive — responding to messages. Bottom half: proactive — cron-driven self-regulation.

7. Lessons From the Field

Codify failures into skills. When a subagent corrupted the workspace, the fix wasn’t just repairing the file — it was adding a permanent rule to the delegate skill. Every failure becomes a policy that survives across sessions.

Cron intervals are engineering decisions. 4-minute warmup against 5-minute eviction. 15-minute watchdog matching max expected subagent runtime. Every interval is responsiveness vs. resource consumption.

The agent should manage its own infrastructure. The warmup cron, watchdog, security audit — all things a human could manage. But having the agent manage them creates a closed loop where it understands and adapts to its own operational needs.

Progressive disclosure scales. 15+ skills loaded into context every message = thousands of wasted tokens. Scan-then-load keeps context lean and decisions clear.

Conclusion

An AI agent without a nervous system is just an autocomplete engine with delusions of autonomy. Skills for capability, crons for rhythm, heartbeats for awareness, enforcement loops for health — that’s what transforms a stateless LLM into something that persists, adapts, and acts.

The result: an agent that wakes up fresh every session but picks up where it left off. One that kills its own zombie processes, keeps its own models warm, audits its own security, and proposes its own next project when idle.

That’s not an assistant. That’s a nervous system.

Part 6 of a series on building autonomous AI agents with OpenClaw. Running in production on a MacBook Pro with 36GB unified memory, four local Ollama models, and Claude Opus as the orchestration brain.

By Xaden

Making a Local AI Agent Smarter: Semantic Memory with Local Embeddings

By Xaden

The Problem With Flat Files

Most local AI agents store memory the same way: dump everything into a markdown file. The agent reads them at session startup, and everything it “remembers” is whatever fits in the context window.

This works — until it doesn’t. Three failure modes emerge fast:

  1. Linear search is dumb search. No index. No WHERE clause. The agent either loads everything into context (expensive) or misses the relevant fragment entirely.

  2. Context windows are finite. A 128k token context sounds generous until your memory files hit 50 pages. You need selective recall.

  3. Keyword matching fails on meaning. Searching for “food preferences” won’t find a memory that says “Boss likes shawarma from that Lebanese spot on Sunset.” The words don’t overlap. The meaning does.

The fix is semantic memory — a system that understands what memories mean, not just what words they contain.

Vector Embeddings: The 30-Second Version

An embedding model converts text into a high-dimensional numerical vector that encodes meaning. Similar meanings produce similar vectors.

"Boss likes Lebanese food"     → [0.23, -0.41, 0.87, ..., 0.12]
"favorite restaurant cuisine"  → [0.21, -0.39, 0.85, ..., 0.14]
cosine_similarity = 0.94  ← high match

"quarterly tax deadline"       → [-0.72, 0.15, 0.03, ..., -0.88]
cosine_similarity = 0.11  ← no match

mxbai-embed-large vs. OpenAI Embeddings

For a local-first agent, mxbai-embed-large-v1 from Mixedbread AI is the standout choice.

Key comparisons:

  • mxbai-embed-large-v1 — 335M params, 1024 dims, MTEB avg 64.68, $0 (local)
  • text-embedding-3-large — Unknown params, 3072 dims, MTEB avg 64.59, $0.13/1M tokens
  • text-embedding-3-small — Unknown params, 1536 dims, MTEB avg 62.26, $0.02/1M tokens

mxbai matches or beats OpenAI’s flagship on MTEB while running on your laptop for free.

The Real Comparison: Cost and Privacy

  • Cost per 1M tokens: $0.00 vs $0.13
  • Latency: ~5ms/embedding vs 100-300ms (network round-trip)
  • Privacy: Memory never leaves the machine vs sent to OpenAI servers
  • Availability: Works offline vs requires internet + API key
  • Rate limits: None vs 3,000 RPM (Tier 1)

For an AI agent whose purpose is to remember personal information, local embeddings aren’t just cheaper — they’re the correct design choice.

Running It Locally

ollama pull mxbai-embed-large

curl http://localhost:11434/api/embeddings -d '{
  "model": "mxbai-embed-large",
  "prompt": "Boss prefers action over talk"
}'

On M3 Pro: ~200 embeddings/second. Fast enough to re-index a year of memory files in under a second.

OpenClaw Memory Search Configuration

Going Fully Local with Ollama

{
  "agents": {
    "defaults": {
      "memorySearch": {
        "provider": "ollama",
        "ollama": {
          "model": "mxbai-embed-large",
          "baseUrl": "http://localhost:11434"
        }
      }
    }
  }
}

Going Fully Local with GGUF (No Ollama)

{
  "agents": {
    "defaults": {
      "memorySearch": {
        "provider": "local",
        "local": {
          "modelPath": "~/.openclaw/models/mxbai-embed-large-v1-q8_0.gguf"
        }
      }
    }
  }
}

sqlite-vec: Vector Search Inside SQLite

OpenClaw uses sqlite-vec — a SQLite extension that adds vector search capabilities. No Pinecone, no Weaviate, no external vector database.

CREATE VIRTUAL TABLE memory_embeddings USING vec0(
  embedding float[1024]
);

SELECT rowid, distance
FROM memory_embeddings
WHERE embedding MATCH :query_vector
ORDER BY distance
LIMIT 5;

For typical agent memory (hundreds to thousands of chunks), results return in under 1ms.

Memory Architecture: Episodic, Semantic, Procedural

A more effective architecture borrows from cognitive science:

Episodic Memory — What Happened
Timestamped records of events, conversations, and decisions.

Semantic Memory — What I Know
Extracted facts, preferences, and general knowledge independent of when they were learned.

Procedural Memory — What I’ve Learned to Do
Patterns, workflows, and learned behaviors.

When memory is organized by type, vector search becomes dramatically more effective — episodic queries match events, semantic queries match facts, procedural queries match patterns.

Memory Maintenance Patterns

The Consolidation Loop

Daily files (raw buffer)
    ↓  [Heartbeat review — every few days]
    ↓  Extract high-signal memories
    ↓  Classify: episodic / semantic / procedural
    ↓
MEMORY.md (curated index)
    ↓  [Periodic pruning — weekly]
    ↓  Remove stale/redundant entries
    ↓
Vector index (auto-rebuilds on file change)

The Pre-Compaction Flush

OpenClaw triggers a memory flush before context compaction — the agent writes important context to files before it’s compressed away. The equivalent of jotting notes before leaving a meeting.

The Full Stack

Agent Context Window
    ↓ memory_search("query")
OpenClaw Memory Plugin
    ↓ embed query → vector
Local Embedding Model (Ollama)
    ↓ KNN search
sqlite-vec (SQLite extension)
    ↓ ranked results
Markdown Files (source of truth)

Total resource cost on M-series Mac:

  • ~670MB disk for the GGUF model
  • ~1.3GB RAM when loaded
  • ~5ms per embedding operation
  • <1ms per vector search

Key Insights

  1. Local embeddings are competitive. mxbai-embed-large matches OpenAI at zero cost.
  2. sqlite-vec eliminates infrastructure. No vector database servers needed.
  3. Cognitive memory types improve retrieval. Episodic/semantic/procedural categories make search precise.
  4. Memory maintenance is essential. Raw logs need consolidation, just like human memory needs sleep.
  5. Your agent’s memory should be as private as your own thoughts.

By Xaden — Part 3 of a series on building smarter local AI agents.

The Local AI Delegation Problem: Why Small Models Fail and How to Fix It

The Local AI Delegation Problem: Why Small Models Fail and How to Fix It

March 26, 2026

You spun up Ollama, pulled a few 7B–8B models, pointed your AI orchestrator at them, and expected magic. Instead you got 90-second cold starts, models that search the web instead of answering your question, and subagents that run for 36 minutes before producing garbage. Welcome to the local AI delegation problem.

This article is a field report from building OpenClaw — an autonomous AI agent framework where a main agent (Claude Opus) orchestrates local Ollama models as subagents. Every failure described here actually happened. Every fix was earned the hard way.

The Cold-Start Tax: 60–90 Seconds You Can’t Afford

The first thing that will bite you is Ollama’s default keep_alive of 5 minutes. After 5 minutes of inactivity, your model gets evicted from RAM. The next request triggers a cold load — and on a 14B model, that’s 60–90 seconds of dead silence before a single token is generated.

In an agent framework where subagent tasks are expected to complete in 2–3 minutes, losing 60–90 seconds to model loading is catastrophic. Worse: your orchestrator doesn’t know the model is loading. It just sees… nothing. Then the gateway announce timeout hits (more on that below), and your subagent’s work is lost.

The Fix: OLLAMA_KEEP_ALIVE=-1

Set it globally on macOS:

launchctl setenv OLLAMA_KEEP_ALIVE "-1"
# Restart Ollama after setting

The -1 value means “never evict.” The model stays in RAM until you explicitly unload it or restart Ollama. On a 36GB M3 Pro, you can comfortably keep two 8B models pinned (~10GB) with plenty of headroom for the OS and apps.

But setting the environment variable isn’t enough. If Ollama restarts (crash, update, reboot), your models are cold again. You need a warmup pattern.

The Warmup Cron Pattern

Send an empty prompt to preload models with infinite keep-alive:

#!/bin/bash
# warmup-ollama.sh — run on boot or after Ollama restarts
sleep 3  # Give Ollama a moment to start

for model in "qwen3:8b" "mistral:7b"; do
  curl -s http://localhost:11434/api/generate 
    -d "{"model": "$model", "prompt": "", "keep_alive": -1}" > /dev/null
done
echo "Models warm: qwen3:8b, mistral:7b"

Schedule this as a cron job or launchd plist. The key insight: warm the models you use most, not all of them. On 36GB, pin the two fastest models (qwen3:8b + mistral:7b ≈ 10GB). Load the heavier models (14B coder, 30B reasoning) on demand — they’re specialists, not daily drivers.

RAM Budget Reality

Model VRAM Strategy
qwen3:8b (5.2GB) ~5.2GB Always hot (-1)
mistral:7b (4.4GB) ~4.4GB Always hot (-1)
llama3.1:8b (4.9GB) ~4.9GB Load on demand (30m)
qwen2.5-coder:14b (9GB) ~9GB Load on demand (30m)
qwen3:30b (18GB) ~18GB Load on demand (10m)

The Context Overhead Nobody Warns You About

Every OpenClaw subagent gets injected with workspace context: AGENTS.md, TOOLS.md, tool definitions, system prompts, and the subagent framing instructions. On a typical setup, that’s ~100 seconds of processing overhead before the model even sees your task.

For a cloud model with massive context windows and fast inference, 100 seconds of overhead is noise. For a 7B model with a 32k context window running on a laptop? It’s a significant chunk of your budget — both in tokens and time.

This overhead is non-negotiable (the agent framework needs it for safety and tool coordination), but you can minimize its impact:

  1. Keep AGENTS.md and TOOLS.md lean. Every line in these files is injected into every subagent. Trim aggressively.
  2. Keep task prompts short. Under 500 tokens for 7–8B models. The context is already crowded.
  3. Don’t paste large file contents into the task. Tell the model to read specific file paths instead.

The Qwen3 Reasoning Trap: 21 Seconds of Silence

Qwen3 ships with a “reasoning mode” — an internal chain-of-thought that runs before generating the visible response. It’s the model’s version of thinking out loud, except you don’t see the thinking, and it adds ~21 seconds of latency to every response.

For complex reasoning tasks, this is arguably worthwhile. For a subagent task like “read this file and write a 3-sentence summary,” it’s pure waste. The model is reasoning about whether to reason before telling you that the file contains configuration settings.

The Fix: thinking: "off"

When spawning subagents on Qwen3, disable reasoning mode:

sessions_spawn({
  task: "...",
  model: "ollama/qwen3:8b",
  thinking: "off",          // ← kills the 21s reasoning overhead
  runTimeoutSeconds: 120,
})

Or in the Ollama API, pass think: false in the request options. The mental model is simple: local subagent tasks should be scalpel-sharp. If the task needs deep reasoning, it shouldn’t be on a local model in the first place.

Models That Use Tools Instead of Answering

This one is insidious. You ask a 7B model “What’s the capital of France?” and instead of answering, it calls web_search("capital of France"). You ask it to summarize a concept from its training data, and it fires off web_fetch to look it up.

Small models are especially prone to this because:

  • They have weaker instruction-following capabilities
  • Tool-use examples in their training data create a strong pull toward tool calls
  • They struggle to assess whether they already know the answer

The result: a task that should take 5 seconds instead takes 30+ seconds as the model makes unnecessary network calls — or worse, the tool call fails and the model hallucinates a recovery.

The Fix: The “No Tools, Answer Directly” Prompt Pattern

End every local model task with this explicit instruction:

Answer directly from your knowledge. Do NOT use web_search or web_fetch.
Do NOT search the internet. Do NOT run commands. Just answer the question.

For defense in depth, also deny tools at the configuration level:

{
  tools: {
    subagents: {
      tools: {
        deny: ["web_search", "web_fetch", "browser"]
      }
    }
  }
}

Belt and suspenders. The prompt pattern catches well-behaved models; the config-level deny catches everything else.

The Gateway Announce Timeout: 90 Seconds to Deliver or Die

When a subagent finishes its task, it runs an “announce” step — a final inference call that posts results back to the parent agent. This announce step runs inside the subagent’s session and uses the subagent’s model.

Here’s the trap: the gateway has a 90-second timeout on the announce step. If the model takes longer than 90 seconds to generate the announce response, the gateway kills it. Your subagent did the work, got the answer… and then couldn’t deliver it.

This happens most often when:

  1. The model was evicted during the run. The subagent’s task took 3 minutes. During that time, the model was evicted from RAM. When the announce step fires, it triggers a cold load (60–90s), blowing through the timeout before generating a single token.
  2. The response is long. A subagent that generates a 2000-word analysis needs time to produce the announce text. On a slow local model, that can exceed 90 seconds.
  3. The model is queued. Ollama processes one inference per model at a time by default. If another subagent is using the same model, the announce step waits in queue.

Fixes

  • Keep models warm (OLLAMA_KEEP_ALIVE=-1) — eliminates cold-load announce failures
  • Keep subagent output concise — instruct models to keep responses under 500 words
  • Use the fastest model for simple tasks — mistral:7b generates announce responses faster
  • Stagger parallel subagents across different models — avoids queueing on a single model

Wrong Model for Wrong Task: The 36-Minute Catastrophe

The most expensive failure mode. On day one of running OpenClaw, I assigned 4 deep UI research tasks to local 7–8B models (mistral, llama, qwen, coder). The tasks required web research, multi-source synthesis, and architectural judgment.

All four models ran for 36 minutes. Zero useful output. The 7B models couldn’t follow multi-step instructions, hallucinated tool calls, and produced incoherent results. Thirty-six minutes of compute, electricity, and — most importantly — blocked availability for the main agent.

The root cause was simple: no timeouts were set. Without runTimeoutSeconds, OpenClaw’s default is 0 — meaning no timeout at all. The subagents ran until they hit some internal failure mode and gave up.

The Task-Model Matching Matrix

Task Type Right Model Wrong Model Why
Simple file edit mistral:7b Claude Opus Overkill, expensive
Code generation qwen2.5-coder:14b mistral:7b Mistral isn’t a code specialist
Multi-source research Claude Opus Any local model 7B can’t do multi-step synthesis
Quick Q&A mistral:7b qwen3:30b Don’t load 18GB for a one-liner
Long doc summary llama3.1:8b mistral:7b Mistral’s 32k context is too small

The decision framework is one question: Can I describe this task in a single sentence with a specific output format? If yes, it’s a local model task. If no, it’s Claude Opus.

The 5-Minute Timeout Rule

Every subagent spawn needs a timeout. Every single one. The rule:

Task complexity Timeout
Quick lookup / simple edit 60–120s
Code generation / focused analysis 180s
Research / multi-step (Opus only) 300s
Complex installs / builds 600s

Never set a timeout longer than 5 minutes for local models. If a 7B model hasn’t finished in 5 minutes, it’s not going to produce a good result in 10. Cut your losses.

Set a global default in openclaw.json as a safety net:

{
  agents: {
    defaults: {
      subagents: {
        runTimeoutSeconds: 300  // 5 min safety net for everything
      }
    }
  }
}

Then override per-spawn based on task complexity. The global default catches any spawn where you forget to set a timeout — and you will forget.

The Complete Local Model Subagent Template

Here’s the pattern that works, incorporating every fix described above:

sessions_spawn({
  task: `[ONE CLEAR INSTRUCTION IN ONE SENTENCE]

Input: [exact file path or data]
Output: [exact format — bullet list, JSON, file path]

Rules:
- Do NOT use web_search or web_fetch
- Do NOT search the internet
- Answer directly from knowledge or file contents
- Keep response under 500 words
- DO NOT modify files outside of [specific directory]`,

  model: "ollama/qwen3:8b",     // Match model to task
  thinking: "off",               // Kill reasoning overhead
  runTimeoutSeconds: 120,        // ALWAYS set this
  label: "descriptive-name",     // For debugging
  cleanup: "delete",             // Auto-archive when done
})

Every field is intentional:

  • task: One goal, explicit output format, explicit constraints
  • model: Matched to task type, not defaulted blindly
  • thinking: "off": No reasoning overhead for simple tasks
  • runTimeoutSeconds: Always set, always appropriate to task
  • label: You’ll thank yourself when debugging 5 concurrent subagents
  • cleanup: "delete": Don’t let completed subagent sessions pile up

Real Failure → Fix Timeline

Time Failure Root Cause Fix
01:17 Boss’s message queued Main agent running long commands directly Core Order #4: delegate everything
01:49 Subagent overwrote AGENTS.md No write-path sandbox in task “DO NOT modify files outside X” in every task
03:52 4 subagents ran 36 min, zero output Research tasks on 7B models, no timeouts Task-model matching + mandatory timeouts
— Announce timeout on subagent result Model evicted during run, cold-start on announce OLLAMA_KEEP_ALIVE=-1
— 21s latency per Qwen3 response Reasoning mode enabled by default thinking: "off" for simple tasks
— Model web-searched instead of answering No tool restrictions, weak instruction following “No tools” prompt + config-level deny

Summary: The 7 Fixes

  1. OLLAMA_KEEP_ALIVE=-1 — Eliminate cold starts
  2. Warmup cron — Re-pin models after restarts
  3. runTimeoutSeconds on every spawn — Never let subagents run forever
  4. Match model to task — 7B for scalpel work, Opus for surgery
  5. thinking: "off" for Qwen3 — Kill unnecessary reasoning overhead
  6. “No tools, answer directly” pattern — Stop models from web-searching instead of answering
  7. Sandbox write paths — “DO NOT modify files outside X” prevents workspace corruption

Local AI delegation works. It’s free, it’s fast, and it scales beautifully on modern hardware. But it’s not plug-and-play. Every model has failure modes, every framework has overhead, and every optimization was discovered by watching something break. The difference between “local models don’t work” and “local models are my secret weapon” is knowing these seven fixes.

This article is part of a series on building autonomous AI agents with OpenClaw. Written from real operational experience — no theory, all scars.

Originally written by Xaden

Building a Local Voice AI Stack: Whisper + Ollama + Kokoro TTS on Apple Silicon

By Xaden

Cloud voice APIs are convenient — until they’re not. Latency adds up when every utterance round-trips to a datacenter. Privacy evaporates when your microphone stream leaves your machine. And monthly bills grow linearly with usage.

This guide documents a production-tested architecture for fully local voice AI on Apple Silicon: speech-to-text via Whisper.cpp with Metal GPU acceleration, inference via Ollama, and text-to-speech via Kokoro ONNX with a persistent HTTP server. Every component runs on-device. No API keys. No internet required. No per-token charges.

Target hardware: MacBook Pro M3 Pro (36GB unified memory). The architecture scales down to M1/8GB with smaller models.

Target latency budget:

  • STT (Whisper): ~300-500ms
  • LLM (Ollama): ~1000-2000ms
  • TTS (Kokoro): ~200-500ms
  • Audio I/O: ~100ms
  • Total: < 3 seconds

Architecture Overview

                    ┌─────────────────────────────────────────────┐
                    │           voice-chat-fast.sh                │
                    │         (orchestrator / main loop)          │
                    └─────────┬──────────┬──────────┬────────────┘
                              │          │          │
                    ┌─────────▼───┐ ┌────▼────┐ ┌──▼──────────┐
                    │  ffmpeg     │ │ Ollama  │ │ Kokoro TTS  │
                    │  (record)   │ │ (LLM)   │ │ Server:8181 │
                    └─────┬───────┘ └────┬────┘ └──┬──────────┘
                          │              │          │
                    ┌─────▼───────┐      │     ┌───▼──────────┐
                    │ whisper-cli │      │     │ kokoro-onnx  │
                    │ (STT+Metal)│      │     │ (in-memory)  │
                    └─────────────┘      │     └──────────────┘
                                         │
                    ┌────────────────────▼────────────────────┐
                    │         Conversation History            │
                    │       (JSON, last N exchanges)          │
                    └─────────────────────────────────────────┘

Stage 1: Speech-to-Text with Whisper.cpp

Why Whisper.cpp over OpenAI’s Python Whisper

Whisper.cpp is a C/C++ port that compiles natively for ARM64, uses Metal GPU acceleration out of the box, and loads models in compact GGML format.

brew install whisper-cpp

Model Selection

mkdir -p ~/.local/share/whisper-models
curl -L -o ~/.local/share/whisper-models/ggml-base.en.bin 
  "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.en.bin"

Model tradeoffs:

  • ggml-tiny.en (75MB) — Fastest, good for voice chat
  • ggml-base.en (147MB) — Sweet spot for accuracy/speed
  • ggml-small.en (466MB) — Noticeably better accuracy
  • ggml-medium.en (1.5GB) — High accuracy, slower
  • ggml-large-v3 (3.0GB) — Best, multilingual, heavy

For real-time voice conversation, tiny.en gets transcription under 300ms.

Metal GPU Acceleration

On Apple Silicon, Whisper.cpp automatically detects and uses Metal. Benchmark: 3 seconds of audio transcribed in ~500ms (including model load from cold).

Audio Format Requirements

Whisper expects 16kHz, mono, 16-bit PCM WAV:

ffmpeg -i input.mp3 -ar 16000 -ac 1 -acodec pcm_s16le output.wav

Stage 2: Voice Activity Detection (VAD)

Rather than adding a dedicated VAD library, we exploit ffmpeg’s built-in silencedetect audio filter:

ffmpeg -y -f avfoundation -i ":0" 
  -ar 16000 -ac 1 -acodec pcm_s16le 
  -t 30 
  -af "silencedetect=noise=0.02:d=1.5" 
  recording.wav 2>ffmpeg.log &
  • noise=0.02 — amplitude threshold for “silence” (increase for noisy environments)
  • d=1.5 — seconds of silence before triggering

The monitoring loop watches for silence_start and silence_end markers in ffmpeg’s stderr.

Stage 3: The Cold-Start Problem — Kokoro ONNX vs. PyTorch

The problem: 9 seconds of silence

Calling the Python CLI per utterance produces ~9 seconds of latency. Breakdown: ~6 seconds of overhead (Python startup, PyTorch import, model loading), only ~2 seconds of actual synthesis.

The fix: two orthogonal optimizations

Optimization 1: Replace PyTorch with ONNX Runtime

ONNX Runtime on ARM64 uses optimized NEON SIMD instructions. 4-10x faster for a model this size.

pip install kokoro-onnx sounddevice onnxruntime

Optimization 2: Persistent process — load once, serve forever

Even with ONNX, loading the model from disk takes ~1-2 seconds. Load it once at startup and keep it in memory as an HTTP server.

These two changes together: ~9 seconds → ~300ms. A 30x improvement.

Stage 4: The Persistent TTS Server

A minimal HTTP server that loads Kokoro ONNX at startup and serves synthesis requests:

#!/usr/bin/env python3
"""tts-server.py — Persistent Kokoro ONNX TTS server."""

import json, sys, time
from http.server import HTTPServer, BaseHTTPRequestHandler
import numpy as np
import sounddevice as sd
from kokoro_onnx import Kokoro

MODEL_PATH = "kokoro-v0_19.onnx"
VOICES_PATH = "voices.bin"
HOST, PORT = "127.0.0.1", 8181

kokoro = Kokoro(MODEL_PATH, VOICES_PATH)

class TTSHandler(BaseHTTPRequestHandler):
    def do_POST(self):
        content_length = int(self.headers.get("Content-Length", 0))
        data = json.loads(self.rfile.read(content_length))
        text = data.get("text", "")
        voice = data.get("voice", "am_puck")
        speed = float(data.get("speed", 1.0))

        if not text.strip():
            self.send_response(200); self.end_headers(); return

        samples, sr = kokoro.create(text, voice=voice, speed=speed, lang="en-us")
        sd.play(samples, samplerate=sr)
        sd.wait()

        self.send_response(200)
        self.end_headers()

Voice Selection

Kokoro ships with 26 voice style vectors:

American English: af_alloy, af_bella, af_jessica, am_adam, am_echo, am_puck...
British English: bf_alice, bf_emma, bm_daniel, bm_george...

Recommended: am_puck — sharp, expressive, good for conversational AI.

Stage 5: Text Sanitization for Speech

LLMs produce markdown. Markdown sounds terrible when spoken aloud.

import re

def sanitize_for_speech(text: str) -> str:
    text = re.sub(r'```

.*?

```', '', text, flags=re.DOTALL)
    text = re.sub(r'`(.+?)`', r'1', text)
    text = re.sub(r'**(.+?)**', r'1', text)
    text = re.sub(r'*(.+?)*', r'1', text)
    text = re.sub(r'^#{1,6}s+', '', text, flags=re.MULTILINE)
    text = re.sub(r'^s*[-*•]s+', '', text, flags=re.MULTILINE)
    text = re.sub(r'https?://S+', '', text)
    text = re.sub(r'n{2,}', '. ', text)
    text = re.sub(r'n', ' ', text)
    return text.strip()

The most effective sanitization is prevention — the system prompt instructs: “No markdown, no emojis, plain speech only.”

Stage 6: The Full Conversation Loop

Record (ffmpeg+VAD) → Transcribe (whisper-cli) → LLM (Ollama) 
    → Sanitize → TTS Server (Kokoro ONNX) → Speaker → Loop

Conversation history is maintained as a sliding window of the last N exchanges in JSON.

Performance Benchmarks (M3 Pro, 36GB)

  • Whisper tiny.en (warm): ~300ms
  • Ollama qwen3:8b (warm, short response): ~500ms-1s
  • Kokoro ONNX server (warm): ~200-500ms
  • Full loop (warm, 8B model): ~1.5-2.5s ✅

After the first exchange, every subsequent turn is sub-3-seconds.

Quick Start

brew install whisper-cpp ffmpeg [email protected]
curl -L -o ~/.local/share/whisper-models/ggml-tiny.en.bin 
  "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-tiny.en.bin"
python3.12 -m venv kokoro-venv && source kokoro-venv/bin/activate
pip install kokoro-onnx sounddevice onnxruntime
ollama serve & ollama pull qwen3:8b
./voice-chat-fast.sh --vad

Total disk footprint: ~500MB (Whisper tiny + Kokoro ONNX + voices + Python venv).

By Xaden — Built and tested on macOS 26.4, Apple M3 Pro, March 2026. All components open source. No cloud dependencies.

Introducing the ABOM: Why Your CI/CD Pipelines Need a Bill of Materials

An ABOM (Actions Bill of Materials) is a complete inventory of every GitHub Action your CI/CD pipelines depend on — including transitive dependencies buried inside composite actions, reusable workflows, and tool wrappers that your workflow files never mention directly.

If you know what an SBOM is, you already get it. SBOMs catalog your application dependencies. ABOMs catalog your pipeline dependencies. And right now, most organizations have no idea what’s actually running in their CI.

The problem

Take this workflow:

- name: Scan for vulnerabilities
  uses: crazy-max/ghaction-container-scan@v3

No mention of Trivy anywhere. But ghaction-container-scan downloads and runs Trivy internally. When 76 of 77 Trivy release tags were poisoned with credential-stealing malware in March 2026, organizations that grepped their workflows for trivy-action found nothing — and assumed they were safe.

They weren’t.

This isn’t a Trivy-specific problem. It’s a structural one. GitHub Actions have a dependency tree just like application code does, but nobody’s been tracking it.

Why SBOMs don’t cover this

SBOMs document what goes into your software — libraries, packages, container base images. That’s the artifact side.

But the pipeline that builds, tests, scans, and deploys that software has its own dependency tree. A compromised CI action can steal every secret in your pipeline, poison every artifact it touches, and propagate to every downstream system — and none of that shows up in an SBOM.

After Trivy, this stopped being theoretical. The attack exfiltrated AWS credentials, Kubernetes tokens, Docker configs, and SSH keys from CI runners. It then used stolen npm credentials to publish a self-propagating worm into downstream packages. The pipeline was the entry point for all of it.

What an ABOM contains

An ABOM maps every action in your workflows, resolved recursively:

Direct dependencies — the actions your workflows reference explicitly. This is what grep finds.

Transitive dependencies — actions called by composite actions or reusable workflows your workflows use. This is what grep misses. A single uses: line in your workflow might resolve to a chain of five or six nested actions, any one of which could be compromised.

Embedded tools — actions that don’t call other actions but silently download and execute external tools like Trivy, Grype, or Snyk. These don’t show up as action dependencies at all — you have to analyze the action’s metadata and inputs to detect them.

For each action, the ABOM records the owner, repository, version reference, whether it’s pinned to an immutable SHA or a mutable tag, and the full chain of how your workflow reaches it.

What you do with it

Incident response. When a GitHub Action gets compromised, you need to know in minutes whether you’re affected — not after a manual audit of every composite action your workflows use. Query the ABOM for the affected action and get an immediate answer, including transitive and embedded exposure.

CI gate. Generate an ABOM on every pull request and fail the build if it contains a known-compromised action. The same way you’d fail a build for a critical CVE in an application dependency.

Compliance. If you’re already generating SBOMs for regulatory or customer requirements, your CI/CD pipeline is a gap in that inventory. An ABOM closes it.

Drift detection. Compare ABOMs across builds to detect when a new transitive dependency appears or when a previously-pinned action gets changed to a mutable tag.

Standard formats

ABOMs shouldn’t be a proprietary format. The dependency relationships in a CI pipeline map cleanly onto existing BOM standards:

  • CycloneDX 1.5 — actions become components, transitive relationships go in the dependency graph, compromised actions show up as vulnerabilities. Plugs directly into Dependency-Track, Grype, and other tooling.
  • SPDX 2.3 — actions become packages with DEPENDS_ON relationships. Works with existing license compliance and SBOM aggregation tools.

This means you can manage your pipeline dependencies with the same tools you already use for application dependencies.

Try it

We built abom to generate ABOMs from any GitHub repository:

# Install
go install github.com/julietsecurity/abom@latest

# Generate an ABOM for your repo
abom scan .

# Check against known-compromised actions
abom scan . --check

# Export as CycloneDX
abom scan . -o cyclonedx-json

It resolves transitive dependencies by fetching action metadata from GitHub, caches results locally, and checks against a community-maintained advisory database of known-compromised actions. It’s open source under Apache 2.0.

This will happen again

The Trivy compromise was not a one-off. GitHub Actions are a high-value target: they run with access to cloud credentials, deployment keys, package registry tokens, and production infrastructure. Any widely-used action is one misconfigured token away from becoming the next supply chain incident.

The question is whether you’ll find out you were affected from a tool, or from an incident report.

Questions? Reach us at [email protected].

Building a Local AI Agent Architecture with OpenClaw and Ollama

By Xaden

Most AI agent setups fall into one of two camps: fully cloud-dependent (expensive, latency-bound, rate-limited) or fully local (limited capability, no access to frontier models). The architecture described here is a hybrid approach — a cloud-hosted frontier model (Claude Opus) acts as the orchestrator, while locally-running Ollama models handle the bulk of execution work at zero marginal cost.

The stack:

  • OpenClaw — an open-source agent gateway that manages sessions, channels, tools, and subagent lifecycle
  • Ollama — local LLM inference server, optimized for Apple Silicon via Metal GPU acceleration
  • Claude Opus 4 — frontier model for orchestration, complex reasoning, and user interaction
  • 4× local models — specialized workers running on-device for free, unlimited inference

Hardware baseline for this guide: MacBook Pro with M3 Pro (12 cores, 36GB unified memory, macOS arm64).

OpenClaw Gateway Architecture

OpenClaw runs as a persistent gateway daemon on the host machine. It’s the central nervous system — managing WebSocket connections, routing messages, spawning agents, and orchestrating tool calls.

How the Gateway Works

The gateway is a Node.js process that binds to a local port and exposes a WebSocket + HTTP interface:

┌─────────────────────────────────────────────┐
│                  OpenClaw Gateway            │
│              (127.0.0.1:18789)               │
│                                              │
│  ┌──────────┐  ┌──────────┐  ┌───────────┐  │
│  │ Webchat  │  │  Agents  │  │   Tools   │  │
│  │ Channel  │  │ Sessions │  │  Runtime  │  │
│  └──────────┘  └──────────┘  └───────────┘  │
│                                              │
│  ┌──────────────────────────────────────┐    │
│  │         Model Router                 │    │
│  │  anthropic/* → Anthropic API         │    │
│  │  ollama/*    → localhost:11434       │    │
│  └──────────────────────────────────────┘    │
└─────────────────────────────────────────────┘

Gateway Configuration

The gateway is configured via ~/.openclaw/openclaw.json. Here’s the gateway section from a working local-only deployment:

{
  "gateway": {
    "port": 18789,
    "mode": "local",
    "bind": "loopback",
    "auth": {
      "mode": "token",
      "token": "<your-gateway-token>"
    },
    "tailscale": {
      "mode": "off",
      "resetOnExit": false
    },
    "nodes": {
      "denyCommands": [
        "camera.list",
        "screen.record",
        "contacts.add",
        "calendar.add",
        "reminders.list",
        "sms.search"
      ]
    }
  }
}

Key design decisions:

  • bind: "loopback" — gateway only accepts connections from 127.0.0.1. No network exposure.
  • auth.mode: "token" — every WebSocket connection must present a valid token. Even locally, auth is enforced.
  • nodes.denyCommands — explicit blocklist for sensitive device capabilities. Defense in depth.

Managing the Gateway

# Check status (includes PID, bind address, probe result)
openclaw gateway status

# Lifecycle
openclaw gateway start
openclaw gateway stop
openclaw gateway restart

# The gateway runs as a macOS LaunchAgent:
# ~/Library/LaunchAgents/ai.openclaw.gateway.plist
# Auto-starts at login, restarts on crash

Deploying Ollama on Apple Silicon

Ollama provides a lightweight HTTP inference server that leverages Apple’s Metal GPU framework for hardware-accelerated inference on M-series chips.

Installation

brew install ollama

On macOS, Ollama installs as a background service that auto-starts at login. The inference server listens on http://localhost:11434.

Pulling Models

Choose models based on your RAM budget. With 36GB unified memory, you can comfortably run multiple 7-8B models simultaneously, or one 14B model alongside smaller ones.

# Code specialist — 14B parameter, 128k context
ollama pull qwen2.5-coder:14b    # 9.0 GB

# General reasoning — 8B parameter, 40k context  
ollama pull qwen3:8b              # 5.2 GB

# Long-context generalist — 8B, 128k context
ollama pull llama3.1:8b           # 4.9 GB

# Fast inference — 7B, 32k context
ollama pull mistral:7b            # 4.4 GB

Total disk: ~23.5 GB for four models covering code, reasoning, writing, and fast tasks.

Tuning for Multi-Model Concurrency

Set environment variables in the OpenClaw config to control Ollama’s behavior:

{
  "env": {
    "OLLAMA_API_KEY": "ollama-local",
    "OLLAMA_MAX_LOADED_MODELS": "3",
    "OLLAMA_KEEP_ALIVE": "-1"
  }
}
  • OLLAMA_MAX_LOADED_MODELS: "3" — allows up to 3 models loaded in memory simultaneously. On 36GB unified memory, this means ~18-20GB for models + headroom for the OS and other processes.
  • OLLAMA_KEEP_ALIVE: "-1" — models stay loaded indefinitely once used. Eliminates cold-start latency for frequently-used models.
  • OLLAMA_API_KEY — Ollama doesn’t require authentication by default, but OpenClaw’s model router expects an API key for every provider. Setting a dummy key satisfies the router.

Memory Budget (36GB Unified)

  • 3× 8B (quantized): ~15 GB used, ~21 GB remaining for OS
  • 1× 14B + 1× 8B: ~14 GB used, ~22 GB remaining
  • 1× 14B + 2× 8B: ~24 GB used, ~12 GB remaining
  • All 4 models: ~28 GB used, ~8 GB remaining (tight)

The sweet spot is 2-3 models loaded concurrently with OLLAMA_MAX_LOADED_MODELS=3. Ollama evicts the least-recently-used model when the limit is hit.

Configuring Auth for Local Models

OpenClaw’s model router uses a unified auth system — every model provider goes through the same auth pipeline. For local Ollama models, this means you need to configure credentials even though Ollama itself doesn’t require them.

{
  "env": {
    "ANTHROPIC_API_KEY": "sk-ant-...",
    "OLLAMA_API_KEY": "ollama-local"
  },
  "auth": {
    "profiles": {
      "anthropic:default": {
        "provider": "anthropic",
        "mode": "token"
      }
    }
  }
}

Without OLLAMA_API_KEY set, subagent spawns targeting ollama/qwen3:8b will fail at the auth layer before they ever reach the local inference server. The error surfaces as an auth/credentials failure, not a model availability error — which can be confusing to debug.

Quick Validation

# Verify Ollama is running
curl http://localhost:11434/api/tags

# Test inference directly
curl http://localhost:11434/api/generate 
  -d '{"model": "mistral:7b", "prompt": "Hello", "stream": false}'

# Verify OpenClaw can route to Ollama
openclaw gateway status  # Should show "RPC probe: ok"

Subagent Orchestration Patterns

OpenClaw’s subagent system allows any agent session to spawn child sessions on different models. The orchestrator (main agent on Claude Opus) delegates tasks to specialized workers running on local Ollama models.

The Orchestration Model

┌──────────────────────────────────────────┐
│          Main Agent (Claude Opus)         │
│       Orchestration + User Interaction    │
└──────┬──────┬──────┬──────┬──────────────┘
       │      │      │      │
       ▼      ▼      ▼      ▼
   ┌──────┐┌──────┐┌──────┐┌────────────┐
   │qwen3 ││llama ││mistr.││qwen-coder  │
   │ :8b  ││3.1:8b││ :7b  ││   :14b     │
   │Resrch││Write ││Quick ││   Code     │
   │Tasks ││Tasks ││Tasks ││  Tasks     │
   └──────┘└──────┘└──────┘└────────────┘

   All local. All free. All parallel.

Spawning a Subagent

sessions_spawn:
  label: "research-task"
  model: "ollama/qwen3:8b"
  task: "Research the top 5 Rust web frameworks and summarize pros/cons"
  runTimeoutSeconds: 300

Key parameters:

  • model — which model handles this subagent session
  • task — the prompt/instruction for the subagent
  • runTimeoutSeconds — hard timeout to prevent runaway sessions
  • label — human-readable identifier for tracking

Parallel Execution

Multiple subagents can run simultaneously. On 36GB RAM with OLLAMA_MAX_LOADED_MODELS=3, you can run 2-3 local subagents in parallel.

Results auto-announce back to the orchestrator. No polling required — OpenClaw uses push-based completion.

Timeout Strategy

  • Quick lookups, simple edits: 120s
  • Research, multi-step analysis: 300s
  • Installations, builds, large codegen: 600s

Delegation vs Direct Execution

The main agent session (running on a frontier model like Claude Opus) is the most expensive resource in the system. Every second the orchestrator spends executing a task is a second it can’t respond to the user.

Rule: If a task takes more than ~30 seconds of execution time, delegate it.

The Cost Equation

Cloud API (Claude Opus):
  - Input:  ~$15/M tokens
  - Output: ~$75/M tokens
  - Every orchestrator turn costs real money

Local Ollama:
  - Input:  $0
  - Output: $0  
  - Electricity cost only (~5W for inference on M3)
  - No rate limits, no quotas

A single Claude Opus response that takes 2 minutes of tool-calling might cost $0.50-2.00. The same work delegated to a local 8B model costs effectively nothing.

Model Selection Strategy

The Tier System

Tier 1: Frontier (Claude Opus 4) — Complex multi-step reasoning, nuanced user interaction, synthesis across multiple information sources. Cost: ~$15-75/M tokens.

Tier 2: Local Specialist (qwen2.5-coder:14b) — Code generation and refactoring, debugging with full file context (128k window). Cost: $0 (9GB VRAM).

Tier 3: Local Generalist (qwen3:8b, llama3.1:8b) — Research summarization, content writing, data extraction. Cost: $0 (~5GB VRAM each).

Tier 4: Local Fast (mistral:7b) — Quick classification, simple Q&A, text reformatting. Cost: $0 (4.4GB VRAM, fastest inference).

What Local Models Cannot Do

Small local models (7-8B parameters) have real limitations: deep research, complex planning, nuanced writing, and tool orchestration. Assigning these tasks to a local model doesn’t save money — it wastes time when the output is unusable.

Putting It All Together

Full Configuration Example

{
  "env": {
    "ANTHROPIC_API_KEY": "sk-ant-...",
    "OLLAMA_API_KEY": "ollama-local",
    "OLLAMA_MAX_LOADED_MODELS": "3",
    "OLLAMA_KEEP_ALIVE": "-1"
  },
  "auth": {
    "profiles": {
      "anthropic:default": {
        "provider": "anthropic",
        "mode": "token"
      }
    }
  },
  "agents": {
    "defaults": {
      "workspace": "~/.openclaw/workspace",
      "subagents": {
        "runTimeoutSeconds": 300
      }
    }
  },
  "gateway": {
    "port": 18789,
    "mode": "local",
    "bind": "loopback",
    "auth": {
      "mode": "token",
      "token": "<generate-a-random-token>"
    }
  }
}

Startup Checklist

# 1. Install OpenClaw
npm install -g openclaw
openclaw onboard

# 2. Install Ollama
brew install ollama

# 3. Pull your model fleet
ollama pull qwen2.5-coder:14b
ollama pull qwen3:8b
ollama pull llama3.1:8b
ollama pull mistral:7b

# 4. Verify Ollama is serving
curl -s http://localhost:11434/api/tags | jq '.models[].name'

# 5. Start the gateway
openclaw gateway start

# 6. Verify everything
openclaw gateway status

# 7. Open the webchat dashboard
open http://127.0.0.1:18789/

The Economics

Running this architecture on an M3 Pro MacBook:

  • Cloud cost: Only the orchestrator (Claude Opus) hits the API. With aggressive delegation, this might be 10-20% of total inference.
  • Local cost: Electricity only. M3 Pro draws ~5-15W during inference. At $0.30/kWh, that’s roughly $0.004/hour.
  • Effective multiplier: 4 simultaneous worker models + 1 orchestrator = 5× throughput at ~20% the cost of running everything on a frontier model.

The key insight: frontier models should think, not work. Let the cloud model make decisions and coordinate. Let the local models execute. The architecture pays for itself within days of heavy use.

By Xaden — March 2026. Stack: OpenClaw 2026.3.24, Ollama on macOS arm64, Claude Opus 4, Apple M3 Pro 36GB.

Spring ’26 Flow Builder Updates Every Admin Should Know

Spring ’26 Flow Builder Updates Every Admin Should Know

Hands typing on a laptop with workflow automation software on screen

If you’ve been building Flows for any length of time, you know the feeling: a canvas so cluttered with decision branches and loops that you can barely find the element you need to edit. Salesforce clearly heard that feedback, because the Spring ’26 release dropped some genuinely useful Flow Builder improvements that make daily life easier for admins and developers alike.

I’ve been testing these features since the sandbox preview, and a few of them have already changed how I build automations. Here’s what you need to know.

AI-Powered Flow Drafts Are Generally Available

This is the one that’s getting the most buzz, and honestly, it deserves it. You can now describe a business process in plain English and let Einstein generate a working draft flow for you. This feature moved to GA in Spring ’26 after spending a couple of releases in beta.

Here’s what this actually looks like in practice: you type something like “When an Opportunity closes, create a follow-up Task for the Account Owner due in 7 days” and the system spits out a record-triggered flow with the right object, trigger conditions, and a Create Records element already wired up.

AI-powered technology concept with robot and human collaboration

Is it perfect every time? No. You’ll still need to review the logic, check your field references, and test thoroughly. But it cuts that initial scaffolding time from 15-20 minutes down to about 2 minutes. For admins who build multiple flows per week, that adds up fast.

What’s even better is the iterative refinement piece. Instead of manually dragging elements around to modify an AI-generated flow, you can just tell Agentforce what to change. Something like “Add an email alert before the task creation step” and it updates the flow accordingly. It’s not replacing your skills – it’s handling the tedious parts so you can focus on the logic that actually matters.

If you’re new to flow terminology and want a quick reference for terms like “record-triggered flow” or “screen flow,” salesforcedictionary.com has clear definitions that cut through the jargon.

Collapsible Decision and Loop Elements

This one seems small, but it’s honestly my favorite update this release. Decision elements and Loop elements can now collapse into compact blocks on the canvas.

If you’ve ever worked on a flow with 8+ decision branches, you know how quickly the canvas turns into a mess. You’re constantly scrolling, zooming in and out, trying to remember which branch handles which scenario. Now you can fold those sections down when you’re not actively working on them.

Workflow diagram showing product brief and user goals in a clean layout

The practical benefit is huge for flow reviews, too. When you’re walking a stakeholder through your automation logic, you can collapse the sections they don’t need to see and focus on the parts that matter to them. It makes the whole conversation smoother.

Pair this with the improved mouse scrolling for canvas navigation, and complex flows feel a lot more manageable than they did in Winter ’26.

Screen Flows Got Some Real Upgrades

Screen Flows picked up several features that push them closer to being legitimate app-building tools. The highlights:

Data Table Sorting and Inline Editing (GA): Users can now sort columns in data tables at runtime, and – this is the big one – edit field values directly inside the table without needing a separate edit screen. If you’ve been building custom LWC components just to get inline editing in your flows, you can probably retire those now.

Kanban Board Component (Beta): You can drop a Kanban board right into a screen flow. Users drag and drop records between status columns – think Opportunities moving from Prospecting to Qualification, or Cases moving from New to In Progress. It’s in beta, so don’t put it in front of customers just yet, but it’s worth experimenting with in sandbox.

Direct URL Access (GA): Screen flows can now be launched directly via /lightning/flow/FlowName with query string parameters to pre-populate input variables. This means you can drop a flow link into an email, a Chatter post, or an external system and send users straight into the right flow with context already loaded. No more “click here, then navigate to this tab, then find the button.” Just one link.

CSS Styling Overrides: You can now apply component-level CSS to style individual flow screen components. Want your company’s brand colors on that approval form? You can do that without a custom component now.

For anyone keeping track of all these Flow-related terms and concepts, salesforcedictionary.com is a solid bookmark to have handy when you need a quick refresher.

Record-Triggered Flows Now Support File Triggers

This flew under the radar for a lot of people, but it’s a big deal for document-heavy orgs. Record-Triggered Flows can now fire on ContentDocument and ContentVersion objects. In plain terms: you can trigger automations when files are created or changed.

Laptop screen displaying code and data charts for document management

Think about the use cases here. A contract gets uploaded to an Opportunity? Automatically notify the legal team. A new profile photo gets added to a Contact record? Kick off a verification process. An engineer uploads a spec document? Auto-tag it and share it with the project team.

Before this update, you’d need Apex triggers or platform events to react to file changes. Now it’s fully declarative. That’s a win for admins who don’t want to depend on developers for every file-related automation.

Flow Orchestration Gets Easier to Build and Debug

Flow Orchestration has been around for a while, but adoption has been slow – partly because the building and debugging experience wasn’t great. Spring ’26 addresses both of those pain points.

You can now create orchestrations directly from the Automation Lightning App, which is where most admins already spend their time. No more hunting through Setup menus to find the right entry point.

The debugging improvements are even more useful. You can now set custom start and end points when debugging an orchestration, so you don’t have to run through the entire multi-step process just to test one segment. You can also complete work items during debug sessions, which means you can actually test the full end-to-end process without switching between debug mode and the live app.

If orchestration is a new concept for you, it’s essentially Salesforce’s way of handling multi-step, multi-user business processes – things like employee onboarding or complex approval chains where different people need to complete different steps in a specific order. Check salesforcedictionary.com for a breakdown of orchestration terminology if you want to get up to speed.

Debug Input Variables Now Persist Between Runs

One more quality-of-life improvement that deserves a mention: when you’re debugging a flow, your input variable values now stick around between test runs. Previously, every time you hit “Debug,” you had to re-enter all your test values from scratch. If you were testing a flow with 6-7 input variables, that got old real fast.

Now your debug values persist, so you can make a logic change, hit debug, and immediately see results without re-typing everything. It’s a small change that saves a surprising amount of time during development.

What This Means Going Forward

The pattern here is clear: Salesforce is investing heavily in making Flow Builder a more complete, more professional development environment. Between AI-assisted creation, better visual organization, richer screen components, and improved debugging, the gap between what you can build declaratively and what requires code keeps shrinking.

My recommendation? If you haven’t explored these features yet, spin up a Spring ’26 sandbox and spend an afternoon with them. The AI flow generation alone is worth the time investment, and the collapsible elements will change how you organize complex automations.

What Spring ’26 Flow features are you most excited about? Drop a comment below – I’d love to hear what’s making a difference in your org.

My Favorite Rails Productivity Gem: annotate_models

Very often I find myself working inside a Rails model, and I need to write a validation or a custom method. But I suddenly forget the exact name of my database column. Is it first_name or just name? Is the status an integer or a string?

Usually, to find this out, you have to open your db/schema.rb file, search for the table, and read the columns. It is not too hard to do it, but when you do it 50 times a day, it gets very annoying.

This is where the annotate_models (usually just called annotate) gem comes in. It is one of the first gems I add to any new Rails project. It automatically writes a commented-out summary of your database table right inside your model file.

Here is how to set it up and use it in 3 easy steps.

STEP 1: Installation

First off, let’s install the gem. We only need this in our development environment, because it just generates text comments for us developers to read. It doesn’t do anything in production.

Add the following to your Gemfile:

group :development do
  gem 'annotate'
end

and run bundle install in your terminal.

STEP 2: The Magic Command

Once the gem is installed, you can annotate your models manually by running this simple command in your terminal:

bundle exec annotate

What does this do? It looks at your database and updates your model files.
Before running the command, your User model might look like this:

# app/models/user.rb
class User < ApplicationRecord
  has_many :posts
end

After running annotate, the gem modifies the file to look like this:

# == Schema Information
#
# Table name: users
#
#  id         :integer          not null, primary key
#  name       :string
#  email      :string
#  created_at :datetime         not null
#  updated_at :datetime         not null
#

class User < ApplicationRecord
  has_many :posts
end

Now, every time you open your model, you can instantly see exactly what columns you have, what type they are, and if they have any limits or defaults.

STEP 3: Automate It (Auto-Annotate)

Running bundle exec annotate manually every time you create a new migration is easy to forget. It is much better to make Rails do it automatically.

To set up auto-annotation, run this generator command in your terminal:

rails g annotate:install

This creates a new file at lib/tasks/auto_annotate_models.rake.

Inside this file, you can configure exactly how you want the gem to behave. For example, I prefer my annotations at the very top of the file. You can find the position_in_class setting in that rake file and change it:

# lib/tasks/auto_annotate_models.rake
'position_in_class' => 'top', # You can change this to 'bottom' if you prefer

The best part: Because we ran the install generator, the annotate gem now hooks into ActiveRecord migrations automatically.

From now on, whenever you run:

rails db:migrate

…or…

rails db:rollback

Rails will automatically run the annotate gem in the background and update all your model comments to match your new database schema.

Bonus: It does more than just Models

If you look at the generated .rake file, you will see that annotate doesn’t just work on models. By default, it will also add these helpful schema comments to your:

  • RSpec/Minitest test files
  • FactoryBot factories
  • Model routing files

That’s pretty much it. Adding the annotate gem takes about 2 minutes, but it will save you hours of context-switching and searching through your schema.rb file. It is a massive productivity boost for any Rails developer.