fix(openclaw): dream gate correctness — cheap-first, session-keyed, verified

Three fixes to the auto-dream gate mechanism:

1. Cheap gates first: checkCheapGates() (local file reads for time +
   session count) runs BEFORE any API call. provider.getAll() only
   executes if both cheap gates pass. Previously every turn downloaded
   the entire memory set just to compute memCount.

2. Session-keyed dream tracking: dreamSessionId replaces the
   process-global dreamJustRan boolean. Only the session that triggered
   dream can complete it. Prevents cross-session false completion.

3. Verified completion: agent_end checks event.messages for actual
   memory tool calls (memory_store, memory_update, memory_forget,
   memory_list). If the model ignored the dream prompt and just
   answered the user, the lock releases but completion is NOT recorded.
   Gates will re-trigger on the next eligible turn.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
chaithanyak42
2026-04-02 12:51:22 +05:30
co-authored by Claude Opus 4.6
parent 30f242dc4c
commit 67afcd90d9
2 changed files with 75 additions and 43 deletions
+27 -22
View File
@@ -92,38 +92,43 @@ export function incrementSessionCount(stateDir: string, sessionId: string): void
// ============================================================================
/**
* Check all three gates. Returns true only if ALL pass.
* Gates are checked cheapest-first (time, session, memory).
* Check cheap gates (time + sessions). These are local file reads only.
* Call this BEFORE any API calls. If this fails, skip the expensive
* memory count check entirely.
*/
export async function shouldDream(
export function checkCheapGates(
stateDir: string,
config: { minHours?: number; minSessions?: number; minMemories?: number },
memoryCount: number,
): Promise<{ pass: boolean; reason?: string }> {
const cfg: DreamGateConfig = {
minHours: config.minHours ?? DEFAULTS.minHours,
minSessions: config.minSessions ?? DEFAULTS.minSessions,
minMemories: config.minMemories ?? DEFAULTS.minMemories,
};
config: { minHours?: number; minSessions?: number },
): { proceed: boolean; reason?: string } {
const minHours = config.minHours ?? DEFAULTS.minHours;
const minSessions = config.minSessions ?? DEFAULTS.minSessions;
const state = readState(stateDir);
// Gate 1: Time
// Gate 1: Time (one local file read)
const hoursSince = (Date.now() - state.lastConsolidatedAt) / 3_600_000;
if (hoursSince < cfg.minHours) {
return { pass: false, reason: `time: ${hoursSince.toFixed(1)}h < ${cfg.minHours}h` };
if (hoursSince < minHours) {
return { proceed: false, reason: `time: ${hoursSince.toFixed(1)}h < ${minHours}h` };
}
// Gate 2: Sessions
if (state.sessionsSince < cfg.minSessions) {
return { pass: false, reason: `sessions: ${state.sessionsSince} < ${cfg.minSessions}` };
// Gate 2: Sessions (same file, already read)
if (state.sessionsSince < minSessions) {
return { proceed: false, reason: `sessions: ${state.sessionsSince} < ${minSessions}` };
}
// Gate 3: Memory count
if (memoryCount < cfg.minMemories) {
return { pass: false, reason: `memories: ${memoryCount} < ${cfg.minMemories}` };
}
return { proceed: true };
}
/**
* Check expensive memory count gate. Only call AFTER checkCheapGates passes.
*/
export function checkMemoryGate(
memoryCount: number,
config: { minMemories?: number },
): { pass: boolean; reason?: string } {
const minMemories = config.minMemories ?? DEFAULTS.minMemories;
if (memoryCount < minMemories) {
return { pass: false, reason: `memories: ${memoryCount} < ${minMemories}` };
}
return { pass: true };
}