init: add source code from src.zip

This commit is contained in:
sigridjineth
2026-03-31 01:55:58 -07:00
commit f5a40b86de
1902 changed files with 513237 additions and 0 deletions

View File

@@ -0,0 +1,141 @@
import { feature } from 'bun:bundle'
import { logForDebugging } from '../utils/debug.js'
import { errorMessage } from '../utils/errors.js'
import { getDefaultSonnetModel } from '../utils/model/model.js'
import { sideQuery } from '../utils/sideQuery.js'
import { jsonParse } from '../utils/slowOperations.js'
import {
formatMemoryManifest,
type MemoryHeader,
scanMemoryFiles,
} from './memoryScan.js'
export type RelevantMemory = {
path: string
mtimeMs: number
}
const SELECT_MEMORIES_SYSTEM_PROMPT = `You are selecting memories that will be useful to Claude Code as it processes a user's query. You will be given the user's query and a list of available memory files with their filenames and descriptions.
Return a list of filenames for the memories that will clearly be useful to Claude Code as it processes the user's query (up to 5). Only include memories that you are certain will be helpful based on their name and description.
- If you are unsure if a memory will be useful in processing the user's query, then do not include it in your list. Be selective and discerning.
- If there are no memories in the list that would clearly be useful, feel free to return an empty list.
- If a list of recently-used tools is provided, do not select memories that are usage reference or API documentation for those tools (Claude Code is already exercising them). DO still select memories containing warnings, gotchas, or known issues about those tools — active use is exactly when those matter.
`
/**
* Find memory files relevant to a query by scanning memory file headers
* and asking Sonnet to select the most relevant ones.
*
* Returns absolute file paths + mtime of the most relevant memories
* (up to 5). Excludes MEMORY.md (already loaded in system prompt).
* mtime is threaded through so callers can surface freshness to the
* main model without a second stat.
*
* `alreadySurfaced` filters paths shown in prior turns before the
* Sonnet call, so the selector spends its 5-slot budget on fresh
* candidates instead of re-picking files the caller will discard.
*/
export async function findRelevantMemories(
query: string,
memoryDir: string,
signal: AbortSignal,
recentTools: readonly string[] = [],
alreadySurfaced: ReadonlySet<string> = new Set(),
): Promise<RelevantMemory[]> {
const memories = (await scanMemoryFiles(memoryDir, signal)).filter(
m => !alreadySurfaced.has(m.filePath),
)
if (memories.length === 0) {
return []
}
const selectedFilenames = await selectRelevantMemories(
query,
memories,
signal,
recentTools,
)
const byFilename = new Map(memories.map(m => [m.filename, m]))
const selected = selectedFilenames
.map(filename => byFilename.get(filename))
.filter((m): m is MemoryHeader => m !== undefined)
// Fires even on empty selection: selection-rate needs the denominator,
// and -1 ages distinguish "ran, picked nothing" from "never ran".
if (feature('MEMORY_SHAPE_TELEMETRY')) {
/* eslint-disable @typescript-eslint/no-require-imports */
const { logMemoryRecallShape } =
require('./memoryShapeTelemetry.js') as typeof import('./memoryShapeTelemetry.js')
/* eslint-enable @typescript-eslint/no-require-imports */
logMemoryRecallShape(memories, selected)
}
return selected.map(m => ({ path: m.filePath, mtimeMs: m.mtimeMs }))
}
async function selectRelevantMemories(
query: string,
memories: MemoryHeader[],
signal: AbortSignal,
recentTools: readonly string[],
): Promise<string[]> {
const validFilenames = new Set(memories.map(m => m.filename))
const manifest = formatMemoryManifest(memories)
// When Claude Code is actively using a tool (e.g. mcp__X__spawn),
// surfacing that tool's reference docs is noise — the conversation
// already contains working usage. The selector otherwise matches
// on keyword overlap ("spawn" in query + "spawn" in a memory
// description → false positive).
const toolsSection =
recentTools.length > 0
? `\n\nRecently used tools: ${recentTools.join(', ')}`
: ''
try {
const result = await sideQuery({
model: getDefaultSonnetModel(),
system: SELECT_MEMORIES_SYSTEM_PROMPT,
skipSystemPromptPrefix: true,
messages: [
{
role: 'user',
content: `Query: ${query}\n\nAvailable memories:\n${manifest}${toolsSection}`,
},
],
max_tokens: 256,
output_format: {
type: 'json_schema',
schema: {
type: 'object',
properties: {
selected_memories: { type: 'array', items: { type: 'string' } },
},
required: ['selected_memories'],
additionalProperties: false,
},
},
signal,
querySource: 'memdir_relevance',
})
const textBlock = result.content.find(block => block.type === 'text')
if (!textBlock || textBlock.type !== 'text') {
return []
}
const parsed: { selected_memories: string[] } = jsonParse(textBlock.text)
return parsed.selected_memories.filter(f => validFilenames.has(f))
} catch (e) {
if (signal.aborted) {
return []
}
logForDebugging(
`[memdir] selectRelevantMemories failed: ${errorMessage(e)}`,
{ level: 'warn' },
)
return []
}
}

507
src/memdir/memdir.ts Normal file
View File

@@ -0,0 +1,507 @@
import { feature } from 'bun:bundle'
import { join } from 'path'
import { getFsImplementation } from '../utils/fsOperations.js'
import { getAutoMemPath, isAutoMemoryEnabled } from './paths.js'
/* eslint-disable @typescript-eslint/no-require-imports */
const teamMemPaths = feature('TEAMMEM')
? (require('./teamMemPaths.js') as typeof import('./teamMemPaths.js'))
: null
import { getKairosActive, getOriginalCwd } from '../bootstrap/state.js'
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js'
/* eslint-enable @typescript-eslint/no-require-imports */
import {
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
logEvent,
} from '../services/analytics/index.js'
import { GREP_TOOL_NAME } from '../tools/GrepTool/prompt.js'
import { isReplModeEnabled } from '../tools/REPLTool/constants.js'
import { logForDebugging } from '../utils/debug.js'
import { hasEmbeddedSearchTools } from '../utils/embeddedTools.js'
import { isEnvTruthy } from '../utils/envUtils.js'
import { formatFileSize } from '../utils/format.js'
import { getProjectDir } from '../utils/sessionStorage.js'
import { getInitialSettings } from '../utils/settings/settings.js'
import {
MEMORY_FRONTMATTER_EXAMPLE,
TRUSTING_RECALL_SECTION,
TYPES_SECTION_INDIVIDUAL,
WHAT_NOT_TO_SAVE_SECTION,
WHEN_TO_ACCESS_SECTION,
} from './memoryTypes.js'
export const ENTRYPOINT_NAME = 'MEMORY.md'
export const MAX_ENTRYPOINT_LINES = 200
// ~125 chars/line at 200 lines. At p97 today; catches long-line indexes that
// slip past the line cap (p100 observed: 197KB under 200 lines).
export const MAX_ENTRYPOINT_BYTES = 25_000
const AUTO_MEM_DISPLAY_NAME = 'auto memory'
export type EntrypointTruncation = {
content: string
lineCount: number
byteCount: number
wasLineTruncated: boolean
wasByteTruncated: boolean
}
/**
* Truncate MEMORY.md content to the line AND byte caps, appending a warning
* that names which cap fired. Line-truncates first (natural boundary), then
* byte-truncates at the last newline before the cap so we don't cut mid-line.
*
* Shared by buildMemoryPrompt and claudemd getMemoryFiles (previously
* duplicated the line-only logic).
*/
export function truncateEntrypointContent(raw: string): EntrypointTruncation {
const trimmed = raw.trim()
const contentLines = trimmed.split('\n')
const lineCount = contentLines.length
const byteCount = trimmed.length
const wasLineTruncated = lineCount > MAX_ENTRYPOINT_LINES
// Check original byte count — long lines are the failure mode the byte cap
// targets, so post-line-truncation size would understate the warning.
const wasByteTruncated = byteCount > MAX_ENTRYPOINT_BYTES
if (!wasLineTruncated && !wasByteTruncated) {
return {
content: trimmed,
lineCount,
byteCount,
wasLineTruncated,
wasByteTruncated,
}
}
let truncated = wasLineTruncated
? contentLines.slice(0, MAX_ENTRYPOINT_LINES).join('\n')
: trimmed
if (truncated.length > MAX_ENTRYPOINT_BYTES) {
const cutAt = truncated.lastIndexOf('\n', MAX_ENTRYPOINT_BYTES)
truncated = truncated.slice(0, cutAt > 0 ? cutAt : MAX_ENTRYPOINT_BYTES)
}
const reason =
wasByteTruncated && !wasLineTruncated
? `${formatFileSize(byteCount)} (limit: ${formatFileSize(MAX_ENTRYPOINT_BYTES)}) — index entries are too long`
: wasLineTruncated && !wasByteTruncated
? `${lineCount} lines (limit: ${MAX_ENTRYPOINT_LINES})`
: `${lineCount} lines and ${formatFileSize(byteCount)}`
return {
content:
truncated +
`\n\n> WARNING: ${ENTRYPOINT_NAME} is ${reason}. Only part of it was loaded. Keep index entries to one line under ~200 chars; move detail into topic files.`,
lineCount,
byteCount,
wasLineTruncated,
wasByteTruncated,
}
}
/* eslint-disable @typescript-eslint/no-require-imports */
const teamMemPrompts = feature('TEAMMEM')
? (require('./teamMemPrompts.js') as typeof import('./teamMemPrompts.js'))
: null
/* eslint-enable @typescript-eslint/no-require-imports */
/**
* Shared guidance text appended to each memory directory prompt line.
* Shipped because Claude was burning turns on `ls`/`mkdir -p` before writing.
* Harness guarantees the directory exists via ensureMemoryDirExists().
*/
export const DIR_EXISTS_GUIDANCE =
'This directory already exists — write to it directly with the Write tool (do not run mkdir or check for its existence).'
export const DIRS_EXIST_GUIDANCE =
'Both directories already exist — write to them directly with the Write tool (do not run mkdir or check for their existence).'
/**
* Ensure a memory directory exists. Idempotent — called from loadMemoryPrompt
* (once per session via systemPromptSection cache) so the model can always
* write without checking existence first. FsOperations.mkdir is recursive
* by default and already swallows EEXIST, so the full parent chain
* (~/.claude/projects/<slug>/memory/) is created in one call with no
* try/catch needed for the happy path.
*/
export async function ensureMemoryDirExists(memoryDir: string): Promise<void> {
const fs = getFsImplementation()
try {
await fs.mkdir(memoryDir)
} catch (e) {
// fs.mkdir already handles EEXIST internally. Anything reaching here is
// a real problem (EACCES/EPERM/EROFS) — log so --debug shows why. Prompt
// building continues either way; the model's Write will surface the
// real perm error (and FileWriteTool does its own mkdir of the parent).
const code =
e instanceof Error && 'code' in e && typeof e.code === 'string'
? e.code
: undefined
logForDebugging(
`ensureMemoryDirExists failed for ${memoryDir}: ${code ?? String(e)}`,
{ level: 'debug' },
)
}
}
/**
* Log memory directory file/subdir counts asynchronously.
* Fire-and-forget — doesn't block prompt building.
*/
function logMemoryDirCounts(
memoryDir: string,
baseMetadata: Record<
string,
| number
| boolean
| AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
>,
): void {
const fs = getFsImplementation()
void fs.readdir(memoryDir).then(
dirents => {
let fileCount = 0
let subdirCount = 0
for (const d of dirents) {
if (d.isFile()) {
fileCount++
} else if (d.isDirectory()) {
subdirCount++
}
}
logEvent('tengu_memdir_loaded', {
...baseMetadata,
total_file_count: fileCount,
total_subdir_count: subdirCount,
})
},
() => {
// Directory unreadable — log without counts
logEvent('tengu_memdir_loaded', baseMetadata)
},
)
}
/**
* Build the typed-memory behavioral instructions (without MEMORY.md content).
* Constrains memories to a closed four-type taxonomy (user / feedback / project /
* reference) — content that is derivable from the current project state (code
* patterns, architecture, git history) is explicitly excluded.
*
* Individual-only variant: no `## Memory scope` section, no <scope> tags
* in type blocks, and team/private qualifiers stripped from examples.
*
* Used by both buildMemoryPrompt (agent memory, includes content) and
* loadMemoryPrompt (system prompt, content injected via user context instead).
*/
export function buildMemoryLines(
displayName: string,
memoryDir: string,
extraGuidelines?: string[],
skipIndex = false,
): string[] {
const howToSave = skipIndex
? [
'## How to save memories',
'',
'Write each memory to its own file (e.g., `user_role.md`, `feedback_testing.md`) using this frontmatter format:',
'',
...MEMORY_FRONTMATTER_EXAMPLE,
'',
'- Keep the name, description, and type fields in memory files up-to-date with the content',
'- Organize memory semantically by topic, not chronologically',
'- Update or remove memories that turn out to be wrong or outdated',
'- Do not write duplicate memories. First check if there is an existing memory you can update before writing a new one.',
]
: [
'## How to save memories',
'',
'Saving a memory is a two-step process:',
'',
'**Step 1** — write the memory to its own file (e.g., `user_role.md`, `feedback_testing.md`) using this frontmatter format:',
'',
...MEMORY_FRONTMATTER_EXAMPLE,
'',
`**Step 2** — add a pointer to that file in \`${ENTRYPOINT_NAME}\`. \`${ENTRYPOINT_NAME}\` is an index, not a memory — each entry should be one line, under ~150 characters: \`- [Title](file.md) — one-line hook\`. It has no frontmatter. Never write memory content directly into \`${ENTRYPOINT_NAME}\`.`,
'',
`- \`${ENTRYPOINT_NAME}\` is always loaded into your conversation context — lines after ${MAX_ENTRYPOINT_LINES} will be truncated, so keep the index concise`,
'- Keep the name, description, and type fields in memory files up-to-date with the content',
'- Organize memory semantically by topic, not chronologically',
'- Update or remove memories that turn out to be wrong or outdated',
'- Do not write duplicate memories. First check if there is an existing memory you can update before writing a new one.',
]
const lines: string[] = [
`# ${displayName}`,
'',
`You have a persistent, file-based memory system at \`${memoryDir}\`. ${DIR_EXISTS_GUIDANCE}`,
'',
"You should build up this memory system over time so that future conversations can have a complete picture of who the user is, how they'd like to collaborate with you, what behaviors to avoid or repeat, and the context behind the work the user gives you.",
'',
'If the user explicitly asks you to remember something, save it immediately as whichever type fits best. If they ask you to forget something, find and remove the relevant entry.',
'',
...TYPES_SECTION_INDIVIDUAL,
...WHAT_NOT_TO_SAVE_SECTION,
'',
...howToSave,
'',
...WHEN_TO_ACCESS_SECTION,
'',
...TRUSTING_RECALL_SECTION,
'',
'## Memory and other forms of persistence',
'Memory is one of several persistence mechanisms available to you as you assist the user in a given conversation. The distinction is often that memory can be recalled in future conversations and should not be used for persisting information that is only useful within the scope of the current conversation.',
'- When to use or update a plan instead of memory: If you are about to start a non-trivial implementation task and would like to reach alignment with the user on your approach you should use a Plan rather than saving this information to memory. Similarly, if you already have a plan within the conversation and you have changed your approach persist that change by updating the plan rather than saving a memory.',
'- When to use or update tasks instead of memory: When you need to break your work in current conversation into discrete steps or keep track of your progress use tasks instead of saving to memory. Tasks are great for persisting information about the work that needs to be done in the current conversation, but memory should be reserved for information that will be useful in future conversations.',
'',
...(extraGuidelines ?? []),
'',
]
lines.push(...buildSearchingPastContextSection(memoryDir))
return lines
}
/**
* Build the typed-memory prompt with MEMORY.md content included.
* Used by agent memory (which has no getClaudeMds() equivalent).
*/
export function buildMemoryPrompt(params: {
displayName: string
memoryDir: string
extraGuidelines?: string[]
}): string {
const { displayName, memoryDir, extraGuidelines } = params
const fs = getFsImplementation()
const entrypoint = memoryDir + ENTRYPOINT_NAME
// Directory creation is the caller's responsibility (loadMemoryPrompt /
// loadAgentMemoryPrompt). Builders only read, they don't mkdir.
// Read existing memory entrypoint (sync: prompt building is synchronous)
let entrypointContent = ''
try {
// eslint-disable-next-line custom-rules/no-sync-fs
entrypointContent = fs.readFileSync(entrypoint, { encoding: 'utf-8' })
} catch {
// No memory file yet
}
const lines = buildMemoryLines(displayName, memoryDir, extraGuidelines)
if (entrypointContent.trim()) {
const t = truncateEntrypointContent(entrypointContent)
const memoryType = displayName === AUTO_MEM_DISPLAY_NAME ? 'auto' : 'agent'
logMemoryDirCounts(memoryDir, {
content_length: t.byteCount,
line_count: t.lineCount,
was_truncated: t.wasLineTruncated,
was_byte_truncated: t.wasByteTruncated,
memory_type:
memoryType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
lines.push(`## ${ENTRYPOINT_NAME}`, '', t.content)
} else {
lines.push(
`## ${ENTRYPOINT_NAME}`,
'',
`Your ${ENTRYPOINT_NAME} is currently empty. When you save new memories, they will appear here.`,
)
}
return lines.join('\n')
}
/**
* Assistant-mode daily-log prompt. Gated behind feature('KAIROS').
*
* Assistant sessions are effectively perpetual, so the agent writes memories
* append-only to a date-named log file rather than maintaining MEMORY.md as
* a live index. A separate nightly /dream skill distills logs into topic
* files + MEMORY.md. MEMORY.md is still loaded into context (via claudemd.ts)
* as the distilled index — this prompt only changes where NEW memories go.
*/
function buildAssistantDailyLogPrompt(skipIndex = false): string {
const memoryDir = getAutoMemPath()
// Describe the path as a pattern rather than inlining today's literal path:
// this prompt is cached by systemPromptSection('memory', ...) and NOT
// invalidated on date change. The model derives the current date from the
// date_change attachment (appended at the tail on midnight rollover) rather
// than the user-context message — the latter is intentionally left stale to
// preserve the prompt cache prefix across midnight.
const logPathPattern = join(memoryDir, 'logs', 'YYYY', 'MM', 'YYYY-MM-DD.md')
const lines: string[] = [
'# auto memory',
'',
`You have a persistent, file-based memory system found at: \`${memoryDir}\``,
'',
"This session is long-lived. As you work, record anything worth remembering by **appending** to today's daily log file:",
'',
`\`${logPathPattern}\``,
'',
"Substitute today's date (from `currentDate` in your context) for `YYYY-MM-DD`. When the date rolls over mid-session, start appending to the new day's file.",
'',
'Write each entry as a short timestamped bullet. Create the file (and parent directories) on first write if it does not exist. Do not rewrite or reorganize the log — it is append-only. A separate nightly process distills these logs into `MEMORY.md` and topic files.',
'',
'## What to log',
'- User corrections and preferences ("use bun, not npm"; "stop summarizing diffs")',
'- Facts about the user, their role, or their goals',
'- Project context that is not derivable from the code (deadlines, incidents, decisions and their rationale)',
'- Pointers to external systems (dashboards, Linear projects, Slack channels)',
'- Anything the user explicitly asks you to remember',
'',
...WHAT_NOT_TO_SAVE_SECTION,
'',
...(skipIndex
? []
: [
`## ${ENTRYPOINT_NAME}`,
`\`${ENTRYPOINT_NAME}\` is the distilled index (maintained nightly from your logs) and is loaded into your context automatically. Read it for orientation, but do not edit it directly — record new information in today's log instead.`,
'',
]),
...buildSearchingPastContextSection(memoryDir),
]
return lines.join('\n')
}
/**
* Build the "Searching past context" section if the feature gate is enabled.
*/
export function buildSearchingPastContextSection(autoMemDir: string): string[] {
if (!getFeatureValue_CACHED_MAY_BE_STALE('tengu_coral_fern', false)) {
return []
}
const projectDir = getProjectDir(getOriginalCwd())
// Ant-native builds alias grep to embedded ugrep and remove the dedicated
// Grep tool, so give the model a real shell invocation there.
// In REPL mode, both Grep and Bash are hidden from direct use — the model
// calls them from inside REPL scripts, so the grep shell form is what it
// will write in the script anyway.
const embedded = hasEmbeddedSearchTools() || isReplModeEnabled()
const memSearch = embedded
? `grep -rn "<search term>" ${autoMemDir} --include="*.md"`
: `${GREP_TOOL_NAME} with pattern="<search term>" path="${autoMemDir}" glob="*.md"`
const transcriptSearch = embedded
? `grep -rn "<search term>" ${projectDir}/ --include="*.jsonl"`
: `${GREP_TOOL_NAME} with pattern="<search term>" path="${projectDir}/" glob="*.jsonl"`
return [
'## Searching past context',
'',
'When looking for past context:',
'1. Search topic files in your memory directory:',
'```',
memSearch,
'```',
'2. Session transcript logs (last resort — large files, slow):',
'```',
transcriptSearch,
'```',
'Use narrow search terms (error messages, file paths, function names) rather than broad keywords.',
'',
]
}
/**
* Load the unified memory prompt for inclusion in the system prompt.
* Dispatches based on which memory systems are enabled:
* - auto + team: combined prompt (both directories)
* - auto only: memory lines (single directory)
* Team memory requires auto memory (enforced by isTeamMemoryEnabled), so
* there is no team-only branch.
*
* Returns null when auto memory is disabled.
*/
export async function loadMemoryPrompt(): Promise<string | null> {
const autoEnabled = isAutoMemoryEnabled()
const skipIndex = getFeatureValue_CACHED_MAY_BE_STALE(
'tengu_moth_copse',
false,
)
// KAIROS daily-log mode takes precedence over TEAMMEM: the append-only
// log paradigm does not compose with team sync (which expects a shared
// MEMORY.md that both sides read + write). Gating on `autoEnabled` here
// means the !autoEnabled case falls through to the tengu_memdir_disabled
// telemetry block below, matching the non-KAIROS path.
if (feature('KAIROS') && autoEnabled && getKairosActive()) {
logMemoryDirCounts(getAutoMemPath(), {
memory_type:
'auto' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
return buildAssistantDailyLogPrompt(skipIndex)
}
// Cowork injects memory-policy text via env var; thread into all builders.
const coworkExtraGuidelines =
process.env.CLAUDE_COWORK_MEMORY_EXTRA_GUIDELINES
const extraGuidelines =
coworkExtraGuidelines && coworkExtraGuidelines.trim().length > 0
? [coworkExtraGuidelines]
: undefined
if (feature('TEAMMEM')) {
if (teamMemPaths!.isTeamMemoryEnabled()) {
const autoDir = getAutoMemPath()
const teamDir = teamMemPaths!.getTeamMemPath()
// Harness guarantees these directories exist so the model can write
// without checking. The prompt text reflects this ("already exists").
// Only creating teamDir is sufficient: getTeamMemPath() is defined as
// join(getAutoMemPath(), 'team'), so recursive mkdir of the team dir
// creates the auto dir as a side effect. If the team dir ever moves
// out from under the auto dir, add a second ensureMemoryDirExists call
// for autoDir here.
await ensureMemoryDirExists(teamDir)
logMemoryDirCounts(autoDir, {
memory_type:
'auto' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
logMemoryDirCounts(teamDir, {
memory_type:
'team' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
return teamMemPrompts!.buildCombinedMemoryPrompt(
extraGuidelines,
skipIndex,
)
}
}
if (autoEnabled) {
const autoDir = getAutoMemPath()
// Harness guarantees the directory exists so the model can write without
// checking. The prompt text reflects this ("already exists").
await ensureMemoryDirExists(autoDir)
logMemoryDirCounts(autoDir, {
memory_type:
'auto' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
return buildMemoryLines(
'auto memory',
autoDir,
extraGuidelines,
skipIndex,
).join('\n')
}
logEvent('tengu_memdir_disabled', {
disabled_by_env_var: isEnvTruthy(
process.env.CLAUDE_CODE_DISABLE_AUTO_MEMORY,
),
disabled_by_setting:
!isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_AUTO_MEMORY) &&
getInitialSettings().autoMemoryEnabled === false,
})
// Gate on the GB flag directly, not isTeamMemoryEnabled() — that function
// checks isAutoMemoryEnabled() first, which is definitionally false in this
// branch. We want "was this user in the team-memory cohort at all."
if (getFeatureValue_CACHED_MAY_BE_STALE('tengu_herring_clock', false)) {
logEvent('tengu_team_memdir_disabled', {})
}
return null
}

53
src/memdir/memoryAge.ts Normal file
View File

@@ -0,0 +1,53 @@
/**
* Days elapsed since mtime. Floor-rounded — 0 for today, 1 for
* yesterday, 2+ for older. Negative inputs (future mtime, clock skew)
* clamp to 0.
*/
export function memoryAgeDays(mtimeMs: number): number {
return Math.max(0, Math.floor((Date.now() - mtimeMs) / 86_400_000))
}
/**
* Human-readable age string. Models are poor at date arithmetic —
* a raw ISO timestamp doesn't trigger staleness reasoning the way
* "47 days ago" does.
*/
export function memoryAge(mtimeMs: number): string {
const d = memoryAgeDays(mtimeMs)
if (d === 0) return 'today'
if (d === 1) return 'yesterday'
return `${d} days ago`
}
/**
* Plain-text staleness caveat for memories >1 day old. Returns ''
* for fresh (today/yesterday) memories — warning there is noise.
*
* Use this when the consumer already provides its own wrapping
* (e.g. messages.ts relevant_memories → wrapMessagesInSystemReminder).
*
* Motivated by user reports of stale code-state memories (file:line
* citations to code that has since changed) being asserted as fact —
* the citation makes the stale claim sound more authoritative, not less.
*/
export function memoryFreshnessText(mtimeMs: number): string {
const d = memoryAgeDays(mtimeMs)
if (d <= 1) return ''
return (
`This memory is ${d} days old. ` +
`Memories are point-in-time observations, not live state — ` +
`claims about code behavior or file:line citations may be outdated. ` +
`Verify against current code before asserting as fact.`
)
}
/**
* Per-memory staleness note wrapped in <system-reminder> tags.
* Returns '' for memories ≤ 1 day old. Use this for callers that
* don't add their own system-reminder wrapper (e.g. FileReadTool output).
*/
export function memoryFreshnessNote(mtimeMs: number): string {
const text = memoryFreshnessText(mtimeMs)
if (!text) return ''
return `<system-reminder>${text}</system-reminder>\n`
}

94
src/memdir/memoryScan.ts Normal file
View File

@@ -0,0 +1,94 @@
/**
* Memory-directory scanning primitives. Split out of findRelevantMemories.ts
* so extractMemories can import the scan without pulling in sideQuery and
* the API-client chain (which closed a cycle through memdir.ts — #25372).
*/
import { readdir } from 'fs/promises'
import { basename, join } from 'path'
import { parseFrontmatter } from '../utils/frontmatterParser.js'
import { readFileInRange } from '../utils/readFileInRange.js'
import { type MemoryType, parseMemoryType } from './memoryTypes.js'
export type MemoryHeader = {
filename: string
filePath: string
mtimeMs: number
description: string | null
type: MemoryType | undefined
}
const MAX_MEMORY_FILES = 200
const FRONTMATTER_MAX_LINES = 30
/**
* Scan a memory directory for .md files, read their frontmatter, and return
* a header list sorted newest-first (capped at MAX_MEMORY_FILES). Shared by
* findRelevantMemories (query-time recall) and extractMemories (pre-injects
* the listing so the extraction agent doesn't spend a turn on `ls`).
*
* Single-pass: readFileInRange stats internally and returns mtimeMs, so we
* read-then-sort rather than stat-sort-read. For the common case (N ≤ 200)
* this halves syscalls vs a separate stat round; for large N we read a few
* extra small files but still avoid the double-stat on the surviving 200.
*/
export async function scanMemoryFiles(
memoryDir: string,
signal: AbortSignal,
): Promise<MemoryHeader[]> {
try {
const entries = await readdir(memoryDir, { recursive: true })
const mdFiles = entries.filter(
f => f.endsWith('.md') && basename(f) !== 'MEMORY.md',
)
const headerResults = await Promise.allSettled(
mdFiles.map(async (relativePath): Promise<MemoryHeader> => {
const filePath = join(memoryDir, relativePath)
const { content, mtimeMs } = await readFileInRange(
filePath,
0,
FRONTMATTER_MAX_LINES,
undefined,
signal,
)
const { frontmatter } = parseFrontmatter(content, filePath)
return {
filename: relativePath,
filePath,
mtimeMs,
description: frontmatter.description || null,
type: parseMemoryType(frontmatter.type),
}
}),
)
return headerResults
.filter(
(r): r is PromiseFulfilledResult<MemoryHeader> =>
r.status === 'fulfilled',
)
.map(r => r.value)
.sort((a, b) => b.mtimeMs - a.mtimeMs)
.slice(0, MAX_MEMORY_FILES)
} catch {
return []
}
}
/**
* Format memory headers as a text manifest: one line per file with
* [type] filename (timestamp): description. Used by both the recall
* selector prompt and the extraction-agent prompt.
*/
export function formatMemoryManifest(memories: MemoryHeader[]): string {
return memories
.map(m => {
const tag = m.type ? `[${m.type}] ` : ''
const ts = new Date(m.mtimeMs).toISOString()
return m.description
? `- ${tag}${m.filename} (${ts}): ${m.description}`
: `- ${tag}${m.filename} (${ts})`
})
.join('\n')
}

271
src/memdir/memoryTypes.ts Normal file
View File

@@ -0,0 +1,271 @@
/**
* Memory type taxonomy.
*
* Memories are constrained to four types capturing context NOT derivable
* from the current project state. Code patterns, architecture, git history,
* and file structure are derivable (via grep/git/CLAUDE.md) and should NOT
* be saved as memories.
*
* The two TYPES_SECTION_* exports below are intentionally duplicated rather
* than generated from a shared spec — keeping them flat makes per-mode edits
* trivial without reasoning through a helper's conditional rendering.
*/
export const MEMORY_TYPES = [
'user',
'feedback',
'project',
'reference',
] as const
export type MemoryType = (typeof MEMORY_TYPES)[number]
/**
* Parse a raw frontmatter value into a MemoryType.
* Invalid or missing values return undefined — legacy files without a
* `type:` field keep working, files with unknown types degrade gracefully.
*/
export function parseMemoryType(raw: unknown): MemoryType | undefined {
if (typeof raw !== 'string') return undefined
return MEMORY_TYPES.find(t => t === raw)
}
/**
* `## Types of memory` section for COMBINED mode (private + team directories).
* Includes <scope> tags and team/private qualifiers in examples.
*/
export const TYPES_SECTION_COMBINED: readonly string[] = [
'## Types of memory',
'',
'There are several discrete types of memory that you can store in your memory system. Each type below declares a <scope> of `private`, `team`, or guidance for choosing between the two.',
'',
'<types>',
'<type>',
' <name>user</name>',
' <scope>always private</scope>',
" <description>Contain information about the user's role, goals, responsibilities, and knowledge. Great user memories help you tailor your future behavior to the user's preferences and perspective. Your goal in reading and writing these memories is to build up an understanding of who the user is and how you can be most helpful to them specifically. For example, you should collaborate with a senior software engineer differently than a student who is coding for the very first time. Keep in mind, that the aim here is to be helpful to the user. Avoid writing memories about the user that could be viewed as a negative judgement or that are not relevant to the work you're trying to accomplish together.</description>",
" <when_to_save>When you learn any details about the user's role, preferences, responsibilities, or knowledge</when_to_save>",
" <how_to_use>When your work should be informed by the user's profile or perspective. For example, if the user is asking you to explain a part of the code, you should answer that question in a way that is tailored to the specific details that they will find most valuable or that helps them build their mental model in relation to domain knowledge they already have.</how_to_use>",
' <examples>',
" user: I'm a data scientist investigating what logging we have in place",
' assistant: [saves private user memory: user is a data scientist, currently focused on observability/logging]',
'',
" user: I've been writing Go for ten years but this is my first time touching the React side of this repo",
" assistant: [saves private user memory: deep Go expertise, new to React and this project's frontend — frame frontend explanations in terms of backend analogues]",
' </examples>',
'</type>',
'<type>',
' <name>feedback</name>',
' <scope>default to private. Save as team only when the guidance is clearly a project-wide convention that every contributor should follow (e.g., a testing policy, a build invariant), not a personal style preference.</scope>',
" <description>Guidance the user has given you about how to approach work — both what to avoid and what to keep doing. These are a very important type of memory to read and write as they allow you to remain coherent and responsive to the way you should approach work in the project. Record from failure AND success: if you only save corrections, you will avoid past mistakes but drift away from approaches the user has already validated, and may grow overly cautious. Before saving a private feedback memory, check that it doesn't contradict a team feedback memory — if it does, either don't save it or note the override explicitly.</description>",
' <when_to_save>Any time the user corrects your approach ("no not that", "don\'t", "stop doing X") OR confirms a non-obvious approach worked ("yes exactly", "perfect, keep doing that", accepting an unusual choice without pushback). Corrections are easy to notice; confirmations are quieter — watch for them. In both cases, save what is applicable to future conversations, especially if surprising or not obvious from the code. Include *why* so you can judge edge cases later.</when_to_save>',
' <how_to_use>Let these memories guide your behavior so that the user and other users in the project do not need to offer the same guidance twice.</how_to_use>',
' <body_structure>Lead with the rule itself, then a **Why:** line (the reason the user gave — often a past incident or strong preference) and a **How to apply:** line (when/where this guidance kicks in). Knowing *why* lets you judge edge cases instead of blindly following the rule.</body_structure>',
' <examples>',
" user: don't mock the database in these tests — we got burned last quarter when mocked tests passed but the prod migration failed",
' assistant: [saves team feedback memory: integration tests must hit a real database, not mocks. Reason: prior incident where mock/prod divergence masked a broken migration. Team scope: this is a project testing policy, not a personal preference]',
'',
' user: stop summarizing what you just did at the end of every response, I can read the diff',
" assistant: [saves private feedback memory: this user wants terse responses with no trailing summaries. Private because it's a communication preference, not a project convention]",
'',
" user: yeah the single bundled PR was the right call here, splitting this one would've just been churn",
' assistant: [saves private feedback memory: for refactors in this area, user prefers one bundled PR over many small ones. Confirmed after I chose this approach — a validated judgment call, not a correction]',
' </examples>',
'</type>',
'<type>',
' <name>project</name>',
' <scope>private or team, but strongly bias toward team</scope>',
' <description>Information that you learn about ongoing work, goals, initiatives, bugs, or incidents within the project that is not otherwise derivable from the code or git history. Project memories help you understand the broader context and motivation behind the work users are working on within this working directory.</description>',
' <when_to_save>When you learn who is doing what, why, or by when. These states change relatively quickly so try to keep your understanding of this up to date. Always convert relative dates in user messages to absolute dates when saving (e.g., "Thursday" → "2026-03-05"), so the memory remains interpretable after time passes.</when_to_save>',
" <how_to_use>Use these memories to more fully understand the details and nuance behind the user's request, anticipate coordination issues across users, make better informed suggestions.</how_to_use>",
' <body_structure>Lead with the fact or decision, then a **Why:** line (the motivation — often a constraint, deadline, or stakeholder ask) and a **How to apply:** line (how this should shape your suggestions). Project memories decay fast, so the why helps future-you judge whether the memory is still load-bearing.</body_structure>',
' <examples>',
" user: we're freezing all non-critical merges after Thursday — mobile team is cutting a release branch",
' assistant: [saves team project memory: merge freeze begins 2026-03-05 for mobile release cut. Flag any non-critical PR work scheduled after that date]',
'',
" user: the reason we're ripping out the old auth middleware is that legal flagged it for storing session tokens in a way that doesn't meet the new compliance requirements",
' assistant: [saves team project memory: auth middleware rewrite is driven by legal/compliance requirements around session token storage, not tech-debt cleanup — scope decisions should favor compliance over ergonomics]',
' </examples>',
'</type>',
'<type>',
' <name>reference</name>',
' <scope>usually team</scope>',
' <description>Stores pointers to where information can be found in external systems. These memories allow you to remember where to look to find up-to-date information outside of the project directory.</description>',
' <when_to_save>When you learn about resources in external systems and their purpose. For example, that bugs are tracked in a specific project in Linear or that feedback can be found in a specific Slack channel.</when_to_save>',
' <how_to_use>When the user references an external system or information that may be in an external system.</how_to_use>',
' <examples>',
' user: check the Linear project "INGEST" if you want context on these tickets, that\'s where we track all pipeline bugs',
' assistant: [saves team reference memory: pipeline bugs are tracked in Linear project "INGEST"]',
'',
" user: the Grafana board at grafana.internal/d/api-latency is what oncall watches — if you're touching request handling, that's the thing that'll page someone",
' assistant: [saves team reference memory: grafana.internal/d/api-latency is the oncall latency dashboard — check it when editing request-path code]',
' </examples>',
'</type>',
'</types>',
'',
]
/**
* `## Types of memory` section for INDIVIDUAL-ONLY mode (single directory).
* No <scope> tags. Examples use plain `[saves X memory: …]`. Prose that
* only makes sense with a private/team split is reworded.
*/
export const TYPES_SECTION_INDIVIDUAL: readonly string[] = [
'## Types of memory',
'',
'There are several discrete types of memory that you can store in your memory system:',
'',
'<types>',
'<type>',
' <name>user</name>',
" <description>Contain information about the user's role, goals, responsibilities, and knowledge. Great user memories help you tailor your future behavior to the user's preferences and perspective. Your goal in reading and writing these memories is to build up an understanding of who the user is and how you can be most helpful to them specifically. For example, you should collaborate with a senior software engineer differently than a student who is coding for the very first time. Keep in mind, that the aim here is to be helpful to the user. Avoid writing memories about the user that could be viewed as a negative judgement or that are not relevant to the work you're trying to accomplish together.</description>",
" <when_to_save>When you learn any details about the user's role, preferences, responsibilities, or knowledge</when_to_save>",
" <how_to_use>When your work should be informed by the user's profile or perspective. For example, if the user is asking you to explain a part of the code, you should answer that question in a way that is tailored to the specific details that they will find most valuable or that helps them build their mental model in relation to domain knowledge they already have.</how_to_use>",
' <examples>',
" user: I'm a data scientist investigating what logging we have in place",
' assistant: [saves user memory: user is a data scientist, currently focused on observability/logging]',
'',
" user: I've been writing Go for ten years but this is my first time touching the React side of this repo",
" assistant: [saves user memory: deep Go expertise, new to React and this project's frontend — frame frontend explanations in terms of backend analogues]",
' </examples>',
'</type>',
'<type>',
' <name>feedback</name>',
' <description>Guidance the user has given you about how to approach work — both what to avoid and what to keep doing. These are a very important type of memory to read and write as they allow you to remain coherent and responsive to the way you should approach work in the project. Record from failure AND success: if you only save corrections, you will avoid past mistakes but drift away from approaches the user has already validated, and may grow overly cautious.</description>',
' <when_to_save>Any time the user corrects your approach ("no not that", "don\'t", "stop doing X") OR confirms a non-obvious approach worked ("yes exactly", "perfect, keep doing that", accepting an unusual choice without pushback). Corrections are easy to notice; confirmations are quieter — watch for them. In both cases, save what is applicable to future conversations, especially if surprising or not obvious from the code. Include *why* so you can judge edge cases later.</when_to_save>',
' <how_to_use>Let these memories guide your behavior so that the user does not need to offer the same guidance twice.</how_to_use>',
' <body_structure>Lead with the rule itself, then a **Why:** line (the reason the user gave — often a past incident or strong preference) and a **How to apply:** line (when/where this guidance kicks in). Knowing *why* lets you judge edge cases instead of blindly following the rule.</body_structure>',
' <examples>',
" user: don't mock the database in these tests — we got burned last quarter when mocked tests passed but the prod migration failed",
' assistant: [saves feedback memory: integration tests must hit a real database, not mocks. Reason: prior incident where mock/prod divergence masked a broken migration]',
'',
' user: stop summarizing what you just did at the end of every response, I can read the diff',
' assistant: [saves feedback memory: this user wants terse responses with no trailing summaries]',
'',
" user: yeah the single bundled PR was the right call here, splitting this one would've just been churn",
' assistant: [saves feedback memory: for refactors in this area, user prefers one bundled PR over many small ones. Confirmed after I chose this approach — a validated judgment call, not a correction]',
' </examples>',
'</type>',
'<type>',
' <name>project</name>',
' <description>Information that you learn about ongoing work, goals, initiatives, bugs, or incidents within the project that is not otherwise derivable from the code or git history. Project memories help you understand the broader context and motivation behind the work the user is doing within this working directory.</description>',
' <when_to_save>When you learn who is doing what, why, or by when. These states change relatively quickly so try to keep your understanding of this up to date. Always convert relative dates in user messages to absolute dates when saving (e.g., "Thursday" → "2026-03-05"), so the memory remains interpretable after time passes.</when_to_save>',
" <how_to_use>Use these memories to more fully understand the details and nuance behind the user's request and make better informed suggestions.</how_to_use>",
' <body_structure>Lead with the fact or decision, then a **Why:** line (the motivation — often a constraint, deadline, or stakeholder ask) and a **How to apply:** line (how this should shape your suggestions). Project memories decay fast, so the why helps future-you judge whether the memory is still load-bearing.</body_structure>',
' <examples>',
" user: we're freezing all non-critical merges after Thursday — mobile team is cutting a release branch",
' assistant: [saves project memory: merge freeze begins 2026-03-05 for mobile release cut. Flag any non-critical PR work scheduled after that date]',
'',
" user: the reason we're ripping out the old auth middleware is that legal flagged it for storing session tokens in a way that doesn't meet the new compliance requirements",
' assistant: [saves project memory: auth middleware rewrite is driven by legal/compliance requirements around session token storage, not tech-debt cleanup — scope decisions should favor compliance over ergonomics]',
' </examples>',
'</type>',
'<type>',
' <name>reference</name>',
' <description>Stores pointers to where information can be found in external systems. These memories allow you to remember where to look to find up-to-date information outside of the project directory.</description>',
' <when_to_save>When you learn about resources in external systems and their purpose. For example, that bugs are tracked in a specific project in Linear or that feedback can be found in a specific Slack channel.</when_to_save>',
' <how_to_use>When the user references an external system or information that may be in an external system.</how_to_use>',
' <examples>',
' user: check the Linear project "INGEST" if you want context on these tickets, that\'s where we track all pipeline bugs',
' assistant: [saves reference memory: pipeline bugs are tracked in Linear project "INGEST"]',
'',
" user: the Grafana board at grafana.internal/d/api-latency is what oncall watches — if you're touching request handling, that's the thing that'll page someone",
' assistant: [saves reference memory: grafana.internal/d/api-latency is the oncall latency dashboard — check it when editing request-path code]',
' </examples>',
'</type>',
'</types>',
'',
]
/**
* `## What NOT to save in memory` section. Identical across both modes.
*/
export const WHAT_NOT_TO_SAVE_SECTION: readonly string[] = [
'## What NOT to save in memory',
'',
'- Code patterns, conventions, architecture, file paths, or project structure — these can be derived by reading the current project state.',
'- Git history, recent changes, or who-changed-what — `git log` / `git blame` are authoritative.',
'- Debugging solutions or fix recipes — the fix is in the code; the commit message has the context.',
'- Anything already documented in CLAUDE.md files.',
'- Ephemeral task details: in-progress work, temporary state, current conversation context.',
'',
// H2: explicit-save gate. Eval-validated (memory-prompt-iteration case 3,
// 0/2 → 3/3): prevents "save this week's PR list" → activity-log noise.
'These exclusions apply even when the user explicitly asks you to save. If they ask you to save a PR list or activity summary, ask what was *surprising* or *non-obvious* about it — that is the part worth keeping.',
]
/**
* Recall-side drift caveat. Single bullet under `## When to access memories`.
* Proactive: verify memory against current state before answering.
*/
export const MEMORY_DRIFT_CAVEAT =
'- Memory records can become stale over time. Use memory as context for what was true at a given point in time. Before answering the user or building assumptions based solely on information in memory records, verify that the memory is still correct and up-to-date by reading the current state of the files or resources. If a recalled memory conflicts with current information, trust what you observe now — and update or remove the stale memory rather than acting on it.'
/**
* `## When to access memories` section. Includes MEMORY_DRIFT_CAVEAT.
*
* H6 (branch-pollution evals #22856, case 5 1/3 on capy): the "ignore" bullet
* is the delta. Failure mode: user says "ignore memory about X" → Claude reads
* code correctly but adds "not Y as noted in memory" — treats "ignore" as
* "acknowledge then override" rather than "don't reference at all." The bullet
* names that anti-pattern explicitly.
*
* Token budget (H6a): merged old bullets 1+2, tightened both. Old 4 lines
* were ~70 tokens; new 4 lines are ~73 tokens. Net ~+3.
*/
export const WHEN_TO_ACCESS_SECTION: readonly string[] = [
'## When to access memories',
'- When memories seem relevant, or the user references prior-conversation work.',
'- You MUST access memory when the user explicitly asks you to check, recall, or remember.',
'- If the user says to *ignore* or *not use* memory: proceed as if MEMORY.md were empty. Do not apply remembered facts, cite, compare against, or mention memory content.',
MEMORY_DRIFT_CAVEAT,
]
/**
* `## Trusting what you recall` section. Heavier-weight guidance on HOW to
* treat a memory once you've recalled it — separate from WHEN to access.
*
* Eval-validated (memory-prompt-iteration.eval.ts, 2026-03-17):
* H1 (verify function/file claims): 0/2 → 3/3 via appendSystemPrompt. When
* buried as a bullet under "When to access", dropped to 0/3 — position
* matters. The H1 cue is about what to DO with a memory, not when to
* look, so it needs its own section-level trigger context.
* H5 (read-side noise rejection): 0/2 → 3/3 via appendSystemPrompt, 2/3
* in-place as a bullet. Partial because "snapshot" is intuitively closer
* to "when to access" than H1 is.
*
* Known gap: H1 doesn't cover slash-command claims (0/3 on the /fork case —
* slash commands aren't files or functions in the model's ontology).
*/
export const TRUSTING_RECALL_SECTION: readonly string[] = [
// Header wording matters: "Before recommending" (action cue at the decision
// point) tested better than "Trusting what you recall" (abstract). The
// appendSystemPrompt variant with this header went 3/3; the abstract header
// went 0/3 in-place. Same body text — only the header differed.
'## Before recommending from memory',
'',
'A memory that names a specific function, file, or flag is a claim that it existed *when the memory was written*. It may have been renamed, removed, or never merged. Before recommending it:',
'',
'- If the memory names a file path: check the file exists.',
'- If the memory names a function or flag: grep for it.',
'- If the user is about to act on your recommendation (not just asking about history), verify first.',
'',
'"The memory says X exists" is not the same as "X exists now."',
'',
'A memory that summarizes repo state (activity logs, architecture snapshots) is frozen in time. If the user asks about *recent* or *current* state, prefer `git log` or reading the code over recalling the snapshot.',
]
/**
* Frontmatter format example with the `type` field.
*/
export const MEMORY_FRONTMATTER_EXAMPLE: readonly string[] = [
'```markdown',
'---',
'name: {{memory name}}',
'description: {{one-line description — used to decide relevance in future conversations, so be specific}}',
`type: {{${MEMORY_TYPES.join(', ')}}}`,
'---',
'',
'{{memory content — for feedback/project types, structure as: rule/fact, then **Why:** and **How to apply:** lines}}',
'```',
]

278
src/memdir/paths.ts Normal file
View File

@@ -0,0 +1,278 @@
import memoize from 'lodash-es/memoize.js'
import { homedir } from 'os'
import { isAbsolute, join, normalize, sep } from 'path'
import {
getIsNonInteractiveSession,
getProjectRoot,
} from '../bootstrap/state.js'
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js'
import {
getClaudeConfigHomeDir,
isEnvDefinedFalsy,
isEnvTruthy,
} from '../utils/envUtils.js'
import { findCanonicalGitRoot } from '../utils/git.js'
import { sanitizePath } from '../utils/path.js'
import {
getInitialSettings,
getSettingsForSource,
} from '../utils/settings/settings.js'
/**
* Whether auto-memory features are enabled (memdir, agent memory, past session search).
* Enabled by default. Priority chain (first defined wins):
* 1. CLAUDE_CODE_DISABLE_AUTO_MEMORY env var (1/true → OFF, 0/false → ON)
* 2. CLAUDE_CODE_SIMPLE (--bare) → OFF
* 3. CCR without persistent storage → OFF (no CLAUDE_CODE_REMOTE_MEMORY_DIR)
* 4. autoMemoryEnabled in settings.json (supports project-level opt-out)
* 5. Default: enabled
*/
export function isAutoMemoryEnabled(): boolean {
const envVal = process.env.CLAUDE_CODE_DISABLE_AUTO_MEMORY
if (isEnvTruthy(envVal)) {
return false
}
if (isEnvDefinedFalsy(envVal)) {
return true
}
// --bare / SIMPLE: prompts.ts already drops the memory section from the
// system prompt via its SIMPLE early-return; this gate stops the other half
// (extractMemories turn-end fork, autoDream, /remember, /dream, team sync).
if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) {
return false
}
if (
isEnvTruthy(process.env.CLAUDE_CODE_REMOTE) &&
!process.env.CLAUDE_CODE_REMOTE_MEMORY_DIR
) {
return false
}
const settings = getInitialSettings()
if (settings.autoMemoryEnabled !== undefined) {
return settings.autoMemoryEnabled
}
return true
}
/**
* Whether the extract-memories background agent will run this session.
*
* The main agent's prompt always has full save instructions regardless of
* this gate — when the main agent writes memories, the background agent
* skips that range (hasMemoryWritesSince in extractMemories.ts); when it
* doesn't, the background agent catches anything missed.
*
* Callers must also gate on feature('EXTRACT_MEMORIES') — that check cannot
* live inside this helper because feature() only tree-shakes when used
* directly in an `if` condition.
*/
export function isExtractModeActive(): boolean {
if (!getFeatureValue_CACHED_MAY_BE_STALE('tengu_passport_quail', false)) {
return false
}
return (
!getIsNonInteractiveSession() ||
getFeatureValue_CACHED_MAY_BE_STALE('tengu_slate_thimble', false)
)
}
/**
* Returns the base directory for persistent memory storage.
* Resolution order:
* 1. CLAUDE_CODE_REMOTE_MEMORY_DIR env var (explicit override, set in CCR)
* 2. ~/.claude (default config home)
*/
export function getMemoryBaseDir(): string {
if (process.env.CLAUDE_CODE_REMOTE_MEMORY_DIR) {
return process.env.CLAUDE_CODE_REMOTE_MEMORY_DIR
}
return getClaudeConfigHomeDir()
}
const AUTO_MEM_DIRNAME = 'memory'
const AUTO_MEM_ENTRYPOINT_NAME = 'MEMORY.md'
/**
* Normalize and validate a candidate auto-memory directory path.
*
* SECURITY: Rejects paths that would be dangerous as a read-allowlist root
* or that normalize() doesn't fully resolve:
* - relative (!isAbsolute): "../foo" — would be interpreted relative to CWD
* - root/near-root (length < 3): "/" → "" after strip; "/a" too short
* - Windows drive-root (C: regex): "C:\" → "C:" after strip
* - UNC paths (\\server\share): network paths — opaque trust boundary
* - null byte: survives normalize(), can truncate in syscalls
*
* Returns the normalized path with exactly one trailing separator,
* or undefined if the path is unset/empty/rejected.
*/
function validateMemoryPath(
raw: string | undefined,
expandTilde: boolean,
): string | undefined {
if (!raw) {
return undefined
}
let candidate = raw
// Settings.json paths support ~/ expansion (user-friendly). The env var
// override does not (it's set programmatically by Cowork/SDK, which should
// always pass absolute paths). Bare "~", "~/", "~/.", "~/..", etc. are NOT
// expanded — they would make isAutoMemPath() match all of $HOME or its
// parent (same class of danger as "/" or "C:\").
if (
expandTilde &&
(candidate.startsWith('~/') || candidate.startsWith('~\\'))
) {
const rest = candidate.slice(2)
// Reject trivial remainders that would expand to $HOME or an ancestor.
// normalize('') = '.', normalize('.') = '.', normalize('foo/..') = '.',
// normalize('..') = '..', normalize('foo/../..') = '..'
const restNorm = normalize(rest || '.')
if (restNorm === '.' || restNorm === '..') {
return undefined
}
candidate = join(homedir(), rest)
}
// normalize() may preserve a trailing separator; strip before adding
// exactly one to match the trailing-sep contract of getAutoMemPath()
const normalized = normalize(candidate).replace(/[/\\]+$/, '')
if (
!isAbsolute(normalized) ||
normalized.length < 3 ||
/^[A-Za-z]:$/.test(normalized) ||
normalized.startsWith('\\\\') ||
normalized.startsWith('//') ||
normalized.includes('\0')
) {
return undefined
}
return (normalized + sep).normalize('NFC')
}
/**
* Direct override for the full auto-memory directory path via env var.
* When set, getAutoMemPath()/getAutoMemEntrypoint() return this path directly
* instead of computing `{base}/projects/{sanitized-cwd}/memory/`.
*
* Used by Cowork to redirect memory to a space-scoped mount where the
* per-session cwd (which contains the VM process name) would otherwise
* produce a different project-key for every session.
*/
function getAutoMemPathOverride(): string | undefined {
return validateMemoryPath(
process.env.CLAUDE_COWORK_MEMORY_PATH_OVERRIDE,
false,
)
}
/**
* Settings.json override for the full auto-memory directory path.
* Supports ~/ expansion for user convenience.
*
* SECURITY: projectSettings (.claude/settings.json committed to the repo) is
* intentionally excluded — a malicious repo could otherwise set
* autoMemoryDirectory: "~/.ssh" and gain silent write access to sensitive
* directories via the filesystem.ts write carve-out (which fires when
* isAutoMemPath() matches and hasAutoMemPathOverride() is false). This follows
* the same pattern as hasSkipDangerousModePermissionPrompt() etc.
*/
function getAutoMemPathSetting(): string | undefined {
const dir =
getSettingsForSource('policySettings')?.autoMemoryDirectory ??
getSettingsForSource('flagSettings')?.autoMemoryDirectory ??
getSettingsForSource('localSettings')?.autoMemoryDirectory ??
getSettingsForSource('userSettings')?.autoMemoryDirectory
return validateMemoryPath(dir, true)
}
/**
* Check if CLAUDE_COWORK_MEMORY_PATH_OVERRIDE is set to a valid override.
* Use this as a signal that the SDK caller has explicitly opted into
* the auto-memory mechanics — e.g. to decide whether to inject the
* memory prompt when a custom system prompt replaces the default.
*/
export function hasAutoMemPathOverride(): boolean {
return getAutoMemPathOverride() !== undefined
}
/**
* Returns the canonical git repo root if available, otherwise falls back to
* the stable project root. Uses findCanonicalGitRoot so all worktrees of the
* same repo share one auto-memory directory (anthropics/claude-code#24382).
*/
function getAutoMemBase(): string {
return findCanonicalGitRoot(getProjectRoot()) ?? getProjectRoot()
}
/**
* Returns the auto-memory directory path.
*
* Resolution order:
* 1. CLAUDE_COWORK_MEMORY_PATH_OVERRIDE env var (full-path override, used by Cowork)
* 2. autoMemoryDirectory in settings.json (trusted sources only: policy/local/user)
* 3. <memoryBase>/projects/<sanitized-git-root>/memory/
* where memoryBase is resolved by getMemoryBaseDir()
*
* Memoized: render-path callers (collapseReadSearchGroups → isAutoManagedMemoryFile)
* fire per tool-use message per Messages re-render; each miss costs
* getSettingsForSource × 4 → parseSettingsFile (realpathSync + readFileSync).
* Keyed on projectRoot so tests that change its mock mid-block recompute;
* env vars / settings.json / CLAUDE_CONFIG_DIR are session-stable in
* production and covered by per-test cache.clear.
*/
export const getAutoMemPath = memoize(
(): string => {
const override = getAutoMemPathOverride() ?? getAutoMemPathSetting()
if (override) {
return override
}
const projectsDir = join(getMemoryBaseDir(), 'projects')
return (
join(projectsDir, sanitizePath(getAutoMemBase()), AUTO_MEM_DIRNAME) + sep
).normalize('NFC')
},
() => getProjectRoot(),
)
/**
* Returns the daily log file path for the given date (defaults to today).
* Shape: <autoMemPath>/logs/YYYY/MM/YYYY-MM-DD.md
*
* Used by assistant mode (feature('KAIROS')): rather than maintaining
* MEMORY.md as a live index, the agent appends to a date-named log file
* as it works. A separate nightly /dream skill distills these logs into
* topic files + MEMORY.md.
*/
export function getAutoMemDailyLogPath(date: Date = new Date()): string {
const yyyy = date.getFullYear().toString()
const mm = (date.getMonth() + 1).toString().padStart(2, '0')
const dd = date.getDate().toString().padStart(2, '0')
return join(getAutoMemPath(), 'logs', yyyy, mm, `${yyyy}-${mm}-${dd}.md`)
}
/**
* Returns the auto-memory entrypoint (MEMORY.md inside the auto-memory dir).
* Follows the same resolution order as getAutoMemPath().
*/
export function getAutoMemEntrypoint(): string {
return join(getAutoMemPath(), AUTO_MEM_ENTRYPOINT_NAME)
}
/**
* Check if an absolute path is within the auto-memory directory.
*
* When CLAUDE_COWORK_MEMORY_PATH_OVERRIDE is set, this matches against the
* env-var override directory. Note that a true return here does NOT imply
* write permission in that case — the filesystem.ts write carve-out is gated
* on !hasAutoMemPathOverride() (it exists to bypass DANGEROUS_DIRECTORIES).
*
* The settings.json autoMemoryDirectory DOES get the write carve-out: it's the
* user's explicit choice from a trusted settings source (projectSettings is
* excluded — see getAutoMemPathSetting), and hasAutoMemPathOverride() remains
* false for it.
*/
export function isAutoMemPath(absolutePath: string): boolean {
// SECURITY: Normalize to prevent path traversal bypasses via .. segments
const normalizedPath = normalize(absolutePath)
return normalizedPath.startsWith(getAutoMemPath())
}

292
src/memdir/teamMemPaths.ts Normal file
View File

@@ -0,0 +1,292 @@
import { lstat, realpath } from 'fs/promises'
import { dirname, join, resolve, sep } from 'path'
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js'
import { getErrnoCode } from '../utils/errors.js'
import { getAutoMemPath, isAutoMemoryEnabled } from './paths.js'
/**
* Error thrown when a path validation detects a traversal or injection attempt.
*/
export class PathTraversalError extends Error {
constructor(message: string) {
super(message)
this.name = 'PathTraversalError'
}
}
/**
* Sanitize a file path key by rejecting dangerous patterns.
* Checks for null bytes, URL-encoded traversals, and other injection vectors.
* Returns the sanitized string or throws PathTraversalError.
*/
function sanitizePathKey(key: string): string {
// Null bytes can truncate paths in C-based syscalls
if (key.includes('\0')) {
throw new PathTraversalError(`Null byte in path key: "${key}"`)
}
// URL-encoded traversals (e.g. %2e%2e%2f = ../)
let decoded: string
try {
decoded = decodeURIComponent(key)
} catch {
// Malformed percent-encoding (e.g. %ZZ, lone %) — not valid URL-encoding,
// so no URL-encoded traversal is possible
decoded = key
}
if (decoded !== key && (decoded.includes('..') || decoded.includes('/'))) {
throw new PathTraversalError(`URL-encoded traversal in path key: "${key}"`)
}
// Unicode normalization attacks: fullwidth (U+FF0E U+FF0F) normalize
// to ASCII ../ under NFKC. While path.resolve/fs.writeFile treat these as
// literal bytes (not separators), downstream layers or filesystems may
// normalize — reject for defense-in-depth (PSR M22187 vector 4).
const normalized = key.normalize('NFKC')
if (
normalized !== key &&
(normalized.includes('..') ||
normalized.includes('/') ||
normalized.includes('\\') ||
normalized.includes('\0'))
) {
throw new PathTraversalError(
`Unicode-normalized traversal in path key: "${key}"`,
)
}
// Reject backslashes (Windows path separator used as traversal vector)
if (key.includes('\\')) {
throw new PathTraversalError(`Backslash in path key: "${key}"`)
}
// Reject absolute paths
if (key.startsWith('/')) {
throw new PathTraversalError(`Absolute path key: "${key}"`)
}
return key
}
/**
* Whether team memory features are enabled.
* Team memory is a subdirectory of auto memory, so it requires auto memory
* to be enabled. This keeps all team-memory consumers (prompt, content
* injection, sync watcher, file detection) consistent when auto memory is
* disabled via env var or settings.
*/
export function isTeamMemoryEnabled(): boolean {
if (!isAutoMemoryEnabled()) {
return false
}
return getFeatureValue_CACHED_MAY_BE_STALE('tengu_herring_clock', false)
}
/**
* Returns the team memory path: <memoryBase>/projects/<sanitized-project-root>/memory/team/
* Lives as a subdirectory of the auto-memory directory, scoped per-project.
*/
export function getTeamMemPath(): string {
return (join(getAutoMemPath(), 'team') + sep).normalize('NFC')
}
/**
* Returns the team memory entrypoint: <memoryBase>/projects/<sanitized-project-root>/memory/team/MEMORY.md
* Lives as a subdirectory of the auto-memory directory, scoped per-project.
*/
export function getTeamMemEntrypoint(): string {
return join(getAutoMemPath(), 'team', 'MEMORY.md')
}
/**
* Resolve symlinks for the deepest existing ancestor of a path.
* The target file may not exist yet (we may be about to create it), so we
* walk up the directory tree until realpath() succeeds, then rejoin the
* non-existing tail onto the resolved ancestor.
*
* SECURITY (PSR M22186): path.resolve() does NOT resolve symlinks. An attacker
* who can place a symlink inside teamDir pointing outside (e.g. to
* ~/.ssh/authorized_keys) would pass a resolve()-based containment check.
* Using realpath() on the deepest existing ancestor ensures we compare the
* actual filesystem location, not the symbolic path.
*
*/
async function realpathDeepestExisting(absolutePath: string): Promise<string> {
const tail: string[] = []
let current = absolutePath
// Walk up until realpath succeeds. ENOENT means this segment doesn't exist
// yet; pop it onto the tail and try the parent. ENOTDIR means a non-directory
// component sits in the middle of the path; pop and retry so we can realpath
// the ancestor to detect symlink escapes.
// Loop terminates when we reach the filesystem root (dirname('/') === '/').
for (
let parent = dirname(current);
current !== parent;
parent = dirname(current)
) {
try {
const realCurrent = await realpath(current)
// Rejoin the non-existing tail in reverse order (deepest popped first)
return tail.length === 0
? realCurrent
: join(realCurrent, ...tail.reverse())
} catch (e: unknown) {
const code = getErrnoCode(e)
if (code === 'ENOENT') {
// Could be truly non-existent (safe to walk up) OR a dangling symlink
// whose target doesn't exist. Dangling symlinks are an attack vector:
// writeFile would follow the link and create the target outside teamDir.
// lstat distinguishes: it succeeds for dangling symlinks (the link entry
// itself exists), fails with ENOENT for truly non-existent paths.
try {
const st = await lstat(current)
if (st.isSymbolicLink()) {
throw new PathTraversalError(
`Dangling symlink detected (target does not exist): "${current}"`,
)
}
// lstat succeeded but isn't a symlink — ENOENT from realpath was
// caused by a dangling symlink in an ancestor. Walk up to find it.
} catch (lstatErr: unknown) {
if (lstatErr instanceof PathTraversalError) {
throw lstatErr
}
// lstat also failed (truly non-existent or inaccessible) — safe to walk up.
}
} else if (code === 'ELOOP') {
// Symlink loop — corrupted or malicious filesystem state.
throw new PathTraversalError(
`Symlink loop detected in path: "${current}"`,
)
} else if (code !== 'ENOTDIR' && code !== 'ENAMETOOLONG') {
// EACCES, EIO, etc. — cannot verify containment. Fail closed by wrapping
// as PathTraversalError so the caller can skip this entry gracefully
// instead of aborting the entire batch.
throw new PathTraversalError(
`Cannot verify path containment (${code}): "${current}"`,
)
}
tail.push(current.slice(parent.length + sep.length))
current = parent
}
}
// Reached filesystem root without finding an existing ancestor (rare —
// root normally exists). Fall back to the input; containment check will reject.
return absolutePath
}
/**
* Check whether a real (symlink-resolved) path is within the real team
* memory directory. Both sides are realpath'd so the comparison is between
* canonical filesystem locations.
*
* If teamDir does not exist, returns true (skips the check). This is safe:
* a symlink escape requires a pre-existing symlink inside teamDir, which
* requires teamDir to exist. If there's no directory, there's no symlink,
* and the first-pass string-level containment check is sufficient.
*/
async function isRealPathWithinTeamDir(
realCandidate: string,
): Promise<boolean> {
let realTeamDir: string
try {
// getTeamMemPath() includes a trailing separator; strip it because
// realpath() rejects trailing separators on some platforms.
realTeamDir = await realpath(getTeamMemPath().replace(/[/\\]+$/, ''))
} catch (e: unknown) {
const code = getErrnoCode(e)
if (code === 'ENOENT' || code === 'ENOTDIR') {
// Team dir doesn't exist — symlink escape impossible, skip check.
return true
}
// Unexpected error (EACCES, EIO) — fail closed.
return false
}
if (realCandidate === realTeamDir) {
return true
}
// Prefix-attack protection: require separator after the prefix so that
// "/foo/team-evil" doesn't match "/foo/team".
return realCandidate.startsWith(realTeamDir + sep)
}
/**
* Check if a resolved absolute path is within the team memory directory.
* Uses path.resolve() to convert relative paths and eliminate traversal segments.
* Does NOT resolve symlinks — for write validation use validateTeamMemWritePath()
* or validateTeamMemKey() which include symlink resolution.
*/
export function isTeamMemPath(filePath: string): boolean {
// SECURITY: resolve() converts to absolute and eliminates .. segments,
// preventing path traversal attacks (e.g. "team/../../etc/passwd")
const resolvedPath = resolve(filePath)
const teamDir = getTeamMemPath()
return resolvedPath.startsWith(teamDir)
}
/**
* Validate that an absolute file path is safe for writing to the team memory directory.
* Returns the resolved absolute path if valid.
* Throws PathTraversalError if the path contains injection vectors, escapes the
* directory via .. segments, or escapes via a symlink (PSR M22186).
*/
export async function validateTeamMemWritePath(
filePath: string,
): Promise<string> {
if (filePath.includes('\0')) {
throw new PathTraversalError(`Null byte in path: "${filePath}"`)
}
// First pass: normalize .. segments and check string-level containment.
// This is a fast rejection for obvious traversal attempts before we touch
// the filesystem.
const resolvedPath = resolve(filePath)
const teamDir = getTeamMemPath()
// Prefix attack protection: teamDir already ends with sep (from getTeamMemPath),
// so "team-evil/" won't match "team/"
if (!resolvedPath.startsWith(teamDir)) {
throw new PathTraversalError(
`Path escapes team memory directory: "${filePath}"`,
)
}
// Second pass: resolve symlinks on the deepest existing ancestor and verify
// the real path is still within the real team dir. This catches symlink-based
// escapes that path.resolve() alone cannot detect.
const realPath = await realpathDeepestExisting(resolvedPath)
if (!(await isRealPathWithinTeamDir(realPath))) {
throw new PathTraversalError(
`Path escapes team memory directory via symlink: "${filePath}"`,
)
}
return resolvedPath
}
/**
* Validate a relative path key from the server against the team memory directory.
* Sanitizes the key, joins with the team dir, resolves symlinks on the deepest
* existing ancestor, and verifies containment against the real team dir.
* Returns the resolved absolute path.
* Throws PathTraversalError if the key is malicious (PSR M22186).
*/
export async function validateTeamMemKey(relativeKey: string): Promise<string> {
sanitizePathKey(relativeKey)
const teamDir = getTeamMemPath()
const fullPath = join(teamDir, relativeKey)
// First pass: normalize .. segments and check string-level containment.
const resolvedPath = resolve(fullPath)
if (!resolvedPath.startsWith(teamDir)) {
throw new PathTraversalError(
`Key escapes team memory directory: "${relativeKey}"`,
)
}
// Second pass: resolve symlinks and verify real containment.
const realPath = await realpathDeepestExisting(resolvedPath)
if (!(await isRealPathWithinTeamDir(realPath))) {
throw new PathTraversalError(
`Key escapes team memory directory via symlink: "${relativeKey}"`,
)
}
return resolvedPath
}
/**
* Check if a file path is within the team memory directory
* and team memory is enabled.
*/
export function isTeamMemFile(filePath: string): boolean {
return isTeamMemoryEnabled() && isTeamMemPath(filePath)
}

View File

@@ -0,0 +1,100 @@
import {
buildSearchingPastContextSection,
DIRS_EXIST_GUIDANCE,
ENTRYPOINT_NAME,
MAX_ENTRYPOINT_LINES,
} from './memdir.js'
import {
MEMORY_DRIFT_CAVEAT,
MEMORY_FRONTMATTER_EXAMPLE,
TRUSTING_RECALL_SECTION,
TYPES_SECTION_COMBINED,
WHAT_NOT_TO_SAVE_SECTION,
} from './memoryTypes.js'
import { getAutoMemPath } from './paths.js'
import { getTeamMemPath } from './teamMemPaths.js'
/**
* Build the combined prompt when both auto memory and team memory are enabled.
* Closed four-type taxonomy (user / feedback / project / reference) with
* per-type <scope> guidance embedded in XML-style <type> blocks.
*/
export function buildCombinedMemoryPrompt(
extraGuidelines?: string[],
skipIndex = false,
): string {
const autoDir = getAutoMemPath()
const teamDir = getTeamMemPath()
const howToSave = skipIndex
? [
'## How to save memories',
'',
"Write each memory to its own file in the chosen directory (private or team, per the type's scope guidance) using this frontmatter format:",
'',
...MEMORY_FRONTMATTER_EXAMPLE,
'',
'- Keep the name, description, and type fields in memory files up-to-date with the content',
'- Organize memory semantically by topic, not chronologically',
'- Update or remove memories that turn out to be wrong or outdated',
'- Do not write duplicate memories. First check if there is an existing memory you can update before writing a new one.',
]
: [
'## How to save memories',
'',
'Saving a memory is a two-step process:',
'',
"**Step 1** — write the memory to its own file in the chosen directory (private or team, per the type's scope guidance) using this frontmatter format:",
'',
...MEMORY_FRONTMATTER_EXAMPLE,
'',
`**Step 2** — add a pointer to that file in the same directory's \`${ENTRYPOINT_NAME}\`. Each directory (private and team) has its own \`${ENTRYPOINT_NAME}\` index — each entry should be one line, under ~150 characters: \`- [Title](file.md) — one-line hook\`. They have no frontmatter. Never write memory content directly into a \`${ENTRYPOINT_NAME}\`.`,
'',
`- Both \`${ENTRYPOINT_NAME}\` indexes are loaded into your conversation context — lines after ${MAX_ENTRYPOINT_LINES} will be truncated, so keep them concise`,
'- Keep the name, description, and type fields in memory files up-to-date with the content',
'- Organize memory semantically by topic, not chronologically',
'- Update or remove memories that turn out to be wrong or outdated',
'- Do not write duplicate memories. First check if there is an existing memory you can update before writing a new one.',
]
const lines = [
'# Memory',
'',
`You have a persistent, file-based memory system with two directories: a private directory at \`${autoDir}\` and a shared team directory at \`${teamDir}\`. ${DIRS_EXIST_GUIDANCE}`,
'',
"You should build up this memory system over time so that future conversations can have a complete picture of who the user is, how they'd like to collaborate with you, what behaviors to avoid or repeat, and the context behind the work the user gives you.",
'',
'If the user explicitly asks you to remember something, save it immediately as whichever type fits best. If they ask you to forget something, find and remove the relevant entry.',
'',
'## Memory scope',
'',
'There are two scope levels:',
'',
`- private: memories that are private between you and the current user. They persist across conversations with only this specific user and are stored at the root \`${autoDir}\`.`,
`- team: memories that are shared with and contributed by all of the users who work within this project directory. Team memories are synced at the beginning of every session and they are stored at \`${teamDir}\`.`,
'',
...TYPES_SECTION_COMBINED,
...WHAT_NOT_TO_SAVE_SECTION,
'- You MUST avoid saving sensitive data within shared team memories. For example, never save API keys or user credentials.',
'',
...howToSave,
'',
'## When to access memories',
'- When memories (personal or team) seem relevant, or the user references prior work with them or others in their organization.',
'- You MUST access memory when the user explicitly asks you to check, recall, or remember.',
'- If the user says to *ignore* or *not use* memory: proceed as if MEMORY.md were empty. Do not apply remembered facts, cite, compare against, or mention memory content.',
MEMORY_DRIFT_CAVEAT,
'',
...TRUSTING_RECALL_SECTION,
'',
'## Memory and other forms of persistence',
'Memory is one of several persistence mechanisms available to you as you assist the user in a given conversation. The distinction is often that memory can be recalled in future conversations and should not be used for persisting information that is only useful within the scope of the current conversation.',
'- When to use or update a plan instead of memory: If you are about to start a non-trivial implementation task and would like to reach alignment with the user on your approach you should use a Plan rather than saving this information to memory. Similarly, if you already have a plan within the conversation and you have changed your approach persist that change by updating the plan rather than saving a memory.',
'- When to use or update tasks instead of memory: When you need to break your work in current conversation into discrete steps or keep track of your progress use tasks instead of saving to memory. Tasks are great for persisting information about the work that needs to be done in the current conversation, but memory should be reserved for information that will be useful in future conversations.',
...(extraGuidelines ?? []),
'',
...buildSearchingPastContextSection(autoDir),
]
return lines.join('\n')
}