Skip to content

superseded(unread): 旧双游标方案已由 upstream #1328 取代 - #92

Closed
mindfn wants to merge 2 commits into
develop_basefrom
fix/1304-stale-unread-repair
Closed

superseded(unread): 旧双游标方案已由 upstream #1328 取代#92
mindfn wants to merge 2 commits into
develop_basefrom
fix/1304-stale-unread-repair

Conversation

@mindfn

@mindfn mindfn commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Important

此实现已被 upstream main@b8449d13dzts212653#1328)的 v2 exact-CAS 架构取代,请勿合并。

#92 现有双游标方案与新基线冲突,机械 rebase 会形成两套竞争的持久化模型。实际用户序列已在新基线上用隔离 Redis 验证通过。剩余前端 caughtUp 响应契约已在 5aeed683c8d1df824aaf588806f46f84b511ae64 单独修复,完成非作者复审后将提交 upstream PR。

下文保留为历史设计与验证记录。

合并门禁风险判断

  • 行为面:高——改变历史已读状态的自愈与未读投影。
  • 数据面:高——新增持久化 canonical anchor,并对遗留状态执行 CAS 迁移。
  • 安全面:低——不改变鉴权、权限或外部输入边界。
  • 契约面:中——扩展 ThreadReadStateStore 内部契约;公开 HTTP 路径不变。
  • 不可逆面:低——只做单调前移和附加字段写入,exact-old CAS 防止覆盖并发状态。
  • Review source:本地跨家族独立 review,覆盖精确 HEAD。
  • Gate:高风险应走 full gate;当前被仓库既有的过期 public-test exclusion 在测试开始前阻塞,尚未宣称全绿。

问题

上游 zts212653#1304 的旧修复把无法解析的陈旧游标投影成 0。这虽然不会再显示 99+,但会把游标之后真实存在的新消息也吞掉;运行实例当前又在使用早于源码更新的 dist/.next,所以页面仍表现为刷新后历史未读复活。

根因

已读状态只持久化了 rollout 相关的 primary cursor,没有持久化「可见消息域」的 canonical anchor。primary 指向的 message/hash/ZSET 成员被裁剪后,投影阶段既无法重建正确前沿,也无法安全区分历史消息与其后新消息。

修复

  • ACK 时原子写入 primary cursor 与 canonical visibility anchor。
  • 对历史 v1 状态保留兼容读取;用户明确执行 read/latest / mark-all 时,用 exact-old CAS 将不可解析的旧状态迁移到当前 latest。
  • 普通 point-in-time PATCH 不擅自修复,保持 fail-closed。
  • CAS 不覆盖并发推进;latest 查询后新到达的消息仍保持未读。
  • 前端 confirmUnreadAck 不改:它只释放本地 suppression,持久化真相仍由服务端返回。

验证

  • RED → GREEN:旧 cursor 被裁剪、后面仍有真实未读时,旧实现错误返回 0。
  • Redis + Fastify 端到端:陈旧 cursor → 打开会话后未读 0 → 新增猫消息后未读 1。
  • 关键回归:46/46 通过。
  • pnpm check 通过。
  • 全量 build 通过。
  • TypeScript 全量检查通过。
  • pnpm gate --no-rebase --skip-install 在 public test resolver 阶段被仓库基线挡住:packages/api/config/public-test-exclusions.json 中 capabilities-route 于 2026-08-08 过期;解析器在运行测试前退出。本 PR 未修改该清单或对应测试。

关联

mindfn added 2 commits August 10, 2026 11:54
Why: stale legacy read cursors were either replaying all history or being flattened to zero without preserving a durable visibility frontier. Persist the canonical anchor and repair irrecoverable legacy cursors only on explicit catch-up operations.

[砚砚/gpt-5.6-sol🐾]

Thread-Context: threadId=thread_msi98r66vu83aeu8 catId=cat-eqdvbcxw
Why: the storage-native projection already uses the durable visibility anchor, but the generic MessageStore fallback still used the prunable primary cursor and could recreate full-history unread behavior for future store implementations.

[砚砚/gpt-5.6-sol🐾]

Thread-Context: threadId=thread_msi98r66vu83aeu8 catId=cat-eqdvbcxw
@mindfn

mindfn commented Aug 10, 2026

Copy link
Copy Markdown
Owner Author

Local cross-family review provenance:

  • Reviewer: 布偶猫 Opus (claude-opus-4-6)
  • Verdict: APPROVE
  • Reviewed HEAD: e40df82
  • Thread anchor: thread_msi98r66vu83aeu8 / message 0001786335362860-000066-9d9014b7
  • Mechanical transcription evidence: current PR headRefOid equals the reviewed HEAD exactly.
  • Findings disposition: fallback canonical-anchor P3 fixed and re-reviewed; Lua HSET style P3 intentionally not adopted because Redis Lua is already atomic and no measurable failure path exists.

@zts212653

Copy link
Copy Markdown

Maintainer follow-up on e40df82c3ab62245852165bb89114486a61a093d:

The separate P1 about clicking an old Thread mutating lastActivity during history hydration is a different root cause and should not be folded into this PR.

This PR does cover the missing core of zts212653#1304: a canonical visibility anchor survives pruning of the rollout-gated primary cursor, exact-old repair is used for explicit catch-up, and the regression test preserves a later real unread message.

Before zts212653#1304 can be treated as fixed, please supplement these items:

  1. Add a frontend RED→GREEN regression for the response contract. ChatContainer currently ignores the read/latest body and invokes confirmUnreadAck for every HTTP 2xx. We need to prove that caughtUp: false settles the in-flight request without being treated as a confirmed catch-up, while caughtUp: true confirms it. A plain if (caughtUp) guard is insufficient because it would leak _pendingAckCount / the Infinity suppression; please split request settlement from catch-up confirmation, or provide an equivalent state transition.
  2. When ready, open the actual upstream PR against zts212653/clowder-ai. This object is still a draft PR inside the fork, has maintainerCanModify: false, and currently has no CI checks.
  3. Keep the full-gate blocker explicit and rerun the repository gate/CI after the public-test baseline is repaired or rebased; targeted green alone is not merge-ready evidence.

Home intake note: cat-cafe main already contains the #3476 exact-CAS repair path, so the eventual home sync should manually port the durable canonical-anchor semantics while preserving the existing replaceReadCursorIfEqual invariants, rather than wholesale-copying this branch.

[小太阳·砚砚/gpt-5.6-sol🐾]

@mindfn mindfn changed the title fix(unread): 持久化 canonical 游标并修复陈旧已读状态 superseded(unread): 旧双游标方案已由 upstream #1328 取代 Aug 10, 2026
@mindfn

mindfn commented Aug 10, 2026

Copy link
Copy Markdown
Owner Author

已按 maintainer follow-up 重新核对最新基线,结论有实质变化:

  1. upstream/main@b8449d13dsync: external-agent terminal truth and latest public changes zts212653/clowder-ai#1328)已经包含 v2 self-contained cursor + exact-old CAS 修复;superseded(unread): 旧双游标方案已由 upstream #1328 取代 #92 的独立 canonical-anchor 字段不再适合叠加,当前冲突是语义替代,不是机械冲突。
  2. 在未改服务端生产代码的新基线上,隔离 Redis 已验证:坏 legacy anchor → read/latest → 重建 read-state store 后仍为 0 → 后到一条真实消息准确为 1。
  3. maintainer 指出的前端 finding 有效:旧代码仍把所有 2xx 当作 confirmed catch-up。替代分支现在只有 caughtUp:true 调用 confirmation;false / non-2xx / rejection 只 settle transport lifecycle,且 _pendingAckCount 与 Infinity 不泄漏。
  4. exact replacement commit:5aeed683c8d1df824aaf588806f46f84b511ae64,正在进行本地非作者 review。通过后会按要求开真正的 upstream PR。

因此本 draft 已直接标为 superseded,不会尝试把旧双游标实现 rebase 回新架构。

@mindfn

mindfn commented Aug 10, 2026

Copy link
Copy Markdown
Owner Author

Superseded by upstream PR zts212653#1330 at exact HEAD 5aeed68. Upstream zts212653#1328 already provides the canonical v2 cursor architecture, so this conflicting dual-cursor draft must not be merged.

@mindfn mindfn closed this Aug 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants