fix(gateway): keep provider-owned CLI sessions across the daily default reset - #97931
Conversation
|
Codex review: needs maintainer review before merge. Reviewed June 29, 2026, 6:23 PM ET / 22:23 UTC. Summary PR surface: Source +10, Tests +55. Total +65 across 3 files. Reproducibility: yes. Source inspection shows current main and the documented contract diverge: docs and the inbound helper exempt provider-owned CLI sessions, while gateway Review metrics: none identified. Stored data model Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Next step before merge
Security Review detailsBest possible solution: Land the focused gateway alignment after ordinary maintainer review and required checks, keeping explicit Do we have a high-confidence way to reproduce the issue? Yes. Source inspection shows current main and the documented contract diverge: docs and the inbound helper exempt provider-owned CLI sessions, while gateway Is this the best way to solve the issue? Yes. Reusing the existing provider-owned predicate at both gateway freshness decisions is the narrow maintainable fix; pulling in the full entry-freshness helper would duplicate loading in a path that already owns the loaded entry and store update flow. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 843ad143647e. Label changesLabel justifications:
Evidence reviewedPR surface: Source +10, Tests +55. Total +65 across 3 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
…lt reset The gateway agent.run freshness decision called evaluateSessionFreshness directly at both of its decision sites with no provider-owned guard, so a provider-owned CLI session (claude-cli, codex, gemini-cli) under the default reset config was rotated after the daily boundary when a turn ran through the gateway path (webchat, openclaw agent, ACP, control UI, cron, heartbeat). The rotation cleared the CLI session binding and split the transcript, violating the documented exemption that the inbound auto-reply path and the canonical session helper already honor. Route both gateway freshness decisions through the same resetPolicy.configured !== true && hasProviderOwnedSession(entry) skip the inbound path uses, and export hasProviderOwnedSession so the predicate has one shared definition instead of a third copy. Explicit session.reset and /reset still cut these sessions.
8859f6c to
498fd8d
Compare
…lt reset (openclaw#97931) The gateway agent.run freshness decision called evaluateSessionFreshness directly at both of its decision sites with no provider-owned guard, so a provider-owned CLI session (claude-cli, codex, gemini-cli) under the default reset config was rotated after the daily boundary when a turn ran through the gateway path (webchat, openclaw agent, ACP, control UI, cron, heartbeat). The rotation cleared the CLI session binding and split the transcript, violating the documented exemption that the inbound auto-reply path and the canonical session helper already honor. Route both gateway freshness decisions through the same resetPolicy.configured !== true && hasProviderOwnedSession(entry) skip the inbound path uses, and export hasProviderOwnedSession so the predicate has one shared definition instead of a third copy. Explicit session.reset and /reset still cut these sessions.
…reset (#98356) The provider-owned CLI session exemption added for the gateway agent.run path in #97931 was not applied to the non-gateway session resolvers. Scheduled isolated-agent cron jobs run through runCronIsolatedAgentTurn -> resolveCronSession, and the local openclaw command runs through resolveSession; both called evaluateSessionFreshness directly with no provider-owned guard. Under the default reset config a persistent-target cron job on a CLI runtime (claude-cli, codex, gemini-cli) therefore rotated its session after the daily boundary, minting a new sessionId and dropping the cliSessionBindings, so the agent silently lost its underlying CLI conversation every morning and the transcript was split. Because resolveCronSession also backs the heartbeat runner, that surface was affected too. Route both resolvers through the same resetPolicy.configured !== true && hasProviderOwnedSession(entry) skip the gateway and inbound paths already use. Explicit session.reset and configured resets still rotate these sessions, and the command path still rotates when the terminal main transcript is newer than the registry.
…lt reset (openclaw#97931) The gateway agent.run freshness decision called evaluateSessionFreshness directly at both of its decision sites with no provider-owned guard, so a provider-owned CLI session (claude-cli, codex, gemini-cli) under the default reset config was rotated after the daily boundary when a turn ran through the gateway path (webchat, openclaw agent, ACP, control UI, cron, heartbeat). The rotation cleared the CLI session binding and split the transcript, violating the documented exemption that the inbound auto-reply path and the canonical session helper already honor. Route both gateway freshness decisions through the same resetPolicy.configured !== true && hasProviderOwnedSession(entry) skip the inbound path uses, and export hasProviderOwnedSession so the predicate has one shared definition instead of a third copy. Explicit session.reset and /reset still cut these sessions.
…reset (openclaw#98356) The provider-owned CLI session exemption added for the gateway agent.run path in openclaw#97931 was not applied to the non-gateway session resolvers. Scheduled isolated-agent cron jobs run through runCronIsolatedAgentTurn -> resolveCronSession, and the local openclaw command runs through resolveSession; both called evaluateSessionFreshness directly with no provider-owned guard. Under the default reset config a persistent-target cron job on a CLI runtime (claude-cli, codex, gemini-cli) therefore rotated its session after the daily boundary, minting a new sessionId and dropping the cliSessionBindings, so the agent silently lost its underlying CLI conversation every morning and the transcript was split. Because resolveCronSession also backs the heartbeat runner, that surface was affected too. Route both resolvers through the same resetPolicy.configured !== true && hasProviderOwnedSession(entry) skip the gateway and inbound paths already use. Explicit session.reset and configured resets still rotate these sessions, and the command path still rotates when the terminal main transcript is newer than the registry.
…reset (openclaw#98356) The provider-owned CLI session exemption added for the gateway agent.run path in openclaw#97931 was not applied to the non-gateway session resolvers. Scheduled isolated-agent cron jobs run through runCronIsolatedAgentTurn -> resolveCronSession, and the local openclaw command runs through resolveSession; both called evaluateSessionFreshness directly with no provider-owned guard. Under the default reset config a persistent-target cron job on a CLI runtime (claude-cli, codex, gemini-cli) therefore rotated its session after the daily boundary, minting a new sessionId and dropping the cliSessionBindings, so the agent silently lost its underlying CLI conversation every morning and the transcript was split. Because resolveCronSession also backs the heartbeat runner, that surface was affected too. Route both resolvers through the same resetPolicy.configured !== true && hasProviderOwnedSession(entry) skip the gateway and inbound paths already use. Explicit session.reset and configured resets still rotate these sessions, and the command path still rotates when the terminal main transcript is newer than the registry.
…lt reset (openclaw#97931) The gateway agent.run freshness decision called evaluateSessionFreshness directly at both of its decision sites with no provider-owned guard, so a provider-owned CLI session (claude-cli, codex, gemini-cli) under the default reset config was rotated after the daily boundary when a turn ran through the gateway path (webchat, openclaw agent, ACP, control UI, cron, heartbeat). The rotation cleared the CLI session binding and split the transcript, violating the documented exemption that the inbound auto-reply path and the canonical session helper already honor. Route both gateway freshness decisions through the same resetPolicy.configured !== true && hasProviderOwnedSession(entry) skip the inbound path uses, and export hasProviderOwnedSession so the predicate has one shared definition instead of a third copy. Explicit session.reset and /reset still cut these sessions. [adapted] 6.11 has no src/config/sessions/entry-freshness.ts; hasProviderOwnedSession hoisted into src/agents/cli-session.ts (next to getCliSessionBinding) and shared by auto-reply/reply/session.ts + gateway agent.ts. (cherry picked from commit d9aedc3)
…reset (openclaw#98356) The provider-owned CLI session exemption added for the gateway agent.run path in openclaw#97931 was not applied to the non-gateway session resolvers. Scheduled isolated-agent cron jobs run through runCronIsolatedAgentTurn -> resolveCronSession, and the local openclaw command runs through resolveSession; both called evaluateSessionFreshness directly with no provider-owned guard. Under the default reset config a persistent-target cron job on a CLI runtime (claude-cli, codex, gemini-cli) therefore rotated its session after the daily boundary, minting a new sessionId and dropping the cliSessionBindings, so the agent silently lost its underlying CLI conversation every morning and the transcript was split. Because resolveCronSession also backs the heartbeat runner, that surface was affected too. Route both resolvers through the same resetPolicy.configured !== true && hasProviderOwnedSession(entry) skip the gateway and inbound paths already use. Explicit session.reset and configured resets still rotate these sessions, and the command path still rotates when the terminal main transcript is newer than the registry. (cherry picked from commit 7c5ce40) [adapted] import hasProviderOwnedSession from src/agents/cli-session.ts (6.11 home; no entry-freshness.ts module).
Related: #70106
What Problem This Solves
Fixes an issue where users with an active provider-owned CLI session (claude-cli, and likewise codex and gemini-cli) under the default reset configuration lose their conversation after the daily 4am boundary whenever the turn is driven through the gateway, that is dashboard webchat, the
openclaw agentCLI, ACP, the control UI, and cron or heartbeat runs. The provider-side conversation binding is dropped and the transcript rotates even though the user never requested a reset.The documented contract is that these sessions are exempt from the implicit daily default:
The inbound auto-reply path already honors this exemption. Only the gateway path violated it, so the same session would survive on an inbound message but rotate when the next turn came through the gateway.
Why This Change Was Made
The provider-owned skip exists in the canonical helper (
src/config/sessions/entry-freshness.ts) and is mirrored by the inbound path (src/auto-reply/reply/session.ts), but the gatewayagent.runhandler calledevaluateSessionFreshnessdirectly at both of its freshness decision sites with no provider-owned guard. When freshness resolved to stale, the gateway minted a new session id, set the rotation flag, and cleared all CLI session bindings. This change routes both gateway freshness decisions through the sameresetPolicy.configured !== true && hasProviderOwnedSession(entry)skip the inbound path uses, reusing the canonical predicate (now exported) rather than adding a third copy. Explicitsession.resetpolicies and/resetare unaffected because the skip only applies when reset is not explicitly configured.User Impact
Provider-owned CLI sessions are no longer silently rotated at the daily boundary when a turn arrives through the gateway. The provider-side conversation binding and transcript are preserved, matching the inbound path and the documented behavior. Users who want timed expiry of these sessions still get it by configuring
session.resetor by running/reset.Evidence
src/gateway/server-methods/agent.test.tsdrives the realagentHandlers.agenthandler with a provider-owned entry under the default reset config past the daily boundary; it fails on pristine main (session rotated, binding dropped) and passes with this patch.Root cause
src/gateway/server-methods/agent.tsresolved freshness directly at both decision sites with no provider-owned guard (before):Once
freshness.freshis false the handler setscanReuseSession = false, mints a new session id, flags the rotation, and clears all CLI session bindings.Fix
Both sites gate the freshness call with the same skip the inbound path applies (after):
hasProviderOwnedSessionis exported fromsrc/config/sessions/entry-freshness.tsand reused, so the predicate has one shared definition rather than a third duplicate.Why this is the right boundary
The gateway is the surface that diverged from the canonical helper and the inbound mirror. Routing both gateway freshness decisions through the same predicate restores a single behavior across surfaces. Both gateway call sites are fixed consistently (initial freshness and the post-build
freshFreshness). The inbound path (session.ts) already had the skip and is unchanged. Explicitsession.resetand/resetpaths keep cutting these sessions because the skip is gated onresetPolicy.configured !== true.Verification
node scripts/run-vitest.mjs src/gateway/server-methods/agent.test.ts -t "provider-owned CLI session across the daily default boundary": passes with the patch, fails on pristine main.node scripts/run-oxlint.mjs <changed files>: clean.oxfmt --check <changed files>: clean.node scripts/run-tsgo.mjs -p tsconfig.core.jsonand-p test/tsconfig/tsconfig.core.test.json: clean.Real behavior proof
Behavior addressed: a provider-owned claude-cli session under the default reset config is rotated and its CLI binding dropped after the daily 4am boundary when the turn is driven through the gateway agent.run handler, despite the documented provider-owned exemption.
Real environment tested: drove the real
agentHandlers.agentgateway handler (src/gateway/server-methods/agent.ts) on pristine main 843ad14 and on the patched tree with identical inputs; the real freshness resolver, reset-policy resolver, provider-owned predicate, and CLI-session-binding lookup all stayed real; only the session store writer and the downstream agent command were stubbed to capture the resolved session identity and lifecycle hook.Exact steps or command run after this patch: invoked the gateway agent handler with a provider-owned entry (modelProvider claude-cli, cliSessionBindings claude-cli to a conversation id) whose session started 25 hours before a fixed now of 2026-04-25T12:00:00Z, under the default reset config, then recorded the resolved run session id, the persisted cliSessionBindings, the lifecycle end hook, and the persisted sessionStartedAt.
Evidence after fix:
Observed result after fix: the gateway now resolves the run to the existing session id, preserves the claude-cli conversation binding, fires no session-end lifecycle hook, and leaves the original session start time intact, so the provider-owned session is no longer cut by the implicit daily default.
What was not tested: no live claude-cli provider request was issued; the persistence writer and downstream agent command were stubbed; full build was not run.