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

  1. Check the id in the log; inspect the row: sqlite queries against messages_out for that id
  2. Mark it failed or delete it so it stops appearing: it will retry otherwise
  3. Avoid writing to session DBs from outside host/container code
  4. 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

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

Related errors


AI-assisted analysis of nanocoai/nanoclaw@294ef2aee8 (2026-08-28). Data as JSON: /api/errors/2377ac4e8ee20440. Report an issue: GitHub.