Conversation
…Research#45110) When the first assistant response never completes — a stall, a streaming client disconnect on a gateway turn, or a process crash mid-generation — nothing from the turn reaches state.db. The user prompt vanishes as if it were never asked, and follow-up turns hit an agent with a hole in its own history. session_search then fills the gap by guessing from older sessions, and the model confidently conflates prior context (NousResearch#45110). The crash-resilience ``_persist_session`` call in the prologue already covers the user message via identity tracking, but the ``_flush_messages_to_session_db`` path it flows through runs ``_apply_persist_user_message_override`` and ``_drop_trailing_empty_response_scaffolding`` first, both of which can mutate the message or no-op silently. The fix adds a focused, explicit persist that runs immediately after the user message is appended to the live ``messages`` list — BEFORE any model call — and writes ONLY the user message directly to SQLite. Fix: - ``AIAgent._persist_inbound_user_message(user_msg)`` (new method) writes the user message to state.db via ``self._session_db.append_message``. Mirrors the existing ``_flush_messages_to_session_db`` guard pattern (``if not self._session_db: return``) and the multimodal-text-summary transformation (base64 image parts become ``[screenshot]`` placeholders; full base64 would bloat the session DB and isn't useful for cross-session replay). Silent ``logger.debug()`` on exception — the existing ``_persist_session`` crash-resilience call later in the prologue is the production-grade safety net. - ``agent/turn_context.py`` calls the new method right after ``messages.append(user_msg)``, wrapped in ``try/except`` so an audit-log bug can never break the agent loop. The existing ``_persist_session`` call stays in place as belt-and-suspenders. Five new regression tests in ``tests/agent/test_turn_context.py``: - ``test_persist_inbound_user_message_writes_user_row_on_receipt``: after ``build_turn_context`` returns, the in-memory SessionDB recorded the user message — exactly the contract the streaming / stalled-first-response failure mode violates without the fix. - ``test_persist_inbound_user_message_handles_no_db``: ``_session_db is None`` is a no-op (test stubs, ephemeral flows). - ``test_persist_inbound_user_message_persists_only_user_not_assistant``: the targeted call never writes assistant / tool / system messages — that's the full ``_persist_session`` job. - ``test_persist_inbound_user_message_skips_non_user_dicts``: defensive guard against accidental misuse. - ``test_persist_inbound_user_message_strips_multimodal_to_text_summary``: multimodal user content (text + image_url parts) lands in state.db as ``"what is in this image?\n[screenshot]"``, not as a base64 blob. 11/11 tests pass in ``test_turn_context.py``; the existing crash-resilience ``_persist_session`` call is unchanged and still fires once per turn. Closes NousResearch#45110
|
Related: #45110 (the issue this addresses — prompt lost when a turn's first response never completes), #45887 (harden early-turn persistence to survive session-DB failures, #45110), #46471 (gateway persists transcript itself), #43853 (backfill missing assistant transcript rows). Part of the SessionDB persistence family (merged anchor #45260 handles the flush-cursor / identity-tracking facet). This PR's mechanism is distinct: an explicit persist of the inbound user message immediately on receipt, before |
|
Thanks @Kewe63 — and apologies for the redundancy here. The core of #45110 is already fixed on current I verified the disconnect-before-response scenario end-to-end against a real On the PR's premise: Closing as implemented-on-main. Thanks for the contribution. |
Summary
When the first assistant response never completes — a stall, a streaming client
disconnect on a gateway turn, or a process crash mid-generation — nothing from
the turn reaches
state.db. The user prompt vanishes as if never asked, andfollow-up turns hit an agent with a hole in its own history.
session_searchthen fills the gap by guessing from older sessions, and the model confidently
conflates prior context (#45110).
Two observed failure modes:
while the agent is thinking, return: no answer, and no record the question
was ever asked.
older sessions, producing wrong answers downstream.
The
_persist_sessioncall in the prologue already covers the user message viaidentity tracking, but the
_flush_messages_to_session_dbpath it flowsthrough runs
_apply_persist_user_message_overrideand_drop_trailing_empty_response_scaffoldingfirst, both of which can mutate orsilently no-op the message. The fix adds a focused, explicit persist that runs
immediately after the user message is appended to the live messages list —
before any model call — and writes only the user message directly to SQLite.
Changes
run_agent.py(L1578-1641) — newAIAgent._persist_inbound_user_message(user_msg):state.dbviaself._session_db.append_message._flush_messages_to_session_dbguard pattern(
if not self._session_db: return).[screenshot]placeholders to avoid bloating the session DB.
logger.debug()on exception — the existing_persist_sessioncall remains the production-grade safety net.
agent/turn_context.py(L226-243) — calls the new method right aftermessages.append(user_msg), wrapped intry/exceptso an audit-log bugcan never break the agent loop. The existing
_persist_sessioncall staysin place as belt-and-suspenders.
tests/agent/test_turn_context.py(L188-337) — 5 new regression tests:test_persist_inbound_user_message_writes_user_row_on_receipt— headlinetest; drives
build_turn_contextend-to-end and asserts the user messagelanded in
SessionDBbefore any model call.test_persist_inbound_user_message_handles_no_db—_session_db is Noneis a no-op.
test_persist_inbound_user_message_persists_only_user_not_assistant—scope check; targeted call never writes assistant/tool/system messages.
test_persist_inbound_user_message_skips_non_user_dicts— defensiveguard against accidental misuse.
test_persist_inbound_user_message_strips_multimodal_to_text_summary—multimodal content lands as
"what is in this image?\n[screenshot]",not a base64 blob.
How to Test
Checklist
test_turn_context.py, 3/3 regression filterruff check— PASS, 0 warningsRisk & Impact
Low. Additive — the existing
_persist_sessioncrash-resilience call isunchanged. The new
_persist_inbound_user_messagemethod is a focused SQLitewrite that runs once per turn and silently no-ops if
_session_dbis notconfigured. The
try/exceptwrapper inturn_context.pyguarantees the agentloop is never broken by an audit-log bug. Public method signatures are unchanged.
Type: 🐛 Bug fix (data-loss prevention)
Closes: #45110