Skip to content

fix(gateway): keep provider-owned CLI sessions across the daily default reset - #97931

Merged
vincentkoc merged 1 commit into
openclaw:mainfrom
yetval:fix/gateway-providerowned-daily-reset
Jul 1, 2026
Merged

vincentkoc merged 1 commit into
openclaw:mainfrom
yetval:fix/gateway-providerowned-daily-reset

Conversation

@yetval

@yetval yetval commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

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 agent CLI, 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:

  • docs/concepts/session.md: "Sessions with an active provider-owned CLI session are not cut by the implicit daily default."
  • docs/gateway/cli-backends.md: "The implicit daily session reset does not cut them; /reset and explicit session.reset policies still do."

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 gateway agent.run handler called evaluateSessionFreshness directly 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 same resetPolicy.configured !== true && hasProviderOwnedSession(entry) skip the inbound path uses, reusing the canonical predicate (now exported) rather than adding a third copy. Explicit session.reset policies and /reset are 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.reset or by running /reset.

Evidence

  • New colocated regression test in src/gateway/server-methods/agent.test.ts drives the real agentHandlers.agent handler 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.
  • Local gates on the changed files: oxlint clean, oxfmt clean, tsgo core and core-test clean.
  • Real before/after runtime output captured below.

Root cause

src/gateway/server-methods/agent.ts resolved freshness directly at both decision sites with no provider-owned guard (before):

let freshness = entry
  ? evaluateSessionFreshness({
      updatedAt: entry.updatedAt,
      ...lifecycleTimestamps,
      now,
      policy: resetPolicy,
    })
  : undefined;
const freshFreshness = freshEntry
  ? evaluateSessionFreshness({
      updatedAt: freshEntry.updatedAt,
      ...freshLifecycleTimestamps,
      now,
      policy: resetPolicy,
    })
  : undefined;

Once freshness.fresh is false the handler sets canReuseSession = 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):

const skipImplicitExpiry =
  resetPolicy.configured !== true && hasProviderOwnedSession(entry);
let freshness = entry
  ? skipImplicitExpiry
    ? ({ fresh: true } satisfies SessionFreshness)
    : evaluateSessionFreshness({ ... })
  : undefined;

hasProviderOwnedSession is exported from src/config/sessions/entry-freshness.ts and 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. Explicit session.reset and /reset paths keep cutting these sessions because the skip is gated on resetPolicy.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.json and -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.agent gateway 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:

# BEFORE (pristine main 843ad14364)
storedSessionId=provider-owned-session-id
resolvedRunSessionId=51375be8-8727-4841-b302-df350fc23946
sessionRotated=true
cliBindingClaudeCli=DROPPED
sessionEndHookFired=true
storedSessionStartedAt=1777118400000 originalStartedAt=1777028400000

# AFTER (this patch, identical inputs)
storedSessionId=provider-owned-session-id
resolvedRunSessionId=provider-owned-session-id
sessionRotated=false
cliBindingClaudeCli=claude-cli-conversation-123
sessionEndHookFired=false
storedSessionStartedAt=1777028400000 originalStartedAt=1777028400000

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.

@openclaw-barnacle openclaw-barnacle Bot added gateway Gateway runtime size: S labels Jun 29, 2026
@clawsweeper

clawsweeper Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed June 29, 2026, 6:23 PM ET / 22:23 UTC.

Summary
The PR exports the existing provider-owned session predicate, applies it to both gateway agent.run freshness checks, and adds a gateway regression test for preserving CLI bindings across the implicit daily reset boundary.

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 agent.run still evaluates freshness directly and can rotate/clear bindings.

Review metrics: none identified.

Stored data model
Persistent data-model change detected: serialized state: src/config/sessions/entry-freshness.ts. Confirm migration or upgrade compatibility proof before merge.

Root-cause cluster
Relationship: canonical
Canonical: #97931
Summary: This PR is the active gateway follow-up for the provider-owned CLI session continuity behavior established by the merged related PR.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Merge readiness
Overall: 🐚 platinum hermit
Proof: 🦞 diamond lobster
Patch quality: 🐚 platinum hermit
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • none.

Next step before merge

  • No ClawSweeper repair lane is needed because there are no actionable review findings; maintainer review and merge decision are the remaining path.

Security
Cleared: The diff reuses an existing session predicate in gateway runtime code and adds a colocated test; it does not change dependencies, CI, secrets, install scripts, package execution, or authorization boundaries.

Review details

Best possible solution:

Land the focused gateway alignment after ordinary maintainer review and required checks, keeping explicit session.reset and /reset behavior unchanged.

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 agent.run still evaluates freshness directly and can rotate/clear bindings.

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 changes

Label justifications:

  • P2: This fixes a bounded gateway session-continuity bug that can drop provider-owned CLI conversation bindings and split transcripts for affected users.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (live_output): The PR body includes copied before/after runtime output from the real gateway agent handler with identical provider-owned inputs showing the patched path preserves the session id, CLI binding, lifecycle hook state, and session start time.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes copied before/after runtime output from the real gateway agent handler with identical provider-owned inputs showing the patched path preserves the session id, CLI binding, lifecycle hook state, and session start time.
Evidence reviewed

PR surface:

Source +10, Tests +55. Total +65 across 3 files.

View PR surface stats
Area Files Added Removed Net
Source 2 23 13 +10
Tests 1 55 0 +55
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 3 78 13 +65

What I checked:

  • Repository policy read and applied: Root policy and scoped gateway/server-methods guidance were read; the review applied the session-state compatibility and whole-path review rules. (AGENTS.md:1, 843ad143647e)
  • Current main initial gateway freshness path lacks the exemption: The initially loaded requested session still calls evaluateSessionFreshness directly, so a provider-owned CLI binding can be treated as stale before reuse is decided. (src/gateway/server-methods/agent.ts:1817, 843ad143647e)
  • Current main store-locked gateway freshness path lacks the exemption: The store-locked patch builder recomputes freshness directly and can rotate the session, which then clears all CLI session bindings. (src/gateway/server-methods/agent.ts:2008, 843ad143647e)
  • Documented contract: The session docs state that active provider-owned CLI sessions are not cut by the implicit daily default and should expire through /reset or explicit session.reset. Public docs: docs/concepts/session.md. (docs/concepts/session.md:86, 843ad143647e)
  • Canonical helper already implements the desired rule: resolveSessionEntryResetFreshness treats provider-owned sessions as fresh only when the reset policy is implicit, preserving explicit reset behavior. (src/config/sessions/entry-freshness.ts:94, 843ad143647e)
  • Inbound sibling path already mirrors the exemption: The auto-reply session path applies hasProviderOwnedSession(entry) && resetPolicy.configured !== true before evaluating freshness. (src/auto-reply/reply/session.ts:514, 843ad143647e)

Likely related people:

  • obviyus: Authored the merged provider-owned CLI session lifecycle PR that added the implicit-expiry exemption, docs, and gateway binding preservation this PR extends. (role: related feature contributor; confidence: high; commits: 2202e353318d, d0b95c94af24, 16f016f07eaa; files: src/auto-reply/reply/session.ts, docs/concepts/session.md, docs/gateway/cli-backends.md)
  • vincentkoc: Git history shows recent work on reset-policy helper isolation and adjacent gateway session maintenance surfaces. (role: reset policy and gateway area contributor; confidence: medium; commits: c70be4b4afd0, 5571c786d3df; files: src/config/sessions/reset-policy.ts, src/gateway/server-methods/agent.ts)
  • steipete: Git history shows related session freshness, system-event reset, and session path documentation work around the same lifecycle contract. (role: session reset lifecycle contributor; confidence: medium; commits: 4cd68fafbb2a, 566d2d73a323, 0e3f7a82fd12; files: src/config/sessions/reset-policy.ts, src/auto-reply/reply/session.ts, docs/concepts/session.md)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

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
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. P2 Normal backlog priority with limited blast radius. labels Jun 29, 2026
…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.
@yetval
yetval force-pushed the fix/gateway-providerowned-daily-reset branch from 8859f6c to 498fd8d Compare June 29, 2026 22:11
@vincentkoc
vincentkoc merged commit d9aedc3 into openclaw:main Jul 1, 2026
102 checks passed
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jul 1, 2026
…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.
vincentkoc pushed a commit that referenced this pull request Jul 1, 2026
…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.
chenyangjun-xy pushed a commit to chenyangjun-xy/openclaw that referenced this pull request Jul 1, 2026
…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.
chenyangjun-xy pushed a commit to chenyangjun-xy/openclaw that referenced this pull request Jul 1, 2026
…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.
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jul 2, 2026
…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.
amittell pushed a commit to amittell/openclaw that referenced this pull request Jul 2, 2026
…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)
amittell pushed a commit to amittell/openclaw that referenced this pull request Jul 2, 2026
…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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gateway Gateway runtime P2 Normal backlog priority with limited blast radius. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. size: S status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants