zeroclaw-labs/zeroclaw · error · anyhow::Error

goal task {task_id} is terminal or missing

Error message

goal task {task_id} is terminal or missing

What it means

pause_goal_task runs in one transaction: it first validates the goal row (ensure_goal_task_row), then sets the canonical status to Paused via update_task_status_record, which only matches existing, non-terminal tasks. Zero rows means the task is already terminal (done/cancelled/failed) or was deleted; the transaction rolls back, so the pause is atomic with the pause-record write that follows — no partial pause state.

Source

Thrown at crates/zeroclaw-runtime/src/control_plane/task_store_sqlite/goal.rs:538

    async fn update_goal_pause(&self, task_id: &str, pause: Option<GoalPauseState>) -> Result<()> {
        let conn = self.conn.lock();
        let updated = update_goal_pause_record(&conn, task_id, pause)?;
        if updated == 0 {
            anyhow::bail!("goal task {task_id} has no goal extension row");
        }
        Ok(())
    }

    async fn pause_goal_task(&self, task_id: &str, pause: GoalPauseState) -> Result<()> {
        let mut conn = self.conn.lock();
        let tx = conn
            .transaction()
            .context("start pause goal task transaction")?;
        ensure_goal_task_row(&tx, task_id)?;
        let updated = update_task_status_record(&tx, task_id, TaskStatus::Paused, None, None)?;
        if updated == 0 {
            anyhow::bail!("goal task {task_id} is terminal or missing");
        }
        let updated = update_goal_pause_record(&tx, task_id, Some(pause))?;
        if updated == 0 {
            anyhow::bail!("goal task {task_id} has no goal extension row");
        }
        tx.commit().context("commit pause goal task transaction")?;
        Ok(())
    }

    async fn resume_goal_task(
        &self,
        task_id: &str,
        owner_pid: u32,
        owner_boot_id: &str,
        continuation_context: Option<TaskContinuationContext>,
    ) -> Result<()> {
        let mut conn = self.conn.lock();
        let tx = conn

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Refresh the task status first; if terminal, skip pausing — there is nothing to pause
  2. Make operator tooling idempotent: treat 'terminal or missing' as already stopped
  3. If the task should still be live, check for concurrent deletion and recreate it

Example fix

// before
store.pause_goal_task(id, pause).await?; // hard-fails on finished goals

// after
if let Some(t) = store.get_task(id).await? {
    if t.kind == TaskKind::Goal && !t.status.is_terminal() {
        store.pause_goal_task(id, pause).await?;
    }
}
Defensive patterns

Strategy: validation

Validate before calling

if let Some(t) = store.get_task(id).await? {
    if t.kind == TaskKind::Goal && !t.status.is_terminal() {
        store.pause_goal_task(id, pause).await?;
    }
}

Type guard

fn is_pauseable(t: &Task) -> bool {
    t.kind == TaskKind::Goal && !t.status.is_terminal()
}

Try / catch

match store.pause_goal_task(id, pause).await {
    Err(ref e) if e.to_string().contains("is terminal or missing") => {
        // already stopped (or deleted): treat as idempotent success for operator tooling
    }
    other => other,
}

Prevention

When it happens

Trigger: Pausing a goal that already completed, failed, or was cancelled; pausing an id deleted concurrently; stale ids from an earlier run or dashboard.

Common situations: Operator UI racing an agent finishing the goal; double-submitted pause commands; stale dashboards after goal completion.

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 zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/b561f92205a3873d. Report an issue: GitHub.