跳到主要内容

ws-ckpt 与会话管理 IPC 协议

English

概述

cosh-ng 通过 Unix Domain Socket 与 ws-ckpt 守护进程通信,实现工作空间快照管理。 通信使用 bincode 序列化 + 4 字节小端长度前缀 的帧格式。

架构

cosh-cli / cosh-core ws-ckpt daemon
│ │
│ Unix socket │
│ /run/ws-ckpt/ws-ckpt.sock │
│─────────────────────────────→│
│ [4B LE len][bincode req] │
│ │
│←─────────────────────────────│
│ [4B LE len][bincode resp] │

客户端实现位于 crates/cosh-platform/src/checkpoint.rsCkptClient), 类型定义位于 crates/cosh-types/src/checkpoint.rs

帧格式

每条消息由两部分组成。

┌──────────────────┬───────────────────────────────┐
│ 4 字节 LE u32 │ bincode 编码的枚举载荷 │
│ (payload 长度) │ (WsCkptRequest / Response) │
└──────────────────┴───────────────────────────────┘
  • 长度前缀使用小端序无符号 32 位整数,表示后续 bincode 载荷的字节数
  • 最大响应限制为 64 MiB,避免耗尽内存
  • 默认超时为 5000 ms,可通过 CkptClient::with_timeout() 配置

请求类型(WsCkptRequest)

bincode 按枚举变体索引序列化(第一个变体 = index 0)。变体顺序即二进制契约,不可重排。

索引变体说明
0Init { workspace }初始化工作空间
1Checkpoint { workspace, id, message, metadata, pin }创建快照
2Rollback { workspace, to }回滚到指定快照
3Delete { workspace, snapshot, force }删除快照
4List { workspace, format }列出快照
5Diff { workspace, from, to }两个快照间的差异
6Status { workspace }查询状态
7Cleanup { workspace, keep }清理旧快照
8Config获取守护进程配置
9ReloadConfig重新加载配置
10Recover { workspace }恢复工作空间
11HealthAdvisory健康检查

响应类型(WsCkptResponse)

变体对应请求关键字段
InitOk { ws_id }Init工作空间 ID
CheckpointOk { snapshot_id }Checkpoint快照 ID
RollbackOk { from, to }Rollback回滚源和目标
DeleteOk { target }Delete被删除的快照标识
Error { code, message }任意错误码 + 人类可读描述
ListOk { snapshots }ListVec<SnapshotEntry>
DiffOk { changes }DiffVec<DiffEntry>
StatusOk { report }StatusStatusReport
CleanupOk { removed }Cleanup被移除的快照 ID 列表
ConfigOk { config }ConfigConfigReport
ReloadConfigOkReloadConfig无载荷
CheckpointSkipped { reason }Checkpoint跳过原因(如无变更)
RecoverOk { workspace }Recover恢复的工作空间路径
HealthAdvisoryOk { ... }HealthAdvisory超限工作空间数、磁盘用量

错误码(WsCkptErrorCode)

索引变体说明
0WorkspaceNotFound工作空间不存在
1SnapshotNotFound快照不存在
2AlreadyInitialized工作空间已初始化
3BtrfsErrorBtrfs 操作错误
4IoErrorI/O 错误
5InvalidPath非法路径
6ConfirmationRequired需要确认(如删除 pinned 快照)
7InternalError内部错误
8SnapshotAlreadyExists快照 ID 冲突
9WriteLockConflict写锁冲突
10DiskSpaceInsufficient磁盘空间不足

客户端使用

use cosh_platform::checkpoint::CkptClient;

// 默认路径 /run/ws-ckpt/ws-ckpt.sock
let client = CkptClient::default_path();

// 或指定路径和超时
let client = CkptClient::with_timeout("/custom/path.sock", 10000);

// 健康检查
if !client.is_available() {
eprintln!("ws-ckpt daemon not running");
}

// 操作示例
let result = client.create("/home/user/project", "snap-001", Some("initial"), None, false)?;
let list = client.list(Some("/home/user/project"))?;
let restored = client.restore("/home/user/project", "snap-001")?;

关键约束

约束说明
变体顺序不可变bincode 使用索引序列化枚举,重排即破坏线格式
新增只能追加新的 Request/Response 变体只能在末尾添加
类型必须同步cosh-types 中的定义必须与 ws-ckpt-common 完全一致
超时处理客户端对 read/write 设置超时,避免守护进程无响应时阻塞
长度限制响应超过 64 MiB 视为异常,立即断开
socket 路径默认 /run/ws-ckpt/ws-ckpt.sock,可通过环境变量或 CLI 参数覆盖

测试验证

cd src/cosh-ng

# bincode 往返序列化测试
cargo test --locked -p cosh-types -- checkpoint

# 变体索引契约测试
cargo test --locked -p cosh-types test_request_bincode_variant_index

# CkptClient 单元测试(不需要运行的守护进程)
cargo test --locked -p cosh-platform -- checkpoint

cosh-core 会话管理 JSON 协议

cosh-core --session-control 是 cosh-shell 用于发现、验证和清理 provider 对话的稳定内部边界。它从标准输入处理一个 JSON 请求,向标准输出写入一个 JSON 响应后退出。该模式会加载配置和作用域会话存储,但不会初始化 provider、 扩展、技能、Hook 或认证。

调用方必须发送要管理的规范工作空间路径。Core 会再次规范化该路径,推导工作 空间作用域存储,并在构造会话文件名之前验证每个小写规范 UUID。Core 还会从 <workspace>/.copilot-shell/config.toml 加载项目配置;不会使用管理进程无关 的当前目录来解析 session.auto_persistsession.persist_dir。标准输入限制 为 1 MiB;输入超限、非法 UTF-8、畸形 JSON 或缺少必填字段时,会在初始化存储 前统一返回 invalid_request

请求动作

请求使用 action 区分。

{"action":"list","workspace_scope":"/work/project","limit":20,"cursor":null,"all_workspaces":false}
{"action":"inspect","workspace_scope":"/work/project","session_id":"2d711642-b726-4b04-8d2a-8a0470f4ed24"}
{"action":"validate","workspace_scope":"/work/project","session_id":"2d711642-b726-4b04-8d2a-8a0470f4ed24"}
{"action":"prepare_clear_all","workspace_scope":"/work/project","protected_session_ids":[],"limit":4096,"cursor":null}
{"action":"clear","workspace_scope":"/work/project","session_ids":["2d711642-b726-4b04-8d2a-8a0470f4ed24"],"protected_session_ids":[]}
动作契约
list返回按更新时间倒序的摘要。limit 默认为 20,并限制在 1 至 100;使用 opaque next_cursor 读取下一页。当 all_workspacestrue 时,Shell 客户端请求更大的初始页大小 100 以减少首页不完整输出,但仍受核心侧 1 至 100 的相同限制。Core 会枚举存储根下所有工作空间哈希目录,返回所有工作空间的会话,并将非当前工作空间的会话标记为 scope_mismatchall_workspaces 默认值为 false,以保持向后兼容。
inspect即使健康状态不允许恢复,也返回摘要。
validate完整加载信封,只有可恢复会话才成功。
prepare_clear_all不加载或传输摘要,按 UUID 字典序分页返回可清理和受保护 ID。limit 限制在 1 至 4096;通过 next_cursor 继续。只有完整计划可放入单个 4096-ID 分页时才允许省略 limit,避免旧客户端静默接受不完整计划。
clear独立删除每个请求 ID,并返回逐项跳过错误。每个请求最多接受 128 个 ID。

摘要包括 session_idworkspace_scope、创建和更新时间、模型、消息数、首条 提示、schema 版本,以及以下健康状态之一,即 readycorruptincompatiblescope_mismatch。 首条提示预览会在序列化前规范为单行,并限制为 160 个 Unicode 字符。列表先 使用有界文件系统元数据对 UUID 文件排序,再只读取当前请求分页,不会在每次 请求中反序列化全部历史。持久化、列表、验证和恢复统一将单个会话文件限制为 32 MiB;超限记录无需分配其内容即可报告为 corrupt。若单个条目在读取时 消失或无法读取,列表会继续扫描,直至填满当前分页或耗尽候选,因此首个被过滤 条目不会隐藏同页后续健康会话。cursor 编码上一页末尾的 文件系统倒序排序键,因此分页之间删除该条目不会令分页重回第一页。

响应信封

成功数据使用对应动作标记。

{
"ok": true,
"data": {
"action": "list",
"sessions": [],
"next_cursor": null
}
}

请求级错误以状态码 1 退出。

{
"ok": false,
"error": {
"code": "not_found",
"message": "session not found: 2d711642-b726-4b04-8d2a-8a0470f4ed24",
"recoverable": true,
"hint": "Refresh the session list and choose an existing entry."
}
}

稳定错误码包括 invalid_idinvalid_cursorinvalid_requestnot_foundiocorruptincompatible_versionscope_mismatchconflictactive_session,交互式调用方可处理这些错误。若 clear 仅有逐项失败, 响应仍为 ok: true;失败条目及其类型化错误位于 data.skipped

protected_session_ids 是交互式删除的强制纵深保护。cosh-shell 会同时发送已 选择和已激活的 provider ID;即使这些 ID 也出现在 session_ids 中,Core 仍拒绝删除。prepare_clear_allclear 都必须携带该字段;显式空数组表示 调用方确认当前没有受保护身份,省略字段则会在执行任何删除前拒绝整个请求。 shell 会抽干有界的 prepare_clear_all ID 分页,在确认前展示精确计划,随后 通过每批 128 条的 clear 提交这些 ID;该流程不会抽干所有摘要分页。Core 会拒绝超限的直接批次,并按 UTF-8 字节限制逐项 ID 与错误文本。Core 还会对 完整序列化 envelope 执行 1 MiB 硬预算检查,因此多字节输入无法绕过客户端响应 上限。 每条 summary 在进入分页累计前,还会分别将不可信 model 元数据限制为 256 个 UTF-8 字节、workspace 元数据限制为 4096 个 UTF-8 字节。因此体积较大但其余 字段合法的会话文件,不会让 listinspectvalidate 分配无界响应或 退化为 invalid_request

若后续 clear 批次失败,shell 会保留已确认的 deletedskipped 结果。 失败批次作为 unknown_session_ids 返回,尚未发送的 ID 作为 unattempted_session_ids 返回;UI 不得将其折叠成隐藏先前删除结果的请求级错误。

shell 为每个一次性管理操作设置统一的十秒 deadline,覆盖进程启动、请求 pipe 写入和响应收集。请求写入使用非阻塞 pipe,并由 deadline-aware 生命周期循环 重试,因此无论 leader 还是脱离进程组的后代持有 stdin,都不能让批量 clear writer 永久阻塞。发生超时或传输失败时,shell 会关闭请求 pipe、终止进程组、 升级为强制终止、等待 leader,并 join 输出 worker。 输出 worker 使用可取消的轮询;完成 leader 和进程组清理后,它们会排空已经可读 的字节并停止,即使有脱离原进程组的后代仍持有继承的输出描述符,也不会延长 管理请求的超时边界。普通 poll 超时只会继续轮询,绝不会进入阻塞 read,因此 静默 leader 不会在生命周期设置 stop 标志前卡住 reader。 客户端还将 JSON 响应限制为 1 MiB、stderr 诊断限制为 256 KiB;任一输出超限 都会关闭 pipe,并终止和回收 session-control 进程组。

持久化兼容性

schema v1 信封包含不可变 provider 会话 UUID、规范工作空间、时间戳、模型、 乐观并发代数和模型可见消息。写入使用同目录临时文件、文件与目录同步、原子 重命名和短期 advisory 锁。进程退出时内核会释放锁,因此未加锁的锁文件可直接 复用,不会被视为冲突。规范 workspace 路径必须是有效 UTF-8;Core 会在派生 scope 或存储哈希前对无效路径返回 invalid_request。乐观并发代数必须单调 递增,已存储的 u64::MAX 代数会无损拒绝,不能覆盖现有历史。在 Unix 上, scoped 目录权限为 0700,会话、临时和锁文件权限为 0600。旧版原始消息 数组以内存中的代数零加载。Core 在构造 store 时先一次性解析存储根路径中的 symlink,随后在该规范根之下逐级且不跟随 symlink 地安全打开或创建每个 scoped 路径组件,并固定 workspace hash 目录;因此经 symlink 管理的家目录 或 dotfile 布局可正常工作,而根之下后续的 symlink 替换仍会被拒绝。scoped 列举、会话及锁打开、临时 文件创建、原子 rename 和删除全部相对该描述符执行;会话和锁打开使用 NOFOLLOW。因此把一个 workspace hash 目录替换为指向另一 workspace 的 symlink,不能重定向 loadpersistlistclear。清除会话时会同步 删除配对的锁文件;崩溃写入者残留的过期临时文件会在目录下次以写模式 打开时被清扫。

使用规范 UUID 显式查找时,Core 只检查能够证明归请求工作空间所有的旧版扁平目录。使用新版 默认根目录时,这只包括请求工作空间原有的相对 sessions/ 目录。自定义根只有 在配置值本身相对工作空间、不含 ..、已经作为目录存在且经过 symlink 解析后 仍位于规范 workspace 内时,才参与 legacy 查找。Core 会逐级且不跟随 symlink 地打开规范 workspace,然后用已打开的描述符固定每个合格 legacy 目录。legacy 列举、会话打开及删除始终相对该描述符执行,会话打开使用 NOFOLLOW。因此并发 rename 加 symlink 替换不能把 load 或 clear 重定向到固定的 workspace-owned 目录之外。绝对路径、~/ 和父目录逃逸根不参与 legacy 查找;它们可以指定 scoped 存储,但 scoped 访问会拒绝规范存储根之下任意路径组件中的 symlink。Core 不会根据 目录前缀包含关系推断 legacy 所有权,也不会检查进程 cwd 或作用域不明确的共享 扁平根目录。归属已确立的 workspace-owned legacy 会话会与 scoped 信封一同 出现在 list 摘要中,因此 picker、prepare_clear_all 与显式 clear 观察到 的是同一批会话;而在已确立目录之外的来源不明文件不会被 inspectvalidate 或加载认领、列出或改写。显式 clear 也可删除 损坏的旧版文件,同时继续遵守受保护 ID。迁移会锁定旧源,把 schema v1 信封 原子写入请求的工作空间作用域,然后删除旧文件。旧文件清理失败会作为类型化 持久化错误返回,并由后续持久化重试。同时存在两份副本时,clear 会先删除 legacy 副本,因此 legacy 权限错误不会删除较新的 scoped 历史或重新暴露旧内容。

JSONL headless 协议和本管理协议共用 SessionStore::load;交互式选择无法绕过 直接 cosh-core --resume 使用的验证逻辑。

JSONL result 会明确标记会话加载失败。

{"type":"result","is_error":true,"errors":["session recovery failed [not_found]: session not found"],"session_error_code":"not_found","session_error_phase":"load","session_id":"..."}

cosh-shell 会区分用户选择的恢复与 active provider 会话的自动续接。Core 的 会话错误会单独携带 session_error_codesession_error_phaseload 阶段的 not_foundcorruptincompatible_versionscope_mismatch, 以及任意类型化的 persist 失败,只会释放本次匹配的尝试身份。owned selected attempt 会进入 failed,同时保留结构化 code、provider 原始 message 和按阶段 生成的恢复 hint;原有 active UUID 保持可用。普通 provider 错误文本不能伪装 成会话错误。无论 selected 还是 active 恢复,提交前 provider 返回的 ID 都必须 与尝试 ID 一致,且无关的 selected ID 会继续保持选择。单轮 disable provider resume hint 会省略 --resume,但不会消费用户尚未执行的选择。

JSONL system/init 消息包含 session_resumable。值为 false 表示禁用了 session.auto_persist。消费方不得捕获输出的 UUID,而且只有本次调用确实通过 --resume 携带某个身份时,才能使该身份失效。一次 fresh fallback 因此不能 清除无关的 selected ID 或旧 active ID。即使本轮随后失败、被取消或异常退出, 该规则仍适用。为兼容尚未实现该扩展的 provider,此字段为可选;字段缺失时 保留既有会话 ID 捕获行为。

active ID、工作空间和调用 generation 由同一个状态锁持有。每次实际启动都会 领取 generation token;成功、失败、取消、不可恢复清理和身份不匹配转换仅在 该 token 仍属于最新尝试时提交。因此取消后延迟退出的旧 worker 无法清除或覆盖 新一轮提交,即使两轮恢复的是同一个 selected ID。若结构化会话结果之后又发生 传输错误,shell 会先完成结构化终态和会话状态收尾,再交付传输错误。替换或拒绝 selection 也会原子推进 generation;未携带 --resume 的 fresh turn 会把被抢占的 restoring owner 退回 selected。取消路径会先应用已解析的结构化会话错误, 再保持对外 AgentCancelled 语义。破坏性的会话管理会在整个 clear 操作期间 持有同一状态 lease,selection 也会在 validate 到提交的完整区间持有它。因此 clear 与激活按线性顺序执行,不再依赖可能过期的 protected ID 快照。

测试验证

cd src/cosh-ng
cargo test --package cosh-core
cargo test --package cosh-shell --test protocol