mirror of
https://github.com/sdwolf4103/opencode-working-memory.git
synced 2026-07-17 12:56:42 +02:00
fix: use plain text labels instead of Markdown headers
- Changed '## Memory Candidates' to 'Memory candidates:' in compaction context - Changed '## Pending Todos' to 'Pending todos:' in todo rendering - Updated extractCandidateBlock() to parse plain text format (primary) - Removed stripXmlTags() function (no longer needed) - All 42 tests pass Root cause: Markdown headings (##) render as purple in OpenCode UI, same issue as XML tags and HTML comments. Plain text labels avoid all special markup rendering.
This commit is contained in:
+11
-7
@@ -223,18 +223,22 @@ function shouldAcceptWorkspaceMemoryCandidate(entry: {
|
||||
|
||||
/**
|
||||
* Extract candidate block from summary using multiple formats.
|
||||
* Supports: HTML comment, Markdown section, legacy XML.
|
||||
* Supports: Plain text label, 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];
|
||||
// 1. Plain text label (primary format, no Markdown header)
|
||||
const plainMatch = summary.match(/Memory candidates:\s*\n([\s\S]*?)(?:\n[A-Z][a-z]+ [a-z]+:|\n##\s|$)/i);
|
||||
if (plainMatch) return plainMatch[1];
|
||||
|
||||
// 2. Markdown section (visible but clean)
|
||||
const markdownMatch = summary.match(/##\s*Workspace Memory Candidates\s*\n([\s\S]*?)(?:\n##|$)/i);
|
||||
// 2. Markdown section (legacy)
|
||||
const markdownMatch = summary.match(/##\s*Memory Candidates\s*\n([\s\S]*?)(?:\n##\s|$)/i);
|
||||
if (markdownMatch) return markdownMatch[1];
|
||||
|
||||
// 3. Legacy XML block (backward compatible)
|
||||
// 3. Legacy "Workspace Memory Candidates" section
|
||||
const legacyMatch = summary.match(/##\s*Workspace Memory Candidates\s*\n([\s\S]*?)(?:\n##\s|$)/i);
|
||||
if (legacyMatch) return legacyMatch[1];
|
||||
|
||||
// 4. Legacy XML block (backward compatible)
|
||||
const xmlMatch = summary.match(/<workspace_memory_candidates>([\s\S]*?)<\/workspace_memory_candidates>/i);
|
||||
if (xmlMatch) return xmlMatch[1];
|
||||
|
||||
|
||||
+24
-50
@@ -45,46 +45,20 @@ import {
|
||||
pendingTodos,
|
||||
} from "./opencode.ts";
|
||||
|
||||
/**
|
||||
* Strip XML-like tags from text for Markdown-neutral rendering.
|
||||
* Converts `<workspace_memory>` blocks to plain "Workspace memory:" sections.
|
||||
*/
|
||||
function stripXmlTags(text: string): string {
|
||||
if (!text) return "";
|
||||
|
||||
// Replace XML tag pairs with Markdown headers
|
||||
return text
|
||||
.replace(/<workspace_memory>\n?/gi, "## Workspace Memory\n")
|
||||
.replace(/<\/workspace_memory>\n?/gi, "")
|
||||
.replace(/<hot_session_state>\n?/gi, "## Hot Session State\n")
|
||||
.replace(/<\/hot_session_state>\n?/gi, "")
|
||||
.replace(/<pending_todos>\n?/gi, "## Pending Todos\n")
|
||||
.replace(/<\/pending_todos>\n?/gi, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate instructions for the compaction model.
|
||||
* IMPORTANT: These instructions make clear what should NOT be in the final output.
|
||||
*/
|
||||
function compactionContextHeader(): string {
|
||||
return `
|
||||
[PRIVATE COMPACTION CONTEXT - DO NOT OUTPUT]
|
||||
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.).
|
||||
Background context for memory extraction (do not output verbatim):
|
||||
- Use facts to update Goal/Progress/Key Decisions/Next Steps sections
|
||||
- At the end, emit durable future-session memories in this format:
|
||||
|
||||
At the VERY END of your summary, if there are durable memory candidates, include this hidden block:
|
||||
|
||||
<!-- workspace_memory_candidates
|
||||
- [type] content (types: feedback, project, decision, reference)
|
||||
-->
|
||||
|
||||
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]
|
||||
Memory candidates:
|
||||
- [feedback] content
|
||||
- [project] content
|
||||
- [decision] content
|
||||
- [reference] content
|
||||
`.trim();
|
||||
}
|
||||
|
||||
@@ -97,11 +71,11 @@ function memoryCandidateInstruction(): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Render todos for compaction context (Markdown-neutral format).
|
||||
* Render todos for compaction context (plain text format, no Markdown headers).
|
||||
*/
|
||||
function renderTodosForCompaction(todos: Array<{ content: string; status: string; priority?: string }>): string {
|
||||
if (todos.length === 0) return "";
|
||||
const lines = ["## Pending Todos"];
|
||||
const lines = ["Pending todos:"];
|
||||
for (const todo of todos) {
|
||||
const priority = todo.priority ? ` [${todo.priority}]` : "";
|
||||
const status = todo.status === "completed" ? "✓" : todo.status === "in_progress" ? "→" : "○";
|
||||
@@ -329,22 +303,22 @@ export const MemoryV2Plugin: Plugin = async (input) => {
|
||||
// Sub-agents don't need compaction support
|
||||
if (await isSubAgent(sessionID)) return;
|
||||
|
||||
// Build private context with Markdown-neutral format
|
||||
const contextParts: string[] = [];
|
||||
// Build private context with Markdown-neutral format
|
||||
const contextParts: string[] = [];
|
||||
|
||||
// 1. Frozen workspace memory (strip XML tags for compaction context)
|
||||
const workspaceMemory = await getFrozenWorkspaceMemory(directory, sessionID);
|
||||
const workspacePrompt = renderWorkspaceMemory(workspaceMemory);
|
||||
if (workspacePrompt) {
|
||||
contextParts.push(stripXmlTags(workspacePrompt));
|
||||
}
|
||||
// 1. Frozen workspace memory
|
||||
const workspaceMemory = await getFrozenWorkspaceMemory(directory, sessionID);
|
||||
const workspacePrompt = renderWorkspaceMemory(workspaceMemory);
|
||||
if (workspacePrompt) {
|
||||
contextParts.push(workspacePrompt);
|
||||
}
|
||||
|
||||
// 2. Hot session state (strip XML tags for compaction context)
|
||||
const sessionState = await loadSessionState(directory, sessionID);
|
||||
const hotPrompt = renderHotSessionState(sessionState, directory);
|
||||
if (hotPrompt) {
|
||||
contextParts.push(stripXmlTags(hotPrompt));
|
||||
}
|
||||
// 2. Hot session state
|
||||
const sessionState = await loadSessionState(directory, sessionID);
|
||||
const hotPrompt = renderHotSessionState(sessionState, directory);
|
||||
if (hotPrompt) {
|
||||
contextParts.push(hotPrompt);
|
||||
}
|
||||
|
||||
// 3. Pending todos from OpenCode (Markdown-neutral format)
|
||||
const todos = await pendingTodos(client, sessionID);
|
||||
|
||||
@@ -180,7 +180,7 @@ export function renderHotSessionState(state: SessionState, workspaceRoot: string
|
||||
|
||||
if (activeFiles.length === 0 && openErrors.length === 0 && decisions.length === 0) return "";
|
||||
|
||||
const lines: string[] = ["<hot_session_state>"];
|
||||
const lines: string[] = ["Hot session state (current session):"];
|
||||
|
||||
if (activeFiles.length > 0) {
|
||||
lines.push("active_files:");
|
||||
@@ -204,7 +204,6 @@ export function renderHotSessionState(state: SessionState, workspaceRoot: string
|
||||
}
|
||||
}
|
||||
|
||||
lines.push("</hot_session_state>");
|
||||
return lines.join("\n").slice(0, HOT_STATE_LIMITS.maxRenderedChars);
|
||||
}
|
||||
|
||||
|
||||
@@ -136,10 +136,8 @@ export function renderWorkspaceMemory(store: WorkspaceMemoryStore): string {
|
||||
// If maxChars smaller than minimum envelope, return empty string
|
||||
if (maxChars < MIN_ENVELOPE_LENGTH) return "";
|
||||
|
||||
const closing = "</workspace_memory>";
|
||||
const lines: string[] = [
|
||||
"<workspace_memory>",
|
||||
"Persistent workspace memory. Use as background; verify stale or code-related claims.",
|
||||
"Workspace memory (cross-session, verify if stale):",
|
||||
];
|
||||
|
||||
for (const type of ["feedback", "project", "decision", "reference"] as const) {
|
||||
@@ -150,17 +148,16 @@ export function renderWorkspaceMemory(store: WorkspaceMemoryStore): string {
|
||||
|
||||
for (const item of items) {
|
||||
const line = `- ${renderEntry(item)}`;
|
||||
if (wouldFit([...lines, ...sectionLines], line, closing, maxChars)) {
|
||||
if ([...lines, ...sectionLines, line].join("\n").length <= maxChars) {
|
||||
sectionLines.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
if (sectionLines.length > 1 && wouldFit(lines, sectionLines[0], closing, maxChars)) {
|
||||
if (sectionLines.length > 1) {
|
||||
lines.push(...sectionLines);
|
||||
}
|
||||
}
|
||||
|
||||
lines.push(closing);
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user