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
- Pass `"closed"` or `"cancelled"` (the correct PendingClosureState members) to finalize_closure
- Check the calling code's state transition — only call finalize_closure after the closure actually succeeded (closed) or was cancelled
- 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
- Map operation outcomes explicitly: success→closed, abort→cancelled
- Use the PendingClosureState enum members, never raw strings
- Only call finalize_closure after the work reached a terminal outcome
- Add a type/narrowing helper so non-terminal states can't be passed by accident
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
- Unsupported language '{value}'. Supported: {}
- Unable to infer language from file extension: {}. Specify `l
- Invalid pattern: {err}
- Failed to load tree-sitter language: {err}
- err.to_string() (invalid glob pattern)
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/bbf1ce65ef394503.
Report an issue: GitHub.