diff --git a/src/extractors.ts b/src/extractors.ts index 5b13a12..34b080f 100644 --- a/src/extractors.ts +++ b/src/extractors.ts @@ -221,14 +221,34 @@ function shouldAcceptWorkspaceMemoryCandidate(entry: { return true; } +/** + * Extract candidate block from summary using multiple formats. + * Supports: HTML comment, Markdown section, legacy XML. + */ +function extractCandidateBlock(summary: string): string | null { + // 1. HTML comment block (preferred, hidden from user) + const commentMatch = summary.match(//i); + if (commentMatch) return commentMatch[1]; + + // 2. Markdown section (visible but clean) + const markdownMatch = summary.match(/##\s*Workspace Memory Candidates\s*\n([\s\S]*?)(?:\n##|$)/i); + if (markdownMatch) return markdownMatch[1]; + + // 3. Legacy XML block (backward compatible) + const xmlMatch = summary.match(/([\s\S]*?)<\/workspace_memory_candidates>/i); + if (xmlMatch) return xmlMatch[1]; + + return null; +} + export function parseWorkspaceMemoryCandidates(summary: string): LongTermMemoryEntry[] { - const match = summary.match(/([\s\S]*?)<\/workspace_memory_candidates>/i); - if (!match) return []; + const block = extractCandidateBlock(summary); + if (!block) return []; const now = new Date().toISOString(); const entries: LongTermMemoryEntry[] = []; - for (const line of match[1].split("\n")) { + for (const line of block.split("\n")) { const item = line.trim().match(/^-\s*\[(feedback|project|decision|reference)\]\s*(.+)$/i); if (!item) continue; const type = item[1].toLowerCase() as LongTermType; diff --git a/src/plugin.ts b/src/plugin.ts index 1df9222..773e619 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -73,14 +73,16 @@ The following sections are PRIVATE INPUT for updating the compaction summary. DO NOT copy these sections, their headings, or their contents verbatim. Use the facts only to update the normal summary sections (Goal, Progress, etc.). -At the VERY END of your summary, you MAY include ONE output block: +At the VERY END of your summary, if there are durable memory candidates, include this hidden block: - + Only include truly durable information useful across FUTURE sessions. If nothing qualifies, omit the block entirely. +DO NOT use XML tags like . +DO NOT start with "---" frontmatter delimiters. [END PRIVATE COMPACTION CONTEXT] `.trim(); diff --git a/tests/plugin.test.ts b/tests/plugin.test.ts index 2cf555b..4261068 100644 --- a/tests/plugin.test.ts +++ b/tests/plugin.test.ts @@ -5,6 +5,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { MemoryV2Plugin } from "../src/plugin.ts"; import { loadSessionState, saveSessionState } from "../src/session-state.ts"; +import { parseWorkspaceMemoryCandidates } from "../src/extractors.ts"; import type { OpenError } from "../src/types.ts"; // Mock client for root session (not a sub-agent) @@ -245,4 +246,95 @@ test("experimental.session.compacting: context contains no XML tags for compacti } finally { await rm(tmpDir, { recursive: true, force: true }); } +}); + +test("compactionContextHeader does not require XML output", async () => { + const tmpDir = await mkdtemp(join(tmpdir(), "memory-plugin-test-")); + + try { + const client = mockRootClient(); + const plugin = await MemoryV2Plugin({ directory: tmpDir, client }); + + // Call the compaction hook to get the actual header + const output = { context: [] as string[] }; + await (plugin as Record)["experimental.session.compacting"]( + { sessionID: "test-header-session" }, + output + ); + + const header = output.context[0] || ""; + + // Should instruct HTML comment format + assert.equal(header.includes(" + +Next steps: continue development. +`; + + const candidates = parseWorkspaceMemoryCandidates(summary); + assert.equal(candidates.length, 2, "Should parse HTML comment format"); + assert.equal(candidates[0].type, "decision"); + assert.equal(candidates[1].type, "project"); +}); + +test("parseWorkspaceMemoryCandidates accepts Markdown section format", async () => { + const summary = ` +## Summary +Progress made on testing. + +## Workspace Memory Candidates +- [reference] Check docs at README.md + +## Next Steps +Continue development. +`; + + const candidates = parseWorkspaceMemoryCandidates(summary); + assert.equal(candidates.length, 1, "Should parse Markdown section format"); + assert.equal(candidates[0].type, "reference"); +}); + +test("parseWorkspaceMemoryCandidates still accepts legacy XML format", async () => { + const summary = ` +## Summary +Progress made on testing. + + +- [feedback] Users prefer darker themes + + +Next steps: continue development. +`; + + const candidates = parseWorkspaceMemoryCandidates(summary); + assert.equal(candidates.length, 1, "Should parse legacy XML format"); + assert.equal(candidates[0].type, "feedback"); }); \ No newline at end of file