# 把项目和它的 agent 对话一起送到另一台机器(设计稿) 状态:已实现(2026-10-31,验收记录见 `docs/acceptance.md` 第 171 条)。第 9 节五条都按推荐做(用户 2026-10-31 说"改吧")。 实现与草稿不同的地方(原因写在各条): - **仓库的文件不按原样复制**:提交按 `push` 的方式走 git bundle(对方得到当前分支的完整历史,按它自己的设置检出),只把和提交不同的文件(git 列出的改动和新文件)按文件送。原因:Windows 上开着 `core.autocrlf` 时工作区是 CRLF,原样复制到 Linux,git 会把每个文件都看成改过。 - **一次一个包**,不分批:包的上限是 `link-agents-limit`(默认 1G,压缩后),超过就报错,提示用 `-s` 少送几段对话。失败后再运行一次会整包重送(对方什么也没写)。分批续传留到以后,实测需要时再做。 - **非仓库的文件夹**:跳过 `node_modules`、`target`、`.venv`、`venv`、`__pycache__`、`.git`、`.DS_Store`;没有做 `--exclude` 和 2 GiB 时停下列目录:一次最多问 900 KiB 的清单,文件太多时报错,提示用 git。 - **Claude Code 的对话只认在项目根目录启动的**(`projects/<由根目录算出的名字>`);在子目录启动的会话不送。 - **对方删掉/放回的文件**:本机上次送过、之后没动过,而这次对方不再列出的文件,按对方处理:放回提交里的样子(已跟踪的)或删掉(新文件)。这样对方撤销改动、删文件,这边也跟着变,而这边自己改过的不受影响。 - 没有实测从 WSL 送回 Windows:WSL(NAT 模式)连不到 Windows 只听 127.0.0.1 的 web,要改防火墙才行。送回由端到端测试覆盖:同一台机器上两个服务器,A 送给 B,B 在副本里改一个文件再送回,落回 A 原来的文件夹,A 上没有新建 `from/…`。 用户已定: - 2026-10-30:Claude Code、Codex 的对话只存在本机,要能带到另一台机器接着聊;手动按需送,不做后台同步;以后再送只送新增部分。 - 2026-10-31(改了结构):一次送两样东西,指定的 agent 对话和整个项目文件夹。对方把它们放在每台来源机器专用的路径下;文件夹和它的对话关联起来,记下来。 这一结构代替 10-30 稿的"按家目录相对路径对应":对方路径由 keepane 定,不用猜。 ## 1. 一次送的单位:一个项目 一个项目 = 一个文件夹 + 在这个文件夹里工作过的 agent 对话。 ``` keepane send-agents [<对方地址>] [目录] [-s 对话id]… [-n] keepane send-agents -l ``` - 不写对方地址时,送到这个文件夹上次去的那台(只去过一台时)。`-l` 列出送出去和收进来的文件夹。 - `目录` 默认当前目录。 - 对话默认送这个文件夹的全部对话(Claude Code、Codex、pi);`-s` 只送 id 以给定字符开头的几段。按目录找对话的是 `agentsync::chats_of`:Claude Code 在项目根目录启动的会话(连同旁边的 subagents、tool-results),第一行写着这个目录的 Codex 会话(所有日期),pi 的会话。 - `-n`:只列出要送什么、放到哪里、多大,不送。 - 结尾打印对方的路径和接着聊的命令(两行:`go on there: cd <对方路径>`,下一行 `claude --resume `)。对方另存为新对话的,由对方在回复里写出新 id:`new chat: claude --resume <新 id>`。 ## 2. 对方放在哪里 ``` /from/<来源机器名>/<项目名>/ 整个文件夹(默认 ~/keepane/from/…) ``` - `link-clone-dir` 是 `keepane clone` 已有的选项,默认 `~/keepane`。 - `<来源机器名>` 是来源机器的主机名,随请求带过来(和 `push` 的 `from` 一样),整理成合法的目录名。不同机器来的同名项目不会撞在一起。link 表里只有公钥和地址,没有名字;关联表按公钥记,来源机器改了主机名也照样对得上(目录名用第一次送时的)。 - `<项目名>` 是本机文件夹的名字。同一台机器的两个不同文件夹同名时,第二个加 `-2`。 - 对话写进对方 agent 自己的目录,`cwd` 改为上面的路径:Claude Code 的 `~/.claude/projects/<由新路径算出的目录名>/`,Codex 的 `~/.codex/sessions/…`。只有放在这里,`claude --resume`、`codex resume` 才找得到;放在 keepane 的路径下它们看不到。 ## 3. 关联表 两边各记一张表,放在 keepane 的数据目录(`agents-links.json`),不放进项目文件夹: | 字段 | 例子 | |---|---| | 本机文件夹 | `C:\Users\stebe\Documents\dfine\wmux` | | 对方机器 | 公钥,和第一次送时的主机名 `desktop` | | 对方文件夹 | `/Users/tom/keepane/from/laptop/wmux` | | 对话分叉 | 送来的 id → 这边另存的新 id(以后同一段对话接着送到新 id 那份) | | 上次送时每个文件的 SHA-256 | 用来判断哪边改过(第 5 节) | | 上次送的时间 | `-l` 列出 | 对话已有多少不记在表里:每次问对方(第 6 节),对方按自己磁盘上的文件回答。 用途: - 再送同一个项目:送到同一个位置,只送变化的文件和对话新增的部分。 - **送回来**:在对方机器上接着干完,从那边 `send-agents` 回这台。对方查表发现这个文件夹本来就是从这台来的,就送回原来的路径(`C:\…\wmux`),不在这台再建 `from/desktop/wmux`。对话的 `cwd` 改回原路径。 - `keepane send-agents -l`:列出这台机器送出去和收进来的项目、对话、时间。 ## 4. 文件夹里送什么 - git 仓库(当前分支要有提交,不能是 detached): - 提交:当前分支对方没有的提交,打成 git bundle(和 `push` 一样)。第一次在对方 `git clone`,以后 fetch 到 `<来源机器>/<分支>`,能快进就快进。对方得到当前分支的完整历史,按它自己的 git 设置检出(换行符跟着对方)。 - 文件:`git status` 列出的改动过和新加的文件(遵守 `.gitignore`,删除的不送)。本仓库实测:整个文件夹 106,860 MB(大多是 `target\`),按 git 认的只有 21.8 MB、578 个文件,平时改动的只有几个。 - 只有对方分支停在这边的提交上、并且检出的是这个分支时,文件才放进去;否则全部放在旁边(`*.from-<来源机器>`),回复里说明。 - 不是 git 仓库:送整个文件夹,跳过 `node_modules`、`target`、`.venv`、`venv`、`__pycache__`、`.git`、`.DS_Store`。一次问对方的清单最多 900 KiB,文件太多时报错,提示用 git。 ## 5. 两边都改过同一个文件 送的时候,对方按关联表里"上次送时的 SHA-256"判断: - 对方的文件和上次一样(对方没改):用新的替换。 - 对方的文件改过、这边没改:不动对方的。 - 两边都改过:不覆盖,把新的存成 `文件名.from-<来源机器名>`,结尾列出来,由你自己合并。 - 没有上次记录的文件:仓库里按对方 `git status` 判断(对方自己改过的才算改过);不是仓库时,已有又不同就算改过。 - 上次送过、对方之后没动过、这次这边不再列出的文件(这边撤销了改动,或删了):对方也放回提交里的样子,或删掉。 - 有提交要来时,对方先把"上次送来、之后没动过"的文件放回提交里的样子,分支才能快进;快进后再按上面的规则写回。 - 对话同理:对方续写过的对话(打开过一次也算,见第 7 节)另存一份新 id,不合并;回复里写出新 id。 ## 6. 传输与权限 - 复用 `push` 的分块上传:每块 512 KiB、每块签名;先问对方已有什么(`/link/agents/look`:每个文件的 SHA-256,每段对话对方已有的长度和那一段的 SHA-256),只送缺的;gzip 压缩(实测对话压到 24%)。一次一个包,不超过 `link-agents-limit`(默认 1G,压缩后);中途失败时对方什么也没写,再运行一次整包重送。 - 对方要授予 `keepane link allow <本机地址> --agents`,默认关,只能在 pane 之外授予。对话里有读过的文件内容和命令输出,可能有密钥。 **警告:** link 的请求有签名,但不加密([link.md](link.md) 第 32 行)。请在 Tailscale 上用,或只在可信的网络里用。 ## 7. 实测(2026-10-31,WSL 当作另一台机器) WSL 里装了 Claude Code 2.1.314,不登录,用单独的配置目录(`CLAUDE_CONFIG_DIR=~/cc-test`)。把 Windows 上一段 44 KB、20 行的对话复制过去,只把 11 行的 `cwd` 从 `C:\Users\stebe\…\smoke1` 改成 `/home/dfine/keepane/from/desktop/smoke1`,放进 `projects/-home-dfine-keepane-from-desktop-smoke1/`(目录名按 `claude_folder` 的规则算)。 | 试的 | 结果 | |---|---| | 新文件夹里 `claude -p --resume ` | 读进对话(这一轮输入 4002 个 token),停在"Not logged in" | | `/tmp` 里 `claude -p --resume ` | 同样读进:按 id 接着聊不看当前目录,在所有项目里找 | | 编一个 id | `No conversation found with session ID` | | 新文件夹里 `claude -p --continue` | 读进对话(4002):按目录找,目录名对了 | | 空文件夹里 `claude -p --continue` | 没有可接的(0) | | 不带 id 的 `claude --resume`(列表) | 没测到:没登录时先出首次设置和登录界面 | 结论: - 复制 + 只改 `cwd` + 按规则算目录名,Windows 的对话在 Linux 上能接着聊。 - **打开一次就算续写**:每次 resume(连没登录失败的那几次)都往对话末尾追加行。实测 20 行变成 159 行(`user`、`assistant`、`api-request` 等)。所以对方只要打开过,再送同一段对话时它就不是"开头一致"了,按第 5 节另存一份新 id。送回来同理。 ## 8. 风险 - **agent 改格式**:transcript 不是公开接口。Claude Code 2.1.3xx 只在 `user`、`assistant`、`attachment`、`system` 行写 `cwd`(2026-10-29 实测);以后可能变。对策:只改 `"cwd":"…"` 这一种写法、只改在项目目录下的路径,其他字节原样;对方写入前检查改写后的每一行都还是 JSON,不是就不写,回复里说明。 - **resume 找不到**:Claude Code 按目录名找对话,目录名由路径算出。用 `claude_folder` 同一个函数算,测试覆盖 Windows 和 Unix 路径。 - **两边版本不同**:对方的 agent 比本机旧时可能读不懂新字段,没法提前检查。 ## 9. 要用户定的事 1. `.git` 带不带? - 带上(推荐):对方得到完整的仓库,能提交、能 `push` 回来。本仓库第一次 250 MB,以后只送变化的部分。 - 不带:只送文件(本仓库 21.8 MB),对方不能提交。 2. 对话默认送这个文件夹的全部,`-s` 指定几段?(推荐:是。) 3. 两边都改过的文件存成 `文件名.from-<机器名>`,不覆盖?(推荐:是。) 4. 命令名 `send-agents`、权限 `--agents`、加 gzip 压缩依赖(`flate2`)?(推荐:都是。) 5. 已有的 `keepane clone`/`push`(只传已提交的 git 历史)怎么办?(推荐:保留不动。它们只传提交,适合只同步代码的时候。)