- 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
+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(
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 {
+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 |
R bugs/BUG-030-score-conversation-link-weak-signals-beat-strong.md =>
bugs/archive/BUG-030-score-conversation-link-weak-signals-beat-strong.md
+0,
-0
+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`)
+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