diff --git a/agent/codex_runtime.py b/agent/codex_runtime.py index 59f9bac25a26..da3bc4f95695 100644 --- a/agent/codex_runtime.py +++ b/agent/codex_runtime.py @@ -778,12 +778,27 @@ def run_codex_app_server_turn( # the already-flushed user turn). See gateway/run.py agent_persisted. if getattr(agent, "_session_db", None) is not None: try: - agent._flush_messages_to_session_db(messages) + _codex_flush_ok = agent._flush_messages_to_session_db(messages) except Exception: - logger.debug( + _codex_flush_ok = False + logger.warning( "codex app-server projected-message flush failed", exc_info=True, ) + if _codex_flush_ok is False: + # Unlike the chat-completions loop (which fails closed BEFORE + # projection — see conversation_loop session_persistence_failed), + # codex output has already streamed to the user by the time this + # flush runs, so there is nothing left to withhold. We cannot + # flip agent_persisted=False either: the gateway fallback write + # would re-INSERT the already-flushed user turn (#860/#42039). + # Surface the durability gap loudly instead of a silent debug. + logger.warning( + "codex app-server turn was delivered but could NOT be " + "persisted to the session DB (session=%s) — this turn " + "will be missing after restart/resume", + getattr(agent, "session_id", None), + ) # Counter ticks for the agent-improvement loop. diff --git a/contributors/emails/elco@thedaoist.gg b/contributors/emails/elco@thedaoist.gg new file mode 100644 index 000000000000..a2a41fca3807 --- /dev/null +++ b/contributors/emails/elco@thedaoist.gg @@ -0,0 +1 @@ +elcocoel diff --git a/run_agent.py b/run_agent.py index 5e97ed24c9c0..fe8db996955d 100644 --- a/run_agent.py +++ b/run_agent.py @@ -3445,6 +3445,14 @@ class AIAgent: "the model produced no follow-up text. Send `continue` to " "let it summarize." ) + if reason == "session_persistence_failed": + return ( + prefix + + "the turn was stopped because session storage could not be " + "written (the transcript would have been lost on restart). " + "Check disk space / permissions for the state DB, then send " + "your message again." + ) # Unknown/diagnostic-only reasons (e.g. "unknown", guardrail_halt # which already surfaces its own message) — don't second-guess. return "" diff --git a/tests/run_agent/test_turn_completion_explainer.py b/tests/run_agent/test_turn_completion_explainer.py index 95a7a4b54a8d..386a74c754c0 100644 --- a/tests/run_agent/test_turn_completion_explainer.py +++ b/tests/run_agent/test_turn_completion_explainer.py @@ -106,6 +106,16 @@ def test_explanation_for_all_retries_exhausted(): assert "retries" in out.lower() +def test_explanation_for_session_persistence_failed(): + """Fail-closed persistence exits (#72425) must explain themselves.""" + out = AIAgent._format_turn_completion_explanation( + "session_persistence_failed" + ) + assert out # non-empty + assert "session storage" in out.lower() + assert "disk space" in out.lower() + + # -------------------------------------------------------------------------- # 2. Enable/disable seam # --------------------------------------------------------------------------