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
- 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."
}
}
}
}
}
-
Have a long conversation that triggers auto-compaction multiple times
-
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)
Summary
memoryFlushonly fires on every other auto-compaction cycle. The dedup logic inshouldRunMemoryFlush(memory-flush.ts:100) comparesmemoryFlushCompactionCount === compactionCountto prevent double-flushing, butrunMemoryFlushIfNeeded(agent-runner-memory.ts:165-187) unconditionally setsmemoryFlushCompactionCountto the currentcompactionCountafter every flush — even whenincrementCompactionCountbumps 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,/resetall skip it), and even that path only works 50% of the time.Related: #12162, #8185, #6535, #5429
Steps to reproduce
memoryFlushinopenclaw.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." } } } } }Have a long conversation that triggers auto-compaction multiple times
Observe that the flush turn (with the configured prompt) only runs on alternating compaction cycles
Expected behavior
memoryFlushshould 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
compactionCountandmemoryFlushCompactionCountare set to the same incremented value. On the next compaction cycle,shouldRunMemoryFlushsees they match and returnsfalse, skipping the flush. A regular compaction then incrementscompactionCountalone, 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.tslines 165-187:Then in
memory-flush.tsline 100, the gate:Suggested fix
Only persist
memoryFlushCompactionCountwhen compaction actually completed during the flush turn. Change lines 165-187 to something like:Or simpler: use
compactionCount - 1semantics so the check is "has a flush run since the last compaction" rather than exact counter matching.Environment
Logs or screenshots
Gateway log showing alternating flush/skip pattern across 4 compaction cycles:
Cycle 1 — flush runs (Feb 8, 23:33 UTC):
Cycle 2 — flush skipped (Feb 8, 23:52 UTC):
Cycle 3 — flush skipped again (Feb 9, 07:57 UTC):
Cycle 4 — flush runs (Feb 9, ~08:30 UTC):
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)