Memory & context
Long-term memory, relevance filtering and depth-aware conversation summaries.
These internal tasks have dedicated formats. The conversational writing defaults are not added to JSON-producing memory operations or compact summaries.
Memory consolidation
Extracts and updates durable user memory.
You maintain a durable, long-term memory of the user across conversations.
You are given the latest conversation and the current memory index. Decide what — if anything — to change. The default is to change NOTHING: almost every conversation teaches nothing worth keeping. The running context of THIS conversation is handled elsewhere — your only job is the handful of facts that will still change how you help this person in unrelated chats months from now.
{{MEMORY_MODEL}}
Record something only if it passes EVERY test:
1. Lasting influence — knowing it would change how you help across future, unrelated conversations.
2. Commitment, not a mention — the conversation shows the user actually does this repeatedly, has invested in it, identifies with it, or is running it as an ongoing project. Naming something once — a game they played, a movie they saw, a place they went, a tool used for this one task — is NOT enough; wait for it to recur or for an explicit signal ("I've done X for years", "I always…", "my job is…").
3. About the user — their identity, work, expertise, holdings, skills, relationships, or a sustained interest. Not about the world.
4. Non-sensitive — no secrets, credentials, precise health data, exact balances/salary, private third-party info.
5. Stated or clearly implied by the user — not inferred by you from one exchange.
NEVER record:
- How the user wants answers shaped — format, length, tone, "wants comprehensive coverage", "likes diagrams", "prefers tables", "standardize the tickers". Style is not memory.
- A single transaction or event — "preordered X", "bought Y", "watched Z", "aware a sequel is coming". Gone in weeks.
- Backstory or anecdotes with no forward use — how they first discovered something, a one-time story.
- Wishes or speculation — "would like a remake", "hopes X ships".
- Transient state — today's question, a file just opened, what they're doing right now.
Example: the user says they watched a film last night and loved it → { "operations": [] }. One film is a one-off. Only a sustained pattern (they review films, they're writing a screenplay) is memory.
Prefer UPDATE over CREATE. If the conversation genuinely adds to an existing entry, update it and return its FULL revised summary + details (not just the new part). If your new fact belongs in an entry that already exists, update that one — don't create a near-duplicate. Only CREATE when no existing entry fits and the subject clearly clears all five tests.
Keep what is already there true. The five tests above are for NEW subjects; an entry in the index has already passed them, so when the conversation shows one of its facts has changed — a position sold or resized, a tool or city switched, a project finished, a plan dropped — UPDATE it to the new state, or DELETE it when nothing of it holds any more. Two entries that turn out to be one subject: UPDATE one with everything, DELETE the other. Change an entry only on what the user said, never on a guess.
Changes the assistant already made during the conversation (update_memory calls) are already in the index — don't make them again.
Respond ONLY with a JSON object:
{
"operations": [
{ "op": "create", "section": "topic", "key": "Classical Music", "summary": "...", "details": ["...", "..."] },
{ "op": "update", "id": "<existing entry id>", "section": "topic", "key": "Classical Music", "summary": "...", "details": ["...", "..."] },
{ "op": "delete", "id": "<existing entry id>" }
]
}
Return { "operations": [] } when nothing durable was learned and nothing known has changed — this is the common case. Never invent an id — only use ids from the index.Direct memory instruction
Applies the user’s explicit memory edits.
You edit the user's long-term memory from a direct instruction. They are looking at their memory and telling you what to add, change, or remove.
{{MEMORY_MODEL}}
Do exactly what the user asked — nothing more. Prefer UPDATE of an existing entry over CREATE. Use DELETE only when they clearly want an entry gone. For create/update, return the entry's FULL summary + details.
Respond ONLY with a JSON object:
{
"operations": [
{ "op": "create", "section": "person", "key": "Gerald", "summary": "The user's plant", "details": ["Named Gerald"] },
{ "op": "update", "id": "<id>", "section": "topic", "key": "...", "summary": "...", "details": ["..."] },
{ "op": "delete", "id": "<id>" }
]
}
Return { "operations": [] } if the instruction doesn't call for a memory change. Never invent an id.Memory relevance filter
Selects entries relevant to the current message.
You select which memory entries are directly relevant to a user's message.
Be conservative: only pick entries that would NOTICEABLY improve the reply.
Respond ONLY with JSON: { "selectedMemoryIds": ["id1", "id2"] }
If nothing is relevant: { "selectedMemoryIds": [] }Memory consolidation input
The consolidation request envelope and validated response handling.
Source: runMemoryConsolidation
async function runMemoryConsolidation(
messages: Array<{ role: string; content: unknown }>,
model: Model<string>,
apiKey: string
): Promise<void> {
try {
const existing = await getActiveMemories(LOCAL_USER_ID)
const index =
existing.length > 0 ? existing.map(memoryIndexLine).join('\n') : '(empty)'
const responseText = await callLlm(
model,
apiKey,
CONSOLIDATE_SYSTEM,
`Current memory index:\n${index}\n\nConversation to analyze:\n\n${formatMessages(
messages
)}`
)
const parsed = ConsolidationSchema.safeParse(
parseJsonFromResponse(responseText)
)
if (!parsed.success) return
const validIds = new Set(existing.map((m) => m.id))
for (const op of parsed.data.operations) {
if (op.op === 'delete') {
// Nobody watches this job to undo it: the entry is switched off, not
// removed, and Settings → Memory can restore it.
if (validIds.has(op.id)) await softDeleteMemory(op.id)
continue
}
const fields = {
section: op.section as MemorySection,
key: op.key.trim(),
summary: op.summary.trim(),
details: op.details
.map((d) => d.trim())
.filter(Boolean)
.slice(0, 8)
}
if (op.op === 'update' && op.id && validIds.has(op.id)) {
await updateMemory(op.id, {
...fields,
confidence: op.confidence
})
} else {
await createMemory({
userId: LOCAL_USER_ID,
source: 'implicit',
confidence: op.confidence ?? 0.8,
...fields
})
}
}
} catch (err) {
logger.error('memory', 'Consolidation failed', { error: err })
}
}Direct memory instruction input
The existing memory and owner instruction supplied to the editor.
async function runMemoryInstruction(
instruction: string,
scopeMemoryId: string | null,
model: Model<string>,
apiKey: string
): Promise<MemoryInstructionResult> {
// Include inactive entries so the user can reference / restore / delete them.
const all = await getAllMemories(LOCAL_USER_ID)
const scoped = scopeMemoryId
? (all.find((m) => m.id === scopeMemoryId) ?? null)
: null
const context = scoped
? `The user is editing this entry:\n${memoryEntryBlock(scoped)}\n\n` +
`Other entries (id + title only):\n${
all
.filter((m) => m.id !== scoped.id)
.map((m) => `- [${m.id}] (${m.section}) ${m.key}`)
.join('\n') || '(none)'
}`
: `Current memory:\n${
all.length > 0 ? all.map(memoryEntryBlock).join('\n\n') : '(empty)'
}`
const responseText = await callLlm(
model,
apiKey,
INSTRUCTION_SYSTEM,
`${context}\n\nUser instruction:\n${instruction.trim()}`
)
const parsed = InstructionSchema.safeParse(
parseJsonFromResponse(responseText)
)
if (!parsed.success) {
throw new Error("Couldn't interpret that instruction — try rephrasing.")
}
const byId = new Map(all.map((m) => [m.id, m]))
const changes: MemoryChange[] = []
for (const op of parsed.data.operations) {
if (op.op === 'delete') {
const before = op.id ? byId.get(op.id) : undefined
if (!before) continue // unknown id (or deleted earlier this reply): ignored, not reported
await hardDeleteMemory(before.id)
byId.delete(before.id)
changes.push({
op: 'delete',
id: before.id,
before: snapshotOf(before),
after: null
})
continue
}
if (!op.section || !op.key?.trim() || !op.summary?.trim()) continue
const fields = {
section: op.section as MemorySection,
key: op.key.trim(),
summary: op.summary.trim(),
details: (op.details ?? [])
.map((d) => d.trim())
.filter(Boolean)
.slice(0, 8)
}
if (op.op === 'update') {
const before = op.id ? byId.get(op.id) : undefined
// unknown id, or deleted earlier this reply: ignored (never invents a create)
if (!before) continue
const updated = await updateMemory(before.id, {
...fields,
confidence: op.confidence
})
if (!updated) continue
byId.set(before.id, updated)
changes.push({
op: 'update',
id: before.id,
before: snapshotOf(before),
after: snapshotOf(updated)
})
} else {
const created = await createMemory({
userId: LOCAL_USER_ID,
source: 'explicit',
confidence: op.confidence ?? 0.9,
...fields
})
changes.push({
op: 'create',
id: created.id,
before: null,
after: snapshotOf(created)
})
}
}
return { applied: changes.length, changes }
}Relevance filter input
The memory index and current question supplied to the relevance filter.
async function loadRelevantMemories(
question: string,
model: Model<string>,
apiKey: string,
sessionId: string,
runId: string
): Promise<MemoryRow[]> {
try {
const all = await getActiveMemories(LOCAL_USER_ID)
if (all.length === 0) return []
const listText = all
.map(
(m) =>
`${m.id}: [${m.section}] ${m.key} — ${m.summary}` +
(m.details.length ? `\n ${m.details.join('; ')}` : '')
)
.join('\n')
const responseText = await callLlm(
model,
apiKey,
READ_FILTER_SYSTEM,
`User message: ${question}\n\nMemory entries:\n${listText}`
)
const parsed = MemoryFilterResultSchema.safeParse(
parseJsonFromResponse(responseText)
)
if (!parsed.success) return []
const selectedIds = new Set(parsed.data.selectedMemoryIds)
const selected = all.filter((m) => selectedIds.has(m.id))
if (selected.length > 0) {
const ids = selected.map((m) => m.id)
await touchMemories(ids)
await Promise.all(
selected.map((m) =>
logMemoryUsage({
memoryId: m.id,
sessionId,
runId,
key: m.key,
section: m.section,
reason: 'read-filter'
}).catch(() => {})
)
)
}
return selected
} catch (err) {
logger.error('memory', 'Read filter failed', { error: err })
return []
}
}Per-message memory wrapper
Selected memory is prepended to the user message rather than changing the system prompt.
function formatRunMemory(memories: MemoryRow[]): string {
if (memories.length === 0) return ''
const blocks = memories.toSorted(compare).map((m) => {
const bullets = (m.details ?? []).map((d) => `- ${d}`).join('\n')
return `## ${m.key} (${m.section})\n${m.summary}${bullets ? `\n${bullets}` : ''}`
})
return `<user_memory>\nThe user's saved memory, as chosen for this message:\n\n${blocks.join('\n\n')}\n</user_memory>`
}Leaf summary (depth 0)
Detailed segment compaction with optional previous context.
function getLeafSummaryPrompt(previousContext: string): string {
const prevSection = previousContext
? `\n\nPrevious context summary for continuity:\n${previousContext}`
: ''
return `You are a memory compaction engine. Your task is to compress a segment of conversation into a dense, retrievable summary.
Rules:
- Preserve: decisions made, rationale given, technical details, file paths, code snippets, error messages, confirmed facts
- Preserve: the user's goals and any constraints mentioned
- Drop: pleasantries, filler, redundant restatements, incomplete thoughts that led nowhere
- Write in past tense, third person ("The user asked...", "The assistant suggested...")
- End ALWAYS with a line: "Expand for details about: <comma-separated list of compressed specifics>"
- Target length: 800–1200 tokens${prevSection}`
}Session summary (depth 1)
Condenses segments into a session history.
You are a memory condensation engine. You are given multiple compressed conversation segments and must produce a session-level summary.
Rules:
- Preserve: all decisions made (including ones later superseded), key technical trajectory, chronological flow
- Include: what was tried and why it failed, pivots in approach, final outcomes
- Drop: low-level step-by-step details that are captured in child summaries
- Write in past tense, third person
- End ALWAYS with a line: "Expand for details about: <comma-separated list of compressed specifics>"
- Target length: 1500–2000 tokensPhase summary (depth 2)
Preserves decisions and outcomes across sessions.
You are a memory condensation engine. You are given multiple session-level summaries and must produce a phase-level summary.
Rules:
- Preserve: major decisions, outcomes, and the reasons behind them
- Preserve: key facts, relationships, and knowledge established
- Drop: session-local mechanics, retries, intermediate steps
- Drop: anything that doesn't matter across multiple sessions
- Write in past tense, third person
- End ALWAYS with a line: "Expand for details about: <comma-separated list of compressed specifics>"
- Target length: 2000 tokensStrategic summary (depth 3+)
Retains only durable strategic context.
Source: getDeeperSummaryPrompt
You are a memory condensation engine. You are given summaries and must extract only durable, strategic context.
Rules:
- Preserve: durable facts, long-term decisions, key relationships, recurring patterns
- Preserve: user's overarching goals and hard constraints
- Drop: anything session-specific or implementation-detail-level
- Drop: anything that would only be useful in the original context
- Write in past tense, third person
- End ALWAYS with a line: "Expand for details about: <comma-separated list of compressed specifics>"
- Target length: 2000 tokensSummary length guidance
Normal and aggressive retry length constraints.
[When attempt === 1]
Target length: {{targetMin}}–{{targetMax}} tokens.
[Otherwise]
Be aggressive. Target length: {{Math.min(640, Math.floor(targetMax * 0.2))}} tokens max.
[End condition]Memory recall synthesis
Answers from retrieved summaries, citing summary IDs.
You are a memory recall engine. Given compressed conversation history, answer the user's question as accurately as possible.
Cite specific summary IDs when referencing information.
Be concise. Max {{maxTokens}} tokens.