2114 words
11 minutes
session-share 架构详解

session-share 架构详解#

解决什么问题:三个 agent,三套散落的会话#

如果你同时用 Claude Code、Codex CLI、Pi 这三个 coding agent,大概率会遇到这种场景:

  • 在 claude 里搞了半天某个 feature,session 结束;
  • 第二天想接着干,但开了个 codex 窗口——claude 那个 session 的上下文 codex 看不到;
  • 想复盘”昨天我在 pi 里改了哪些文件”,得手动翻 ~/.pi/agent/sessions/ 里的 JSONL;
  • 多个 agent 改过同一个项目,但没有一个统一视图能告诉你”这个项目被谁动过、改了什么”。

session-share 就是冲着这个来的:它只读地扫描三个 agent 写在 ~/.claude/~/.codex/~/.pi/ 的 JSONL 会话文件,把它们归一化到一个 project-local 的存储(<project>/.session-share/),再用 CLI 和 MCP server 两种方式暴露出来。它改造任何 agent,也写入它们的目录。

一图看全#

session-share 架构图

图里能直接读出来的几件事:

  1. 左边虚框 = 只读源。三个 agent 各自的 JSONL 路径不一样,sync 通过 cwd 字段判断每条会话属于哪个项目。
  2. 右边虚框 = session-share 自身,中间一条 sync → store → query 的流水线,右边再分叉到 CLI(给人用)和 MCP Server(给 agent 在 handoff 时调用)。
  3. Pi RPC 是侧支:query → pi_rpc 走虚线,因为它是 LLM brief 的可选子进程,失败时降级到结构化数据。
  4. ~/.session-share/<slug> 是审计入口——一个全局 symlink,指向每个项目的 .session-share/

三层管道,模块依赖无环#

整张图的核心是一条单向管道,模块依赖严格无环:

cli ──┬──► sync ──► parsers ──► model
│ │
├──► query ──► store ──► model
│ │
│ └──► pi_rpc
└──► registry
mcp_server ──┬──► sync
└──► query

每一层的边界是硬的:

职责关键类型
model/纯 dataclass,无 IO,无外部依赖SessionHeader, Event, Metadata, TaskLine
parsers/唯一知道三个 agent JSONL schema 的地方ClaudeParser, CodexParser, PiParser
store/唯一.session-share/ 磁盘格式的地方ProjectStore, SessionStore, IndexStore, MetadataStore, LinksStore
sync/发现 + 增量同步 + registry 清理sync_project(), discover_project_sessions(), register(), prune()
query/读侧 facade,CLI 和 MCP 共用list_sessions(), get_session(), search(), build_brief()
pi_rpc/无状态子进程驱动 pi --mode rpc --no-sessionPiRpcClient, PiRpcResult
cli / mcp_server薄壳,只 orchestrate sync + query

climcp_server 故意做得很薄——业务逻辑都在 sync 和 query 里。这意味着 CLI 加一个命令和 MCP 加一个 tool 的工作量基本一样,因为它们都只是在调同一组底层函数。

数据流:从散落的 JSONL 到归一化 Event#

session-share sync <project> 一次同步做的事:

  1. 发现:对每个配置的 AgentSource,glob <root>/<pattern>,扫到所有 JSONL 文件;
  2. 过滤:每个文件读前 50 行,取出 cwd 字段(claude/pi 是 cwd,codex 是 payload.cwd),Path.resolve() 后跟传入的 project path 比对——不匹配就跳过;
  3. 增量:对匹配的文件算 sha256,跟已存档的 rawDigest 比;一样的就跳过,完全不重解析;
  4. 归一化:让对应的 parser 把 JSONL 转成统一的 Event 流;
  5. 写盘:整文件覆盖 sessions/<sessionId>.jsonl(不可变,永远整文件覆盖),并 upsert index.sqlite 里的 sessions / session_tags / events FTS5 表;
  6. 清理:扫一遍 ~/.session-share/*,把指向已不存在项目的死 symlink 删掉。

容错策略:单个文件解析失败不致命,失败信息塞进 SyncResult.errors,继续处理别的文件。

项目存储:.session-share/ 里有什么#

<project>/.session-share/
├── sessions/<sessionId>.jsonl # 归一化会话,header 在第一行,后面每行一个 event
├── index.sqlite # sessions + session_tags + events FTS5(纯加速层)
├── metadata.json # 用户可编辑:每个 session 的 title/tags/notes/hidden/pinned
├── links.json # 用户可编辑:跨 agent 的 task line 分组
└── briefs/<sha256>.txt # 缓存的 LLM brief,内容寻址
~/.session-share/<slug> # symlink → <project>/.session-share/

几个有意识的设计选择:

  • metadata.jsonlinks.json 故意是 JSON,不是 SQLite。它们是要被人手编辑、被 git 追踪的;sync 永远不动它们,所以你的 tag / 笔记 / link 不会被同步覆盖。
  • index.sqlite 是加速层,不是真相源。删了它再 sync 一次就重建。这意味着它永远不会”坏掉”——出问题就 rm 然后重 sync。
  • sessions/<id>.jsonl 不可变。sync 要么完全覆盖一个文件,要么完全跳过(基于 sha256),不存在”patch 一行”。
  • briefs/ 内容寻址。cache key = sha256(json(sessions + rawDigests)),输入变了一点都会生成新的 key,所以 cache 永远不会”对旧输入给出新 brief”。

CLI 与 MCP:同一份 store,两种消费姿势#

CLI 是 Click 的命令组,MCP server 是 FastMCP 的 stdio server。两者都是 sync / query 之上的薄壳,所以你用 CLI 还是 MCP 拿到的数据完全一致——没有第二份数据通路,也没有第二份缓存。

CLI 主要命令:

命令作用
init <path>创建 .session-share/ 并注册全局 symlink
sync <path> [--agent X]拉取匹配 cwd 的所有 session
list [path] [--agent --since --tag --include-hidden]列出 session,pinned 优先
show <sessionId>完整 dump 一个 session
search <query>FTS5 全文检索 event 内容
brief [--scope recent|taskLine] [--raw]生成 handoff brief
tag / note / hide / pin编辑 metadata
link --name <title> <sid>...把多个 session 编成一条 task line
audit列出所有注册过的项目
doctor <path>一致性检查(目前查 orphan metadata)

MCP 暴露的六个 tool:

Tool用途
init_project(project_path)幂等初始化,agent 在对话里就能 self-init
sync_project(project_path, agent?)幂等同步,可指定单一 agent
list_sessions(project_path, agent?, since?, tag?, include_hidden?)列出 session
get_session(session_id, project_path)取单个 session 的 header + events + metadata
search_sessions(project_path, query, limit?)FTS5 搜索
get_handoff_brief(project_path, scope?, task_line_id?, limit?, llm_brief?)生成 handoff brief

init_projectsync_project 是后加的两个幂等 MCP tool——它们让一个新进对话的 agent 不用 shell 就能完成 self-initialization。这是个关键缺口补丁:在那之前,agent 必须用 Bash 调 CLI 才能初始化项目,现在直接走 MCP。

brief 的五种 mode#

get_handoff_brief / session-share brief 是 handoff 的核心。它有五种返回 mode,理解了它们就理解了整个 brief 的降级链:

mode触发条件返回内容
empty项目里没有任何 session啥都没有
cached同样的输入集早先生成过 brief缓存的 text
raw调用方传了 llm_brief=False结构化 data(sessions + rawDigests)
llmpi rpc 调用成功LLM 生成的 text
fallback_rawpi rpc 失败(超时 / 崩溃 / binary 缺失)结构化 data + fallback_reason

cache 是内容寻址的,所以”用户说他刚刚又干了点活”的判断很自然——只要 sync 拉到了新文件或 rawDigest 变了,cache key 就变,直接落到 llmfallback_raw 重新生成。

fallback_raw 不是错误,是正常路径之一。Pi 没装、pi 卡死、pi 输出超 10 MB 都会落到这里,agent 拿到结构化数据自己也能继续工作。

一个不显眼但关键的实现细节:_run_coroutine#

brief 调 pi rpc 是异步的,但 build_brief 既要支持 CLI(同步上下文)又要支持 MCP server(已经在 event loop 里)。直接 asyncio.run() 在 MCP 那条路径上会爆 “event loop is already running”。

解法是一个叫 _run_coroutine 的小 helper:

  • 调用方不在 event loop 里 → asyncio.run(coro);
  • 调用方已经在 event loop 里(MCP server)→ 起一个 worker thread 跑 asyncio.run(coro),把结果 marshal 回来。

看起来像”过度工程”,但有一条回归测试 tests/test_query/test_brief.py::test_brief_llm_works_inside_running_event_loop 把这个约束钉死了——任何想”简化”它回 asyncio.run() 的 PR 都会让那条测试挂。这是用测试固化非显然约束的典型例子,值得在 PR description 里贴。

几条硬约束(违反就算 bug)#

  1. Agent 源只读。永远不写 ~/.{claude,codex,pi}/tests/test_sync/test_importer.py 在 sync 前后比对源文件 sha256 强制这一点。
  2. 项目身份 = Path(cwd).resolve()。同路径 = 同项目;项目挪到别的路径 = 新项目(Phase 2 才支持手工合并)。
  3. sessionId 是 UUID,跨 agent 几乎不会碰撞,所以不需要 merge / conflict 逻辑。
  4. 格式知识只能活在 parsers/store/。其他模块不准直接读 JSONL 或 SQLite。这条规则让替换某个 agent 的 parser 不会牵动 query / cli / mcp。
  5. metadata.json / links.json 必须保持 JSON。手编、git-friendly、sync 不动它们——这条不动摇。

当前阶段#

Phase 1 feature-complete:

  • 23 个 SDD 任务全部完成(model / parsers / store / sync / query / cli / mcp);
  • 安全与完整性加固(FTS5 异常输入兜底、source byte-hash 不变量测试);
  • 两个幂等 MCP tool(init_project / sync_project)让 agent self-initialize;
  • 配套的 session-share-recall skill,教 agent 什么时候该调哪个 MCP tool。

Phase 2 留了几个 extension point:watch mode(sync/discovery.py 已经暴露了 discover_project_sessions(),接 inotify 就行)、多路径项目身份Web UISessionStart hook 注入。架构上故意留的口子,具体什么时候做看需求。


如果你在做一次好奇玩家的自我审查:session-share 不是 orchestration 层,不替任何 agent 决定下一步干啥;它是个审计 + handoff 数据层,把三个 agent 各自为政的会话文件缝成一份可查、可标、可交接的资料库。它解决的问题不大,但踩过的人才懂有多烦。

session-share 架构详解
https://sgjki547.top/posts/session-share-architecture/
Author
SGJki
Published at
2026-07-05
License
CC BY-NC-SA 4.0