202 lines
5.5 KiB
TypeScript
202 lines
5.5 KiB
TypeScript
import {
|
|
SYSTEM_PROMPT_MEMFS_ADDON,
|
|
SYSTEM_PROMPT_MEMORY_ADDON,
|
|
} from "./promptAssets";
|
|
|
|
export type MemoryPromptMode = "standard" | "memfs";
|
|
|
|
export interface MemoryPromptDrift {
|
|
code:
|
|
| "legacy_memory_language_with_memfs"
|
|
| "memfs_language_with_standard_mode"
|
|
| "orphan_memfs_fragment";
|
|
message: string;
|
|
}
|
|
|
|
interface Heading {
|
|
level: number;
|
|
title: string;
|
|
startOffset: number;
|
|
}
|
|
|
|
function normalizeNewlines(text: string): string {
|
|
return text.replace(/\r\n/g, "\n");
|
|
}
|
|
|
|
function scanHeadingsOutsideFences(text: string): Heading[] {
|
|
const lines = text.split("\n");
|
|
const headings: Heading[] = [];
|
|
let inFence = false;
|
|
let fenceToken = "";
|
|
let offset = 0;
|
|
|
|
for (const line of lines) {
|
|
const trimmed = line.trimStart();
|
|
const fenceMatch = trimmed.match(/^(```+|~~~+)/);
|
|
if (fenceMatch) {
|
|
const token = fenceMatch[1] ?? fenceMatch[0] ?? "";
|
|
const tokenChar = token.startsWith("`") ? "`" : "~";
|
|
if (!inFence) {
|
|
inFence = true;
|
|
fenceToken = tokenChar;
|
|
} else if (fenceToken === tokenChar) {
|
|
inFence = false;
|
|
fenceToken = "";
|
|
}
|
|
}
|
|
|
|
if (!inFence) {
|
|
const headingMatch = line.match(/^\s*(#{1,6})\s+(.+?)\s*$/);
|
|
if (headingMatch) {
|
|
const hashes = headingMatch[1] ?? "";
|
|
const rawTitle = headingMatch[2] ?? "";
|
|
if (hashes && rawTitle) {
|
|
const level = hashes.length;
|
|
const title = rawTitle.replace(/\s+#*$/, "").trim();
|
|
headings.push({
|
|
level,
|
|
title,
|
|
startOffset: offset,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
offset += line.length + 1;
|
|
}
|
|
|
|
return headings;
|
|
}
|
|
|
|
function stripHeadingSections(
|
|
text: string,
|
|
shouldStrip: (heading: Heading) => boolean,
|
|
): string {
|
|
let current = text;
|
|
while (true) {
|
|
const headings = scanHeadingsOutsideFences(current);
|
|
const target = headings.find(shouldStrip);
|
|
if (!target) {
|
|
return current;
|
|
}
|
|
|
|
const nextHeading = headings.find(
|
|
(heading) =>
|
|
heading.startOffset > target.startOffset &&
|
|
heading.level <= target.level,
|
|
);
|
|
const end = nextHeading ? nextHeading.startOffset : current.length;
|
|
current = `${current.slice(0, target.startOffset)}${current.slice(end)}`;
|
|
}
|
|
}
|
|
|
|
function getMemfsTailFragment(): string {
|
|
const tailAnchor = "# See what changed";
|
|
const start = SYSTEM_PROMPT_MEMFS_ADDON.indexOf(tailAnchor);
|
|
if (start === -1) return "";
|
|
return SYSTEM_PROMPT_MEMFS_ADDON.slice(start).trim();
|
|
}
|
|
|
|
function stripExactAddon(text: string, addon: string): string {
|
|
const trimmedAddon = addon.trim();
|
|
if (!trimmedAddon) return text;
|
|
let current = text;
|
|
while (current.includes(trimmedAddon)) {
|
|
current = current.replace(trimmedAddon, "");
|
|
}
|
|
return current;
|
|
}
|
|
|
|
function stripOrphanMemfsTail(text: string): string {
|
|
const tail = getMemfsTailFragment();
|
|
if (!tail) return text;
|
|
let current = text;
|
|
while (current.includes(tail)) {
|
|
current = current.replace(tail, "");
|
|
}
|
|
return current;
|
|
}
|
|
|
|
function compactBlankLines(text: string): string {
|
|
return text.replace(/\n{3,}/g, "\n\n").trimEnd();
|
|
}
|
|
|
|
export function stripManagedMemorySections(systemPrompt: string): string {
|
|
let current = normalizeNewlines(systemPrompt);
|
|
|
|
// Strip exact current addons first (fast path).
|
|
current = stripExactAddon(current, SYSTEM_PROMPT_MEMORY_ADDON);
|
|
current = stripExactAddon(current, SYSTEM_PROMPT_MEMFS_ADDON);
|
|
|
|
// Strip known orphan fragment produced by the old regex bug.
|
|
current = stripOrphanMemfsTail(current);
|
|
|
|
// Strip legacy/variant memory sections by markdown heading parsing.
|
|
current = stripHeadingSections(
|
|
current,
|
|
(heading) => heading.title === "Memory",
|
|
);
|
|
current = stripHeadingSections(current, (heading) =>
|
|
heading.title.startsWith("Memory Filesystem"),
|
|
);
|
|
|
|
return compactBlankLines(current);
|
|
}
|
|
|
|
export function reconcileMemoryPrompt(
|
|
systemPrompt: string,
|
|
mode: MemoryPromptMode,
|
|
): string {
|
|
const base = stripManagedMemorySections(systemPrompt).trimEnd();
|
|
const addon =
|
|
mode === "memfs"
|
|
? SYSTEM_PROMPT_MEMFS_ADDON.trimStart()
|
|
: SYSTEM_PROMPT_MEMORY_ADDON.trimStart();
|
|
return `${base}\n\n${addon}`.trim();
|
|
}
|
|
|
|
export function detectMemoryPromptDrift(
|
|
systemPrompt: string,
|
|
expectedMode: MemoryPromptMode,
|
|
): MemoryPromptDrift[] {
|
|
const prompt = normalizeNewlines(systemPrompt);
|
|
const drifts: MemoryPromptDrift[] = [];
|
|
|
|
const hasLegacyMemoryLanguage = prompt.includes(
|
|
"Your memory consists of core memory (composed of memory blocks)",
|
|
);
|
|
const hasMemfsLanguage =
|
|
prompt.includes("## Memory Filesystem") ||
|
|
prompt.includes("Your memory is stored in a git repository at");
|
|
const hasOrphanFragment =
|
|
prompt.includes("# See what changed") &&
|
|
prompt.includes("git add system/") &&
|
|
prompt.includes('git commit -m "<type>: <what changed>"');
|
|
|
|
if (expectedMode === "memfs" && hasLegacyMemoryLanguage) {
|
|
drifts.push({
|
|
code: "legacy_memory_language_with_memfs",
|
|
message:
|
|
"System prompt contains legacy memory-block language while memfs is enabled.",
|
|
});
|
|
}
|
|
|
|
if (expectedMode === "standard" && hasMemfsLanguage) {
|
|
drifts.push({
|
|
code: "memfs_language_with_standard_mode",
|
|
message:
|
|
"System prompt contains Memory Filesystem language while memfs is disabled.",
|
|
});
|
|
}
|
|
|
|
if (hasOrphanFragment && !hasMemfsLanguage) {
|
|
drifts.push({
|
|
code: "orphan_memfs_fragment",
|
|
message:
|
|
"System prompt contains orphaned memfs sync fragment without a full memfs section.",
|
|
});
|
|
}
|
|
|
|
return drifts;
|
|
}
|