Skip to content

[Bug]: memoryFlush does not fire reliably #12590

@dial481

Description

@dial481

Summary

memoryFlush only fires on every other auto-compaction cycle. The dedup logic in shouldRunMemoryFlush (memory-flush.ts:100) compares memoryFlushCompactionCount === compactionCount to prevent double-flushing, but runMemoryFlushIfNeeded (agent-runner-memory.ts:165-187) unconditionally sets memoryFlushCompactionCount to the current compactionCount after every flush — even when incrementCompactionCount bumps the count in the same turn. This synchronizes both counters, causing the next cycle's flush to be skipped.

This compounds the issue described in #12162 — auto-compaction is the only path that runs memoryFlush (manual /compact, /new, /reset all skip it), and even that path only works 50% of the time.

Related: #12162, #8185, #6535, #5429

Steps to reproduce

  1. Configure memoryFlush in openclaw.json:
{
  "agents": {
    "defaults": {
      "compaction": {
        "memoryFlush": {
          "enabled": true,
          "prompt": "Save a summary to memory. If nothing to store, reply with NO_REPLY.",
          "systemPrompt": "Pre-compaction memory flush. Summarize the session. If nothing to store, reply with NO_REPLY."
        }
      }
    }
  }
}
  1. Have a long conversation that triggers auto-compaction multiple times

  2. Observe that the flush turn (with the configured prompt) only runs on alternating compaction cycles

Expected behavior

memoryFlush should fire before every auto-compaction that meets the token threshold, not every other one.

Actual behavior

After a flush turn where compaction also completes, both compactionCount and memoryFlushCompactionCount are set to the same incremented value. On the next compaction cycle, shouldRunMemoryFlush sees they match and returns false, skipping the flush. A regular compaction then increments compactionCount alone, desyncing the counters, so the flush runs again on the cycle after that.

Pattern observed: flush, skip, flush, skip, flush, skip...

Root cause

In agent-runner-memory.ts lines 165-187:

// Line 165: Initialize to CURRENT compactionCount
let memoryFlushCompactionCount =
  activeSessionEntry?.compactionCount ?? 0;

// Line 169: Only increment if compaction completed during flush turn
if (memoryCompactionCompleted) {
  const nextCount = await incrementCompactionCount({...});
  memoryFlushCompactionCount = nextCount;  // e.g. 5 → 6
}

// Line 187: ALWAYS persist — locks counters together
await updateSessionStoreEntry({
  update: async () => ({
    memoryFlushAt: Date.now(),
    memoryFlushCompactionCount,  // writes 6
  }),
});

Then in memory-flush.ts line 100, the gate:

const compactionCount = params.entry?.compactionCount ?? 0;  // also 6
const lastFlushAt = params.entry?.memoryFlushCompactionCount; // 6
if (typeof lastFlushAt === "number" && lastFlushAt === compactionCount) {
  return false;  // 6 === 6 → skip flush
}

Suggested fix

Only persist memoryFlushCompactionCount when compaction actually completed during the flush turn. Change lines 165-187 to something like:

if (memoryCompactionCompleted) {
  const nextCount = await incrementCompactionCount({...});
  await updateSessionStoreEntry({
    update: async () => ({
      memoryFlushAt: Date.now(),
      memoryFlushCompactionCount: nextCount,
    }),
  });
} else {
  await updateSessionStoreEntry({
    update: async () => ({
      memoryFlushAt: Date.now(),
      // Don't update memoryFlushCompactionCount — let it stay desynced
    }),
  });
}

Or simpler: use compactionCount - 1 semantics so the check is "has a flush run since the last compaction" rather than exact counter matching.

Environment

  • OpenClaw version: 2026.02.6
  • OS: Linux (Debian 12)
  • Install method: npm

Logs or screenshots

Gateway log showing alternating flush/skip pattern across 4 compaction cycles:

Cycle 1 — flush runs (Feb 8, 23:33 UTC):

[hooks] running before_agent_start (1 handlers, sequential)
hooks: prepended context to prompt (8305 chars)
embedded run compaction start: runId=22060963
embedded run agent start: runId=22060963
embedded run tool start: tool=memory_store   ← flush fired, saved to memory
embedded run tool end: tool=memory_store
embedded run agent end: runId=22060963

Cycle 2 — flush skipped (Feb 8, 23:52 UTC):

[hooks] running before_agent_start (1 handlers, sequential)
hooks: prepended context to prompt (8310 chars)
embedded run compaction start: runId=c585f14a   ← compaction runs, NO flush turn
embedded run done: runId=c585f14a

Cycle 3 — flush skipped again (Feb 9, 07:57 UTC):

[hooks] running before_agent_start (1 handlers, sequential)
hooks: prepended context to prompt (8312 chars)
embedded run agent start: runId=55f472e1
embedded run agent end: runId=55f472e1
embedded run compaction start: runId=55f472e1   ← compaction runs, NO flush turn

Cycle 4 — flush runs (Feb 9, ~08:30 UTC):

Flush fired, memory_store called             ← confirmed by user observation

Pattern: flush, skip, skip, flush — consistent with counter synchronization bug.

Source files audited:

  • src/auto-reply/reply/agent-runner-memory.ts (lines 165-187)
  • src/auto-reply/reply/memory-flush.ts (lines 77-105, shouldRunMemoryFlush)
  • src/auto-reply/reply/session-updates.ts (lines 225-275, incrementCompactionCount)

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions