- 人工智能
- AI 应用
- 桌面应用
- 代码智能体
- MCP Clients
【免费下载链接】cc-haha
Local-first cross-platform desktop workspace for Claude Code / agents: multi-agent, Git worktrees, code diffs, skill marketplace, multi-model, Computer Use, task-aware desktop pets, with WeChat, Feishu, DingTalk, Telegram, WhatsApp and H5 access.
本指南以 cc-haha 仓库(Local-first 桌面工作区,面向 Claude Code / 多 Agent 场景)中的记忆(Memory)子系统为对象,逐层拆解其五大核心模块与若干辅助模块的实现细节:路径解析、系统提示注入、自动记忆提取、智能检索与团队记忆同步。读者读完本文后,将掌握内存目录的优先级解析与安全校验规则、MEMORY.md 索引的双重截断策略、基于 forked agent 的后台提取机制、Sonnet 相关性选择器的提示词设计,以及 TEAMMEM 团队同步的增量上传语义,可直接据此调试、扩展或在自己的 Agent 产品中复刻同类设计。
总体架构
内存系统由 5 个核心模块协同工作,源码集中在src/memdir/与src/services/extractMemories/两个目录:
| 模块 | 源码位置 | 职责 |
|---|---|---|
| 路径解析(Path Resolution) | src/memdir/paths.ts | 计算记忆存储目录,处理覆盖(override)与安全校验 |
| 提示构建(Prompt Construction) | src/memdir/memdir.ts | 将记忆指令注入系统提示 |
| 记忆扫描(Memory Scanning) | src/memdir/memoryScan.ts | 扫描目录、解析 frontmatter、对条目排序 |
| 智能检索(Intelligent Retrieval) | src/memdir/findRelevantMemories.ts | 使用 Sonnet 挑选与当前查询相关的记忆 |
| 自动提取(Auto-Extraction) | src/services/extractMemories/ | 后台 forked agent 从对话中提取记忆 |
辅助模块:
| 模块 | 源码位置 | 职责 |
|---|---|---|
| AutoDream | src/services/autoDream/ | 后台记忆整合("梦境"),详见 AutoDream 文档 |
| 类型定义 | src/memdir/memoryTypes.ts | 四种记忆类型的分类学与提示模板 |
| 新鲜度(Freshness) | src/memdir/memoryAge.ts | 计算记忆年龄,生成过期警告 |
| 文件检测 | src/utils/memoryFileDetection.ts | 判断某路径是否属于记忆系统 |
| Agent 记忆 | src/tools/AgentTool/agentMemory.ts | 子代理专属的三级记忆目录 |
| 团队同步 | src/services/teamMemorySync/ | 记忆的远端上传/下载 |
路径解析系统
核心函数:getAutoMemPath()
记忆目录的解析优先级(从高到低):
1. CLAUDE_COWORK_MEMORY_PATH_OVERRIDE <- Cowork 环境变量(完整路径) 2. settings.json -> autoMemoryDirectory <- 用户设置(支持 ~/ 展开) 3. {memoryBase}/projects/{sanitized-git-root}/memory/ <- 默认计算路径关键源码 src/memdir/paths.ts#L223-L235:
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(), // 缓存键 = 项目根目录 )实现细节值得注意:
- memoize 以项目根目录为缓存键:
getAutoMemPath被渲染路径的调用方(如collapseReadSearchGroups → isAutoManagedMemoryFile)在每个 tool-use 消息的每次 Messages 重渲染时触发;每次 miss 都会引发getSettingsForSource × 4 → parseSettingsFile(realpathSync + readFileSync),因此必须缓存。以项目根为键既保证了测试中 mock 变更能重新计算,又利用了生产环境下 env / settings / config 目录的会话稳定性(src/memdir/paths.ts#L216-L222)。 - 默认路径基于"规范化 Git 根"而非当前目录:
getAutoMemBase()使用findCanonicalGitRoot(getProjectRoot()),同一仓库的所有 worktree 共享同一个自动记忆目录,避免一个仓库多个 worktree 各建一份记忆(src/memdir/paths.ts#L203-L205)。 ~展开仅对 settings.json 生效:环境变量覆盖路径由 Cowork/SDK 程序化设置,恒为绝对路径,不做展开;同时~、~/、~/.、~/..这类"平凡剩余"会被显式拒绝——它们会展开到$HOME或其父目录,与/、C:\属于同一类危险(src/memdir/paths.ts#L116-L135)。
路径安全校验
validateMemoryPath()会拒绝以下危险路径(src/memdir/paths.ts#L109-L150):
| 被拒绝的路径 | 原因 |
|---|---|
../foo | 相对路径,依赖 CWD |
/或/a | 根路径或过短路径 |
C:\ | Windows 盘符根 |
\\server\share | UNC 网络路径 |
含\0 | 空字节,可在系统调用中被截断 |
返回的路径保证恰好一个尾随分隔符(normalized + sep),并做NFC规范化。
安全限制:项目级.claude/settings.json不允许设置autoMemoryDirectory。原因在源码注释中讲得很清楚:恶意仓库可以设置autoMemoryDirectory: "~/.ssh",从而借助 filesystem 写入 carve-out(当isAutoMemPath()命中且hasAutoMemPathOverride()为 false 时触发)静默获得对敏感目录的写权限(src/memdir/paths.ts#L168-L178)。这与hasSkipDangerousModePermissionPrompt()等采用同一防护模式。settings.json 覆盖路径只从policySettings、flagSettings、localSettings、userSettings四个受信来源读取。
启用条件
isAutoMemoryEnabled()的检查链(src/memdir/paths.ts#L30-L55):
CLAUDE_CODE_DISABLE_AUTO_MEMORY=1 -> 禁用(1/true 关闭,0/false 显式开启) CLAUDE_CODE_SIMPLE (--bare) -> 禁用 远程模式且未设 REMOTE_MEMORY_DIR -> 禁用 settings.json autoMemoryEnabled -> 遵循设置(支持项目级 opt-out) 默认 -> 启用其中--bare分支值得注意:注释指出 prompts.ts 已通过 SIMPLE 早退在系统提示中删除了记忆段,这里的开关是为了同时关停另一半功能——turn-end 提取 fork、autoDream、/remember、/dream、团队同步(src/memdir/paths.ts#L38-L42)。
系统提示注入
入口:loadMemoryPrompt()
这是内存系统与系统提示之间的接口,启动时调用一次(通过systemPromptSection缓存)。分发逻辑(src/memdir/memdir.ts#L419-L506):
loadMemoryPrompt() |-- KAIROS 模式? -> buildAssistantDailyLogPrompt() [日志追加模式] |-- TEAMMEM 开启? -> buildCombinedMemoryPrompt() [个人 + 团队双目录] |-- 正常模式 -> buildMemoryLines() [单一目录] |-- 禁用 -> 返回 null(并上报 tengu_memdir_disabled 遥测)两个模式的细节:
- KAIROS(助手日常日志)模式:助手会话近似"永久",模型不再维护 MEMORY.md 实时索引,而是以追加方式写入按日期命名的日志文件
<autoMemPath>/logs/YYYY/MM/YYYY-MM-DD.md;提示词用YYYY/MM模式串描述路径而非内联当天字面路径——因为该提示被systemPromptSection('memory', ...)缓存且不会因日期变更而失效,模型靠尾部 date_change 附件推导当前日期,以在跨午夜时保留提示缓存前缀(src/memdir/memdir.ts#L327-L370、src/memdir/paths.ts#L246-L251)。 - TEAMMEM 模式:团队记忆必须是自动记忆的子目录(
getTeamMemPath()定义为join(getAutoMemPath(), 'team')),因此不存在"仅团队"分支;递归 mkdir 团队目录时会把自动目录一并创建出来(src/memdir/memdir.ts#L448-L473)。
buildMemoryLines()构建的提示结构
# auto memory You have a persistent file-based memory system located at `{memoryDir}`... ## Types of memory <- 四种类型的定义与示例 ## What NOT to save <- 排除规则 ## How to save memories <- 两步保存流程 ## When to access memories <- 何时查阅 ## Before recommending <- 引用前先验证 ## Memory and other forms <- 与 Plan/Task 的区分 ## MEMORY.md <- 索引内容(或 "currently empty")提示中所有引导文本都来自 src/memdir/memoryTypes.ts 的具名导出。有几处设计直接反映了评测(eval)驱动的迭代结果:
- "Before recommending from memory" 标题优于 "Trusting what you recall":源码注释记录了 H1 实验——在决策点给出动作提示的标题(3/3 通过)胜过抽象标题(0/3),正文完全相同,只有标题不同(src/memdir/memoryTypes.ts#L240-L256)。
- 记忆漂移警告:
MEMORY_DRIFT_CAVEAT要求模型把记忆当作"某个时间点的观察",回答前先读当前文件/资源验证,记忆与现状冲突时信任现状并更新或删除过期记忆(src/memdir/memoryTypes.ts#L201-L202)。 - "忽略记忆"指令的显式处理:当用户说ignore或not use记忆时,模型应视 MEMORY.md 为空,不得引用、对比或提及记忆内容——这是针对"把 ignore 理解为 acknowledge-then-override"这一失败模式的显式反模式说明(src/memdir/memoryTypes.ts#L216-L222)。
- "What NOT to save" 的显式保存门:即使用户明确要求保存,排除规则依然生效;若用户要求保存 PR 列表/活动摘要,应追问其中"令人惊讶或非常规"的部分(src/memdir/memoryTypes.ts#L183-L195)。
四种记忆类型的 frontmatter 模板(MEMORY_FRONTMATTER_EXAMPLE,src/memdir/memoryTypes.ts#L261-L271):
--- name: {{memory name}} description: {{one-line description — 用于判断未来对话中的相关性,请具体}} type: {{user, feedback, project, reference}} --- {{memory content — feedback/project 类型建议按:规则/事实,然后 **Why:** 和 **How to apply:** 行组织}}MEMORY.md 截断策略
truncateEntrypointContent()施加双重限制(src/memdir/memdir.ts#L57-L103):
// 先按行数截断 if (lineCount > 200) -> 截断到 200 行 // 再按字节数截断(兜住超长行) if (bytes > 25,000) -> 在最后一个换行符处截断 // 追加警告 "WARNING: MEMORY.md is {reason}. Only part of it was loaded."MAX_ENTRYPOINT_LINES = 200,MAX_ENTRYPOINT_BYTES = 25_000(约 200 行 × 125 字符/行的 p97 分位;p100 实测出现过 200 行以内达到 197KB 的极端情况,因此字节上限针对的是"行数上限漏网的长行索引")。- 字节截断发生在最后一个换行符处,避免从行中间切断。
- 警告文本区分触发原因:仅超字节时给出格式化大小对比("index entries are too long"),并建议索引条目控制在 ~200 字符以内的一行、把细节挪进主题文件。
目录自动创建
ensureMemoryDirExists()在加载提示时保证目录存在(src/memdir/memdir.ts#L129-L147):
- 使用
FsOperations.mkdir递归创建,一条调用搞定整条父链(~/.claude/projects/<slug>/memory/); - 内部吞掉
EEXIST,天然幂等; - 真正的权限错误(EACCES/EPERM/EROFS)只记录到 debug 日志、不抛出——提示构建照常继续,真实错误由后续 Write 工具浮现(FileWriteTool 会自行 mkdir 父目录)。
配合DIR_EXISTS_GUIDANCE常量("This directory already exists — write to it directly with the Write tool (do not run mkdir or check for its existence)"),提示明确告诉模型目录已存在,避免模型浪费回合执行ls或mkdir(src/memdir/memdir.ts#L116-L119)。
自动记忆提取
触发时机
当模型产出最终响应(无工具调用)时,在handleStopHooks中触发。关键源码:src/services/extractMemories/extractMemories.ts。
触发入口executeExtractMemories()通过extractor(由initExtractMemories()创建的闭包)执行,fire-and-forget 调用,与 prompt suggestion/coaching 并列于 stop hooks(src/services/extractMemories/extractMemories.ts#L598-L603)。状态全部闭包作用域化(lastMemoryMessageUuid游标、inProgress、turnsSinceLastExtraction、pendingContext),测试在beforeEach中调用initExtractMemories()获取全新闭包(src/services/extractMemories/extractMemories.ts#L296-L326)。
完整提取流程
1. 模型完成响应(无 tool_use) | 2. 调用 executeExtractMemories() | 3. 守卫检查: - 是主代理吗?(子代理不提取;context.toolUseContext.agentId 存在则直接返回) - 特性门控开启?(tengu_passport_quail) - 自动记忆开启?(isAutoMemoryEnabled) - 非远程模式?(getIsRemoteMode) - 无并行提取进行中? | 4. 频率控制: turnsSinceLastExtraction++ if < tengu_bramble_lintel -> 跳过(默认 1,即每轮都允许) | 5. 互斥检查: 主代理自己写入了记忆?-> 跳过,推进游标 | 6. 扫描现有记忆目录(scanMemoryFiles) 生成清单(formatMemoryManifest) | 7. 构建提取提示(buildExtractAutoOnlyPrompt / buildExtractCombinedPrompt) | 8. 运行 forked agent(runForkedAgent) - 共享父会话的提示缓存 - 最多 5 轮 - 受限工具权限 | 9. 提取已写入的文件路径 推进游标到最后一条消息 | 10. 通知用户:"Memory updated in ..."几个实现要点:
- 扫描放在频率闸门之后:被跳过的轮次不必支付
scanMemoryFiles的开销(src/services/extractMemories/extractMemories.ts#L395-L400)。 - 游标仅在成功运行后推进:如果 forked agent 出错,游标原地不动,这些消息会在下次提取中被重新考虑(src/services/extractMemories/extractMemories.ts#L428-L435)。
- 提取完成度可预期:注释指出行为良好的提取通常 2–4 轮完成(读→写),5 轮硬上限防止"验证兔子洞"烧掉 token(src/services/extractMemories/extractMemories.ts#L424-L426)。
drainPendingExtraction():print.ts 在响应刷出后、gracefulShutdownSync之前调用,带 60 秒软超时等待在途提取(含 trailing 合并运行)完成,避免 5 秒关闭保险丝把 forked agent 杀掉(src/services/extractMemories/extractMemories.ts#L579-L614)。
Forked Agent
自动提取使用runForkedAgent——主会话的完美分叉:
- 共享提示缓存:
createCacheSafeParams(context)复用父会话的缓存参数,避免重复的 API 缓存创建成本;fork 的工具列表必须与主会话一致,否则破坏提示缓存共享(工具是缓存键的一部分),REPL 工具因此被显式放行(src/services/extractMemories/extractMemories.ts#L171-L182)。 - 隔离执行:不影响主会话的消息历史。
- 受限工具:仅允许 Read、Grep、Glob、只读 Bash 以及记忆目录内的 Edit/Write。
- 不记录转录:
skipTranscript: true——避免与主线程产生竞态(src/services/extractMemories/extractMemories.ts#L420-L423)。
工具权限(createAutoMemCanUseTool)
允许:Read、Grep、Glob(无限制) 允许:Bash(仅只读命令:ls、find、grep、cat、stat、wc、head、tail 等) 允许:Edit/Write(仅限自动记忆目录内路径) 拒绝:MCP、Agent、非只读 Bash、其他写操作实现上是返回一个CanUseToolFn:Bash 必须通过BashTool.isReadOnly对解析后的输入做校验,否则给出明确拒绝理由;Edit/Write 必须带file_path且通过isAutoMemPath()检查(路径经normalize防..穿越)。该函数同时被 extractMemories 与 autoDream 共享(src/services/extractMemories/extractMemories.ts#L171-L222)。
互斥机制
主代理与提取代理互斥(src/services/extractMemories/extractMemories.ts#L121-L148):
function hasMemoryWritesSince(messages, sinceUuid): boolean { // 扫描 sinceUuid 之后的所有 assistant 消息 // 若任何 Edit/Write tool_use 指向自动记忆目录 // -> 返回 true(跳过提取,推进游标) }这防止重复保存:主代理已写入记忆时,后台提取被跳过,并把游标推进到最后一条消息,使下次提取只考虑主代理写入之后的新增内容。主代理提示中携带完整保存指令,因此这种冗余提取本就不必要。
合并机制
若上一次提取仍在运行:
- 新上下文被暂存(
pendingContext,后到者覆盖先到者——只有最新消息最多的一次有意义); - 旧提取完成后立即启动一次尾随提取(tail extraction);
- 尾随提取只处理两次调用之间新增的消息——它以刚推进过的游标为基准计算
newMessageCount(src/services/extractMemories/extractMemories.ts#L506-L522)。
智能记忆检索
工作方式
每次用户发送查询时触发findRelevantMemories()(src/memdir/findRelevantMemories.ts):
1. scanMemoryFiles(memoryDir) - 递归读取所有 .md 文件(排除 MEMORY.md) - 解析 frontmatter(前 30 行) - 按修改时间降序排序 - 最多 200 个文件 | 2. 过滤先前已展示过的记忆(alreadySurfaced) | 3. 格式化清单(formatMemoryManifest) - [type] filename (ISO timestamp): description | 4. Sonnet 模型选择(sideQuery) - 系统提示:You are a memory selector... - 用户消息:Query + Available memories + Recently used tools - 输出:JSON { selected_memories: string[] } - 最多 5 个选择 | 5. 返回选中的记忆作为 { path, mtimeMs }设计细节:
alreadySurfaced过滤在 Sonnet 调用之前:让选择器把 5 个名额花在新鲜候选项上,而不是反复挑选调用方即将丢弃的旧文件(src/memdir/findRelevantMemories.ts#L46-L48)。sideQuery使用getDefaultSonnetModel(),max_tokens: 256,输出约束为 JSON Schema(selected_memories: string[]);返回的文件名会与合法文件名集合比对,防止模型幻觉出不存在的文件(src/memdir/findRelevantMemories.ts#L97-L140)。recentTools上下文抑制噪音:当 Claude Code 正在使用某工具(如mcp__X__spawn)时,再浮现该工具的使用参考文档是噪音——对话中已含可用用法;但工具的warnings/gotchas/known issues记忆仍应选中,因为"正在使用"恰恰是它们最该出现的时候(src/memdir/findRelevantMemories.ts#L87-L95)。- MEMORY_SHAPE_TELEMETRY特性门开启时记录召回形状(
logMemoryRecallShape),即使空选择也会上报——选择率需要分母,"跑过但没选"与"从没跑过"要能区分(src/memdir/findRelevantMemories.ts#L64-L72)。
Sonnet 选择器提示
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, do not include it. Be selective and discerning. - If there are no memories 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.新鲜度警告
选中的记忆带着新鲜度信息注入上下文。memoryFreshnessText()(src/memdir/memoryAge.ts#L33-L42):
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.` }memoryAgeDays按天向下取整(今天 0、昨天 1、更早 2+),未来时间戳(时钟偏移)钳制到 0(src/memdir/memoryAge.ts#L6-L8)。- 动机来自用户报告:过期的"代码状态记忆"(引用已变更代码的 file:line)被当作事实断言——引用让过期声明听起来更有权威,而非更弱。
- 另有两个变体:
memoryAge()输出人类可读的 "47 days ago"(模型不擅长日期算术);memoryFreshnessNote()把警告包进<system-reminder>标签,供不加自有包装的调用方(如 FileReadTool 输出)使用(src/memdir/memoryAge.ts#L15-L20、src/memdir/memoryAge.ts#L49-L53)。
记忆扫描详解
scanMemoryFiles()
关键设计:单遍扫描(先读后排序),避免双倍 stat 系统调用。readFileInRange内部自行 stat 并返回mtimeMs,因此是"读-然后-排序"而非"stat-排序-读"。常见场景(N ≤ 200)下相比单独 stat 一轮减少一半系统调用;大 N 时多读几个小文件,但仍避免了存活 200 个文件的二次 stat(src/memdir/memoryScan.ts#L24-L33)。
async function scanMemoryFiles(memoryDir, signal): Promise<MemoryHeader[]> { const entries = await readdir(memoryDir, { recursive: true }) const mdFiles = entries.filter(f => f.endsWith('.md') && basename(f) !== 'MEMORY.md') // 并行读取所有文件的 frontmatter(前 30 行) const headerResults = await Promise.allSettled( mdFiles.map(async (relativePath) => { const { content, mtimeMs } = await readFileInRange(filePath, 0, 30) const { frontmatter } = parseFrontmatter(content) return { filename, filePath, mtimeMs, description, type } }) ) // 过滤成功结果,按时间降序排序,取前 200 return fulfilled.sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, 200) }- 该模块从
findRelevantMemories.ts中拆出,专门供 extractMemories 复用扫描而不引入 sideQuery 与 API client 链(曾通过 memdir.ts 形成循环依赖,#25372)(src/memdir/memoryScan.ts#L1-L5)。 - 类型解析
parseMemoryType()对无效或缺失的type:字段返回undefined——无 type 字段的旧文件继续可用,未知类型优雅降级(src/memdir/memoryTypes.ts#L28-L31)。
formatMemoryManifest()
生成供 Sonnet 或提取代理消费的清单:
- [feedback] testing_policy.md (2026-03-15T10:30:00.000Z): Integration tests use real DB - [user] role.md (2026-03-14T08:00:00.000Z): Data scientist, focused on logging - [project] freeze.md (2026-03-10T15:00:00.000Z): Merge freeze starting 3/5实现上逐行[type] filename (ISO 时间戳): description,无 description 时省略冒号后的部分(src/memdir/memoryScan.ts#L84-L94)。
Agent 记忆
通过 Agent 工具启动的子代理拥有独立的三级记忆系统(src/tools/AgentTool/agentMemory.ts):
| 作用域 | 路径 | 描述 |
|---|---|---|
| user | ~/.claude/agent-memory/{agentType}/ | 全局用户级 |
| project | .claude/agent-memory/{agentType}/ | 项目级(提交进版本库) |
| local | .claude/agent-memory-local/{agentType}/ | 本地级(不提交) |
与主记忆的差异:
- 无 MEMORY.md 索引步骤(
skipIndex = true):buildMemoryLines因此省略"Step 2 — 在 MEMORY.md 添加指针"的指令,直接写文件即可(src/memdir/memdir.ts#L205-L217); - 文件可直接写入,无需两步流程;
- 每种 agent 类型相互隔离(explorer、planner 等各有独立目录)——agentType 会先经
sanitizeAgentTypeForPath处理,把 Windows 上非法且用于插件命名空间的冒号(如my-plugin:my-agent)替换为连字符(src/tools/AgentTool/agentMemory.ts#L20-L23); - local 作用域在设置
CLAUDE_CODE_REMOTE_MEMORY_DIR时改挂载到远端目录下的projects/{sanitized-git-root}/agent-memory-local/(src/tools/AgentTool/agentMemory.ts#L29-L44)。
团队记忆同步
当TEAMMEM特性门开启时(isTeamMemoryEnabled()要求自动记忆启用,否则一律 false——所有团队记忆消费方由此保持一致,src/memdir/teamMemPaths.ts#L73-L78):
目录结构
~/.claude/projects/{hash}/memory/ ├── MEMORY.md <- 个人记忆索引 ├── user_*.md <- 个人记忆 └── team/ <- 团队共享目录 ├── MEMORY.md <- 团队记忆索引 └── *.md <- 团队记忆同步 API
GET /api/claude_code/team_memory?repo={owner/repo} <- 拉取 GET /api/claude_code/team_memory?repo={owner/repo}&view=hashes <- 仅元数据 + entryChecksums(不带正文) PUT /api/claude_code/team_memory?repo={owner/repo} <- 推送(upsert 语义) 404 = 尚无数据同步语义
- Pull:服务端内容覆盖本地文件(按 key 服务端优先);
- Push:仅上传与
serverChecksums内容哈希不同的 key(增量上传);服务端 upsert,PUT 中未出现的 key 保留(src/services/teamMemorySync/index.ts#L14-L24); - 删除不传播:本地删除不会移除远端条目,下次 pull 会恢复;
- 限制:单文件最大 250KB(
MAX_FILE_SIZE_BYTES = 250_000,预过滤超大条目以省带宽),上传体最大 200KB(MAX_PUT_BODY_BYTES = 200_000,超限分批顺序 PUT——服务端 upsert-merge 语义保证分批安全)(src/services/teamMemorySync/index.ts#L71-L91); - 重试:最多 3 次常规重试 + 2 次冲突重试;可变的
SyncState(ETag、serverChecksums、serverMaxEntries)由调用方创建并贯穿所有调用,避免模块级可变状态、利于测试隔离(src/services/teamMemorySync/index.ts#L100-L119)。
团队写入还有一层纵深防御:validateTeamMemWritePath()/validateTeamMemKey()除字符串级..检查外,还会对"最深已存在祖先"做realpath解析再比对真实目录——path.resolve()不解析符号链接,攻击者若能在 teamDir 内放置指向~/.ssh/authorized_keys的符号链接即可绕过字符串级包含检查(PSR M22186);悬空符号链接(ENOENT 但 lstat 成功)与符号链接环(ELOOP)也会被显式识别为PathTraversalError(src/memdir/teamMemPaths.ts#L96-L171、src/memdir/teamMemPaths.ts#L228-L284)。
团队 vs 个人路由规则
在 src/memdir/memoryTypes.ts 中,每种类型声明<scope>指令:
| 类型 | 默认作用域 |
|---|---|
| user | 恒为个人 |
| feedback | 默认个人;项目级约定才进团队 |
| project | 偏向团队 |
| reference | 通常团队 |
对应到提示文案:TYPES_SECTION_COMBINED中的 feedback 类型明确写道 "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)",并给出"集成测试必须连真实数据库"进团队、而"用户偏好简洁回复"留私人的对照示例(src/memdir/memoryTypes.ts#L57-L73)。
关键常量速查
// 索引文件 ENTRYPOINT_NAME = 'MEMORY.md' // src/memdir/memdir.ts MAX_ENTRYPOINT_LINES = 200 MAX_ENTRYPOINT_BYTES = 25_000 // 扫描 MAX_MEMORY_FILES = 200 // src/memdir/memoryScan.ts FRONTMATTER_MAX_LINES = 30 // 路径 AUTO_MEM_DIRNAME = 'memory' // src/memdir/paths.ts // 提取 maxTurns = 5 // forked agent 最多 5 轮,extractMemories.ts // 检索 最多返回 5 条相关记忆 // findRelevantMemories.ts // 团队同步 // src/services/teamMemorySync/index.ts MAX_FILE_SIZE_BYTES = 250_000 // 单文件上限 MAX_PUT_BODY_BYTES = 200_000 // 上传体上限(分批) MAX_RETRIES = 3 / MAX_CONFLICT_RETRIES = 2数据流总览
┌─────────────────────────────────────────────────────┐ │ 会话启动(Session Startup) │ │ │ │ loadMemoryPrompt() │ │ -> ensureMemoryDirExists() │ │ -> buildMemoryLines() + MEMORY.md content │ │ -> 注入系统提示(systemPromptSection 缓存) │ └────────────────────────┬────────────────────────────┘ | ┌─────────────────────────────────────────────────────┐ │ 用户查询(User Query) │ │ │ │ findRelevantMemories() │ │ -> scanMemoryFiles() [扫描 + frontmatter] │ │ -> Sonnet 选择最多 5 条相关记忆 │ │ -> 注入对话上下文 │ │ + 新鲜度警告 │ └────────────────────────┬────────────────────────────┘ | ┌─────────────────────────────────────────────────────┐ │ Claude 响应(Response) │ │ │ │ 模型可直接写入记忆 │ │ (遵循系统提示指引,两步流程) │ │ 或未写入 -> 触发后台提取 │ └────────────────────────┬────────────────────────────┘ | ┌─────────────────────────────────────────────────────┐ │ 后台自动提取(Auto-Extraction) │ │ │ │ executeExtractMemories() │ │ -> 互斥检查 │ │ (主代理已写入?跳过) │ │ -> 构建提取提示 + 记忆清单 │ │ -> runForkedAgent() │ │ [共享缓存、受限工具、5 轮上限] │ │ -> 写入新记忆文件 + 更新 MEMORY.md │ │ -> 通知用户 │ └────────────────────────┬────────────────────────────┘ | ┌─────────────────────────────────────────────────────┐ │ 后台记忆整合(AutoDream,可选) │ │ │ │ executeAutoDream() │ │ [每 24h + 5 个会话触发] │ │ -> 五重闸门检查 │ │ (开关/时间/节流/会话/锁) │ │ -> buildConsolidationPrompt() │ │ -> runForkedAgent() │ │ [只读 Bash、仅记忆目录写入] │ │ -> 四个阶段: │ │ Orient -> Gather -> Consolidate -> Prune │ │ -> 合并重复 / 修复过期 / 压缩索引 │ │ -> 通知用户:"Improved N memories" │ │ │ │ 详见 autodream.md │ └─────────────────────────────────────────────────────┘延伸阅读
- 记忆整合(AutoDream)的完整机制见 docs/en/internals/autodream.md;
- 记忆的顶层概念与使用指南见 docs/en/internals/memory.md;
- 记忆类型提示文案与作用域路由的完整定义在 src/memdir/memoryTypes.ts,团队同步的 API 契约与限流实现在 src/services/teamMemorySync/index.ts,子代理记忆路径解析在 src/tools/AgentTool/agentMemory.ts。
综上,cc-haha 的记忆系统是一个"提示注入 + 显式写入 + 后台兜底 + 模型选择召回 + 团队同步"的完整闭环:路径解析层用三级优先级与多层安全校验守住写入边界,提示层用 eval 验证过的措辞约束模型行为,提取层用 forked agent 与互斥/合并机制在后台零打扰地补全遗漏,检索层则把"选什么进上下文"交给 Sonnet 并用新鲜度警告对抗记忆漂移。这套设计对任何希望为 Agent 增加持久化记忆能力的实现者都有直接的参考价值。
- 人工智能
- AI 应用
- 桌面应用
- 代码智能体
- MCP Clients
【免费下载链接】cc-haha
Local-first cross-platform desktop workspace for Claude Code / agents: multi-agent, Git worktrees, code diffs, skill marketplace, multi-model, Computer Use, task-aware desktop pets, with WeChat, Feishu, DingTalk, Telegram, WhatsApp and H5 access.
相关推荐
cc-haha Skills 系统实现原理:Skill 从发现、注入到执行的完整链路剖析
cc haha Skills 系统实现原理:Skill 从发现、注入到执行的完整链路剖析 Skills 是 cc haha(Claude Code 本地优先工作
人工智能AI 应用桌面应用代码智能体MCP Clients深入解析 py-spy 内存读取机制:process_vm_readv 系统调用剖析
深入解析 py spy 内存读取机制:process_vm_readv 系统调用剖析 py spy 是一个革命性的 Python 采样分析器,它能够在无需修改代
开发工具性能剖析CLIAgent Zero 记忆插件的碎片自动提取机制:深入解析 memories_sum 系统提示词
Agent Zero 记忆插件的碎片自动提取机制:深入解析 memories_sum 系统提示词 memory.memories_sum.sys.md 是 Ag
人工智能大模型AI AgentAgent 框架自主智能体多智能体工具调用MCP 服务浏览器控制
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考