跨 Session 的记忆持久化——明天的 Agent 怎么接上今天的工作

4 阅读31分钟

跨 Session 的记忆持久化——明天的 Agent 怎么接上今天的工作

上一篇解决了"一个 session 内"的 context 管理。但还有一个更本质的问题:session 结束后,一切清零

你今天花了 2 小时和 Agent 一起调试一个复杂的分布式锁问题,终于找到了根因。明天继续,新 session 启动——Agent 完全不记得昨天发生了什么。你只能把昨天的聊天记录复制粘贴一大段给它,或者从头再解释一遍。

ECC 用一套 Hook 驱动的 Memory Persistence 系统解决了这个问题:session 结束时自动保存状态,下一个 session 启动时自动加载。

源仓库:affaan-m/everything-claude-code


Session 断裂的真实代价

每次新 session 的"冷启动"成本包括:

  • 重复解释项目背景:Agent 不知道你的项目结构、技术选型、当前进度
  • 重复失败的探索:昨天已经试过 3 种方案都不行,今天 Agent 又从第 1 种开始试
  • 丢失调试上下文:昨天定位到的关键线索("问题出在 Redis cluster 的 failover 时序")消失了
  • 丢失偏好和纠正:你昨天纠正了 5 次"不要用 class,用函数式"——今天又从头纠正

ECC 作者的观察:

For sharing memory across sessions, a skill or command that summarizes 
and checks in on progress then saves to a .tmp file in your .claude 
folder and appends to it until the end of your session is the best bet. 
The next day it can use that as context and pick up where you left off.

翻译:对于跨 session 共享记忆,一个能总结并检查进度、然后保存到 .claude 文件夹中 .tmp 文件的 skill 或 command 是最好的方案。第二天它可以用那个文件作为上下文,接着上次的进度继续。

来源:the-longform-guide.md - "Context and Memory Management"


Memory Persistence 三阶段架构

ECC 的 memory persistence 系统由三个 lifecycle hook 组成,分别在 session 的不同阶段触发:

Hook 事件触发时机做什么
SessionStart新 session 启动加载上一次的 session 状态 + 项目元信息
PreCompactcontext 压缩之前保存当前重要状态,避免 compact 丢失
Stop (SessionEnd)Agent 完成回答后从 transcript 中提取 summary 并持久化
{
  "events": [
    {
      "event": "SessionStart",
      "id": "session:start",
      "script": "scripts/hooks/session-start-bootstrap.js",
      "purpose": "Load bounded prior context and detect project state at session start."
    },
    {
      "event": "PreCompact",
      "id": "pre:compact",
      "script": "scripts/hooks/pre-compact.js",
      "purpose": "Persist session state before context compaction."
    },
    {
      "event": "SessionEnd",
      "id": "session:end",
      "script": "scripts/hooks/session-end.js",
      "purpose": "Persist session-end summaries when transcript metadata is available."
    }
  ]
}

来源:hooks/memory-persistence/hooks.json


SessionStart:智能加载上下文

SessionStart hook 是整个系统的入口——新 session 启动时,它决定给 Agent 注入什么上下文。

核心逻辑(723 行 Node.js)

session-start.js 做了这些事:

1. 查找最近的 session 文件

在 sessions 目录中搜索 *-session.tmp 文件,按修改时间排序,取最新的。每个 session 一个文件——这是关键设计,避免旧 session 的信息污染新工作。

const recentSessions = dedupeRecentSessions(searchDirs);
// searchDirs 可能包含多个位置:项目级 + 用户级

2. 注入 bounded context

不是把整个 session 文件都塞给 Agent——而是控制总量:

const DEFAULT_SESSION_START_CONTEXT_MAX_CHARS = 8000;
// 可通过 ECC_SESSION_START_MAX_CHARS 环境变量调整

8000 字符 ≈ 2000-3000 tokens,对 200K window 来说只占 ~1.5%。足够传递关键信息,又不会污染新 session 的空间。

3. 加载 learned instincts

如果 Continuous Learning 系统(下一篇会详细讲)产出了高置信度的 instinct,也会在此时注入:

const INSTINCT_CONFIDENCE_THRESHOLD = 0.7;
const MAX_INJECTED_INSTINCTS = 6;
// 只注入置信度 > 0.7 的 instinct,最多 6 个

4. 检测项目类型

通过 detectProjectType() 判断当前项目是什么技术栈,决定加载什么上下文:

const projectType = detectProjectType();
// React? Go? Python? 基于 package.json / go.mod / pyproject.toml 等判断

5. 允许完全关闭

# 如果你不需要 memory persistence
export ECC_SESSION_START_CONTEXT=off

来源:scripts/hooks/session-start.js


Stop Hook(SessionEnd):从 Transcript 中提取精华

Session 期间的每次 Agent 回答完毕后(Stop 事件),session-end hook 会从 transcript 中提取 summary。

为什么用 Stop 而不是 UserPromptSubmit

ECC Longform Guide 里专门解释了这个设计决策:

The key design decision is using a Stop hook instead of 
UserPromptSubmit. UserPromptSubmit runs on every single message - 
adds latency to every prompt. Stop runs once at session end - 
lightweight, doesn't slow you down during the session.

翻译:关键的设计决策是使用 Stop hook 而不是 UserPromptSubmit。UserPromptSubmit 每条消息都跑——给每个 prompt 加延迟。Stop 在 session 结束时跑一次——轻量,不会在 session 期间拖慢你。

来源:the-longform-guide.md - "Why Stop Hook (Not UserPromptSubmit)"

Summary 提取逻辑

session-end.js 读取 session 的 JSONL transcript,提取三类信息:

// 从 transcript 中提取:
const userMessages = [];    // 用户的任务请求(每条截取前 200 字符)
const toolsUsed = new Set(); // 使用过的工具
const filesModified = new Set(); // 修改过的文件

然后把这些信息写入 session 文件:

## Session Summary
- Tasks: [用户请求的摘要]
- Tools: [Edit, Write, Bash, Grep...]
- Files Modified: [src/auth.ts, tests/auth.test.ts...]
- Compaction Notes: [如果发生过 compact,记录时间点]

来源:scripts/hooks/session-end.js


PreCompact:在 Context 被压缩前抢救状态

PreCompact hook 的逻辑相对简单——在 context 被压缩前记录一条标记:

// 记录 compact 事件
appendFile(compactionLog, `[${timestamp}] Context compaction triggered\n`);

// 在活跃的 session 文件中标记
appendFile(activeSession, 
  `\n---\n**[Compaction occurred at ${timeStr}]** - Context was summarized\n`);

为什么要记录?因为 compact 后 Agent 不知道自己"忘了什么"。有了这个标记,下次加载 session 时 Agent 可以看到"曾经发生过 compact",知道之前有一些上下文被压缩掉了——它会更主动地去读文件确认状态,而不是凭记忆做假设。

来源:scripts/hooks/pre-compact.js


Session 文件的设计原则

ECC 的 session 文件设计有几个值得注意的选择:

每个 session 独立文件

~/.claude/.sessions/
├── 2026-06-10-auth-refactor-session.tmp
├── 2026-06-11-redis-debug-session.tmp
└── 2026-06-12-new-feature-session.tmp

不是把所有 session 追加到同一个文件里。原因:

Create a new file for each session so you don't pollute old context 
into new work.

翻译:每个 session 创建一个新文件,这样旧的 context 不会污染新工作。

Session 文件的三要素

ECC Longform Guide 指出 session 文件应该包含:

- What approaches worked (verifiably with evidence)
- Which approaches were attempted but did not work
- Which approaches have not been attempted and what's left to do

翻译:

  • 什么方案有效(有可验证的证据)
  • 哪些方案尝试过但没用
  • 哪些方案还没尝试、还有什么要做的

第二条特别重要——记录失败的方案。如果没有这个,新 session 的 Agent 很可能又走一遍昨天已经试过的死路。

自动清理

session 文件有保留期限,避免无限积累:

const DEFAULT_SESSION_RETENTION_DAYS = 30;
// 可通过 ECC_SESSION_RETENTION_DAYS 环境变量调整
// 设为 'off' 或 '0' 可以禁用自动清理

来源:the-longform-guide.md - "Context and Memory Management"


Dynamic System Prompt Injection:按场景切换人格

除了 session 文件这种"自动记忆",ECC 还有一种"手动模式切换"——通过 CLI 参数注入不同的 system prompt:

claude --system-prompt "$(cat memory.md)"

实际用法是建一组 alias:

# 日常开发模式
alias claude-dev='claude --system-prompt "$(cat ~/.claude/contexts/dev.md)"'

# PR Review 模式
alias claude-review='claude --system-prompt "$(cat ~/.claude/contexts/review.md)"'

# 研究/探索模式
alias claude-research='claude --system-prompt "$(cat ~/.claude/contexts/research.md)"'

ECC 仓库里提供了这些 context 文件的模板:

dev.md(开发模式):

Mode: Active development
Focus: Implementation, coding, building features

## Behavior
- Write code first, explain after
- Prefer working solutions over perfect solutions
- Run tests after changes
- Keep commits atomic

## Priorities
1. Get it working
2. Get it right
3. Get it clean

review.md / research.md 有各自的行为定义。

来源:contexts/the-longform-guide.md - "Dynamic System Prompt Injection"

权重层级

为什么用 --system-prompt 而不是直接放 CLAUDE.md?因为注入位置不同,权重不同:

System prompt content has higher authority than user messages, which 
have higher authority than tool results.

翻译:System prompt 内容的权威性高于用户消息,用户消息高于工具结果。

当你需要某些指令绝对优先时(比如"在这个 session 里绝对不要碰生产数据库"),用 system prompt 注入比写在 CLAUDE.md 里更可靠。


指令分层体系:SOUL → AGENTS → RULES → WORKING-CONTEXT → Contexts

ECC 的记忆不只是 session 文件——它有一套完整的分层指令体系,每层解决不同的问题:

层级文件作用变化频率
IdentitySOUL.md定义"我是谁"——核心原则和身份极少变
OperationsAGENTS.md定义操作规范——怎么用 agents/skills/hooks偶尔更新
Constraintsrules/语言/框架级约束——编码标准、安全规则按项目设
StateWORKING-CONTEXT.md当前状态——活跃工作队列、版本号经常更新
Modecontexts/*.md当前行为模式——dev/review/research按 session 切换

这套体系的核心理念:不是所有信息都需要在每个 session 加载

  • SOUL 和 AGENTS 是"始终加载"的(通过 CLAUDE.md 引用)
  • Rules 按项目类型选择性加载
  • WORKING-CONTEXT 按需更新
  • Contexts 按 session 模式动态注入

实操:最小实现一个 Memory Persistence 系统

不需要装 ECC 也能用这个模式。核心只需要两个 hook:

1. Stop Hook:session 结束时保存状态

~/.claude/settings.json 中:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "node ~/.claude/scripts/save-session.js"
          }
        ]
      }
    ]
  }
}

save-session.js 的最小实现思路:

  • 读取 stdin 中的 transcript_path
  • 解析 JSONL,提取用户消息和修改的文件
  • 写入 ~/.claude/.sessions/YYYY-MM-DD-session.tmp

2. SessionStart Hook:新 session 加载上次状态

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "node ~/.claude/scripts/load-session.js"
          }
        ]
      }
    ]
  }
}

load-session.js 的最小实现思路:

  • 在 sessions 目录中找最近的文件
  • 截取前 8000 字符
  • 输出到 stdout(Claude Code 会把 SessionStart hook 的 stdout 注入 context)

3. 别忘了 PreCompact

{
  "hooks": {
    "PreCompact": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "echo '[Compact at '$(date)']' >> ~/.claude/.sessions/compact-log.txt"
          }
        ]
      }
    ]
  }
}

Session Aliases:给 Session 起名字

ECC 还有一个小而实用的功能——session aliases。Claude Code 自带 /rename,但 ECC 在 session-start 时会列出之前的 session 让你选择恢复哪个:

const { listAliases } = require('../lib/session-aliases');

这在长期项目中很有用——你可能有 "auth-refactor"、"perf-optimization"、"api-v2" 多条并行工作线,每条都有自己的 session 历史。


总结

Memory Persistence 解决的是 Agent 开发中最被低估的问题:跨 session 的知识延续

ECC 的方案核心是三句话:

  1. Session 结束时提取精华——不存原始 transcript,存 summary
  2. 新 Session 启动时有限注入——不超过 8000 字符,只给最需要的上下文
  3. 每个 Session 独立文件——新工作不被旧 context 污染

这比"把聊天记录贴给它"高效一个数量级——Agent 拿到的是结构化的、有优先级的工作状态,而不是一大段未加工的对话历史。

下一篇我们从"记忆"进化到"学习":不只是记住发生了什么,而是从每次 session 中自动提取模式、内化为行为偏好——Continuous Learning 系统。


直接拿走:最小化 Memory Persistence

不需要 ECC 的 723 行 Node.js 脚本。一个最小化的跨 session 记忆方案:

加到 CLAUDE.md 里:

## Session 记忆规则
每次 session 结束前,我会说"输出 Session Summary"。
你需要总结:正在做什么、踩了什么坑、下一步做什么。
写入 .claude/session-state.md。下次启动我会自动读到它。

每次 session 结束前对 Agent 说:

输出 Session Summary,写入 .claude/session-state.md

下次 session 启动时 Agent 会自动读到这个文件。不需要 hooks、不需要脚本——只靠文件读写。

这是最小方案。ECC 的高级方案用 Stop hook 自动从 transcript 提取 summary,但需要几百行脚本。从最小方案开始,烦了再升级。


下一篇: Memory 让 Agent 记住了"发生了什么",但"记住了"不等于"学到了"。下次遇到同样问题时它会自动跳过错误方案吗?还是又从头踩一遍?下一篇讲 ECC 的 Continuous Learning——怎么让 Agent 从你的纠正中提取行为模式并内化。


本文素材来源:affaan-m/everything-claude-codehooks/memory-persistence/scripts/hooks/session-start.jsscripts/hooks/session-end.jsthe-longform-guide.mdcontexts/