can1357/oh-my-pi · error · ValueError

finalize_closure: invalid terminal state {state!r}

Error message

finalize_closure: invalid terminal state {state!r}

What it means

`finalize_closure` on the DB layer only accepts the terminal states `"closed"` or `"cancelled"`; any other `PendingClosureState` value raises this ValueError before the UPDATE runs. The guard exists because marking a row terminal with a non-terminal state (e.g. `pending` or `claimed`) would corrupt the closure state machine.

Source

Thrown at python/robomp/src/db.py:1312

                  LIMIT ?
                )
                RETURNING issue_key, repo, number, comment_id, issue_author,
                          close_at, state, cancel_reason, created_at, updated_at
                """,
                (now, now, int(limit)),
            ).fetchall()
        return [_pending_closure_from_row(row) for row in rows]

    def finalize_closure(
        self,
        issue_key: str,
        *,
        state: PendingClosureState,
        reason: str | None,
    ) -> None:
        """Mark a claimed row terminal (`closed` / `cancelled`)."""
        if state not in ("closed", "cancelled"):
            raise ValueError(f"finalize_closure: invalid terminal state {state!r}")
        with self._lock:
            self._conn.execute(
                """
                UPDATE pending_closures
                SET state = ?, cancel_reason = ?, updated_at = ?
                WHERE issue_key = ?
                """,
                (state, reason, _utcnow(), issue_key),
            )

    def requeue_claimed_closure(self, issue_key: str) -> bool:
        """Return a `claimed` row to `pending` so the next tick retries it.

        Used by the scheduler when a transient GitHub error prevents the
        close from completing. Only flips `claimed -> pending`; rows in any
        other state are left untouched.
        """
        with self._lock:

View on GitHub (pinned to 9690622007)

Solutions

  1. Pass `"closed"` or `"cancelled"` (the correct PendingClosureState members) to finalize_closure
  2. Check the calling code's state transition — only call finalize_closure after the closure actually succeeded (closed) or was cancelled
  3. If branching, map outcomes explicitly: success → "closed", abort/cancel → "cancelled"

Example fix

// before
db.finalize_closure(issue_key=key, state="claimed", reason=None)
// after
db.finalize_closure(issue_key=key, state="closed", reason=None)
Defensive patterns

Strategy: validation

Validate before calling

TERMINAL = {"closed", "cancelled"}
if state not in TERMINAL:
    raise ValueError(f"refusing finalize: {state!r} is not terminal")
db.finalize_closure(issue_key=key, state=state, reason=reason)

Type guard

def is_terminal(state) -> bool:
    return state in ("closed", "cancelled")

Try / catch

try:
    db.finalize_closure(issue_key=key, state=state, reason=reason)
except ValueError as exc:
    log.error("closure state machine violation: %s", exc)
    raise

Prevention

When it happens

Trigger: Calling `db.finalize_closure(issue_key=..., state="pending", ...)` or passing any state other than `"closed"`/`"cancelled"`. The repo's own test `test_finalize_closure_rejects_non_terminal_state` exercises exactly this.

Common situations: Passing an unconverted enum member, forwarding a state variable straight from an earlier scheduling stage without checking it reached a terminal value, refactors that rename or add PendingClosureState members and route the wrong one here.

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 can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/bbf1ce65ef394503. Report an issue: GitHub.