fix(sessions): keep a stream-interrupt recovery inside the original session
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
This commit is contained in:
@@ -282,7 +282,10 @@ class SessionSessionsMixin:
|
||||
git_repo_root: str = None, origin_json: str = None, display_name: str = None,
|
||||
) -> None:
|
||||
"""Upsert a session row, never overwriting what an earlier writer set (the gateway creates a
|
||||
bare row before create_session carries the real model/prompt). chat_id/thread_id scope gateway
|
||||
bare row before create_session carries the real model/prompt) — the one exception is the
|
||||
token-accounting guard's placeholder ``source='unknown'``, which a later writer's real surface
|
||||
replaces (#111999): once minted, that placeholder otherwise labelled a real session anonymous
|
||||
for life, because this upsert is the only writer that could correct it. chat_id/thread_id scope gateway
|
||||
/resume (IDOR). Children backfill from the parent; a missing profile_name is stamped with THIS
|
||||
store's own (NULL reads as unowned).
|
||||
|
||||
@@ -320,6 +323,11 @@ class SessionSessionsMixin:
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
source = CASE
|
||||
WHEN sessions.source = 'unknown'
|
||||
THEN COALESCE(excluded.source, 'unknown')
|
||||
ELSE sessions.source
|
||||
END,
|
||||
model = COALESCE(sessions.model, excluded.model),
|
||||
model_config = CASE
|
||||
WHEN excluded.model_config IS NOT NULL
|
||||
|
||||
Reference in New Issue
Block a user