Skip to content

fix(memory-flush): allow first flush when compactionCount is 0#47174

Closed
MorikawaSouma wants to merge 2 commits into
openclaw:mainfrom
MorikawaSouma:fix/47143-memory-flush-check
Closed

fix(memory-flush): allow first flush when compactionCount is 0#47174
MorikawaSouma wants to merge 2 commits into
openclaw:mainfrom
MorikawaSouma:fix/47143-memory-flush-check

Conversation

@MorikawaSouma

Copy link
Copy Markdown

Summary

Fixes #47143

Root Cause

hasAlreadyFlushedForCurrentCompaction returned true when both compactionCount and memoryFlushCompactionCount were 0. This incorrectly blocked the first memory flush for sessions that had never been compacted.

// Before: 0 === 0 → true (incorrectly blocks flush)
return typeof lastFlushAt === "number" && lastFlushAt === compactionCount;

Fix

When compactionCount is 0 (never compacted), the function now returns false to allow the first memory flush.

// After: When compactionCount is 0, allow flush
if (compactionCount === 0) {
  return false;
}
return typeof lastFlushAt === "number" && lastFlushAt === compactionCount;

Impact

  • Sessions with high token counts but no compaction history will now correctly trigger auto-flush
  • Prevents unbounded context growth for sessions that haven't been compacted yet

Test Plan

  • Updated hasAlreadyFlushedForCurrentCompaction tests to reflect new behavior
  • Test case returns false when compactionCount is 0 (never compacted) verifies the fix

Previously, hasAlreadyFlushedForCurrentCompaction returned true when
both compactionCount and memoryFlushCompactionCount were 0, which
incorrectly blocked the first memory flush for sessions that had never
been compacted.

Now when compactionCount is 0 (never compacted), the function returns
false to allow the first memory flush. This fixes the edge case where
sessions with high token counts but no compaction history would never
auto-flush.

Fixes openclaw#47143

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

Copy link
Copy Markdown
Contributor

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: 5e3d48f1ed

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +234 to +235
if (compactionCount === 0) {
return false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Badge Preserve cycle gating when compaction count is zero

Returning false unconditionally when compactionCount is 0 removes the “already flushed this cycle” guard for the initial cycle, so sessions that have crossed the threshold can run memory flush on every turn before the first compaction. This is a regression because agent-runner-memory.ts persists memoryFlushCompactionCount as 0 when a flush run does not emit a compaction-end event, and this helper now ignores that persisted marker entirely for compactionCount=0, leading to repeated flush runs and duplicate memory writes.

Useful? React with 👍 / 👎.

@greptile-apps

greptile-apps Bot commented Mar 15, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR attempts to fix a bug where hasAlreadyFlushedForCurrentCompaction incorrectly returned true for sessions with compactionCount === 0, blocking their first-ever memory flush. The fix adds an early return when compactionCount === 0, and the test suite is updated accordingly. A stray pr-body.md file (belonging to a different PR, #47139) has also been accidentally committed.

Key issues found:

  • Regression — repeated flushes for compactionCount === 0 sessions: The early return if (compactionCount === 0) { return false; } is too broad. After the first flush completes, memoryFlushCompactionCount is stored as 0 (see agent-runner-memory.ts). On the next evaluation, compactionCount is still 0, so the guard is skipped and another flush is allowed — creating a loop of repeated flushes for any session that has never been compacted. The correct fix should only bypass the guard when memoryFlushCompactionCount is undefined (never recorded), not when it is 0 (already recorded). A simpler alternative: check typeof lastFlushAt !== "number" first and return false, then return lastFlushAt === compactionCount — this handles the original bug without removing the post-flush guard.
  • Missing test coverage: The updated test suite does not include a case for {compactionCount: 0, memoryFlushCompactionCount: 0}true (i.e., flush already happened for cycle 0), which is the scenario that now silently regresses.
  • Stray pr-body.md: This file contains the description for an unrelated PR (#47139) and should be removed from the repository.

Confidence Score: 2/5

  • Not safe to merge — the fix introduces a regression that allows unbounded repeated memory flushes for never-compacted sessions.
  • The core logic change removes the flush de-duplication guard entirely for compactionCount === 0 rather than only for the "never-flushed" (memoryFlushCompactionCount === undefined) case. This turns a one-time first-flush fix into an infinite-flush loop for sessions that have never been compacted. Combined with an accidentally committed unrelated file, the PR requires changes before it is safe to merge.
  • src/auto-reply/reply/memory-flush.ts — the hasAlreadyFlushedForCurrentCompaction implementation needs a more precise condition; pr-body.md — stray file to be deleted.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/auto-reply/reply/memory-flush.ts
Line: 234-236

Comment:
**Guard disabled for all `compactionCount === 0` sessions after first flush**

The early return unconditionally allows flushes whenever `compactionCount === 0`, but after the first flush completes, `memoryFlushCompactionCount` is stored as `0` (see `agent-runner-memory.ts` lines 523–526: `let memoryFlushCompactionCount = activeSessionEntry?.compactionCount ?? ... ?? 0`). On the very next evaluation, `compactionCount` is still `0`, so `hasAlreadyFlushedForCurrentCompaction` returns `false` again — allowing a second flush, and a third, and so on, until the session is finally compacted (incrementing `compactionCount` to ≥1).

The correct fix should distinguish between "never flushed" (`lastFlushAt === undefined`) and "already flushed at cycle 0" (`lastFlushAt === 0`):

```
// Before fix: 0 === 0 → true (incorrectly blocks first flush when memoryFlushCompactionCount
// was somehow 0 without a real flush occurring)
return typeof lastFlushAt === "number" && lastFlushAt === compactionCount;
```

A safer alternative that preserves the guard after the first flush completes:

```typescript
  if (typeof lastFlushAt !== "number") {
    // Never flushed yet — always allow.
    return false;
  }
  return lastFlushAt === compactionCount;
```

This returns `false` when `lastFlushAt` is `undefined` (no flush recorded) regardless of `compactionCount`, and correctly returns `true` for `{compactionCount: 0, memoryFlushCompactionCount: 0}` after the first flush has been stored.

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

---

This is a comment left during a code review.
Path: pr-body.md
Line: 1-19

Comment:
**Stray `pr-body.md` file belongs to a different PR**

This file describes a fix for issue `#47139` (skill bundled-status display in the Control Panel), which is unrelated to this PR (`#47174`, the memory-flush `compactionCount === 0` bug). It appears to have been accidentally committed and should be removed from the repository.

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

---

This is a comment left during a code review.
Path: src/auto-reply/reply/reply-state.test.ts
Line: 389-406

Comment:
**Missing test for "already flushed at cycle 0" — the regression introduced by this fix**

The new test suite covers `{compactionCount: 0, memoryFlushCompactionCount: 0}``false`, but there is no test asserting what should happen *after* a flush has been persisted for `compactionCount = 0`. Because the implementation now unconditionally returns `false` when `compactionCount === 0`, the following case is not covered and would incorrectly return `false`:

```typescript
it("returns true when compactionCount is 0 and memoryFlushCompactionCount is 0 after a flush", () => {
  // After runMemoryFlush completes for a never-compacted session, the store is
  // updated with memoryFlushCompactionCount = 0.  The guard must block a
  // second flush within the same compaction cycle.
  expect(
    hasAlreadyFlushedForCurrentCompaction({
      compactionCount: 0,
      memoryFlushCompactionCount: 0, // flush already recorded
    }),
  ).toBe(true); // currently fails with the new implementation
});
```

This directly relates to the regression described in the comment on `memory-flush.ts`: since there is no way to distinguish "never flushed" (`undefined`) from "already flushed at cycle 0" (`0`) with the current guard, flushes will repeat indefinitely for `compactionCount === 0` sessions.

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

Last reviewed commit: 5e3d48f

Comment on lines +234 to +236
if (compactionCount === 0) {
return false;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Guard disabled for all compactionCount === 0 sessions after first flush

The early return unconditionally allows flushes whenever compactionCount === 0, but after the first flush completes, memoryFlushCompactionCount is stored as 0 (see agent-runner-memory.ts lines 523–526: let memoryFlushCompactionCount = activeSessionEntry?.compactionCount ?? ... ?? 0). On the very next evaluation, compactionCount is still 0, so hasAlreadyFlushedForCurrentCompaction returns false again — allowing a second flush, and a third, and so on, until the session is finally compacted (incrementing compactionCount to ≥1).

The correct fix should distinguish between "never flushed" (lastFlushAt === undefined) and "already flushed at cycle 0" (lastFlushAt === 0):

// Before fix: 0 === 0 → true (incorrectly blocks first flush when memoryFlushCompactionCount
// was somehow 0 without a real flush occurring)
return typeof lastFlushAt === "number" && lastFlushAt === compactionCount;

A safer alternative that preserves the guard after the first flush completes:

  if (typeof lastFlushAt !== "number") {
    // Never flushed yet — always allow.
    return false;
  }
  return lastFlushAt === compactionCount;

This returns false when lastFlushAt is undefined (no flush recorded) regardless of compactionCount, and correctly returns true for {compactionCount: 0, memoryFlushCompactionCount: 0} after the first flush has been stored.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/auto-reply/reply/memory-flush.ts
Line: 234-236

Comment:
**Guard disabled for all `compactionCount === 0` sessions after first flush**

The early return unconditionally allows flushes whenever `compactionCount === 0`, but after the first flush completes, `memoryFlushCompactionCount` is stored as `0` (see `agent-runner-memory.ts` lines 523–526: `let memoryFlushCompactionCount = activeSessionEntry?.compactionCount ?? ... ?? 0`). On the very next evaluation, `compactionCount` is still `0`, so `hasAlreadyFlushedForCurrentCompaction` returns `false` again — allowing a second flush, and a third, and so on, until the session is finally compacted (incrementing `compactionCount` to ≥1).

The correct fix should distinguish between "never flushed" (`lastFlushAt === undefined`) and "already flushed at cycle 0" (`lastFlushAt === 0`):

```
// Before fix: 0 === 0 → true (incorrectly blocks first flush when memoryFlushCompactionCount
// was somehow 0 without a real flush occurring)
return typeof lastFlushAt === "number" && lastFlushAt === compactionCount;
```

A safer alternative that preserves the guard after the first flush completes:

```typescript
  if (typeof lastFlushAt !== "number") {
    // Never flushed yet — always allow.
    return false;
  }
  return lastFlushAt === compactionCount;
```

This returns `false` when `lastFlushAt` is `undefined` (no flush recorded) regardless of `compactionCount`, and correctly returns `true` for `{compactionCount: 0, memoryFlushCompactionCount: 0}` after the first flush has been stored.

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

Comment thread pr-body.md Outdated
Comment on lines +1 to +19
## Summary

Fixes #47139

Previously, the Control Panel displayed workspace-installed skills as "bundled" when they shared names with bundled skills (e.g., summarize, nano-pdf, video-frames). This was because the bundled check used `bundledNames.has(entry.skill.name)` instead of checking the actual source path.

### Root Cause
The `bundled` property in `buildSkillStatus` checked if the skill name existed in `bundledNames` set, rather than checking the actual `source` field of the skill entry. This caused workspace-installed copies of bundled skills to be incorrectly displayed as bundled.

### Fix
Now the `bundled` property is determined solely by the skill's `source` field, which correctly reflects where the skill was loaded from.

### Test Plan
1. Install a skill from ClawHub that originally came bundled with OpenClaw (e.g., summarize, video-frames)
2. Open Control Panel Web UI
3. Check skills list
4. Verify the skill shows as "workspace" instead of "bundled"

Also verify `openclaw skills list` and Control Panel show consistent source values.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Stray pr-body.md file belongs to a different PR

This file describes a fix for issue #47139 (skill bundled-status display in the Control Panel), which is unrelated to this PR (#47174, the memory-flush compactionCount === 0 bug). It appears to have been accidentally committed and should be removed from the repository.

Prompt To Fix With AI
This is a comment left during a code review.
Path: pr-body.md
Line: 1-19

Comment:
**Stray `pr-body.md` file belongs to a different PR**

This file describes a fix for issue `#47139` (skill bundled-status display in the Control Panel), which is unrelated to this PR (`#47174`, the memory-flush `compactionCount === 0` bug). It appears to have been accidentally committed and should be removed from the repository.

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

Comment on lines +389 to 406
it("returns false when compactionCount is 0 (never compacted)", () => {
// When compactionCount is 0, the session has never been compacted,
// so we should allow the first memory flush.
expect(
hasAlreadyFlushedForCurrentCompaction({
compactionCount: 0,
memoryFlushCompactionCount: 0,
}),
).toBe(true);
).toBe(false);
});

it("treats missing compactionCount as 0 and allows flush", () => {
expect(
hasAlreadyFlushedForCurrentCompaction({
memoryFlushCompactionCount: 0,
}),
).toBe(false);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Missing test for "already flushed at cycle 0" — the regression introduced by this fix

The new test suite covers {compactionCount: 0, memoryFlushCompactionCount: 0}false, but there is no test asserting what should happen after a flush has been persisted for compactionCount = 0. Because the implementation now unconditionally returns false when compactionCount === 0, the following case is not covered and would incorrectly return false:

it("returns true when compactionCount is 0 and memoryFlushCompactionCount is 0 after a flush", () => {
  // After runMemoryFlush completes for a never-compacted session, the store is
  // updated with memoryFlushCompactionCount = 0.  The guard must block a
  // second flush within the same compaction cycle.
  expect(
    hasAlreadyFlushedForCurrentCompaction({
      compactionCount: 0,
      memoryFlushCompactionCount: 0, // flush already recorded
    }),
  ).toBe(true); // currently fails with the new implementation
});

This directly relates to the regression described in the comment on memory-flush.ts: since there is no way to distinguish "never flushed" (undefined) from "already flushed at cycle 0" (0) with the current guard, flushes will repeat indefinitely for compactionCount === 0 sessions.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/auto-reply/reply/reply-state.test.ts
Line: 389-406

Comment:
**Missing test for "already flushed at cycle 0" — the regression introduced by this fix**

The new test suite covers `{compactionCount: 0, memoryFlushCompactionCount: 0}``false`, but there is no test asserting what should happen *after* a flush has been persisted for `compactionCount = 0`. Because the implementation now unconditionally returns `false` when `compactionCount === 0`, the following case is not covered and would incorrectly return `false`:

```typescript
it("returns true when compactionCount is 0 and memoryFlushCompactionCount is 0 after a flush", () => {
  // After runMemoryFlush completes for a never-compacted session, the store is
  // updated with memoryFlushCompactionCount = 0.  The guard must block a
  // second flush within the same compaction cycle.
  expect(
    hasAlreadyFlushedForCurrentCompaction({
      compactionCount: 0,
      memoryFlushCompactionCount: 0, // flush already recorded
    }),
  ).toBe(true); // currently fails with the new implementation
});
```

This directly relates to the regression described in the comment on `memory-flush.ts`: since there is no way to distinguish "never flushed" (`undefined`) from "already flushed at cycle 0" (`0`) with the current guard, flushes will repeat indefinitely for `compactionCount === 0` sessions.

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

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

Copy link
Copy Markdown
Contributor

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: e4da6a5188

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +234 to +235
if (compactionCount === 0) {
return false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Badge Keep flush-cycle guard when compaction count is zero

Returning false for every compactionCount === 0 drops the duplicate-run guard for the entire pre-compaction cycle: after the first flush, runMemoryFlushTurn persists memoryFlushCompactionCount to the current count (which is still 0 when no compaction-end event is emitted), but this helper now ignores that marker and allows another flush on every subsequent turn that stays above threshold. That causes repeated memory-flush runs and duplicate memory writes until compaction finally increments.

Useful? React with 👍 / 👎.

@steipete

Copy link
Copy Markdown
Contributor

Closing this as not reproducible on current main after Codex review.

Close PR #47174. Current main already allows the first memory flush for a new session because memoryFlushCompactionCount starts as undefined; the proposed compactionCount === 0 early return would instead disable the duplicate-flush guard for cycle 0 and allow repeated flushes.

What I checked:

  • New sessions start with no recorded memory flush: When a new session is created, core initializes compactionCount = 0 and explicitly clears memoryFlushCompactionCount to undefined, so the first flush is already eligible on current main. That contradicts the PR premise that never-compacted sessions are blocked because both values are 0. (src/auto-reply/reply/session.ts:751, 0bef73d151bd)
  • Current guard only blocks when a flush was actually recorded for this compaction cycle: hasAlreadyFlushedForCurrentCompaction returns true only when memoryFlushCompactionCount is a number equal to the current compactionCount. If memoryFlushCompactionCount is undefined, the helper returns false, which already allows the first flush. (src/auto-reply/reply/memory-flush.ts:113, 0bef73d151bd)
  • Flush persistence makes {compactionCount: 0, memoryFlushCompactionCount: 0} mean 'already flushed': After a memory flush run, the runner persists memoryFlushCompactionCount to the active compaction count. Before the first compaction completes, that persisted value can still be 0, so forcing false for all compactionCount === 0 cases would re-enable repeated flushes within the same cycle. (src/auto-reply/reply/agent-runner-memory.ts:800, 0bef73d151bd)
  • Existing test on main matches the duplicate-run guard: Current test coverage expects hasAlreadyFlushedForCurrentCompaction({ memoryFlushCompactionCount: 0 }) to return true when compactionCount is missing and therefore treated as 0. That matches the persisted 'already flushed at cycle 0' interpretation, not the PR’s proposed change. (src/auto-reply/reply/reply-state.test.ts:359, 0bef73d151bd)
  • PR discussion already identified the regression: The supplied review context includes both Codex and Greptile review comments pointing out the same problem: returning false for every compactionCount === 0 would drop the cycle-0 dedup guard and allow repeated memory flushes until compaction increments. (e4da6a51888b)

Review notes: reviewed against 0bef73d151bd.

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.

memoryFlush never triggers when compactionCount and memoryFlushCompactionCount are both 0

2 participants