Installs whose state.db reached schema_version >= 22 before the task
dimension was added carry a 5-column PRIMARY KEY on
session_model_usage. The column reconciler ADDs task as a bare
nullable, but SQLite cannot ALTER a primary key, and the version-gated
v22 rebuild is unreachable (current_version < 22 already false), so
the composite 6-column key never lands. Every upsert in
_record_model_usage then fails with 'ON CONFLICT clause does not match
any PRIMARY KEY or UNIQUE constraint', aborting the enclosing write
transaction — token/cost accounting permanently dead (#73823).
Add an idempotent _heal_session_model_usage_pk() modeled on
_heal_gateway_routing_pk(), run unconditionally from _init_schema on
every open. Salvaged from #73838 with fix-ups:
- ported to SessionSchemaMixin in hermes_state_schema.py (the schema
code moved out of hermes_state.py in 21c7ae8563; the PR targeted the
old location)
- rebuild wrapped in a PRAGMA foreign_keys=OFF/ON window: the
connection enables FKs before _init_schema and OR IGNORE does NOT
suppress FK violations, so a single orphaned usage row (session
pruned while accounting was broken) would have aborted the heal
- COALESCE('') on the nullable reconciler-added task column (and the
billing columns) during the copy
- stale-v22+ regression tests: rebuilt PK + restored upsert, orphan
rows survive the FK window, healthy-DB no-op, no legacy leftover
Fixes#73823