openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
2,177 lines • 83.6 kB
JavaScript
import { a as normalizeLowercaseStringOrEmpty } from "./string-coerce-mnp54Vah.js";
import { n as asNullableRecord } from "./record-coerce-DHZ4bFlT.js";
import { i as formatErrorMessage } from "./errors-BXgSefBE.js";
import { _ as uniqueStrings, l as normalizeStringEntries } from "./string-normalization-WNUDCpXX.js";
import { H as formatMemoryDreamingDay, U as isSameMemoryDreamingDay } from "./dreaming-SqhFI2Et.js";
import "./string-coerce-runtime-CEGJWkQ_.js";
import "./memory-core-host-status-DVjlVr90.js";
import { n as appendMemoryHostEvent } from "./events-DvaHlgwL.js";
import "./memory-host-events-Bq7C7Kfo.js";
import { a as SHORT_TERM_LOCK_NAMESPACE, c as SHORT_TERM_RECALL_NAMESPACE, d as memoryCoreStateReference, f as memoryCoreWorkspaceStateKey, g as writeMemoryCoreWorkspaceEntry, h as writeMemoryCoreWorkspaceEntries, i as SHORT_TERM_LOCK_MAX_ENTRIES, m as readMemoryCoreWorkspaceEntries, o as SHORT_TERM_META_NAMESPACE, p as openMemoryCoreStateStore, s as SHORT_TERM_PHASE_SIGNAL_NAMESPACE } from "./dreaming-state-D7W5VtYL.js";
import "./dreaming-shared-BChAYuyu.js";
import { n as resolveMemoryCoreTimestamp, t as resolveMemoryCoreNowMs } from "./time-DhPCijtC.js";
import path from "node:path";
import fs from "node:fs/promises";
import { createHash } from "node:crypto";
const CONCEPT_STOP_WORDS = new Set(Object.values({
shared: [
"about",
"after",
"agent",
"again",
"also",
"assistant",
"because",
"before",
"being",
"between",
"build",
"called",
"could",
"daily",
"default",
"deploy",
"during",
"every",
"file",
"files",
"from",
"have",
"into",
"just",
"line",
"lines",
"long",
"main",
"make",
"memory",
"month",
"more",
"most",
"move",
"much",
"next",
"note",
"notes",
"over",
"part",
"past",
"port",
"same",
"score",
"search",
"session",
"sessions",
"short",
"should",
"since",
"some",
"subagent",
"system",
"than",
"that",
"their",
"there",
"these",
"they",
"this",
"through",
"today",
"user",
"using",
"with",
"work",
"workspace",
"year"
],
english: [
"and",
"are",
"for",
"into",
"its",
"our",
"the",
"then",
"were",
"you",
"your"
],
spanish: [
"al",
"con",
"como",
"de",
"del",
"el",
"en",
"es",
"la",
"las",
"los",
"para",
"por",
"que",
"se",
"sin",
"su",
"sus",
"una",
"uno",
"unos",
"unas",
"y"
],
french: [
"au",
"aux",
"avec",
"dans",
"de",
"des",
"du",
"en",
"est",
"et",
"la",
"le",
"les",
"ou",
"pour",
"que",
"qui",
"sans",
"ses",
"son",
"sur",
"une",
"un"
],
german: [
"auf",
"aus",
"bei",
"das",
"dem",
"den",
"der",
"des",
"die",
"ein",
"eine",
"einem",
"einen",
"einer",
"für",
"im",
"in",
"mit",
"nach",
"oder",
"ohne",
"über",
"und",
"von",
"zu",
"zum",
"zur"
],
cjk: [
"が",
"から",
"する",
"して",
"した",
"で",
"と",
"に",
"の",
"は",
"へ",
"まで",
"も",
"や",
"を",
"与",
"为",
"了",
"及",
"和",
"在",
"将",
"或",
"把",
"是",
"用",
"的",
"과",
"는",
"도",
"로",
"를",
"에",
"에서",
"와",
"은",
"으로",
"을",
"이",
"하다",
"한",
"할",
"해",
"했다",
"했다"
],
pathNoise: [
"cjs",
"cpp",
"cts",
"jsx",
"json",
"md",
"mjs",
"mts",
"text",
"toml",
"ts",
"tsx",
"txt",
"yaml",
"yml"
]
}).flat().map((word) => normalizeLowercaseStringOrEmpty(word)));
const PROTECTED_GLOSSARY = [
"backup",
"backups",
"embedding",
"embeddings",
"failover",
"gateway",
"glacier",
"gpt",
"kv",
"network",
"openai",
"qmd",
"router",
"s3",
"vlan",
"sauvegarde",
"routeur",
"passerelle",
"konfiguration",
"sicherung",
"überwachung",
"configuración",
"respaldo",
"enrutador",
"puerta-de-enlace",
"バックアップ",
"フェイルオーバー",
"ルーター",
"ネットワーク",
"ゲートウェイ",
"障害対応",
"路由器",
"备份",
"故障转移",
"网络",
"网关",
"라우터",
"백업",
"페일오버",
"네트워크",
"게이트웨이",
"장애대응"
].map((word) => normalizeLowercaseStringOrEmpty(word.normalize("NFKC")));
const COMPOUND_TOKEN_RE = /[\p{L}\p{N}]+(?:[._/-][\p{L}\p{N}]+)+/gu;
const LETTER_OR_NUMBER_RE = /[\p{L}\p{N}]/u;
const LATIN_RE = /\p{Script=Latin}/u;
const HAN_RE = /\p{Script=Han}/u;
const HIRAGANA_RE = /\p{Script=Hiragana}/u;
const KATAKANA_RE = /\p{Script=Katakana}/u;
const HANGUL_RE = /\p{Script=Hangul}/u;
const DEFAULT_WORD_SEGMENTER = typeof Intl.Segmenter === "function" ? new Intl.Segmenter("und", { granularity: "word" }) : null;
function containsLetterOrNumber(value) {
return LETTER_OR_NUMBER_RE.test(value);
}
function classifyConceptTagScript(tag) {
const normalized = tag.normalize("NFKC");
const hasLatin = LATIN_RE.test(normalized);
const hasCjk = HAN_RE.test(normalized) || HIRAGANA_RE.test(normalized) || KATAKANA_RE.test(normalized) || HANGUL_RE.test(normalized);
if (hasLatin && hasCjk) return "mixed";
if (hasCjk) return "cjk";
if (hasLatin) return "latin";
return "other";
}
function minimumTokenLengthForScript(script) {
if (script === "cjk") return 2;
return 3;
}
function isKanaOnlyToken(value) {
return !HAN_RE.test(value) && !HANGUL_RE.test(value) && (HIRAGANA_RE.test(value) || KATAKANA_RE.test(value));
}
function normalizeConceptToken(rawToken) {
const normalized = normalizeLowercaseStringOrEmpty(rawToken.normalize("NFKC").replace(/^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/gu, "").replaceAll("_", "-"));
if (!normalized || !containsLetterOrNumber(normalized) || normalized.length > 32) return null;
if (/^\d+$/.test(normalized) || /^\d{4}-\d{2}-\d{2}$/u.test(normalized) || /^\d{4}-\d{2}-\d{2}\.[\p{L}\p{N}]+$/u.test(normalized)) return null;
const script = classifyConceptTagScript(normalized);
if (normalized.length < minimumTokenLengthForScript(script)) return null;
if (isKanaOnlyToken(normalized) && normalized.length < 3) return null;
if (CONCEPT_STOP_WORDS.has(normalized)) return null;
return normalized;
}
function collectGlossaryMatches(source) {
const normalizedSource = normalizeLowercaseStringOrEmpty(source.normalize("NFKC"));
const matches = [];
for (const entry of PROTECTED_GLOSSARY) {
if (!normalizedSource.includes(entry)) continue;
matches.push(entry);
}
return matches;
}
function collectCompoundTokens(source) {
return source.match(COMPOUND_TOKEN_RE) ?? [];
}
function collectSegmentTokens(source) {
if (DEFAULT_WORD_SEGMENTER) return Array.from(DEFAULT_WORD_SEGMENTER.segment(source), (part) => part.isWordLike ? part.segment : "").filter(Boolean);
return source.split(/[^\p{L}\p{N}]+/u).filter(Boolean);
}
function pushNormalizedTag(tags, rawToken, limit) {
const normalized = normalizeConceptToken(rawToken);
if (!normalized || tags.includes(normalized)) return;
tags.push(normalized);
if (tags.length > limit) tags.splice(limit);
}
function deriveConceptTags(params) {
const source = `${path.basename(params.path)} ${params.snippet}`;
const limit = Number.isFinite(params.limit) ? Math.max(0, Math.floor(params.limit)) : 8;
if (limit === 0) return [];
const tags = [];
for (const rawToken of [
...collectGlossaryMatches(source),
...collectCompoundTokens(source),
...collectSegmentTokens(source)
]) {
pushNormalizedTag(tags, rawToken, limit);
if (tags.length >= limit) break;
}
return tags;
}
function summarizeConceptTagScriptCoverage(conceptTagsByEntry) {
const coverage = {
latinEntryCount: 0,
cjkEntryCount: 0,
mixedEntryCount: 0,
otherEntryCount: 0
};
for (const conceptTags of conceptTagsByEntry) {
let hasLatin = false;
let hasCjk = false;
let hasOther = false;
for (const tag of conceptTags) {
const family = classifyConceptTagScript(tag);
if (family === "mixed") {
hasLatin = true;
hasCjk = true;
continue;
}
if (family === "latin") {
hasLatin = true;
continue;
}
if (family === "cjk") {
hasCjk = true;
continue;
}
hasOther = true;
}
if (hasLatin && hasCjk) coverage.mixedEntryCount += 1;
else if (hasCjk) coverage.cjkEntryCount += 1;
else if (hasLatin) coverage.latinEntryCount += 1;
else if (hasOther) coverage.otherEntryCount += 1;
}
return coverage;
}
//#endregion
//#region extensions/memory-core/src/memory-budget.ts
/**
* Bounded MEMORY.md compaction for dreaming/promotion writes.
*
* Background: the dreaming pipeline appends promoted entries to MEMORY.md
* via short-term-promotion.applyShortTermPromotions. Without a size budget,
* MEMORY.md grows unboundedly across deep-phase sweeps and eventually
* exceeds bootstrap's per-file injection cap, breaking session bootstrap.
* See issue #73691.
*
* Strategy: drop the OLDEST auto-promoted sections (date-ordered) until
* the file plus the new section fit within the budget. User-authored
* content (anything that is not a `## Promoted From Short-Term Memory
* (DATE)` section) is preserved unconditionally — only dreaming-owned
* sections are eligible for compaction.
*/
const PROMOTION_SECTION_HEADING_RE = /^## Promoted From Short-Term Memory \(([^)]+)\)\s*$/;
/**
* Default budget for MEMORY.md content on disk, in characters. Chosen to
* stay safely below the bootstrap injection cap (~12KB per file at the
* time of writing) so promoted memory keeps reaching new sessions instead
* of being silently dropped by bootstrap truncation.
*/
const DEFAULT_MEMORY_FILE_MAX_CHARS = 1e4;
/**
* Reserve for writer-side overhead that the helper does not see directly:
* the `# Long-Term Memory\n\n` header re-emitted when compaction empties
* out (20 chars) and `withTrailingNewline`'s trailing `\n` (1 char). See
* the actual write expression in `applyShortTermPromotions`. Subtracting
* this from `budgetChars` keeps the on-disk file inside the caller's
* stated budget instead of exceeding it by up to ~21 chars in edge cases.
*/
const WRITE_OVERHEAD_RESERVE = 21;
function parseMemoryBlocks(content) {
if (content.length === 0) return [];
const lines = content.split(/\r?\n/);
const blocks = [];
let currentLines = [];
let currentKind = "preserved";
let currentDate;
const flush = () => {
if (currentLines.length === 0) return;
const text = currentLines.join("\n");
if (currentKind === "promotion" && currentDate) blocks.push({
kind: "promotion",
date: currentDate,
text
});
else blocks.push({
kind: "preserved",
text
});
currentLines = [];
currentKind = "preserved";
currentDate = void 0;
};
for (const line of lines) if (line.startsWith("## ")) {
flush();
const match = PROMOTION_SECTION_HEADING_RE.exec(line);
if (match) {
currentKind = "promotion";
currentDate = match[1];
} else currentKind = "preserved";
currentLines = [line];
} else currentLines.push(line);
flush();
return blocks;
}
function joinBlocks(blocks) {
return blocks.map((block) => block.text).join("\n");
}
/**
* Drop oldest auto-promotion sections from `existingMemory` until
* `existingMemory + newSection` fits within `budgetChars`. Returns the
* (possibly trimmed) existing memory and the dates of dropped sections.
*
* Guarantees:
* - Non-promotion content (user-authored markdown, the file header, any
* `##` heading not matching the promotion pattern) is preserved.
* - Promotion sections are dropped in ascending date order (oldest first).
* - If `existingMemory + newSection` already fits the budget, the existing
* memory is returned unchanged.
* - If the budget cannot be satisfied even by dropping every promotion
* section, the function drops them all and returns; the caller writes
* the new section anyway. This is the "log and continue" failure mode —
* refusing the new write would silently swallow the freshest material.
*/
function compactMemoryForBudget(params) {
const { existingMemory, newSection, budgetChars } = params;
if (budgetChars <= 0) return {
compacted: existingMemory,
droppedDates: []
};
const effectiveBudget = Math.max(0, budgetChars - WRITE_OVERHEAD_RESERVE);
if (existingMemory.length + newSection.length <= effectiveBudget) return {
compacted: existingMemory,
droppedDates: []
};
const blocks = parseMemoryBlocks(existingMemory);
const promotionEntries = blocks.map((block, index) => block.kind === "promotion" ? {
index,
date: block.date,
length: block.text.length
} : null).filter((entry) => entry !== null).toSorted((a, b) => a.date.localeCompare(b.date));
if (promotionEntries.length === 0) return {
compacted: existingMemory,
droppedDates: []
};
const droppedIndices = /* @__PURE__ */ new Set();
const droppedDates = [];
let projectedExistingSize = existingMemory.length;
const blockSeparatorCost = blocks.length > 1 ? 1 : 0;
for (const entry of promotionEntries) {
if (projectedExistingSize + newSection.length <= effectiveBudget) break;
droppedIndices.add(entry.index);
droppedDates.push(entry.date);
projectedExistingSize = Math.max(0, projectedExistingSize - entry.length - blockSeparatorCost);
}
if (droppedIndices.size === 0) return {
compacted: existingMemory,
droppedDates: []
};
return {
compacted: joinBlocks(blocks.filter((_, index) => !droppedIndices.has(index))),
droppedDates
};
}
//#endregion
//#region extensions/memory-core/src/short-term-promotion.ts
const SHORT_TERM_PATH_RE = /(?:^|\/)memory\/(?:[^/]+\/)*(\d{4})-(\d{2})-(\d{2})(?:-[^/]+)?\.md$/;
const DREAMING_MEMORY_PATH_RE = /(?:^|\/)memory\/dreaming\//;
const SHORT_TERM_SESSION_CORPUS_RE = /(?:^|\/)memory\/\.dreams\/session-corpus\/(\d{4})-(\d{2})-(\d{2})\.(?:md|txt)$/;
const SHORT_TERM_BASENAME_RE = /^(\d{4})-(\d{2})-(\d{2})(?:-[^/]+)?\.md$/;
const DAY_MS = 1440 * 60 * 1e3;
const DEFAULT_RECENCY_HALF_LIFE_DAYS = 14;
const DEFAULT_PROMOTION_MIN_SCORE = .75;
const DEFAULT_PROMOTION_MIN_RECALL_COUNT = 3;
const DEFAULT_PROMOTION_MIN_UNIQUE_QUERIES = 2;
const PROMOTION_MARKER_PREFIX = "openclaw-memory-promotion:";
const PROMOTED_SNIPPET_CHARS_PER_TOKEN_ESTIMATE = 4;
const MAX_QUERY_HASHES = 32;
const MAX_RECALL_DAYS = 16;
const SHORT_TERM_RECALL_MAX_ENTRIES = 512;
const SHORT_TERM_RECALL_MAX_SNIPPET_CHARS = 800;
const SHORT_TERM_STORE_RELATIVE_PATH = path.join("memory", ".dreams", "short-term-recall.json");
const SHORT_TERM_PHASE_SIGNAL_RELATIVE_PATH = path.join("memory", ".dreams", "phase-signals.json");
const SHORT_TERM_LOCK_RELATIVE_PATH = path.join("memory", ".dreams", "short-term-promotion.lock");
const SHORT_TERM_LOCK_WAIT_TIMEOUT_MS = 1e4;
const SHORT_TERM_LOCK_STALE_MS = 6e4;
const SHORT_TERM_LOCK_RETRY_DELAY_MS = 40;
const DREAMING_ENTRY_LIST_LIMIT = 8;
const PHASE_SIGNAL_LIGHT_BOOST_MAX = .06;
const PHASE_SIGNAL_REM_BOOST_MAX = .09;
const PHASE_SIGNAL_HALF_LIFE_DAYS = 14;
const DREAMING_TRANSCRIPT_PROMPT_LINE_RE = /\[[^\]]*dreaming-narrative[^\]]*]\s*(?:User|Assistant):\s*Write a dream diary entry from these memory fragments:?/i;
const DREAMING_DIFF_PREFIX_RE = /@@\s*-\d+(?:,\d+)?\s+[-*+]\s+/iy;
const GENERIC_DAY_HEADING_RE = /^(?:(?:mon|monday|tue|tues|tuesday|wed|wednesday|thu|thur|thurs|thursday|fri|friday|sat|saturday|sun|sunday)(?:,\s+)?)?(?:(?:jan|january|feb|february|mar|march|apr|april|may|jun|june|jul|july|aug|august|sep|sept|september|oct|october|nov|november|dec|december)\s+\d{1,2}(?:st|nd|rd|th)?(?:,\s*\d{4})?|\d{1,2}[/-]\d{1,2}(?:[/-]\d{2,4})?|\d{4}[/-]\d{2}[/-]\d{2})$/i;
const PROMOTION_LIST_MARKER_RE = /^(?:\d+\.\s+|[-*+]\s+)/;
const MANAGED_DREAMING_HEADINGS = new Set(["light sleep", "rem sleep"]);
const inProcessShortTermLocks = /* @__PURE__ */ new Map();
const DEFAULT_PROMOTION_WEIGHTS = {
frequency: .24,
relevance: .3,
diversity: .15,
recency: .15,
consolidation: .1,
conceptual: .06
};
function clampScore(value) {
if (!Number.isFinite(value)) return 0;
return Math.max(0, Math.min(1, value));
}
function toFiniteScore(value, fallback) {
const num = Number(value);
if (!Number.isFinite(num)) return fallback;
if (num < 0 || num > 1) return fallback;
return num;
}
function normalizeSnippet(raw) {
const trimmed = raw.trim();
if (!trimmed) return "";
return trimmed.replace(/\s+/g, " ");
}
function truncateShortTermSnippet(snippet) {
if (snippet.length <= SHORT_TERM_RECALL_MAX_SNIPPET_CHARS) return snippet;
return snippet.slice(0, SHORT_TERM_RECALL_MAX_SNIPPET_CHARS).trimEnd();
}
function enforceShortTermRecallSnippetCap(store) {
for (const entry of Object.values(store.entries)) entry.snippet = truncateShortTermSnippet(entry.snippet);
}
function consumeDreamingLeadPrefix(snippet) {
let index = 0;
while (index < snippet.length) {
DREAMING_DIFF_PREFIX_RE.lastIndex = index;
if (DREAMING_DIFF_PREFIX_RE.exec(snippet)) {
index = DREAMING_DIFF_PREFIX_RE.lastIndex;
continue;
}
const char = snippet[index];
if (char === "[" || char === "(") {
index += 1;
while (snippet[index] === " ") index += 1;
continue;
}
if ((char === "-" || char === "*" || char === "+" || char === ">") && snippet[index + 1] === " ") {
index += 2;
continue;
}
break;
}
return snippet.slice(index);
}
function hasDreamingNarrativeLead(snippet) {
const withoutPrefix = consumeDreamingLeadPrefix(snippet);
if (/^(?:Candidate|Reflections?):/i.test(withoutPrefix)) return true;
const head = withoutPrefix.slice(0, 200);
return /\b(?:Candidate|Reflections?):/i.test(head);
}
function isContaminatedDreamingSnippet(raw) {
const snippet = normalizeSnippet(raw);
if (!snippet) return false;
if (/<!--\s*openclaw-memory-promotion:/i.test(snippet) || DREAMING_TRANSCRIPT_PROMPT_LINE_RE.test(snippet)) return true;
const hasNarrativeLead = hasDreamingNarrativeLead(snippet);
const hasConfidence = /\bconfidence:\s*\d/i.test(snippet);
const hasEvidence = /\bevidence:\s*(?:memory\/\.dreams\/session-corpus\/|memory\/)/i.test(snippet);
const hasStatus = /\bstatus:\s*staged\b/i.test(snippet);
const hasRecalls = /\brecalls:\s*\d+\b/i.test(snippet);
return hasNarrativeLead && hasConfidence && hasEvidence && hasStatus && hasRecalls;
}
function normalizeMemoryPath(rawPath) {
return rawPath.replaceAll("\\", "/").replace(/^\.\//, "");
}
function buildClaimHash(snippet) {
return createHash("sha1").update(normalizeSnippet(snippet)).digest("hex").slice(0, 12);
}
function buildEntryKey(result) {
const base = `${result.source}:${normalizeMemoryPath(result.path)}:${result.startLine}:${result.endLine}`;
return result.claimHash ? `${base}:${result.claimHash}` : base;
}
function hashQuery(query) {
return createHash("sha1").update(normalizeLowercaseStringOrEmpty(query)).digest("hex").slice(0, 12);
}
function mergeQueryHashes(existing, queryHash) {
if (!queryHash) return existing;
const seen = /* @__PURE__ */ new Set();
const next = existing.filter((value) => {
if (!value || seen.has(value)) return false;
seen.add(value);
return true;
});
if (!seen.has(queryHash)) next.push(queryHash);
if (next.length <= MAX_QUERY_HASHES) return next;
return next.slice(next.length - MAX_QUERY_HASHES);
}
function mergeRecentDistinct(existing, nextValue, limit) {
const seen = /* @__PURE__ */ new Set();
const next = existing.filter((value) => {
if (typeof value !== "string" || value.length === 0 || seen.has(value)) return false;
seen.add(value);
return true;
});
if (nextValue && !next.includes(nextValue)) next.push(nextValue);
if (next.length <= limit) return next;
return next.slice(next.length - limit);
}
function normalizeIsoDay(isoLike) {
if (typeof isoLike !== "string") return null;
return isoLike.trim().match(/^(\d{4}-\d{2}-\d{2})/)?.[1] ?? null;
}
function normalizeDistinctStrings(values, limit) {
const seen = /* @__PURE__ */ new Set();
const normalized = [];
for (const value of values) {
if (typeof value !== "string") continue;
const trimmed = value.trim();
if (!trimmed || seen.has(trimmed)) continue;
seen.add(trimmed);
normalized.push(trimmed);
if (normalized.length >= limit) break;
}
return normalized;
}
function totalSignalCountForEntry(entry) {
return Math.max(0, Math.floor(entry.recallCount ?? 0)) + Math.max(0, Math.floor(entry.dailyCount ?? 0)) + Math.max(0, Math.floor(entry.groundedCount ?? 0));
}
function calculateConsolidationComponent(recallDays) {
if (recallDays.length === 0) return 0;
if (recallDays.length === 1) return .2;
const parsed = recallDays.map((recallDay) => Date.parse(recallDay + "T00:00:00.000Z")).filter((value) => Number.isFinite(value)).toSorted((left, right) => left - right);
if (parsed.length <= 1) return .2;
const spanDays = Math.max(0, (parsed.at(-1) - parsed[0]) / DAY_MS);
const spacing = clampScore(Math.log1p(parsed.length - 1) / Math.log1p(4));
const span = clampScore(spanDays / 7);
return clampScore(.55 * spacing + .45 * span);
}
function calculateConceptualComponent(conceptTags) {
return clampScore(conceptTags.length / 6);
}
function emptyStore(nowIso) {
return {
version: 1,
updatedAt: nowIso,
entries: {}
};
}
function normalizeShortTermRecallStore(raw, nowIso) {
if (!raw || typeof raw !== "object") return emptyStore(nowIso);
const record = raw;
const entriesRaw = record.entries;
const entries = {};
if (entriesRaw && typeof entriesRaw === "object") for (const [key, value] of Object.entries(entriesRaw)) {
if (!value || typeof value !== "object") continue;
const entry = value;
const entryPath = typeof entry.path === "string" ? normalizeMemoryPath(entry.path) : "";
const startLine = Number(entry.startLine);
const endLine = Number(entry.endLine);
const source = entry.source === "memory" ? "memory" : null;
if (!entryPath || !Number.isInteger(startLine) || !Number.isInteger(endLine) || !source) continue;
const recallCount = Math.max(0, Math.floor(Number(entry.recallCount) || 0));
const dailyCount = Math.max(0, Math.floor(Number(entry.dailyCount) || 0));
const groundedCount = Math.max(0, Math.floor(Number(entry.groundedCount) || 0));
const totalScore = Math.max(0, Number(entry.totalScore) || 0);
const maxScore = clampScore(Number(entry.maxScore) || 0);
const firstRecalledAt = typeof entry.firstRecalledAt === "string" ? entry.firstRecalledAt : nowIso;
const lastRecalledAt = typeof entry.lastRecalledAt === "string" ? entry.lastRecalledAt : nowIso;
const promotedAt = typeof entry.promotedAt === "string" ? entry.promotedAt : void 0;
const claimHash = typeof entry.claimHash === "string" && entry.claimHash.trim().length > 0 ? entry.claimHash.trim() : void 0;
const fullSnippet = typeof entry.snippet === "string" ? normalizeSnippet(entry.snippet) : "";
if (fullSnippet && isContaminatedDreamingSnippet(fullSnippet)) continue;
const snippet = truncateShortTermSnippet(fullSnippet);
const queryHashes = Array.isArray(entry.queryHashes) ? normalizeDistinctStrings(entry.queryHashes, MAX_QUERY_HASHES) : [];
const recallDays = Array.isArray(entry.recallDays) ? entry.recallDays.map((recallDay) => typeof recallDay === "string" ? normalizeIsoDay(recallDay) : null).filter((valueLocal) => valueLocal !== null) : [];
const conceptTags = Array.isArray(entry.conceptTags) ? normalizeDistinctStrings(entry.conceptTags.map((tag) => typeof tag === "string" ? normalizeLowercaseStringOrEmpty(tag) : tag), 8) : deriveConceptTags({
path: entryPath,
snippet: fullSnippet
});
const normalizedKey = key || buildEntryKey({
path: entryPath,
startLine,
endLine,
source,
claimHash
});
entries[normalizedKey] = {
key: normalizedKey,
path: entryPath,
startLine,
endLine,
source,
snippet,
recallCount,
dailyCount,
groundedCount,
totalScore,
maxScore,
firstRecalledAt,
lastRecalledAt,
queryHashes,
recallDays: recallDays.slice(-16),
conceptTags,
...claimHash ? { claimHash } : {},
...promotedAt ? { promotedAt } : {}
};
}
return {
version: 1,
updatedAt: typeof record.updatedAt === "string" ? record.updatedAt : nowIso,
entries
};
}
function parseStoreTimestampMs(value) {
if (!value) return Number.NEGATIVE_INFINITY;
const parsed = Date.parse(value);
return Number.isFinite(parsed) ? parsed : Number.NEGATIVE_INFINITY;
}
function compareStoreTimestampDesc(left, right) {
const leftMs = parseStoreTimestampMs(left);
const rightMs = parseStoreTimestampMs(right);
if (leftMs === rightMs) return 0;
return rightMs > leftMs ? 1 : -1;
}
function compareShortTermRecallRetention(a, b) {
const lastDiff = compareStoreTimestampDesc(a.lastRecalledAt, b.lastRecalledAt);
if (lastDiff !== 0) return lastDiff;
const signalDiff = totalSignalCountForEntry(b) - totalSignalCountForEntry(a);
if (signalDiff !== 0) return signalDiff;
const totalScoreDiff = b.totalScore - a.totalScore;
if (totalScoreDiff !== 0) return totalScoreDiff;
const maxScoreDiff = b.maxScore - a.maxScore;
if (maxScoreDiff !== 0) return maxScoreDiff;
const promotedDiff = compareStoreTimestampDesc(a.promotedAt, b.promotedAt);
if (promotedDiff !== 0) return promotedDiff;
return a.key.localeCompare(b.key);
}
function enforceShortTermRecallStoreRetention(store) {
const entries = Object.entries(store.entries);
if (entries.length <= SHORT_TERM_RECALL_MAX_ENTRIES) return 0;
const retained = entries.toSorted(([, a], [, b]) => compareShortTermRecallRetention(a, b)).slice(0, SHORT_TERM_RECALL_MAX_ENTRIES);
store.entries = Object.fromEntries(retained.toSorted(([a], [b]) => a.localeCompare(b)));
return entries.length - retained.length;
}
function toFinitePositive(value, fallback) {
const num = Number(value);
if (!Number.isFinite(num) || num <= 0) return fallback;
return num;
}
function toFiniteNonNegativeInt(value, fallback) {
const num = Number(value);
if (!Number.isFinite(num)) return fallback;
const floored = Math.floor(num);
if (floored < 0) return fallback;
return floored;
}
function normalizeWeights(weights) {
const merged = {
...DEFAULT_PROMOTION_WEIGHTS,
...weights
};
const frequency = Math.max(0, merged.frequency);
const relevance = Math.max(0, merged.relevance);
const diversity = Math.max(0, merged.diversity);
const recency = Math.max(0, merged.recency);
const consolidation = Math.max(0, merged.consolidation);
const conceptual = Math.max(0, merged.conceptual);
const sum = frequency + relevance + diversity + recency + consolidation + conceptual;
if (sum <= 0) return { ...DEFAULT_PROMOTION_WEIGHTS };
return {
frequency: frequency / sum,
relevance: relevance / sum,
diversity: diversity / sum,
recency: recency / sum,
consolidation: consolidation / sum,
conceptual: conceptual / sum
};
}
function calculateRecencyComponent(ageDays, halfLifeDays) {
if (!Number.isFinite(ageDays) || ageDays < 0) return 1;
if (!Number.isFinite(halfLifeDays) || halfLifeDays <= 0) return 1;
const lambda = Math.LN2 / halfLifeDays;
return Math.exp(-lambda * ageDays);
}
function calculatePhaseSignalAgeDays(lastSeenAt, nowMs) {
if (!lastSeenAt) return null;
const parsed = Date.parse(lastSeenAt);
if (!Number.isFinite(parsed)) return null;
return Math.max(0, (nowMs - parsed) / DAY_MS);
}
function calculatePhaseSignalBoost(entry, nowMs) {
if (!entry) return 0;
const lightStrength = clampScore(Math.log1p(Math.max(0, entry.lightHits)) / Math.log1p(6));
const remStrength = clampScore(Math.log1p(Math.max(0, entry.remHits)) / Math.log1p(6));
const lightAgeDays = calculatePhaseSignalAgeDays(entry.lastLightAt, nowMs);
const remAgeDays = calculatePhaseSignalAgeDays(entry.lastRemAt, nowMs);
const lightRecency = lightAgeDays === null ? 0 : clampScore(calculateRecencyComponent(lightAgeDays, PHASE_SIGNAL_HALF_LIFE_DAYS));
const remRecency = remAgeDays === null ? 0 : clampScore(calculateRecencyComponent(remAgeDays, PHASE_SIGNAL_HALF_LIFE_DAYS));
return clampScore(PHASE_SIGNAL_LIGHT_BOOST_MAX * lightStrength * lightRecency + PHASE_SIGNAL_REM_BOOST_MAX * remStrength * remRecency);
}
function resolveStorePath(workspaceDir) {
return memoryCoreStateReference(SHORT_TERM_RECALL_NAMESPACE, workspaceDir);
}
function resolvePhaseSignalPath(workspaceDir) {
return memoryCoreStateReference(SHORT_TERM_PHASE_SIGNAL_NAMESPACE, workspaceDir);
}
function resolveLockPath(workspaceDir) {
return memoryCoreStateReference(SHORT_TERM_LOCK_NAMESPACE, workspaceDir);
}
function parseLockOwnerPid(raw) {
const match = raw.trim().match(/^(\d+):/);
if (!match) return null;
const pid = Number.parseInt(match[1] ?? "", 10);
if (!Number.isInteger(pid) || pid <= 0) return null;
return pid;
}
function isProcessLikelyAlive(pid) {
try {
process.kill(pid, 0);
return true;
} catch (err) {
if (err.code === "ESRCH") return false;
return true;
}
}
async function canStealStaleLock(lockPath) {
const ownerPid = await fs.readFile(lockPath, "utf-8").then((raw) => parseLockOwnerPid(raw)).catch(() => null);
if (ownerPid === null) return true;
return !isProcessLikelyAlive(ownerPid);
}
async function sleep(ms) {
await new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function withInProcessShortTermLock(lockPath, task) {
const previous = inProcessShortTermLocks.get(lockPath) ?? Promise.resolve();
let releaseCurrent;
const current = new Promise((resolve) => {
releaseCurrent = resolve;
});
const queued = previous.catch(() => void 0).then(() => current);
inProcessShortTermLocks.set(lockPath, queued);
await previous.catch(() => void 0);
try {
return await task();
} finally {
releaseCurrent();
if (inProcessShortTermLocks.get(lockPath) === queued) inProcessShortTermLocks.delete(lockPath);
}
}
async function withShortTermLock(workspaceDir, task) {
const lockKey = memoryCoreWorkspaceStateKey(workspaceDir);
const lockRef = resolveLockPath(workspaceDir);
const lockStore = openMemoryCoreStateStore({
namespace: SHORT_TERM_LOCK_NAMESPACE,
maxEntries: SHORT_TERM_LOCK_MAX_ENTRIES
});
return withInProcessShortTermLock(lockKey, async () => {
const startedAt = Date.now();
while (true) {
const owner = `${process.pid}:${Date.now()}`;
if (await lockStore.registerIfAbsent(lockKey, {
owner,
acquiredAt: Date.now()
})) try {
return await task();
} finally {
if ((await lockStore.lookup(lockKey).catch(() => void 0))?.owner === owner) await lockStore.delete(lockKey).catch(() => false);
}
const existing = await lockStore.lookup(lockKey);
if (existing && Date.now() - existing.acquiredAt > SHORT_TERM_LOCK_STALE_MS) {
const ownerPid = parseLockOwnerPid(existing.owner);
if (ownerPid === null || !isProcessLikelyAlive(ownerPid)) {
await lockStore.delete(lockKey);
continue;
}
}
if (Date.now() - startedAt >= SHORT_TERM_LOCK_WAIT_TIMEOUT_MS) throw new Error(`Timed out waiting for short-term promotion lock at ${lockRef}`);
await sleep(SHORT_TERM_LOCK_RETRY_DELAY_MS);
}
});
}
async function readStore(workspaceDir, nowIso) {
const [entryRows, metaRows] = await Promise.all([readMemoryCoreWorkspaceEntries({
namespace: SHORT_TERM_RECALL_NAMESPACE,
workspaceDir
}), readMemoryCoreWorkspaceEntries({
namespace: SHORT_TERM_META_NAMESPACE,
workspaceDir
})]);
const meta = metaRows.find((entry) => entry.key === "recall")?.value;
const store = normalizeShortTermRecallStore({
version: 1,
updatedAt: meta?.updatedAt ?? nowIso,
entries: Object.fromEntries(entryRows.map((entry) => [entry.key, entry.value]))
}, nowIso);
enforceShortTermRecallStoreRetention(store);
return store;
}
function emptyPhaseSignalStore(nowIso) {
return {
version: 1,
updatedAt: nowIso,
entries: {}
};
}
function normalizeShortTermPhaseSignalStore(raw, nowIso) {
const record = asNullableRecord(raw);
if (!record) return emptyPhaseSignalStore(nowIso);
const entriesRaw = asNullableRecord(record?.entries);
if (!entriesRaw) return emptyPhaseSignalStore(nowIso);
const entries = {};
for (const [mapKey, value] of Object.entries(entriesRaw)) {
const entry = asNullableRecord(value);
if (!entry) continue;
const key = typeof entry.key === "string" && entry.key.trim().length > 0 ? entry.key : mapKey;
const lightHits = toFiniteNonNegativeInt(entry.lightHits, 0);
const remHits = toFiniteNonNegativeInt(entry.remHits, 0);
if (lightHits === 0 && remHits === 0) continue;
const lastLightAt = typeof entry.lastLightAt === "string" && entry.lastLightAt.trim().length > 0 ? entry.lastLightAt : void 0;
const lastRemAt = typeof entry.lastRemAt === "string" && entry.lastRemAt.trim().length > 0 ? entry.lastRemAt : void 0;
const lastRemConsideredAt = typeof entry.lastRemConsideredAt === "string" && entry.lastRemConsideredAt.trim().length > 0 ? entry.lastRemConsideredAt : void 0;
entries[key] = {
key,
lightHits,
remHits,
...lastLightAt ? { lastLightAt } : {},
...lastRemAt ? { lastRemAt } : {},
...lastRemConsideredAt ? { lastRemConsideredAt } : {}
};
}
return {
version: 1,
updatedAt: typeof record.updatedAt === "string" && record.updatedAt.trim().length > 0 ? record.updatedAt : nowIso,
entries
};
}
async function readPhaseSignalStore(workspaceDir, nowIso) {
const [entryRows, metaRows] = await Promise.all([readMemoryCoreWorkspaceEntries({
namespace: SHORT_TERM_PHASE_SIGNAL_NAMESPACE,
workspaceDir
}), readMemoryCoreWorkspaceEntries({
namespace: SHORT_TERM_META_NAMESPACE,
workspaceDir
})]);
const meta = metaRows.find((entry) => entry.key === "phase")?.value;
return normalizeShortTermPhaseSignalStore({
version: 1,
updatedAt: meta?.updatedAt ?? nowIso,
entries: Object.fromEntries(entryRows.map((entry) => [entry.key, entry.value]))
}, nowIso);
}
async function writePhaseSignalStore(workspaceDir, store) {
await Promise.all([writeMemoryCoreWorkspaceEntries({
namespace: SHORT_TERM_PHASE_SIGNAL_NAMESPACE,
workspaceDir,
entries: Object.entries(store.entries).map(([key, value]) => ({
key,
value
}))
}), writeMemoryCoreWorkspaceEntry({
namespace: SHORT_TERM_META_NAMESPACE,
workspaceDir,
key: "phase",
value: { updatedAt: store.updatedAt }
})]);
}
async function writeStore(workspaceDir, store) {
enforceShortTermRecallSnippetCap(store);
enforceShortTermRecallStoreRetention(store);
await Promise.all([writeMemoryCoreWorkspaceEntries({
namespace: SHORT_TERM_RECALL_NAMESPACE,
workspaceDir,
entries: Object.entries(store.entries).map(([key, value]) => ({
key,
value
}))
}), writeMemoryCoreWorkspaceEntry({
namespace: SHORT_TERM_META_NAMESPACE,
workspaceDir,
key: "recall",
value: { updatedAt: store.updatedAt }
})]);
}
function isShortTermMemoryPath(filePath) {
const normalized = normalizeMemoryPath(filePath);
if (DREAMING_MEMORY_PATH_RE.test(normalized)) return false;
if (SHORT_TERM_PATH_RE.test(normalized)) return true;
if (SHORT_TERM_SESSION_CORPUS_RE.test(normalized)) return true;
return SHORT_TERM_BASENAME_RE.test(normalized);
}
function normalizeMemoryPathForWorkspace(workspaceDir, rawPath) {
const normalized = normalizeMemoryPath(rawPath);
const workspaceNormalized = normalizeMemoryPath(workspaceDir);
if (path.isAbsolute(rawPath) && normalized.startsWith(`${workspaceNormalized}/`)) return normalized.slice(workspaceNormalized.length + 1);
return normalized;
}
function toNonNegativeInt(value) {
const num = Number(value);
if (!Number.isFinite(num)) return 0;
return Math.max(0, Math.floor(num));
}
function parseEntryRangeFromKey(key, fallbackStartLine, fallbackEndLine) {
const startLine = toNonNegativeInt(fallbackStartLine);
const endLine = toNonNegativeInt(fallbackEndLine);
if (startLine > 0 && endLine > 0) return {
startLine,
endLine
};
const match = key.match(/:(\d+):(\d+)$/);
if (match) return {
startLine: Math.max(1, toNonNegativeInt(match[1])),
endLine: Math.max(1, toNonNegativeInt(match[2]))
};
return {
startLine: 1,
endLine: 1
};
}
function compareDreamingStatsEntryByRecency(a, b) {
const aMs = a.lastRecalledAt ? Date.parse(a.lastRecalledAt) : Number.NEGATIVE_INFINITY;
const bMs = b.lastRecalledAt ? Date.parse(b.lastRecalledAt) : Number.NEGATIVE_INFINITY;
if (Number.isFinite(aMs) || Number.isFinite(bMs)) {
if (bMs !== aMs) return bMs - aMs;
}
if (b.totalSignalCount !== a.totalSignalCount) return b.totalSignalCount - a.totalSignalCount;
return a.path.localeCompare(b.path);
}
function compareDreamingStatsEntryBySignals(a, b) {
if (b.totalSignalCount !== a.totalSignalCount) return b.totalSignalCount - a.totalSignalCount;
if (b.phaseHitCount !== a.phaseHitCount) return b.phaseHitCount - a.phaseHitCount;
return compareDreamingStatsEntryByRecency(a, b);
}
function compareDreamingStatsEntryByPromotion(a, b) {
const aMs = a.promotedAt ? Date.parse(a.promotedAt) : Number.NEGATIVE_INFINITY;
const bMs = b.promotedAt ? Date.parse(b.promotedAt) : Number.NEGATIVE_INFINITY;
if (Number.isFinite(aMs) || Number.isFinite(bMs)) {
if (bMs !== aMs) return bMs - aMs;
}
return compareDreamingStatsEntryBySignals(a, b);
}
function trimDreamingStatsEntries(entries, compare) {
const selected = [];
for (const entry of entries) {
let insertAt = selected.length;
for (let index = 0; index < selected.length; index += 1) if (compare(entry, selected[index]) < 0) {
insertAt = index;
break;
}
if (insertAt < DREAMING_ENTRY_LIST_LIMIT) {
selected.splice(insertAt, 0, entry);
if (selected.length > DREAMING_ENTRY_LIST_LIMIT) selected.pop();
} else if (selected.length < DREAMING_ENTRY_LIST_LIMIT) selected.push(entry);
}
return selected;
}
async function loadShortTermPromotionDreamingStats(params) {
const workspaceDir = params.workspaceDir.trim();
const nowIso = new Date(params.nowMs).toISOString();
const store = await readStore(workspaceDir, nowIso);
let phaseSignalError;
let phaseStore;
try {
phaseStore = await readPhaseSignalStore(workspaceDir, nowIso);
} catch (err) {
phaseSignalError = formatErrorMessage(err);
phaseStore = emptyPhaseSignalStore(nowIso);
}
let shortTermCount = 0;
let recallSignalCount = 0;
let dailySignalCount = 0;
let groundedSignalCount = 0;
let totalSignalCount = 0;
let phaseSignalCount = 0;
let lightPhaseHitCount = 0;
let remPhaseHitCount = 0;
let promotedTotal = 0;
let promotedToday = 0;
let latestPromotedAtMs = Number.NEGATIVE_INFINITY;
let latestPromotedAt;
const activeKeys = /* @__PURE__ */ new Set();
const activeEntries = /* @__PURE__ */ new Map();
const shortTermEntries = [];
const promotedEntries = [];
for (const [entryKey, entry] of Object.entries(store.entries)) {
if (entry.source !== "memory" || !entry.path || !isShortTermMemoryPath(entry.path)) continue;
const range = parseEntryRangeFromKey(entryKey, entry.startLine, entry.endLine);
const recallCount = toNonNegativeInt(entry.recallCount);
const dailyCount = toNonNegativeInt(entry.dailyCount);
const groundedCount = toNonNegativeInt(entry.groundedCount);
const totalEntrySignalCount = recallCount + dailyCount + groundedCount;
const normalizedEntryPath = normalizeMemoryPathForWorkspace(workspaceDir, entry.path);
const detail = {
key: entryKey,
path: normalizedEntryPath,
startLine: range.startLine,
endLine: Math.max(range.startLine, range.endLine),
snippet: normalizeSnippet(entry.snippet) || normalizedEntryPath,
recallCount,
dailyCount,
groundedCount,
totalSignalCount: totalEntrySignalCount,
lightHits: 0,
remHits: 0,
phaseHitCount: 0,
...entry.lastRecalledAt ? { lastRecalledAt: entry.lastRecalledAt } : {}
};
if (!entry.promotedAt) {
shortTermCount += 1;
activeKeys.add(entryKey);
recallSignalCount += recallCount;
dailySignalCount += dailyCount;
groundedSignalCount += groundedCount;
totalSignalCount += totalEntrySignalCount;
shortTermEntries.push(detail);
activeEntries.set(entryKey, detail);
continue;
}
promotedTotal += 1;
promotedEntries.push({
...detail,
promotedAt: entry.promotedAt
});
const promotedAtMs = Date.parse(entry.promotedAt);
if (Number.isFinite(promotedAtMs) && isSameMemoryDreamingDay(promotedAtMs, params.nowMs, params.timezone)) promotedToday += 1;
if (Number.isFinite(promotedAtMs) && promotedAtMs > latestPromotedAtMs) {
latestPromotedAtMs = promotedAtMs;
latestPromotedAt = entry.promotedAt;
}
}
for (const [key, phaseEntry] of Object.entries(phaseStore.entries)) {
if (!activeKeys.has(key)) continue;
const lightHits = toNonNegativeInt(phaseEntry.lightHits);
const remHits = toNonNegativeInt(phaseEntry.remHits);
lightPhaseHitCount += lightHits;
remPhaseHitCount += remHits;
phaseSignalCount += lightHits + remHits;
const detail = activeEntries.get(key);
if (detail) {
detail.lightHits = lightHits;
detail.remHits = remHits;
detail.phaseHitCount = lightHits + remHits;
}
}
return {
shortTermCount,
recallSignalCount,
dailySignalCount,
groundedSignalCount,
totalSignalCount,
phaseSignalCount,
lightPhaseHitCount,
remPhaseHitCount,
promotedTotal,
promotedToday,
storePath: resolveStorePath(workspaceDir),
phaseSignalPath: resolvePhaseSignalPath(workspaceDir),
shortTermEntries: trimDreamingStatsEntries(shortTermEntries, compareDreamingStatsEntryByRecency),
signalEntries: trimDreamingStatsEntries(shortTermEntries, compareDreamingStatsEntryBySignals),
promotedEntries: trimDreamingStatsEntries(promotedEntries, compareDreamingStatsEntryByPromotion),
...phaseSignalError ? { phaseSignalError } : {},
...latestPromotedAt ? { lastPromotedAt: latestPromotedAt } : {}
};
}
async function shortTermRecallSourceExists(params) {
const workspaceDir = params.workspaceDir.trim();
if (!workspaceDir) return false;
for (const sourcePath of resolveShortTermSourcePathCandidates(workspaceDir, params.entry.path)) try {
if ((await fs.stat(sourcePath)).isFile()) return true;
} catch (err) {
if (err.code === "ENOENT") continue;
throw err;
}
return false;
}
async function filterLiveShortTermRecallEntries(params) {
return (await Promise.all(params.entries.map(async (entry) => ({
entry,
exists: await shortTermRecallSourceExists({
workspaceDir: params.workspaceDir,
entry
})
})))).filter((result) => result.exists).map((result) => result.entry);
}
async function recordShortTermRecalls(params) {
const workspaceDir = params.workspaceDir?.trim();
if (!workspaceDir) return;
const query = params.query.trim();
if (!query) return;
const relevant = params.results.filter((result) => result.source === "memory" && isShortTermMemoryPath(result.path));
if (relevant.length === 0) return;
const nowMs = resolveMemoryCoreNowMs(params.nowMs);
const nowIso = resolveMemoryCoreTimestamp(nowMs);
const signalType = params.signalType ?? "recall";
const queryHash = hashQuery(query);
const todayBucket = normalizeIsoDay(params.dayBucket ?? "") ?? formatMemoryDreamingDay(nowMs, params.timezone);
await withShortTermLock(workspaceDir, async () => {
const store = await readStore(workspaceDir, nowIso);
for (const result of relevant) {
const normalizedPath = normalizeMemoryPath(result.path);
const rawSnippet = normalizeSnippet(result.snippet);
const snippet = truncateShortTermSnippet(rawSnippet);
if (!rawSnippet || isContaminatedDreamingSnippet(rawSnippet)) continue;
const claimHash = buildClaimHash(rawSnippet);
const groundedKey = claimHash ? buildEntryKey({
path: normalizedPath,
startLine: Math.max(1, Math.floor(result.startLine)),
endLine: Math.max(1, Math.floor(result.endLine)),
source: "memory",
claimHash
}) : null;
const baseKey = buildEntryKey(result);
const key = groundedKey && store.entries[groundedKey] ? groundedKey : baseKey;
const existing = store.entries[key];
const score = clampScore(result.score);
const recallDaysBase = existing?.recallDays ?? [];
const queryHashesBase = existing?.queryHashes ?? [];
const dedupeSignal = Boolean(params.dedupeByQueryPerDay) && queryHashesBase.includes(queryHash) && recallDaysBase.includes(todayBucket);
const recallCount = signalType === "recall" ? Math.max(0, Math.floor(existing?.recallCount ?? 0) + (dedupeSignal ? 0 : 1)) : Math.max(0, Math.floor(existing?.recallCount ?? 0));
const dailyCount = signalType === "daily" ? Math.max(0, Math.floor(existing?.dailyCount ?? 0) + (dedupeSignal ? 0 : 1)) : Math.max(0, Math.floor(existing?.dailyCount ?? 0));
const totalScore = Math.max(0, (existing?.totalScore ?? 0) + (dedupeSignal ? 0 : score));
const maxScore = Math.max(existing?.maxScore ?? 0, dedupeSignal ? 0 : score);
const queryHashes = mergeQueryHashes(existing?.queryHashes ?? [], queryHash);
const recallDays = mergeRecentDistinct(recallDaysBase, todayBucket, MAX_RECALL_DAYS);
const conceptTags = deriveConceptTags({
path: normalizedPath,
snippet
});
store.entries[key] = {
key,
path: normalizedPath,
startLine: Math.max(1, Math.floor(result.startLine)),
endLine: Math.max(1, Math.floor(result.endLine)),
source: "memory",
snippet: snippet || existing?.snippet || "",
recallCount,
dailyCount,
groundedCount: Math.max(0, Math.floor(existing?.groundedCount ?? 0)),
totalScore,
maxScore,
firstRecalledAt: existing?.firstRecalledAt ?? nowIso,
lastRecalledAt: nowIso,
queryHashes,
recallDays,
conceptTags: conceptTags.length > 0 ? conceptTags : existing?.conceptTags ?? [],
...existing?.claimHash ? { claimHash: existing.claimHash } : {},
...existing?.promotedAt ? { promotedAt: existing.promotedAt } : {}
};
}
store.updatedAt = nowIso;
await writeStore(workspaceDir, store);
await appendMemoryHostEvent(workspaceDir, {
type: "memory.recall.recorded",
timestamp: nowIso,
query,
resultCount: relevant.length,
results: relevant.map((result) => ({
path: normalizeMemoryPath(result.path),
startLine: Math.max(1, Math.floor(result.startLine)),
endLine: Math.max(1, Math.floor(result.endLine)),
score: clampScore(result.score)
}))
});
});
}
async function recordGroundedShortTermCandidates(params) {
const workspaceDir = params.workspaceDir?.trim();
if (!workspaceDir) return;
const query = params.query.trim();
if (!query) return;
const relevant = params.items.map((item) => {
const rawSnippet = normalizeSnippet(item.snippet);
const snippet = truncateShortTermSnippet(rawSnippet);
const normalizedPath = normalizeMemoryPath(item.path);
if (!rawSnippet || isContaminatedDreamingSnippet(rawSnippet) || !normalizedPath || !isShortTermMemoryPath(normalizedPath) || !Number.isFinite(item.startLine) || !Number.isFinite(item.endLine)) return null;
return {
path: normalizedPath,
startLine: Math.max(1, Math.floor(item.startLine)),
endLine: Math.max(1, Math.floor(item.endLine)),
snippet,
identitySnippet: rawSnippet,
score: clampScore(item.score),
query: normalizeSnippet(item.query ?? query),
signalCount: Math.max(1, Math.floor(item.signalCount ?? 1)),
dayBucket: normalizeIsoDay(item.dayBucket ?? params.dayBucket ?? "")
};
}).filter((item) => item !== null);
if (relevant.length === 0) return;
const nowMs = resolveMemoryCoreNowMs(params.nowMs);
const nowIso = resolveMemoryCoreTimestamp(nowMs);
const fallbackDayBucket = formatMemoryDreamingDay(nowMs, params.timezone);
await withShortTermLock(workspaceDir, async () => {
const store = await readStore(workspaceDir, nowIso);
for (const item of relevant) {
const dayBucket = item.dayBucket ?? fallbackDayBucket;
const effectiveQuery = item.query || query;
if (!effectiveQuery) continue;
const queryHash = hashQuery(effectiveQuery);
const claimHash = buildClaimHash(item.identitySnippet);
const key = buildEntryKey({
path: item.path,
startLine: item.startLine,
endLine: item.endLine,
source: "memory",
claimHash
});
const existing = store.entries[key];
const recallDaysBase = existing?.recallDays ?? [];
const queryHashesBase = existing?.queryHashes ?? [];
const dedupeSignal = Boolean(params.dedupeByQueryPerDay) && queryHashesBase.includes(queryHash) && recallDaysBase.includes(dayBucket);
const groundedCount = Math.max(0, Math.floor(existing?.groundedCount ?? 0) + (dedupeSignal ? 0 : item.signalCount));
const totalScore = Math.max(0, (existing?.totalScore ?? 0) + (dedupeSignal ? 0 : item.score * item.signalCount));
const maxScore = Math.max(existing?.maxScore ?? 0, dedupeSignal ? 0 : item.score);
const queryHashes = mergeQueryHashes(existing?.queryHashes ?? [], queryHash);
const recallDays = mergeRecentDistinct(recallDaysBase, dayBucket, MAX_RECALL_DAYS);
const conceptTags = deriveConceptTags({
path: item.path,
snippet: item.snippet
});
store.entries[key] = {
key,
path: item.path,
startLine: item.startLine,
endLine: item.endLine,
source: "memory",
snippet: item.snippet,
recallCount: Math.max(0, Math.floor(existing?.recallCount ?? 0)),
dailyCount: Math.max(0, Math.floor(existing?.dailyCount ?? 0)),
groundedCount,
totalScore,
maxScore,
firstRecalledAt: existing?.firstRecalledAt ?? nowIso,
lastRecalledAt: nowIso,
queryHashes,
recallDays,
conceptTags: conceptTags.length > 0 ? conceptTags : existing?.conceptTags ?? [],
claimHash,
...existing?.promotedAt ? { promotedAt: existing.promotedAt } : {}
};
}
store.updatedAt = nowIso;
await writeStore(workspaceDir, store);
});
}
async function recordDreamingPhaseSignals(params) {
const workspaceDir = params.workspaceDir?.trim();
if (!workspaceDir) return;
const keys = uniqueStrings(normalizeStringEntries(params.keys));
if (keys.length === 0) return;
const nowIso = resolveMemoryCoreTimestamp(resolveMemoryCoreNowMs(params.nowMs));
await withShortTermLock(workspaceDir, async () => {
const [store, phaseSignals] = await Promise.all([readStore(workspaceDir, nowIso), readPhaseSignalStore(workspaceDir, nowIso)]);
const knownKeys = new Set(Object.keys(store.entries));
for (const key of keys) {
if (!knownKeys.has(key)) continue;
const entry = phaseSignals.entries[key] ?? {
key,
lightHits: 0,
remHits: 0
};
if (params.phase === "light") {
entry.lightHits = Math.min(9999, entry.lightHits + 1);
entry.lastLightAt = nowIso;
} else {
entry.remHits = Math.min(9999, entry.remHits + 1);
entry.lastRemAt = nowIso;
}
phaseSignals.entries[key] = entry;
}
for (const [key, entry] of Object.entries(phaseSignals.entries)) if (!knownKeys.has(key) || entry.lightHits <= 0 && entry.remHits <= 0) delete phaseSignals.entries[key];
phaseSignals.updatedAt = nowIso;
await writePhaseSignalStore(workspaceDir, phaseSignals);
});
}
async function recordRemConsideredPhaseSignals(params) {
const workspaceDir = params.workspaceDir?.trim();
if (!workspaceDir) return;
const keys = uniqueStrings(normalizeStringEntries(params.keys));
if (keys.length === 0) return;
const nowIso = resolveMemoryCoreTimestamp(resolveMemoryCoreNowMs(params.nowMs));
await withShortTermLock(workspaceDir, async () => {
const [store, phaseSignals] = await Promise.all([readStore(workspaceDir, nowIso), readPhaseSignalStore(workspaceDir, nowIso)]);
const knownKeys = new Set(Object.keys(store.entries));
for (const key of keys) {
if (!knownKeys.has(key)) continue;
const entry = phaseSignals.entries[key] ?? {
key,
lightHits: 0,
remHits: 0
};
entry.lastRemConsideredAt = nowIso;
phaseSignals.entries[key] = entry;
}
for (const [key, entry] of Object.entries(phaseSignals.entries)) if (!knownKeys.has(key) || entry.lightHits <= 0 && entry.remHits <= 0) delete phaseSignals.entries[key];
phaseSignals.updatedAt = nowIso;
await writePhaseSignalStore(workspaceDir, phaseSignals);
});
}
async function readLightStagedKeys(params) {
const workspaceDir = params.workspaceDir?.trim();
if (!workspaceDir) return /* @__PURE__ */ new Set();
const store = await readPhaseSignalStore(workspaceDir, resolveMemoryCoreTimestamp(resolveMemoryCoreNowMs(params.nowMs)));
const keys = /* @__PURE__ */ new Set();
for (const [key, entry] of Object.entries(store.entries)) {
if (entry.lightHits <= 0) continue;
const lastLightMs = Date.parse(entry.lastLightAt ?? "");
const lastRemMs = Date.parse(entry.lastRemAt ?? "");
const lastRemConsideredMs = Date.parse(entry.lastRemConsideredAt ?? "");
const lastConsumedMs = Math.max(Number.isFinite(lastRemMs) ? lastRemMs : Number.NEGATIVE_INFINITY, Number.isFinite(lastRemConsideredMs) ? lastRemConsideredMs : Number.NEGATIVE_INFINITY);
if (Number.isFinite(lastLightMs) ? lastLightMs > lastConsumedMs : !entry.lastRemAt) keys.add(key);
}
return keys;
}
async function rankShortTermPromotionCandidates(options) {
const workspaceDir = options.workspaceDir.trim();
if (!workspaceDir) return [];
const nowMs = resolveMemoryCoreNowMs(options.nowMs);
const nowIso = resolveMemoryCoreTimestamp(nowMs);
const minScore = toFiniteScore(options.minScore, DEFAULT_PROMOTION_MIN_SCORE);
const minRecallCount = toFiniteNonNegativeInt(options.minRecallCount, 3);
const minUniqueQueries = toFiniteNonNegativeInt(options.minUniqueQueries, 2);
const maxAgeDays = toFiniteNonNegativeInt(options.maxAgeDays, -1);
const includePromoted = Boolean(options.includePromoted);
const halfLifeDays = toFinitePositive(options.recencyHalfLifeDays, DEFAULT_RECENCY_HALF_LIFE_DAYS);
const weights = normalizeWeights(options.weights);
const [store, phaseSignals] = await Promise.all([readStore(workspaceDir, nowIso), readPhaseSignalStore(workspaceDir, nowIso)]);
const candidates = [];
for (const entry of Object.values(store.entries)) {
if (!entry || entry.source !== "memory" || !isShortTermMemoryPath(entry.path)) continue;
if (isContaminatedDreamingSnippet(entry.snippet)) continue;
if (!includePromoted && entry.promotedAt) continue;
const recallCount = Math.max(0, Math.floor(entry.recallCount ?? 0));
const dailyCount = Math.max(0, Math.floor(entry.dailyCount ?? 0));
const groundedCount = Math.max(0, Math.floor(entry.groundedCount ?? 0));
const signalCount = totalSignalCountForEntry(entry);
if (signalCount <= 0) continue;
if (signalCount < minRecallCount) continue;
const avgScore = clampScore(entry.totalScore / Math.max(1, signalCount));
const frequency = clampScore(Math.log1p(signalCount) / Math.log1p(10));
const uniqueQueries = entry.queryHashes?.length ?? 0;
const contextDiversity = Math.max(uniqueQueries, entry.recallDays?.length ?? 0);
if (contextDiversity < minUniqueQueries) continue;
const diversity = clampScore(contextDiversity / 5);
const lastRecalledAtMs = Date.parse(entry.lastRecalledAt);
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 ?? [];
const consolidation = Math.max(calculateConsolidationComponent(recallDays), clampScore(groundedCount / 3));
const conceptual = calculateConceptualComponent(conceptTags);
const phaseBoost = calculatePhaseSignalBoost(phaseSignals.entries[entry.key], nowMs);
const score = weights.frequency * frequency + weights.relevance * avgScore + weights.diversity * diversity + weights.recency * recency + weights.consolidation * consolidation + weights.conceptual * conceptual + phaseBoost;
if (score < minScore) continue;
candidates.push({
key: entry.key,
path: entry.path,
startLine: entry.startLine,
endLine: entry.endLine,
source: entry.source,
snippet: entry.snippet,
recallCount,
dailyCount,
groundedCount,
signalCount,
avgScore,
maxScore: clampScore(entry.maxScore),
uniqueQueries,
...entry.claimHash ? { claimHash: entry.claimHash } : {},
promotedAt: entry.promotedAt,
firstRecalledAt: entry.firstRecalledAt,
lastRecalledAt: entry.lastRecalledAt,
ageDays,
score: clampScore(score),
recallDays,
conceptTags,
components: {
frequency,
relevance: avgScore,
diversity,
recency,
consolidation,
conceptual
}
});
}
const sorted = candidates.toSorted((a, b) => {
if (b.score !== a.score) return b.score - a.score;
if (b.recallCount !== a.recallCount) return b.recallCount - a.recallCount;
return a.path.localeCompare(b.path);
});
const limit = Number.isFinite(options.limit) ? Math.max(0, Math.floor(options.limit)) : sorted.length;
return sorted.slice(0, limit);
}
async function readShortTermRecallEntries(params) {
const workspaceDir = params.workspaceDir.trim();
if (!workspaceDir) return [];
const store = await readStore(workspaceDir, resolveMemoryCoreTimestamp(resolveMemoryCoreNowMs(params.nowMs)));
return Object.values(store.entries).filter((entry) => Boolean(entry) && entry.source === "memory" && isShortTermMemoryPath(entry.path));
}
function resolveShortTermSourcePathCandidates(workspaceDir, candidatePath) {
const normalizedPath = normalizeMemoryPath(candidatePath);
const basenames = [normalizedPath];
if (!normalizedPath.startsWith("memory/")) basenames.push(path.posix.join("memory", path.posix.basename(normalizedPath)));
const seen = /* @__PURE__ */ new Set();
const resolved = [];
for (const relativePath of basenames) {
const absolutePath = path.resolve(workspaceDir, relativePath);
if (seen.has(absolutePath)) continue;
seen.add(absolutePath);
resolved.push(absolutePath);
}
return resolved;
}
function normalizeRangeSnippet(lines, startLine, endLine) {
const startIndex = Math.max(0, startLine - 1);
const endIndex = Math.min(lines.length, endLine);
if (startIndex >= endIndex) return "";
return normalizeSnippet(lines.slice(startIndex, endIndex).join(" "));
}
function normalizeListMarkerFreeRangeSnippet(lines, startLine, endLine) {
const startIndex = Math.max(0, startLine - 1);
const endIndex = Math.min(lines.length, endLine);
if (startIndex >= endIndex) return "";
const strippedLines = lines.slice(startIndex, endIndex).map((line) => {
const trimmed = line.trim();
const withoutMarker = trimmed.replace(PROMOTION_LIST_MARKER_RE, "");
return {
text: withoutMarker,
hadListMarker: withoutMarker !== trimmed
};
});
const joiner = strippedLines.length > 1 && strippedLines.every((line) => line.hadListMarker) ? "; " : " ";
return normalizeSnippet(strippedLines.map((line) => line.text).join(joiner));
}
function normalizeDailyHeadingForPromotion(line) {
const normalized = normalizeSnippet(line.trim().match(/^#{1,6}\s+(.+)$/)?.[1]?.replace(PROMOTION_LIST_MARKER_RE, "").trim() ?? "");
if (!normalized || SHORT_TERM_BASENAME_RE.test(normalized) || isGenericDailyHeadingForPromotion(normalized)) return null;
return normalized;
}
function isGenericDailyHeadingForPromotion(heading) {
const normalized = heading.trim().replace(/\s+/g, " ");
const lower = normalized.toLowerCase();
if (MANAGED_DREAMING_HEADINGS.has(lower)) return true;
if (lower === "today" || lower === "yesterday" || lower === "tomorrow") return true;
if (lower === "morning" || lower === "afternoon" || lower === "evening" || lower === "night") return true;
return GENERIC_DAY_HEADING_RE.test(normalized);
}
function buildRelocatedDailyHeadingLookup(lines) {
const headings = Array.from({ length: lines.length + 1 }, () => null);
let currentHeading = null;
for (let index = 0; index < lines.length; index += 1) {
headings[index + 1] = currentHeading;
const line = lines[index] ?? "";
if (DREAMING_FENCE_START_RE.test(line) || DREAMING_FENCE_END_RE.test(line)) {
currentHeading = null;
continue;
}
if (/^#{1,6}\s+.+$/.test(line.trim())) currentHeading = normalizeDailyHeadingForPromotion(line);
}
return headings;
}
function buildListMarkerFreeMatchSnippet(heading, listMarkerFreeSnippet) {
if (!listMarkerFreeSnippet) return listMarkerFreeSnippet;
return heading ? normalizeSnippet(`${heading}: ${listMarkerFreeSnippet}`) : listMarkerFreeSnippet;
}
function targetSnippetHasHeadingContext(targetSnippet, bodySnippet) {
if (!targetSnippet || !bodySnippet || targetSnippet === bodySnippet) return false;
const bodyIndex = targetSnippet.indexOf(bodySnippet);
if (bodyIndex <= 0) return false;
return targetSnippet.slice(0, bodyIndex).trimEnd().endsWith(":");
}
function extractTargetHeadingBodySnippet(targetSnippet, bodySnippet) {
if (!targetSnippet || !bodySnippet || targetSnippet === bodySnippet) return null;
if (bodySnippet.startsWith(targetSnippet)) return null;
const normalizedBody = normalizeSnippet(bodySnippet);
for (let separatorIndex = targetSnippet.indexOf(": "); separatorIndex > 0;) {
const targetBody = normalizeSnippet(targetSnippet.slice(separatorIndex + 2));
if (targetBody && normalizedBody.startsWith(targetBody)) return targetBody;
separatorIndex = targetSnippet.indexOf(": ", separatorIndex + 2);
}
return null;
}
function compareCandidateWindow(targetSnippet, windowSnippet) {
if (!targetSnippet || !windowSnippet) return {
matched: false,
quality: 0
};
if (windowSnippet === targetSnippet) return {
matched: true,
quality: 3
};
if (windowSnippet.includes(targetSnippet)) return {
matched: true,
quality: 2
};
if (targetSnippet.includes(windowSnippet)) return {
matched: true,
quality: 1
};
return {
matched: false,
quality: 0
};
}
function relocateCandidateRange(lines, candidate) {
const targetSnippet = normalizeSnippet(candidate.snippet);
const preferredSpan = Math.max(1, candidate.endLine - candidate.startLine + 1);
if (targetSnippet.length === 0) {
const fallbackSnippet = normalizeRangeSnippet(lines, candidate.startLine, candidate.endLine);
if (!fallbackSnippet) return null;
return {
startLine: candidate.startLine,
endLine: candidate.endLine,
snippet: fallbackSnippet
};
}
const exactSnippet = normalizeRangeSnippet(lines, candidate.startLine, candidate.endLine);
if (exactSnippet === targetSnippet) return {
startLine: candidate.startLine,
endLine: candidate.endLine,
snippet: exactSnippet
};
const maxSpan = Math.min(lines.length, Math.max(preferredSpan + 3, 8));
const headingLookup = buildRelocatedDailyHeadingLookup(lines);
let bestMatch;
for (let startIndex = 0; startIndex < lines.length; startIndex += 1) for (let span = 1; span <= maxSpan && startIndex + span <= lines.length; span += 1) {
const startLine = startIndex + 1;
const endLine = startIndex + span;
const snippet = normalizeRangeSnippet(lines, startLine, endLine);
const comparison = compareCandidateWindow(targetSnippet, snippet);
const listMarkerFreeSnippet = normalizeListMarkerFreeRangeSnippet(lines, startLine, endLine);
const listMarkerFreeMatchSnippet = buildListMarkerFreeMatchSnippet(headingLookup[startLine] ?? null, listMarkerFreeSnippet);
const listMarkerFreeComparison = listMarkerFreeSnippet === snippet ? {
matched: false,
quality: 0
} : compareCandidateWindow(targetSnippet, listMarkerFreeSnippet);
const listMarkerFreeContextComparison = listMarkerFreeMatchSnippet === listMarkerFreeSnippet ? {
matched: false,
quality: 0
} : compareCandidateWindow(targetSnippet, listMarkerFreeMatchSnippet);
const targetHeadingBodySnippet = extractTargetHeadingBodySnippet(targetSnippet, listMarkerFreeSnippet);
const targetHeadingBodyComparison = targetHeadingBodySnippet && listMarkerFreeMatchSnippet !== listMarkerFreeSnippet ? compareCandidateWindow(targetHeadingBodySnippet, listMarkerFreeSnippet) : {
matched: false,
quality: 0
};
const useTargetHeadingBodyContext = targetHeadingBodyComparison.matched && targetHeadingBodyComparison.quality >= comparison.quality && targetHeadingBodyComparison.quality >= listMarkerFreeComparison.quality;
const useListMarkerFreeContext = !useTargetHeadingBodyContext && listMarkerFreeContextComparison.quality > comparison.quality && listMarkerFreeContextComparison.quality >= listMarkerFreeComparison.quality;
const useListMarkerFree = !useListMarkerFreeContext && listMarkerFreeComparison.quality > comparison.quality;
const bestComparison = useTargetHeadingBodyContext ? targetHeadingBodyComparison : useListMarkerFreeContext ? listMarkerFreeContextComparison : useListMarkerFree ? listMarkerFreeComparison : comparison;
if (!bestComparison.matched) continue;
const matchedSnippet = useTargetHeadingBodyContext || useListMarkerFreeContext ? listMarkerFreeMatchSnippet : useListMarkerFree ? targetSnippetHasHeadingContext(targetSnippet, listMarkerFreeSnippet) ? listMarkerFreeMatchSnippet : listMarkerFreeSnippet : snippet;
const distance = Math.abs(startLine - candidate.startLine);
if (!bestMatch || bestComparison.quality > bestMatch.quality || bestComparison.quality === bestMatch.quality && distance < bestMatch.distance || bestComparison.quality === bestMatch.quality && distance === bestMatch.distance && Math.abs(span - preferredSpan) < Math.abs(bestMatch.endLine - bestMatch.startLine + 1 - preferredSpan)) bestMatch = {
startLine,
endLine,
snippet: matchedSnippet,
quality: bestComparison.quality,
distance
};
}
if (!bestMatch) return null;
return {
startLine: bestMatch.startLine,
endLine: bestMatch.endLine,
snippet: bestMatch.snippet
};
}
const DREAMING_FENCE_START_RE = /<!--\s*openclaw:dreaming:[a-z][a-z0-9-]*:start\s*-->/i;
const DREAMING_FENCE_END_RE = /<!--\s*openclaw:dreaming:[a-z][a-z0-9-]*:end\s*-->/i;
function lineRangeOverlapsDreamingFence(lines, startLine, endLine) {
if (lines.length === 0) return false;
const safeStart = Math.max(1, Math.min(startLine, lines.length));
const safeEnd = Math.max(safeStart, Math.min(endLine, lines.length));
let insideFence = false;
for (let i = 0; i < safeEnd; i += 1) {
const line = lines[i] ?? "";
if (DREAMING_FENCE_START_RE.test(line)) {
insideFence = true;
continue;
}
if (DREAMING_FENCE_END_RE.test(line)) {
insideFence = false;
continue;
}
const oneIndexed = i + 1;
if (insideFence && oneIndexed >= safeStart && oneIndexed <= safeEnd) return true;
}
return false;
}
async function rehydratePromotionCandidate(workspaceDir, candidate) {
const sourcePaths = resolveShortTermSourcePathCandidates(workspaceDir, candidate.path);
for (const sourcePath of sourcePaths) {
let rawSource;
try {
rawSource = await fs.readFile(sourcePath, "utf-8");
} catch (err) {
if (err?.code === "ENOENT") continue;
throw err;
}
const lines = rawSource.split(/\r?\n/);
const relocated = relocateCandidateRange(lines, candidate);
if (!relocated) continue;
if (lineRangeOverlapsDreamingFence(lines, relocated.startLine, relocated.endLine)) continue;
return {
...candidate,
startLine: relocated.startLine,
endLine: relocated.endLine,
snippet: relocated.snippet
};
}
return null;
}
function buildPromotionSection(candidates, nowMs, timezone, maxPromotedSnippetTokens = 160) {
const lines = [
"",
`## Promoted From Short-Term Memory (${formatMemoryDreamingDay(nowMs, timezone)})`,
""
];
for (const candidate of candidates) {
const source = `${candidate.path}:${candidate.startLine}-${candidate.endLine}`;
const metadata = `[score=${candidate.score.toFixed(3)} recalls=${candidate.recallCount} avg=${candidate.avgScore.toFixed(3)} source=${source}]`;
lines.push(`<!-- ${PROMOTION_MARKER_PREFIX}${candidate.key} -->`);
lines.push(`- ${formatPromotedSnippetForMemory(candidate.snippet, maxPromotedSnippetTokens)} ${metadata}`);
}
lines.push("");
return lines.join("\n");
}
function resolvePromotedSnippetCharLimit(maxTokens) {
return toFiniteNonNegativeInt(maxTokens, 160) * PROMOTED_SNIPPET_CHARS_PER_TOKEN_ESTIMATE;
}
function truncatePromotedSnippet(snippet, maxTokens) {
const limit = resolvePromotedSnippetCharLimit(maxTokens);
if (limit === 0 || snippet.length <= limit) return snippet;
const hardLimit = snippet.slice(0, limit);
const sentenceBoundary = Math.max(hardLimit.lastIndexOf(". "), hardLimit.lastIndexOf("! "), hardLimit.lastIndexOf("? "));
const wordBoundary = hardLimit.lastIndexOf(" ");
const cutAt = sentenceBoundary >= Math.floor(limit * .55) ? sentenceBoundary + 1 : wordBoundary >= Math.floor(limit * .65) ? wordBoundary : limit;
return `${hardLimit.slice(0, cutAt).trimEnd()}...`;
}
function formatPromotedSnippetForMemory(rawSnippet, maxTokens) {
return truncatePromotedSnippet(normalizeSnippet(rawSnippet || "(no snippet captured)").replace(/^[-*+] +/, "").trim() || "(no snippet captured)", maxTokens);
}
function withTrailingNewline(content) {
if (!content) return "";
return content.endsWith("\n") ? content : `${content}\n`;
}
function extractPromotionMarkers(memoryText) {
const markers = /* @__PURE__ */ new Set();
const matches = memoryText.matchAll(/<!--\s*openclaw-memory-promotion:([^\n]*?)\s*-->/gi);
for (const match of matches) {
const key = match[1]?.trim();
if (key) markers.add(key);
}
return markers;
}
async function applyShortTermPromotions(options) {
const workspaceDir = options.workspaceDir.trim();
const nowMs = resolveMemoryCoreNowMs(options.nowMs);
const nowIso = resolveMemoryCoreTimestamp(nowMs);
const limit = Number.isFinite(options.limit) ? Math.max(0, Math.floor(options.limit)) : options.candidates.length;
const minScore = toFiniteScore(options.minScore, DEFAULT_PROMOTION_MIN_SCORE);
const minRecallCount = toFiniteNonNegativeInt(options.minRecallCount, 3);
const minUniqueQueries = toFiniteNonNegativeInt(options.minUniqueQueries, 2);
const maxAgeDays = toFiniteNonNegativeInt(options.maxAgeDays, -1);
const memoryPath = path.join(workspaceDir, "MEMORY.md");
return await withShortTermLock(workspaceDir, async () => {
const store = await readStore(workspaceDir, nowIso);
const selected = options.candidates.filter((candidate) => {
if (isContaminatedDreamingSnippet(candidate.snippet)) return false;
if (candidate.promotedAt) return false;
if (candidate.score < minScore) return false;
if (Math.max(0, candidate.signalCount ?? totalSignalCountForEntry({
recallCount: candidate.recallCount,
dailyCount: candidate.dailyCount,
groundedCount: candidate.groundedCount
})) < minRecallCount) return false;
if (Math.max(candidate.uniqueQueries, candidate.recallDays.length) < minUniqueQueries) return false;
if (maxAgeDays >= 0 && candidate.ageDays > maxAgeDays) return false;
if (store.entries[candidate.key]?.promotedAt) return false;
return true;
}).slice(0, limit);
const rehydratedSelected = [];
for (const candidate of selected) {
const rehydrated = await rehydratePromotionCandidate(workspaceDir, candidate);
if (rehydrated && !isContaminatedDreamingSnippet(rehydrated.snippet)) rehydratedSelected.push(rehydrated);
}
if (rehydratedSelected.length === 0) return {
memoryPath,
applied: 0,
appended: 0,
reconciledExisting: 0,
appliedCandidates: [],
compactedSections: 0,
compactedDates: []
};
const existingMemory = await fs.readFile(memoryPath, "utf-8").catch((err) => {
if (err?.code === "ENOENT") return "";
throw err;
});
const existingMarkers = extractPromotionMarkers(existingMemory);
const alreadyWritten = rehydratedSelected.filter((candidate) => existingMarkers.has(candidate.key));
const toAppend = rehydratedSelected.filter((candidate) => !existingMarkers.has(candidate.key));
let compactedDates = [];
if (toAppend.length > 0) {
const section = buildPromotionSection(toAppend, nowMs, options.timezone, options.maxPromotedSnippetTokens);
const compaction = compactMemoryForBudget({
existingMemory,
newSection: section,
budgetChars: typeof options.memoryFileMaxChars === "number" && Number.isFinite(options.memoryFileMaxChars) ? Math.max(0, Math.floor(options.memoryFileMaxChars)) : DEFAULT_MEMORY_FILE_MAX_CHARS
});
compactedDates = compaction.droppedDates;
const baseMemory = compaction.compacted;
const header = baseMemory.trim().length > 0 ? "" : "# Long-Term Memory\n\n";
await fs.writeFile(memoryPath, `${header}${withTrailingNewline(baseMemory)}${section}`, "utf-8");
}
for (const candidate of rehydratedSelected) {
const entry = store.entries[candidate.key];
if (!entry) continue;
entry.startLine = candidate.startLine;
entry.endLine = candidate.endLine;
entry.snippet = candidate.snippet;
entry.promotedAt = nowIso;
}
store.updatedAt = nowIso;
await writeStore(workspaceDir, store);
await appendMemoryHostEvent(workspaceDir, {
type: "memory.promotion.applied",
timestamp: nowIso,
memoryPath,
applied: rehydratedSelected.length,
candidates: rehydratedSelected.map((candidate) => ({
key: candidate.key,
path: candidate.path,
startLine: candidate.startLine,
endLine: candidate.endLine,
score: candidate.score,
recallCount: candidate.recallCount
}))
});
return {
memoryPath,
applied: rehydratedSelected.length,
appended: toAppend.length,
reconciledExisting: alreadyWritten.length,
appliedCandidates: rehydratedSelected,
compactedSections: compactedDates.length,
compactedDates
};
});
}
function resolveShortTermRecallStorePath(workspaceDir) {
return resolveStorePath(workspaceDir);
}
function resolveShortTermPhaseSignalStorePath(workspaceDir) {
return resolvePhaseSignalPath(workspaceDir);
}
function resolveShortTermRecallLockPath(workspaceDir) {
return resolveLockPath(workspaceDir);
}
async function auditShortTermPromotionArtifacts(params) {
const workspaceDir = params.workspaceDir.trim();
const storePath = resolveStorePath(workspaceDir);
const lockPath = resolveLockPath(workspaceDir);
const issues = [];
let entryCount = 0;
let promotedCount = 0;
let spacedEntryCount = 0;
let conceptTaggedEntryCount = 0;
let conceptTagScripts;
let invalidEntryCount = 0;
let updatedAt;
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
const rawEntries = await readMemoryCoreWorkspaceEntries({
namespace: SHORT_TERM_RECALL_NAMESPACE,
workspaceDir
});
const exists = rawEntries.length > 0;
if (exists) {
const store = normalizeShortTermRecallStore({
version: 1,
updatedAt: nowIso,
entries: Object.fromEntries(rawEntries.map((entry) => [entry.key, entry.value]))
}, nowIso);
const normalizedEntryCount = Object.keys(store.entries).length;
updatedAt = store.updatedAt;
entryCount = normalizedEntryCount;
promotedCount = Object.values(store.entries).filter((entry) => Boolean(entry.promotedAt)).length;
spacedEntryCount = Object.values(store.entries).filter((entry) => (entry.recallDays?.length ?? 0) > 1).length;
conceptTaggedEntryCount = Object.values(store.entries).filter((entry) => (entry.conceptTags?.length ?? 0) > 0).length;
conceptTagScripts = summarizeConceptTagScriptCoverage(Object.values(store.entries).filter((entry) => (entry.conceptTags?.length ?? 0) > 0).map((entry) => entry.conceptTags ?? []));
invalidEntryCount = rawEntries.length - entryCount;
if (invalidEntryCount > 0) issues.push({
severity: "warn",
code: "recall-store-invalid",
message: `Short-term recall store contains ${invalidEntryCount} invalid entr${invalidEntryCount === 1 ? "y" : "ies"}.`,
fixable: true
});
if (normalizedEntryCount > SHORT_TERM_RECALL_MAX_ENTRIES) issues.push({
severity: "warn",
code: "recall-store-over-limit",
message: `Short-term recall store contains ${normalizedEntryCount} entries; only the newest ${SHORT_TERM_RECALL_MAX_ENTRIES} are kept at runtime.`,
fixable: true
});
}
const lockKey = memoryCoreWorkspaceStateKey(workspaceDir);
const lockEntry = await openMemoryCoreStateStore({
namespace: SHORT_TERM_LOCK_NAMESPACE,
maxEntries: SHORT_TERM_LOCK_MAX_ENTRIES
}).lookup(lockKey);
if (lockEntry) {
const ageMs = Date.now() - lockEntry.acquiredAt;
const ownerPid = parseLockOwnerPid(lockEntry.owner);
if (ageMs > SHORT_TERM_LOCK_STALE_MS && (ownerPid === null || !isProcessLikelyAlive(ownerPid))) issues.push({
severity: "warn",
code: "recall-lock-stale",
message: "Short-term promotion lock appears stale.",
fixable: true
});
}
let qmd;
if (params.qmd) {
qmd = {
dbPath: params.qmd.dbPath,
collections: params.qmd.collections
};
if (typeof params.qmd.collections === "number" && params.qmd.collections <= 0) issues.push({
severity: "warn",
code: "qmd-collections-empty",
message: "QMD reports zero managed collections.",
fixable: false
});
const dbPath = params.qmd.dbPath?.trim();
if (dbPath) try {
const stat = await fs.stat(dbPath);
qmd.dbBytes = stat.size;
if (!stat.isFile() || stat.size <= 0) issues.push({
severity: "error",
code: "qmd-index-empty",
message: "QMD index file exists but is empty.",
fixable: false
});
} catch (err) {
if (err.code === "ENOENT") issues.push({
severity: "error",
code: "qmd-index-missing",
message: "QMD index file is missing.",
fixable: false
});
else throw err;
}
}
return {
storePath,
lockPath,
updatedAt,
exists,
entryCount,
promotedCount,
spacedEntryCount,
conceptTaggedEntryCount,
...conceptTagScripts ? { conceptTagScripts } : {},
invalidEntryCount,
issues,
...qmd ? { qmd } : {}
};
}
async function repairShortTermPromotionArtifacts(params) {
const workspaceDir = params.workspaceDir.trim();
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
let rewroteStore = false;
let removedInvalidEntries = 0;
let removedOverflowEntries = 0;
let removedStaleLock = false;
const lockKey = memoryCoreWorkspaceStateKey(workspaceDir);
const lockStore = openMemoryCoreStateStore({
namespace: SHORT_TERM_LOCK_NAMESPACE,
maxEntries: SHORT_TERM_LOCK_MAX_ENTRIES
});
const lockEntry = await lockStore.lookup(lockKey);
if (lockEntry && Date.now() - lockEntry.acquiredAt > SHORT_TERM_LOCK_STALE_MS) {
const ownerPid = parseLockOwnerPid(lockEntry.owner);
if (ownerPid === null || !isProcessLikelyAlive(ownerPid)) removedStaleLock = await lockStore.delete(lockKey);
}
await withShortTermLock(workspaceDir, async () => {
const rawEntries = await readMemoryCoreWorkspaceEntries({
namespace: SHORT_TERM_RECALL_NAMESPACE,
workspaceDir
});
if (rawEntries.length > 0) {
const normalized = normalizeShortTermRecallStore({
version: 1,
updatedAt: nowIso,
entries: Object.fromEntries(rawEntries.map((entry) => [entry.key, entry.value]))
}, nowIso);
removedInvalidEntries = Math.max(0, rawEntries.length - Object.keys(normalized.entries).length);
const nextEntries = Object.fromEntries(Object.entries(normalized.entries).map(([key, entry]) => {
const conceptTags = deriveConceptTags({
path: entry.path,
snippet: entry.snippet
});
const fallbackDay = normalizeIsoDay(entry.lastRecalledAt) ?? nowIso.slice(0, 10);
return [key, {
...entry,
dailyCount: Math.max(0, Math.floor(entry.dailyCount ?? 0)),
groundedCount: Math.max(0, Math.floor(entry.groundedCount ?? 0)),
queryHashes: (entry.queryHashes ?? []).slice(-32),
recallDays: mergeRecentDistinct(entry.recallDays ?? [], fallbackDay, MAX_RECALL_DAYS),
conceptTags: conceptTags.length > 0 ? conceptTags : entry.conceptTags ?? []
}];
}));
const comparableStore = {
version: 1,
updatedAt: normalized.updatedAt,
entries: nextEntries
};
removedOverflowEntries = enforceShortTermRecallStoreRetention(comparableStore);
if (removedInvalidEntries > 0 || removedOverflowEntries > 0 || JSON.stringify(normalized.entries) !== JSON.stringify(comparableStore.entries)) {
await writeStore(workspaceDir, {
...comparableStore,
updatedAt: nowIso
});
rewroteStore = true;
}
}
});
return {
changed: rewroteStore || removedStaleLock,
removedInvalidEntries,
removedOverflowEntries,
rewroteStore,
removedStaleLock
};
}
async function removeGroundedShortTermCandidates(params) {
const workspaceDir = params.workspaceDir.trim();
const storePath = resolveStorePath(workspaceDir);
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
let removed = 0;
await withShortTermLock(workspaceDir, async () => {
const [store, phaseSignals] = await Promise.all([readStore(workspaceDir, nowIso), readPhaseSignalStore(workspaceDir, nowIso)]);
for (const [key, entry] of Object.entries(store.entries)) if (Math.max(0, Math.floor(entry.groundedCount ?? 0)) > 0 && Math.max(0, Math.floor(entry.recallCount ?? 0)) === 0 && Math.max(0, Math.floor(entry.dailyCount ?? 0)) === 0) {
delete store.entries[key];
removed += 1;
}
for (const key of Object.keys(phaseSignals.entries)) if (!Object.hasOwn(store.entries, key)) delete phaseSignals.entries[key];
if (removed > 0) {
store.updatedAt = nowIso;
phaseSignals.updatedAt = nowIso;
await Promise.all([writeStore(workspaceDir, store), writePhaseSignalStore(workspaceDir, phaseSignals)]);
}
});
return {
removed,
storePath
};
}
const testing = {
parseLockOwnerPid,
canStealStaleLock,
isProcessLikelyAlive,
readRecallStore: readStore,
readPhaseSignalStore,
writeRawRecallStore: async (workspaceDir, raw) => {
const record = asNullableRecord(raw);
const entries = asNullableRecord(record?.entries);
await Promise.all([writeMemoryCoreWorkspaceEntries({
namespace: SHORT_TERM_RECALL_NAMESPACE,
workspaceDir,
entries: entries ? Object.entries(entries).map(([key, value]) => ({
key,
value
})) : []
}), writeMemoryCoreWorkspaceEntry({
namespace: SHORT_TERM_META_NAMESPACE,
workspaceDir,
key: "recall",
value: { updatedAt: typeof record?.updatedAt === "string" && record.updatedAt.trim() ? record.updatedAt : (/* @__PURE__ */ new Date()).toISOString() }
})]);
},
writeRawPhaseSignalStore: async (workspaceDir, raw) => {
const record = asNullableRecord(raw);
const entries = asNullableRecord(record?.entries);
await Promise.all([writeMemoryCoreWorkspaceEntries({
namespace: SHORT_TERM_PHASE_SIGNAL_NAMESPACE,
workspaceDir,
entries: entries ? Object.entries(entries).map(([key, value]) => ({
key,
value
})) : []
}), writeMemoryCoreWorkspaceEntry({
namespace: SHORT_TERM_META_NAMESPACE,
workspaceDir,
key: "phase",
value: { updatedAt: typeof record?.updatedAt === "string" && record.updatedAt.trim() ? record.updatedAt : (/* @__PURE__ */ new Date()).toISOString() }
})]);
},
writeShortTermLock: async (workspaceDir, entry) => {
await openMemoryCoreStateStore({
namespace: SHORT_TERM_LOCK_NAMESPACE,
maxEntries: SHORT_TERM_LOCK_MAX_ENTRIES
}).register(memoryCoreWorkspaceStateKey(workspaceDir), entry);
},
deleteShortTermLock: async (workspaceDir) => {
await openMemoryCoreStateStore({
namespace: SHORT_TERM_LOCK_NAMESPACE,
maxEntries: SHORT_TERM_LOCK_MAX_ENTRIES
}).delete(memoryCoreWorkspaceStateKey(workspaceDir));
},
deriveConceptTags,
calculateConsolidationComponent,
calculatePhaseSignalBoost,
buildClaimHash,
totalSignalCountForEntry,
isContaminatedDreamingSnippet,
lineRangeOverlapsDreamingFence,
SHORT_TERM_RECALL_MAX_ENTRIES,
SHORT_TERM_RECALL_MAX_SNIPPET_CHARS
};
//#endregion
export { resolveShortTermPhaseSignalStorePath as C, testing as E, repairShortTermPromotionArtifacts as S, resolveShortTermRecallStorePath as T, recordDreamingPhaseSignals as _, SHORT_TERM_PHASE_SIGNAL_RELATIVE_PATH as a, recordShortTermRecalls as b, auditShortTermPromotionArtifacts as c, loadShortTermPromotionDreamingStats as d, normalizeShortTermPhaseSignalStore as f, readShortTermRecallEntries as g, readLightStagedKeys as h, SHORT_TERM_LOCK_RELATIVE_PATH as i, filterLiveShortTermRecallEntries as l, rankShortTermPromotionCandidates as m, DEFAULT_PROMOTION_MIN_SCORE as n, SHORT_TERM_STORE_RELATIVE_PATH as o, normalizeShortTermRecallStore as p, DEFAULT_PROMOTION_MIN_UNIQUE_QUERIES as r, applyShortTermPromotions as s, DEFAULT_PROMOTION_MIN_RECALL_COUNT as t, isShortTermMemoryPath as u, recordGroundedShortTermCandidates as v, resolveShortTermRecallLockPath as w, removeGroundedShortTermCandidates as x, recordRemConsideredPhaseSignals as y };