224 lines
7.9 KiB
JavaScript
224 lines
7.9 KiB
JavaScript
import {
|
|
MAX_RECENT_ANALYTIC_THREADS,
|
|
MAX_RECENT_EPISODIC_RECORDS,
|
|
MAX_RECENT_QUIET_ANALYTIC_THREADS,
|
|
MAX_RECENT_QUIET_EPISODIC_RECORDS,
|
|
} from "./void-memory-organ-constants.mjs";
|
|
import {
|
|
ensureArray,
|
|
ensureMemoryId,
|
|
ensureObject,
|
|
ensureStringArray,
|
|
isLowSignalQuietRoomText,
|
|
isObject,
|
|
normalizeText,
|
|
readString,
|
|
selectRecentRecords,
|
|
} from "./void-memory-organ-utils.mjs";
|
|
|
|
export function collectMemoryRecords({ memories, runtime }) {
|
|
const records = [];
|
|
|
|
pushArrayRecords(records, selectRecentRecords(ensureArray(memories.episodic), {
|
|
limit: MAX_RECENT_EPISODIC_RECORDS,
|
|
quietLimit: MAX_RECENT_QUIET_EPISODIC_RECORDS,
|
|
}), "episodic", (entry) => ({
|
|
text: readString(entry, "summary"),
|
|
label: readString(entry, "summary") ?? "episodic memory",
|
|
timestamp: readString(entry, "timestamp"),
|
|
sourceRefs: gatherSourceRefs(entry),
|
|
sourceMeta: gatherSourceMeta({ entry, kind: "episodic" }),
|
|
}));
|
|
pushArrayRecords(records, ensureArray(memories.semantic).slice(-24), "semantic", (entry) => ({
|
|
text: [readString(entry, "subjectLabel"), readString(entry, "kind"), readString(entry, "summary")]
|
|
.filter(Boolean)
|
|
.join(" | "),
|
|
label: readString(entry, "subjectLabel") ?? readString(entry, "kind") ?? "semantic memory",
|
|
timestamp: readString(entry, "lastObservedAt"),
|
|
sourceRefs: gatherSourceRefs(entry),
|
|
sourceMeta: gatherSourceMeta({ entry, kind: "semantic" }),
|
|
}));
|
|
pushArrayRecords(records, ensureArray(memories.musings).slice(-12), "musing", (entry) => ({
|
|
text: [readString(entry, "topic"), readString(entry, "summary")].filter(Boolean).join(" | "),
|
|
label: readString(entry, "topic") ?? "musing",
|
|
timestamp: readString(entry, "timestamp"),
|
|
sourceRefs: gatherSourceRefs(entry),
|
|
sourceMeta: gatherSourceMeta({ entry, kind: "musing" }),
|
|
}));
|
|
pushArrayRecords(records, ensureArray(memories.dreams).slice(-10), "dream", (entry) => ({
|
|
text: [readString(entry, "theme"), readString(entry, "summary")].filter(Boolean).join(" | "),
|
|
label: readString(entry, "theme") ?? "dream",
|
|
timestamp: readString(entry, "timestamp"),
|
|
sourceRefs: gatherSourceRefs(entry),
|
|
sourceMeta: gatherSourceMeta({ entry, kind: "dream" }),
|
|
}));
|
|
pushArrayRecords(records, ensureArray(runtime.recent_archive_excursions).slice(-8), "archive_excursion", (entry) => ({
|
|
text: [readString(entry, "topicHint"), readString(entry, "whyItWasFresh")].filter(Boolean).join(" | "),
|
|
label: readString(entry, "topicHint") ?? "archive excursion",
|
|
timestamp: readString(entry, "timestamp"),
|
|
sourceRefs: gatherSourceRefs(entry),
|
|
sourceMeta: gatherSourceMeta({ entry, kind: "archive_excursion" }),
|
|
}));
|
|
pushArrayRecords(records, ensureArray(runtime.recent_repo_activity_sweeps).slice(-8), "repo_sweep", (entry) => ({
|
|
text: [readString(entry, "summary"), readString(entry, "whyItMattered")].filter(Boolean).join(" | "),
|
|
label: readString(entry, "summary") ?? "repo sweep",
|
|
timestamp: readString(entry, "timestamp"),
|
|
sourceRefs: gatherSourceRefs(entry),
|
|
sourceMeta: gatherSourceMeta({ entry, kind: "repo_sweep" }),
|
|
}));
|
|
pushArrayRecords(
|
|
records,
|
|
selectRecentRecords(ensureArray(ensureObject(runtime.thought_lanes).analytic?.active_threads), {
|
|
limit: MAX_RECENT_ANALYTIC_THREADS,
|
|
quietLimit: MAX_RECENT_QUIET_ANALYTIC_THREADS,
|
|
}),
|
|
"analytic_thread",
|
|
(entry) => ({
|
|
text: [readString(entry, "topic"), readString(entry, "claim"), readString(entry, "counterweight")]
|
|
.filter(Boolean)
|
|
.join(" | "),
|
|
label: readString(entry, "topic") ?? "analytic thread",
|
|
timestamp: readString(entry, "lastTouchedAt"),
|
|
sourceRefs: gatherSourceRefs(entry),
|
|
sourceMeta: gatherSourceMeta({ entry, kind: "analytic_thread" }),
|
|
}),
|
|
);
|
|
pushArrayRecords(
|
|
records,
|
|
ensureArray(ensureObject(runtime.thought_lanes).associative?.active_threads).slice(-6),
|
|
"associative_thread",
|
|
(entry) => ({
|
|
text: [readString(entry, "topic"), readString(entry, "claim"), readString(entry, "counterweight")]
|
|
.filter(Boolean)
|
|
.join(" | "),
|
|
label: readString(entry, "topic") ?? "associative thread",
|
|
timestamp: readString(entry, "lastTouchedAt"),
|
|
sourceRefs: gatherSourceRefs(entry),
|
|
sourceMeta: gatherSourceMeta({ entry, kind: "associative_thread" }),
|
|
}),
|
|
);
|
|
pushArrayRecords(records, ensureArray(ensureObject(runtime.bridge).recent_syntheses).slice(-8), "bridge_synthesis", (entry) => ({
|
|
text: [readString(entry, "summary"), ...ensureStringArray(entry.dominantTopics)].filter(Boolean).join(" | "),
|
|
label: readString(entry, "summary") ?? "bridge synthesis",
|
|
timestamp: readString(entry, "timestamp"),
|
|
sourceRefs: gatherSourceRefs(entry),
|
|
sourceMeta: gatherSourceMeta({ entry, kind: "bridge_synthesis" }),
|
|
}));
|
|
pushArrayRecords(records, ensureArray(runtime.candidate_interventions).slice(-8), "candidate_intervention", (entry) => ({
|
|
text: [readString(entry, "summary"), readString(entry, "draft")].filter(Boolean).join(" | "),
|
|
label: readString(entry, "summary") ?? "candidate intervention",
|
|
timestamp: readString(entry, "timestamp"),
|
|
sourceRefs: gatherSourceRefs(entry),
|
|
sourceMeta: gatherSourceMeta({ entry, kind: "candidate_intervention" }),
|
|
}));
|
|
|
|
return records;
|
|
}
|
|
|
|
function pushArrayRecords(target, entries, kind, toRecord) {
|
|
for (const entry of entries) {
|
|
if (!isObject(entry)) {
|
|
continue;
|
|
}
|
|
|
|
const record = toRecord(entry);
|
|
const text = normalizeText(record.text);
|
|
|
|
if (!text) {
|
|
continue;
|
|
}
|
|
|
|
entry.memoryId = ensureMemoryId(entry, kind, text);
|
|
target.push({
|
|
kind,
|
|
entry,
|
|
memoryId: entry.memoryId,
|
|
text,
|
|
label: record.label ?? kind,
|
|
timestamp: record.timestamp ?? null,
|
|
sourceRefs: ensureStringArray(record.sourceRefs),
|
|
sourceMeta: isObject(record.sourceMeta) ? record.sourceMeta : {},
|
|
lowSignalQuietRoom: isLowSignalQuietRoomText(text),
|
|
});
|
|
}
|
|
}
|
|
|
|
function gatherSourceRefs(entry) {
|
|
if (!isObject(entry)) {
|
|
return [];
|
|
}
|
|
return [
|
|
...ensureStringArray(entry.evidenceRefs),
|
|
...ensureStringArray(entry.evidenceMessageIds),
|
|
...ensureStringArray(entry.distilledFrom),
|
|
...ensureStringArray(entry.sourceMemoryIds),
|
|
];
|
|
}
|
|
|
|
function gatherSourceMeta({ entry, kind }) {
|
|
const sourceMeta = {};
|
|
const repoNames = [
|
|
...new Set([
|
|
...ensureStringArray(entry.repoNames),
|
|
...inferRepoNamesFromEntry(entry),
|
|
]),
|
|
];
|
|
if (repoNames.length > 0) {
|
|
sourceMeta.repoNames = repoNames;
|
|
}
|
|
|
|
const channelId = readString(entry, "channelId");
|
|
if (channelId) {
|
|
sourceMeta.channelId = channelId;
|
|
}
|
|
|
|
if (kind === "archive_excursion") {
|
|
const archiveTimestamp =
|
|
readString(entry, "anchorTimestamp") ??
|
|
readString(entry, "sourceTimestamp") ??
|
|
readString(entry, "timestamp");
|
|
const archiveYear = extractYear(archiveTimestamp);
|
|
if (archiveYear) {
|
|
sourceMeta.archiveYear = archiveYear;
|
|
}
|
|
}
|
|
|
|
if (kind === "bridge_synthesis") {
|
|
const dominantTopics = ensureStringArray(entry.dominantTopics);
|
|
if (dominantTopics.length > 0) {
|
|
sourceMeta.dominantTopics = dominantTopics;
|
|
}
|
|
}
|
|
|
|
return sourceMeta;
|
|
}
|
|
|
|
function inferRepoNamesFromEntry(entry) {
|
|
const subjectLabel =
|
|
readString(entry, "subjectLabel") ??
|
|
readString(entry, "topic") ??
|
|
readString(entry, "label") ??
|
|
readString(entry, "fascinationTarget");
|
|
const repoName = extractRepoNameFromLabel(subjectLabel);
|
|
return repoName ? [repoName] : [];
|
|
}
|
|
|
|
export function extractYear(timestamp) {
|
|
if (typeof timestamp !== "string" || timestamp.length < 4) {
|
|
return undefined;
|
|
}
|
|
const match = timestamp.match(/^(\d{4})-/);
|
|
return match ? match[1] : undefined;
|
|
}
|
|
|
|
function extractRepoNameFromLabel(label) {
|
|
const normalized = normalizeText(label);
|
|
if (!normalized || !normalized.includes(":")) {
|
|
return null;
|
|
}
|
|
const [prefix] = normalized.split(/:\s*/, 1);
|
|
if (!prefix || !/[A-Z]/.test(prefix)) {
|
|
return null;
|
|
}
|
|
return prefix;
|
|
}
|