observability — logging, audit, metrics, healthLa Legge 3 — tracciabilità — dice che myclaw preserva il proprio audit log e non occulta le proprie azioni. Qui la implementiamo: append-only, firmata, queryable, backed-up. Più tre canali complementari: structured logging per debug, metrics per monitoring, digest umano perché Roberto possa chiedere a colpo d'occhio "cosa ha combinato oggi?".
/metrics./health.| Fonte | Registro | Consumatori |
|---|---|---|
| Trace + decisioni (agent runtime) | Audit JSONL in workspace/.audit/ | Legge 3, eval, fitness darwiniana, digest, forensica |
| Eventi operativi (errori, warnings, stati) | structlog → stdout → journald | Debug, journalctl, troubleshooting |
| Counter numerici (trace count, cost, latency) | Prometheus metrics su /metrics | Monitoring, dashboard |
Più componenti scrivono journal (neuroni, sinapsi, trace, approvazioni). Per evitare che il lettore li tratti come equivalenti, qui la gerarchia canonica:
| Livello | Sorgente | Chi scrive | Derivabile? |
|---|---|---|---|
| 1 — ombrello | .audit/*.jsonl (trace, approval, workspace_edit, constitution_modified) | Observability — scrive eventi a valle di Gateway/Runtime/Approval | no: è la fonte autoritativa |
| 2 — rollup | Oggetto ExecutionTrace JSONL immutabile per-turno (contenuto dentro .audit) | Runtime a fine turno, poi Observability lo inoltra al JSONL | no: è la proiezione per-turno del livello 1 |
| 3 — derivato | episodic.db (memory), co_activations.sqlite (neuron §6), grafo sinapsi (synapse) | Memory / Neuron / Synapse come propri worker | sì: in caso di perdita, ricostruibili da (1)+(2) con un replay |
/opt/myclaw/workspace/.audit/
├── 2026-04.jsonl # traces del mese corrente
├── 2026-03.jsonl # precedenti
├── approvals.jsonl # storico approvazioni (separato per query veloci)
├── workspace.jsonl # modifiche ai 5 file markdown
├── constitution.jsonl # modifiche a SOUL.md (raro, importante)
├── budget.json # contatori correnti (non log)
└── archived/ # episodi retention-spostati da memory
// esempio: un trace chiusa
{
"kind": "trace",
"trace_id": "01HQWK3...",
"session_id": "s_4a7b...",
"channel": "telegram:@roberto",
"started_at": "2026-04-22T14:03:22Z",
"finished_at": "2026-04-22T14:03:28Z",
"user_message": "scarica il rapporto di marzo",
"final_response": "Fatto. Scaricati 2.4 MB in ~/downloads/rapporto_marzo.pdf.",
"steps_count": 5,
"tool_calls": [
{"name": "web_fetch", "args_summary": "drive.google.com/...", "outcome": "success"},
{"name": "fs_write", "args_summary": "~/downloads/rapporto_marzo.pdf", "outcome": "success"}
],
"cost_usd": 0.023,
"wall_time_ms": 5840,
"outcome": "success",
"approvals_requested": 1,
"approvals_granted": 1
}
// esempio: una approvazione
{
"kind": "approval",
"request_id": "ar_...",
"trace_id": "01HQWK3...",
"sender": "telegram:@roberto",
"tool_name": "fs_write",
"effect_class": "fs_write:~/downloads/*",
"granted": true,
"via": "batch",
"batch_window_expires_at": "2026-04-22T14:13:22Z",
"at": "2026-04-22T14:03:24Z"
}
// esempio: modifica workspace
{
"kind": "workspace_edit",
"file": "MEMORY.md",
"author": "agent|manual",
"trace_id": "01HQWK3...", // null se manual
"size_before": 3421,
"size_after": 3498,
"diff_stat": "+3 -0",
"approved_by_request_id": "ar_...",
"at": "2026-04-22T14:03:28Z"
}
// esempio: constitution
{
"kind": "constitution_modified",
"hash_before": "sha256:...",
"hash_after": "sha256:...",
"diff": "...", // pieno
"at": "2026-04-22T22:15:00Z",
"notes": "Added Legge 4 omeostasi"
}
O_APPEND); ogni write è atomica.at ISO8601 UTC. Line-parsing = record-parsing (JSON per riga).rm -f .audit/*, viene bloccato al livello sandbox (forbidden
path implicito — l'agente non vede .audit/ del tutto per design).
Per il debug e il troubleshooting. Non sostituisce l'audit.
import structlog
logger = structlog.get_logger(__name__)
# uso tipico
logger.info(
"tool_call_completed",
trace_id=str(trace.trace_id),
tool_name="fs_read",
wall_ms=42,
)
logger.warning(
"rate_limit_hit",
sender=sender,
counter=current,
limit=limit,
)
journald. Query via journalctl --user -u myclaw-gateway.SystemMaxUse=500M in drop-in).| Livello | Esempio | Default production |
|---|---|---|
debug | Dump completo prompt LLM | off |
info | Eventi normali: tool call, turn start/end | on |
warning | Rate limit, budget approaching, retry | on |
error | Tool failure, policy deny con contesto | on |
critical | Constitution modified, audit corruption detection | on |
Endpoint: GET /metrics (auth admin). Formato Prometheus text
exposition.
# HELP myclaw_traces_total Numero totale di trace chiuse
# TYPE myclaw_traces_total counter
myclaw_traces_total{outcome="success"} 1247
myclaw_traces_total{outcome="tool_hallucination"} 3
myclaw_traces_total{outcome="policy_denied"} 12
myclaw_traces_total{outcome="user_aborted"} 5
myclaw_traces_total{outcome="budget_hit"} 1
myclaw_traces_total{outcome="timeout_steps"} 2
# HELP myclaw_cost_eur_today Costo aggregato di oggi
# TYPE myclaw_cost_eur_today gauge
myclaw_cost_eur_today 1.23
# HELP myclaw_trace_latency_ms Latenza wall della trace (p50/p95/p99)
# TYPE myclaw_trace_latency_ms summary
myclaw_trace_latency_ms{quantile="0.5"} 2100
myclaw_trace_latency_ms{quantile="0.95"} 6200
myclaw_trace_latency_ms{quantile="0.99"} 11400
# HELP myclaw_active_sessions Sessioni attualmente attive
# TYPE myclaw_active_sessions gauge
myclaw_active_sessions{channel="telegram"} 1
myclaw_active_sessions{channel="cli"} 0
# HELP myclaw_budget_remaining_eur Budget rimanente oggi
# TYPE myclaw_budget_remaining_eur gauge
myclaw_budget_remaining_eur{cap="soft"} 0.77
myclaw_budget_remaining_eur{cap="hard"} 3.77
# HELP myclaw_tool_calls_total Chiamate per tool
# TYPE myclaw_tool_calls_total counter
myclaw_tool_calls_total{tool="fs_read"} 423
myclaw_tool_calls_total{tool="shell_run"} 128
myclaw_tool_calls_total{tool="web_fetch"} 67
# ...
# HELP myclaw_approvals_total Approvazioni chieste/concesse/negate
# TYPE myclaw_approvals_total counter
myclaw_approvals_total{verdict="granted",via="explicit"} 89
myclaw_approvals_total{verdict="granted",via="batch"} 214
myclaw_approvals_total{verdict="denied"} 7
myclaw_approvals_total{verdict="timeout"} 3
# HELP myclaw_llm_tier_calls_total Chiamate LLM per tier
# TYPE myclaw_llm_tier_calls_total counter
myclaw_llm_tier_calls_total{tier="local-fast"} 1876
myclaw_llm_tier_calls_total{tier="frontier"} 1203
/metrics e basta. Se un giorno Roberto
vuole dashboard ricche, un Prometheus esterno scrapa. Intanto, ordine di
grandezza + trend dall'output text è sufficiente.
GET /health
{
"status": "ok", // "ok" | "degraded" | "down"
"version": "0.0.1",
"uptime_s": 86430,
"gateway_pid": 12345,
"workspace": {
"mounted": true,
"soul_hash_matches": true // SOUL.md non modificato senza restart
},
"suprastructure": {
"reachable": true,
"llm_default_provider": "anthropic",
"stt_default": "faster-whisper",
"last_successful_call_at": "2026-04-22T14:02:18Z"
},
"channels": {
"cli": "listening",
"telegram": "pump_active"
},
"memory": {
"episodic_db_size_mb": 8.2,
"embeddings_db_size_mb": 3.4,
"last_reflection": "2026-04-22T03:00:00Z",
"pending_proposals": 7
},
"budget": {
"soft_cap_eur": 2.0,
"hard_cap_eur": 5.0,
"spent_today_eur": 1.23,
"override_active": false
},
"recent_errors_1h": 0
}
status è "degraded" se: budget soft hit, un canale
non risponde, supra non raggiungibile ma riprovabile. "down"
se: SOUL.md manca, workspace non scrivibile, supra irraggiungibile per > 5
minuti.
Ogni sera alle 22:00 (o quando Roberto chiede myclaw digest today):
un messaggio strutturato, breve, con l'essenziale. Questa è la reificazione
della critica #5 del giudizio (audit JSONL illeggibile come "cosa ha fatto oggi
il mio maggiordomo").
myclaw · digest · 2026-04-22
📨 Richieste gestite: 23
├─ 21 completate con successo
├─ 1 annullata (tu hai negato)
└─ 1 fallita (tool hallucination, LLM inventava un tool)
🔧 Tool usati più di 3 volte:
- fs_read (8)
- supra_llm (23 — ogni Thought)
- web_fetch (4)
- shell_run (5)
✅ Approvazioni:
- 14 concesse esplicitamente
- 3 approvate via batch (fs_write:~/downloads/*)
- 1 negata (tentativo di scrivere in /opt/suprastructure/)
💰 Costo: 1.23 € (soft cap 2€)
⏱ Latenza p95: 6.2s (stabile)
🧠 Memoria:
- 23 nuovi episodi indicizzati
- 7 proposte di reflection pronte (digest mattutino approverà)
⚠ Cose notevoli:
- Alle 14:03 tentativo di indirect prompt injection da una pagina web
(pattern "IGNORA ISTRUZIONI" intercettato dal boundary untrusted). Nessun effetto.
- Alle 19:47 Tutor mode è scattato per fs_write:~/downloads/*
(10a approvazione consecutiva). Tu hai confermato, tutto regolare.
Nessuna constitution o workspace edit.
🟢 stato: regolare.
Il digest viene costruito da un job cron notturno che legge audit JSONL del
giorno e produce il testo via LLM tier summarize-rich (~0.01€
per digest) con template strutturato.
YYYY-MM.jsonl).YYYY-MM.jsonl.gz).# ~/.config/systemd/user/myclaw-backup.service
[Unit]
Description=myclaw daily backup
Requires=myclaw-gateway.service
[Service]
Type=oneshot
ExecStart=/bin/bash -c '\
tar czf /var/backups/myclaw/workspace-$(date +%%Y%%m%%d-%%H%%M).tar.gz \
/opt/myclaw/workspace/ \
&& find /var/backups/myclaw/ -name "workspace-*.tar.gz" -mtime +30 \
! -newermt $(date -d "first day of month" +%%Y-%%m-%%d) \
-delete'
# ~/.config/systemd/user/myclaw-backup.timer
[Unit]
Description=Daily workspace backup
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.target
Retention: ultimi 30 daily + primo di ogni mese per 12 mesi.
from typing import Protocol, Literal
class AuditLog(Protocol):
async def append(self, record: dict) -> None:
"""Append atomico di un record JSON. record['kind'] obbligatorio."""
...
async def query(
self,
kind: str | None = None,
since: datetime | None = None,
until: datetime | None = None,
sender: str | None = None,
limit: int = 100,
) -> list[dict]: ...
async def verify_integrity(self) -> IntegrityReport:
"""Controlla gaps nei timestamp, JSON malformed, size anomalies."""
...
class Metrics(Protocol):
def inc(self, name: str, labels: dict | None = None, value: float = 1.0) -> None: ...
def gauge(self, name: str, value: float, labels: dict | None = None) -> None: ...
def observe(self, name: str, value: float, labels: dict | None = None) -> None: ...
def render_prometheus(self) -> str: ...
class HealthChecker(Protocol):
async def check(self) -> HealthStatus: ...
@dataclass
class HealthStatus:
status: Literal["ok", "degraded", "down"]
details: dict
checked_at: datetime
class DigestBuilder(Protocol):
async def build_daily(self, date: date, sender: str) -> str:
"""Costruisce il digest markdown per un giorno specifico."""
...
| Invariante | Test |
|---|---|
| Append atomico | 2 task concorrenti che appendono 1000 record → file finale ha esattamente 2000 righe valide JSON, ordine temporale coerente. |
| No delete sull'audit | Tentativo di rm .audit/* dalla sandbox → bloccato (.audit non bindato nel bwrap). |
| Integrity check passa su file valido | File JSONL corretto → verify_integrity() ritorna ok=True. |
| Integrity check segnala gap | File con un record corrotto → report con corrupted_line_numbers. |
| Metrics endpoint richiede auth | GET /metrics senza bearer token → 401. |
| Health down se SOUL.md manca | Rinominare SOUL.md → /health restituisce status=down entro 30s. |
| Backup timer attivo | systemctl --user list-timers mostra myclaw-backup.timer. |
| Digest produttibile offline | myclaw digest 2026-04-22 funziona anche se supra è down (usa tier local-fast). |
| Rotation mensile | Primo del mese alle 00:01 → file YYYY-MM.jsonl rinominato .gz, nuovo file nascere. |
| Records monotoni in timestamp | Append di 100 record in 10s → ogni record ha at ≥ del precedente. |
| Riferimento | Cosa abbiamo preso |
|---|---|
| OpenHands event stream | Append-only come pattern primario di trace. |
| Prometheus exposition format | Metrics naming convention. |
| structlog | Logging strutturato. |
| systemd.journald | Log destination + rotation integrata. |
| Giudizio di fase 0 — critica #5 UI | Audit leggibile come "cosa ha fatto oggi" (§7 activity digest). |
| Legge 3 (tracciabilità) | Il motivo per cui esiste questo doc. |
| Memory §4 | Trace chiuse → episodes; stessa sorgente serve due consumer. |
| Policy §5 | budget.json è scritto dalla Policy, letto da metrics + health + digest. |
myclaw — observability microprogettazione v1.0 — 2026-04-22
Fase 2 completa. Prossimo: pairing.html (fase 3).