Fix: preserve modelOverride in agent handler (#5369)#8398
Fix: preserve modelOverride in agent handler (#5369)#8398CodeReclaimers wants to merge 3 commits into
Conversation
When spawning sub-agents via sessions_spawn, the model parameter was intermittently ignored due to a race condition. The agent handler would read session data early (potentially from cache), build nextEntry, then overwrite the session store - clobbering any modelOverride that sessions.patch had just written. The fix moves session entry construction inside the updateSessionStore mutator, ensuring it always uses fresh store data (loaded with skipCache: true). This eliminates the race condition entirely. Changes: - Build session entry inside updateSessionStore using fresh store data - Add fallback path for when storePath is not available - Add comprehensive tests for the fix and edge cases Fixes openclaw#5369 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Additional Comments (2)
The pre-flight Prompt To Fix With AIThis is a comment left during a code review.
Path: src/gateway/server-methods/agent.ts
Line: 238:245
Comment:
[P1] **Send-policy check can still use stale cached entry**
The pre-flight `resolveSendPolicy({ entry, channel: entry?.channel, chatType: entry?.chatType })` at `agent.ts:238-245` still uses the potentially stale `loadSessionEntry()` result. If `sessions.patch` updates policy-relevant fields (e.g., `sendPolicy`, `channel`, `chatType`) between the cached read and the `updateSessionStore(skipCache: true)` write, this handler can incorrectly allow/deny sends based on stale data even though the store has a newer policy. Consider resolving policy from the fresh store entry inside `updateSessionStore` (or doing a fresh read for policy) when `storePath` is present.
How can I resolve this? If you propose a fix, please make it concise.
In Prompt To Fix With AIThis is a comment left during a code review.
Path: src/gateway/server-methods/agent.test.ts
Line: 380:382
Comment:
[P3] **Fake timers aren’t restored if the test fails early**
In `agent.test.ts:380-382`, `vi.useRealTimers()` is called at the end of the test body. If an assertion throws before that line, subsequent tests can run under fake timers. Using a `try/finally` (or `afterEach(() => vi.useRealTimers())`) would make the suite more robust.
How can I resolve this? If you propose a fix, please make it concise. |
P1: Move send policy check inside updateSessionStore mutator to use fresh store data, avoiding potential stale policy decisions when sessions.patch updates the entry between initial read and write. P3: Add afterEach hook to restore real timers in agent tests, ensuring cleanup even if tests fail mid-execution with fake timers enabled. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Follow-up: Addressed greptile-apps review commentsPushed commit P1: Fresh data for send policy checkMoved the The mutator now returns a discriminated union P3: Fake timers cleanupAdded All 5,043 tests pass with these changes. |
bfc1ccb to
f92900f
Compare
Resolve conflicts integrating main's legacy key pruning and new features with the openclaw#5369 race condition fix that moves session entry construction inside the updateSessionStore mutator. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
24 similar comments
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
Summary
Fixes the intermittent race condition where sub-agent model overrides were silently ignored (~3% failure rate).
sessions_spawnsets a model override viasessions.patch, the agent handler could read stale cached data and overwrite the freshmodelOverridewhen writing back to the session storeupdateSessionStoremutator, ensuring it always uses fresh store data (loaded withskipCache: true)Root Cause
The agent handler was:
loadSessionEntry()(could return cached/stale data)nextEntrywithmodelOverride: entry?.modelOverride(potentially undefined)updateSessionStore()which correctly re-reads withskipCache: truestore[key] = nextEntryThis meant freshly-written
modelOverridevalues fromsessions.patchcould be overwritten.Changes
src/gateway/server-methods/agent.ts: Build session entry insideupdateSessionStoreusing fresh store datasrc/gateway/server-methods/agent.test.ts: Add 4 comprehensive tests for the fixTest plan
issue #5369: agent handler preserves fresh modelOverride from storeissue #5369: request params override fresh store values for label/spawnedByissue #5369: creates new entry when store has no existing entryissue #5369: preserves all important fields from fresh storeFixes #5369
🤖 Generated with Claude Code
Greptile Overview
Greptile Summary
Fixes a race where the agent handler could overwrite freshly-patched session fields (notably
modelOverride) by moving session entry construction into theupdateSessionStoremutator so it always uses the fresh store snapshot.Adds targeted tests in
src/gateway/server-methods/agent.test.tsto simulate staleloadSessionEntry()data vs fresh store data and assert that request params override where intended while preserving store-only fields.Confidence Score: 4/5
updateSessionStore, which directly addresses the described overwrite race. Main remaining concern is that the send-policy precheck still relies on a potentially stale cached entry, which could be incorrect if sessions.patch updates policy-relevant fields. Tests are deterministic and cover the modelOverride scenario well.(2/5) Greptile learns from your feedback when you react with thumbs up/down!