unicity-aos/aos-ce · error

grant pending read error for capsule_id

Error message

grant pending read error for capsule_id '{capsule_id}', surfacing a fresh prompt: {e}

What it means

grant_pending reads the KV grant-pending record for a capsule_id to decide whether to resume a pending grant prompt. On a read error (not a clean miss), it logs this warning and returns false, which surfaces a fresh prompt to the user instead of resuming — a fail-open-to-fresh-prompt behavior to avoid getting stuck.

Solutions

  1. Restore KV availability per the {e} detail and retry the grant flow; the user will be re-prompted either way.
  2. Delete the stale/corrupt pending key for that capsule_id so subsequent reads are clean misses.
  3. Check for version drift between the writer and reader of the pending record schema.
  4. Accept the fresh-prompt behavior if the pending grant was stale; complete the new grant flow.

Example fix

// before
Err(e) => {
    log::warn(format!("{}: grant pending read error for capsule_id '{capsule_id}', \
         surfacing a fresh prompt: {e}", crate::profile::log_tag()));
    false
}
// after
// clear stale record then retry:
kv::delete(&grant_key(capsule_id)).ok();
Err(e) => {
    log::warn(format!("{}: grant pending read error for capsule_id '{capsule_id}', \
         surfacing a fresh prompt: {e}", crate::profile::log_tag()));
    false
}
Defensive patterns

Strategy: try-catch

Validate before calling

// probe grant state store before initiating a grant flow
const reachable = await kv.ping();
if (!reachable) throw new Error('kv unavailable; grant will surface a fresh prompt');

Try / catch

match kv::get(&grant_key) {
    Ok(Some(v)) => resume_grant(v),
    Ok(None) => surface_fresh_prompt(),
    Err(e) => { log::warn("grant pending read error: {e}"); surface_fresh_prompt() }
}

Prevention

When it happens

Trigger: KV read of the grant-pending entry for capsule_id returns Err in grant_pending — storage backend error, value decode failure, or I/O problem on the record; also reachable after a prior flow's kv::delete raced with the read.

Common situations: KV outage mid-grant flow; pending record written by an older broker version that no longer deserializes; concurrent grant flows deleting the key while another reads it.

Understand the failure class

Background: Database query failed: Internal Server Error 500s wrapping SQL, Prisma, and connection failures — what to check first — this error's family across 16 libraries.

Related errors


AI-assisted analysis of unicity-aos/aos-ce@f6f22024fb (2026-09-13). Data as JSON: /api/errors/3563d461462e3c68. Report an issue: GitHub.

Appendix: source

Thrown at crates/aos-mcp-broker/src/execute.rs:548

    let Some(key) = grant_pending_key(principal, capsule_id) else {
        return false;
    };
    match kv::get_bytes_opt(&key) {
        Ok(Some(bytes)) => {
            if marker_is_fresh(&bytes) {
                true
            } else {
                // Stale (or unparseable / clock-unavailable) marker: the paired
                // respond never cleared it. Treat as not pending and best-effort
                // delete so the next ungranted call re-prompts — the self-heal
                // that keeps a dropped respond from wedging the pair forever.
                let _ = kv::delete(&key);
                false
            }
        }
        Ok(None) => false,
        Err(e) => {
            log::warn(format!(
                "{}: grant pending read error for capsule_id '{capsule_id}', \
                 surfacing a fresh prompt: {e}",
                crate::profile::log_tag()
            ));
            false
        }
    }
}

/// Consume the outstanding grant-consent prompt marker for
/// `(principal, capsule_id)`, returning whether one existed.
///
/// Called by [`crate::approval::handle_mcp_grant_respond`] on BOTH approve and
/// deny so the marker is single-use and can never stick: a declined prompt must
/// not leave a marker that suppresses every future grant prompt for the pair.
/// The return value is informational (the grant itself is driven by the
/// published decision, not this marker); clearing the marker is the effect that
/// matters here.

View on GitHub (pinned to f6f22024fb)