fix(memory-wiki): keep claim freshness tied to evidence timestamps (#97465)

* fix(memory-wiki): keep claim freshness tied to evidence

* fix(memory-wiki): preserve page freshness fallback only for untimestamped claims

---------

Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
This commit is contained in:
ly-wang19
2026-06-29 02:58:45 +08:00
committed by GitHub
parent 25490d4c42
commit 630034ef62
2 changed files with 121 additions and 8 deletions
+102 -2
View File
@@ -1,7 +1,7 @@
// Memory Wiki tests cover claim health plugin behavior.
import { describe, expect, it } from "vitest";
import { buildPageContradictionClusters } from "./claim-health.js";
import type { WikiPageSummary } from "./markdown.js";
import { assessClaimFreshness, buildPageContradictionClusters } from "./claim-health.js";
import type { WikiClaim, WikiPageSummary } from "./markdown.js";
function createPage(params: {
relativePath: string;
@@ -64,3 +64,103 @@ describe("buildPageContradictionClusters", () => {
expect(clusters.every((cluster) => cluster.entries)).toBe(true);
});
});
describe("assessClaimFreshness", () => {
it("uses the latest claim evidence timestamp without relying on page freshness", () => {
const page = createPage({
relativePath: "entities/alpha.md",
title: "Alpha",
contradictions: [],
});
page.updatedAt = "2026-01-01T00:00:00.000Z";
const claim: WikiClaim = {
text: "Alpha prefers current evidence.",
updatedAt: "2026-01-05T00:00:00.000Z",
evidence: [
{ updatedAt: "2026-01-03T00:00:00.000Z" },
{ updatedAt: "2026-01-20T00:00:00.000Z" },
],
};
const freshness = assessClaimFreshness({
page,
claim,
now: new Date("2026-01-25T00:00:00.000Z"),
});
expect(freshness.level).toBe("fresh");
expect(freshness.lastTouchedAt).toBe("2026-01-20T00:00:00.000Z");
expect(freshness.daysSinceTouch).toBe(5);
});
it("does not let a newer page timestamp make stale claim evidence fresh", () => {
const page = createPage({
relativePath: "entities/beta.md",
title: "Beta",
contradictions: [],
});
page.updatedAt = "2026-04-20T00:00:00.000Z";
const claim: WikiClaim = {
text: "Beta still needs old evidence checked.",
updatedAt: "2026-01-01T00:00:00.000Z",
evidence: [{ updatedAt: "2026-01-05T00:00:00.000Z" }],
};
const freshness = assessClaimFreshness({
page,
claim,
now: new Date("2026-04-20T00:00:00.000Z"),
});
expect(freshness.level).toBe("stale");
expect(freshness.lastTouchedAt).toBe("2026-01-05T00:00:00.000Z");
expect(freshness.daysSinceTouch).toBe(105);
});
it("falls back to page freshness when claim and evidence timestamps are absent", () => {
const page = createPage({
relativePath: "entities/gamma.md",
title: "Gamma",
contradictions: [],
});
page.updatedAt = "2026-04-20T00:00:00.000Z";
const claim: WikiClaim = {
text: "Gamma was written through wiki_apply without per-claim timestamps.",
evidence: [{ kind: "session", sourceId: "session-1" }],
};
const freshness = assessClaimFreshness({
page,
claim,
now: new Date("2026-04-25T00:00:00.000Z"),
});
expect(freshness.level).toBe("fresh");
expect(freshness.lastTouchedAt).toBe("2026-04-20T00:00:00.000Z");
expect(freshness.daysSinceTouch).toBe(5);
});
it("keeps malformed claim timestamps unknown instead of using page freshness", () => {
const page = createPage({
relativePath: "entities/delta.md",
title: "Delta",
contradictions: [],
});
page.updatedAt = "2026-04-20T00:00:00.000Z";
const claim: WikiClaim = {
text: "Delta has malformed claim freshness metadata.",
updatedAt: "not-a-date",
evidence: [],
};
const freshness = assessClaimFreshness({
page,
claim,
now: new Date("2026-04-25T00:00:00.000Z"),
});
expect(freshness.level).toBe("unknown");
expect(freshness.lastTouchedAt).toBeUndefined();
expect(freshness.reason).toBe("missing updatedAt");
});
});
+19 -6
View File
@@ -136,12 +136,25 @@ export function assessClaimFreshness(params: {
claim: WikiClaim;
now?: Date;
}): WikiFreshness {
const latestTimestamp = resolveLatestTimestamp([
params.claim.updatedAt,
params.page.updatedAt,
...params.claim.evidence.map((evidence) => evidence.updatedAt),
]);
return buildFreshnessFromTimestamp({ timestamp: latestTimestamp, now: params.now });
let hasClaimTimestamp = typeof params.claim.updatedAt === "string" &&
params.claim.updatedAt.trim().length > 0;
let latestTimestamp = resolveLatestTimestamp([params.claim.updatedAt]);
let latestMs = parseTimestamp(latestTimestamp) ?? -1;
for (const evidence of params.claim.evidence) {
if (typeof evidence.updatedAt === "string" && evidence.updatedAt.trim().length > 0) {
hasClaimTimestamp = true;
}
const evidenceMs = parseTimestamp(evidence.updatedAt);
if (evidenceMs === null || !evidence.updatedAt || evidenceMs <= latestMs) {
continue;
}
latestMs = evidenceMs;
latestTimestamp = evidence.updatedAt;
}
return buildFreshnessFromTimestamp({
timestamp: latestTimestamp ?? (hasClaimTimestamp ? undefined : params.page.updatedAt),
now: params.now,
});
}
function buildWikiClaimHealth(params: {