Review follow-up for #113119.
auxiliary_usage_by_task walked parent_session_id past an explicit /branch
copy, so a resumed branch one-shot's per-run delta absorbed aux spend that
another process billed to the source session in the meantime. The walk now
stops at the nearest explicit-branch node (inclusive), mirroring the cutoff
hermes_state_messages applies for resume; compression children of the branch
still count because they never carry _branched_from.
_attach_auxiliary_usage read the tip id from the result dict, which a failed
turn (billing/entitlement failure return) does not carry, so the ledger's
auxiliary block came back empty exactly when the docs promise it is written.
The caller now passes agent.session_id (or the start id) as a fallback.
`hermes -z --usage-file` copied only the main-loop result, so title generation,
vision, compression, web_extract and background-review calls — recorded per task
in session_model_usage — never reached the pipeline ledger the flag advertises
as "so pipelines can always account for spend". The Insights page already folds
those rows in (#23270); the ledger is now consistent with it.
- SessionDB.auxiliary_usage_by_task(session_id): per-task sums over the
session's compression lineage (aux calls bill to the id the turn started with
while compression mints child ids mid-turn).
- oneshot snapshots aux usage before the turn and attaches the delta after it,
so a resumed session's earlier runs are not re-billed.
- The report gains `auxiliary` (totals + `by_task`) and
`total_including_auxiliary`; every existing key keeps its main-loop meaning.
- The auto-title upgrade runs on a daemon thread and can still be writing when
the turn returns: title_generator tracks in-flight upgrade threads and
oneshot joins them (bounded) before reading — no sleep, no eager read.
Fixes#112848. Direction shared with #112852 (@KoNit-K), which folded aux into
the headline counters; this keeps them backward compatible instead.
Co-authored-by: KoNit-K <124019182+KoNit-K@users.noreply.github.com>
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
A stream that dies mid-answer could leave an orphan session behind: source='unknown', its
first message an assistant message and no user prompt anywhere — invisible to the startup
orphan sweep, unrepairable by the session's own creator. Three links made it permanent:
* the token-accounting guard (hermes_state_usage.update_token_counts, the only writer that
mints source='unknown') mints whenever the row is missing — which is exactly the state a
recovery dispatch resumed from: _run_prompt_submit (the crash auto-continue, the
queued-prompt drain) went straight into the turn without persisting the session's own row,
unlike the prompt.submit handler, so the first durable writer for that session was the
accounting side effect, and the turn's prompt could not be written at all (the messages FK
needs the row);
* _insert_session_row's upsert deliberately keeps what the first writer set, so the real
creator could never repair that placeholder;
* _ORPHAN_SWEEP_SOURCES skipped 'unknown', so such a row stayed ended_at IS NULL forever.
Every dispatch now binds its own row (original session_key, real source) before the turn
writes anything; the upsert repairs the placeholder source when the session's real creator
arrives; the sweep collects a phantom an older build already left on disk. Regression tests
(red before, green after) in tests/tui_gateway/test_stream_interrupt_recovery_orphan.py.
Refs #111999
For each issue anchor present in BASE 63279301bc non-test .py and absent on HEAD, the BASE comment/docstring block was re-attached at the HEAD location of the code it explained (matched by the distinctive code line / enclosing def). Sentences already covered by an existing HEAD comment were deduped; the issue number always survives. Insert-only: no code lines changed.