- commit
- 833be60
- parent
- d3e8faa
- author
- claude@macbookpro
- date
- 2026-03-30 17:45:52 +0800 CST
docs: add BUG-032, BUG-033, OPT-008 from code review BUG-032: dispatcher does not set cooldownUntil after successful renewal, causing repeated messages to the same conversation BUG-033: UPSERT SQL unconditionally overwrites created_at OPT-008: timed-jobs appendFileSync blocks event loop Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
4 files changed,
+178,
-0
1@@ -0,0 +1,70 @@
2+# BUG-032: Dispatcher 成功完成续命后未设置 cooldownUntil,同一对话会被连续续命
3+
4+> 提交者:Claude
5+> 日期:2026-03-30
6+
7+## 现象
8+
9+Renewal dispatcher 在标记 job 为 `done` 后,没有回写 `cooldownUntil` 到 `local_conversations` 表。而 renewal projector 在判断对话是否可续命时会检查 `cooldownUntil`(`projector.ts:324`)。由于该字段始终为 null,projector 每个 tick 都会为同一对话的新消息生成新的 renewal job,导致:
10+
11+- 同一对话在短时间内被反复续命
12+- 上游 AI 平台收到大量重复消息,触发限流或产生垃圾对话内容
13+- 续命的节流机制形同虚设
14+
15+## 触发路径
16+
17+```text
18+1. projector 扫描到 auto 对话的新 assistant 消息,创建 renewal_job (status=pending)
19+2. dispatcher 执行 job,proxyDelivery 成功,标 status=done
20+3. 下一个 tick(10s 后),projector 再次扫描
21+4. 同一对话 cooldownUntil 仍为 null → projector 判定"可续命"
22+5. 新消息到达后再次创建 renewal_job
23+6. 循环重复
24+```
25+
26+## 根因
27+
28+`apps/conductor-daemon/src/renewal/dispatcher.ts` 第 309-321 行,job 成功后只更新了 `renewal_jobs` 表:
29+
30+```typescript
31+await artifactStore.updateRenewalJob({
32+ attemptCount,
33+ finishedAt,
34+ jobId: job.jobId,
35+ // ...
36+ status: "done",
37+});
38+```
39+
40+缺少对 `local_conversations.cooldownUntil` 的回写。
41+
42+## 修复建议
43+
44+在 dispatcher 标记 job 为 `done` 后,调用 `artifactStore.upsertLocalConversation` 设置 cooldown:
45+
46+```typescript
47+await artifactStore.upsertLocalConversation({
48+ cooldownUntil: finishedAt + cooldownIntervalMs,
49+ localConversationId: job.localConversationId,
50+ platform: dispatchContext.conversation.platform,
51+ updatedAt: finishedAt
52+});
53+```
54+
55+`cooldownIntervalMs` 应为可配置参数,建议默认值与 timed-jobs interval 对齐或更长(如 60s~300s)。
56+
57+## 严重程度
58+
59+**High**
60+
61+这是续命自动化的核心节流机制缺口。上线 auto 模式后会立即触发,导致重复发消息。
62+
63+## 发现时间
64+
65+`2026-03-30 by Claude`
66+
67+## 相关代码
68+
69+- dispatcher 成功路径:[apps/conductor-daemon/src/renewal/dispatcher.ts](/Users/george/code/baa-conductor/apps/conductor-daemon/src/renewal/dispatcher.ts) 第 309-321 行
70+- projector cooldown 检查:[apps/conductor-daemon/src/renewal/projector.ts](/Users/george/code/baa-conductor/apps/conductor-daemon/src/renewal/projector.ts) 第 323-331 行
71+- schema cooldown_until 字段:[packages/artifact-db/src/schema.ts](/Users/george/code/baa-conductor/packages/artifact-db/src/schema.ts) `local_conversations.cooldown_until`
1@@ -0,0 +1,51 @@
2+# BUG-033: UPSERT_LOCAL_CONVERSATION_SQL 无条件覆盖 created_at
3+
4+> 提交者:Claude
5+> 日期:2026-03-30
6+
7+## 现象
8+
9+`UPSERT_LOCAL_CONVERSATION_SQL` 的 `ON CONFLICT` 分支包含 `created_at = excluded.created_at`,即冲突更新时会用新值覆盖原有的 `created_at`。
10+
11+当前应用层 `buildLocalConversationRecord` 通过 `input.createdAt ?? existing?.createdAt ?? Date.now()` 做了 fallback,正常路径下 `existing.createdAt` 会被保留。但如果:
12+
13+- `getLocalConversation` 因任何原因返回 null(例如 WAL 读延迟、表被 vacuum、或未来代码路径绕过了 existing 查询)
14+- `input.createdAt` 未提供
15+
16+则 `createdAt` 会被刷成 `Date.now()`,丢失原始创建时间。
17+
18+同样的问题存在于 `UPSERT_CONVERSATION_LINK_SQL` 和 `UPSERT_RENEWAL_JOB_SQL`。
19+
20+## 根因
21+
22+SQL 层没有保护 `created_at` 的不可变性。三条 UPSERT 语句的 `ON CONFLICT DO UPDATE SET` 都包含 `created_at = excluded.created_at`。
23+
24+## 修复建议
25+
26+在 SQL 层用 `COALESCE` 保护:
27+
28+```sql
29+ON CONFLICT(local_conversation_id) DO UPDATE SET
30+ -- ...其他字段...
31+ created_at = COALESCE(local_conversations.created_at, excluded.created_at),
32+ updated_at = excluded.updated_at;
33+```
34+
35+对 `conversation_links` 和 `renewal_jobs` 的 UPSERT 做相同处理。
36+
37+这样即使应用层传入了错误的 `created_at`,数据库层也会优先保留已有值。
38+
39+## 严重程度
40+
41+**Medium**
42+
43+当前单进程 SQLite 下触发概率低,但属于数据正确性防御缺口。未来如果新增不经 existing 查询的 upsert 路径,会直接暴露。
44+
45+## 发现时间
46+
47+`2026-03-30 by Claude`
48+
49+## 相关代码
50+
51+- 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`
52+- 应用层 fallback:同文件 `buildLocalConversationRecord`、`buildConversationLinkRecord`
1@@ -0,0 +1,54 @@
2+# OPT-008: timed-jobs 日志改用异步写入避免阻塞事件循环
3+
4+> 提交者:Claude
5+> 日期:2026-03-30
6+
7+## 现象
8+
9+`ConductorTimedJobs` 的 `writeLogEntry` 使用 `appendFileSync` 写 JSONL 日志。每个 tick 的 framework 日志 + 每个 runner 的启动/完成日志都会触发同步 IO。当 runner 数量增多或 tick 频率较高时,同步写入会阻塞事件循环,拖慢同一 tick 内后续 runner 的执行和其他并发请求的处理。
10+
11+## 当前行为
12+
13+```typescript
14+// timed-jobs/runtime.ts writeLogEntry()
15+appendFileSync(filePath, `${JSON.stringify(entry)}\n`);
16+```
17+
18+每次 tick 至少写 2 条日志(tick_started + tick_completed),每个 runner 再加 2 条(runner_started + runner_completed)。当前 2 个 runner(projector + dispatcher),一个 tick 最少 6 次 `appendFileSync`。
19+
20+## 建议
21+
22+### 方案 A(推荐)
23+
24+改用 `appendFile`(异步版本),`writeLogEntry` 变为 fire-and-forget:
25+
26+```typescript
27+appendFile(filePath, `${JSON.stringify(entry)}\n`, (err) => {
28+ if (err) console.error(`[timed-jobs-log] write failed: ${String(err)}`);
29+});
30+```
31+
32+### 方案 B
33+
34+攒 buffer,每个 tick 结束后批量写一次:
35+
36+```typescript
37+this.logBuffer.push(entry);
38+// 在 tick 完成后
39+appendFileSync(filePath, this.logBuffer.map(e => JSON.stringify(e)).join('\n') + '\n');
40+this.logBuffer = [];
41+```
42+
43+## 严重程度
44+
45+**Low**
46+
47+当前 2 个 runner + 10s interval,影响可忽略。但随 runner 增多或 interval 缩短会逐步放大。
48+
49+## 发现时间
50+
51+`2026-03-30 by Claude`
52+
53+## 相关代码
54+
55+- 同步写入:[apps/conductor-daemon/src/timed-jobs/runtime.ts](/Users/george/code/baa-conductor/apps/conductor-daemon/src/timed-jobs/runtime.ts) `writeLogEntry` 方法
+3,
-0
1@@ -16,11 +16,14 @@ bugs/
2 - `BUG-027`:[`BUG-027-startup-plugin-diagnostic-events-lost-before-ws-open.md`](./BUG-027-startup-plugin-diagnostic-events-lost-before-ws-open.md)
3 - `BUG-028`:[`BUG-028-gemini-shell-final-message-raw-protocol.md`](./BUG-028-gemini-shell-final-message-raw-protocol.md)
4 - `BUG-031`:[`BUG-031-link-scan-limit-silent-truncation.md`](./BUG-031-link-scan-limit-silent-truncation.md)
5+- `BUG-032`:[`BUG-032-dispatcher-missing-cooldown-after-success.md`](./BUG-032-dispatcher-missing-cooldown-after-success.md)
6+- `BUG-033`:[`BUG-033-upsert-local-conversation-overwrites-created-at.md`](./BUG-033-upsert-local-conversation-overwrites-created-at.md)
7 - `OPT-002`:[`OPT-002-executor-timeout.md`](./OPT-002-executor-timeout.md)
8 - `OPT-003`:[`OPT-003-policy-configurable.md`](./OPT-003-policy-configurable.md)
9 - `OPT-004`:[`OPT-004-final-message-claude-sse-fallback.md`](./OPT-004-final-message-claude-sse-fallback.md)
10 - `OPT-005`:[`OPT-005-normalize-parse-error-isolation.md`](./OPT-005-normalize-parse-error-isolation.md)
11 - `OPT-007`:[`plans/OPT-007-DISPATCHER-JITTER.md`](../plans/OPT-007-DISPATCHER-JITTER.md) — renewal dispatcher 加入随机抖动避免限流
12+- `OPT-008`:[`OPT-008-timed-jobs-async-log-writes.md`](./OPT-008-timed-jobs-async-log-writes.md)
13
14 ## 已归档(archive/)
15