refactor(memory-neo4j): remove in-process auto sleep cycle, use system cron instead

Sleep cycle is now triggered by a system cron job (`0 3 * * *`) calling
`openclaw memory neo4j sleep` rather than an in-process 6-hour interval
timer with mutex. Simpler, more reliable, and easier to manage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Tarun Sukhani
2026-02-16 10:33:00 +08:00
parent 1bc6cdd00c
commit fee43d505d
4 changed files with 4 additions and 82 deletions

View File

@@ -474,14 +474,6 @@ describe("memoryNeo4jConfigSchema.parse", () => {
expect(config.sleepCycle.auto).toBe(true);
});
it("should default sleepCycle.autoIntervalMs to 6 hours (21600000)", () => {
const config = memoryNeo4jConfigSchema.parse({
neo4j: { uri: "bolt://localhost:7687", password: "" },
embedding: { provider: "ollama" },
});
expect(config.sleepCycle.autoIntervalMs).toBe(21600000);
});
it("should respect explicit sleepCycle.auto = false", () => {
const config = memoryNeo4jConfigSchema.parse({
neo4j: { uri: "bolt://localhost:7687", password: "" },
@@ -491,33 +483,13 @@ describe("memoryNeo4jConfigSchema.parse", () => {
expect(config.sleepCycle.auto).toBe(false);
});
it("should respect explicit sleepCycle.autoIntervalMs", () => {
it("should still accept autoIntervalMs without error (backwards compat)", () => {
const config = memoryNeo4jConfigSchema.parse({
neo4j: { uri: "bolt://localhost:7687", password: "" },
embedding: { provider: "ollama" },
sleepCycle: { autoIntervalMs: 3600000 },
});
expect(config.sleepCycle.autoIntervalMs).toBe(3600000);
});
it("should throw when sleepCycle.autoIntervalMs is not positive", () => {
expect(() =>
memoryNeo4jConfigSchema.parse({
neo4j: { uri: "bolt://localhost:7687", password: "" },
embedding: { provider: "ollama" },
sleepCycle: { autoIntervalMs: 0 },
}),
).toThrow("sleepCycle.autoIntervalMs must be positive");
});
it("should throw when sleepCycle.autoIntervalMs is negative", () => {
expect(() =>
memoryNeo4jConfigSchema.parse({
neo4j: { uri: "bolt://localhost:7687", password: "" },
embedding: { provider: "ollama" },
sleepCycle: { autoIntervalMs: -1000 },
}),
).toThrow("sleepCycle.autoIntervalMs must be positive");
expect(config.sleepCycle.auto).toBe(true);
});
it("should reject unknown sleepCycle keys", () => {

View File

@@ -64,7 +64,6 @@ export type MemoryNeo4jConfig = {
decayCurves: Record<string, { halfLifeDays: number }>;
sleepCycle: {
auto: boolean;
autoIntervalMs: number;
};
};
@@ -359,15 +358,6 @@ export const memoryNeo4jConfigSchema = {
const sleepCycleRaw = cfg.sleepCycle as Record<string, unknown> | undefined;
assertAllowedKeys(sleepCycleRaw ?? {}, ["auto", "autoIntervalMs"], "sleepCycle config");
const sleepCycleAuto = sleepCycleRaw?.auto !== false; // enabled by default
const sleepCycleAutoIntervalMs =
typeof sleepCycleRaw?.autoIntervalMs === "number"
? sleepCycleRaw.autoIntervalMs
: 6 * 60 * 60 * 1000; // 6 hours
if (sleepCycleAutoIntervalMs <= 0) {
throw new Error(
`sleepCycle.autoIntervalMs must be positive, got: ${sleepCycleAutoIntervalMs}`,
);
}
return {
neo4j: {
@@ -401,7 +391,6 @@ export const memoryNeo4jConfigSchema = {
decayCurves,
sleepCycle: {
auto: sleepCycleAuto,
autoIntervalMs: sleepCycleAutoIntervalMs,
},
};
},

View File

@@ -32,7 +32,6 @@ import { isSemanticDuplicate, rateImportance } from "./extractor.js";
import { extractUserMessages, extractAssistantMessages } from "./message-utils.js";
import { Neo4jMemoryClient } from "./neo4j-client.js";
import { hybridSearch } from "./search.js";
import { runSleepCycle } from "./sleep-cycle.js";
// ============================================================================
// Plugin Definition
@@ -386,9 +385,6 @@ const memoryNeo4jPlugin = {
const sessionLastSeen = new Map<string, number>();
let lastTtlSweep = Date.now();
// Auto sleep cycle state
let lastSleepCycleAt = 0;
let sleepCycleRunning = false;
const sleepAbortController = new AbortController();
/** Evict stale entries from session tracking maps older than SESSION_TTL_MS. */
@@ -728,36 +724,6 @@ const memoryNeo4jPlugin = {
extractionConfig,
api.logger,
);
// Auto sleep cycle: fire-and-forget if interval has elapsed
if (
cfg.sleepCycle.auto &&
!sleepCycleRunning &&
Date.now() - lastSleepCycleAt >= cfg.sleepCycle.autoIntervalMs
) {
sleepCycleRunning = true;
void (async () => {
try {
api.logger.info("memory-neo4j: [auto-sleep] starting background sleep cycle");
const t0 = Date.now();
const result = await runSleepCycle(db, embeddings, extractionConfig, api.logger, {
abortSignal: sleepAbortController.signal,
decayCurves: Object.keys(cfg.decayCurves).length > 0 ? cfg.decayCurves : undefined,
});
lastSleepCycleAt = Date.now();
api.logger.info(
`memory-neo4j: [auto-sleep] complete in ${((Date.now() - t0) / 1000).toFixed(1)}s` +
` — dedup=${result.dedup.memoriesMerged}, extracted=${result.extraction.succeeded},` +
` decayed=${result.decay.memoriesPruned}, credentials=${result.credentialScan.credentialsFound}` +
(result.aborted ? " (aborted)" : ""),
);
} catch (err) {
api.logger.warn(`memory-neo4j: [auto-sleep] failed: ${String(err)}`);
} finally {
sleepCycleRunning = false;
}
})();
}
});
}

View File

@@ -82,11 +82,7 @@
},
"sleepCycle.auto": {
"label": "Auto Sleep Cycle",
"help": "Automatically run memory consolidation (dedup, extraction, decay) in the background (default: on)"
},
"sleepCycle.autoIntervalMs": {
"label": "Sleep Cycle Interval (ms)",
"help": "Minimum time between automatic sleep cycles (default: 6 hours = 21600000)"
"help": "Automatically run memory consolidation (dedup, extraction, decay) daily at 3:00 AM local time (default: on)"
}
},
"configSchema": {
@@ -201,8 +197,7 @@
"type": "object",
"additionalProperties": false,
"properties": {
"auto": { "type": "boolean" },
"autoIntervalMs": { "type": "number", "minimum": 60000 }
"auto": { "type": "boolean" }
}
}
},