Minimal agent observability & self-healing for AI coding agents.
OpenLog gives every agent session a flight recorder. Agents describe what went wrong in plain text. A fuzzy indexer classifies failures into a self-improving registry. Before the next session starts, relevant failure history is injected into the agent's context — so it learns from past mistakes without anyone maintaining a database.
Zero external dependencies. No LLM in the pipeline. Works offline, in CI, on a plane.
When you run AI agents (Claude Code, Codex, custom agents), failures happen. Today:
- Failures produce unstructured logs nobody reads
- The same failure hits the same agent type over and over
- Diagnosis is manual — 15-45 minutes per incident
- Knowledge stays in your head, not in the system
OpenLog fixes this with a three-step loop:
Agent fails → logs event → Indexer classifies → Registry learns
↓
Next agent starts ← Injector provides context ← Registry queried
The system gets smarter with every session. No human effort required after setup.
# From source (development)
git clone https://github.com/OpenCnid/openlog.git
cd openlog
pip install -e ".[dev]"# From PyPI (when published)
pip install openlog-agentRequirements: Python 3.10+. No other dependencies.
fromopenlogimportlog_event# When something failslog_event('error', 'circular import between auth and user modules',
ref='task-1',
stderr='Error: Cannot resolve module ./user from auth.ts',
exit_code=1)
# When a task completeslog_event('complete', 'refactored auth module successfully',
ref='task-1',
exit_code=0)Events are appended to .openlog/events/ in the current directory as JSONL (one JSON object per line).
openlog index --allThe indexer reads all unprocessed event files, matches failure descriptions against the fingerprint registry using fuzzy string matching, and creates or updates fingerprint entries.
openlog statusOutput:
Sessions: 4
Events: 12
Fingerprints: 3 (2 confirmed, 1 provisional)
openlog inject code-reviewOutput (only shows confirmed fingerprints — provisional ones need 3 occurrences to confirm):
Previous runs of [code-review] tasks encountered these failure patterns:
1. circular-import (seen 14x, last: 2026-04-04)
Description: circular import between auth module and user types
Remedy: Check barrel files, split shared types into types.ts
2. rate-limited (seen 7x, last: 2026-04-01)
Description: rate limit hit after 3 retries
Remedy: Add exponential backoff, check provider limits
Paste this into the agent's system prompt before it starts. That's the whole feedback loop.
An event is a single JSON line written by log_event():
{"ts": 1720000000, "kind": "error", "ref": "task-1", "parent": null, "f_raw": "circular import between auth and user", "stderr": "Cannot resolve module", "exit_code": 1}| Field | Type | Required | Description |
|---|---|---|---|
ts | int | auto | Unix timestamp (seconds) |
kind | string | yes | One of: spawn, output, decision, error, complete |
ref | string | no | Identifier for the current unit of work |
parent | string | no | Ref of the parent task (enables execution tree reconstruction) |
f_raw | string | yes for errors | Freeform description of what happened. No controlled vocabulary — write whatever makes sense. |
stderr | string | no | Raw stderr output, auto-truncated to 500 chars |
exit_code | int | no | Process exit code |
data | string | no | Additional context |
The agent's only job is to describe what happened. No taxonomy to memorize, no categories to pick from. Just write f_raw in 5-10 words.
A fingerprint is a canonical failure class in the registry. The indexer creates them automatically from f_raw values:
{
"circular-import-between-auth-module-and": {
"patterns": [
"circular import between auth module and user types",
"circular import between auth and user module"
],
"count": 4,
"status": "confirmed",
"last_seen": "2026-04-04",
"remedy": null,
"severity": null
}
}- patterns — all known phrasings that map to this fingerprint. Grows automatically.
- status —
provisional(count < 3) orconfirmed(count ≥ 3). Only confirmed fingerprints are injected. - remedy — human-written fix description. Added during monthly review.
- severity — auto-derived from outcomes:
fatal(>60% non-zero exit codes) orrecoverable.
When the indexer sees a new f_raw, it:
- Substring check (fast): Does any existing pattern appear as a substring? If yes → match.
- Trigram similarity (fallback): Compute Jaccard similarity of character trigrams. If ≥ 0.7 → match.
- No match: Create a new provisional fingerprint seeded with this
f_raw.
When a match is found, the new phrasing is added to the pattern list only if it would not already match via an existing pattern (dedup). This means the registry converges — more runs = better matching = fewer false positives.
The indexer also checks for internal contradictions, no LLM required:
- Exit code contradiction: Agent reported "success" but had a non-zero exit code
- Description contradiction:
f_rawandstderrdescribe very different things (trigram similarity < 0.3) - Incomplete output: Fewer
outputevents than expected for the task type
Contradictions are logged as mismatch events — they're a signal that the agent misreported.
Run the post-session indexer. Classifies new events and updates the fingerprint registry.
openlog index --all # Process all unindexed sessions
openlog index --session abc123 # Process a specific sessionOutput a pre-session context block for the given task type.
openlog inject code-review # Context for code review tasks
openlog inject feature-build # Context for feature builds
openlog inject --limit 5 # Show top 5 instead of default 3Returns empty output if no confirmed fingerprints exist. This is normal for a fresh install.
Generate the monthly review document.
openlog report # Current month
openlog report --month 2026-04 # Specific monthCreates .openlog/reports/{YYYY-MM}.md with:
- Summary (session count, event count, fingerprint stats)
- Singletons (fingerprints that appeared only once — noise?)
- Merge candidates (similar fingerprints that might be the same thing)
- Missing remedies (confirmed fingerprints without a fix description)
- Ineffective remedies (remedy exists but failure keeps recurring)
- Staleness (fingerprints not seen in 60+ days)
- Recommended actions (top 5 highest-impact items)
Time needed: ~5 minutes per month. Optional but high-leverage.
Seed .openlog/ with sample data for testing the full pipeline.
openlog seed # Generate sample sessions and configLog a single event from any language — bash, Node, Python, anything with shell access.
openlog log error "circular import between auth and user" --ref task-1 --exit-code 1
openlog log error "API returned 429" --ref api-call --stderr "HTTP 429 Too Many Requests"
openlog log complete"refactored auth module" --ref task-1 --exit-code 0
openlog log spawn "starting codex for feature build" --ref feature-build --data codex| Argument | Required | Description |
|---|---|---|
kind | yes | One of: spawn, output, decision, error, complete |
message | yes | Description of what happened |
--ref | no | Task/step identifier |
--parent | no | Parent task ref |
--stderr | no | Raw error output |
--exit-code | no | Process exit code |
--data | no | Additional context |
Tail a log file and automatically convert error/exception/fatal lines to OpenLog events.
openlog watch /var/log/app.log --source my-app # tail forever
openlog watch output.log --source codex --once # read to end and exit
openlog watch server.log --source api --from-start # read from beginning| Argument | Required | Description |
|---|---|---|
file | yes | Log file path to watch |
--source | no | Label for events (default: "log-watcher") |
--interval | no | Poll interval in seconds (default: 1.0) |
--from-start | no | Start from beginning of file |
--once | no | Read to end and exit (no tail) |
Show current state at a glance.
openlog statusOutput:
Sessions: 12
Events: 47
Fingerprints: 8 (5 confirmed, 3 provisional)
# Pre-session: get context
CONTEXT=$(openlog inject code-review)# Run agent with injected context
claude --permission-mode bypassPermissions --print "$CONTEXTYour actual task here..."# Post-session: index new events
openlog index --allfromopenlogimportlog_eventimportsubprocess# Pre-session injectionresult=subprocess.run(['openlog', 'inject', 'feature-build'], capture_output=True, text=True)
context=result.stdout# ... run your agent, passing context into its prompt ...# In-agent error loggingtry:
do_work()
exceptExceptionase:
log_event('error', f'failed to {task}: {type(e).__name__}',
stderr=str(e)[:500], exit_code=1)
raise# Post-session indexingsubprocess.run(['openlog', 'index', '--all'])#!/bin/bash# wrap-agent.sh — Run any command with OpenLog observability
TASK_TYPE="${1:?Usage: wrap-agent.sh <task-type> <command...>}"shiftecho"=== OpenLog: Pre-session context ==="
openlog inject "$TASK_TYPE"echo"=== Running agent ===""$@"
EXIT_CODE=$?echo"=== OpenLog: Post-session indexing ==="
openlog index --all
exit$EXIT_CODERun the indexer on a schedule to catch events from background agents:
# Every 2 hours
0 */2 ***cd /your/project && openlog index --all| Variable | Default | Description |
|---|---|---|
OPENLOG_DIR | .openlog/ (relative to CWD) | Override the data directory location |
.openlog/config.json (optional — the system works without it):
{
"similarity_threshold": 0.7,
"injection_limit": 3,
"injection_max_chars": 1200,
"alert_on_mismatch": false
}All runtime data lives in .openlog/ relative to your working directory:
.openlog/
├── events/ # Raw JSONL, one file per agent session
│ ├── 2026-04-04-abc1.jsonl
│ └── 2026-04-05-def2.jsonl
├── trees/ # Reconstructed execution trees
│ └── abc1.json
├── fingerprints.json # Canonical fingerprint registry
├── indexed.log # List of processed session IDs
├── config.json # Optional configuration
└── reports/ # Monthly review documents
└── 2026-04.md
Add .openlog/ to your .gitignore. The registry is runtime data, not source code.
┌──────────────────────────────────────────────────┐
│ IN-SESSION (agent) │
│ │
│ from openlog import log_event │
│ log_event('error', 'what happened', exit_code=1) │
│ → appends JSONL to .openlog/events/ │
│ │
└──────────────────┬────────────────────────────────┘
│ post-session
▼
┌──────────────────────────────────────────────────┐
│ INDEXER (openlog index) │
│ │
│ 1. Validate: exit codes, f_raw vs stderr │
│ 2. Fingerprint: substring → trigram → new entry │
│ 3. Update: count, last_seen, pattern list │
│ 4. Promote: provisional → confirmed at count ≥ 3 │
│ │
└──────────────────┬────────────────────────────────┘
│ pre-session
▼
┌──────────────────────────────────────────────────┐
│ INJECTOR (openlog inject) │
│ │
│ Read registry → rank by severity/count/recency │
│ → output ≤ 300 token context block to stdout │
│ → paste into agent's system prompt │
│ │
└──────────────────────────────────────────────────┘
Why no LLM in the classification pipeline? The indexer must work offline, in CI, during API outages, and on machines with no API keys. String similarity gets 90% of the way for 0% of the cost. LLM classification can be added as an optional fallback later.
Why fuzzy matching instead of exact categories? Agents describe failures differently across sessions and across models. "circular import between files" and "import cycle in auth module" are the same failure class. Fuzzy matching handles vocabulary drift; exact matching doesn't.
Why provisional → confirmed promotion? One-off events are noise. Recurring patterns are signal. The threshold of 3 prevents the registry from filling with transient errors while ensuring real patterns get captured.
Why ≤ 300 tokens for injection? Agent context windows are expensive. The injector gives a nudge, not a briefing. More isn't better — it's more cost and more distraction.
Why log_event() never raises? Observability must not break the work it observes. If logging fails (disk full, permissions), the agent continues working. Silent skip is always better than crashing the task.
# Clone and install with dev dependencies
git clone https://github.com/OpenCnid/openlog.git
cd openlog
pip install -e ".[dev]"# Run tests
pytest
# Run tests with verbose output
pytest -v71 tests covering: JSONL validation, fuzzy matching, pattern dedup, provisional promotion, structural validator, injector caps, malformed input handling, and full end-to-end pipeline.
Everything lives in .openlog/ relative to your working directory. Here's how to inspect it:
openlog statusSessions: 12
Events: 47
Fingerprints: 8 (5 confirmed, 3 provisional)
Events are plain JSONL — one JSON object per line, one file per agent session:
# List all session files
ls .openlog/events/
# Read a specific session
cat .openlog/events/2026-04-04-abc123.jsonl
# Find all errors across all sessions
grep '"kind": "error"' .openlog/events/*.jsonl
# Find errors mentioning "import"
grep -i 'import' .openlog/events/*.jsonl# See all classified failure patterns
cat .openlog/fingerprints.json | python3 -m json.tool
# Quick summary: which fingerprints exist and their status
python3 -c "import jsonwith open('.openlog/fingerprints.json') as f: d = json.load(f)for slug, fp in d['fingerprints'].items(): print(f'{fp[\"status\"]:12s} count={fp[\"count\"]:3d} {slug}')"Output:
confirmed count= 14 circular-import-between-auth-module
confirmed count= 7 rate-limit-hit-after-3-retries
provisional count= 1 webpack-bundle-size-exceeded
If your events use ref and parent fields, the indexer reconstructs the execution tree:
cat .openlog/trees/abc123.json | python3 -m json.tool# Generate the report
openlog report
# Read it
cat .openlog/reports/2026-04.md# This is the exact text block that gets injected into the prompt
openlog inject feature-buildIf this outputs nothing, either no fingerprints are confirmed yet (need 3+ occurrences) or the registry is empty.
The contrib/ directory contains ready-to-use scripts for integrating OpenLog into an existing agentic system. Copy and customize for your setup.
Automatically adds OpenLog pre/post hooks to Claude Code, Codex, Pi, or any CLI agent:
# Claude Code
./contrib/agent_wrapper.sh code-review claude --permission-mode bypassPermissions --print "review auth module"# Codex
./contrib/agent_wrapper.sh feature-build codex exec"build the auth module"# Codex with full-auto
./contrib/agent_wrapper.sh refactor codex --full-auto "refactor the database layer"# Any agent binary
./contrib/agent_wrapper.sh test-suite my-agent run --task "write integration tests"What it does:
- Pre-session: Runs
openlog inject <task-type>and prints known failure patterns - Run: Executes your command, captures stderr
- Post-session: Logs success/failure via
openlog logwith exit code and stderr - Classify: Runs
openlog index --allto update the fingerprint registry
Setup:
cp contrib/agent_wrapper.sh ~/bin/agent_wrapper.sh
chmod +x ~/bin/agent_wrapper.shChecks that your infrastructure is healthy and logs the results to OpenLog:
- Is the gateway process running and responding?
- Is the
.openlog/directory writable? - Are there any recent cron failures?
# Run once (e.g., from your main agent's heartbeat)
python3 contrib/heartbeat_hook.py
# Run via system cron every 6 hours
0 */6 ***cd /your/project && python3 contrib/heartbeat_hook.py && openlog index --allHealthy output: logs a complete event. Unhealthy: logs an error event with what failed.
Tails the OpenClaw gateway's stderr and converts error/exception/fatal lines into OpenLog events automatically.
# Start as a background process
nohup ./contrib/openclaw_watcher.sh &# Or watch a specific log file
./contrib/openclaw_watcher.sh /path/to/openclaw.logThis captures gateway crashes, plugin failures, channel disconnects, and any other system-level errors that happen outside agent sessions.
Here's every step from fresh install to the feedback loop closing:
# 1. Install
pip install openlog-agent
cd /your/project
# 2. First agent run — no injection yet (empty registry)
openlog inject feature-build
# (no output — nothing to inject yet)
claude --permission-mode bypassPermissions --print "build auth module"# Agent runs. Hits a circular import error. Finishes.# 3. Log the error (agent does this, or wrapper does it)
openlog log error "circular import between auth and user" --ref task-1 --exit-code 1
# 4. Index
openlog index --all
openlog status
# Sessions: 1, Events: 1, Fingerprints: 1 (0 confirmed, 1 provisional)# 5. Repeat 2 more times with similar errors...# (Different agents, different sessions, same failure pattern)
openlog index --all
openlog status
# Sessions: 3, Events: 3, Fingerprints: 1 (1 confirmed, 0 provisional)# ^^^ promoted at count=3!# 6. NOW the injection works
openlog inject feature-build
# Previous runs of [feature-build] tasks encountered these failure patterns:# 1. circular-import (seen 3x, last: 2026-04-04)# Description: circular import between auth and user# Remedy: No remedy documented yet# 7. Add a remedy (edit fingerprints.json directly)# Change "remedy": null to "remedy": "Check barrel files, split shared types"# 8. Next agent gets the full context + fix
openlog inject feature-build
# 1. circular-import (seen 3x, last: 2026-04-04)# Description: circular import between auth and user# Remedy: Check barrel files, split shared types ← agent now knows the fix# 9. Monthly review
openlog report
cat .openlog/reports/2026-04.mdOpenLog is the recording layer — the flight recorder for agent systems. Here's the full vision:
✅ Event capture (5 kinds, JSONL, any language) ✅ Fuzzy classification (substring + trigram, self-improving registry) ✅ Context injection (pre-session, ≤300 tokens) ✅ Structural validation (contradiction detection) ✅ Monthly reports ✅ CLI + Python API ✅ Pipeline self-monitoring (staleness detection via heartbeat)
- Cross-session querying — "show me all rate-limit errors this week"
- Trend detection — "error rate is increasing on code-review tasks"
- Correlation analysis — "errors spike when using model X on task type Y"
- Duration tracking — "this task type is getting slower over time"
- Cost correlation — "failures cost $X in wasted tokens"
- Agent tree viewer — see parent-child agent relationships as a tree
- Timeline view — Gantt-style chart of agent sessions with error markers
- Error hotspot map — which task types and agent types fail most
- Registry dashboard — fingerprint growth, match rates, pattern convergence
- Think Jaeger or Datadog, but for agent orchestration.
- Trace IDs across agent spawns — when an orchestrator spawns a coding agent, the trace ID propagates. When that agent spawns a sub-task, the span chains.
- Standard wire format — compatible with OpenTelemetry trace context so agent traces can live alongside service traces.
- Requires integration with orchestrators (OpenClaw, omc, LangGraph, CrewAI).
OpenTriage (planned — companion project)
OpenLog records and classifies. It doesn't act. OpenTriage is the separate system that consumes OpenLog data and makes decisions:
- Auto-remediation — known failure + known remedy → retry with the fix injected
- Escalation — novel failure or failed remedy → alert the human
- Novel pattern capture — draft new failure catalog entries for human review
- Budget enforcement — kill runaway agents that exceed cost or time thresholds
OpenTriage is intentionally a separate package from OpenLog. Install both or just OpenLog — your choice:
pip install openlog-agent # just recording (zero deps, works offline)
pip install opentriage # intelligence layer (auto-installs openlog-agent)Separation of concerns:
| OpenLog | OpenTriage | |
|---|---|---|
| Does | Records, classifies, injects | Decides, acts, remediates |
| Intelligence | Zero (string matching) | LLM-based (tiered models) |
| Can go wrong | Misclassification (detectable, fixable) | Wrong action (may need rollback) |
| Alignment surface | None (too dumb to misalign) | Real (needs trust model + circuit breakers) |
| Dependencies | Python stdlib only | LLM API, orchestrator access, openlog-agent |
| Works offline | Yes | No |
OpenTriage needs its own trust model: bounded authority, circuit breakers, cost caps, human escalation paths. It needs to be watched. OpenLog doesn't, because it can't break anything — it only appends data and matches strings.
The recursion stops here: OpenLog watches agents. OpenTriage watches OpenLog data and acts. OpenLog's heartbeat hook watches that events are flowing. OpenTriage's circuit breakers watch OpenTriage. Each layer is simpler than the thing it monitors.
Pull requests welcome. Run pytest before submitting. Zero external dependencies is a hard constraint — if it can't be done with stdlib, it doesn't go in the core.
MIT