TokSync v0.5 做完 Public Proof Pack 和 Wrapped 后,我心里其实有一点松。公开证明包能跑,私有 dashboard 能跑,README embed 也开始有形状。它看上去已经从一个“把 AI coding usage 收起来”的工具,长成了一个能被展示、能被验证、能被分享的系统。
然后审查报告把我拉回了地面。
最刺眼的问题藏在 FileStore 里。开发阶段为了让本地 demo 和测试不依赖外部数据库,我让 TokSync 默认把数据写进一个 JSON store。这个选择很方便:没有 Postgres、没有 migration 成本、一个命令就能跑完整闭环。问题也很朴素:repository 里的很多写路径都在做同一件事,先 read() 整份数据,在内存里改 rollup、event、device、health,再 write() 回去。
单请求时它很安静。两个设备同时同步时,风险就出来了。A 请求读到旧数据,B 请求也读到旧数据;B 先写入成功,A 后写入时拿着自己的旧视角覆盖回去。文件锁保护了写入动作,却没有保护“读取、修改、写回”这一整段。最坏的结果很难看:事件丢失,rollup 对不上,设备删除也可能被旧状态抹掉。它不会每天都发生,但一旦发生,用户看到的只是 totals 少了一截。
我当时卡住的点,是这个 bug 很符合“本地工具”的错觉。因为 TokSync 还在开发阶段,数据文件也在自己机器上,很多路径看起来天然串行。可产品的核心承诺偏偏是多设备同步:同一份历史会从不同机器、不同 agent、不同时间窗口进来。只要承认这个入口,FileStore 就不能再被当成普通配置文件对待。
修复没有多华丽。我把持久化 mutation 统一改到 FileTokSyncStore.transaction() 里,让锁拿到之后重新读取磁盘上的最新数据,再执行 mutation 并提交。也就是说,锁保护的不再只是最后那一下写文件,它包住了决定新状态的整个过程。TokSyncRepository 里涉及 device auth、token、sync ingest、source health、cost guardrails、vault import/export、public profile、leaderboard opt-in 的写路径,都改走同一条事务路由。
这个改动写完后,我又回头看了一遍所有会改状态的接口,确认没有新的旁路继续绕开事务。
同一天还翻出来一个 dedup key 的裂缝。generic parser 和 source-specific parser 对同一个 usage event 生成身份时,种子组成不一致:一边少了 modelId。这类 bug 很烦,因为它不会像异常那样直接炸掉,它会让重复数据和漏计数据慢慢变成“看起来合理”的数字。我把生成逻辑收进共享的 usageDedupKey(),用包含 modelId 的五段 seed 作为统一规则,再用 fixture snapshot 锁住。
审查还指出 API 认证分支重复了大约 25 次。这个问题没有竞态那么危险,但它会让以后每次改 auth 都像在修补一张漏风的网。我抽了 authMiddleware(),让私有路由从中间件上下文里取用户身份。顺手补上全局错误处理,确保未捕获异常也能回结构化 JSON。
这轮修复真正让我踏实的地方在测试。TokSync 的用例数从 99 增到 121,覆盖率从 43.67% 到 60.73%。Agent CLI 从几乎没有关键路径测试,到覆盖 local receipt、device fingerprint、分块上传摘要和 TOKSYNC_API_TOKEN 清理。Web 侧补了 dashboard、vault、proof-pack、wrapped 的基础渲染,隐私边界也继续压在测试里:公开 payload 不能带 dedupKey、source session/message id、workspace hash,metrics-only payload 不能带 prompt、assistant response、tool args、file content 或 secret。
我后来回看这次修复,最有价值的部分并非某个评分变好。它让我重新校准了 TokSync 的底线:这个产品可以先用 FileStore 跑本地闭环,但同步语义必须像数据库一样严肃;公开展示可以做得漂亮,但每个出口都得知道自己读的是 public cache 还是 private raw event。
Hosted Postgres 事务、真实 worker 队列、OS keychain 还留在后面。FileStore transaction 只是当前阶段的地基补强。可这个小竞态被修掉后,我对 TokSync 的信任感明显变了:它不再只是把数字收集起来的工具,它开始懂得保护那些数字从哪里来、经过了哪些边界、为什么没有被悄悄吞掉。
后来
到 v0.6,我把登录后的应用收窄成 Dashboard、Sync、Sources、Share、Settings 五个入口,旧地址保留跳转,公开接口和 SVG 地址不变。采集器覆盖了 Codex CLI、Claude Code、OpenCode、Cursor、Copilot、Gemini CLI 和 OpenClaw 七种工具。
存储仍然默认是这个 FileStore。托管版需要的 Postgres 和队列运行时,我决定先搁着,等单独想清楚再动——这次竞态修复留下的事务边界,到时候正好是迁移的起点。