fix(openclaw): dream verification, failed-turn cleanup, atomic lock
1. Write-only verification: only count memory_store, memory_update, memory_forget, memory_delete_all as proof of dream execution. Removed memory_list (read-only, orient-only pass should not count). Scan only the last assistant message (this turn), not the full session snapshot, to avoid matching earlier turns' tool calls. 2. Failed-turn cleanup: agent_end now handles dreamSessionId cleanup BEFORE the !event.success early return. A failed/aborted turn after lock acquisition releases the lock immediately instead of waiting for the 1-hour stale timeout. 3. Atomic lock: replaced read-then-write with exclusive create flag (wx). Two racing processes: only one succeeds, other gets EEXIST. Stale locks are unlinked before the exclusive create attempt. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
67afcd90d9
commit
7e9824e394
+11
-6
@@ -150,18 +150,23 @@ export function acquireDreamLock(stateDir: string): boolean {
|
|||||||
const lock = JSON.parse(raw) as DreamLock;
|
const lock = JSON.parse(raw) as DreamLock;
|
||||||
const age = Date.now() - lock.startedAt;
|
const age = Date.now() - lock.startedAt;
|
||||||
if (age < LOCK_STALE_MS) {
|
if (age < LOCK_STALE_MS) {
|
||||||
// Lock is held and not stale
|
return false; // Held and not stale
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
// Stale lock, reclaim
|
// Stale lock — remove it before attempting exclusive create
|
||||||
|
try { fs.unlinkSync(lp); } catch { /* race ok */ }
|
||||||
} catch {
|
} catch {
|
||||||
// No lock file, proceed
|
// No lock file, proceed
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write lock
|
// Atomic create with exclusive flag (wx). If two processes race,
|
||||||
|
// only one succeeds. The other gets EEXIST.
|
||||||
const lock: DreamLock = { pid: process.pid, startedAt: Date.now() };
|
const lock: DreamLock = { pid: process.pid, startedAt: Date.now() };
|
||||||
fs.writeFileSync(lp, JSON.stringify(lock));
|
try {
|
||||||
return true;
|
fs.writeFileSync(lp, JSON.stringify(lock), { flag: "wx" });
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false; // Lost race
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+31
-19
@@ -1348,40 +1348,52 @@ function registerHooks(
|
|||||||
let dreamSessionId: string | undefined;
|
let dreamSessionId: string | undefined;
|
||||||
|
|
||||||
api.on("agent_end", async (event: any, ctx: any) => {
|
api.on("agent_end", async (event: any, ctx: any) => {
|
||||||
if (!event.success) return;
|
|
||||||
const sessionId = ctx?.sessionKey ?? undefined;
|
const sessionId = ctx?.sessionKey ?? undefined;
|
||||||
const trigger = ctx?.trigger ?? undefined;
|
const trigger = ctx?.trigger ?? undefined;
|
||||||
if (sessionId) session.setCurrentSessionId(sessionId);
|
if (sessionId) session.setCurrentSessionId(sessionId);
|
||||||
|
|
||||||
// If dream was triggered for THIS session, check if it actually ran
|
// If dream was triggered for THIS session, handle cleanup regardless
|
||||||
|
// of success/failure. A failed turn must still release the lock.
|
||||||
const stateDir = session.getStateDir();
|
const stateDir = session.getStateDir();
|
||||||
if (dreamSessionId && dreamSessionId === sessionId && stateDir) {
|
if (dreamSessionId && dreamSessionId === sessionId && stateDir) {
|
||||||
// Verify the model actually called memory tools (not false completion).
|
dreamSessionId = undefined;
|
||||||
// Check event.messages for memory_store, memory_update, memory_forget tool calls.
|
|
||||||
const messages = event.messages ?? [];
|
|
||||||
const dreamToolsUsed = messages.some((m: any) => {
|
|
||||||
if (m.role !== "assistant") return false;
|
|
||||||
const content = Array.isArray(m.content) ? m.content : [];
|
|
||||||
return content.some((block: any) =>
|
|
||||||
block.type === "tool_use" &&
|
|
||||||
["memory_store", "memory_update", "memory_forget", "memory_delete_all", "memory_list"].includes(block.name)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (dreamToolsUsed) {
|
if (!event.success) {
|
||||||
|
// Turn failed/aborted after lock acquired. Release lock, do not
|
||||||
|
// record completion. Gates will re-trigger next eligible turn.
|
||||||
|
releaseDreamLock(stateDir);
|
||||||
|
api.logger.warn("openclaw-mem0: auto-dream turn failed, lock released, will retry");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the model actually performed WRITE operations (not just reads).
|
||||||
|
// Only count memory_store, memory_update, memory_forget, memory_delete_all.
|
||||||
|
// Exclude memory_list and memory_search (read-only, orient-only pass).
|
||||||
|
// Scan only the LAST assistant message (this turn), not the full session
|
||||||
|
// snapshot, to avoid matching earlier tool calls from prior turns.
|
||||||
|
const WRITE_TOOLS = new Set(["memory_store", "memory_update", "memory_forget", "memory_delete_all"]);
|
||||||
|
const messages = event.messages ?? [];
|
||||||
|
// Find the last assistant message (this turn's output)
|
||||||
|
const lastAssistant = [...messages].reverse().find((m: any) => m.role === "assistant");
|
||||||
|
const writeToolUsed = lastAssistant && Array.isArray(lastAssistant.content)
|
||||||
|
? lastAssistant.content.some((block: any) =>
|
||||||
|
block.type === "tool_use" && WRITE_TOOLS.has(block.name)
|
||||||
|
)
|
||||||
|
: false;
|
||||||
|
|
||||||
|
if (writeToolUsed) {
|
||||||
releaseDreamLock(stateDir);
|
releaseDreamLock(stateDir);
|
||||||
recordDreamCompletion(stateDir);
|
recordDreamCompletion(stateDir);
|
||||||
api.logger.info("openclaw-mem0: auto-dream completed (verified tool usage), lock released");
|
api.logger.info("openclaw-mem0: auto-dream completed (verified write tool usage), lock released");
|
||||||
} else {
|
} else {
|
||||||
// Model did not run dream tools. Release lock but do NOT record completion.
|
|
||||||
// Gates will re-trigger on the next eligible turn.
|
|
||||||
releaseDreamLock(stateDir);
|
releaseDreamLock(stateDir);
|
||||||
api.logger.warn("openclaw-mem0: auto-dream injected but model did not execute consolidation. Lock released, will retry.");
|
api.logger.warn("openclaw-mem0: auto-dream injected but no write tools executed. Lock released, will retry.");
|
||||||
}
|
}
|
||||||
dreamSessionId = undefined;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!event.success) return;
|
||||||
|
|
||||||
// Track session for dream gating (interactive turns only)
|
// Track session for dream gating (interactive turns only)
|
||||||
if (stateDir && sessionId && !isNonInteractiveTrigger(trigger, sessionId)) {
|
if (stateDir && sessionId && !isNonInteractiveTrigger(trigger, sessionId)) {
|
||||||
incrementSessionCount(stateDir, sessionId);
|
incrementSessionCount(stateDir, sessionId);
|
||||||
|
|||||||
Reference in New Issue
Block a user