mirror of
https://github.com/sdwolf4103/opencode-working-memory.git
synced 2026-06-02 06:19:36 +02:00
fix: change compaction output to HTML comment, prevent Markdown rendering issues
Root cause: Model was instructed to output <workspace_memory_candidates> XML tags in the user-visible compaction summary, causing purple/italic rendering when combined with --- delimiters in Markdown. Fixes: - compactionContextHeader(): Now instructs model to use HTML comment format <!-- workspace_memory_candidates ... --> which is hidden from users - extractCandidateBlock(): New function supports 3 formats: 1. HTML comment (preferred, hidden from user) 2. Markdown section (visible but clean) 3. Legacy XML (backward compatible) - Added "DO NOT use XML tags" and "DO NOT start with ---" instructions Tests: - Verify compaction context header uses HTML comment format - Test parser accepts all 3 formats (HTML comment, Markdown, legacy XML)
This commit is contained in:
+23
-3
@@ -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(/<!--\s*workspace_memory_candidates\s*\n([\s\S]*?)-->/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(/<workspace_memory_candidates>([\s\S]*?)<\/workspace_memory_candidates>/i);
|
||||
if (xmlMatch) return xmlMatch[1];
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function parseWorkspaceMemoryCandidates(summary: string): LongTermMemoryEntry[] {
|
||||
const match = summary.match(/<workspace_memory_candidates>([\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;
|
||||
|
||||
+5
-3
@@ -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:
|
||||
|
||||
<workspace_memory_candidates>
|
||||
<!-- workspace_memory_candidates
|
||||
- [type] content (types: feedback, project, decision, reference)
|
||||
</workspace_memory_candidates>
|
||||
-->
|
||||
|
||||
Only include truly durable information useful across FUTURE sessions.
|
||||
If nothing qualifies, omit the block entirely.
|
||||
DO NOT use XML tags like <workspace_memory_candidates>.
|
||||
DO NOT start with "---" frontmatter delimiters.
|
||||
|
||||
[END PRIVATE COMPACTION CONTEXT]
|
||||
`.trim();
|
||||
|
||||
@@ -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<string, Function>)["experimental.session.compacting"](
|
||||
{ sessionID: "test-header-session" },
|
||||
output
|
||||
);
|
||||
|
||||
const header = output.context[0] || "";
|
||||
|
||||
// Should instruct HTML comment format
|
||||
assert.equal(header.includes("<!-- workspace_memory_candidates"), true,
|
||||
"Header should instruct HTML comment format");
|
||||
|
||||
// Should NOT instruct legacy XML format as primary
|
||||
// Note: We check that the instruction for HTML comments comes before any XML mention
|
||||
const htmlCommentIndex = header.indexOf("<!-- workspace_memory_candidates");
|
||||
const xmlTagIndex = header.indexOf("<workspace_memory_candidates>\n");
|
||||
if (xmlTagIndex !== -1) {
|
||||
assert.equal(htmlCommentIndex < xmlTagIndex, true,
|
||||
"HTML comment format should be the primary instruction, not XML");
|
||||
}
|
||||
|
||||
// Should explicitly forbid XML
|
||||
assert.equal(header.includes("DO NOT use XML tags"), true,
|
||||
"Header should forbid XML tags");
|
||||
|
||||
} finally {
|
||||
await rm(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("parseWorkspaceMemoryCandidates accepts HTML comment format", async () => {
|
||||
const summary = `
|
||||
## Summary
|
||||
Progress made on testing.
|
||||
|
||||
<!-- workspace_memory_candidates
|
||||
- [decision] Use HTML comments for candidates
|
||||
- [project] This repo uses Markdown for docs
|
||||
-->
|
||||
|
||||
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.
|
||||
|
||||
<workspace_memory_candidates>
|
||||
- [feedback] Users prefer darker themes
|
||||
</workspace_memory_candidates>
|
||||
|
||||
Next steps: continue development.
|
||||
`;
|
||||
|
||||
const candidates = parseWorkspaceMemoryCandidates(summary);
|
||||
assert.equal(candidates.length, 1, "Should parse legacy XML format");
|
||||
assert.equal(candidates[0].type, "feedback");
|
||||
});
|
||||
Reference in New Issue
Block a user