mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 12:50:42 +00:00
fix(memory): dedupe managed markdown blocks
This commit is contained in:
@@ -32,6 +32,7 @@ Docs: https://docs.openclaw.ai
|
||||
### Fixes
|
||||
|
||||
- Control UI/chat: show inline feedback when local slash-command dispatch is unavailable or fails unexpectedly instead of clearing the composer silently. Fixes #52105. Thanks @MooreQiao.
|
||||
- Memory/markdown: replace CRLF managed blocks in place and collapse duplicate marker blocks without rewriting unmanaged markdown, so Dreaming and Memory Wiki files self-heal from repeated generated sections. Fixes #75491; supersedes #75495, #75810, and #76008. Thanks @asaenokkostya-coder, @ottodeng, @everettjf, and @lrg913427-dot.
|
||||
- Agents/tools: return critical tool-loop circuit-breaker stops as blocked tool results instead of thrown tool failures, so models see the guardrail and stop retrying the same call. Thanks @rayraiser.
|
||||
- Model commands: clarify direct and inline `/model` acknowledgements for non-default selections as session-scoped. Thanks @addu2612.
|
||||
- TUI/chat: skip full provider model normalization during context-window warmup while preserving provider-owned context metadata, avoiding cold-start stalls with large model registries. Thanks @547895019.
|
||||
|
||||
@@ -47,4 +47,103 @@ describe("replaceManagedMarkdownBlock", () => {
|
||||
}),
|
||||
).toBe("alpha\n\n<!-- start -->\nbeta\n<!-- end -->\n");
|
||||
});
|
||||
|
||||
it("replaces headed blocks with CRLF line endings in place", () => {
|
||||
expect(
|
||||
replaceManagedMarkdownBlock({
|
||||
original: "# Title\r\n\r\n## Generated\r\n<!-- start -->\r\n- old\r\n<!-- end -->\r\n",
|
||||
heading: "## Generated",
|
||||
startMarker: "<!-- start -->",
|
||||
endMarker: "<!-- end -->",
|
||||
body: "- new",
|
||||
}),
|
||||
).toBe("# Title\r\n\r\n## Generated\n<!-- start -->\n- new\n<!-- end -->\r\n");
|
||||
});
|
||||
|
||||
it("collapses pre-existing duplicate managed blocks into one", () => {
|
||||
const original = [
|
||||
"# Title",
|
||||
"",
|
||||
"## Generated",
|
||||
"<!-- start -->",
|
||||
"- run-1",
|
||||
"<!-- end -->",
|
||||
"",
|
||||
"## Generated",
|
||||
"<!-- start -->",
|
||||
"- run-2",
|
||||
"<!-- end -->",
|
||||
"",
|
||||
"## Generated",
|
||||
"<!-- start -->",
|
||||
"- run-3",
|
||||
"<!-- end -->",
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
const updated = replaceManagedMarkdownBlock({
|
||||
original,
|
||||
heading: "## Generated",
|
||||
startMarker: "<!-- start -->",
|
||||
endMarker: "<!-- end -->",
|
||||
body: "- latest",
|
||||
});
|
||||
|
||||
expect(updated).toBe("# Title\n\n## Generated\n<!-- start -->\n- latest\n<!-- end -->\n");
|
||||
expect(updated.match(/<!-- start -->/g)?.length).toBe(1);
|
||||
expect(updated).not.toContain("run-");
|
||||
});
|
||||
|
||||
it("preserves unmanaged markdown while removing duplicate blocks", () => {
|
||||
const original = [
|
||||
"# Title",
|
||||
"",
|
||||
"Paragraph A",
|
||||
"",
|
||||
"",
|
||||
"Paragraph B",
|
||||
"",
|
||||
"## Generated",
|
||||
"<!-- start -->",
|
||||
"- old",
|
||||
"<!-- end -->",
|
||||
"",
|
||||
"## Generated",
|
||||
"<!-- start -->",
|
||||
"- stale",
|
||||
"<!-- end -->",
|
||||
"",
|
||||
"## Notes",
|
||||
"kept",
|
||||
"",
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
expect(
|
||||
replaceManagedMarkdownBlock({
|
||||
original,
|
||||
heading: "## Generated",
|
||||
startMarker: "<!-- start -->",
|
||||
endMarker: "<!-- end -->",
|
||||
body: "- new",
|
||||
}),
|
||||
).toBe(
|
||||
"# Title\n\nParagraph A\n\n\nParagraph B\n\n## Generated\n<!-- start -->\n- new\n<!-- end -->\n\n## Notes\nkept\n\n",
|
||||
);
|
||||
});
|
||||
|
||||
it("is idempotent across repeated calls with the same body", () => {
|
||||
const params = {
|
||||
heading: "## Generated",
|
||||
startMarker: "<!-- start -->",
|
||||
endMarker: "<!-- end -->",
|
||||
body: "- only",
|
||||
} as const;
|
||||
const first = replaceManagedMarkdownBlock({ original: "# Title\n", ...params });
|
||||
const second = replaceManagedMarkdownBlock({ original: first, ...params });
|
||||
const third = replaceManagedMarkdownBlock({ original: second, ...params });
|
||||
|
||||
expect(second).toBe(first);
|
||||
expect(third).toBe(first);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,10 @@ function escapeRegex(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
function isLineWhitespace(value: string): boolean {
|
||||
return /^[\t \r\n]*$/.test(value);
|
||||
}
|
||||
|
||||
export function withTrailingNewline(content: string): string {
|
||||
return content.endsWith("\n") ? content : `${content}\n`;
|
||||
}
|
||||
@@ -17,13 +21,31 @@ export function withTrailingNewline(content: string): string {
|
||||
export function replaceManagedMarkdownBlock(params: ManagedMarkdownBlockParams): string {
|
||||
const headingPrefix = params.heading ? `${params.heading}\n` : "";
|
||||
const managedBlock = `${headingPrefix}${params.startMarker}\n${params.body}\n${params.endMarker}`;
|
||||
const headingPattern = params.heading
|
||||
? `${escapeRegex(params.heading)}(?:[ \t]*(?:\r\n|\n|\r))+[ \t]*`
|
||||
: "";
|
||||
const existingPattern = new RegExp(
|
||||
`${params.heading ? `${escapeRegex(params.heading)}\\n` : ""}${escapeRegex(params.startMarker)}[\\s\\S]*?${escapeRegex(params.endMarker)}`,
|
||||
"m",
|
||||
`${headingPattern}${escapeRegex(params.startMarker)}[\\s\\S]*?${escapeRegex(params.endMarker)}`,
|
||||
"g",
|
||||
);
|
||||
const matches = Array.from(params.original.matchAll(existingPattern));
|
||||
|
||||
if (existingPattern.test(params.original)) {
|
||||
return params.original.replace(existingPattern, managedBlock);
|
||||
if (matches.length > 0) {
|
||||
let updated = "";
|
||||
let lastEnd = 0;
|
||||
matches.forEach((match, index) => {
|
||||
const matchStart = match.index ?? 0;
|
||||
const matchEnd = matchStart + match[0].length;
|
||||
const betweenMatches = params.original.slice(lastEnd, matchStart);
|
||||
if (index === 0) {
|
||||
updated += params.original.slice(0, matchStart);
|
||||
updated += managedBlock;
|
||||
} else if (!isLineWhitespace(betweenMatches)) {
|
||||
updated += betweenMatches;
|
||||
}
|
||||
lastEnd = matchEnd;
|
||||
});
|
||||
return updated + params.original.slice(lastEnd);
|
||||
}
|
||||
|
||||
const trimmed = params.original.trimEnd();
|
||||
|
||||
Reference in New Issue
Block a user