← blog AI Agent · 2026-09-22

session-share:为 Coding Agent 构建本地优先的共享记忆层

拆解 session-share 当前的双层 SQLite 架构、按需 ingestion、MCP recall、受控 Pi 摘要出口、durable messaging 与 TaskRun 监督模型。

11 min read

Coding agent 很擅长完成眼前的任务,却不太擅长回答昨天的问题:哪个 agent 改过这个文件?当时为什么这样改?之前是否已经尝试过一个失败方案?

Claude Code、Codex、Pi、Qoder 等工具都会保存自己的 session history,但它们使用不同的 JSONL 格式和目录布局。session-share 就是为了解决这段断裂而设计的项目级记忆与审计层:它以只读方式读取各个 agent 的会话文件,将内容归一化成统一事件流,再通过 CLI 和 MCP server 提供 recall、handoff、消息通信与任务监督能力。

真正有挑战的地方不是再写一个 parser,而是让系统在功能增长时仍然保持边界清晰:源文件只能读,运行时存储必须有唯一权威,摘要生成必须经过明确的 egress 边界,worker 执行也不能因为一次 MCP 调用而被悄悄触发。

当前架构:两级 SQLite 边界

当前 target 架构使用两级 SQLite 存储。项目本地数据库保存该项目的会话和协作数据;中心 registry 负责项目发现,以及在调用方只有 session id、没有项目路径时进行反向解析。

flowchart LR
    A[Claude / Codex / Pi / Qoder / Gemini\\n只读 JSONL 源]
    B[Adapters\\n发现 + 归一化]
    C[ProjectContext + Ingestor\\n有界刷新]
    D[(项目 .session-share/index.db\\nsessions、events、FTS5、tags、task lines)]
    E[(项目 inbox.db\\n消息 + receiver state)]
    F[(项目 tasks.db\\nTask + TaskRun + 审计链)]
    G[(中心 registry.db\\nprojects + reverse index)]
    H[Typer CLI\\nsessionshare]
    I[MCP server\\nsession-share-mcp]
    J[本地 Pi RPC\\n只负责摘要生成]

    A --> B --> C
    C --> D
    C --> G
    H --> C
    I --> C
    H --> E
    I --> E
    H --> F
    I --> F
    D -. full / summarize .-> J

模块依赖方向被刻意压缩成一条单向链:

cli / mcp_server -> core -> adapters / pi_rpc

core 负责 target 的存储和业务规则。新 target 代码不能反向依赖保留的 v1 store、sync、query、parsers 模块。这个约束不是为了追求形式上的整洁,而是为了在新旧表面并存期间防止两条运行时路径互相污染。

初始化明确,打开失败时保持安全

ProjectContext 是 target 存储的唯一入口。初始化操作幂等,并且只创建 target 所需的目录:

uv run sessionshare init .

命令会创建 .session-share/index.db、profiles/、summaries/,并在 SESSION_SHARE_STORAGE_HOME 指向的 registry.db 中登记项目。中心存储还会创建 projects/<slug>,指向项目目录下的 .session-share/。

初始化和打开是两件事。读命令使用 ProjectContext.open(create=False, refresh=True):项目已经登记时,打开上下文会先执行一次有界 refresh;如果项目已经登记但 .session-share 存储消失,则抛出 ProjectStorageMissing。系统不会悄悄创建一个空数据库,因为空结果可能掩盖真实的数据丢失。

按需 ingestion:不重复解析没有变化的文件

Ingestor 通过 adapter contract 发现源文件。每个 adapter 负责理解一种 agent 的格式,core 只消费统一的 session skeleton 和 event list。agent 原始目录始终以只读方式打开,刷新过程不会修改它们。

一次 refresh 使用三步 decision matrix:

  1. 先比较源文件的 (mtime_ns, size) 与数据库记录。签名不变时直接返回 unchanged,跳过解析。
  2. 如果签名变化,再计算 SHA-256。字节内容相同则只更新文件签名,仍然跳过解析。
  3. 如果 digest 变化,才执行解析,并以原子事务 upsert 归一化 session 与 events。

refresh 会返回 added、updated、unchanged、archived、errors 五类计数。某个文件格式损坏只会进入 errors,同一次刷新中其他文件仍然可以成功提交。系统还会记录 source path,因此文件名中的 session id 提示值不会覆盖 JSONL 内真正的 id。

普通的读取流程不需要先手动同步:

uv run sessionshare list .
uv run sessionshare query "schema migration" . --depth brief

如果需要一份完整的刷新报告,仍然可以显式执行:

uv run sessionshare sync . --agent codex

Recall 分为三种深度

查询层通过三种深度控制返回数据量和副作用:

  • brief:返回 session 元数据、tags、notes、时间和事件数量。
  • raw:在 brief 的基础上增加 source metadata 与归一化 events。
  • full:在 raw 的基础上增加生成的或降级的摘要。

搜索使用 SQLite FTS5。配置的 tokenizer 同时支持中英文查询;agent、时间、隐藏状态和 archived 状态等过滤条件会在项目归一化后再应用。调用方可以通过中心 (session_id, cwd) reverse index 只提供 session id 来解析项目。如果同一个 id 属于多个项目,API 会返回明确的 ambiguity error,而不是猜一个项目。

查询返回值保持稳定而简单。无效 depth、项目不存在、session 不存在和 id 歧义都会在 MCP 边界转换为固定 error object,不会把 Python exception 直接泄露给 agent。CLI 和自动化调用方因此可以共享同一套行为。

摘要生成是一条明确的 egress 边界

会话内容可能包含源代码、误粘贴的凭据或项目内部信息。因此,摘要生成不能隐藏在每一次 recall 里。

只有 summarize_session 和 depth=full 可以调用本地的 pi —mode rpc —no-session。brief、raw、缓存 conclusion、messaging、tasks、telemetry、improve 分析和可选 UI 都保持本地执行。成功生成的结果会标记 _meta.egress=“via-pi-rpc”;达到上限、不支持或调用失败时使用 _meta.egress=“none”,并返回确定性的状态。

摘要按 session 的 raw digest 缓存。transcript 发生变化后,旧缓存不会被误认为是新内容的摘要。调用方可以先读取低成本的本地数据,再在确实需要时付出摘要生成成本:

uv run sessionshare get SESSION_ID --project . --depth raw
uv run sessionshare summarize SESSION_ID --project .
uv run sessionshare conclusion SESSION_ID --project .

MCP:稳定的本地协作契约

target session-share-mcp 使用 stdio,提供四组工具:

  • 五个 recall 工具:query_sessions、get_session、list_sessions、get_session_conclusion、summarize_session;
  • 两个 setup / maintenance 工具:init_project、sync_project;
  • 三个 durable messaging 工具:send_message、receive_message、ack_message;
  • 八个 task 工具,用于任务记录、审核和 TaskRun 查询。

每个 handler 都独立打开并关闭自己的 ProjectContext。recall handler 可以刷新 session index;messaging 和 task handler 使用 storage-only open,不会扫描 agent 目录。这样,一次消息 ack 不会意外触发 ingestion,一次任务读取也不会带来额外的外部副作用。

Messaging 是队列,不是隐藏的 dispatcher

agent 协作使用追加写入的 .session-share/messages.jsonl 和可重建的 inbox.db 索引。每个 frame 都包含 allowlisted kind、明确的 addressing mode、sender、receiver、refs 和 256 KiB body 上限。日志 frame 会先写入并 fsync,再写入索引。

普通 receive 采用 at-least-once 语义。返回给 reader 的消息会进入 pending;未 ack 的消息会被重复投递,并阻塞更新的消息。ack_message 追加 acknowledgement frame,随后释放该 reader 的 ack gate。带过滤条件的读取和显式时间窗口读取适合审计,但不会移动持久化 cursor。

task notification 可以用 kind=request frame 作为本地 wake-up,但这个 frame 不是 dispatch authority。它不会 claim TaskRun、启动进程或调用 Pi。把这些职责分开,可以避免一次普通消息读取变成执行操作。

Task 与 TaskRun:把意图和执行分开

任务监督器在 tasks.db 中保存两种相关但独立的记录:

  • Task 描述要交付什么、由谁负责、由谁审核。
  • TaskRun 描述某一次执行尝试如何被 claim、如何更新心跳、如何结束和审计。

Task status transition 有严格 guard。worker 不能把自己的提交直接标记为 accepted,只有不同的 reviewer 才能把 submitted 变成 accepted 或 rejected。写操作使用 optimistic compare-and-set;过期 writer 会得到 stale_task_writer,不会覆盖更新的决策。

TaskRun claim 使用 BEGIN IMMEDIATE,并限制每个 worker slot 只能有一个 active run。状态变化和 audit event 在同一事务中提交。如果进程中途停止,启动恢复逻辑会把 run 标记为 recovery_required,而不是在 MCP 读取时悄悄重试。worker 执行、listener/orchestrator 和显式 recovery 都保留在 CLI 路径。

对于 agent 系统,这是一个可复用的模式:先持久化业务记录,再独立记录执行事实,最后在明确的人或策略边界之后才能进入终态。

v1 兼容面仍然保留

仓库仍然提供旧的 session-share Click CLI 和 v1 MCP facade,供尚未迁移的安装方使用。它的 JSON metadata、links 和 index.sqlite 是兼容性资产,不是 target 的运行时权威。

系统没有自动迁移,也没有混合模式。v1 store 与 target index.db 不能当作同一个数据库使用。重新执行 sessionshare init 会创建新的 target store,同时保留旧文件给兼容路径使用。迁移边界因此是显式的,回滚路径也更容易理解。

默认本地化,并保持可观测

可选的 audit UI 是独立项目。它只绑定 127.0.0.1,读取 target store,并展示 message、task、TaskRun 与 telemetry。它不会调用 Pi、派发 worker 或暴露 recovery endpoint。telemetry 同样只写本地,并可以通过 SESSION_SHARE_TRACE=0 关闭。

整个系统的权限边界可以概括为:

只读 agent 源文件
        -> 本地归一化存储
        -> 显式 recall 深度
        -> 显式摘要 egress
        -> 显式任务执行与审核

session-share 不试图替代 coding agent,而是为多个 agent 提供一个共享、可检查、可审计的记忆面,并明确数据、side effect 与决策分别由谁负责。

结语:跨 agent 连续性不只是上下文窗口问题

跨 agent 的连续性经常被描述成 context window 问题,但它同样是存储、身份和权限问题。一个可用的方案至少要回答四件事:

  1. 这个 session 属于哪个项目?
  2. 哪些字节真的发生了变化?
  3. 哪些操作允许离开本机?
  4. 谁可以宣布工作已经完成?

session-share 用项目级 SQLite authority、digest-aware ingestion、窄化的 Pi egress 边界和 guarded task supervisor 回答了这些问题。架构足够小,开发者可以直接检查;边界又足够清晰,可以支持多个 agent 长期协作维护同一个代码库。

Sources

No external sources for this entry.

Related