fix(agent-server): make base_state.json authoritative for the resume agent (#4032) - #4219
fix(agent-server): make base_state.json authoritative for the resume agent (#4032)#4219smolpaws wants to merge 1 commit into
Conversation
…agent (OpenHands#4032) The timeout-after-restart bug (OpenHands#4032) is a symptom of a duplicate source of truth: the full agent is persisted into BOTH base_state.json (SDK-owned, rewritten on every live mutation via ConversationState.agent = ...) and meta.json (a creation-time StoredConversation snapshot). A live switch_llm writes only base_state.json; on restart, start() rebuilt the agent from the stale meta.json snapshot and ConversationState.create() copied it over base_state.json — reverting the switched LLM (and its timeout). Rather than add another write-side mirror to keep the two copies agreeing (one already exists for the ACP model switch), make base_state.json authoritative on resume: take llm/condenser from base_state.json while keeping tools/agent_context/mcp_config from the creation-time snapshot (those are re-derived by the plugin merge; the already-merged base_state copy would double-merge). This is the read-side dual of the field scoping a mirror would need, and it also covers any future llm/condenser divergence for free. Adds a real restart test: create timeout=300, switch to 600 (base_state only), tear down + fresh EventService over the same dir, assert the live agent uses 600. The test fails on the pre-fix behavior. Co-authored-by: smolpaws <engel@enyst.org>
deb68d5 to
36177aa
Compare
enyst
left a comment
There was a problem hiding this comment.
👋 I'm an AI agent (Opus 5) reviewing this PR on behalf of Engel Nyst (@enyst).
🟢 Good taste - this fixes the source-of-truth problem at the read boundary instead of maintaining a second write-side mirror. The resume path preserves creation-time plugin-merge inputs while taking only the live-switchable LLM/condenser state from base_state.json, and the round-trip tests cover restart behavior.
[RISK ASSESSMENT]
- [Overall PR]
⚠️ Risk Assessment: 🟡 MEDIUM
The implementation is focused and well-tested, but it changes authoritative persisted state during conversation resume, a core runtime path with moderate blast radius.
VERDICT:
✅ Worth merging: the source-of-truth choice is coherent and the regression coverage exercises the real persistence boundary.
KEY INSIGHT:
Reading live-switchable state from its authoritative snapshot is simpler and safer than synchronizing duplicate persistence records.
|
🤖 OpenHands is reviewing this PR. Requested reviewer: This comment was posted by an AI agent (OpenHands). |
all-hands-bot
left a comment
There was a problem hiding this comment.
🟢 Good taste — this is a focused read-side fix for the duplicate-source-of-truth bug. The resume path keeps creation-time plugin inputs while restoring only the live-switchable llm and condenser from base_state.json, and the regression test exercises the real write → close → reopen persistence boundary. I found no material correctness, security, maintainability, or test-coverage issues.
Targeted local verification passed: 2 passed for the new restart test and the existing ACP persistence test. Current PR checks also report no failures.
[RISK ASSESSMENT]
- [Overall PR]
⚠️ Risk Assessment: 🟡 MEDIUM
The change is small and well-contained, but it alters which persisted agent configuration is authoritative on the core conversation-resume path. The scoped field selection and restart regression coverage mitigate that risk.
VERDICT:
✅ Worth merging: no material findings. Because this changes resumed agent configuration, this COMMENT leaves the final merge decision to a human maintainer.
KEY INSIGHT:
Using the live state snapshot as the authority for mutable fields avoids fragile write-side synchronization between duplicate persistence records.
This review was created by an AI agent (OpenHands) on behalf of the repository reviewer.
|
[Automatic Post]: It has been a while since there was any activity on this PR. @smolpaws, are you still working on it? If so, please go ahead, if not then please request review, close it, or request that someone else follow up. This comment was created by an AI agent (OpenHands) on behalf of the user. |
|
Superseded by #4440 , which solves the underlying architectural issue: it cleans up a smell in the inheritance relationship between |
What & why
Fixes #4032 (LLM/profile timeout reverts after an agent-server restart) by removing its root cause rather than papering over it.
The full agent is persisted in two places:
base_state.json— the SDK's own conversation state, rewritten on every live mutation (ConversationState.agent = ..., e.g.switch_llm);meta.json— aStoredConversationsnapshot taken at creation.On resume,
EventService.start()rebuilt the runtime agent frommeta.json, andConversationState.create()then copies that agent overbase_state.json(state.agent = agent). So a liveswitch_llm— which writes onlybase_state.json— was silently reverted to the creation-time LLM (and itstimeout) on the next restart.This is a duplicate-source-of-truth bug. Background write-up (plain-English, code-grounded): https://enyst.github.io/arch/meta-vs-base-state-duplication.html
The fix
Make
base_state.jsonauthoritative for the resume agent. On resume, takellmandcondenserfrombase_state.json(the fields a live switch can change, and where those changes are already persisted) while keepingtools/agent_context/mcp_configfrom the creation-time snapshot — those are re-derived by the plugin merge on first run, so taking the already-mergedbase_statecopy would double-merge them.No new write-side mirror.
mainalready carries one hand-rolled mirror for the ACP model switch (switch_acp_model→save_meta); this read-side approach avoids adding a second forswitch_llm, and covers any futurellm/condenserdivergence for free.Relationship to #4028
Same bug, different level. #4028 (Vasco) fixes it by mirroring the switched
llm/condenserintometa.jsonon change — correct and minimal within the current model. This PR removes the need to mirror at all by making the SDK-owned file authoritative on read. Same field-scoping insight (llm+condenser), applied on the read side. Posting as an alternative for discussion — happy to converge on whichever the maintainers prefer.Tests
Adds
test_switched_llm_survives_restart_via_base_state: create withtimeout=300,switch_llmto600(writesbase_state.jsononly), tear the service down and start a fresh one over the same dir (a restart), assert the live agent uses600. Verified it fails on the pre-fix behavior. Fulltest_event_service.py+test_conversation_service.py(212) pass; ruff + pyright clean.Not in this PR
Fully removing the
agentblob frommeta.json(it's still read cold for codex-detection and conversation-info). That's the larger "slim meta to server-only fields" step; this PR makesbase_stateauthoritative first, which is what actually fixes #4032.