Hmbown/CodeWhale · error

Cannot follow up agent

Error message

Cannot follow up agent {agent_id}: status is {} and the child cannot resume

What it means

Thrown by `followup_child` in the subagent registry when a caller sends a follow-up (`agents/followup`) to a child agent whose status is a terminal state: Completed, Failed, Cancelled, or BudgetExhausted. Terminal records are immutable receipts, not mailboxes, so the message surface fails closed rather than acknowledging undeliverable work. Only Running, Interrupted, or Interrupted-with-checkpoint children can accept follow-ups.

Solutions

  1. Check the child's status before following up and use the continue/continue_agent path for Completed or Interrupted children instead of followup.
  2. Start a new child agent and deliver the message as its initial task if the old child is terminal.
  3. If the child finished, incorporate its recorded output/receipt rather than sending more mail.
  4. If BudgetExhausted, raise the budget or spawn a fresh agent with a larger allowance.

Example fix

// before
let receipt = registry.followup_child(&agent_ref, text)?;
// after
let status = registry.status_of(&agent_ref);
let receipt = if matches!(status, SubAgentStatus::Running | SubAgentStatus::Interrupted(_)) {
    registry.followup_child(&agent_ref, text)?
} else {
    registry.continue_agent(&agent_ref, text)? // resume path for terminal/interrupted children
};
Defensive patterns

Strategy: validation

Validate before calling

let status = registry.status_of(&agent_ref);
if matches!(status, SubAgentStatus::Completed | SubAgentStatus::Failed(_) | SubAgentStatus::Cancelled | SubAgentStatus::BudgetExhausted) {
    // terminal: use continue_agent or spawn a new child instead
}

Type guard

fn is_resumable(status: &SubAgentStatus) -> bool {
    !matches!(status, SubAgentStatus::Completed | SubAgentStatus::Failed(_) | SubAgentStatus::Cancelled | SubAgentStatus::BudgetExhausted)
}

Try / catch

match registry.followup_child(&agent_ref, text) {
    Ok(receipt) => /* delivered or queued */,
    Err(e) if e.to_string().contains("cannot resume") => {
        let _ = registry.continue_agent(&agent_ref, text); // fall back to resume path
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling `SubagentRegistry::followup_child(agent_ref, text)` (or the `agents/followup` tool) when the resolved child already finished successfully, failed, was cancelled, or exhausted its budget.

Common situations: A UI or orchestrator races a follow-up against a child that just finished; an agent was cancelled by the user before the follow-up arrived; a retry loop keeps messaging an agent that already failed; a budget-exhausted child is still addressed by stale code paths.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/53ca85a9135fcd6f. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/tools/subagent/mod.rs:5845

        self.queue_running_parent_message(&agent_id, text)
    }

    /// Queue mail and attempt a live wake (`agents/followup`).
    pub fn followup_child(&mut self, agent_ref: &str, text: String) -> Result<ParentMailReceipt> {
        let agent_id = self.resolve_agent_ref(agent_ref)?;
        let status = self
            .agents
            .get(&agent_id)
            .map(|agent| agent.status.clone())
            .ok_or_else(|| anyhow!("Agent {agent_id} not found"))?;
        if matches!(
            status,
            SubAgentStatus::Completed
                | SubAgentStatus::Failed(_)
                | SubAgentStatus::Cancelled
                | SubAgentStatus::BudgetExhausted
        ) {
            return Err(anyhow!(
                "Cannot follow up agent {agent_id}: status is {} and the child cannot resume",
                subagent_status_name(&status)
            ));
        }
        let mut receipt = self.queue_parent_message(&agent_id, text.clone(), true)?;
        let has_input_tx = self
            .agents
            .get(&agent_id)
            .is_some_and(|agent| agent.input_tx.is_some());
        let continuation_handle = self.agents.get(&agent_id).and_then(|agent| {
            agent.checkpoint.as_ref().and_then(|cp| {
                (cp.continuable && !cp.messages.is_empty()).then(|| cp.continuation_handle.clone())
            })
        });
        let continuable = continuation_handle.is_some();

        match status {
            SubAgentStatus::Running if has_input_tx => {

View on GitHub (pinned to 73e0f67d83)