nanocoai/nanoclaw · warning
Malformed outbound row — delivering best-effort
Error message
Malformed outbound row — delivering best-effort
What it means
A row in outbound.db failed strict parsing when reading pending messages. Instead of failing the whole drain, the wrapper coerces fields best-effort so the row flows through normal per-message retry/mark-failed containment. One bad row doesn't block delivery of the rest.
Source
Thrown at src/mailbox/sqlite/index.ts:367
id: row.id,
sequence: row.seq,
inReplyTo: row.in_reply_to,
timestamp: sqliteTimestamp(row.timestamp),
deliverAfter: row.deliver_after === null ? null : sqliteTimestamp(row.deliver_after),
recurrence: row.recurrence,
kind: row.kind,
platformId: row.platform_id,
channelType: row.channel_type,
threadId: row.thread_id,
content: row.content,
}),
);
} catch (err) {
// One malformed row must not block the whole delivery queue: fall
// back to a best-effort read so the row goes through the normal
// per-message retry → mark-failed containment instead of throwing
// out of the entire drain on every poll.
log.warn('Malformed outbound row — delivering best-effort', { id: String(row.id), err });
const text = (value: unknown): string =>
typeof value === 'string' ? value : value == null ? '' : String(value);
const nullableText = (value: unknown): string | null => (value == null ? null : String(value));
return {
id: text(row.id),
kind: text(row.kind),
platformId: nullableText(row.platform_id),
channelType: nullableText(row.channel_type),
threadId: nullableText(row.thread_id),
content: text(row.content),
inReplyTo: nullableText(row.in_reply_to),
};
}
}),
writeDirect: async (message) => {
const writer = writable();
const sequence = nextSequence();
const record = createDirectOutboundRecord(message, sequence, new Date().toISOString());View on GitHub (pinned to 294ef2aee8)
Solutions
- Check the id in the log; inspect the row: sqlite queries against messages_out for that id
- Mark it failed or delete it so it stops appearing: it will retry otherwise
- Avoid writing to session DBs from outside host/container code
- Rebuild/upgrade agent-runner if a version wrote bad rows
Defensive patterns
Strategy: fallback
Validate before calling
// inspect suspicious rows before they clog the queue SELECT id, kind, typeof(text) FROM messages_out WHERE ok=0;
Type guard
function isWellFormedOutboundRow(row: unknown): boolean {
const r = row as Record<string, unknown>;
return typeof r.id === 'string' && typeof r.kind === 'string';
} Try / catch
try { parseStrict(row); } catch { deliverBestEffort(coerce(row)); /* goes through retry/mark-failed */ } Prevention
- Never write to session DBs outside host/agent-runner code
- Keep agent-runner image and host versions in lockstep
- Mark chronically malformed rows failed so drains stay clean
When it happens
Trigger: A messages_out row has unexpected types (numbers in text columns, null in required fields) — from a buggy agent-runner version, disk corruption, or manual DB edits.
Common situations: Upgrading agent-runner with a schema drift, or someone editing outbound.db directly.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- Delivery batch-preview hook failed
- Post-delivery hook failed
- Failed to clear orphan processing claims
- Pre-existing FK violations carried through migration (not in
- Delivery action denied by guard
AI-assisted analysis of nanocoai/nanoclaw@294ef2aee8 (2026-08-28).
Data as JSON: /api/errors/2377ac4e8ee20440.
Report an issue: GitHub.