codex@macbookpro
·
2026-03-31
BUG-033-upsert-local-conversation-overwrites-created-at.md
1# BUG-033: UPSERT_LOCAL_CONVERSATION_SQL 无条件覆盖 created_at
2
3> 提交者:Claude
4> 日期:2026-03-30
5
6## 现象
7
8`UPSERT_LOCAL_CONVERSATION_SQL` 的 `ON CONFLICT` 分支包含 `created_at = excluded.created_at`,即冲突更新时会用新值覆盖原有的 `created_at`。
9
10当前应用层 `buildLocalConversationRecord` 通过 `input.createdAt ?? existing?.createdAt ?? Date.now()` 做了 fallback,正常路径下 `existing.createdAt` 会被保留。但如果:
11
12- `getLocalConversation` 因任何原因返回 null(例如 WAL 读延迟、表被 vacuum、或未来代码路径绕过了 existing 查询)
13- `input.createdAt` 未提供
14
15则 `createdAt` 会被刷成 `Date.now()`,丢失原始创建时间。
16
17同样的问题存在于 `UPSERT_CONVERSATION_LINK_SQL` 和 `UPSERT_RENEWAL_JOB_SQL`。
18
19## 根因
20
21SQL 层没有保护 `created_at` 的不可变性。三条 UPSERT 语句的 `ON CONFLICT DO UPDATE SET` 都包含 `created_at = excluded.created_at`。
22
23## 修复建议
24
25在 SQL 层用 `COALESCE` 保护:
26
27```sql
28ON CONFLICT(local_conversation_id) DO UPDATE SET
29 -- ...其他字段...
30 created_at = COALESCE(local_conversations.created_at, excluded.created_at),
31 updated_at = excluded.updated_at;
32```
33
34对 `conversation_links` 和 `renewal_jobs` 的 UPSERT 做相同处理。
35
36这样即使应用层传入了错误的 `created_at`,数据库层也会优先保留已有值。
37
38## 严重程度
39
40**Medium**
41
42当前单进程 SQLite 下触发概率低,但属于数据正确性防御缺口。未来如果新增不经 existing 查询的 upsert 路径,会直接暴露。
43
44## 发现时间
45
46`2026-03-30 by Claude`
47
48## 相关代码
49
50- UPSERT SQL 定义:[packages/artifact-db/src/store.ts](/Users/george/code/baa-conductor/packages/artifact-db/src/store.ts) `UPSERT_LOCAL_CONVERSATION_SQL`、`UPSERT_CONVERSATION_LINK_SQL`、`UPSERT_RENEWAL_JOB_SQL`
51- 应用层 fallback:同文件 `buildLocalConversationRecord`、`buildConversationLinkRecord`