rayhpeng 45626f6383 fix(scheduler): keep a skipped dispatch from failing its own write-back
`_finalize_skip` carries a task's launch bookkeeping over unchanged by
reading the current values off the task dict and passing them back into
`update_after_launch`. But repository dicts hold ISO *strings* for
timestamps -- `_row_to_dict` runs every datetime column through
`coerce_iso` -- while `scheduled_tasks.last_run_at` is a DateTime
column, so that round trip fed a string into a datetime bind parameter
and raised StatementError.

It only reproduces once the task has launched at least once: before
that `last_run_at` is NULL, which the column accepts. Every existing
dispatch test seeds a fresh task, so none of them reached it.

The blast radius was the whole poll cycle rather than the one task: the
exception escapes `dispatch_task` into `_run_loop`, so every task still
queued behind it in that round goes undispatched, and the failing task
holds its lease in `running` until it expires. The skip tombstone was
already written by then, so the run history was left inconsistent with
the task row.

Coerced at the call site instead of loosening the repository's parameter
type: the skip path is the only one that round-trips a stored timestamp,
every other write passes `now` straight through.
2026-07-28 11:20:31 +08:00
..