baa-conductor

git clone 

commit
d3e8faa
parent
4cfc0f6
author
codex@macbookpro
date
2026-03-30 17:41:19 +0800 CST
fix: prioritize targetId in renewal link scoring
6 files changed,  +190, -15
M apps/conductor-daemon/src/index.test.js
+116, -0
  1@@ -6694,6 +6694,122 @@ test("observeRenewalConversation ignores inactive remote links and creates a new
  2   }
  3 });
  4 
  5+test("observeRenewalConversation gives targetId absolute priority over weaker page signals", async () => {
  6+  const rootDir = mkdtempSync(join(tmpdir(), "baa-conductor-renewal-target-priority-"));
  7+  const stateDir = join(rootDir, "state");
  8+  const store = new ArtifactStore({
  9+    artifactDir: join(stateDir, ARTIFACTS_DIRNAME),
 10+    databasePath: join(stateDir, ARTIFACT_DB_FILENAME)
 11+  });
 12+  const observedAt = Date.UTC(2026, 2, 30, 12, 5, 0);
 13+
 14+  try {
 15+    await store.upsertLocalConversation({
 16+      localConversationId: "lc-target-priority-tab-1",
 17+      platform: "chatgpt"
 18+    });
 19+    await store.upsertLocalConversation({
 20+      localConversationId: "lc-target-priority-tab-2",
 21+      platform: "chatgpt"
 22+    });
 23+
 24+    await store.upsertConversationLink({
 25+      clientId: "firefox-chatgpt",
 26+      createdAt: observedAt - 10_000,
 27+      isActive: true,
 28+      linkId: "link-target-priority-tab-1",
 29+      localConversationId: "lc-target-priority-tab-1",
 30+      observedAt: observedAt - 10_000,
 31+      pageTitle: "Old Tab State",
 32+      pageUrl: "https://chatgpt.com/",
 33+      platform: "chatgpt",
 34+      routePath: "/",
 35+      routePattern: "/",
 36+      targetId: "tab:1",
 37+      targetKind: "browser.proxy_delivery",
 38+      targetPayload: {
 39+        clientId: "firefox-chatgpt",
 40+        tabId: 1
 41+      },
 42+      updatedAt: observedAt - 10_000
 43+    });
 44+    await store.upsertConversationLink({
 45+      clientId: "firefox-chatgpt",
 46+      createdAt: observedAt - 5_000,
 47+      isActive: true,
 48+      linkId: "link-target-priority-tab-2",
 49+      localConversationId: "lc-target-priority-tab-2",
 50+      observedAt: observedAt - 5_000,
 51+      pageTitle: "Shared Thread",
 52+      pageUrl: "https://chatgpt.com/c/conv-target-priority",
 53+      platform: "chatgpt",
 54+      routePath: "/c/conv-target-priority",
 55+      routePattern: "/c/:conversationId",
 56+      targetId: "tab:2",
 57+      targetKind: "browser.proxy_delivery",
 58+      targetPayload: {
 59+        clientId: "firefox-chatgpt",
 60+        pageUrl: "https://chatgpt.com/c/conv-target-priority",
 61+        tabId: 2
 62+      },
 63+      updatedAt: observedAt - 5_000
 64+    });
 65+
 66+    const observation = await observeRenewalConversation({
 67+      assistantMessageId: "msg-target-priority",
 68+      clientId: "firefox-chatgpt",
 69+      observedAt,
 70+      pageTitle: "Shared Thread",
 71+      pageUrl: "https://chatgpt.com/c/conv-target-priority",
 72+      platform: "chatgpt",
 73+      route: {
 74+        assistantMessageId: "msg-target-priority",
 75+        conversationId: null,
 76+        observedAt,
 77+        organizationId: null,
 78+        pageTitle: "Shared Thread",
 79+        pageUrl: "https://chatgpt.com/c/conv-target-priority",
 80+        platform: "chatgpt",
 81+        shellPage: false,
 82+        tabId: 1
 83+      },
 84+      store
 85+    });
 86+
 87+    assert.equal(observation.created, false);
 88+    assert.equal(observation.resolvedBy, "active_link");
 89+    assert.equal(observation.conversation.localConversationId, "lc-target-priority-tab-1");
 90+    assert.equal(observation.link.linkId, "link-target-priority-tab-1");
 91+    assert.equal(observation.link.targetId, "tab:1");
 92+    assert.equal(observation.link.pageTitle, "Shared Thread");
 93+    assert.equal(observation.link.pageUrl, "https://chatgpt.com/c/conv-target-priority");
 94+    assert.equal(observation.link.routePath, "/c/conv-target-priority");
 95+    assert.equal(observation.link.remoteConversationId, "conv-target-priority");
 96+
 97+    const preservedLink = await store.getConversationLink("link-target-priority-tab-1");
 98+    assert.ok(preservedLink);
 99+    assert.equal(preservedLink.isActive, true);
100+    assert.equal(preservedLink.localConversationId, "lc-target-priority-tab-1");
101+    assert.equal(preservedLink.remoteConversationId, "conv-target-priority");
102+
103+    const deactivatedLink = await store.getConversationLink("link-target-priority-tab-2");
104+    assert.ok(deactivatedLink);
105+    assert.equal(deactivatedLink.isActive, false);
106+    assert.equal(deactivatedLink.localConversationId, "lc-target-priority-tab-2");
107+
108+    assert.deepEqual(
109+      await store.findConversationLinkByRemoteConversation("chatgpt", "conv-target-priority"),
110+      preservedLink
111+    );
112+  } finally {
113+    store.close();
114+    rmSync(rootDir, {
115+      force: true,
116+      recursive: true
117+    });
118+  }
119+});
120+
121 test("ConductorRuntime persists renewal conversation links from browser.final_message and exposes renewal controls", async () => {
122   const stateDir = mkdtempSync(join(tmpdir(), "baa-conductor-renewal-control-"));
123   const runtime = new ConductorRuntime(
M apps/conductor-daemon/src/renewal/conversations.ts
+37, -13
 1@@ -62,6 +62,11 @@ interface ExistingLinkResolution {
 2   resolvedBy: ObserveRenewalConversationResult["resolvedBy"];
 3 }
 4 
 5+interface ConversationLinkScore {
 6+  matchedTargetId: boolean;
 7+  weakSignalScore: number;
 8+}
 9+
10 export async function getRenewalConversationDetail(
11   store: ArtifactStore,
12   localConversationId: string,
13@@ -525,19 +530,19 @@ async function resolveExistingConversationLink(
14 
15   const candidates = await store.listConversationLinks(filters);
16   let bestLink: ConversationLinkRecord | null = null;
17-  let bestScore = 0;
18+  let bestScore: ConversationLinkScore | null = null;
19 
20   for (const candidate of candidates) {
21     const score = scoreConversationLink(candidate, input.observedRoute);
22 
23-    if (score <= 0) {
24+    if (!score.matchedTargetId && score.weakSignalScore <= 0) {
25       continue;
26     }
27 
28     if (
29       bestLink == null
30-      || score > bestScore
31-      || (score === bestScore && candidate.observedAt > bestLink.observedAt)
32+      || bestScore == null
33+      || compareConversationLinkScores(score, bestScore, candidate, bestLink) > 0
34     ) {
35       bestLink = candidate;
36       bestScore = score;
37@@ -550,29 +555,48 @@ async function resolveExistingConversationLink(
38   };
39 }
40 
41-function scoreConversationLink(
42+function compareConversationLinkScores(
43+  candidateScore: ConversationLinkScore,
44+  bestScore: ConversationLinkScore,
45   candidate: ConversationLinkRecord,
46-  observedRoute: NormalizedObservedRoute
47+  bestLink: ConversationLinkRecord
48 ): number {
49-  let score = 0;
50+  if (candidateScore.matchedTargetId !== bestScore.matchedTargetId) {
51+    return candidateScore.matchedTargetId ? 1 : -1;
52+  }
53 
54-  if (observedRoute.targetId != null && candidate.targetId === observedRoute.targetId) {
55-    score += 100;
56+  if (candidateScore.weakSignalScore !== bestScore.weakSignalScore) {
57+    return candidateScore.weakSignalScore - bestScore.weakSignalScore;
58   }
59 
60+  return candidate.observedAt - bestLink.observedAt;
61+}
62+
63+function scoreConversationLink(
64+  candidate: ConversationLinkRecord,
65+  observedRoute: NormalizedObservedRoute
66+): ConversationLinkScore {
67+  const matchedTargetId =
68+    observedRoute.targetId != null
69+    && candidate.targetId === observedRoute.targetId;
70+  let weakSignalScore = 0;
71+
72   if (observedRoute.pageUrl != null && candidate.pageUrl === observedRoute.pageUrl) {
73-    score += 80;
74+    weakSignalScore += 80;
75   }
76 
77   if (observedRoute.routePath != null && candidate.routePath === observedRoute.routePath) {
78-    score += 60;
79+    weakSignalScore += 60;
80   }
81 
82   if (observedRoute.pageTitle != null && candidate.pageTitle === observedRoute.pageTitle) {
83-    score += 10;
84+    weakSignalScore += 10;
85   }
86 
87-  return score;
88+  return {
89+    matchedTargetId,
90+    weakSignalScore
91+  };
92 }
93 
94 function normalizeOptionalString(value: unknown): string | null {
M bugs/README.md
+1, -1
 1@@ -15,7 +15,6 @@ bugs/
 2 
 3 - `BUG-027`:[`BUG-027-startup-plugin-diagnostic-events-lost-before-ws-open.md`](./BUG-027-startup-plugin-diagnostic-events-lost-before-ws-open.md)
 4 - `BUG-028`:[`BUG-028-gemini-shell-final-message-raw-protocol.md`](./BUG-028-gemini-shell-final-message-raw-protocol.md)
 5-- `BUG-030`:[`BUG-030-score-conversation-link-weak-signals-beat-strong.md`](./BUG-030-score-conversation-link-weak-signals-beat-strong.md)
 6 - `BUG-031`:[`BUG-031-link-scan-limit-silent-truncation.md`](./BUG-031-link-scan-limit-silent-truncation.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@@ -47,6 +46,7 @@ bugs/
10 | BUG-025 | FIXED | delivery 已优先路由到业务页,不再默认落到 shell 页 |
11 | BUG-026 | FIXED | repo 根路径现在会正确 fallback 到默认 `log.html` |
12 | BUG-029 | FIXED | 已停用 conversation link 不会再被远端对话查询命中 |
13+| BUG-030 | FIXED | `targetId` 匹配现在绝对优先于弱信号叠加 |
14 | MISSING-001 | FIXED | 执行结果已经接到 AI 对话 delivery 主链 |
15 | MISSING-002 | FIXED | 插件侧 delivery plan 执行器已落地 |
16 | MISSING-003 | FIXED | Phase 1 已补齐 browser.claude target |
A bugs/archive/FIX-BUG-030.md
+34, -0
 1@@ -0,0 +1,34 @@
 2+# FIX-BUG-030: targetId 强信号优先于弱信号叠加
 3+
 4+## 执行状态
 5+
 6+- 已完成(2026-03-30,代码 + 自动化验证已落地)
 7+
 8+## 关联 Bug
 9+
10+BUG-030-score-conversation-link-weak-signals-beat-strong.md
11+
12+## 实际修改文件
13+
14+- `apps/conductor-daemon/src/renewal/conversations.ts`
15+- `apps/conductor-daemon/src/index.test.js`
16+- `bugs/README.md`
17+- `bugs/archive/README.md`
18+
19+## 实际修改
20+
21+- 把 `resolveExistingConversationLink` 的候选比较从单个累计分数改为分层比较:
22+  - 先看是否命中 `targetId`
23+  - 只有在 `targetId` 同层时,才比较 `pageUrl`、`routePath`、`pageTitle` 的弱信号分数
24+  - 若仍然持平,再用 `observedAt` 作为最终 tie-breaker
25+- 保留原有弱信号权重,但它们不再能叠加压过 `targetId`
26+- 新增回归测试,覆盖多 tab 场景下:
27+  - 目标 tab 只命中 `targetId`
28+  - 非目标 tab 命中 `pageUrl + routePath + pageTitle`
29+  - 最终仍必须选中 `targetId` 对应的 link,并停用冲突 link
30+
31+## 验收标准
32+
33+1. `targetId` 匹配在对话关联解析中具有绝对优先级
34+2. 多 tab 场景下不会因 `pageUrl` / `routePath` / `pageTitle` 叠加而选错 link
35+3. `pnpm -C /Users/george/code/baa-conductor/worktrees/bug-030-targetid-priority/apps/conductor-daemon test` 通过(`72/72`)
M bugs/archive/README.md
+2, -1
 1@@ -3,7 +3,7 @@
 2 本目录保留 `已关闭` 或 `已修复` 的 `BUG-*`、`FIX-BUG-*`、`MISSING-*` 和 `OPT-*` 文档。
 3 
 4 - 归档时间:`2026-03-30`
 5-- 最近新增归档:`BUG-018`、`BUG-019`、`BUG-020`、`BUG-021`、`BUG-022`、`BUG-023`、`BUG-024`、`BUG-025`、`BUG-026`、`BUG-029`、`MISSING-003`
 6+- 最近新增归档:`BUG-018`、`BUG-019`、`BUG-020`、`BUG-021`、`BUG-022`、`BUG-023`、`BUG-024`、`BUG-025`、`BUG-026`、`BUG-029`、`BUG-030`、`MISSING-003`
 7 
 8 最近归档的问题:
 9 
10@@ -17,4 +17,5 @@
11 - `BUG-025`:delivery 已优先路由到业务页,不再默认落到 shell 页
12 - `BUG-026`:repo 根路径现在会正确 fallback 到默认 `log.html`
13 - `BUG-029`:已停用 conversation link 不会再被远端对话查询命中
14+- `BUG-030`:`targetId` 匹配现在绝对优先于弱信号叠加
15 - `MISSING-003`:Phase 1 已补齐 `browser.claude` target