mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-18 12:14:32 +00:00
fix(memory-core): unblock dreaming-only promotion
This commit is contained in:
@@ -30,6 +30,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Plugins/memory: restore cached memory capability public artifacts on plugin-registry cache hits so memory-backed artifact surfaces stay visible after warm loads. Thanks @sercada and @vincentkoc.
|
||||
- Gateway/cron: preserve requested isolated-agent config across runtime reloads so subagent jobs and heartbeat overrides keep the right workspace and heartbeat settings when the hot-loaded snapshot is stale. Thanks @l0cka and @vincentkoc.
|
||||
- Gateway/plugins: always send a non-empty `idempotencyKey` for plugin subagent runs, so dreaming narrative jobs stop failing gateway schema validation. (#65354) Thanks @CodeForgeNet and @vincentkoc.
|
||||
- Dreaming/promotion: raise phase reinforcement enough for repeated dreaming-only revisits to clear the default durable-memory gate after multiple days, instead of stalling just below the score threshold. Thanks @vincentkoc.
|
||||
- CLI/plugins: honor `memory-wiki` when `plugins.allow` is set for `openclaw wiki`, and register `wiki` as the plugin-owned command alias so doctor/config stop treating it as stale. (#64779) Thanks @feiskyer and @vincentkoc.
|
||||
- Cron/isolated sessions: persist the right transcript path for each isolated run, including fresh session rollovers, so cron runs stop appending to stale session files. Thanks @samrusani and @vincentkoc.
|
||||
- CLI/memory-wiki: pass the active app config into the metadata registrar so built `openclaw wiki` commands resolve the live wiki plugin config instead of silently falling back to defaults. (#65012) Thanks @leonardsellem and @vincentkoc.
|
||||
|
||||
@@ -41,7 +41,11 @@ const LIGHT_DREAMING_TEST_CONFIG: OpenClawConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
function createHarness(config: OpenClawConfig, workspaceDir?: string) {
|
||||
function createHarness(
|
||||
config: OpenClawConfig,
|
||||
workspaceDir?: string,
|
||||
subagent?: Parameters<typeof __testing.runPhaseIfTriggered>[0]["subagent"],
|
||||
) {
|
||||
const logger = {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
@@ -82,6 +86,7 @@ function createHarness(config: OpenClawConfig, workspaceDir?: string) {
|
||||
workspaceDir: ctx.workspaceDir,
|
||||
cfg: resolvedConfig,
|
||||
logger,
|
||||
subagent,
|
||||
phase: "light",
|
||||
eventText: __testing.constants.LIGHT_SLEEP_EVENT_TEXT,
|
||||
config: light,
|
||||
@@ -96,6 +101,7 @@ function createHarness(config: OpenClawConfig, workspaceDir?: string) {
|
||||
workspaceDir: ctx.workspaceDir,
|
||||
cfg: resolvedConfig,
|
||||
logger,
|
||||
subagent,
|
||||
phase: "rem",
|
||||
eventText: __testing.constants.REM_SLEEP_EVENT_TEXT,
|
||||
config: rem,
|
||||
@@ -104,6 +110,23 @@ function createHarness(config: OpenClawConfig, workspaceDir?: string) {
|
||||
return { beforeAgentReply, logger };
|
||||
}
|
||||
|
||||
function createMockNarrativeSubagent(response = "The archive hummed softly.") {
|
||||
const run = vi.fn(async (_params: { sessionKey: string; message: string }) => ({
|
||||
runId: "dream-run-1",
|
||||
}));
|
||||
const waitForRun = vi.fn(async () => ({ status: "ok" }));
|
||||
const getSessionMessages = vi.fn(async () => ({
|
||||
messages: [{ role: "assistant", content: response }],
|
||||
}));
|
||||
const deleteSession = vi.fn(async () => {});
|
||||
return {
|
||||
run,
|
||||
waitForRun,
|
||||
getSessionMessages,
|
||||
deleteSession,
|
||||
};
|
||||
}
|
||||
|
||||
function setDreamingTestTime(offsetMinutes = 0) {
|
||||
vi.setSystemTime(new Date(DREAMING_TEST_BASE_TIME.getTime() + offsetMinutes * 60_000));
|
||||
}
|
||||
@@ -1448,4 +1471,83 @@ describe("memory-core dreaming phases", () => {
|
||||
remHits: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("passes staged light-dreaming snippets into the narrative pipeline", async () => {
|
||||
const workspaceDir = await createDreamingWorkspace();
|
||||
const subagent = createMockNarrativeSubagent("The backup plan glowed like cold storage.");
|
||||
const { beforeAgentReply } = createHarness(LIGHT_DREAMING_TEST_CONFIG, workspaceDir, subagent);
|
||||
|
||||
await withDreamingTestClock(async () => {
|
||||
await writeDailyNote(workspaceDir, [
|
||||
`# ${DREAMING_TEST_DAY}`,
|
||||
"",
|
||||
"- Move backups to S3 Glacier.",
|
||||
"- Keep retention at 365 days.",
|
||||
]);
|
||||
|
||||
await triggerLightDreaming(beforeAgentReply, workspaceDir, 5);
|
||||
});
|
||||
|
||||
expect(subagent.run).toHaveBeenCalledTimes(1);
|
||||
const firstRun = subagent.run.mock.calls[0]?.[0];
|
||||
expect(firstRun?.message).toContain("Move backups to S3 Glacier.");
|
||||
expect(firstRun?.message).toContain("Keep retention at 365 days.");
|
||||
await expect(fs.readFile(path.join(workspaceDir, "DREAMS.md"), "utf-8")).resolves.toContain(
|
||||
"The backup plan glowed like cold storage.",
|
||||
);
|
||||
});
|
||||
|
||||
it("passes rem-dreaming snippets into the narrative pipeline", async () => {
|
||||
const workspaceDir = await createDreamingWorkspace();
|
||||
const subagent = createMockNarrativeSubagent("The traces braided themselves into a map.");
|
||||
const { beforeAgentReply } = createHarness(
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
"memory-core": {
|
||||
config: {
|
||||
dreaming: {
|
||||
enabled: true,
|
||||
phases: {
|
||||
rem: {
|
||||
enabled: true,
|
||||
limit: 10,
|
||||
lookbackDays: 7,
|
||||
minPatternStrength: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
workspaceDir,
|
||||
subagent,
|
||||
);
|
||||
|
||||
await withDreamingTestClock(async () => {
|
||||
await writeDailyNote(workspaceDir, [
|
||||
`# ${DREAMING_TEST_DAY}`,
|
||||
"",
|
||||
"- Move backups to S3 Glacier.",
|
||||
"- Keep retention at 365 days.",
|
||||
"- Rotate access keys after the audit.",
|
||||
]);
|
||||
|
||||
setDreamingTestTime(5);
|
||||
await beforeAgentReply(
|
||||
{ cleanedBody: "__openclaw_memory_core_rem_sleep__" },
|
||||
{ trigger: "heartbeat", workspaceDir },
|
||||
);
|
||||
});
|
||||
|
||||
expect(subagent.run).toHaveBeenCalledTimes(1);
|
||||
const firstRun = subagent.run.mock.calls[0]?.[0];
|
||||
expect(firstRun?.message).toContain("Move backups to S3 Glacier.");
|
||||
expect(firstRun?.message).toContain("Keep retention at 365 days.");
|
||||
await expect(fs.readFile(path.join(workspaceDir, "DREAMS.md"), "utf-8")).resolves.toContain(
|
||||
"The traces braided themselves into a map.",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -179,6 +179,80 @@ describe("short-term promotion", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("lets repeated dreaming-only daily signals clear the default promotion gates", async () => {
|
||||
await withTempWorkspace(async (workspaceDir) => {
|
||||
const queryDays = ["2026-04-01", "2026-04-02", "2026-04-03"];
|
||||
let candidateKey = "";
|
||||
|
||||
for (const [index, day] of queryDays.entries()) {
|
||||
const nowMs = Date.parse(`${day}T10:00:00.000Z`);
|
||||
await recordShortTermRecalls({
|
||||
workspaceDir,
|
||||
query: `__dreaming_daily__:${day}`,
|
||||
signalType: "daily",
|
||||
dedupeByQueryPerDay: true,
|
||||
dayBucket: day,
|
||||
nowMs,
|
||||
results: [
|
||||
{
|
||||
path: "memory/2026-04-01.md",
|
||||
startLine: 1,
|
||||
endLine: 2,
|
||||
score: 0.62,
|
||||
snippet: "Move backups to S3 Glacier.",
|
||||
source: "memory",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const ranked = await rankShortTermPromotionCandidates({
|
||||
workspaceDir,
|
||||
minScore: 0,
|
||||
minRecallCount: 0,
|
||||
minUniqueQueries: 0,
|
||||
nowMs,
|
||||
});
|
||||
candidateKey = ranked[0]?.key ?? candidateKey;
|
||||
expect(candidateKey).toBeTruthy();
|
||||
|
||||
await recordDreamingPhaseSignals({
|
||||
workspaceDir,
|
||||
phase: "light",
|
||||
keys: [candidateKey],
|
||||
nowMs,
|
||||
});
|
||||
await recordDreamingPhaseSignals({
|
||||
workspaceDir,
|
||||
phase: "rem",
|
||||
keys: [candidateKey],
|
||||
nowMs: nowMs + 60_000,
|
||||
});
|
||||
|
||||
if (index < 2) {
|
||||
const beforeThreshold = await rankShortTermPromotionCandidates({
|
||||
workspaceDir,
|
||||
nowMs,
|
||||
});
|
||||
expect(beforeThreshold).toHaveLength(0);
|
||||
}
|
||||
}
|
||||
|
||||
const ranked = await rankShortTermPromotionCandidates({
|
||||
workspaceDir,
|
||||
nowMs: Date.parse("2026-04-03T10:01:00.000Z"),
|
||||
});
|
||||
|
||||
expect(ranked).toHaveLength(1);
|
||||
expect(ranked[0]).toMatchObject({
|
||||
recallCount: 0,
|
||||
dailyCount: 3,
|
||||
uniqueQueries: 3,
|
||||
});
|
||||
expect(ranked[0]?.recallDays).toEqual(queryDays);
|
||||
expect(ranked[0]?.score).toBeGreaterThanOrEqual(0.75);
|
||||
});
|
||||
});
|
||||
|
||||
it("lets grounded durable evidence satisfy default deep thresholds", async () => {
|
||||
await withTempWorkspace(async (workspaceDir) => {
|
||||
await writeDailyMemoryNote(workspaceDir, "2026-04-03", [
|
||||
|
||||
@@ -31,8 +31,10 @@ const SHORT_TERM_LOCK_RELATIVE_PATH = path.join("memory", ".dreams", "short-term
|
||||
const SHORT_TERM_LOCK_WAIT_TIMEOUT_MS = 10_000;
|
||||
const SHORT_TERM_LOCK_STALE_MS = 60_000;
|
||||
const SHORT_TERM_LOCK_RETRY_DELAY_MS = 40;
|
||||
const PHASE_SIGNAL_LIGHT_BOOST_MAX = 0.05;
|
||||
const PHASE_SIGNAL_REM_BOOST_MAX = 0.08;
|
||||
// Repeated dreaming revisits should be able to clear the default promotion gate
|
||||
// without requiring separate organic recall traffic for the same snippet.
|
||||
const PHASE_SIGNAL_LIGHT_BOOST_MAX = 0.06;
|
||||
const PHASE_SIGNAL_REM_BOOST_MAX = 0.09;
|
||||
const PHASE_SIGNAL_HALF_LIFE_DAYS = 14;
|
||||
const inProcessShortTermLocks = new Map<string, Promise<void>>();
|
||||
const ensuredShortTermDirs = new Map<string, Promise<void>>();
|
||||
|
||||
Reference in New Issue
Block a user