03. Session
关掉窗口,后面接着写:一次写作任务 = 一个 Session
前两篇我们把引擎拆完了:一颗会自转的心跳(Loop),加一套让它转久了不被上下文撑爆的压缩(Compact)。但它们都聚焦在「一次写作过程内部」。这一篇我们把眼光拉长一些:今天写到一半,把窗口一关,明天打开怎么接着写?同时开着三四个写作任务,它们怎么各回各的、不串台呢?答案的核心在 Session
🪧 想多任务并行写作,切换后前文都丢了?
前面踩过一个坑,在每周额度快要到期前,想着赶工同时写几篇,别把额度浪费了
SDK 支持 resume(恢复)的能力,它能根据会话 ID 把历史任务重新拉起来。按理说,SmartWriter 从设计上就能多任务并发:只要不超过模型限制(正常写作大概率也不会),同时推进几篇文章问题不大
可当我信心满满地发起多个写作任务后,却傻眼了,前面还在正常工作中的会话,切换回去就啥也没有了! 这下不仅流量额度没有薅到,还发现了一个深坑需要填补
这篇正好就以此为例,把 session 是什么、怎么存、怎么恢复,一层层讲清楚。这也是理解「一个写作产品怎么跨时间记住你」的第一步
🧭 基础概念:session 是「一条历史」,不是「一个项目」
先把两个特别容易糊在一起的概念区分一下:session(会话) 和 合集(项目)。
一个 session,就是一条会话历史。 具体到 SmartWriter,它是「你这一次写作任务,从新建到定稿,中间所有来回对话」的完整记录。你让它列大纲、它列了,你说第二段太平、它改了,这一整条你来我往的流水账,就是一个 session。它有几个特性:是一次性的(对应一次具体的写作任务)、会被压缩(就是上一篇讲的 Compact)、可以被恢复(跨天跨窗口接着写)。技术上,它落盘成一个 .jsonl 文件(可以理解成一行一条消息的对话日志)
而合集,是装很多个 session 的容器。 比如你有个系列专栏要写十篇,这十篇是一个「合集」;合集里每写一篇,是一个独立的写作任务,也就是一个独立的 session
打个比方:session 是一本正在写的笔记本,合集是装着好几本笔记本的一个抽屉。 抽屉本身不是笔记本,它是一个「归置的地方」,它还额外放着一张便签,记着这个系列共同的调性和已经写过的篇目。这个区分比较关键,它决定了「合集」在技术上该做成什么:一个目录 + 几个记忆文件,而不是「一条巨长的 session」
还有一个正交的概念也顺带厘清,就是上一篇反复出现的
CLAUDE.md。它和 session 是「互补的一对」:session 是这次写作的「动态历史」(一次性、会被压缩、结束就归档);CLAUDE.md是跨越所有 session 的「静态常驻规则」(每次请求都重注、抗压缩)。一个管「这次聊了啥」,一个管「所有次都得守的规矩」
🔩 Claude Code 怎么做:一个项目下,可以有很多条独立会话
session 和项目的这个层级关系,也是参考了 Claude Code 的实现,理解它那边的做法,就有了参照系
在用 Claude Code 时,同一个代码仓库(一个项目目录)底下,可以先后开很多次独立的会话:今天开一次,让它修个 bug;明天再开一次,让它加个功能。这些会话,每一条都有自己独立的历史记录(各自一条 .jsonl),它们互不干扰。 但共享着两样东西:一是这个项目的 CLAUDE.md(项目规则),二是同一套代码文件
所以 Claude Code 的架构设计是这样的:「项目」是共享的底座(规则 + 文件),「会话」是一次次独立的干活记录。 会话之间不共享同一条历史,但站在同一片底座上。SmartWriter 的「合集与任务」,其实就是把这套关系平移过来:合集是那片共享底座,每个写作任务是一次独立会话。项目中的很多设计细节,其实都是参考了 Claude Code 的项目模型,再按写作场景做适配
🛠 SDK 提供了什么:一个存储位置,和三种「回到过去」的姿势
Claude Agent SDK 把「会话怎么存、怎么恢复」封装成了几个明确的能力
先说存储
SDK 文档有明确说明,开箱即用的模式下,会话就落在 ~/.claude/projects/<把工作目录编码过的名字>/<会话ID>.jsonl,这个路径基本是写死的,不是你想放哪就放哪。不过 SDK 留了一个不起眼的口子,可以把这份历史「另存一份」到自己的地方;具体怎么接、什么场景非接不可,后面有一个真实踩过的坑会展开细说
再说恢复
这里是重点。SDK 给了三种「回到过去」的姿势,分别是:
| 姿势 | 怎么用 | 一句话理解 | 典型场景 |
|---|---|---|---|
| continue | 不用记 ID,直接接当前目录最近一条 | 「接着刚才那条聊」 | 单个任务,刚关掉想接着写 |
| resume | 传一个具体的会话 ID,精确恢复 | 「翻到指定那一本接着写」 | 多个任务并存,得点名恢复哪一个 |
| fork | 从某条历史复制一份另起,原的不动 | 「照着这本抄一份,在抄本上试」 | 想换个方向试,又怕弄丢现在这版 |
除了恢复,SDK 还给了 list_sessions(列出所有会话)、rename_session(重命名)、tag_session(打标签归类)这些零件,足够用来搭一个「任务列表 / 草稿历史查看器」了
⚠️ 避雷:resume 的时候,工作目录(cwd)必须和当初创建时一模一样。 因为 SDK 是靠「工作目录编码 + 会话 ID」去磁盘上找那条历史的,目录一变,它就找错地方、给你开一条全新的空会话,你还以为恢复成功了,可是却啥也没有
🧩 SmartWriter 怎么做:主要用的是 resume,本期并没有展开 fork
三个能力已经足够灵活,应用侧要做的是如何与「写作是长跨度反复打磨」这个场景绑定好
主用 resume:先解决并行写作打磨
最简单的工具可以是「单会话」的,关了再开,接着最近那条就行,用 continue 最省事。但我个人经验,一篇文章往往不是一蹴而就的,需要经过反复的打磨,沉淀后再改稿也是常有的事儿,这也意味着我常常多任务并存:同时推进一篇随笔、一篇产品文、一篇常规周报。这种情况下,continue「接最近一条」就不够用了,必须能精确点名「恢复的是随笔那条」
本期 SmartWriter 主要用 resume。实现起来并不复杂,应用侧把每个任务的会话 ID 记下来就好。具体是这样的,每次任务跑完,SDK 会在结束消息里返回一个 session_id,拿到后要把它存进这个任务的元数据里,下次要恢复时,就拿这个 ID 去 resume
善用 fork:写作试错的「后悔药」
虽然前端暂时还未接上fork 这个能力,但我个人觉得这是写作场景里的重要法宝,Phase 2 也会考虑加上。写代码要的是往前推进;写作有时候却要横向试探:这段用第一人称还是第三人称?这个开头是先抛结论还是先讲故事?这些岔路,感觉都可以试一下,但试的时候却又担心「把好不容易写顺的那版搞坏了」
fork 能给你尝试的底气:从当前这条历史复制一份平行草稿,可以在副本上大胆折腾,原版先稳稳保留。试出来更好的话,就采用副本;试砸了,那就切回原版,一个字没丢
合集不做成「一条巨长的 session」:传承靠喂记忆,不靠 resume
接上前面埋下的钩子:为什么把同主题的合集设计为「目录 + 记忆文件」,而不是「一条贯穿整个系列的大 session」?
先换个角度想,「一个系列十篇,共享一条长会话,它不就自然记得整个系列了吗」,理论上是不是也能搞?但真要落地,有两个绕不开的问题:一是那条会话会长到没边,反复触发压缩,前面几篇的细节早被摘没了(Compact!);二是十篇的历史搅在一条 session 里,要想单独恢复第三篇来改,根本没法干净地「点名」
说到底,这也是 session 边界的取舍:跨任务的传承,不靠 session resume,而靠「把合集的共享记忆,作为上下文喂进每一个新 session」。 具体是这么做的,合集目录下放着「合集画像」和「合集 memory」(那个系列的调性、主张、已写篇目索引),每开一篇新文章(一个新 session),就把这些记忆注入进去。这样每篇的 session 各自干净、可独立恢复,而「系列一致性」由那份共享记忆来保证,不绑死在一条脆弱的长会话上
那个「切换任务后丢失前文」的问题,到底是怎么回事
这个问题很让人头秃,当时解了很久,它耦合了前、后端的几个 bug:前端切任务时没记住 taskID 导致 SSE 没能续上、切任务时还调用了 abort fetch 把后端进程给误杀了、最后叠加了 streaming mode 不会把内存里的 session 明细自动落盘。最后一个和本篇强相关,尽管应用侧把当次写作任务的 sessionID 记住了,但 session 历史没能存下来也是白搭
🔧 避坑 · Streaming 模式,根本没往磁盘写会话(架构级)
现象:切换后切回,
load_history返空数组,前端看到「什么都没」。去~/.claude/projects/翻,一个文件都没有;全局搜 session ID,也没有结果根因:SmartWriter 的写作走的是 SDK 的流式输入模式(streaming input mode,为了能实时打字、随时打断)。而底层的 CLI 程序在这个模式下,有个容易掉坑的行为:它不把会话历史落盘,只存在自己进程的内存里。 所以「多轮接着聊」没问题(还是同一个进程、内存还在);可一旦进程被杀(同期还踩中的前端 abort fetch bug),内存里的历史跟着一起没了。SDK 文档说的那个存储路径,是传统(非流式)模式才落盘的,这里用的流式模式压根没往那写
正解:给 SDK 设一个
session_store(会话存储器)。我自己实现了一个FileSessionStore,把本该丢在内存里的会话历史,镜像一份到工作区的磁盘上。这样就算进程死了,历史还在磁盘上,切回时能重新读回来
📐 进阶深挖 · FileSessionStore 是怎么把内存里的会话「接」到磁盘的
一旦给
ClaudeAgentOptions设了session_store,SDK 内部会自动串起三步,把「只在 CLI 内存里的会话」镜像成「磁盘上可 resume 的文件」拆开说:① SDK 自动给 CLI 加一个
--session-mirror开关,让它一边在内存里跑、一边通过标准输出把每条会话事件「广播」出来;② 一个叫TranscriptMirrorBatcher的批处理器接住这些事件,调我实现的store.append()落盘;③ 等fork/resume时,SDK 调store.load()把历史读回来,物化(materialize)到一个临时目录,让新的 CLI 进程从那儿 resume我的
FileSessionStore就把这些历史,存到了工作区里一个可控的位置:.smartwriter/sessions/<项目键>/<会话ID>.jsonl,跟这个用户的其它工作区数据物理放在一起,既能 resume,也顺带成了「草稿完整演化史」的存档本质:流式模式为了「实时」牺牲了「落盘」,而
session_store就是 SDK 留的一根线,它可以把这份实时的会话,重新接回持久化的世界。fork/resume强依赖它
⚖️ 收个尾:用完即走 vs 反复回来,session 的分量完全不同
这一篇讲了很多技术细节,但背后的主线很明确:写作对「会话的持久化」有强要求。 「会话」并不是「用完即走」的耗材,聊完这次、下次的事下次说;它在写作产品里,是要反复回来、反复打磨、还得留住每一版的资产:
⚖️ 取舍现场 · 一次会话,是「用完即走」还是「反复回来」
「偷懒」的默认做法 SmartWriter 的选择 为什么这么选 恢复方式 多为 continue,接最近一条就够主用 resume,按 ID 精确点名写作是多任务并存,必须能各回各的、不串台 试错 不满意就重开一段对话 fork出平行草稿,原版不丢写作要反复横向试探,且历史版本一个都不能丢 会话持久化 用默认,很少自己接管 自建 FileSessionStore兜住流式模式不落盘写作跨天跨窗口,会话丢了等于心血白费 跨任务传承 基本没有 合集记忆喂进新 session,不绑长会话 系列一致性要稳,又要每篇能独立恢复
好,到这我们让「这一次写作任务」的历史,稳稳地跨越了关机、也能分叉试错了(理论上,暂未实现)。但你有没有注意到,我们解决的始终是「这一次」的记忆,session 再怎么恢复,装的也只是「这一篇聊了什么」。可 SmartWriter 的口号是「越写越懂你」,靠的是另一种记忆:跨越一次次 session、把你这个人本身记下来的长期记忆,你爱用什么词、有什么忌讳、长期偏好的调性
这种「跨 session 的记忆」到底分几层、每样放在哪、谁能写、又怎么防止被污染?这就是下一篇《记忆》的主题,也是我个人觉得整个专栏里,最能体现「垂直产品灵魂」的一篇。下一篇继续聊。