Claude Code v2.1.88 源码分析 -- 记忆与持久化子系统
Claude Code 的记忆系统是一个多层次、基于文件的持久化架构,核心目标是让 AI 在跨会话间保持对用户、项目和团队的理解。系统由以下主要组件构成:
+-----------------------+
| System Prompt |
| (MEMORY.md 内容注入) |
+-----------+-----------+
|
+----------------------+----------------------+
| | |
+-------v--------+ +--------v--------+ +---------v--------+
| Auto Memory | | Team Memory | | Session Memory |
| (个人记忆) | | (团队共享记忆) | | (会话内记忆) |
+-------+--------+ +--------+--------+ +---------+--------+
| | |
v v v
~/.claude/projects/ team/ 子目录 .claude/session-memory/
<slug>/memory/ (服务器同步) session-<id>.md
| |
+-------v--------+ +--------v--------+
| Extract Agent | | File Watcher |
| (后台提取) | | + Sync Service |
+----------------+ +-----------------+
数据流方向:
- 写入路径: 用户对话 -> 主模型/提取子代理 -> 文件系统 -> (团队记忆) -> 服务器
- 读取路径: 文件系统 -> MEMORY.md (系统提示) + 相关性过滤 (sideQuery) -> 对话上下文
- 会话记忆: 对话消息 -> 后台子代理 -> session notes 文件 -> compact 时注入
源码位置: src/memdir/memoryTypes.ts
export const MEMORY_TYPES = ['user', 'feedback', 'project', 'reference'] as const
export type MemoryType = (typeof MEMORY_TYPES)[number]| 类型 | 描述 | 作用域倾向 | 典型场景 |
|---|---|---|---|
user |
用户角色、目标、知识背景 | 始终 private | "我是数据科学家" |
feedback |
用户对工作方式的纠正/确认 | 默认 private,项目约定可 team | "不要在测试中 mock 数据库" |
project |
项目进行中的工作、目标、截止日期 | 强烈倾向 team | "周四开始 merge freeze" |
reference |
外部系统的指针和位置 | 通常 team | "bug 在 Linear INGEST 项目中跟踪" |
设计约束: 以下内容不应保存为记忆:
- 代码模式、架构、文件路径 (可从项目当前状态推导)
- Git 历史 (git log/blame 是权威来源)
- 调试方案 (修复在代码中,上下文在 commit message 中)
- CLAUDE.md 已记录的内容
- 临时任务详情
export function parseMemoryType(raw: unknown): MemoryType | undefined {
if (typeof raw !== 'string') return undefined
return MEMORY_TYPES.find(t => t === raw)
}无效或缺失的 type: 字段返回 undefined,向后兼容老文件。
系统维护两套独立的类型描述文本:
TYPES_SECTION_COMBINED: 合并模式 (private + team),包含<scope>标签TYPES_SECTION_INDIVIDUAL: 单目录模式,无 scope 标签
这两套文本是刻意重复的,而非从共享模板生成 -- 保持扁平结构使得逐模式编辑更简单。
源码位置: src/memdir/memdir.ts
export const ENTRYPOINT_NAME = 'MEMORY.md'
export const MAX_ENTRYPOINT_LINES = 200
export const MAX_ENTRYPOINT_BYTES = 25_000 // ~125 chars/line at 200 linesexport type EntrypointTruncation = {
content: string
lineCount: number
byteCount: number
wasLineTruncated: boolean
wasByteTruncated: boolean
}
export function truncateEntrypointContent(raw: string): EntrypointTruncation截断策略:
- 先行截断 (自然边界): 超过 200 行时只取前 200 行
- 再字节截断: 在 25KB 限制前的最后一个换行符处截断,避免切断行中间
- 追加警告: 告知用户哪个上限被触发
> WARNING: MEMORY.md is 250 lines (limit: 200). Only part of it was loaded.
> Keep index entries to one line under ~200 chars; move detail into topic files.
警告信息会根据触发的上限组合动态生成:
- 仅字节超限:
"24.4KB (limit: 25.0KB) -- index entries are too long" - 仅行数超限:
"250 lines (limit: 200)" - 两者都超:
"250 lines and 26.5KB"
MEMORY.md 是索引,不是记忆本身:
- 每条记录一行,约 150 字符:
- [Title](file.md) -- one-line hook - 无 frontmatter
- 始终加载到对话上下文中
- 实际记忆内容存储在单独的 .md 文件中
---
name: {{memory name}}
description: {{one-line description -- 用于未来对话中判断相关性,需具体}}
type: {{user, feedback, project, reference}}
---
{{记忆内容 -- feedback/project 类型建议使用:
规则/事实,然后 **Why:** 和 **How to apply:** 行}}---
name: no-db-mocking
description: Integration tests must hit real database, not mocks
type: feedback
---
Integration tests must use a real database, not mocks.
**Why:** Prior incident where mock/prod divergence masked a broken migration.
**How to apply:** When writing or reviewing test code that touches database
operations, ensure tests connect to a real test database instance.- 按主题语义组织,非按时间
- 更新或删除过时的记忆
- 写入前检查是否已有可更新的现有记忆
- 相对日期必须转换为绝对日期 ("Thursday" -> "2026-03-05")
源码位置: src/memdir/paths.ts
// 解析优先级 (首个定义的生效):
// 1. CLAUDE_CODE_DISABLE_AUTO_MEMORY env var (1/true -> OFF)
// 2. CLAUDE_CODE_SIMPLE (--bare) -> OFF
// 3. CCR 无持久存储 -> OFF
// 4. settings.json 中 autoMemoryEnabled
// 5. 默认: enabled
export function isAutoMemoryEnabled(): boolean自动记忆路径解析 (memoized):
export const getAutoMemPath = memoize((): string => {
// 1. CLAUDE_COWORK_MEMORY_PATH_OVERRIDE (完整路径覆盖, Cowork 使用)
// 2. settings.json autoMemoryDirectory (受信来源, 支持 ~/ 展开)
// 3. <memoryBase>/projects/<sanitized-git-root>/memory/
const override = getAutoMemPathOverride() ?? getAutoMemPathSetting()
if (override) return override
const projectsDir = join(getMemoryBaseDir(), 'projects')
return join(projectsDir, sanitizePath(getAutoMemBase()), AUTO_MEM_DIRNAME) + sep
}, () => getProjectRoot())Git worktree 共享: 使用 findCanonicalGitRoot 使同一仓库的所有 worktree 共享一个记忆目录。
function validateMemoryPath(raw: string | undefined, expandTilde: boolean): string | undefined安全检查:
- 拒绝相对路径 (
!isAbsolute) - 拒绝根/近根路径 (length < 3)
- 拒绝 Windows 驱动器根 (
C:) - 拒绝 UNC 路径 (
\\server\share) - 拒绝 null 字节
~展开后验证不会指向$HOME或其父目录
安全: projectSettings (.claude/settings.json committed to repo) 被故意排除在 autoMemoryDirectory 读取范围之外 -- 恶意仓库可能设置 autoMemoryDirectory: "~/.ssh" 来获取写入权限。
export function getAutoMemDailyLogPath(date: Date = new Date()): string {
// <autoMemPath>/logs/YYYY/MM/YYYY-MM-DD.md
}长时间运行的 Assistant 会话使用追加式日志而非维护 MEMORY.md 索引:
- 每天一个日志文件
- 带时间戳的 bullet 条目
- 单独的 nightly
/dream技能将日志蒸馏为 MEMORY.md + 主题文件
源码位置: src/memdir/memoryScan.ts
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
export async function scanMemoryFiles(
memoryDir: string,
signal: AbortSignal,
): Promise<MemoryHeader[]>扫描策略: 单遍扫描 -- readFileInRange 内部做 stat 并返回 mtimeMs,读取后排序。对于常见情况 (N <= 200) 这比先 stat 排序再读取少一半系统调用。
结果格式:
export function formatMemoryManifest(memories: MemoryHeader[]): string
// 输出: - [type] filename (ISO timestamp): description源码位置: src/memdir/findRelevantMemories.ts
export type RelevantMemory = {
path: string
mtimeMs: number
}
export async function findRelevantMemories(
query: string,
memoryDir: string,
signal: AbortSignal,
recentTools: readonly string[] = [],
alreadySurfaced: ReadonlySet<string> = new Set(),
): Promise<RelevantMemory[]>过滤流程:
用户查询
|
v
scanMemoryFiles (扫描 frontmatter)
|
v
过滤已呈现的文件 (alreadySurfaced)
|
v
sideQuery (Sonnet 模型选择, JSON schema 输出)
|
v
最多返回 5 个最相关记忆
Sonnet 选择器 Prompt 要点:
- 仅根据文件名和描述判断相关性
- 如果不确定则不包含
- 如果提供了 recently-used tools 列表,不选择那些工具的 API 文档类记忆(但仍然选择包含 warnings/gotchas 的记忆)
源码位置: src/memdir/memoryAge.ts
export function memoryAgeDays(mtimeMs: number): number // 距今天数, >= 0
export function memoryAge(mtimeMs: number): string // "today" | "yesterday" | "47 days ago"
export function memoryFreshnessText(mtimeMs: number): string // 超过 1 天时返回过时警告
export function memoryFreshnessNote(mtimeMs: number): string // 包裹在 <system-reminder> 中超过 1 天的记忆会附加过时警告:
This memory is 47 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.
源码位置: src/memdir/teamMemPaths.ts
export function isTeamMemoryEnabled(): boolean // 需 auto memory 已启用 + feature flag
export function getTeamMemPath(): string // <autoMemPath>/team/
export function getTeamMemEntrypoint(): string // <autoMemPath>/team/MEMORY.md团队记忆路径验证极其严格,防止路径穿越和符号链接攻击:
export class PathTraversalError extends Error { name = 'PathTraversalError' }
// 写入验证: 两遍检查
export async function validateTeamMemWritePath(filePath: string): Promise<string>
// 第一遍: normalize + 字符串级包含检查 (快速拒绝)
// 第二遍: realpath 解析符号链接 + 真实路径包含验证
// 服务器 key 验证
export async function validateTeamMemKey(relativeKey: string): Promise<string>sanitizePathKey 检查:
- null 字节
- URL 编码穿越 (
%2e%2e%2f=../) - Unicode 规范化攻击 (NFKC 下全宽
.归一化为 ASCII) - 反斜杠 (Windows 路径分隔符)
- 绝对路径
realpathDeepestExisting: 沿目录树向上 walk 直到 realpath() 成功,处理:
- ENOENT: 可能是真正不存在或悬空符号链接 (攻击向量), 用
lstat区分 - ELOOP: 符号链接循环
- EACCES/EIO: 无法验证包含关系时 fail closed
源码位置: src/memdir/teamMemPrompts.ts
export function buildCombinedMemoryPrompt(
extraGuidelines?: string[],
skipIndex?: boolean,
): string合并模式增加:
## Memory scope部分: 说明 private / team 两个目录- 每个类型的
<scope>标签指导目录选择 - 团队记忆安全约束: "MUST avoid saving sensitive data within shared team memories"
源码位置: src/services/teamMemorySync/
API 协议:
GET /api/claude_code/team_memory?repo={owner/repo} -> TeamMemoryData
GET /api/claude_code/team_memory?repo={owner/repo}&view=hashes -> 仅元数据+校验和
PUT /api/claude_code/team_memory?repo={owner/repo} -> 上传条目 (upsert)
同步语义:
- Pull: 服务器覆盖本地 (server wins per-key)
- Push: 仅上传内容哈希与 serverChecksums 不同的 key (增量上传)
- 删除不传播: 删除本地文件不会从服务器移除,下次 pull 会恢复
数据结构:
// 服务器响应
const TeamMemoryDataSchema = z.object({
organizationId: z.string(),
repo: z.string(),
version: z.number(),
lastModified: z.string(), // ISO 8601
checksum: z.string(), // SHA256 带 'sha256:' 前缀
content: z.object({
entries: z.record(z.string(), z.string()),
entryChecksums: z.record(z.string(), z.string()).optional(),
}),
})
// Push 结果
type TeamMemorySyncPushResult = {
success: boolean
filesUploaded: number
checksum?: string
conflict?: boolean // 412 Precondition Failed
skippedSecrets?: SkippedSecretFile[]
errorType?: 'auth' | 'timeout' | 'network' | 'conflict' | 'no_oauth' | 'no_repo'
}限制常量:
const TEAM_MEMORY_SYNC_TIMEOUT_MS = 30_000
const MAX_FILE_SIZE_BYTES = 250_000 // 服务器默认单条上限
const MAX_PUT_BODY_BYTES = 200_000 // 网关 body 大小限制
const MAX_RETRIES = 3
const MAX_CONFLICT_RETRIES = 2源码位置: src/services/teamMemorySync/watcher.ts
const DEBOUNCE_MS = 2000 // 最后修改后等待 2s 再 push
export async function startTeamMemoryWatcher(): Promise<void>
export async function notifyTeamMemoryWrite(): Promise<void>
export async function stopTeamMemoryWatcher(): Promise<void>监视器实现选择: 使用 fs.watch({recursive: true}) 而非 chokidar 4+:
- chokidar 4+ 放弃了 fsevents,Bun 的 fallback 使用 kqueue (每个文件一个 fd)
- macOS: FSEvents, O(1) fd 不论树大小
- Linux: inotify, O(subdirs)
Push 抑制机制: 永久性失败 (no_oauth, 4xx 非 409/429) 后抑制重试,防止无限循环。仅在文件删除 (unlink 事件) 时清除抑制 -- 这是 too-many-entries 恢复操作。
启动流程:
- 检查 TEAMMEM build flag + isTeamMemoryEnabled + OAuth 可用 + github.com remote
- 初始 pull (在 watcher 启动前,避免磁盘写触发 schedulePush)
- 启动文件监视器 (即使服务器无内容也启动,避免 bootstrap dead zone)
源码位置: src/services/teamMemorySync/secretScanner.ts
上传前客户端扫描,secret 绝不离开用户机器。使用 gitleaks 精选的高置信度规则子集:
type SecretRule = {
id: string // gitleaks rule ID (kebab-case)
source: string // regex source
flags?: string
}
export function scanForSecrets(content: string): SecretMatch[]
export function redactSecrets(content: string): string覆盖范围: AWS/GCP/Azure、Anthropic/OpenAI/HuggingFace、GitHub/GitLab、Slack、Stripe/Shopify、Grafana/Sentry、npm/PyPI、private keys 等 30+ 规则。
源码位置: src/services/SessionMemory/
Session Memory 是会话内的持久笔记系统,通过后台子代理自动维护一个 markdown 文件,记录当前对话的关键信息。主要用于 auto-compact 时保持上下文连续性。
export type SessionMemoryConfig = {
minimumMessageTokensToInit: number // 初始化前最小 token 数 (默认 10000)
minimumTokensBetweenUpdate: number // 更新间最小 token 增长 (默认 5000)
toolCallsBetweenUpdates: number // 更新间最小 tool call 数 (默认 3)
}
const MAX_SECTION_LENGTH = 2000 // 每节最大 token
const MAX_TOTAL_SESSION_MEMORY_TOKENS = 12000 // 总文件最大 tokenexport function shouldExtractMemory(messages: Message[]): boolean触发条件 (必须满足 token 阈值):
- Token 阈值 AND tool call 阈值都满足, 或
- Token 阈值满足 AND 最后 assistant turn 无 tool call (自然对话断点)
关键: Token 阈值始终是必要条件,即使 tool call 阈值满足也不会在 token 不足时提取。
export const DEFAULT_SESSION_MEMORY_TEMPLATE = `
# Session Title
# Current State
# Task specification
# Files and Functions
# Workflow
# Errors & Corrections
# Codebase and System Documentation
# Learnings
# Key results
# Worklog
`每个 section 有 _italic description_ 作为模板指令,更新时严禁修改或删除。
自定义模板: ~/.claude/session-memory/config/template.md
自定义 prompt: ~/.claude/session-memory/config/prompt.md
registerPostSamplingHook(extractSessionMemory)
|
v
shouldExtractMemory() -- token + tool call 阈值检查
|
v
setupSessionMemoryFile() -- 创建/读取 session notes
|
v
buildSessionMemoryUpdatePrompt() -- 包含当前内容 + section 大小提醒
|
v
runForkedAgent() -- 分叉的子代理执行编辑
| (共享父级 prompt cache)
| (canUseTool 仅允许 Edit 目标文件)
v
recordExtractionTokenCount() -- 记录 token 水位
export function truncateSessionMemoryForCompact(content: string): {
truncatedContent: string
wasTruncated: boolean
}每个 section 不超过 MAX_SECTION_LENGTH * 4 字符 (粗略 token 估算使用 length/4),在行边界截断。
源码位置: src/services/extractMemories/
在每个完整查询循环结束时 (模型产生无 tool call 的最终响应),自动从对话转录中提取持久记忆并写入 auto-memory 目录。
handleStopHooks (stopHooks.ts)
|
v (fire-and-forget)
executeExtractMemories()
|
v
initExtractMemories() 创建的闭包
- lastMemoryMessageUuid (游标)
- inProgress (互斥锁)
- turnsSinceLastExtraction (节流计数器)
- pendingContext (合并等待)
|
v
runExtraction()
1. 检查主代理是否已写入记忆 (hasMemoryWritesSince)
-> 是: 跳过,推进游标
2. 节流检查 (tengu_bramble_lintel, 默认每 1 个 turn)
3. 扫描现有记忆 (scanMemoryFiles -> formatMemoryManifest)
4. 构建提取 prompt (auto-only 或 combined)
5. runForkedAgent (共享 prompt cache, maxTurns=5)
6. 提取写入路径,注入 memory_saved 系统消息
function hasMemoryWritesSince(messages, sinceUuid): boolean主代理的 prompt 始终有完整的记忆保存指令。当主代理自己写了记忆时,提取代理跳过该范围并推进游标。两者在每个 turn 上互斥。
export function createAutoMemCanUseTool(memoryDir: string): CanUseToolFn| 工具 | 权限 |
|---|---|
| Read/Grep/Glob | 无限制 (只读) |
| Bash | 仅只读命令 (ls, find, grep, cat, stat 等) |
| Edit/Write | 仅 auto-memory 目录内路径 |
| REPL | 允许 (ant 默认模式,内部 primitive 仍受上述检查) |
| 其他 | 全部拒绝 |
当提取进行中收到新请求时:
- 暂存最新 context (只保留最新的,因为它包含最多消息)
- 当前运行结束后,执行尾随提取 (isTrailingRun=true,跳过节流)
- 尾随运行的 newMessageCount 基于已推进的游标计算
export function buildExtractAutoOnlyPrompt(
newMessageCount: number,
existingMemories: string,
skipIndex?: boolean,
): string
export function buildExtractCombinedPrompt(
newMessageCount: number,
existingMemories: string,
skipIndex?: boolean,
): stringPrompt 关键指令:
- 分析最近 ~N 条消息
- 可用工具: Read/Grep/Glob + 只读 Bash + Edit/Write (仅记忆目录)
- 有限 turn 预算: turn 1 并行 Read,turn 2 并行 Write/Edit
- 不得调查/验证内容 (不 grep 源码,不读代码确认)
- 包含现有记忆清单,先检查再写入避免重复
源码位置: src/migrations/
共 11 个迁移文件,均为幂等函数,在启动时按序执行:
| 迁移 | 描述 | 源 -> 目标 |
|---|---|---|
migrateAutoUpdatesToSettings |
自动更新偏好 | globalConfig -> settings.json |
migrateBypassPermissionsAcceptedToSettings |
危险模式确认 | globalConfig -> settings.json (skipDangerousModePermissionPrompt) |
migrateEnableAllProjectMcpServersToSettings |
MCP 服务器批准 | projectConfig -> localSettings |
migrateReplBridgeEnabledToRemoteControlAtStartup |
REPL bridge 重命名 | globalConfig.replBridgeEnabled -> remoteControlAtStartup |
| 迁移 | 描述 |
|---|---|
migrateFennecToOpus |
fennec-latest -> opus, fennec-fast-latest -> opus[1m] + fast mode |
migrateLegacyOpusToCurrent |
Opus 4.0/4.1 显式字符串 -> 当前 Opus |
migrateOpusToOpus1m |
'opus' -> 'opus[1m]' (Max/Team Premium 用户) |
migrateSonnet1mToSonnet45 |
'sonnet[1m]' -> 'sonnet-4-5-20250929[1m]' (sonnet 别名现指 4.6) |
migrateSonnet45ToSonnet46 |
Sonnet 4.5 -> Sonnet 4.6 的过渡 |
| 迁移 | 描述 |
|---|---|
resetAutoModeOptInForDefaultOffer |
清除旧的 auto mode opt-in,重新展示新的 "make it my default" 选项 |
resetProToOpusDefault |
Pro 用户重置为 Opus 默认 |
所有迁移遵循统一模式:
- 检查幂等守卫 (已迁移标记或目标已存在)
- 读取旧值
- 写入新位置
- 清理旧值 (可选)
- 记录分析事件
// 典型模式
export function migrateFoo(): void {
const config = getGlobalConfig()
if (config.fooMigrationComplete) return // 幂等守卫
// ... 迁移逻辑
saveGlobalConfig(prev => ({ ...prev, fooMigrationComplete: true }))
logEvent('tengu_migration_foo', { ... })
}源码位置: src/utils/filePersistence/
面向 BYOC (Bring Your Own Cloud) 场景的文件持久化系统,在每个 turn 结束时将 outputs 目录中的修改文件上传到 Files API。
export function getEnvironmentKind(): EnvironmentKind | null
// 'byoc' | 'anthropic_cloud' | null
// 来自 CLAUDE_CODE_ENVIRONMENT_KIND env varexport async function runFilePersistence(
turnStartTime: TurnStartTime,
signal?: AbortSignal,
): Promise<FilesPersistedEventData | null>启用条件:
feature('FILE_PERSISTENCE')开启- environmentKind === 'byoc'
- 有 session access token
- 有 CLAUDE_CODE_REMOTE_SESSION_ID
BYOC 模式流程:
findModifiedFiles: 递归扫描{cwd}/{sessionId}/outputs/目录- 对比 mtime >= turnStartTime 找出修改文件
- 安全过滤: 跳过符号链接,跳过目录外的文件
- 文件数限制检查 (FILE_COUNT_LIMIT)
- 并行上传到 Files API (DEFAULT_UPLOAD_CONCURRENCY)
export async function findModifiedFiles(
turnStartTime: TurnStartTime,
outputsDir: string,
): Promise<string[]>使用 readdir({recursive: true, withFileTypes: true}) 一次获取所有条目,然后并行 stat 检查 mtime。跳过符号链接 (安全),处理 readdir 和 stat 之间的竞态条件。
源码位置: src/commands/memory/
const memory: Command = {
type: 'local-jsx',
name: 'memory',
description: 'Edit Claude memory files',
load: () => import('./memory.js'),
}- MemoryFileSelector (
src/components/memory/MemoryFileSelector.tsx, 47KB): 大型组件,提供记忆文件选择器 UI - MemoryUpdateNotification (
src/components/memory/MemoryUpdateNotification.tsx, 5KB): 记忆更新通知
/memory 命令
|
v
清除缓存 + 预加载 getMemoryFiles()
|
v
MemoryFileSelector (Ink 组件, Dialog 容器)
|
v
用户选择文件
|
v
mkdir (如需要) -> 创建文件 (如不存在) -> editFileInEditor()
|
v
显示结果 + 编辑器提示 ($VISUAL / $EDITOR)
export async function loadMemoryPrompt(): Promise<string | null>调度优先级:
- KAIROS + autoEnabled:
buildAssistantDailyLogPrompt(追加式日志模式) - TEAMMEM + teamEnabled:
buildCombinedMemoryPrompt(双目录模式) - autoEnabled only:
buildMemoryLines(单目录模式) - disabled: 返回 null,记录遥测
# auto memory / # Memory
|
+-- 系统介绍 (目录路径, 写入指导)
+-- ## Types of memory (四类型分类)
+-- ## What NOT to save in memory
+-- ## How to save memories (两步或单步)
+-- ## When to access memories
+-- ## Before recommending from memory (信任验证)
+-- ## Memory and other forms of persistence
+-- ## Searching past context (可选)
+-- ## MEMORY.md (截断后的索引内容)
## 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: verify first.
"The memory says X exists" is not the same as "X exists now."
// 记忆文件的分类类型 (frontmatter `type:` 字段)
type MemoryType = 'user' | 'feedback' | 'project' | 'reference'// UI 层的记忆文件来源分类
export const MEMORY_TYPE_VALUES = [
'User', // ~/.claude/CLAUDE.md
'Project', // .claude/CLAUDE.md (项目级)
'Local', // .claude/settings.local/CLAUDE.md
'Managed', // 系统管理的记忆
'AutoMem', // 自动记忆目录
'TeamMem', // 团队记忆目录 (feature('TEAMMEM'))
] as const注意: 这两个 MemoryType 是不同的概念 -- 前者是记忆内容的语义分类,后者是记忆文件的来源分类。
| 决策 | 原因 |
|---|---|
| MEMORY.md 是索引不是记忆 | 200 行限制下保持简洁,实际内容在主题文件中 |
| 四类型闭合分类 | 约束记忆范围,避免保存可推导信息 |
| 主代理与提取代理互斥 | 避免重复写入,通过 hasMemoryWritesSince 检测 |
| sideQuery 使用 Sonnet | 平衡相关性判断质量与成本 |
| Team memory server wins | 简化冲突解决,本地写入通过 watcher 异步推送 |
| Secret scanner 在客户端 | 敏感数据绝不离开用户机器 |
| Session memory 使用 Edit 而非 Write | 保持模板结构完整,只更新内容部分 |
| 迁移使用幂等守卫 | 安全的重复执行,无副作用 |
| paths.ts 排除 projectSettings | 防止恶意仓库通过 autoMemoryDirectory 获取写入权限 |
| fs.watch 而非 chokidar | 避免 kqueue 的 per-file fd 问题 |