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:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user