baa-conductor

git clone 

commit
e3bc310
parent
8affb3b
author
claude@macbookpro
date
2026-03-31 15:09:53 +0800 CST
docs: add review for renewal fixes round 2 (833be60..8affb3b)

Covers BUG-032 (cooldown), BUG-033 (created_at COALESCE),
BUG-035 (null link dedupe). Identifies merge data loss risk
in deduplicateNullRemoteConversationLinks and cooldown/interval
coupling in resolveSuccessCooldownMs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 files changed,  +100, -0
A reviews/2026-03-31_renewal-fixes-round2_review.md
+100, -0
  1@@ -0,0 +1,100 @@
  2+# Review: Renewal 修复第二轮(cooldown / created_at / null link dedupe)
  3+
  4+- 审查者:Claude Opus 4.6
  5+- 审查日期:2026-03-31
  6+- 关联任务:BUG-032、BUG-033、BUG-035
  7+- 任务文档:`bugs/BUG-032-dispatcher-missing-cooldown-after-success.md`、`bugs/BUG-033-upsert-local-conversation-overwrites-created-at.md`、`bugs/archive/FIX-BUG-035.md`
  8+- 审查范围:`833be60..8affb3b`(4 commits)
  9+- 执行者:Codex
 10+- 关联文档:
 11+  - `bugs/BUG-034-projector-route-unavailable-missing-detail.md`
 12+  - `bugs/BUG-036-projector-cursor-updated-at-unit-mismatch.md`
 13+  - `bugs/OPT-009-renewal-duplicate-utility-functions.md`
 14+
 15+## 涉及文件
 16+
 17+- `apps/conductor-daemon/src/renewal/dispatcher.ts`(修改,+17 行)
 18+- `packages/artifact-db/src/store.ts`(修改,+251 行)
 19+- `packages/artifact-db/src/schema.ts`(修改,+41/-7 行)
 20+- `packages/d1-client/src/d1-setup.sql`(修改,+26/-1 行)
 21+- `packages/d1-client/src/sync-worker.ts`(修改,+10/-1 行)
 22+- `apps/conductor-daemon/src/index.test.js`(修改,+239 行)
 23+- `packages/artifact-db/src/index.test.js`(修改,+386 行)
 24+- `packages/d1-client/src/index.test.js`(修改,+182 行)
 25+
 26+## 实验结论
 27+
 28+三个 bug 均已修复,测试覆盖充分:
 29+
 30+| 修复 | commit | 验证 |
 31+|---|---|---|
 32+| BUG-032 cooldown | `5580526` | dispatcher 测试验证 cooldownUntil 写入 |
 33+| BUG-033 created_at | `8affb3b` | artifact-db 测试验证 COALESCE 保留原值 |
 34+| BUG-035 null dedupe | `cf7aa3a` | 启动去重迁移 + 5 个 partial unique index + 全套测试 |
 35+
 36+## 做对的事
 37+
 38+1. BUG-035 的修复非常彻底:不仅加了 partial unique index 防止未来重复,还做了启动时的迁移逻辑去清理存量脏数据,merge 策略保留最新记录并吸收旧记录的非空字段
 39+2. COALESCE 保护同时应用到所有四条 UPSERT(sessions、local_conversations、conversation_links、renewal_jobs),不留死角
 40+3. D1 sync schema 同步更新了相同的 index 结构,保持 local/remote 一致
 41+4. 测试覆盖量大(807 行新测试),覆盖了 dedupe 迁移、null identity 查找、cooldown 回写等关键路径
 42+
 43+## 问题
 44+
 45+### 中等:deduplicateNullRemoteConversationLinks merge 可能静默丢失 localConversation 关联
 46+
 47+`store.ts:971-974`,当 canonical 和 candidate 指向不同的 `localConversationId` 时,merge 保留 canonical 的 `localConversationId`,然后 DELETE candidate。被删 candidate 关联的那个 `localConversation` 就失去了这条 link,且没有日志记录。
 48+
 49+```typescript
 50+const merged = mergeConversationLinkRecords(canonical, candidate);
 51+// merged.localConversationId = canonical.localConversationId
 52+this.run(UPDATE_CONVERSATION_LINK_SQL, [...conversationLinkParams(merged).slice(1), merged.linkId]);
 53+this.run("DELETE FROM conversation_links WHERE link_id = ?;", [candidate.linkId]);
 54+// candidate.localConversationId 的关联静默丢失
 55+```
 56+
 57+实际触发条件:同一 platform + routePath(或 pageUrl/targetId),remote_conversation_id 均为 NULL,但 localConversationId 不同。这在正常流程中不太可能,但如果历史数据有脏数据就会触发。
 58+
 59+建议:在 DELETE 前检查 `canonical.localConversationId !== candidate.localConversationId`,至少输出 warn 日志。
 60+
 61+### 低:resolveSuccessCooldownMs 与 intervalMs 强耦合
 62+
 63+`dispatcher.ts:801-807`:
 64+
 65+```typescript
 66+return Math.max(DEFAULT_SUCCESS_COOLDOWN_MS, intervalMs + 1);
 67+```
 68+
 69+fallback 逻辑把 cooldown 和 tick interval 耦合。如果 intervalMs 被调到 300000(5 分钟),cooldown 会跟着变成 5 分钟。cooldown 是业务参数("多久后允许再续命"),不应受调度频率影响。
 70+
 71+建议:fallback 直接返回 `DEFAULT_SUCCESS_COOLDOWN_MS`,不与 intervalMs 取 max。`intervalMs + 1` 的意图(确保 cooldown 至少跨一个 tick)可以在 projector 的 eligibility 检查中由 `settleDelayMs` 保证。
 72+
 73+## 反向用例
 74+
 75+### 1. 高并发 observe 导致 null link 唯一索引冲突
 76+
 77+如果两个并发 `observeRenewalConversation` 同时为同一 platform + routePath 创建 link(remote_conversation_id 均为 NULL),第二个会命中 `idx_conversation_links_null_route` 的 UNIQUE 约束。当前 `upsertConversationLink` 的 `ON CONFLICT(link_id)` 不会匹配到这个 index 冲突(link_id 不同),SQLite 会抛 UNIQUE constraint 错误。
 78+
 79+当前单进程 + 同步 SQLite 下不会并发,但如果未来引入异步或多进程,需要处理此场景。
 80+
 81+### 2. 启动去重迁移在大数据量下的性能
 82+
 83+`deduplicateNullRemoteConversationLinks` 一次性加载所有 `remote_conversation_id IS NULL` 的行到内存。如果历史数据量大(数千条 null link),启动时间会被拉长。当前规模下不是问题。
 84+
 85+## 验收对照
 86+
 87+| 验收标准 | 是否满足 |
 88+|---------|---------|
 89+| BUG-032:dispatcher 成功后 cooldownUntil 被设置 | 通过 |
 90+| BUG-032:projector 在 cooldown 内跳过该对话 | 通过(已有测试) |
 91+| BUG-033:UPSERT 不会覆盖已有 created_at | 通过 |
 92+| BUG-033:所有四条 UPSERT 都使用 COALESCE | 通过 |
 93+| BUG-035:NULL remote_conversation_id 的 link 受唯一约束 | 通过 |
 94+| BUG-035:启动时清理存量重复 link | 通过 |
 95+| BUG-035:D1 schema 同步更新 | 通过 |
 96+
 97+## 建议
 98+
 99+1. **deduplicateNullRemoteConversationLinks** 中跨 localConversation 的 merge 加 warn 日志(中等优先)
100+2. **resolveSuccessCooldownMs** 去掉与 intervalMs 的耦合,直接用 `DEFAULT_SUCCESS_COOLDOWN_MS`(低优先)
101+3. **TASK_OVERVIEW.md** 和 **STATUS_SUMMARY.md** 需更新:移除已修的 BUG-030,补充 BUG-032~036 和 OPT-007~009,更新基线到 `8affb3b`(文档卫生)