{"record":{"id":"0ca67a09fb5bdf8c","repo":"nanocoai/nanoclaw","slug":"reject-with-reason-cannot-reach-approver-finaliz","errorCode":null,"errorMessage":"reject-with-reason: cannot reach approver, finalizing plain reject","messagePattern":"reject-with-reason: cannot reach approver, finalizing plain reject","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"src/modules/approvals/reason-capture.ts","lineNumber":92,"sourceCode":"  } catch {\n    return '';\n  }\n}\n\n/**\n * Begin the reject-with-reason hold for an approval the admin chose not to\n * finalize outright. Prompts the admin's DM, then parks the row and arms\n * capture. If we can't reach the admin (no DM, no adapter, delivery throws) we\n * finalize a plain reject immediately rather than strand the requesting agent.\n */\nexport async function armReasonCapture(approval: PendingApproval, session: Session, userId: string): Promise<void> {\n  const expiresAt = new Date(Date.now() + REASON_CAPTURE_WINDOW_MS).toISOString();\n  if (!(await markApprovalAwaitingReason(approval.approval_id, expiresAt))) return;\n\n  const dm = userId ? await ensureUserDm(userId) : null;\n  const adapter = getDeliveryAdapter();\n  if (!dm || !adapter) {\n    log.warn('reject-with-reason: cannot reach approver, finalizing plain reject', {\n      approvalId: approval.approval_id,\n      userId,\n      hasDm: Boolean(dm),\n      hasAdapter: Boolean(adapter),\n    });\n    await finalizeReject(approval, session, userId, undefined, 'awaiting_reason');\n    return;\n  }\n\n  try {\n    await adapter.deliver(dm.channel_type, dm.platform_id, null, 'chat-sdk', JSON.stringify({ text: PROMPT_TEXT }));\n  } catch (err) {\n    log.error('reject-with-reason: reason prompt delivery failed, finalizing plain reject', {\n      approvalId: approval.approval_id,\n      err,\n    });\n    await finalizeReject(approval, session, userId, undefined, 'awaiting_reason');\n    return;","sourceCodeStart":74,"sourceCodeEnd":110,"githubUrl":"https://github.com/nanocoai/nanoclaw/blob/294ef2aee85218b23ad30eda9dfe10e590b54a8c/src/modules/approvals/reason-capture.ts#L74-L110","documentation":"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.","triggerScenarios":"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.","commonSituations":"Fresh install with no DM channel wired to the approver; channel adapters not loaded; approver identity exists only on a channel that was removed.","solutions":["Wire a DM channel for the approver (e.g. run /init-first-agent) so ensureUserDm can resolve a DM","Verify the delivery adapter for the approver's channel is registered in the channel registry","Check the user_dms table for a stale/missing entry for the approver user id"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"const dm = userId ? await ensureUserDm(userId) : null;\nconst adapter = getDeliveryAdapter();\nif (!dm || !adapter) {\n  // degrade before arming: reject without reason flow\n  await finalizeReject(approval, session, userId, undefined, 'no_dm');\n  return;\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Wire a DM channel for approvers at install time (/init-first-agent)","Monitor logs for 'cannot reach approver' to catch missing DM wiring early"],"tags":["approvals","dm","delivery-adapter","fallback"],"backgroundTag":"no-delivery-channel-available","analyzedSha":"294ef2aee85218b23ad30eda9dfe10e590b54a8c","analyzedAt":"2026-08-28T13:59:10.357Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}