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

goal task {task_id} has no canonical TaskKind::Goal row and

Error message

goal task {task_id} has no canonical TaskKind::Goal row and goal extension

What it means

ensure_goal_task_row is the fail-fast guard at the start of goal lifecycle transactions (pause_goal_task, resume_goal_task, set/get_continuation_context). It verifies the canonical TaskKind::Goal row AND its goal extension row exist; if either is missing the operation is refused before any status mutation, so no partial state is written.

Source

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

}

fn ensure_goal_task_row(conn: &Connection, task_id: &str) -> Result<()> {
    let exists = conn
        .query_row(
            "SELECT EXISTS(
                 SELECT 1
                   FROM tasks
                   JOIN goal_tasks ON goal_tasks.task_id = tasks.id
                  WHERE tasks.id = ?1
                    AND tasks.kind = 'goal'
             )",
            params![task_id],
            |row| row.get::<_, i64>(0),
        )
        .context("verify goal task row")?
        != 0;
    if !exists {
        anyhow::bail!("goal task {task_id} has no canonical TaskKind::Goal row and goal extension");
    }
    Ok(())
}

fn upsert_continuation_context(
    conn: &Connection,
    task_id: &str,
    context: &TaskContinuationContext,
) -> Result<()> {
    conn.execute(
        "INSERT INTO task_continuation_contexts (task_id, context_json)
         VALUES (?1, ?2)
         ON CONFLICT(task_id) DO UPDATE
             SET context_json = excluded.context_json",
        params![task_id, continuation_context_to_db(context)?],
    )
    .context("upsert task continuation context")?;
    Ok(())

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Verify the id exists and is TaskKind::Goal via the task list/get API before calling lifecycle methods
  2. If the extension row is missing, recreate the goal through the proper creation API rather than hand-INSERTing
  3. Check for concurrent deletion of the task
  4. Add a pre-flight check in operator tooling that lists goals and their extension status
Defensive patterns

Strategy: validation

Validate before calling

// pre-flight before lifecycle calls
if !matches!(store.get_task(id).await, Ok(Some(ref t)) if t.kind == TaskKind::Goal) {
    anyhow::bail!("{id} is not a goal task; nothing to pause/resume");
}
store.pause_goal_task(id, pause).await?;

Type guard

async fn is_goal_task(store: &SqliteTaskStore, id: &str) -> bool {
    matches!(store.get_task(id).await, Ok(Some(t)) if t.kind == TaskKind::Goal)
}

Try / catch

match store.pause_goal_task(id, pause).await {
    Err(ref e) if e.to_string().contains("has no canonical TaskKind::Goal row") => {
        // wrong id or structurally incomplete goal: verify and recreate via the creation API
    }
    other => other,
}

Prevention

When it happens

Trigger: Calling a goal lifecycle method with a task id that is not a goal task (wrong id, regular task, cross-environment paste) or whose goal extension row is missing (partial insert, manual DB edit, schema drift after migration).

Common situations: Task ids copied between environments; databases migrated or hand-edited; goals created through a path that skipped the extension insert; races with concurrent deletion.

Understand the failure class

Background: "Not found" and "does not exist" errors: why "Task not found", "No such folder", and "Can't find" fire when a lookup comes back empty — this error's family across 14 libraries.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/8dff8ecc7d46e411. Report an issue: GitHub.