@brglng/pi-session-sync
v0.1.0
Published
Sync Pi coding agent session files to an external mirror directory with three-way conflict merging. Per-project session directories use portable names compatible with pi-portable-sessions, but this extension is independent and does not depend on or read t
Maintainers
Readme
@brglng/pi-session-sync
把 Pi coding agent 的 session 文件 同步到外部镜像目录,并支持三方冲突合并。
Pi 默认把每个项目的 session 以 JSONL 文件存放在
~/.pi/agent/sessions/。本扩展把它们镜像到可配置的外部根目录,并使用可移植
的项目目录名,让同一项目在不同机器上映射到相同位置。可移植命名由本扩展独立
实现——它只依赖普通文件系统,既不创建也不跟随符号链接桥。
工作原理
项目默认 session 目录:
~/.pi/agent/sessions/--Users-alice-my-project--/*.jsonl被镜像到外部同步根目录下的可移植名:
<syncRoot>/HOME%2Fmy-project/*.jsonl每个项目的同步状态存放在:
<syncRoot>/.pi-session-sync/sessions/<portable-name>/包含该项目的 manifest、最近基线快照、冲突归档、备份和锁。按项目分片保存状态, 这样单项目同步就无需全局锁。
同步根目录还包含一个根级元数据文件:
<syncRoot>/.pi-session-sync/global.json它只保存格式元数据(kind: "global"、formatVersion);所有项目状态仍在各自
分片内。该文件在首次同步时创建;已存在但格式损坏或使用不受支持的格式版本时,同步会
在任何写入前拒绝执行,从而及时发现外部目录被误用或损坏。requireGit: true 时,
该文件属于扩展管理路径,会被暂存并提交。
同步时机
- 退出时同步(默认开启): Pi 退出前,当前项目的 session 文件与镜像对账。
- 启动时同步(可选、实验性): 运行前拉取外部变更。若当前 session 文件被
替换,内存中的会话已过期,必须重启 Pi(当前公开 API 无法安全地针对外部
cwd重建 runtime)。 /session-sync sync命令: 按需同步当前项目。
支持 scope: "current" 和 scope: "all"。配置 scope: "all" 时,扩展会枚举
sessions 根目录下的所有项目,在单个全局锁下逐个同步,并在最后对所有受管路径
做一次 Git 提交。
变更检测与合并
每个同步文件都与上次基线快照比较。单侧变更直接复制;只有一侧变更则应用; 两侧都变则对 entry 树做确定性三方合并。当无法在不猜测的情况下合并时,两侧分歧 版本会被快照到该项目的冲突归档,文件本身保持不变。
策略:
sameIdConflict(conflict|newer-wins):同一entry.id的两份分歧版本 如何处理。默认conflict。deletionConflict(keep-modified|keep-deleted|conflict):一侧删除 文件/entry 而另一侧修改时如何处理。默认keep-modified。initialize(merge|local-wins|external-wins|abort):首次同步时 两侧持有分歧文件且没有基线快照时如何处理。默认merge,会尝试对两份 entry 树做确定性的结构合并(分歧的同entry.id遵循sameIdConflict);当合并结果 无法安全解析或校验时,两侧版本归档为冲突且不写入。abort总是归档;local-wins和external-wins用一侧覆盖另一侧。
session 文件在复制或合并前都会经过严格校验:header 必须包含 id、cwd 和
ISO-8601 时间戳(逐项校验日历/时钟分量,不可能的值如 13 月或 2 月 31 日会被拒绝,
而不是被静默归一化),每个 entry 必须包含 id、显式的字符串/null parentId、
严格合法的时间戳和完整的父子树。非法文件会连同原始字节一起归档为冲突,绝不会
被复制进 Pi 或镜像。
镜像、基线快照和冲突归档中的 header cwd 始终保存为可移植路径
pi-session-sync://<portable-name>,绝不把机器绝对路径留在同步目录;写回本地 Pi
session 目录时,再反向把该 URI(或旧镜像遗留的机器绝对 cwd)映射为当前机器的
项目 cwd。无法安全验证或映射(机器路径/URI 不属于该项目、格式损坏、空值)时
拒绝写入并归档,不猜测改写。
每个文件的 header cwd 必须通过可移植命名规则映射到它将被同步的项目分片;不
匹配时归档该文件而不是复制。共享侧(镜像、基线快照、冲突归档)存储的是规范可移植
URI;写回本地时再反向映射为当前机器项目 cwd。同一 session.id 出现在
两个不同文件名下(同一侧或本地/镜像两侧)会被归档为身份冲突,而不是静默合并。
所有破坏性步骤都保证旧内容可恢复:替换或删除前先把现有各侧备份(每文件最新备份, 外加多文件事务的逐操作 rollback journal),删除只有在每一侧都留有可恢复副本后才 传播。备份、journal 文件及其路径在每次复制、恢复或删除前都会用相同的符号链接/路径链 防护重新检查,因此操作中途的符号链接替换不能把备份、回滚或清理重定向到受管根目录 之外(非 kernel CAS 仍留有狭窄窗口,属已接受限制)。若回滚或清理无法安全完成,副本 会被原地保留并报告路径供手动恢复,自动 Git checkpoint 也会暂停。失败的冲突归档写入 会移除本次调用创建的快照文件;任何无法安全移除的产物会被保留并随错误一起报告, 因此失败的归档操作既不会遗留无主快照,也不会沿中途被替换的路径删除。
当共享 session 写入本地 Pi session 目录时,如果映射无歧义(父文件的 basename
存在于本项目的本地或镜像目录,且存储路径是绝对 Pi sessions 路径),
header.parentSession 会被归一化到当前机器的项目目录,同时 header.cwd 会从
规范 URI 反向映射为当前机器的项目路径(旧镜像遗留的绝对 cwd 同样迁移)。镜像和
基线快照始终保留规范 URI 形式,header.cwd 在共享侧会被改写为可移植 URI,避免机器
路径泄漏。
有歧义或不安全的父引用保持不变。
entry 级 tombstone 在所有写入路径上生效,包括单侧复制和删除解决:被删除的
entry 永远不会复活。如果移除 tombstone 标记的 entry 会导致孤儿节点或破坏树结构,
写入会被拒绝并归档冲突。tombstone 只能通过显式 /session-sync resolve 清除。
Git
当 requireGit: true(默认)时,同步根目录必须是 Git 仓库。扩展只暂存并提交
自己管理的路径,绝不暂存或提交无关用户文件。它不执行 fetch、pull 或
push。当 requireGit: false 时,同步直接在普通文件系统上进行。
嵌入 Git pathspec 的所有用户派生值(可移植项目目录名)都按字面匹配
(:(literal)),运行时锁目录用 :(exclude,literal) 显式排除,因此标签永远
不能充当 Git pathspec 魔法或 glob 来改变被暂存/提交的文件集。
每次对账之后(以及每次显式冲突解决之后),扩展会在 fail-closed 防护下做一次
checkpoint 提交:受管路径在操作前必须干净;HEAD 在操作期间不得移动;当前唯一受管
改动必须恰好是本次操作写入或删除的文件——porcelain 中触及受管路径的
rename/copy 行始终阻止提交(扩展自己从不暂存或移动文件),被保留的恢复产物同样
阻止提交;本次同步创建的 global.json 仅在仍携带本次写入的完全一致字节时才被提交。
被解决记录的已移除冲突快照文件计入该次 resolve 自身的删除,并随它一起提交。resolve
上的每次跳过或 Git 失败都会作为已成功解决结果的提示信息展示,绝不会把已解决的冲突
重新变成失败。
安装
npm install -g @brglng/pi-session-sync包通过 pi.extensions 注册,会被自动加载。
配置
扩展读取标准的 Pi 扩展配置文件:
- 全局:
~/.pi/agent/extensions/pi-session-sync/config.json - 项目级:
<cwd>/.pi/extensions/pi-session-sync/config.json
项目级配置覆盖全局配置;portableName.extraPrefixes 映射会合并。参见
config/config.example.json 与
schemas/config.schema.json。
{
"$schema": "https://raw.githubusercontent.com/brglng/pi-packages/main/packages/pi-session-sync/schemas/config.schema.json",
"syncRoot": "~/session-sync",
"scope": "current",
"requireGit": true,
"syncOnStartup": false,
"syncOnShutdown": true,
"sameIdConflict": "conflict",
"deletionConflict": "keep-modified",
"portableName": {
"homeLabel": "HOME",
"rootLabel": "ROOT",
"extraPrefixes": {
"/Volumes/Backup": "BACKUP"
}
}
}syncRoot 必填,可使用 ~。session 根目录与 Pi 自身解析方式一致
(PI_CODING_AGENT_SESSION_DIR → settings.json 的 sessionDir → 默认的
<agentDir>/sessions)。
命令
/session-sync status
显示当前同步配置(scope、requireGit、启动/退出同步及各合并策略)、可移植名、
session 根目录、同步根目录、检测到的 session 布局和配置警告,并只读地显示当前
项目的未解决冲突数和 tombstone 数(不会修改任何同步状态)。Conflicts 为冲突
归档中未解决的记录数;Tombstones 为文件级 tombstone 数及 manifest 中跟踪的
entry 级 tombstone 总数。当根元数据、项目 manifest、冲突索引或 session 布局无法
被信任(缺失、损坏、foreign 或符号链接)时,status 会报告不可用原因,而不是显示
具有误导性的零计数。
/session-sync sync
同步当前项目。Pi 忙时拒绝执行。
/session-sync conflicts
列出当前项目冲突归档中的冲突条目。
/session-sync resolve <id> --local|--external|--merge
采用归档的 local 或 external 快照,或把两侧归档快照与归档基线快照做 确定性合并来解决一条已记录冲突:
--local/--external直接采用所选快照。写入前会严格校验所选快照。--merge使用与普通同步相同的确定性三方结构化合并:自基线以来未变的 entry 只保留一份,单侧新增/删除会传播,同一entry.id的分歧内容按sameIdConflict处理(默认conflict,不猜测)。三方合并的共同祖先是归档冲突 时保存的基线快照,因此之后对实时基线的修改不会影响本次合并。在基线归档支持 之前创建的记录(或归档基线缺失/损坏)、以及存在歧义的合并,都会报出明确错误 并保持所有文件不变。
成功后原子替换本地 session 文件、镜像文件和基线快照,清除该文件的 tombstone, 并移除冲突记录。失败时各侧会从替换前备份回滚;回滚不完整时恢复副本会被原地保留 并随错误报告路径,冲突记录保持打开。在 Git 管理的同步根上,该次 resolve 会像普通 同步一样做 checkpoint(见 Git 一节):只暂存并提交本次 resolve 写入或移除的受管路径, 任何跳过或失败都会作为已成功 resolve 的提示信息展示。Pi 忙或分片锁被占用时拒绝执行。
/session-sync unlock --force [--global|--git]
强制移除过期锁。默认目标是当前项目的分片锁;--global 目标是全量同步的根锁,
--git 目标是当前项目的 Git 操作锁。会先读取锁持有者并检查其 PID 是否存活:同主机
上仍存活的进程所持有的锁会被拒绝移除;移除前会立即重新读取 owner 文件
(compare-before-delete),若锁在检查期间易主则不会删除。同主机身份使用所有锁写入方
统一记录的标准主机名(node:os hostname,回退到 HOSTNAME 环境变量与固定字面量);
使用回退身份时,主机名不匹配并不能证明是另一台机器,因此 force-unlock 仍然执行 PID
存活检查(fail-closed),绝不会仅凭不可靠的主机名比较移除一个可能存活的锁。普通
sync 从不自动抢占已有锁。绝不能在 Pi 写 session 时使用:普通 sync 仍要求 idle,
--force 只作用于锁,绝不绕过 idle 门限。
当前局限
- 支持
scope: "current"和scope: "all"。scope: "all"只枚举 sessions 根目录下的逐项目子目录;符号链接项目目录会被直接跳过(绝不跟随)。 - 扁平的自定义
sessionDir布局(session 文件直接存放在 sessions 根目录, 例如通过PI_CODING_AGENT_SESSION_DIR或settings.json的sessionDir, 而不是每个项目一个--<cwd>--子目录)不受支持。同步会被拒绝并给出可见提示,/session-sync status也会报告该布局。 - 启动同步属于实验性,可能让当前内存 session 过期。公开扩展 API 无法用
外部
cwd重建活跃SessionManager,因此写入会一直冻结,直到退出并重启 Pi;没有进程内 reload 命令。unlock只影响锁,绝不解除写入冻结。 - entry/文件删除会用 tombstone 跟踪;
/session-sync resolve <id> --local|--external|--merge可显式解决冲突(--merge需要两侧归档快照和有效 基线快照;显式的 tombstone 恢复流程尚未实现;可用conflicts检查归档)。 - session header 中的
parentSession引用在共享镜像中保留。写入本地目录时, 仅当存储的父路径可证明位于配置的 Pi sessions 根目录下的逐项目 session 目录、 且属于当前项目时,才会把跨机器的绝对路径归一化到当前机器的项目目录; 镜像字节保持不变,不满足条件的引用保持原样。 - 当前项目同步在 managed-path preflight 与自动提交周围持有短生命周期的根级
Git 操作锁,避免两个项目并发同步时交错执行
git add/git commit。scope: "all"使用全局锁达到同样目的。若 Git 操作锁被其他进程占用, 文件系统同步仍会执行,但自动提交会跳过并给出提示。
License
MPL-2.0
