ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

claude-mem SyncHub 内部元数据契约:面向 Pro 控制面的无负载设备与同步状态读取协议

claude-mem SyncHub 内部元数据契约:面向 Pro 控制面的无负载设备与同步状态读取协议 claude-mem SyncHub 内部元数据契约面向 Pro 控制面的无负载设备与同步状态读取协议【免费下载链接】claude-memPersistent Context Across Sessions for Every Agent – Captures everything your agent does during sessions, compresses it with AI, and injects relevant context back into future sessions. Works with Claude Code, OpenClaw, Codex, Gemini, Hermes, Copilot, OpenCode More项目地址: https://gitcode.com/GitHub_Trending/cl/claude-mem导读SyncHub 是 claude-mem 双通道同步方案中负责承接设备身份、last-seen 状态、同步游标与权威 Turbopuffer 投影检查点的多用户 Durable Object 层。本文以 workers/sync-hub/METADATA-CONTRACT.md 为骨架完整解析这套面向服务端内部控制面Pro的轻量契约包括POST /internal/v1/sync/metadata无负载元数据读取、POST /internal/v1/sync/device-name设备重命名两条内部路由的报文格式与校验规则以及单用户 64 台设备的原子准入上限与非准入路径的内存模型。读者读完后将掌握该契约为什么刻意不携带任何内容数据、sync_health与各游标/滞后的精确定义、规范十进制字符串为何绝不能经 JSnumber传递以及 push/pull/WebSocket/status 等不同路径在设备上限上的行为差异。契约定位控制面状态而非内容文档开篇给出这条契约的根本意图SyncHub 是设备身份、last-seen 状态、同步游标与权威 Turbopuffer 投影检查点的唯一所有者。Pro 端服务端订阅控制面需要了解某用户的同步进展时直接读这套 payload-free无负载状态而不是去查内容表或pro_sync_state。为什么要无负载从 index.ts 头部注释 可以看到 SyncHub 的整体架构分工每个用户的 SyncHub Durable ObjectDO执行零出站 I/O只保存规范的顺序操作日志与检查点无状态 Worker 承担认证与 Turbopuffer 投影调用内容的投影结果由另一条projection 通道带负载推给 Pro而元数据通道只回答同步到哪了、健康吗、有哪些设备。两条通道负载不同契约也刻意分离。文档强调元数据响应绝不包含内容、内容计数、本地 outbox 深度或迁移/回填遥测——因为这些属于投影通道的职责混入控制面契约只会让读者方对数据源产生歧义。getMetadata()返回结构中的确只含epoch/head_seq/projected_seq/projection_lag_ops/sync_health/devices与文档声明完全一致见 SyncHub.getMetadata。两条路由的公共要求两条内部路由均要求Authorization: Bearer CMEM_INTERNAL_PROJECTOR_SECRET Content-Type: application/json其强校验规则可总结为下表违规情形HTTP 状态说明缺失或错误的凭据401见 hasInternalCredential与普通用户 token 体系完全隔离请求体含未知字段 / 键不精确400exactKeys要求键集合精确相等多一个字段也拒绝非POST方法405见 index.ts 路由分派未知设备的 rename404rename 永不创建幻影设备exactKeys()的实现index.ts把请求体视为精确版本化的对象字段既不能缺也不能多。测试中直接用include_content_counts: true这样的合理扩展触发400见 sync-hub.test.ts证明未知字段返回 400是硬性契约而非宽松解析。Read metadata读取 Hub 控制面状态请求与响应路由POST /internal/v1/sync/metadata请求体仅两个字段精确键{ protocol_version: 1, user_id: canonical-user-id }成功响应体{ protocol_version: 1, user_id: canonical-user-id, epoch: 1784531270123, head_seq: 42, projected_seq: 40, projection_lag_ops: 2, sync_health: projector_lagging, devices: [ { device_id: a-stable-device-id, name: Alexs Laptop, last_seen_at: 2026-07-20T12:00:00.000Z, last_seen_epoch_ms: 1784548800000, last_ack_seq: 39, cursor_lag_ops: 3, connection_state: disconnected } ] }user_id在服务端被 trim 后作为路由键直接定位到该用户的 DO 实例env.SYNC_HUB.getByName(userId)因此它必须是跨设备一致、服务端认可的 canonical 用户标识即认证时由 token 绑定出的规范 id详见 DEPLOY.md §1.2 的绑定说明。每个字段的语义epochHub 实例的随机代数。在 initializePristineState 中首次引导时由 newEpoch() 生成 64 位随机值reset后重新生成。它用于隔离不同代际的日志——任何持旧游标的设备看到新 epoch 都必须重新引导。head_seq当前 Hub 顺序日志的头部游标已接受操作的最新序号。projected_seq权威投影检查点——Turbopuffer 侧已成功应用的序号。它由投影租约通道acquire → page → advance CAS在 advanceProjectionCheckpoint 中单调推进。projection_lag_opshead_seq与projected_seq的差即尚未投影完成的操作数。sync_health当且仅当projected_seq head_seq时为healthy否则恒为projector_lagging。这正是 getMetadata 中head projected ? healthy : projector_lagging的一行判定。devices[]该用户已注册设备的元数据数组至多 64 个按最近 last_seen 优先其次按 device_id 字典序排序见 getMetadata 的 ORDER BY 子句。设备对象内部字段字段类型说明device_idstring稳定设备 id经 trim1–128 字符namestring | null展示名可为空见下文命名策略last_seen_atstring | nullISO 8601 UTC 时间由毫秒时间戳转换而来测试断言其以Z结尾last_seen_epoch_msstring | null原始毫秒 epoch十进制字符串last_ack_seqstring该设备最近一次确认的拉取游标cursor_lag_opsstringhead_seq与该设备last_ack_seq的差connection_stateconnected \| disconnected该设备当前是否存在被接受的咨询性 WebSocket注意last_seen_at是展示用的 ISO 字符串last_seen_epoch_ms是数值等价物——两者同时暴露因为数据链路上的数字永远是字符串展示层才转成 ISO。测试逐一断言了二者的形态见 sync-hub.test.ts。为什么这些值必须是无符号十进制字符串文档给出的铁律值得单独强调epoch、每一个序号/游标、每一个滞后量都是 canonical 无符号十进制字符串永远不允许经 JSnumber传递。其理由在源码中层层加固JSnumber只能精确表示2^53以内的整数而日志序号会跨多个设备长期增长epoch 更是完整的 64 位随机值。精度丢失会破坏游标比较与租约语义。canonical-content.ts 中的 assertCanonicalDecimal 用正则^(?:0|[1-9][0-9]*)$校验无前导零的无符号十进制并上限到uint64compareCanonicalDecimals 先比长度再字典序本质上就是逐位十进制大数比较。自增操作 incrementCanonicalDecimal 用BigInt完成并转回十进制字符串绝不触碰number滞后计算 decimalLag 同样基于BigInt。前端路由的启发式更强注释Parse only deliberately small control-plane integers, never uint64 data——parseBoundedPositiveInteger 只用于解析limit这类有上限的小整数since参数则走完整十进制正则校验。结论性建议任何消费方Pro、仪表盘、调试脚本在 JSON 解析后都不得将head_seq/epoch/lag 字段Number()化再比较跨语言比较应使用十进制字符串或大整数库。sync_health 的免责声明sync_health只回答投影器是否追平了头部是一个写路径健康信号。文档特别澄清离线设备的 cursor lag 仅是信息性指标不会让投影变不健康。离线设备只是没拉取并不代表投影器出故障区分这两件事正是把projection_lag_opsHub 级与cursor_lag_ops设备级分开暴露的原因。测试断言同样验证了这一层级关系见 sync-hub.test.ts 的 metadata 用例。设备名来源与X-Device-Name规则客户端会随 Hub 请求发送X-Device-Name头trim 后至多 80 字符。命名语义上有两条精心设计的原则首见即收Hub 保留该设备第一次非空客户端名touchDevice中nameCOALESCE(devices.name, excluded.name)见 SyncHub.ts——已存在则保留旧名新插入才用新名。仪表盘改名优先一次 dashboard内部路由的 rename不会被之后某个 hostname 头覆盖。测试renames only registered devices and preserves dashboard names over client headers专门验证了这一点先 rename 为Desk Mac再以Changed hostname的X-Device-Name调用 status元数据中名字仍是Desk Mac见 sync-hub.test.ts。名字也会被 trim请求头中的 Alexs Laptop 会被规范化为Alexs Laptop同名测试。normalizeDeviceName 展示了完整逻辑null/空/纯空白折叠为 null超过 80 字符直接抛校验错误。connection_state 是咨询性的connection_state反映的是读取时该设备是否存在一条已被接受的咨询性 WebSocket。它是纯提示WS 是hint lane推送/拉取的权威永远在 HTTP 上DO 构造函数中ctx.setWebSocketAutoResponse且webSocketMessage/Close/Error全部为空实现见 SyncHub.ts。connection_state通过遍历当前 socket 的deserializeAttachment().device_id集合计算getMetadata正确性从不依赖它——即使状态滞后或缺失也只是一个展示瑕疵。Device admission bound单用户 64 台设备上限每个用户的 Hub 至多保存64 个不同的设备 id这一常量在源码中具名导出为MAX_DEVICES_PER_USER 64见 SyncHub.ts同时DEVICE_LIMIT_ERROR device_limit_exceeded同文件第 29 行就是稳定错误码。准入路径 vs 非准入路径文档用两条清单精确划分行为准入路径会占设备名额pushPOST /v1/sync/ops、pullGET /v1/sync/changes、WebSocket upgradeGET /v1/sync/ws包括这些请求可选的X-Device-Name头。任何一条路径上出现以前未见过的设备 id都会触发设备注册。非准入路径metadata 读取本文两条内部路由之一与所有公开 status 请求GET /v1/sync/status。路径设备是否被准入达上限时新设备表现已注册设备表现push / pull / WS是409 {error:device_limit_exceeded}正常更新 last-seen/游标继续同步status公开否被忽略不持久化可刷新 last-seen/名字metadata内部否—只读返回关键实现是 touchDevice 与 touchExistingDevice 的分工准入走INSERT ... WHERE EXISTS(已存在) OR (SELECT COUNT(*) 64)的单条原子 SQLrowsWritten 0即抛deviceLimitError而 status/metadata 只走touchExistingDevice的UPDATE ... WHERE device_id ?对未知 id 的 UPDATE 影响 0 行、静默无害。原子性并发首见不会冲破 64准入是在 per-user DO 内的一条原子 SQLite 语句完成的因此并发首见请求不可能把数量冲到 64 以上。这是因为单条 SQL 的判断与插入在同一语句内原子完成且整个 Hub 的存储层是单 DO 的同步事务。结合测试可见DO 内部对设备上限的施加是在transactionSync中与操作提交一起完成的pushOps 中先touchDevice再写日志。为何 status 探测不能耗尽名额这是防止认证过的连通性探测如常驻状态轮询、仪表盘心跳把名额白白占光的守卫。测试concurrent status probes create zero devices and cannot exhaust admission并发发起 80 个不同设备名的 status 探测全部200且 metadata 中设备数保持 0随后 80 个不同 id 的changes拉取只有前 64 个被接受、后 16 个稳定返回409 device_limit_exceeded见 sync-hub.test.ts。达上限时的既有设备不受影响已达上限后已有设备仍然更新 last-seen/cursor 并继续同步只有此前未见过的 id 才会收到409。测试keeps existing devices writable/readable while new admitting paths are rejected at the cap验证已有设备 status 正常、可读写而新设备的 pull/push 稳定 409sync-hub.test.ts。从运维角度看这个稳定的 409 是账户/设备管理条件而非可无限重试的传输失败——DEPLOY.md §1.4 对此有同样的告诫。另外两个防御细节也值得记住metadata 最多返回 64 台设备给未知设备rename 同样什么都不创建返回404rename never creates a phantom device因为 rename 底层是UPDATE devices SET name? WHERE device_id?靠rowsWritten 0判断是否命中见 renameDevice。Rename a device重命名已注册设备路由POST /internal/v1/sync/device-name请求体四个字段精确键顺序无关{ protocol_version: 1, user_id: canonical-user-id, device_id: a-stable-device-id, name: Desk Mac }校验规则nametrim 后必须包含1–80 字符空名会被拒绝80 与X-Device-Name的上限一致。device_idtrim 后必须包含1–128 字符。两层校验同时存在Worker 路由层先做快速 400 校验见 handleDeviceRenameDO 层renameDevice再做一次SyncHub.ts——防御纵深任何一层漏过都会被另一层拦下。已注册设备返回{ protocol_version: 1, user_id: canonical-user-id, device_id: a-stable-device-id, name: Desk Mac }未知设备返回404重命名绝不创建幻影设备。需要留意name与device_id都会被 trim因此 Desk Mac 会存成Desk Mac这与认证层对X-Device-Id/X-Device-Name的 trim 策略dev-a 与 dev-a 必须是同一逻辑设备见 authenticateRequest保持一致——传输层身份永远规范化写入的 canonical 操作体则永不被改写。与整体架构的关系谁在调用这套契约本文讨论的内部路由是 sync-hub 无状态 Workerindex.ts入口的一部分。从路由表第 863–874 行可见同属/internal/v1命名空间、共用同一CMEM_INTERNAL_PROJECTOR_SECRET的还有POST /internal/v1/sync/reset—— 按用户彻底重置 Hub预发布状态卫生用见 DEPLOY.md §1.6POST /internal/v1/projection/drain—— Pro 侧定时修复任务对滞后用户补齐投影。它们的请求体契约与本文两条路由同构{protocol_version, user_id}精确键 Bearer 内部密钥互证了这套内部契约的设计一致性。本契约对应整体同步方案的落点可追溯到规划文档 plans/2026-07-17-phase5-two-lane-sync.md其运行时行为由 sync-hub.test.ts含internal payload-free Hub metadata专节与 DEPLOY.md 交叉印证。若要扩展验证可在仓库根目录运行 canonical 客户端矩阵 E2Enpm run e2e:sync-matrix见 DEPLOY.md §1.5。消费方速查与最佳实践面向要接入这套内部契约的 Pro 服务端/控制面开发者将全篇收敛为可执行清单凭据统一使用环境级随机共享密钥CMEM_INTERNAL_PROJECTOR_SECRETwrangler secret put注入DEPLOY.md §1.3绝不用普通用户 token失败即401。报文格式请求体必须精确等于契约字段metadata 只带protocol_version:1,user_id多带任何字段都会400方法固定POST否则405。状态判定只信projected_seq/sync_health判断投影健康度不要把设备级cursor_lag_ops混入整体健康评估。数字处理所有游标/滞后字段按十进制字符串处理比较/自增用大整数Number()是精度事故的根源。设备管理判断某设备是否可继续同步看它是否已注册metadata 设备列表注册用 push/pull 路径409 即名额满改名只走本文的 rename 路由探测连通性用 status永不占名额。不做的事不要在元数据通道请求内容、计数或 outbox 深度——那是投影通道的职责这套契约对它永久关闭。【免费下载链接】claude-memPersistent Context Across Sessions for Every Agent – Captures everything your agent does during sessions, compresses it with AI, and injects relevant context back into future sessions. Works with Claude Code, OpenClaw, Codex, Gemini, Hermes, Copilot, OpenCode More项目地址: https://gitcode.com/GitHub_Trending/cl/claude-mem创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表