fix: rank receipt-backed memory first
This commit is contained in:
parent
975ef28b15
commit
3301a6be1e
|
|
@ -0,0 +1,58 @@
|
|||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { StateStore } from "../core/state-store.js";
|
||||
import { PersistentMemory } from "./persistent-memory.js";
|
||||
|
||||
function tmpStore() {
|
||||
return new StateStore(fs.mkdtempSync(path.join(os.tmpdir(), "fable-memory-")));
|
||||
}
|
||||
|
||||
describe("persistent memory provenance", () => {
|
||||
it("ranks receipt-backed verified memories before stale unverified memory", () => {
|
||||
const memory = new PersistentMemory(tmpStore());
|
||||
memory.recordEpisode({
|
||||
sessionId: "old",
|
||||
task: "factory containers",
|
||||
summary: "stale memory says 25 containers",
|
||||
iterations: 1,
|
||||
finalQuality: 0.5,
|
||||
keyEvents: [],
|
||||
tags: ["factory"],
|
||||
observed_at: "2026-06-18T00:00:00.000Z",
|
||||
verification: "unverified",
|
||||
});
|
||||
memory.recordEpisode({
|
||||
sessionId: "new",
|
||||
task: "factory containers",
|
||||
summary: "live receipt says 16 containers",
|
||||
iterations: 1,
|
||||
finalQuality: 1,
|
||||
keyEvents: [],
|
||||
tags: ["factory"],
|
||||
observed_at: "2026-06-26T00:00:00.000Z",
|
||||
verification: "verified",
|
||||
receipt_id: "verify-cycle:live",
|
||||
});
|
||||
|
||||
const [first] = memory.queryEpisodic({ tags: ["factory"] });
|
||||
expect(first.summary).toContain("16 containers");
|
||||
expect(first.receipt_id).toBe("verify-cycle:live");
|
||||
});
|
||||
|
||||
it("filters superseded memories from recall", () => {
|
||||
const memory = new PersistentMemory(tmpStore());
|
||||
memory.recordEpisode({
|
||||
sessionId: "old",
|
||||
task: "deploy route",
|
||||
summary: "8098 is deploy authority",
|
||||
iterations: 1,
|
||||
finalQuality: 0.1,
|
||||
keyEvents: [],
|
||||
tags: ["deploy"],
|
||||
status: "superseded",
|
||||
});
|
||||
expect(memory.queryEpisodic({ tags: ["deploy"] })).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
@ -17,7 +17,7 @@ import type { LoopIteration, StateEntry } from "../core/types.js";
|
|||
|
||||
// ── Types ──────────────────────────────────────────────────
|
||||
|
||||
export interface EpisodicMemory {
|
||||
export interface EpisodicMemory extends MemoryProvenance {
|
||||
/** Unique episode identifier */
|
||||
episodeId: string;
|
||||
sessionId: string;
|
||||
|
|
@ -37,7 +37,16 @@ export interface MemoryEvent {
|
|||
importance: number; // 0-1, for retrieval weighting
|
||||
}
|
||||
|
||||
export interface SemanticMemory {
|
||||
export interface MemoryProvenance {
|
||||
observed_at?: string;
|
||||
source?: string;
|
||||
verification?: "verified" | "unverified" | "superseded";
|
||||
receipt_id?: string;
|
||||
supersedes_id?: string;
|
||||
status?: "active" | "superseded";
|
||||
}
|
||||
|
||||
export interface SemanticMemory extends MemoryProvenance {
|
||||
id: string;
|
||||
concept: string;
|
||||
content: string;
|
||||
|
|
@ -48,7 +57,7 @@ export interface SemanticMemory {
|
|||
accessCount: number;
|
||||
}
|
||||
|
||||
export interface ProceduralMemory {
|
||||
export interface ProceduralMemory extends MemoryProvenance {
|
||||
id: string;
|
||||
skillName: string;
|
||||
steps: string[];
|
||||
|
|
@ -100,9 +109,13 @@ export class PersistentMemory {
|
|||
|
||||
/** Record an episode (a completed session or significant subtask) */
|
||||
recordEpisode(episode: Omit<EpisodicMemory, "episodeId" | "timestamp">): EpisodicMemory {
|
||||
const timestamp = new Date().toISOString();
|
||||
const full: EpisodicMemory = {
|
||||
episodeId: StateStore.uid(),
|
||||
timestamp: new Date().toISOString(),
|
||||
timestamp,
|
||||
observed_at: timestamp,
|
||||
verification: "unverified",
|
||||
status: "active",
|
||||
...episode,
|
||||
};
|
||||
this.store.write("memory/episodic", `${full.episodeId}.json`, full);
|
||||
|
|
@ -189,6 +202,12 @@ export class PersistentMemory {
|
|||
];
|
||||
existing.tags = [...new Set([...existing.tags, ...entry.tags])];
|
||||
existing.lastAccessed = new Date().toISOString();
|
||||
existing.observed_at = entry.observed_at ?? existing.observed_at ?? existing.lastAccessed;
|
||||
existing.source = entry.source ?? existing.source;
|
||||
existing.receipt_id = entry.receipt_id ?? existing.receipt_id;
|
||||
existing.supersedes_id = entry.supersedes_id ?? existing.supersedes_id;
|
||||
existing.status = entry.status ?? existing.status ?? "active";
|
||||
existing.verification = entry.verification ?? existing.verification ?? "unverified";
|
||||
this.store.write(
|
||||
"memory/semantic",
|
||||
`${existing.id}.json`,
|
||||
|
|
@ -197,10 +216,14 @@ export class PersistentMemory {
|
|||
return existing;
|
||||
}
|
||||
|
||||
const timestamp = new Date().toISOString();
|
||||
const full: SemanticMemory = {
|
||||
id: StateStore.uid(),
|
||||
lastAccessed: new Date().toISOString(),
|
||||
lastAccessed: timestamp,
|
||||
accessCount: 1,
|
||||
observed_at: timestamp,
|
||||
verification: entry.verification ?? "unverified",
|
||||
status: "active",
|
||||
...entry,
|
||||
};
|
||||
this.store.write("memory/semantic", `${full.id}.json`, full);
|
||||
|
|
@ -272,6 +295,9 @@ export class PersistentMemory {
|
|||
recordProcedural(entry: Omit<ProceduralMemory, "id">): ProceduralMemory {
|
||||
const full: ProceduralMemory = {
|
||||
id: StateStore.uid(),
|
||||
observed_at: new Date().toISOString(),
|
||||
verification: "unverified",
|
||||
status: "active",
|
||||
...entry,
|
||||
};
|
||||
this.store.write("memory/procedural", `${full.id}.json`, full);
|
||||
|
|
@ -418,8 +444,15 @@ export class PersistentMemory {
|
|||
);
|
||||
}
|
||||
|
||||
// Sort by recency (assuming items have timestamp or lastAccessed)
|
||||
filtered.sort(() => 0); // stable sort
|
||||
filtered = filtered
|
||||
.filter((item: T & MemoryProvenance) => (item.status ?? "active") === "active")
|
||||
.sort((a: T & MemoryProvenance, b: T & MemoryProvenance) => {
|
||||
const receiptRank = (b.receipt_id ? 1 : 0) - (a.receipt_id ? 1 : 0);
|
||||
if (receiptRank !== 0) return receiptRank;
|
||||
const verifiedRank = (b.verification === "verified" ? 1 : 0) - (a.verification === "verified" ? 1 : 0);
|
||||
if (verifiedRank !== 0) return verifiedRank;
|
||||
return Date.parse(b.observed_at ?? "") - Date.parse(a.observed_at ?? "");
|
||||
});
|
||||
|
||||
if (query.limit) {
|
||||
filtered = filtered.slice(0, query.limit);
|
||||
|
|
|
|||
Loading…
Reference in New Issue