Skip to content

fix: memoryFlush never triggers when compactionCount and memoryFlushCompactionCount are both 0#47247

Closed
MorikawaSouma wants to merge 3 commits into
openclaw:mainfrom
MorikawaSouma:fix/issue-47143-memory-flush-never-triggers
Closed

fix: memoryFlush never triggers when compactionCount and memoryFlushCompactionCount are both 0#47247
MorikawaSouma wants to merge 3 commits into
openclaw:mainfrom
MorikawaSouma:fix/issue-47143-memory-flush-never-triggers

Conversation

@MorikawaSouma

Copy link
Copy Markdown

Fixes #47143

Problem

In hasAlreadyFlushedForCurrentCompaction, when a session has:

  • compactionCount: 0 (never compacted)
  • memoryFlushCompactionCount: 0 (never flushed)

The function returned 0 === 0 → true, incorrectly indicating "already flushed", so the memory flush was skipped.

Solution

Added a special case to return false when both compactionCount and memoryFlushCompactionCount are 0. This treats the 0/0 case as the initial state (never flushed) rather than "already flushed at count 0".

Changes

  1. Modified hasAlreadyFlushedForCurrentCompaction in memory-flush.ts:

    • Added explicit check for undefined memoryFlushCompactionCount
    • Added special case to return false when both counts are 0
  2. Updated tests in reply-state.test.ts:

    • Changed the "treats missing compactionCount as 0" test to expect false
    • Added test for both counts being 0 (initial state)
    • Added test for missing compactionCount with memoryFlushCompactionCount: 0
    • Added test for memoryFlushCompactionCount: 0 with compactionCount: 1

Testing

All related tests pass:

  • 6 tests in hasAlreadyFlushedForCurrentCompaction suite
  • 5 tests in memory-flush.test.ts

@openclaw-barnacle openclaw-barnacle Bot added app: web-ui App: web-ui size: S r: too-many-prs Auto-close: author has more than twenty active PRs. labels Mar 15, 2026
@openclaw-barnacle

Copy link
Copy Markdown

Closing this PR because the author has more than 10 active PRs in this repo. Please reduce the active PR queue and reopen or resubmit once it is back under the limit. You can close your own PRs to get back under the limit.

@greptile-apps

greptile-apps Bot commented Mar 15, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR patches the hasAlreadyFlushedForCurrentCompaction guard to allow the very first memory flush when compactionCount and memoryFlushCompactionCount are both 0, and adds a lastMessagePreview field to the sessions UI. The core logic fix has a soundness issue that could cause the repeated-flush protection to stop working for sessions that never undergo compaction.

Key changes

  • memory-flush.ts – adds a lastFlushAt === 0 && compactionCount === 0 → return false branch to allow an initial flush in the 0/0 state.
  • reply-state.test.ts – updates and expands the hasAlreadyFlushedForCurrentCompaction test suite to cover the new branch.
  • ui/src/ui/views/sessions.ts – renders a [MM/DD HH:mm] preview line below each session key using a new formatSessionTimestamp helper.
  • ui/src/ui/types.ts – adds optional lastMessagePreview to GatewaySessionRow.
  • ui/src/ui/controllers/sessions.ts – passes includeLastMessage: true to loadSessions.
  • package.json – pins punycode to the userland punycode.js@^2.3.1 override.

Notable concern
The 0/0 special case conflates "never flushed" with "already flushed at cycle 0 without a compaction". In agent-runner-memory.ts, memoryFlushCompactionCount is written as currentCompactionCount (defaulting to 0) after any flush that does not trigger a compaction. With the new code that path also returns false, meaning the guard is effectively disabled for sessions whose compactionCount never advances past 0. This reintroduces the repeated-flush problem the guard was designed to prevent. A sentinel value (e.g. -1) would cleanly distinguish "flushed at cycle 0 without compaction" from "never flushed".

Confidence Score: 2/5

  • The UI additions are safe, but the core logic fix can silently bypass the repeated-flush guard for sessions that never compact, potentially causing unbounded flush attempts.
  • The 0/0 early-return in hasAlreadyFlushedForCurrentCompaction is logically unsound: it treats both the "initial state" and the "post-flush at cycle 0 without compaction" as equivalent and returns false for both. Because the flush machinery in agent-runner-memory.ts always writes memoryFlushCompactionCount = currentCompactionCount (which is 0 when no compaction has occurred), any session with a stable compactionCount: 0 will have its flush guard permanently disabled by this change, not just on the first run.
  • Pay close attention to src/auto-reply/reply/memory-flush.ts lines 231-236 and the interaction with src/auto-reply/reply/agent-runner-memory.ts lines 523-545.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/auto-reply/reply/memory-flush.ts
Line: 231-236

Comment:
**Fix breaks flush guard for cycle-0 flushes without compaction**

The special-case `false` for the `0/0` pair conflates two genuinely different states:

1. **Initial state**`compactionCount: 0`, `memoryFlushCompactionCount: undefined` (a flush has never occurred; correctly returns `false` via the earlier `typeof` check).
2. **Post-flush at cycle 0**`compactionCount: 0`, `memoryFlushCompactionCount: 0` (a flush already ran but no compaction occurred, so `incrementCompactionCount` was never called and both counters stayed at 0).

In `agent-runner-memory.ts` (lines 523–526), `memoryFlushCompactionCount` is initialised to the _current_ `compactionCount` (defaulting to `0`) and only changes when `memoryCompactionCompleted = true`. If a flush completes without emitting a compaction-end event, it writes `memoryFlushCompactionCount: 0` back to storage. The new code then returns `false` on every subsequent call, meaning the flush guard is permanently bypassed for any session where `compactionCount` remains `0`. Every time the token threshold is hit the flush will run again, defeating the idempotency guarantee the function was designed to provide.

The existing `undefined`-check already handles the "never flushed" path cleanly. A safer fix would be to use a sentinel (e.g. `-1`) to represent "flushed at cycle 0, no compaction yet" vs. leaving `memoryFlushCompactionCount` `undefined` until the first real flush, rather than overriding the equality check at `0/0`:

```ts
// In agent-runner-memory.ts, store -1 to mean
// "flushed before first compaction"
let memoryFlushCompactionCount =
  (activeSessionEntry?.compactionCount ?? 0) === 0 && !memoryCompactionCompleted
    ? -1
    : activeSessionEntry?.compactionCount ?? 0;
```

And then in `hasAlreadyFlushedForCurrentCompaction`:
```ts
// -1 sentinel = flushed at pre-compaction cycle; treat as "already flushed" only while compactionCount is still 0
if (lastFlushAt === -1) {
  return compactionCount === 0;
}
return lastFlushAt === compactionCount;
```

This makes the distinction explicit and restores the idempotency guard for the cycle-0 case.

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

Last reviewed commit: eea7873

Comment on lines +231 to +236
// If both are 0, this is the initial state (never flushed for real).
// This handles the case where memoryFlushCompactionCount was set to 0
// during the first flush but compactionCount hasn't changed yet.
if (lastFlushAt === 0 && 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.

Fix breaks flush guard for cycle-0 flushes without compaction

The special-case false for the 0/0 pair conflates two genuinely different states:

  1. Initial statecompactionCount: 0, memoryFlushCompactionCount: undefined (a flush has never occurred; correctly returns false via the earlier typeof check).
  2. Post-flush at cycle 0compactionCount: 0, memoryFlushCompactionCount: 0 (a flush already ran but no compaction occurred, so incrementCompactionCount was never called and both counters stayed at 0).

In agent-runner-memory.ts (lines 523–526), memoryFlushCompactionCount is initialised to the current compactionCount (defaulting to 0) and only changes when memoryCompactionCompleted = true. If a flush completes without emitting a compaction-end event, it writes memoryFlushCompactionCount: 0 back to storage. The new code then returns false on every subsequent call, meaning the flush guard is permanently bypassed for any session where compactionCount remains 0. Every time the token threshold is hit the flush will run again, defeating the idempotency guarantee the function was designed to provide.

The existing undefined-check already handles the "never flushed" path cleanly. A safer fix would be to use a sentinel (e.g. -1) to represent "flushed at cycle 0, no compaction yet" vs. leaving memoryFlushCompactionCount undefined until the first real flush, rather than overriding the equality check at 0/0:

// In agent-runner-memory.ts, store -1 to mean
// "flushed before first compaction"
let memoryFlushCompactionCount =
  (activeSessionEntry?.compactionCount ?? 0) === 0 && !memoryCompactionCompleted
    ? -1
    : activeSessionEntry?.compactionCount ?? 0;

And then in hasAlreadyFlushedForCurrentCompaction:

// -1 sentinel = flushed at pre-compaction cycle; treat as "already flushed" only while compactionCount is still 0
if (lastFlushAt === -1) {
  return compactionCount === 0;
}
return lastFlushAt === compactionCount;

This makes the distinction explicit and restores the idempotency guard for the cycle-0 case.

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

Comment:
**Fix breaks flush guard for cycle-0 flushes without compaction**

The special-case `false` for the `0/0` pair conflates two genuinely different states:

1. **Initial state**`compactionCount: 0`, `memoryFlushCompactionCount: undefined` (a flush has never occurred; correctly returns `false` via the earlier `typeof` check).
2. **Post-flush at cycle 0**`compactionCount: 0`, `memoryFlushCompactionCount: 0` (a flush already ran but no compaction occurred, so `incrementCompactionCount` was never called and both counters stayed at 0).

In `agent-runner-memory.ts` (lines 523–526), `memoryFlushCompactionCount` is initialised to the _current_ `compactionCount` (defaulting to `0`) and only changes when `memoryCompactionCompleted = true`. If a flush completes without emitting a compaction-end event, it writes `memoryFlushCompactionCount: 0` back to storage. The new code then returns `false` on every subsequent call, meaning the flush guard is permanently bypassed for any session where `compactionCount` remains `0`. Every time the token threshold is hit the flush will run again, defeating the idempotency guarantee the function was designed to provide.

The existing `undefined`-check already handles the "never flushed" path cleanly. A safer fix would be to use a sentinel (e.g. `-1`) to represent "flushed at cycle 0, no compaction yet" vs. leaving `memoryFlushCompactionCount` `undefined` until the first real flush, rather than overriding the equality check at `0/0`:

```ts
// In agent-runner-memory.ts, store -1 to mean
// "flushed before first compaction"
let memoryFlushCompactionCount =
  (activeSessionEntry?.compactionCount ?? 0) === 0 && !memoryCompactionCompleted
    ? -1
    : activeSessionEntry?.compactionCount ?? 0;
```

And then in `hasAlreadyFlushedForCurrentCompaction`:
```ts
// -1 sentinel = flushed at pre-compaction cycle; treat as "already flushed" only while compactionCount is still 0
if (lastFlushAt === -1) {
  return compactionCount === 0;
}
return lastFlushAt === compactionCount;
```

This makes the distinction explicit and restores the idempotency guard for the cycle-0 case.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app: web-ui App: web-ui r: too-many-prs Auto-close: author has more than twenty active PRs. size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

memoryFlush never triggers when compactionCount and memoryFlushCompactionCount are both 0

1 participant