Skip to content

Latest commit

 

History

History
908 lines (690 loc) · 27.6 KB

File metadata and controls

908 lines (690 loc) · 27.6 KB

12 - Memory & Persistence Systems

Claude Code v2.1.88 源码分析 -- 记忆与持久化子系统

Architecture Diagram

Memory and Persistence Architecture

1. 架构总览

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 |
  +----------------+   +-----------------+

数据流方向:

  1. 写入路径: 用户对话 -> 主模型/提取子代理 -> 文件系统 -> (团队记忆) -> 服务器
  2. 读取路径: 文件系统 -> MEMORY.md (系统提示) + 相关性过滤 (sideQuery) -> 对话上下文
  3. 会话记忆: 对话消息 -> 后台子代理 -> session notes 文件 -> compact 时注入

2. 记忆类型系统 (Memory Type Taxonomy)

2.1 四种核心类型

源码位置: 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 已记录的内容
  • 临时任务详情

2.2 类型解析

export function parseMemoryType(raw: unknown): MemoryType | undefined {
  if (typeof raw !== 'string') return undefined
  return MEMORY_TYPES.find(t => t === raw)
}

无效或缺失的 type: 字段返回 undefined,向后兼容老文件。

2.3 双模式 Prompt 文本

系统维护两套独立的类型描述文本:

  • TYPES_SECTION_COMBINED: 合并模式 (private + team),包含 <scope> 标签
  • TYPES_SECTION_INDIVIDUAL: 单目录模式,无 scope 标签

这两套文本是刻意重复的,而非从共享模板生成 -- 保持扁平结构使得逐模式编辑更简单。


3. MEMORY.md 入口文件

3.1 常量定义

源码位置: 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 lines

3.2 截断机制

export type EntrypointTruncation = {
  content: string
  lineCount: number
  byteCount: number
  wasLineTruncated: boolean
  wasByteTruncated: boolean
}

export function truncateEntrypointContent(raw: string): EntrypointTruncation

截断策略:

  1. 先行截断 (自然边界): 超过 200 行时只取前 200 行
  2. 再字节截断: 在 25KB 限制前的最后一个换行符处截断,避免切断行中间
  3. 追加警告: 告知用户哪个上限被触发
> 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"

3.3 MEMORY.md 的角色

MEMORY.md 是索引,不是记忆本身:

  • 每条记录一行,约 150 字符: - [Title](file.md) -- one-line hook
  • 无 frontmatter
  • 始终加载到对话上下文中
  • 实际记忆内容存储在单独的 .md 文件中

4. 记忆文件格式

4.1 Frontmatter 结构

---
name: {{memory name}}
description: {{one-line description -- 用于未来对话中判断相关性,需具体}}
type: {{user, feedback, project, reference}}
---

{{记忆内容 -- feedback/project 类型建议使用:
  规则/事实,然后 **Why:****How to apply:** 行}}

4.2 反馈/项目类记忆的 Body 结构

---
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.

4.3 记忆文件组织原则

  • 按主题语义组织,非按时间
  • 更新或删除过时的记忆
  • 写入前检查是否已有可更新的现有记忆
  • 相对日期必须转换为绝对日期 ("Thursday" -> "2026-03-05")

5. 记忆路径系统

5.1 路径解析

源码位置: 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 共享一个记忆目录。

5.2 路径安全验证

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" 来获取写入权限。

5.3 Assistant 模式 (KAIROS)

export function getAutoMemDailyLogPath(date: Date = new Date()): string {
  // <autoMemPath>/logs/YYYY/MM/YYYY-MM-DD.md
}

长时间运行的 Assistant 会话使用追加式日志而非维护 MEMORY.md 索引:

  • 每天一个日志文件
  • 带时间戳的 bullet 条目
  • 单独的 nightly /dream 技能将日志蒸馏为 MEMORY.md + 主题文件

6. 记忆扫描与相关性过滤

6.1 目录扫描

源码位置: 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

6.2 相关性过滤 (sideQuery)

源码位置: 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 的记忆)

6.3 记忆新鲜度

源码位置: 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.

7. 团队记忆系统

7.1 团队记忆路径

源码位置: 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

7.2 路径安全验证 (PSR M22186)

团队记忆路径验证极其严格,防止路径穿越和符号链接攻击:

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

7.3 合并 Prompt 构建

源码位置: 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"

7.4 团队记忆同步

源码位置: 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

7.5 文件监视器

源码位置: 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 恢复操作。

启动流程:

  1. 检查 TEAMMEM build flag + isTeamMemoryEnabled + OAuth 可用 + github.com remote
  2. 初始 pull (在 watcher 启动前,避免磁盘写触发 schedulePush)
  3. 启动文件监视器 (即使服务器无内容也启动,避免 bootstrap dead zone)

7.6 Secret 扫描器

源码位置: 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+ 规则。


8. 会话记忆 (Session Memory)

8.1 概述

源码位置: src/services/SessionMemory/

Session Memory 是会话内的持久笔记系统,通过后台子代理自动维护一个 markdown 文件,记录当前对话的关键信息。主要用于 auto-compact 时保持上下文连续性。

8.2 配置与阈值

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  // 总文件最大 token

8.3 提取触发逻辑

export function shouldExtractMemory(messages: Message[]): boolean

触发条件 (必须满足 token 阈值):

  1. Token 阈值 AND tool call 阈值都满足, 或
  2. Token 阈值满足 AND 最后 assistant turn 无 tool call (自然对话断点)

关键: Token 阈值始终是必要条件,即使 tool call 阈值满足也不会在 token 不足时提取。

8.4 模板结构

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

8.5 执行流程

registerPostSamplingHook(extractSessionMemory)
        |
        v
shouldExtractMemory() -- token + tool call 阈值检查
        |
        v
setupSessionMemoryFile() -- 创建/读取 session notes
        |
        v
buildSessionMemoryUpdatePrompt() -- 包含当前内容 + section 大小提醒
        |
        v
runForkedAgent() -- 分叉的子代理执行编辑
        |  (共享父级 prompt cache)
        |  (canUseTool 仅允许 Edit 目标文件)
        v
recordExtractionTokenCount() -- 记录 token 水位

8.6 Compact 时截断

export function truncateSessionMemoryForCompact(content: string): {
  truncatedContent: string
  wasTruncated: boolean
}

每个 section 不超过 MAX_SECTION_LENGTH * 4 字符 (粗略 token 估算使用 length/4),在行边界截断。


9. 记忆提取 (Auto-Extraction)

9.1 概述

源码位置: src/services/extractMemories/

在每个完整查询循环结束时 (模型产生无 tool call 的最终响应),自动从对话转录中提取持久记忆并写入 auto-memory 目录。

9.2 架构

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 系统消息

9.3 主代理与提取代理的互斥

function hasMemoryWritesSince(messages, sinceUuid): boolean

主代理的 prompt 始终有完整的记忆保存指令。当主代理自己写了记忆时,提取代理跳过该范围并推进游标。两者在每个 turn 上互斥。

9.4 工具权限

export function createAutoMemCanUseTool(memoryDir: string): CanUseToolFn
工具 权限
Read/Grep/Glob 无限制 (只读)
Bash 仅只读命令 (ls, find, grep, cat, stat 等)
Edit/Write 仅 auto-memory 目录内路径
REPL 允许 (ant 默认模式,内部 primitive 仍受上述检查)
其他 全部拒绝

9.5 合并与尾随执行

当提取进行中收到新请求时:

  1. 暂存最新 context (只保留最新的,因为它包含最多消息)
  2. 当前运行结束后,执行尾随提取 (isTrailingRun=true,跳过节流)
  3. 尾随运行的 newMessageCount 基于已推进的游标计算

9.6 提取 Prompt

export function buildExtractAutoOnlyPrompt(
  newMessageCount: number,
  existingMemories: string,
  skipIndex?: boolean,
): string

export function buildExtractCombinedPrompt(
  newMessageCount: number,
  existingMemories: string,
  skipIndex?: boolean,
): string

Prompt 关键指令:

  • 分析最近 ~N 条消息
  • 可用工具: Read/Grep/Glob + 只读 Bash + Edit/Write (仅记忆目录)
  • 有限 turn 预算: turn 1 并行 Read,turn 2 并行 Write/Edit
  • 不得调查/验证内容 (不 grep 源码,不读代码确认)
  • 包含现有记忆清单,先检查再写入避免重复

10. 设置迁移系统

源码位置: src/migrations/

共 11 个迁移文件,均为幂等函数,在启动时按序执行:

10.1 配置迁移

迁移 描述 源 -> 目标
migrateAutoUpdatesToSettings 自动更新偏好 globalConfig -> settings.json
migrateBypassPermissionsAcceptedToSettings 危险模式确认 globalConfig -> settings.json (skipDangerousModePermissionPrompt)
migrateEnableAllProjectMcpServersToSettings MCP 服务器批准 projectConfig -> localSettings
migrateReplBridgeEnabledToRemoteControlAtStartup REPL bridge 重命名 globalConfig.replBridgeEnabled -> remoteControlAtStartup

10.2 模型迁移

迁移 描述
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 的过渡

10.3 用户体验迁移

迁移 描述
resetAutoModeOptInForDefaultOffer 清除旧的 auto mode opt-in,重新展示新的 "make it my default" 选项
resetProToOpusDefault Pro 用户重置为 Opus 默认

10.4 迁移模式

所有迁移遵循统一模式:

  1. 检查幂等守卫 (已迁移标记或目标已存在)
  2. 读取旧值
  3. 写入新位置
  4. 清理旧值 (可选)
  5. 记录分析事件
// 典型模式
export function migrateFoo(): void {
  const config = getGlobalConfig()
  if (config.fooMigrationComplete) return  // 幂等守卫
  // ... 迁移逻辑
  saveGlobalConfig(prev => ({ ...prev, fooMigrationComplete: true }))
  logEvent('tengu_migration_foo', { ... })
}

11. 文件持久化 (File Persistence)

11.1 概述

源码位置: src/utils/filePersistence/

面向 BYOC (Bring Your Own Cloud) 场景的文件持久化系统,在每个 turn 结束时将 outputs 目录中的修改文件上传到 Files API。

11.2 环境检测

export function getEnvironmentKind(): EnvironmentKind | null
// 'byoc' | 'anthropic_cloud' | null
// 来自 CLAUDE_CODE_ENVIRONMENT_KIND env var

11.3 执行流程

export async function runFilePersistence(
  turnStartTime: TurnStartTime,
  signal?: AbortSignal,
): Promise<FilesPersistedEventData | null>

启用条件:

  • feature('FILE_PERSISTENCE') 开启
  • environmentKind === 'byoc'
  • 有 session access token
  • 有 CLAUDE_CODE_REMOTE_SESSION_ID

BYOC 模式流程:

  1. findModifiedFiles: 递归扫描 {cwd}/{sessionId}/outputs/ 目录
  2. 对比 mtime >= turnStartTime 找出修改文件
  3. 安全过滤: 跳过符号链接,跳过目录外的文件
  4. 文件数限制检查 (FILE_COUNT_LIMIT)
  5. 并行上传到 Files API (DEFAULT_UPLOAD_CONCURRENCY)

11.4 输出扫描器

export async function findModifiedFiles(
  turnStartTime: TurnStartTime,
  outputsDir: string,
): Promise<string[]>

使用 readdir({recursive: true, withFileTypes: true}) 一次获取所有条目,然后并行 stat 检查 mtime。跳过符号链接 (安全),处理 readdir 和 stat 之间的竞态条件。


12. /memory 命令与 UI

12.1 命令入口

源码位置: src/commands/memory/

const memory: Command = {
  type: 'local-jsx',
  name: 'memory',
  description: 'Edit Claude memory files',
  load: () => import('./memory.js'),
}

12.2 UI 组件

  • MemoryFileSelector (src/components/memory/MemoryFileSelector.tsx, 47KB): 大型组件,提供记忆文件选择器 UI
  • MemoryUpdateNotification (src/components/memory/MemoryUpdateNotification.tsx, 5KB): 记忆更新通知

12.3 交互流程

/memory 命令
    |
    v
清除缓存 + 预加载 getMemoryFiles()
    |
    v
MemoryFileSelector (Ink 组件, Dialog 容器)
    |
    v
用户选择文件
    |
    v
mkdir (如需要) -> 创建文件 (如不存在) -> editFileInEditor()
    |
    v
显示结果 + 编辑器提示 ($VISUAL / $EDITOR)

13. 记忆系统内的 Prompt 层级

13.1 loadMemoryPrompt 调度逻辑

export async function loadMemoryPrompt(): Promise<string | null>

调度优先级:

  1. KAIROS + autoEnabled: buildAssistantDailyLogPrompt (追加式日志模式)
  2. TEAMMEM + teamEnabled: buildCombinedMemoryPrompt (双目录模式)
  3. autoEnabled only: buildMemoryLines (单目录模式)
  4. disabled: 返回 null,记录遥测

13.2 Prompt 组成部分

# 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 (截断后的索引内容)

13.3 过时记忆的信任验证

## 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."

14. 内部类型对照

14.1 memdir/memoryTypes.ts 中的 MemoryType

// 记忆文件的分类类型 (frontmatter `type:` 字段)
type MemoryType = 'user' | 'feedback' | 'project' | 'reference'

14.2 utils/memory/types.ts 中的 MemoryType

// 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不同的概念 -- 前者是记忆内容的语义分类,后者是记忆文件的来源分类。


15. 关键设计决策总结

决策 原因
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 问题