can1357/oh-my-pi · error · ValueError

tasks must be a list

Error message

tasks must be a list

What it means

parse_todo_phase() validates the shape of a TodoPhase RPC payload. When the "tasks" key is present but is not a JSON list, the parser refuses to coerce it and raises ValueError, since tasks must be a list of todo-item objects to become the tasks tuple.

Source

Thrown at python/omp-rpc/src/omp_rpc/protocol.py:1363

            _require_literal(
                payload.get("status", "pending"),
                _TODO_STATUS_VALUES,
                field="todo.status",
            ),
        ),
        notes=_optional_str(payload, "notes"),
        details=_optional_str(payload, "details"),
        blocker=_optional_str(payload, "blocker"),
    )


def parse_todo_phase(payload: JsonObject) -> TodoPhase:
    raw_tasks = payload.get("tasks")
    if raw_tasks is None:
        tasks = ()
    else:
        if not isinstance(raw_tasks, list):
            raise ValueError("tasks must be a list")
        tasks = tuple(
            parse_todo_item(_clone_json_object(item, field="tasks[]"))
            for item in raw_tasks
        )
    return TodoPhase(
        id=str(payload.get("id", "")),
        name=_require_str(payload, "name"),
        tasks=tasks,
    )


def parse_todo_phases(payload: JsonValue | None) -> tuple[TodoPhase, ...]:
    if not isinstance(payload, list):
        return ()
    return tuple(parse_todo_phase(cast(JsonObject, item)) for item in payload)


def parse_session_state(payload: JsonObject) -> SessionState:

View on GitHub (pinned to 9690622007)

Solutions

  1. Inspect the raw payload and ensure "tasks" is serialized as a JSON array of todo-item objects.
  2. Fix the producer (server/tool) to emit tasks as a list; if keyed, convert values to an array.
  3. Check client/server protocol versions match; upgrade omp-rpc on both sides.
  4. If interception/middleware is in play, verify it does not transform arrays into objects.

Example fix

// before (server emits object)
{"id": "p1", "tasks": {"t1": {"text": "do x"}}}
// after
{"id": "p1", "tasks": [{"id": "t1", "text": "do x"}]}
Defensive patterns

Strategy: type-guard

Validate before calling

raw = payload.get("tasks")
if raw is not None and not isinstance(raw, list):
    raise TypeError(f"tasks must be a list, got {type(raw).__name__}")

Type guard

def is_task_list(payload: dict) -> bool:
    raw = payload.get("tasks")
    return raw is None or (isinstance(raw, list) and all(isinstance(t, dict) for t in raw))

Try / catch

try:
    phase = parse_todo_phase(payload)
except ValueError as exc:
    logger.error("invalid todo phase payload", extra={"payload": payload})
    raise ProtocolError("malformed todo phase") from exc

Prevention

When it happens

Trigger: Calling an RPC that returns a TodoPhase where the server serializes "tasks" as an object, string, or null-like non-list value instead of an array (e.g. a map keyed by task id, or a dict instead of a list).

Common situations: Version mismatch between client and server protocol schemas; a hand-written mock or test fixture using a dict for tasks; a proxy or middleware re-encoding the array as an object; a server bug in a custom tool emitting todo phases.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/d207a7c6f7bd93dd. Report an issue: GitHub.