Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 55 additions & 23 deletions src/context/materialization-coordinator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,36 +381,68 @@ type DurableSignals = {
};

function extractDurableSignalsFromSummary(summary: string): DurableSignals {
const empty: DurableSignals = { decisions: [], constraints: [], preferences: [] };
const match = summary.match(/##\s+Key Decisions\s*\n([\s\S]*?)(?:\n##\s+|$)/i);
const section = match?.[1]?.trim();
if (!section) return empty;

const signals: DurableSignals = { decisions: [], constraints: [], preferences: [] };
const lines = section
.split('\n')
.map((line) => line.trim())
.filter(Boolean);
for (const line of lines) {
const normalized = line.replace(/^[*-]\s*/, '').trim();
if (!normalized) continue;
if (/^key decisions?:/i.test(normalized)) {
pushDurableItems(signals.decisions, normalized.replace(/^key decisions?:/i, '').trim());
continue;
}
if (/^constraints?:/i.test(normalized)) {
pushDurableItems(signals.constraints, normalized.replace(/^constraints?:/i, '').trim());
continue;

const decisionsSection = extractSummarySection(summary, 'Key Decisions');
if (decisionsSection) {
const lines = decisionsSection
.split('\n')
.map((line) => line.trim())
.filter(Boolean);
for (const line of lines) {
const normalized = line.replace(/^[*-]\s*/, '').trim();
if (!normalized) continue;
if (/^key decisions?:/i.test(normalized)) {
pushDurableItems(signals.decisions, normalized.replace(/^key decisions?:/i, '').trim());
continue;
}
if (/^constraints?:/i.test(normalized)) {
pushDurableItems(signals.constraints, normalized.replace(/^constraints?:/i, '').trim());
continue;
}
if (/^preferences?:/i.test(normalized)) {
pushDurableItems(signals.preferences, normalized.replace(/^preferences?:/i, '').trim());
continue;
}
pushUnique(signals.decisions, normalized);
}
if (/^preferences?:/i.test(normalized)) {
pushDurableItems(signals.preferences, normalized.replace(/^preferences?:/i, '').trim());
continue;
}

// User-Pinned Notes: content the user explicitly asked us to remember
// (in any language — the compressor prompt recognises the INTENT, not a
// keyword list). Each non-empty line is promoted to a durable preference
// verbatim so "记住 X" survives both compression AND the durable-memory
// promotion filter. Preferences is the right bucket because these are
// user-authored instructions that persist across sessions; decisions and
// constraints have implementation-specific semantics the user didn't
// necessarily intend.
const pinnedSection = extractSummarySection(summary, 'User-Pinned Notes');
if (pinnedSection) {
const lines = pinnedSection
.split('\n')
.map((line) => line.trim())
.filter(Boolean);
for (const line of lines) {
// Strip leading bullet markers but otherwise keep the line EXACTLY as
// the compressor produced it — the compressor is already instructed
// to preserve the user's original words.
const normalized = line.replace(/^[*-]\s*/, '').trim();
if (!normalized) continue;
pushUnique(signals.preferences, normalized);
}
pushUnique(signals.decisions, normalized);
}

return signals;
}

/** Extract the body of a `## <title>` section up to the next `## ` or EOF. */
function extractSummarySection(summary: string, title: string): string | null {
const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const re = new RegExp(`##\\s+${escaped}\\s*\\n([\\s\\S]*?)(?:\\n##\\s+|$)`, 'i');
const match = summary.match(re);
return match?.[1]?.trim() ?? null;
}

function pushDurableItems(bucket: string[], value: string): void {
if (!value) return;
for (const part of value.split(/\s*;\s*/)) {
Expand Down
7 changes: 7 additions & 0 deletions src/context/summary-compressor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,9 @@ function buildCompressionPrompt(
## Key Decisions
[Important technical decisions and why — include constraints and preferences]

## User-Pinned Notes
[If the user explicitly asked you to remember, memorize, take note of, or never forget any specific piece of information (in any language — e.g. English "remember/keep in mind/note this", Chinese "记住/记得/记下/牢记", Japanese "覚えて/覚えておいて", Korean "기억해줘", Spanish "recuerda", Russian "запомни", etc. — recognise the INTENT, not any fixed keyword list), copy the exact content here VERBATIM. Never paraphrase, summarise, translate, truncate, or reword pinned content. Preserve the user's original words exactly, including code, paths, numbers, names, and formatting. If there are no such requests in this batch, omit this section entirely.]

## Active State
[Files modified, test results, current branch — only if relevant]`;

Expand All @@ -538,6 +541,8 @@ ${serializedEvents}

Update the summary using this exact structure. PRESERVE all existing information that is still relevant. ADD new actions and outcomes. Move completed items from pending to resolved. Update active state. Remove information only if clearly obsolete.

CRITICAL — VERBATIM PRESERVATION RULE: If the previous summary contains a "User-Pinned Notes" section, every line in it MUST be carried forward UNCHANGED (word-for-word, character-for-character) into the updated summary. Also scan NEW EVENTS for any user message expressing an intent to be remembered (in any language — see the "User-Pinned Notes" description below). Append such content verbatim to that section. Never drop, paraphrase, translate, or compress pinned content, even if it looks redundant.

${template}

Target ~${targetTokens} tokens. Be CONCRETE — include file paths, error messages, and specific values. Write only the summary.`;
Expand All @@ -552,6 +557,8 @@ Use this exact structure:

${template}

CRITICAL — VERBATIM PRESERVATION RULE: If any user message in the events above expresses an intent to be remembered (in any language — see the "User-Pinned Notes" description above), copy that exact content word-for-word into the "User-Pinned Notes" section. Never paraphrase, translate, summarise, or reorder pinned content.

Target ~${targetTokens} tokens. Be CONCRETE — include file paths, error messages, and specific values. Write only the summary.`;
}

Expand Down
Loading