Conversation
…all)
The codex_app_server runtime path (run_codex_app_server_turn in
agent/codex_runtime.py) is an early-return that bypasses
conversation_loop and never calls _flush_messages_to_session_db().
Meanwhile, gateway/run.py sets:
agent_persisted = self._session_db is not None # always True
and passes skip_db=agent_persisted to every append_to_transcript call,
assuming the agent self-persisted (correct for the standard runtime,
wrong for codex). The result: codex turn messages are persisted nowhere.
state.db accumulates only session_meta rows; session_search (full-text
search over state.db) and conversation-distill are blind to real gateway
conversations, causing 'the agent has no memory of what we discussed'.
Fix (three-part, all backward-compatible):
1. agent/codex_runtime.py — run_codex_app_server_turn success return
now includes 'agent_persisted': False, signalling that the codex path
did NOT self-persist its turn.
2. gateway/run.py — the agent_persisted assignment now reads:
agent_result.get('agent_persisted', self._session_db is not None)
For the standard runtime (which does not set the key) the default
(self._session_db is not None) preserves the existing skip-db
behaviour so no duplicate-write regression (NousResearch#860 / NousResearch#42039) occurs.
For the codex runtime the flag is False, so the gateway writes the
new turn's messages to state.db and FTS index.
3. gateway/run.py — the rebuilt result dict (run_agent return, which
becomes agent_result upstream) now includes agent_persisted passed
through from result_holder[0], with a safe True default. Without
this passthrough the flag set in step 1 was discarded when the result
was reconstructed, causing agent_result.get('agent_persisted', ...)
to always see the default True and never write codex turns.
|
Related: #38210 (root issue — codex sessions persist 0 messages), #38254 (open competitor — persists in This PR takes a distinct mechanism from #38254: instead of persisting inside the runtime, it propagates an |
Maps the two plain-email contributors whose PRs are being salvaged so contributor_audit.py passes: - info@djimit.nl -> djimit (PR #48034) - lubos@komfi.health -> lubosxyz (PR #49225) The other two PRs in the batch (#50405 sasquatch9818, #48764 srojk34) use users.noreply.github.com emails, which check-attribution auto-skips.
|
Merged via #56343 (commit 5558382) with your authorship preserved via rebase. Thanks @lubosxyz — your diagnosis and gateway wiring landed. During review I found the original agent_persisted=False approach reintroduced the #860/#42039 duplicate user-message write (the gateway agent already flushes the user turn at turn start), verified via E2E, so a follow-up commit (dc1ea00) switched to a single-writer design: the codex runtime flushes its own projected messages and returns agent_persisted=True. You are credited as co-author. |
Maps the two plain-email contributors whose PRs are being salvaged so contributor_audit.py passes: - info@djimit.nl -> djimit (PR NousResearch#48034) - lubos@komfi.health -> lubosxyz (PR NousResearch#49225) The other two PRs in the batch (NousResearch#50405 sasquatch9818, NousResearch#48764 srojk34) use users.noreply.github.com emails, which check-attribution auto-skips.
Maps the two plain-email contributors whose PRs are being salvaged so contributor_audit.py passes: - info@djimit.nl -> djimit (PR NousResearch#48034) - lubos@komfi.health -> lubosxyz (PR NousResearch#49225) The other two PRs in the batch (NousResearch#50405 sasquatch9818, NousResearch#48764 srojk34) use users.noreply.github.com emails, which check-attribution auto-skips.
Maps the two plain-email contributors whose PRs are being salvaged so contributor_audit.py passes: - info@djimit.nl -> djimit (PR NousResearch#48034) - lubos@komfi.health -> lubosxyz (PR NousResearch#49225) The other two PRs in the batch (NousResearch#50405 sasquatch9818, NousResearch#48764 srojk34) use users.noreply.github.com emails, which check-attribution auto-skips.
Maps the two plain-email contributors whose PRs are being salvaged so contributor_audit.py passes: - info@djimit.nl -> djimit (PR NousResearch#48034) - lubos@komfi.health -> lubosxyz (PR NousResearch#49225) The other two PRs in the batch (NousResearch#50405 sasquatch9818, NousResearch#48764 srojk34) use users.noreply.github.com emails, which check-attribution auto-skips.
Maps the two plain-email contributors whose PRs are being salvaged so contributor_audit.py passes: - info@djimit.nl -> djimit (PR NousResearch#48034) - lubos@komfi.health -> lubosxyz (PR NousResearch#49225) The other two PRs in the batch (NousResearch#50405 sasquatch9818, NousResearch#48764 srojk34) use users.noreply.github.com emails, which check-attribution auto-skips.
Maps the two plain-email contributors whose PRs are being salvaged so contributor_audit.py passes: - info@djimit.nl -> djimit (PR NousResearch#48034) - lubos@komfi.health -> lubosxyz (PR NousResearch#49225) The other two PRs in the batch (NousResearch#50405 sasquatch9818, NousResearch#48764 srojk34) use users.noreply.github.com emails, which check-attribution auto-skips.
Maps the two plain-email contributors whose PRs are being salvaged so contributor_audit.py passes: - info@djimit.nl -> djimit (PR NousResearch#48034) - lubos@komfi.health -> lubosxyz (PR NousResearch#49225) The other two PRs in the batch (NousResearch#50405 sasquatch9818, NousResearch#48764 srojk34) use users.noreply.github.com emails, which check-attribution auto-skips.
Problem
Gateway sessions using the
codex_app_serverruntime have no conversation memory:session_search(full-text search overstate.db) andconversation-distillreturn empty results for all turns driven by the Codex app-server path. The agent effectively forgets everything that happened in a conversation.Root cause
agent/codex_runtime.py—run_codex_app_server_turnis an early-return path that bypassesconversation_loop.The standard runtime drives every turn through
conversation_loop, which calls_flush_messages_to_session_db()to persist user + assistant + tool messages tostate.db. The codex app-server path returns early (beforeconversation_loopis entered) and never calls_flush_messages_to_session_db().gateway/run.py— persistence block assumes the agent self-persisted.The comment says "the agent already persisted these messages". That assumption is true for the standard runtime, but false for the codex app-server path. With
skip_db=True, the gateway also skips writing tostate.db. End result: codex turns are written to neither location.state.dbaccumulates onlysession_metarows — no user messages, no assistant responses, no tool calls.session_searchFTS returns nothing;conversation-distillhas nothing to distill.gateway/run.py—agent_persistedflag is silently dropped in the result rebuild.Even if
run_codex_app_server_turnreturnedagent_persisted=False, the rebuilt result dict at thereturn {…}of_run_agentdid not include a passthrough for that key.agent_result.get("agent_persisted", …)would always see the…default.Fix
Three backward-compatible changes:
1.
agent/codex_runtime.py—run_codex_app_server_turnsuccess return now includes:This signals to the gateway that the codex path did NOT self-persist its turn.
2.
gateway/run.py— persistence block now reads the flag instead of hard-codingTrue:For the standard runtime (which does not set the key), the default
self._session_db is not Nonepreserves the existing skip-db behaviour — no duplicate-write regression (#860 / #42039). For the codex app-server runtime, the flag isFalse, so the gateway writes the new turn's messages tostate.dband the FTS index.3.
gateway/run.py— result rebuild now passes the flag through:Without this, the flag returned by
run_codex_app_server_turnwas discarded when_run_agentrebuilt the result dict fromresult_holder[0], so edit 2 would never seeFalse.Repro
api_mode = "codex_app_server"(the Codex CLI runtime).session_searchwith a keyword from one of those messages.Alternatively, check
state.dbdirectly:Test plan
run_codex_app_server_turnto return{"agent_persisted": False, …}; assert that_run_agent's return dict contains"agent_persisted": False.agent_resultwithagent_persisted=Falseand a non-Noneself._session_db; assertagent_persistedlocal variable isFalse(gateway writes to DB).agent_resultwithoutagent_persistedkey (standard runtime); assertagent_persisteddefaults toself._session_db is not None(existing behaviour preserved).state.dbcontainsrole=userandrole=assistantrows for both turns.state.db(no duplicate-write regression from bug: SQLite session transcript accumulates duplicate messages (3-4x token inflation) #860 / Bug: User messages stored twice in state.db when agent and gateway both write to SQLite #42039).🤖 Generated with Claude Code