Wework 会话与配置云同步
Wework 的 Core DSH 插件 @wegent/dsh-transcript-sync 同步原生 Codex
rollout、任务工作区、旧版本同结构的回合摘要和可移植偏好。双机恢复不再把会话压缩成
用户/助手文本,也不再通过 thread/inject_items 重建历史。
可用性与显式开启
跨设备同步属于实验性功能,默认关闭。用户需要先在“通用”设置中开启“实验性功能”, 再到“云端连接”中手动开启“跨设备同步会话和配置”。关闭时 Wework 仍会保留本地任务、 会话和待上传定位信息,但不会上传、下载或同步偏好配置。
升级到采用显式授权策略的版本时,已有设备即使曾经开启同步,也会一次性迁移为关闭, 并在任何云端同步请求发出前持久化该状态。用户迁移后重新手动开启会记录新的授权版本, 后续启动保持开启,不会再次重置。
存储边界
Backend 使用三张表:
| 表 | 用途 |
|---|---|
wework_transcripts | transcript 身份、分支关系、全局 sequence、状态和单写租约 |
wework_transcript_archives | 不可变原生 segment 的 sequence、对象 key、SHA-256、大小和格式 |
wework_transcript_turns | 每个已完成回合的结构化摘要,沿用旧版本的数据契约 |
正文先在 Executor 中使用 AES-256-GCM 加密,再通过已认证的 Backend API 上传。Backend
负责把密文写入私有 wework-transcripts 对象存储;桌面客户端不会获得对象存储地址、
凭据或预签名 URL。MySQL 的 wework_transcript_turns.payload 会按回合保存原有协议中的
用户消息、助手最终文本、reasoning 摘要、完成状态和任务 ID,但不保存完整工具协议、
usage、rollout JSONL 或工作区文件。摘要是一回合一行 JSON,不会把整个 transcript 持续
追加进单个字段;完整数据容量和精确恢复均由分段 tgz 对象承担。
同一 sequence 的 archive 索引、turn 摘要和 transcript head 在一个 MySQL 事务中提交。 仅当对象元数据与摘要都完全一致时,重复提交才视为幂等;任一侧缺失或不一致都会报冲突, 不会形成“数据库显示已同步但摘要或 tgz 缺一份”的半状态。
Backend 基于 WEWORK_TRANSCRIPT_ENCRYPTION_SECRET 和用户 ID 派生稳定的每用户密钥,
通过已认证的 GET /{id}/encryption-key 接口短暂下发。同一用户的所有 transcript 使用
同一密钥,不同用户的密钥不同。密钥不写入同步状态、outbox 或对象内容。
每个 segment 的 nonce 由密钥、AAD 和明文摘要确定性派生;AAD 绑定 transcript ID、
sequence 和格式。相同内容重试会得到相同密文,仍可通过 SHA-256 对账;不同内容不会
复用 nonce。
每个云端 sequence 恰好对应一个对象:
- 第 1 个 sequence、每第 10 个 sequence、冲突分支的第 1 个 sequence,以及跨设备恢复
后首次继续对话的 sequence,是完整加密快照
codex-snapshot.v1.tgz.aes256gcm。恢复 会重写本机 thread ID 和工作区路径,因此必须用新快照建立新的可移植字节基线。 - 其他 sequence 是加密增量
codex-delta.v1.tgz.aes256gcm。 - 每个 segment 同时携带工作区覆盖层,避免只恢复会话却丢失最近文件。
- 工作区打包会排除
.git、node_modules、构建产物和常见缓存目录,避免重复上传 仓库对象库或无关的大体积派生文件。 - outbox 只保存任务、session、turn、sequence 和分支路由,不复制正文。
- 原生对象的快照清理不会删除
wework_transcript_turns中对应的结构化摘要。 - 新完整快照提交后,服务端保留“上一个完整快照 + 其后的全部 segment”,删除更旧的 OSS 对象和元数据。快照间隔为 10 时,每个持续活跃的 transcript 通常保留 11 个、 峰值不超过约 20 个对象,不会随对话轮数无限增长。
- 单个原生 rollout segment 的明文上限为 256 MiB。生成增量时 Executor 从已同步的 rollout offset 开始读取后缀,不会先把整个持续增长的 rollout 文件读入内存;因此 总 rollout 超过旧版 128 MiB 阈值时,只要本次待同步 segment 未超过上限,仍可继续 上传。
两台电脑可以同时保持 Wework 打开。Wework 为每个桌面安装持久化一个稳定设备 ID。
writer_client_id 在租约有效时表示当前写入设备,租约释放后保留为最近一次写入设备;
租约是否有效只由 writer_lease_expires_at 判断。客户端每 5 秒只拉取未归档会话:最近
写入设备只上传该 transcript,不下载恢复;其他设备在本机缺少该 transcript 时自动恢复。
因此全新设备会恢复其他设备上所有未归档任务,而同步来源设备不会反复下载自己生成的
工作区。归档任务不会自动恢复。写入时才申请短租约,上传完成立即释放;没有新 turn 的
电脑不会长期占锁。正在运行或已经绑定的本地任务不会被云端恢复覆盖。两台电脑若同时完成
同一 sequence,先提交者进入主线,后提交者按确定性 ID 建立分支,两边内容都保留。
设备创建冲突分支后,原本的本地任务会转为该 fork;同步器只恢复因此缺失的父主线,
让主线与分支成为两个独立本地任务。fork 本身仍由来源设备只上传、不下载。
已有 wework_transcript_turns 表继续保留,并沿用旧版本的摘要字段。该表不参与双机
恢复,也不能替代原生 tgz。
状态转换
冲突时不合并两个 rollout 文件。云端主线保持不变,本机冲突链切换到由
clientId + transcriptId + turnId 确定的分支,并以完整快照作为分支 sequence 1。
双设备验证
GitHub CI 的 transcript-sync desktop checkpoint 启动真实 Electron、Executor 和
Codex,并在同一个测试中顺序模拟设备 A、设备 B。两个设备使用不同的 HOME、
WEGENT_EXECUTOR_HOME、WEGENT_CODEX_HOME、CODEX_SQLITE_HOME、
Electron user data、应用配置目录和 device identity;切换到设备 B 时不会删除或复用
设备 A 的状态。
该 checkpoint 必须验证设备 A 上传加密快照和增量,设备 B 从空状态恢复工作区与完整 历史,并继续对话、上传下一个 sequence;每个 sequence 还必须产生对应结构化摘要。 共享同一本地状态的重启测试不能替代该验证。 两台物理电脑的测试保留为发布验收,用于覆盖真实网络、休眠和操作系统差异,但不作为 GitHub CI 的执行前提。
恢复顺序
- 从
wework_transcript_archives选择不晚于当前 head 的最近完整快照。 - 下载快照及后续连续增量并逐个校验密文 SHA-256、格式和 sequence。
- 使用当前用户密钥验证 GCM tag 并解密,再校验 identity,在 staging 目录恢复工作区,并拼接、解析 rollout JSONL。
- 重写目标设备的工作区路径;thread ID 冲突时生成新 ID。
- 在事务中恢复 Codex
threads和thread_dynamic_tools状态。 - 全部成功后绑定本地任务;失败时删除 staging、rollout 和工作区,不留下半恢复状态。
API
认证前缀为 /api/wework-transcripts:
| 方法与路径 | 用途 |
|---|---|
GET / | 列出 transcript 和原生 segment 元数据 |
GET /{id} | 读取一个 transcript |
GET /{id}/turns | 分页读取结构化回合摘要 |
GET /{id}/encryption-key | 获取当前用户的 transcript 加解密密钥 |
POST /{id}/lease | 创建 transcript 或获取写租约 |
PUT /{id}/lease/{token} | 续租 |
POST /{id}/lease/release | 释放租约 |
POST /{id}/segments | 接收密文并提交对象索引、回合摘要和 head |
POST /{id}/archive | 标记 transcript 为 archived |
GET /{id}/archives/{archiveId}/download | 通过 Backend 流式下载密文 |
对象 key 使用 transcript ID 的 SHA-256 摘要,不暴露原始 transcript 标识。
部署配置
对象存储复用 ATTACHMENT_S3_* 连接配置:
| 环境变量 | 默认值 | 说明 |
|---|---|---|
WEWORK_TRANSCRIPT_S3_BUCKET | wework-transcripts | 私有原生会话 segment bucket |
WEWORK_TRANSCRIPT_ENCRYPTION_SECRET | 空 | 派生每用户密钥的稳定高熵根密钥 |
本方案直接复用已有的三张 transcript 表,不新增 Alembic migration,也不要求已有部署 调整数据库结构。对象存储不可用时,segment 不会提交到 MySQL,outbox 继续保留定位 信息,本地任务仍可离线执行。
transcript bucket 与附件 bucket 是两个独立桶,ATTACHMENT_S3_* 对应的账号必须对该桶
持有 ListBucket、GetObject、PutObject、DeleteObject 权限。只授权了附件桶的账号
会在首次上传时以 503 失败;此时 Backend 日志记录 S3 错误码,设备侧错误信息末尾括号内
也会给出同一错误码(如 AccessDenied)。
未配置独立根密钥时兼容使用 SECRET_KEY。生产环境应配置独立值,并在相关 tgz 保留期间
保持不变。
故障语义与排查
设置页会按 Conversation upload、Conversation download 和
Preference synchronization 标注失败阶段,避免把上传租约、归档下载和偏好同步错误
合并成无法定位的通用异常。Electron 请求失败时会保留底层网络原因,但会清除错误文本中的
URL 凭据。
如果数据库中的 archive 索引仍存在、对应对象却已从对象存储丢失,下载接口返回
404 archive_not_found。客户端会记录 transcript ID、archive ID 和 sequence,跳过该条
无法完整恢复的云端 transcript,并继续同步其他会话;普通对象存储故障仍会使本轮下载失败,
不会被误判为单个归档缺失。该处理只隔离损坏数据,不会伪造或重建丢失对象;运维仍需根据
Backend 日志和对象存储审计记录定位对象被删除的原因。