同步算法
Mindwtr 采用本地优先同步,并以确定性方式处理冲突。
本页是供维护者与调试工作使用的技术合并参考。有关面向用户的后端设置、恢复步骤和操作指南,请参阅数据与同步。
输入与输出
- 输入 A:本地快照(
tasks、projects、sections、areas、settings) - 输入 B:远程快照(结构相同)
- 输出:合并后的快照 + 合并统计信息(
conflicts、clockSkew、timestampAdjustments、futureTimestampClamps、conflictIds、conflictReasonCounts、conflictSamples、timestampAdjustmentIds、futureTimestampClampIds),以及有上限的同步诊断日志。
基于快照的传输
Mindwtr 目前通过合并完整快照进行同步。这是既定设计,并非尚未构建增量层。
- ADR 0003 和 ADR 0007 定义了在快照载荷之上运行、感知修订的合并行为。
- ADR 0008 记录了保留快照同步并推迟任何仅追加增量日志的传输决策。
- ADR 0009 记录了 SQLite 到 JSON 的桥接契约:SQLite 是主要本地存储,而
data.json是同步/备份快照。 - 对于当前规模,这让同步具备原子性,也比重放和压缩各设备的操作日志更容易推理。
仅当快照文件经常超过 5 MB、典型网络上的同步往返时间超过 5 秒,或 Mindwtr 需要实时多设备流式同步时,才应重新审视 ADR 0008。届时,增量设计应扩展现有 rev 和 revBy 元数据,而不是引入并行的序列方案。
合并规则
- 实体按
id匹配。 - 如果实体只存在于一端,则保留该实体。
- 如果两端都存在,则合并采用感知修订的 LWW:
- 存在修订元数据时,先比较
rev(较高者胜出)。rev是每个实体的编辑计数器,而不是向量时钟,因此一台设备上的多次离线编辑可能胜过另一台设备上的一次较新编辑。 - 如果修订相同,则比较
updatedAt(较新者胜出)。 - 如果时间戳相同,并且两端具有不同的设备 ID,则按字典顺序比较
revBy,然后按规范化内容签名进行确定性决胜。revBy步骤并非设备优先级;它让相同修订、相同时间戳的冲突在内容签名回退生效之前,就能在每个对等端收敛到同一胜方。 - 没有修订元数据的旧实体会把相差在 5 分钟时钟偏差阈值内的
updatedAt值视为不明确的平局,并采用确定性签名胜方。超出该窗口时,较新的updatedAt胜出。
- 存在修订元数据时,先比较
- 软删除使用操作时间:
- 删除标记的操作时间 =
max(updatedAt, deletedAt)。 - 有效记录与已删除记录的冲突选择操作时间较新的一方。
- 如果删除与有效记录的操作时间彼此相差不超过 30 秒,且修订号相同,Mindwtr 会保留有效记录,而不是立即让删除标记胜出。这是刻意设置的不明确窗口规则,可能会让刚删除的任务在另一台设备并发编辑后重新出现。
- 如果在这 30 秒窗口内修订不同,修订较高者仍然胜出。
- 没有修订元数据的旧记录在同一窗口内优先选择删除标记。
- 备份恢复是不明确窗口之外唯一刻意设置的例外:如果有效一端的
revBy = "backup-restore",且其操作时间不早于删除标记的操作时间,则恢复后的有效记录胜出。 - 当删除胜过有效编辑时,Mindwtr 会发出一条有上限的
syncConflictDiscarded诊断记录,其中包含实体类型、ID、操作时间和修订元数据。 - 当保留不明确窗口内的有效记录时,Mindwtr 会发出一条有上限的
Preserved live item during ambiguous delete-vs-live merge诊断记录,并将冲突元数据存入同步历史/设置。
- 删除标记的操作时间 =
- 无效的
deletedAt会回退到updatedAt,以保守确定操作时间。 - 附件按附件
id合并,并采用相同的 LWW 规则。 - 项目归档恢复元数据是不透明的同步元数据:
- 归档项目时,会在归档操作更改的任务和分区上记录临时恢复元数据。
- 可比较的确定性同步签名会忽略此元数据,因此仅归档簿记不会产生内容冲突或确定性胜方。
- 取消归档只会恢复仍与归档所生成更改相匹配的记录。已删除、手动更改或移至其他项目的任务会保持原样,并且可能继续保留不透明元数据,直至下一次真正的用户更改或同步更改重写该记录。
- 领域使用删除标记:
- 删除领域只会为领域本身添加删除标记。该领域内的有效项目和任务会被解除关联(其
areaId会以新的修订清除),并保持有效。删除领域绝不会删除其子项。 - 恢复领域不会重新关联之前已解除关联的子项;它们会保持有效但不属于任何领域。
- 如果传入快照引用了缺失或已删除的领域,同步修复会清除过期的
areaId引用,并标记一个修复修订。 - 同步修复也会在删除标记上运行,因此已删除的项目/任务在以后恢复时不会保留过期的领域链接。
- 合并期间会补全缺失的领域顺序值,并标记
revBy: "sync-repair",这样该修复就不会被其他对等端反复覆盖。
- 删除领域只会为领域本身添加删除标记。该领域内的有效项目和任务会被解除关联(其
- 设置按同步偏好合并:
- 外观/语言/GTD 日程安排/外部日历/AI/已保存筛选器可以独立合并。
- 冲突解决使用组级时间戳(
appearance、language、gtd、externalCalendars、ai、savedFilters)。 - 对同一组内不同字段的并发编辑仍可能折叠为较新的组更新。
- 已保存筛选器按筛选器
id合并。有效记录之间的已保存筛选器冲突严格使用筛选器的updatedAt;只有在时间戳相同或不可用时,才采用确定性决胜。 - 本地
syncPreferences对某一组的退出设置是双向的:Mindwtr 不会将该组发送到远程,也不会接受该组传入的远程更改。 - 密钥(API 密钥、本地模型路径)绝不同步。
- 远程写入恢复是显式的:
- 本地数据会先带着
pendingRemoteWriteAt写入。 - 远程写入成功后清除该标记。
- 远程写入失败后,会安排采用指数退避的重试,间隔从 5 秒逐步增加到 5 分钟。
- 远程写入重试失败 12 次后,Mindwtr 会将同步标记为
error,并显示后端故障,而不是无限重试。 - 设备本地的同步诊断只保留在本地,并会在远程写入前剥离。
- 时钟偏差遥测:
- 合并统计信息会记录观察到的最大偏差。
- 偏差超过 5 分钟时显示警告。
- 超出合并时钟 5 分钟以上的未来
updatedAt值会在比较时被钳制,并计入futureTimestampClamps。 - 如果同一记录的两端都被未来时间钳制,Mindwtr 会发出一条有上限的
Both merge candidates had future updatedAt timestamps clamped诊断记录,其中包含记录 ID 和钳制时间。
- 同步期间的本地编辑不会采用硬锁:
- 桌面端和移动端会检测本地状态在同步写入阶段是否发生更改。
- 发生更改时,当前周期会中止并排队一次全新同步,而不是覆盖较新的本地快照。
伪代码
read local
read remote
validate payload shape
normalize entities (timestamps, revision metadata)
for each entity type in [tasks, projects, sections, areas, people]:
index local by id
index remote by id
for each id in union(localIds, remoteIds):
if only one side exists: keep it
else:
winner = resolveWinner(localItem, remoteItem)
mergedItem = mergeConflict(localItem, remoteItem, winner) // attachments/settings-specific logic
push mergedItem
merge settings by sync preferences
validate merged payload
write local
write remote
record sync history and diagnostics冲突示例
示例 1:有效记录与已删除记录
- 本地:任务
t1更新于10:01,未删除 - 远程:任务
t1删除于10:03 - 结果:已删除版本胜出(
10:03的操作时间较新)
示例 1b:不明确的删除与有效记录
- 本地:任务
t1编辑于10:00:05,仍然有效 - 远程:任务
t1删除于10:00:20 - 两条记录的修订号相同
- 结果:有效记录胜出,因为两个操作仅相隔 15 秒,且修订元数据在不明确窗口内相同
示例 2:修订和时间戳相同
- 本地和远程均为
rev=4、updatedAt=10:00 - 内容不同(
title、tags等) - 结果:确定性签名比较会在所有设备上选择同一胜方
示例 3:无效的 deletedAt
- 本地删除标记的
deletedAt="invalid-date",updatedAt=09:30 - 远程有效记录的
updatedAt=10:00 - 结果:有效记录胜出,因为无效删除使用
updatedAt回退值(09:30)
附件
- 元数据合并在文件传输协调之前运行。
- 胜方附件的 URI/本地状态在可用时会保留。
- 如果胜方没有可用的本地 URI,合并可以回退到另一端的 URI/状态。
- 附件删除与有效记录的竞争采用与任务/项目相同的合并和
syncConflictDiscarded诊断,因此已删除附件胜过并发元数据编辑的情况会显示在诊断中。 - 缺失的本地文件稍后由附件同步/下载处理。
settings.attachments.pendingRemoteDeletes记录本地删除附件后仍需删除的远程文件。- 待处理的远程删除会一直保留,直到远程删除成功。它们不会按时间清除,因为成功前将其丢弃可能会在后端留下已删除的孤立文件。
- Mindwtr Cloud 还提供经过身份验证的孤立文件清理端点,用于删除当前快照未引用的附件文件。
Cloud 服务器合并
对于 /v1/data,Mindwtr Cloud 并非简单的对象存储。在经过身份验证的 PUT /v1/data 请求中,服务器会读取现有命名空间快照,使用传入快照运行相同的合并算法,验证合并结果,并将该结果写回。
操作层面的后果:
- 推送完整快照并非强制覆盖。修订更高、操作时间更新或删除标记胜出的现有远程记录可能在 PUT 后保留。
- 服务器端引用修复可能产生级联更新,例如为已删除项目下的分区添加删除标记。
- 服务器生成的修复时间戳使用服务器墙上时钟。这可以避免较快的客户端时钟推进服务器修复元数据。
- 成功的
PUT /v1/data响应包含{ ok: true, stats, clockSkewWarning },便于客户端和测试检查服务器采用的合并结果。
快速未更改检查
Cloud 客户端可以在下载完整快照之前发出 HEAD /v1/data。服务器返回不含响应正文的 ETag、Last-Modified 和 Content-Length 元数据。客户端将这些元数据与上次成功同步进行比较,并在远程命名空间未更改时跳过完整的 GET /v1/data 路径。
服务器按文件状态元数据缓存 SHA-256 ETag,因此重复进行未更改的 HEAD 检查时,无需重新对整个快照计算哈希。
定时后台同步
移动端后台同步采用保守的 15 分钟最短间隔进行调度。后台任务仅在需要时延迟加载导入器/同步代码,然后运行上述相同的快照合并和远程写入重试逻辑。
后台同步是机会性的:操作系统可能延迟或跳过某次运行,因此当连接或凭据发生变化时,手动同步和前台同步仍是可靠的恢复路径。
重试恢复
- 远程写入失败不会静默丢弃刚合并的本地状态。
pendingRemoteWriteAt、pendingRemoteWriteRetryAt和pendingRemoteWriteAttempts存储在本地。- 下一次同步会暂停到重试窗口到期,然后使用保留的本地快照以及任何更新的本地编辑进行重试。
- 重试 12 次后,同步状态变为
error。保留的本地快照仍留在本地,状态 UI 应引导用户检查后端连接或凭据。
删除标记清除界限
删除标记只有在保留期间才能保护删除操作。当前保留策略由 tombstoneRetentionDays 限定。
从操作角度看,一台离线时间超过保留窗口的设备,可能会重新引入其删除标记已在其他设备上清除的记录。Mindwtr 将此视为快照同步中已记录的一致性界限。用户在依赖长期离线设备上的旧本地数据之前,应先同步该设备;如果需要更严格的保证,未来的协议工作应拒绝上次成功同步早于清除期限的快照。
可检查的诊断信息
- 冲突数量和 ID
- 冲突原因数量和有上限的冲突样本
- 观察到的最大时钟偏差
- 时间戳规范化调整
- 时间戳被规范化的记录 ID
- 未来时间戳钳制数量和 ID
- 有效一端在删除与有效记录的冲突中被丢弃时产生的
syncConflictDiscarded记录 - 设置中的上次同步状态/历史
- 设置 → 同步中的逐冲突详情:受影响项的名称、哪些字段不同,以及哪个设备的版本胜出(仅限最近的样本,且数量有上限)
- 即使关闭调试日志,也会写入
mindwtr.log的冲突摘要(仅包含 ID 和字段名称,绝不包含记录内容)
相关文档
故障排除
如果反复出现冲突或时钟偏差警告:
- 验证设备时钟(已启用自动网络时间)。
- 检查同步后端连接/身份验证。
- 检查应用设置和日志中的同步诊断信息。