shareAI-lab/learn-claude-code · error · ValueError

Invalid task ID: {task_id!r}

Error message

Invalid task ID: {task_id!r}

What it means

Raised by TaskStore._path() in s10_task_system/code.py:90 when task_id is not a string or does not fully match TASK_ID_PATTERN, r'^task_[0-9a-f]{8}$' — exactly the literal prefix 'task_' followed by 8 lowercase hexadecimal characters. All store APIs (exists, load, save, delete, transition helpers) build the file path through this gate, so any malformed ID is rejected before touching the filesystem. The {!r} in the message shows the offending value with its Python repr.

Source

Thrown at s10_task_system/code.py:90

    owner: str | None
    blockedBy: list[str]


class TaskStore:
    def __init__(self, directory: Path):
        self.directory = directory

    def _root(self, create: bool = False) -> Path:
        if create:
            self.directory.mkdir(parents=True, exist_ok=True)
        root = self.directory.resolve()
        if not root.is_relative_to(WORKDIR.resolve()):
            raise ValueError("Task store escapes the workspace")
        return root

    def _path(self, task_id: str, create_root: bool = False) -> Path:
        if not isinstance(task_id, str) or not TASK_ID_PATTERN.fullmatch(task_id):
            raise ValueError(f"Invalid task ID: {task_id!r}")
        root = self._root(create=create_root)
        path = (root / f"{task_id}.json").resolve()
        if not path.is_relative_to(root):
            raise ValueError(f"Invalid task ID: {task_id!r}")
        return path

    def exists(self, task_id: str) -> bool:
        return self._path(task_id).is_file()

    def create(self, subject: str, description: str = "",
               blocked_by: list[str] | None = None) -> Task:
        subject = subject.strip()
        if not subject:
            raise ValueError("Task subject cannot be empty")

        dependencies = list(dict.fromkeys(blocked_by or []))
        for dependency in dependencies:
            if not self.exists(dependency):

View on GitHub (pinned to 985456f4ad)

Solutions

  1. Only use IDs returned by TaskStore.create(), which are always 'task_' + 8 lowercase hex chars.
  2. Normalize before calling: strip whitespace and lowercase the hex portion, then re-check the pattern.
  3. If you import external IDs, maintain a mapping table to freshly created task_ IDs instead of forcing foreign formats through the API.

Example fix

# before
store.load('TASK_ab12cd34')

# after
import re
TASK_ID = re.compile(r'^task_[0-9a-f]{8}$')
tid = task_id.strip().lower()
if isinstance(task_id, str) and TASK_ID.fullmatch(tid):
    task = store.load(tid)
else:
    raise ValueError(f'not a valid task id: {task_id!r}')
Defensive patterns

Strategy: type-guard

Validate before calling

import re
from s10_task_system.code import TASK_ID_PATTERN

def valid_task_id(task_id) -> bool:
    return isinstance(task_id, str) and bool(TASK_ID_PATTERN.fullmatch(task_id))

Type guard

from typing import Any
import re
_TASK_ID = re.compile(r'^task_[0-9a-f]{8}$')

def is_task_id(value: Any) -> bool:
    return isinstance(value, str) and bool(_TASK_ID.fullmatch(value))

Try / catch

try:
    task = store.load(task_id)
except ValueError as e:
    if 'Invalid task ID' in str(e):
        return None  # treat as not-found; log the repr for debugging
    raise

Prevention

When it happens

Trigger: Calling store.load('42'), store.load('task_ABCD1234') (uppercase hex), 'task_12345' (too short), 'task_12345678g' (non-hex 'g'), a value with whitespace or a path suffix, or a non-string like None or an int. IDs originate from secrets.token_hex(4) at creation, so hand-typed or externally imported IDs are the usual offenders.

Common situations: Hardcoding IDs from another store or an older format; passing a UI-provided or LLM-provided identifier without normalization; copy/paste typos including the leading/trailing spaces or uppercase; feeding a bg_ background-task ID (s11) into the task store by mistake.

Related errors


AI-assisted analysis of shareAI-lab/learn-claude-code@985456f4ad (2026-08-14). Data as JSON: /api/errors/e46f0c03e7e2dccf. Report an issue: GitHub.