nanocoai/nanoclaw · warning

reject-with-reason: cannot reach approver, finalizing plain

Error message

reject-with-reason: cannot reach approver, finalizing plain reject

What it means

A reject-with-reason flow was armed, but the approver cannot be reached: either no DM channel exists for the approver user (ensureUserDm returned null) or no delivery adapter is registered (getDeliveryAdapter returned null). The system degrades gracefully by finalizing the rejection without capturing a reason.

Source

Thrown at src/modules/approvals/reason-capture.ts:92

  } catch {
    return '';
  }
}

/**
 * Begin the reject-with-reason hold for an approval the admin chose not to
 * finalize outright. Prompts the admin's DM, then parks the row and arms
 * capture. If we can't reach the admin (no DM, no adapter, delivery throws) we
 * finalize a plain reject immediately rather than strand the requesting agent.
 */
export async function armReasonCapture(approval: PendingApproval, session: Session, userId: string): Promise<void> {
  const expiresAt = new Date(Date.now() + REASON_CAPTURE_WINDOW_MS).toISOString();
  if (!(await markApprovalAwaitingReason(approval.approval_id, expiresAt))) return;

  const dm = userId ? await ensureUserDm(userId) : null;
  const adapter = getDeliveryAdapter();
  if (!dm || !adapter) {
    log.warn('reject-with-reason: cannot reach approver, finalizing plain reject', {
      approvalId: approval.approval_id,
      userId,
      hasDm: Boolean(dm),
      hasAdapter: Boolean(adapter),
    });
    await finalizeReject(approval, session, userId, undefined, 'awaiting_reason');
    return;
  }

  try {
    await adapter.deliver(dm.channel_type, dm.platform_id, null, 'chat-sdk', JSON.stringify({ text: PROMPT_TEXT }));
  } catch (err) {
    log.error('reject-with-reason: reason prompt delivery failed, finalizing plain reject', {
      approvalId: approval.approval_id,
      err,
    });
    await finalizeReject(approval, session, userId, undefined, 'awaiting_reason');
    return;

View on GitHub (pinned to 294ef2aee8)

Solutions

  1. Wire a DM channel for the approver (e.g. run /init-first-agent) so ensureUserDm can resolve a DM
  2. Verify the delivery adapter for the approver's channel is registered in the channel registry
  3. Check the user_dms table for a stale/missing entry for the approver user id
Defensive patterns

Strategy: validation

Validate before calling

const dm = userId ? await ensureUserDm(userId) : null;
const adapter = getDeliveryAdapter();
if (!dm || !adapter) {
  // degrade before arming: reject without reason flow
  await finalizeReject(approval, session, userId, undefined, 'no_dm');
  return;
}

Prevention

When it happens

Trigger: An approver clicks 'reject with reason' but the approver has no cached DM (user_dms) and no DM-capable channel adapter is registered, so the reason prompt cannot be delivered.

Common situations: Fresh install with no DM channel wired to the approver; channel adapters not loaded; approver identity exists only on a channel that was removed.

Related errors


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