mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-05 14:50:21 +00:00
feat(memory-core): add dreaming aging controls
This commit is contained in:
@@ -42,6 +42,16 @@
|
||||
"placeholder": "2",
|
||||
"help": "Minimum unique query count required for automatic promotion."
|
||||
},
|
||||
"dreaming.recencyHalfLifeDays": {
|
||||
"label": "Recency Half-Life Days",
|
||||
"placeholder": "14",
|
||||
"help": "Days for the recency score to decay by half during dreaming ranking."
|
||||
},
|
||||
"dreaming.maxAgeDays": {
|
||||
"label": "Promotion Max Age Days",
|
||||
"placeholder": "30",
|
||||
"help": "Optional maximum daily-note age in days for automatic or manual promotion."
|
||||
},
|
||||
"dreaming.verboseLogging": {
|
||||
"label": "Dreaming Verbose Logging",
|
||||
"placeholder": "false",
|
||||
@@ -86,6 +96,14 @@
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"recencyHalfLifeDays": {
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"maxAgeDays": {
|
||||
"type": "number",
|
||||
"exclusiveMinimum": 0
|
||||
},
|
||||
"verboseLogging": {
|
||||
"type": "boolean"
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ function formatDreamingSummary(cfg: OpenClawConfig): string {
|
||||
return "off";
|
||||
}
|
||||
const timezone = dreaming.timezone ? ` (${dreaming.timezone})` : "";
|
||||
return `${dreaming.cron}${timezone} · limit=${dreaming.limit} · minScore=${dreaming.minScore} · minRecallCount=${dreaming.minRecallCount} · minUniqueQueries=${dreaming.minUniqueQueries}`;
|
||||
return `${dreaming.cron}${timezone} · limit=${dreaming.limit} · minScore=${dreaming.minScore} · minRecallCount=${dreaming.minRecallCount} · minUniqueQueries=${dreaming.minUniqueQueries} · recencyHalfLifeDays=${dreaming.recencyHalfLifeDays} · maxAgeDays=${dreaming.maxAgeDays ?? "none"}`;
|
||||
}
|
||||
|
||||
function formatAuditCounts(audit: ShortTermAuditSummary): string {
|
||||
@@ -927,6 +927,10 @@ export async function runMemoryPromote(opts: MemoryPromoteCommandOptions) {
|
||||
run: async (manager) => {
|
||||
const status = manager.status();
|
||||
const workspaceDir = status.workspaceDir?.trim();
|
||||
const dreaming = resolveShortTermPromotionDreamingConfig({
|
||||
pluginConfig: resolveMemoryPluginConfig(cfg),
|
||||
cfg,
|
||||
});
|
||||
if (!workspaceDir) {
|
||||
defaultRuntime.error("Memory promote requires a resolvable workspace directory.");
|
||||
process.exitCode = 1;
|
||||
@@ -938,9 +942,11 @@ export async function runMemoryPromote(opts: MemoryPromoteCommandOptions) {
|
||||
candidates = await rankShortTermPromotionCandidates({
|
||||
workspaceDir,
|
||||
limit: opts.limit,
|
||||
minScore: opts.minScore,
|
||||
minRecallCount: opts.minRecallCount,
|
||||
minUniqueQueries: opts.minUniqueQueries,
|
||||
minScore: opts.minScore ?? dreaming.minScore,
|
||||
minRecallCount: opts.minRecallCount ?? dreaming.minRecallCount,
|
||||
minUniqueQueries: opts.minUniqueQueries ?? dreaming.minUniqueQueries,
|
||||
recencyHalfLifeDays: dreaming.recencyHalfLifeDays,
|
||||
maxAgeDays: dreaming.maxAgeDays,
|
||||
includePromoted: Boolean(opts.includePromoted),
|
||||
});
|
||||
} catch (err) {
|
||||
@@ -952,17 +958,14 @@ export async function runMemoryPromote(opts: MemoryPromoteCommandOptions) {
|
||||
let applyResult: Awaited<ReturnType<typeof applyShortTermPromotions>> | undefined;
|
||||
if (opts.apply) {
|
||||
try {
|
||||
const dreaming = resolveShortTermPromotionDreamingConfig({
|
||||
pluginConfig: resolveMemoryPluginConfig(cfg),
|
||||
cfg,
|
||||
});
|
||||
applyResult = await applyShortTermPromotions({
|
||||
workspaceDir,
|
||||
candidates,
|
||||
limit: opts.limit,
|
||||
minScore: opts.minScore,
|
||||
minRecallCount: opts.minRecallCount,
|
||||
minUniqueQueries: opts.minUniqueQueries,
|
||||
minScore: opts.minScore ?? dreaming.minScore,
|
||||
minRecallCount: opts.minRecallCount ?? dreaming.minRecallCount,
|
||||
minUniqueQueries: opts.minUniqueQueries ?? dreaming.minUniqueQueries,
|
||||
maxAgeDays: dreaming.maxAgeDays,
|
||||
timezone: dreaming.timezone,
|
||||
});
|
||||
} catch (err) {
|
||||
|
||||
@@ -120,6 +120,8 @@ describe("memory-core /dreaming command", () => {
|
||||
dreaming: {
|
||||
mode: "deep",
|
||||
timezone: "America/Los_Angeles",
|
||||
recencyHalfLifeDays: 21,
|
||||
maxAgeDays: 45,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -132,6 +134,7 @@ describe("memory-core /dreaming command", () => {
|
||||
expect(result.text).toContain("Dreaming status:");
|
||||
expect(result.text).toContain("- mode: deep");
|
||||
expect(result.text).toContain("- cadence: 0 */12 * * * (America/Los_Angeles)");
|
||||
expect(result.text).toContain("- aging: recencyHalfLifeDays=21, maxAgeDays=45");
|
||||
expect(result.text).toContain("- verboseLogging: off");
|
||||
expect(runtime.config.writeConfigFile).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -83,7 +83,8 @@ function formatModeGuideLine(mode: DreamingMode): string {
|
||||
return (
|
||||
`- ${mode}: cadence=${resolved.cron}; ` +
|
||||
`minScore=${resolved.minScore}, minRecallCount=${resolved.minRecallCount}, ` +
|
||||
`minUniqueQueries=${resolved.minUniqueQueries}.`
|
||||
`minUniqueQueries=${resolved.minUniqueQueries}, recencyHalfLifeDays=${resolved.recencyHalfLifeDays}, ` +
|
||||
`maxAgeDays=${resolved.maxAgeDays ?? "none"}.`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -107,6 +108,7 @@ function formatStatus(cfg: OpenClawConfig): string {
|
||||
`- cadence: ${cadence}${timezone}`,
|
||||
`- limit: ${resolved.limit}`,
|
||||
`- thresholds: minScore=${resolved.minScore}, minRecallCount=${resolved.minRecallCount}, minUniqueQueries=${resolved.minUniqueQueries}`,
|
||||
`- aging: recencyHalfLifeDays=${resolved.recencyHalfLifeDays}, maxAgeDays=${resolved.maxAgeDays ?? "none"}`,
|
||||
`- verboseLogging: ${resolved.verboseLogging ? "on" : "off"}`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
@@ -127,6 +127,7 @@ describe("short-term dreaming config", () => {
|
||||
minScore: constants.DEFAULT_DREAMING_MIN_SCORE,
|
||||
minRecallCount: constants.DEFAULT_DREAMING_MIN_RECALL_COUNT,
|
||||
minUniqueQueries: constants.DEFAULT_DREAMING_MIN_UNIQUE_QUERIES,
|
||||
recencyHalfLifeDays: constants.DEFAULT_DREAMING_RECENCY_HALF_LIFE_DAYS,
|
||||
verboseLogging: false,
|
||||
});
|
||||
});
|
||||
@@ -142,6 +143,8 @@ describe("short-term dreaming config", () => {
|
||||
minScore: 0.4,
|
||||
minRecallCount: 2,
|
||||
minUniqueQueries: 3,
|
||||
recencyHalfLifeDays: 21,
|
||||
maxAgeDays: 30,
|
||||
verboseLogging: true,
|
||||
},
|
||||
},
|
||||
@@ -154,6 +157,8 @@ describe("short-term dreaming config", () => {
|
||||
minScore: 0.4,
|
||||
minRecallCount: 2,
|
||||
minUniqueQueries: 3,
|
||||
recencyHalfLifeDays: 21,
|
||||
maxAgeDays: 30,
|
||||
verboseLogging: true,
|
||||
});
|
||||
});
|
||||
@@ -168,6 +173,8 @@ describe("short-term dreaming config", () => {
|
||||
minScore: "0.6",
|
||||
minRecallCount: "2",
|
||||
minUniqueQueries: "3",
|
||||
recencyHalfLifeDays: "9",
|
||||
maxAgeDays: "45",
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -178,6 +185,8 @@ describe("short-term dreaming config", () => {
|
||||
minScore: 0.6,
|
||||
minRecallCount: 2,
|
||||
minUniqueQueries: 3,
|
||||
recencyHalfLifeDays: 9,
|
||||
maxAgeDays: 45,
|
||||
verboseLogging: false,
|
||||
});
|
||||
});
|
||||
@@ -191,6 +200,8 @@ describe("short-term dreaming config", () => {
|
||||
minScore: "",
|
||||
minRecallCount: " ",
|
||||
minUniqueQueries: "",
|
||||
recencyHalfLifeDays: "",
|
||||
maxAgeDays: " ",
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -201,6 +212,7 @@ describe("short-term dreaming config", () => {
|
||||
minScore: constants.DREAMING_PRESET_DEFAULTS.deep.minScore,
|
||||
minRecallCount: constants.DREAMING_PRESET_DEFAULTS.deep.minRecallCount,
|
||||
minUniqueQueries: constants.DREAMING_PRESET_DEFAULTS.deep.minUniqueQueries,
|
||||
recencyHalfLifeDays: constants.DREAMING_PRESET_DEFAULTS.deep.recencyHalfLifeDays,
|
||||
verboseLogging: false,
|
||||
});
|
||||
});
|
||||
@@ -247,6 +259,8 @@ describe("short-term dreaming config", () => {
|
||||
minScore: -0.2,
|
||||
minRecallCount: -2,
|
||||
minUniqueQueries: -4,
|
||||
recencyHalfLifeDays: -10,
|
||||
maxAgeDays: -5,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -255,7 +269,9 @@ describe("short-term dreaming config", () => {
|
||||
minScore: constants.DREAMING_PRESET_DEFAULTS.rem.minScore,
|
||||
minRecallCount: constants.DREAMING_PRESET_DEFAULTS.rem.minRecallCount,
|
||||
minUniqueQueries: constants.DREAMING_PRESET_DEFAULTS.rem.minUniqueQueries,
|
||||
recencyHalfLifeDays: constants.DREAMING_PRESET_DEFAULTS.rem.recencyHalfLifeDays,
|
||||
});
|
||||
expect(resolved.maxAgeDays).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps dreaming disabled when mode is off", () => {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
DEFAULT_MEMORY_DREAMING_MIN_RECALL_COUNT,
|
||||
DEFAULT_MEMORY_DREAMING_MIN_SCORE,
|
||||
DEFAULT_MEMORY_DREAMING_MIN_UNIQUE_QUERIES,
|
||||
DEFAULT_MEMORY_DREAMING_RECENCY_HALF_LIFE_DAYS,
|
||||
DEFAULT_MEMORY_DREAMING_MODE,
|
||||
DEFAULT_MEMORY_DREAMING_PRESET,
|
||||
MEMORY_DREAMING_PRESET_DEFAULTS,
|
||||
@@ -80,6 +81,8 @@ export type ShortTermPromotionDreamingConfig = {
|
||||
minScore: number;
|
||||
minRecallCount: number;
|
||||
minUniqueQueries: number;
|
||||
recencyHalfLifeDays: number;
|
||||
maxAgeDays?: number;
|
||||
verboseLogging: boolean;
|
||||
};
|
||||
|
||||
@@ -130,7 +133,7 @@ function formatRepairSummary(repair: {
|
||||
}
|
||||
|
||||
function resolveManagedCronDescription(config: ShortTermPromotionDreamingConfig): string {
|
||||
return `${MANAGED_DREAMING_CRON_TAG} Promote weighted short-term recalls into MEMORY.md (limit=${config.limit}, minScore=${config.minScore.toFixed(3)}, minRecallCount=${config.minRecallCount}, minUniqueQueries=${config.minUniqueQueries}).`;
|
||||
return `${MANAGED_DREAMING_CRON_TAG} Promote weighted short-term recalls into MEMORY.md (limit=${config.limit}, minScore=${config.minScore.toFixed(3)}, minRecallCount=${config.minRecallCount}, minUniqueQueries=${config.minUniqueQueries}, recencyHalfLifeDays=${config.recencyHalfLifeDays}, maxAgeDays=${config.maxAgeDays ?? "none"}).`;
|
||||
}
|
||||
|
||||
function buildManagedDreamingCronJob(
|
||||
@@ -269,6 +272,8 @@ export function resolveShortTermPromotionDreamingConfig(params: {
|
||||
minScore: resolved.minScore,
|
||||
minRecallCount: resolved.minRecallCount,
|
||||
minUniqueQueries: resolved.minUniqueQueries,
|
||||
recencyHalfLifeDays: resolved.recencyHalfLifeDays,
|
||||
...(typeof resolved.maxAgeDays === "number" ? { maxAgeDays: resolved.maxAgeDays } : {}),
|
||||
verboseLogging: resolved.verboseLogging,
|
||||
};
|
||||
}
|
||||
@@ -387,7 +392,7 @@ export async function runShortTermDreamingPromotionIfTriggered(params: {
|
||||
|
||||
if (params.config.verboseLogging) {
|
||||
params.logger.info(
|
||||
`memory-core: dreaming verbose enabled (cron=${params.config.cron}, limit=${params.config.limit}, minScore=${params.config.minScore.toFixed(3)}, minRecallCount=${params.config.minRecallCount}, minUniqueQueries=${params.config.minUniqueQueries}, workspaces=${workspaces.length}).`,
|
||||
`memory-core: dreaming verbose enabled (cron=${params.config.cron}, limit=${params.config.limit}, minScore=${params.config.minScore.toFixed(3)}, minRecallCount=${params.config.minRecallCount}, minUniqueQueries=${params.config.minUniqueQueries}, recencyHalfLifeDays=${params.config.recencyHalfLifeDays}, maxAgeDays=${params.config.maxAgeDays ?? "none"}, workspaces=${workspaces.length}).`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -408,6 +413,8 @@ export async function runShortTermDreamingPromotionIfTriggered(params: {
|
||||
minScore: params.config.minScore,
|
||||
minRecallCount: params.config.minRecallCount,
|
||||
minUniqueQueries: params.config.minUniqueQueries,
|
||||
recencyHalfLifeDays: params.config.recencyHalfLifeDays,
|
||||
maxAgeDays: params.config.maxAgeDays,
|
||||
});
|
||||
totalCandidates += candidates.length;
|
||||
if (params.config.verboseLogging) {
|
||||
@@ -431,6 +438,7 @@ export async function runShortTermDreamingPromotionIfTriggered(params: {
|
||||
minScore: params.config.minScore,
|
||||
minRecallCount: params.config.minRecallCount,
|
||||
minUniqueQueries: params.config.minUniqueQueries,
|
||||
maxAgeDays: params.config.maxAgeDays,
|
||||
timezone: params.config.timezone,
|
||||
});
|
||||
totalApplied += applied.applied;
|
||||
@@ -528,6 +536,7 @@ export const __testing = {
|
||||
DEFAULT_DREAMING_MIN_SCORE: DEFAULT_MEMORY_DREAMING_MIN_SCORE,
|
||||
DEFAULT_DREAMING_MIN_RECALL_COUNT: DEFAULT_MEMORY_DREAMING_MIN_RECALL_COUNT,
|
||||
DEFAULT_DREAMING_MIN_UNIQUE_QUERIES: DEFAULT_MEMORY_DREAMING_MIN_UNIQUE_QUERIES,
|
||||
DEFAULT_DREAMING_RECENCY_HALF_LIFE_DAYS: DEFAULT_MEMORY_DREAMING_RECENCY_HALF_LIFE_DAYS,
|
||||
DREAMING_PRESET_DEFAULTS: MEMORY_DREAMING_PRESET_DEFAULTS,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -206,6 +206,80 @@ describe("short-term promotion", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("lets recency half-life tune the temporal score", async () => {
|
||||
await withTempWorkspace(async (workspaceDir) => {
|
||||
await recordShortTermRecalls({
|
||||
workspaceDir,
|
||||
query: "glacier retention",
|
||||
nowMs: Date.parse("2026-04-01T10:00:00.000Z"),
|
||||
results: [
|
||||
{
|
||||
path: "memory/2026-04-01.md",
|
||||
startLine: 1,
|
||||
endLine: 2,
|
||||
score: 0.92,
|
||||
snippet: "Move backups to S3 Glacier.",
|
||||
source: "memory",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const slowerDecay = await rankShortTermPromotionCandidates({
|
||||
workspaceDir,
|
||||
minScore: 0,
|
||||
minRecallCount: 0,
|
||||
minUniqueQueries: 0,
|
||||
nowMs: Date.parse("2026-04-15T10:00:00.000Z"),
|
||||
recencyHalfLifeDays: 14,
|
||||
});
|
||||
const fasterDecay = await rankShortTermPromotionCandidates({
|
||||
workspaceDir,
|
||||
minScore: 0,
|
||||
minRecallCount: 0,
|
||||
minUniqueQueries: 0,
|
||||
nowMs: Date.parse("2026-04-15T10:00:00.000Z"),
|
||||
recencyHalfLifeDays: 7,
|
||||
});
|
||||
|
||||
expect(slowerDecay).toHaveLength(1);
|
||||
expect(fasterDecay).toHaveLength(1);
|
||||
expect(slowerDecay[0]?.components.recency).toBeCloseTo(0.5, 3);
|
||||
expect(fasterDecay[0]?.components.recency).toBeCloseTo(0.25, 3);
|
||||
expect(slowerDecay[0]!.score).toBeGreaterThan(fasterDecay[0]!.score);
|
||||
});
|
||||
});
|
||||
|
||||
it("filters out candidates older than maxAgeDays during ranking", async () => {
|
||||
await withTempWorkspace(async (workspaceDir) => {
|
||||
await recordShortTermRecalls({
|
||||
workspaceDir,
|
||||
query: "old note",
|
||||
nowMs: Date.parse("2026-04-01T10:00:00.000Z"),
|
||||
results: [
|
||||
{
|
||||
path: "memory/2026-04-01.md",
|
||||
startLine: 1,
|
||||
endLine: 2,
|
||||
score: 0.92,
|
||||
snippet: "Move backups to S3 Glacier.",
|
||||
source: "memory",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const ranked = await rankShortTermPromotionCandidates({
|
||||
workspaceDir,
|
||||
minScore: 0,
|
||||
minRecallCount: 0,
|
||||
minUniqueQueries: 0,
|
||||
nowMs: Date.parse("2026-04-15T10:00:00.000Z"),
|
||||
maxAgeDays: 7,
|
||||
});
|
||||
|
||||
expect(ranked).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("treats negative threshold overrides as invalid and keeps defaults", async () => {
|
||||
await withTempWorkspace(async (workspaceDir) => {
|
||||
await recordShortTermRecalls({
|
||||
@@ -271,6 +345,53 @@ describe("short-term promotion", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("skips direct candidates that exceed maxAgeDays during apply", async () => {
|
||||
await withTempWorkspace(async (workspaceDir) => {
|
||||
const applied = await applyShortTermPromotions({
|
||||
workspaceDir,
|
||||
maxAgeDays: 7,
|
||||
minScore: 0,
|
||||
minRecallCount: 0,
|
||||
minUniqueQueries: 0,
|
||||
candidates: [
|
||||
{
|
||||
key: "memory:memory/2026-04-01.md:1:1",
|
||||
path: "memory/2026-04-01.md",
|
||||
startLine: 1,
|
||||
endLine: 1,
|
||||
source: "memory",
|
||||
snippet: "Expired short-term note.",
|
||||
recallCount: 3,
|
||||
avgScore: 0.95,
|
||||
maxScore: 0.95,
|
||||
uniqueQueries: 2,
|
||||
firstRecalledAt: "2026-04-01T00:00:00.000Z",
|
||||
lastRecalledAt: "2026-04-02T00:00:00.000Z",
|
||||
ageDays: 10,
|
||||
score: 0.95,
|
||||
recallDays: ["2026-04-01", "2026-04-02"],
|
||||
conceptTags: ["expired"],
|
||||
components: {
|
||||
frequency: 1,
|
||||
relevance: 1,
|
||||
diversity: 1,
|
||||
recency: 1,
|
||||
consolidation: 1,
|
||||
conceptual: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(applied.applied).toBe(0);
|
||||
await expect(
|
||||
fs.readFile(path.join(workspaceDir, "MEMORY.md"), "utf-8"),
|
||||
).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("applies promotion candidates to MEMORY.md and marks them promoted", async () => {
|
||||
await withTempWorkspace(async (workspaceDir) => {
|
||||
await writeDailyMemoryNote(workspaceDir, "2026-04-01", [
|
||||
|
||||
@@ -146,6 +146,7 @@ export type RankShortTermPromotionOptions = {
|
||||
minScore?: number;
|
||||
minRecallCount?: number;
|
||||
minUniqueQueries?: number;
|
||||
maxAgeDays?: number;
|
||||
includePromoted?: boolean;
|
||||
recencyHalfLifeDays?: number;
|
||||
weights?: Partial<PromotionWeights>;
|
||||
@@ -159,6 +160,7 @@ export type ApplyShortTermPromotionsOptions = {
|
||||
minScore?: number;
|
||||
minRecallCount?: number;
|
||||
minUniqueQueries?: number;
|
||||
maxAgeDays?: number;
|
||||
nowMs?: number;
|
||||
timezone?: string;
|
||||
};
|
||||
@@ -651,6 +653,7 @@ export async function rankShortTermPromotionCandidates(
|
||||
options.minUniqueQueries,
|
||||
DEFAULT_PROMOTION_MIN_UNIQUE_QUERIES,
|
||||
);
|
||||
const maxAgeDays = toFiniteNonNegativeInt(options.maxAgeDays, -1);
|
||||
const includePromoted = Boolean(options.includePromoted);
|
||||
const halfLifeDays = toFinitePositive(
|
||||
options.recencyHalfLifeDays,
|
||||
@@ -686,6 +689,9 @@ export async function rankShortTermPromotionCandidates(
|
||||
const ageDays = Number.isFinite(lastRecalledAtMs)
|
||||
? Math.max(0, (nowMs - lastRecalledAtMs) / DAY_MS)
|
||||
: 0;
|
||||
if (maxAgeDays >= 0 && ageDays > maxAgeDays) {
|
||||
continue;
|
||||
}
|
||||
const recency = clampScore(calculateRecencyComponent(ageDays, halfLifeDays));
|
||||
const recallDays = entry.recallDays ?? [];
|
||||
const conceptTags = entry.conceptTags ?? [];
|
||||
@@ -946,6 +952,7 @@ export async function applyShortTermPromotions(
|
||||
options.minUniqueQueries,
|
||||
DEFAULT_PROMOTION_MIN_UNIQUE_QUERIES,
|
||||
);
|
||||
const maxAgeDays = toFiniteNonNegativeInt(options.maxAgeDays, -1);
|
||||
const memoryPath = path.join(workspaceDir, "MEMORY.md");
|
||||
|
||||
return await withShortTermLock(workspaceDir, async () => {
|
||||
@@ -964,6 +971,9 @@ export async function applyShortTermPromotions(
|
||||
if (candidate.uniqueQueries < minUniqueQueries) {
|
||||
return false;
|
||||
}
|
||||
if (maxAgeDays >= 0 && candidate.ageDays > maxAgeDays) {
|
||||
return false;
|
||||
}
|
||||
const latest = store.entries[candidate.key];
|
||||
if (latest?.promotedAt) {
|
||||
return false;
|
||||
|
||||
Reference in New Issue
Block a user