diff --git a/src/functions/context.ts b/src/functions/context.ts
index 65c7ec551..d453cb00e 100644
--- a/src/functions/context.ts
+++ b/src/functions/context.ts
@@ -20,6 +20,7 @@ import {
} from "./slots.js";
import { getAgentId, isAgentScopeIsolated } from "../config.js";
import { selectDurableMemories } from "../state/memory-selection.js";
+import { renderMemoryText } from "../state/memory-utils.js";
function estimateTokens(text: string): number {
return Math.ceil(text.length / 3);
@@ -136,7 +137,7 @@ export function registerContextFunction(
});
if (durableMemories.length > 0) {
const content = `## Durable Memories\n${durableMemories
- .map((memory) => `- ${memory.title}: ${memory.content}`)
+ .map((memory) => `- ${renderMemoryText(memory)}`)
.join("\n")}`;
blocks.push({
type: "memory",
diff --git a/src/functions/enrich.ts b/src/functions/enrich.ts
index 032ec97b4..8d884b5d5 100644
--- a/src/functions/enrich.ts
+++ b/src/functions/enrich.ts
@@ -3,6 +3,7 @@ import type { Memory } from "../types.js";
import { KV } from "../state/schema.js";
import { StateKV } from "../state/kv.js";
import { logger } from "../logger.js";
+import { renderMemoryText } from "../state/memory-utils.js";
const MAX_CONTEXT_LENGTH = 4000;
@@ -111,7 +112,7 @@ export function registerEnrichFunction(sdk: ISdk, kv: StateKV): void {
if (bugMemories.length > 0) {
const bugs = bugMemories
.slice(0, 3)
- .map((m) => `- ${escapeXml(m.title)}: ${escapeXml(m.content)}`)
+ .map((memory) => `- ${escapeXml(renderMemoryText(memory))}`)
.join("\n");
parts.push(
`\n${bugs}\n`,
diff --git a/src/functions/working-memory.ts b/src/functions/working-memory.ts
index 127195e52..a50ee8148 100644
--- a/src/functions/working-memory.ts
+++ b/src/functions/working-memory.ts
@@ -5,6 +5,7 @@ import { StateKV } from "../state/kv.js";
import { recordAudit } from "./audit.js";
import { recordAccessBatch } from "./access-tracker.js";
import { logger } from "../logger.js";
+import { renderMemoryText } from "../state/memory-utils.js";
const CORE_SCOPE = "mem:core-memory";
@@ -147,7 +148,7 @@ export function registerWorkingMemoryFunctions(
for (const mem of active) {
const tokens = estimateTokens(mem.content);
if (usedTokens + tokens > budget) continue;
- archivalLines.push(`- [${mem.type}] ${mem.title}: ${mem.content}`);
+ archivalLines.push(`- [${mem.type}] ${renderMemoryText(mem)}`);
archivalIds.push(mem.id);
usedTokens += tokens;
}
diff --git a/src/state/memory-utils.ts b/src/state/memory-utils.ts
index 8428da2a6..34bbdaf67 100644
--- a/src/state/memory-utils.ts
+++ b/src/state/memory-utils.ts
@@ -1,5 +1,17 @@
import type { CompressedObservation, Lesson, Memory } from "../types.js";
+export function renderMemoryText(
+ memory: Pick,
+): string {
+ const slicedTitle = memory.content.slice(0, 80);
+ const safeTitle = /[\uD800-\uDBFF]$/.test(slicedTitle)
+ ? slicedTitle.slice(0, -1)
+ : slicedTitle;
+ return memory.title === slicedTitle || memory.title === safeTitle
+ ? memory.content
+ : `${memory.title}: ${memory.content}`;
+}
+
// Wraps a Memory record in the CompressedObservation shape that
// SearchIndex / VectorIndex / enrichment paths consume. Memories share
// the same searchable fields as observations (title + content +
diff --git a/test/durable-recall.test.ts b/test/durable-recall.test.ts
index eb7b7d262..74275ee5a 100644
--- a/test/durable-recall.test.ts
+++ b/test/durable-recall.test.ts
@@ -173,6 +173,42 @@ describe("durable memory recall", () => {
expect(accessRows).toHaveLength(10);
});
+ it("does not repeat a durable memory title derived from its content", async () => {
+ const derivedContent =
+ "Always pin lockfiles before dependency updates so installs stay reproducible across every supported agent runtime.";
+ const memories = [
+ makeMemory({
+ id: "mem_derived_title",
+ title: derivedContent.slice(0, 80),
+ content: derivedContent,
+ project: "project-a",
+ }),
+ makeMemory({
+ id: "mem_independent_title",
+ title: "Database migration",
+ content: "Database migration requires downtime.",
+ project: "project-a",
+ }),
+ ];
+ for (const memory of memories) {
+ await kv.set(KV.memories, memory.id, memory);
+ }
+ registerContextFunction(sdk as never, kv as never, 20_000);
+
+ const result = (await sdk.trigger("mem::context", {
+ sessionId: "ses_current",
+ project: "project-a",
+ })) as { context: string };
+
+ expect(result.context).toContain(`- ${derivedContent}`);
+ expect(result.context).not.toContain(
+ `${derivedContent.slice(0, 80)}: ${derivedContent}`,
+ );
+ expect(result.context).toContain(
+ "- Database migration: Database migration requires downtime.",
+ );
+ });
+
it("returns capped, ranked, scoped durable memories when hybrid search is empty", async () => {
const candidates = [
makeMemory({ id: "mem_global", content: "needle sentinel global", agentId: "agent-a", strength: 10 }),
diff --git a/test/enrich.test.ts b/test/enrich.test.ts
index adfb31f86..7504cfbb0 100644
--- a/test/enrich.test.ts
+++ b/test/enrich.test.ts
@@ -210,4 +210,47 @@ describe("Enrich Function", () => {
expect(result.context).toContain("Race condition");
expect(result.context).not.toContain("Singleton pattern");
});
+
+ it("does not repeat a bug memory title derived from its content", async () => {
+ sdk.overrideTrigger("mem::file-context", async () => ({ context: "" }));
+ sdk.overrideTrigger("mem::search", async () => ({ results: [] }));
+ const content = "Race condition in worker pool";
+ const memory = makeMemory({
+ id: "bug_derived_title",
+ title: content,
+ content,
+ files: ["src/worker.ts"],
+ });
+ await kv.set("mem:memories", memory.id, memory);
+
+ const result = (await sdk.trigger("mem::enrich", {
+ sessionId: "ses_1",
+ files: ["src/worker.ts"],
+ })) as { context: string };
+
+ expect(result.context).toContain(`- ${content}`);
+ expect(result.context).not.toContain(`${content}: ${content}`);
+ });
+
+ it("preserves an authored prefix title and escapes the rendered memory", async () => {
+ sdk.overrideTrigger("mem::file-context", async () => ({ context: "" }));
+ sdk.overrideTrigger("mem::search", async () => ({ results: [] }));
+ const memory = makeMemory({
+ id: "bug_authored_title",
+ title: "Race & lock",
+ content: "Race & lock failures occur before .",
+ files: ["src/worker.ts"],
+ });
+ await kv.set("mem:memories", memory.id, memory);
+
+ const result = (await sdk.trigger("mem::enrich", {
+ sessionId: "ses_1",
+ files: ["src/worker.ts"],
+ })) as { context: string };
+
+ expect(result.context).toContain(
+ "- Race & lock: Race & lock failures occur before <shutdown>.",
+ );
+ expect(result.context).not.toContain("");
+ });
});
diff --git a/test/memory-utils.test.ts b/test/memory-utils.test.ts
new file mode 100644
index 000000000..57fc51a18
--- /dev/null
+++ b/test/memory-utils.test.ts
@@ -0,0 +1,44 @@
+import { describe, expect, it } from "vitest";
+import { renderMemoryText } from "../src/state/memory-utils.js";
+
+const longContent =
+ "Always pin lockfiles before dependency updates so installs stay reproducible across every supported runtime.";
+const surrogateContent = `${"a".repeat(79)}😀tail`;
+const rawSurrogateTitle = surrogateContent.slice(0, 80);
+
+describe("renderMemoryText", () => {
+ it.each([
+ {
+ name: "short derived title",
+ title: "Always pin lockfiles",
+ content: "Always pin lockfiles",
+ expected: "Always pin lockfiles",
+ },
+ {
+ name: "80-character derived title",
+ title: longContent.slice(0, 80),
+ content: longContent,
+ expected: longContent,
+ },
+ {
+ name: "legacy raw surrogate slice",
+ title: rawSurrogateTitle,
+ content: surrogateContent,
+ expected: surrogateContent,
+ },
+ {
+ name: "safe surrogate slice",
+ title: rawSurrogateTitle.slice(0, -1),
+ content: surrogateContent,
+ expected: surrogateContent,
+ },
+ {
+ name: "authored prefix title",
+ title: "Database migration",
+ content: "Database migration requires downtime.",
+ expected: "Database migration: Database migration requires downtime.",
+ },
+ ])("renders $name", ({ title, content, expected }) => {
+ expect(renderMemoryText({ title, content })).toBe(expected);
+ });
+});
diff --git a/test/working-memory.test.ts b/test/working-memory.test.ts
index 446438d1d..b62c11e4d 100644
--- a/test/working-memory.test.ts
+++ b/test/working-memory.test.ts
@@ -85,6 +85,8 @@ describe("working-memory", () => {
});
it("working-context builds core + archival sections", async () => {
+ const derivedContent =
+ "Always pin lockfiles before dependency updates so installs stay reproducible across every supported runtime.";
mockKv.list.mockImplementation((scope: string) => {
if (scope === "mem:core-memory") {
return [
@@ -104,12 +106,21 @@ describe("working-memory", () => {
{
id: "m1",
type: "pattern",
- title: "API pattern",
- content: "REST endpoints follow /api/resource convention",
+ title: derivedContent.slice(0, 80),
+ content: derivedContent,
isLatest: true,
strength: 0.8,
updatedAt: new Date().toISOString(),
},
+ {
+ id: "m2",
+ type: "architecture",
+ title: "Database migration",
+ content: "Database migration requires downtime.",
+ isLatest: true,
+ strength: 0.7,
+ updatedAt: new Date().toISOString(),
+ },
];
}
return [];
@@ -123,6 +134,13 @@ describe("working-memory", () => {
expect(result.coreEntries).toBe(1);
expect(result.context).toContain("Core Memory");
expect(result.context).toContain("Use iii primitives");
+ expect(result.context).toContain(`- [pattern] ${derivedContent}`);
+ expect(result.context).not.toContain(
+ `${derivedContent.slice(0, 80)}: ${derivedContent}`,
+ );
+ expect(result.context).toContain(
+ "- [architecture] Database migration: Database migration requires downtime.",
+ );
});
it("auto-page removes lowest-value unpinned entries", async () => {