fix(sessions): token-accounting guard stamps the agent's real source; trim salvage

When every row create of a turn loses to the SQLite lock, the queued token delta's
"ensure the row exists" guard becomes the session's first writer and minted the row as
source='unknown'. That placeholder was permanent on the real path even with the upsert
repair from #112045: the turn lease (turn_facade_lease.admit_durable_turn) treats an existing
row as proof the create already happened and sets _session_db_created, so the creator never
returns to repair it. Live probe: a platform="desktop" AIAgent whose create_session raised
"database is locked" for the whole first turn ended with a source='unknown' row on base AND
on the contributor head; with this change the row is minted 'desktop' by the guard itself.

Producer fix: update_token_counts gains an optional source= that the two agent call sites
(agent/turn_usage.py, agent/codex_runtime.py) fill from _session_source_for_agent(platform),
the same value _ensure_db_session would stamp. record_auxiliary_usage has no surface and
keeps the placeholder, which the creator's upsert now repairs.

Salvage trims: the contributor's SimpleNamespace dispatch test is replaced by a real-AIAgent
invariant test under tests/agent/ (the dispatch hunk in _run_prompt_submit is kept; the
INSERT-OR-IGNORE is idempotent under prompt.submit's own persist); narration comments cut
to the WHY; docs list 'unknown' among the startup-sweep sources.

Refs #111999
This commit is contained in:
teknium1
2026-09-15 12:17:38 -07:00
committed by Teknium
parent a7dde8a57d
commit cfd752e6f7
8 changed files with 95 additions and 113 deletions
+4 -8
View File
@@ -829,14 +829,10 @@ def _run_prompt_submit(
queued_prompt_generation: int | None = None,
terminal_callback: Callable[[dict[str, Any]], None] | None = None,
turn_author: dict | None = None) -> bool:
# Every dispatch owns a durable row for THIS session before the turn writes anything. The
# prompt.submit handler persists it; the recovery dispatches that call straight in here (the
# crash auto-continue, the queued-prompt drain, the compute-host fallback) did not, so a turn whose
# row never landed — create deferred/failed under the SQLite lock, a record whose session_key had
# not been stamped yet — was materialized by the token-accounting guard instead: an anonymous
# source='unknown' session holding an assistant-first fragment, which the real creator's later
# upsert can never repair (the row insert keeps the first writer's source) and which the orphan
# sweep skips. Binding the row here keeps the recovery inside the ORIGINAL session (#111999).
# Every dispatch binds the session's own row (session_key, real source) before the turn writes:
# the synthesized turns that enter here directly (crash auto-continue, queued-prompt drain,
# wake-ups) bypass prompt.submit's persist, and a row-less turn is otherwise materialized by
# the token-accounting guard as an anonymous session (#111999).
if _ensure_session_db_row(session) is False:
logger.warning(
"prompt dispatch: session store unavailable for %s — this turn may not persist",