Skip to content

fix(heartbeat): clear sessionFile on new isolated session to prevent context accumulation#57136

Open
lubao515 wants to merge 1 commit intoopenclaw:mainfrom
lubao515:fix/heartbeat-isolated-session-clears-session-file
Open

fix(heartbeat): clear sessionFile on new isolated session to prevent context accumulation#57136
lubao515 wants to merge 1 commit intoopenclaw:mainfrom
lubao515:fix/heartbeat-isolated-session-clears-session-file

Conversation

@lubao515
Copy link
Copy Markdown

Problem

When isolatedSession: true is configured for heartbeats, resolveCronSession is called with forceNew: true and correctly generates a new sessionId (UUID) on every heartbeat tick.

However, the new session entry was spread from the prior entry (...entry), which preserved the old sessionFile path from the previous run.

resolveSessionFilePath has this logic:

function resolveSessionFilePath(sessionId, entry, opts) {
  const candidate = entry?.sessionFile?.trim();
  if (candidate) return resolvePathWithinSessionsDir(...);  // prefers entry.sessionFile!
  return resolveSessionTranscriptPathInDir(sessionId, sessionsDir);  // only fallback
}

So every heartbeat continued appending to the same old transcript file, even though a new sessionId was assigned. This caused unbounded context growth that eventually triggered model fallbacks and failed heartbeat runs — despite isolatedSession: true being set.

Observed: context grew from ~18K tokens to 190K+ tokens over 24 hours, causing repeated Gemini API errors and fallback to backup models.

Fix

Clear sessionFile when isNewSession is true, so resolveSessionFilePath falls through to derive a fresh path from the new sessionId.

...(isNewSession && { sessionFile: undefined }),

This is analogous to the existing cleanup of lastChannel, lastTo, deliveryContext, etc. that was already done on new session rollover.

Testing

  • Verified locally with isolatedSession: true + 1h heartbeat interval: each heartbeat now creates a new <uuid>.jsonl transcript file, starting at ~18K tokens instead of inheriting prior session history.
  • Existing session.test.ts passes.

Related

Fixes #43767

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 29, 2026

Greptile Summary

This PR fixes a bug where isolatedSession: true on heartbeats failed to actually isolate each run's context, because the new sessionEntry spread the old entry (including its sessionFile path) before assigning a new sessionId. Since resolveSessionFilePath prefers the stored entry.sessionFile over a freshly derived path, every heartbeat continued appending to the same transcript file regardless of the new UUID.

The fix is a single targeted change: when isNewSession is true, explicitly set sessionFile: undefined in the new entry so resolveSessionFilePath falls through to its ID-based path derivation.

  • The spread idiom ...(isNewSession && { sessionFile: undefined }) is correct TypeScript; it shadows the inherited sessionFile from ...entry with undefined, which is treated as absent by the truthy check in resolveSessionFilePath.
  • The change is minimal and well-commented, with a direct reference to the upstream issue.
  • No new test was added to assert that sessionEntry.sessionFile is undefined when isNewSession is true (e.g. the forceNew: true and stale-session tests don't verify this field), though the existing test suite passes and the logic is straightforward.

Confidence Score: 5/5

Safe to merge — single-line targeted fix with correct semantics and no risk of regressions on the non-isolated session path.

The fix is minimal, well-reasoned, and directly addresses the root cause. The ...(isNewSession && { sessionFile: undefined }) idiom correctly overrides the inherited field, and resolveSessionFilePath already handles undefined as 'no stored path' — so the behavior change is exactly as intended. The only gap is the absence of a dedicated regression test for the sessionFile clearing, which is a P2 concern at most and does not block merge.

No files require special attention.

Important Files Changed

Filename Overview
src/cron/isolated-agent/session.ts Adds ...(isNewSession && { sessionFile: undefined }) to clear the inherited session file path when a new session is created, fixing unbounded context growth with isolatedSession: true.

Comments Outside Diff (1)

  1. src/cron/isolated-agent/session.test.ts, line 126-144 (link)

    P2 Missing regression test for sessionFile clearing

    The existing forceNew and stale-session tests verify that sessionId rotates and that user-configured overrides (e.g. modelOverride) are preserved, but none of them assert that sessionFile is set to undefined on the new entry — which is exactly the behaviour this PR adds. Without such an assertion, a future refactor of the spread order could silently reintroduce the same bug.

    Consider adding an explicit check in the forceNew test (and/or the stale-session test):

    it("creates new sessionId when forceNew is true", () => {
      const result = resolveWithStoredEntry({
        entry: {
          sessionId: "existing-session-id-456",
          updatedAt: NOW_MS - 1000,
          systemSent: true,
          modelOverride: "sonnet-4",
          providerOverride: "anthropic",
          sessionFile: "old-uuid.jsonl",   // ← add a stored sessionFile
        },
        fresh: true,
        forceNew: true,
      });
    
      expect(result.sessionEntry.sessionId).not.toBe("existing-session-id-456");
      expect(result.isNewSession).toBe(true);
      expect(result.sessionEntry.modelOverride).toBe("sonnet-4");
      // Core regression guard for issue #43767
      expect(result.sessionEntry.sessionFile).toBeUndefined();
    });

    The same assertion would be valuable in the "creates new sessionId when session is stale" test.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/cron/isolated-agent/session.test.ts
    Line: 126-144
    
    Comment:
    **Missing regression test for `sessionFile` clearing**
    
    The existing `forceNew` and stale-session tests verify that `sessionId` rotates and that user-configured overrides (e.g. `modelOverride`) are preserved, but none of them assert that `sessionFile` is set to `undefined` on the new entry — which is exactly the behaviour this PR adds. Without such an assertion, a future refactor of the spread order could silently reintroduce the same bug.
    
    Consider adding an explicit check in the `forceNew` test (and/or the stale-session test):
    
    ```ts
    it("creates new sessionId when forceNew is true", () => {
      const result = resolveWithStoredEntry({
        entry: {
          sessionId: "existing-session-id-456",
          updatedAt: NOW_MS - 1000,
          systemSent: true,
          modelOverride: "sonnet-4",
          providerOverride: "anthropic",
          sessionFile: "old-uuid.jsonl",   // ← add a stored sessionFile
        },
        fresh: true,
        forceNew: true,
      });
    
      expect(result.sessionEntry.sessionId).not.toBe("existing-session-id-456");
      expect(result.isNewSession).toBe(true);
      expect(result.sessionEntry.modelOverride).toBe("sonnet-4");
      // Core regression guard for issue #43767
      expect(result.sessionEntry.sessionFile).toBeUndefined();
    });
    ```
    
    The same assertion would be valuable in the "creates new sessionId when session is stale" test.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/cron/isolated-agent/session.test.ts
Line: 126-144

Comment:
**Missing regression test for `sessionFile` clearing**

The existing `forceNew` and stale-session tests verify that `sessionId` rotates and that user-configured overrides (e.g. `modelOverride`) are preserved, but none of them assert that `sessionFile` is set to `undefined` on the new entry — which is exactly the behaviour this PR adds. Without such an assertion, a future refactor of the spread order could silently reintroduce the same bug.

Consider adding an explicit check in the `forceNew` test (and/or the stale-session test):

```ts
it("creates new sessionId when forceNew is true", () => {
  const result = resolveWithStoredEntry({
    entry: {
      sessionId: "existing-session-id-456",
      updatedAt: NOW_MS - 1000,
      systemSent: true,
      modelOverride: "sonnet-4",
      providerOverride: "anthropic",
      sessionFile: "old-uuid.jsonl",   // ← add a stored sessionFile
    },
    fresh: true,
    forceNew: true,
  });

  expect(result.sessionEntry.sessionId).not.toBe("existing-session-id-456");
  expect(result.isNewSession).toBe(true);
  expect(result.sessionEntry.modelOverride).toBe("sonnet-4");
  // Core regression guard for issue #43767
  expect(result.sessionEntry.sessionFile).toBeUndefined();
});
```

The same assertion would be valuable in the "creates new sessionId when session is stale" test.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(heartbeat): clear sessionFile on new..." | Re-trigger Greptile

@lubao515 lubao515 force-pushed the fix/heartbeat-isolated-session-clears-session-file branch 2 times, most recently from 15704d8 to d80337e Compare March 29, 2026 20:33
@lubao515 lubao515 force-pushed the fix/heartbeat-isolated-session-clears-session-file branch 4 times, most recently from 8c3c201 to 544834d Compare April 3, 2026 23:19
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 544834d95e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

lastAccountId: undefined,
lastThreadId: undefined,
deliveryContext: undefined,
sessionFile: undefined,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Archive old transcript before clearing sessionFile

Clearing sessionFile on every new cron/heartbeat session causes each isolated run to switch to a fresh <sessionId>.jsonl, but the previous transcript path is dropped from the only retained session entry and never archived/deleted in the resolveCronSession call paths (run.ts/heartbeat-runner.ts just overwrite the same key). In environments using isolatedSession: true, this creates an ever-growing set of orphan transcript files and can eventually consume significant disk space; this needs a rollover cleanup step (or explicit archival) before nulling the field.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While the point makes sense, I feel orphan clearances are a separate issue which is out of scope for this fix.

@lubao515 lubao515 force-pushed the fix/heartbeat-isolated-session-clears-session-file branch from 544834d to 0df8ec2 Compare April 3, 2026 23:34
…context accumulation

When isolatedSession: true is configured, resolveCronSession generates a
new sessionId (UUID) on every heartbeat tick. However the new session entry
was spread from the prior entry, preserving the old sessionFile path.

resolveSessionFilePath prefers entry.sessionFile when set, so every heartbeat
continued appending to the same old transcript file despite a new sessionId.
This caused unbounded context growth (18K → 190K+ tokens over 24h).

Fix: clear sessionFile when isNewSession is true, analogous to the existing
cleanup of lastChannel, lastTo, deliveryContext, etc.

Fixes openclaw#43767
@lubao515 lubao515 force-pushed the fix/heartbeat-isolated-session-clears-session-file branch from 0df8ec2 to 0c2c5a8 Compare April 3, 2026 23:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Heartbeat ignores lightContext: true, loads full agent context + unbounded session history

1 participant