Status: Research complete Topic: agents Type: Audit & Debugging Date: 2026-04-20 Last-Validated: 2026-05-21 Original-Query: What silent failures exist in the ZAO agent system? Find and prioritize fixes. (reconstructed) Tier: v2-ready Tool:
general-purposesubagent runningsilent-failure-hunterpersona (ECC plugin agent not in registered subagent export — workaround used) Scope:src/lib/agents/+ 3 admin API routes
| Finding | Severity | Fix Priority |
|---|---|---|
| runner.ts:128 — Burn failure ignored after swap | CRITICAL | FIX THIS WEEK. Tokenomics broken (deflationary mechanism bypassed). |
| runner.ts:93 — Auto-stake failure unguarded, trade continues | CRITICAL | FIX THIS WEEK. Agent buys ZABAL unstaked. |
| autostake.ts:43-75 — Auto-stake catch swallows failure, no DB record | HIGH | Fix same PR as runner.ts:93. |
events.ts:19-23 — logAgentEvent silently drops DB insert errors |
HIGH | Fix this week. Audit trail loss. |
| burn.ts:31-35 — Burn error returns null, no DB log | MEDIUM | Fix same PR as runner.ts:128. |
| runner.ts:143 — Farcaster post failure doesn't fail trade | MEDIUM | Accept. Log only (trades > casts). |
| broadcast/route.ts:65-72 — audit logged after response | MEDIUM | Wrap in .catch() safe fire-and-forget. |
agents/route.ts:30-42 — Promise.allSettled falls back to empty arrays on DB failure |
MEDIUM | Throw on rejection instead of silent fallback. |
Totals: 2 critical, 2 high, 4 medium, 0 low = 8 findings.
| Option | Scope | Pro | Con |
|---|---|---|---|
| A. Fix all 8 in one PR | big | fastest to clean state | larger review surface |
| B. Criticals only (runner.ts + autostake.ts + burn.ts + events.ts) in PR 1, rest in PR 2 | focused | easier review | 2 PRs |
| C. One PR per fix | atomic | tiniest reviews | 8 PRs, overhead |
Recommendation: B — criticals first for merge-to-prod speed, medium fixes as follow-up.
File: src/lib/agents/runner.ts:128
const hash = await executeSwap(agentName, quote);
await burnZabal(agentName, BigInt(quote.buyAmount));
await logAgentEvent({
tx_hash: hash,
status: 'success', // <-- marked success even if burn failed
});What breaks: burnZabal() returns null on failure (burn.ts:34). Trade logs as success regardless. ZABAL meant to burn accumulates in agent wallet.
Who won't notice: No one — no event logged, no alert. Tokenomics deflationary mechanism broken silently. Days or weeks of unexecuted burns.
Fix:
const hash = await executeSwap(agentName, quote);
const burnHash = await burnZabal(agentName, BigInt(quote.buyAmount));
if (!burnHash) {
throw new Error('Burn failed after successful swap — trade marked failed');
}File: src/lib/agents/runner.ts:93
try {
// Auto-stake check (14-day cycle)
await maybeAutoStake(agentName);What breaks: maybeAutoStake() catches internally (autostake.ts:72-75), logs, returns. If Privy wallet rejects the stake tx, function exits normally. Runner proceeds to buy ZABAL. ZABAL accumulates unstaked.
Who won't notice: Admin dashboard shows no stake event (correct), no failure event either (missing). Agent keeps buying.
Fix: Change autostake.ts to throw on failure (see HIGH 1 below). Runner will then catch + halt cycle.
File: src/lib/agents/autostake.ts:43-75
try {
await executeSwap(...approve data...);
await executeSwap(...stake data...);
await logAgentEvent({ status: 'success' });
logger.info(`Auto-staked 100M ZABAL...`);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
logger.error(`[${agentName}] Auto-stake failed: ${msg}`);
}What breaks: Catch block only logs. No logAgentEvent({ status: 'failed' }). No rethrow. Caller (runner.ts:93) sees success.
Fix:
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
await logAgentEvent({
agent_name: agentName,
action: 'add_lp',
status: 'failed',
error_message: msg,
});
logger.error(`[${agentName}] Auto-stake failed: ${msg}`);
throw err; // <-- CRITICAL: stop caller from assuming success
}File: src/lib/agents/events.ts:19-23
export async function logAgentEvent(params: {...}) {
const db = getSupabaseAdmin();
const { error } = await db.from('agent_events').insert(params);
if (error) {
logger.error(`[${params.agent_name}] Failed to log event: ${error.message}`);
}
}What breaks: Insert fails (DB down, RLS, constraint), function logs and returns void. All callers await logAgentEvent(...) expecting durable log, get nothing. Audit trail silently incomplete.
Fix:
if (error) {
throw new Error(`Failed to log agent event: ${error.message}`);
}Risk: throwing might break some callers. Triage per call site — most callers should propagate (this is load-bearing audit).
File: src/lib/agents/burn.ts:31-35
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
logger.error(`[${agentName}] Burn failed: ${msg}`);
return null;
}What breaks: Null return, no agent_events row. Caller must check for null (currently doesn't — see CRITICAL 1).
Fix: Pair with CRITICAL 1 fix. Add logAgentEvent({ status: 'failed', ... }) before return null, OR change to throw so caller must handle.
Accept. Trades shouldn't depend on cast success. Log only.
File: src/app/api/admin/broadcast/route.ts:65-72
const data = await res.json();
await logAuditEvent({...});
return NextResponse.json({ success: true, hash: data.cast?.hash });What breaks: audit await'd but after response commits. If logAuditEvent throws, response already sent successful, audit missing.
Fix:
logAuditEvent({...}).catch(err => logger.error('Audit log failed:', err));
return NextResponse.json({ success: true, ... });File: src/app/api/admin/agents/route.ts:30-42
const [configResult, eventsResult] = await Promise.allSettled([...]);
const configs = configResult.status === 'fulfilled' ? (configResult.value.data ?? []) : [];
const events = eventsResult.status === 'fulfilled' ? (eventsResult.value.data ?? []) : [];What breaks: DB down → admin sees empty list, no error. Can't tell "no agents" from "DB failure".
Fix:
if (configResult.status === 'rejected') throw configResult.reason;
if (eventsResult.status === 'rejected') throw new Error('Failed to fetch agent events');Tool used: Agent tool with subagent_type: general-purpose, prompt embedded silent-failure-hunter persona. ECC plugin's silent-failure-hunter agent exists as a file but isn't registered as a subagent by the plugin (only ~40 of 48 agents registered). Mid-session copy to ~/.claude/agents/silent-failure-hunter.md doesn't register until next Claude Code restart.
Time taken: ~50s for full sweep of 11 files.
Fixes touch:
src/lib/agents/runner.ts— 2 critical fixessrc/lib/agents/autostake.ts— 1 high fixsrc/lib/agents/burn.ts— 1 medium fixsrc/lib/agents/events.ts— 1 high fix (load-bearing)src/app/api/admin/broadcast/route.ts— 1 medium fixsrc/app/api/admin/agents/route.ts— 1 medium fix
Pair with:
- Vitest tests under
src/lib/agents/__tests__/andsrc/app/api/admin/__tests__/ evals/fixtures (doc 441) once populated with real scenarios
- ECC
silent-failure-hunteragent definition:~/.claude/plugins/cache/everything-claude-code/everything-claude-code/1.10.0/agents/silent-failure-hunter.md - ZAO OS docs 441/442/448 (ECC integration)
- Silent failure anti-patterns reference