BigPizzaV3/CodexPlusPlus · error · anyhow::Error

Remote Control recovery requires a valid thread id

Error message

Remote Control recovery requires a valid thread id

What it means

Thrown by validate_request in remote_control_recovery.rs, which guards every PendingRemoteControlRecovery record before it is persisted to the pending-recovery state file. The thread_id must be a non-blank string of at most 128 bytes after trimming; blank or oversized ids are rejected so the on-disk queue never holds records that can never be matched back to a real codex thread.

Source

Thrown at crates/codex-plus-core/src/remote_control_recovery.rs:150

        }
        Err(error) => Err(error.into()),
    }
}

fn save_state(path: &Path, state: &PendingRemoteControlRecoveryState) -> anyhow::Result<()> {
    if state.requests.is_empty() {
        match std::fs::remove_file(path) {
            Ok(()) => return Ok(()),
            Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
            Err(error) => return Err(error.into()),
        }
    }
    atomic_write(path, serde_json::to_string_pretty(state)?.as_bytes())
}

fn validate_request(request: &PendingRemoteControlRecovery) -> anyhow::Result<()> {
    if request.thread_id.trim().is_empty() || request.thread_id.len() > 128 {
        anyhow::bail!("Remote Control recovery requires a valid thread id");
    }
    if request.profile_id.trim().is_empty() || request.target_provider.trim().is_empty() {
        anyhow::bail!("Remote Control recovery requires profile and provider provenance");
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::tempdir;

    fn request(thread_id: &str) -> PendingRemoteControlRecovery {
        PendingRemoteControlRecovery {
            thread_id: thread_id.to_string(),
            profile_id: "official-mix".to_string(),
            target_provider: "custom".to_string(),
            config_generation: "generation".to_string(),

View on GitHub (pinned to 1f431ae49b)

Solutions

  1. Pass the real codex thread id (the session file stem / thread_id from the session list), non-empty and <= 128 bytes
  2. Trim the id at the call site before building the record
  3. If the id legitimately exceeds 128 bytes, store a hash or mapping instead of the raw value
  4. Add a UI-side length check so users see the problem before the request reaches the core

Example fix

// before
let req = PendingRemoteControlRecovery {
    thread_id: format!("{a}-{b}-{c}", /* ... */),
    // ...
};
// after
let thread_id = a.to_string(); // single real thread id
assert!(!thread_id.trim().is_empty() && thread_id.len() <= 128);
Defensive patterns

Strategy: validation

Validate before calling

fn valid_thread_id(id: &str) -> bool {
    !id.trim().is_empty() && id.len() <= 128
}

if !valid_thread_id(&req.thread_id) {
    return Err(anyhow::anyhow!("thread id must be 1..=128 bytes, got {}", req.thread_id.len()));
}

Type guard

fn valid_thread_id(id: &str) -> bool {
    !id.trim().is_empty() && id.len() <= 128
}

Try / catch

match record_pending_remote_control_recovery(path, req) {
    Err(e) if e.to_string().contains("valid thread id") => { /* drop the record, log thread_id length */ }
    other => other?,
}

Prevention

When it happens

Trigger: Calling record_pending_remote_control_recovery (or complete_pending_remote_control_recovery flows that re-validate) with a PendingRemoteControlRecovery whose thread_id.trim().is_empty() is true, or whose thread_id length exceeds 128 bytes (e.g. an id built by concatenating ids, or a whole session title pasted as the id).

Common situations: Frontend sends an empty string because the thread was never opened; a bridge caller passes null coerced to ""; the id is generated by joining multiple identifiers and grows past 128 bytes; whitespace-only id from a mis-trimmed form field.

Related errors


AI-assisted analysis of BigPizzaV3/CodexPlusPlus@1f431ae49b (2026-08-16). Data as JSON: /api/errors/bee405bf4b4a89a6. Report an issue: GitHub.