- commit
- ca554cc
- parent
- 7982f5e
- author
- codex@macbookpro
- date
- 2026-03-31 17:40:28 +0800 CST
fix: surface detailed projector route skip reasons
2 files changed,
+227,
-24
+179,
-15
1@@ -39,6 +39,7 @@ import {
2 parseBaaInstructionBlock,
3 parseConductorCliRequest,
4 routeBaaInstruction,
5+ shouldRenew,
6 writeHttpResponse
7 } from "../dist/index.js";
8 import { observeRenewalConversation } from "../dist/renewal/conversations.js";
9@@ -3061,24 +3062,87 @@ test("renewal projector scans settled messages with cursor semantics and skips i
10
11 await artifactStore.upsertLocalConversation({
12 automationStatus: "auto",
13- localConversationId: "lc_missing_target",
14+ localConversationId: "lc_non_proxy_target",
15 platform: "claude"
16 });
17 await artifactStore.upsertConversationLink({
18- clientId: "firefox-missing-target",
19- linkId: "link_missing_target",
20- localConversationId: "lc_missing_target",
21+ clientId: "firefox-non-proxy-target",
22+ linkId: "link_non_proxy_target",
23+ localConversationId: "lc_non_proxy_target",
24 observedAt: nowMs - 5_000,
25- pageUrl: "https://claude.ai/chat/conv_missing_target",
26+ pageUrl: "https://claude.ai/chat/conv_non_proxy_target",
27 platform: "claude",
28- remoteConversationId: "conv_missing_target"
29+ remoteConversationId: "conv_non_proxy_target",
30+ targetId: "tab:5",
31+ targetKind: "browser.shell_page",
32+ targetPayload: {
33+ clientId: "firefox-non-proxy-target",
34+ tabId: 5
35+ }
36 });
37- const missingTargetMessage = await artifactStore.insertMessage({
38- conversationId: "conv_missing_target",
39- id: "msg_missing_target",
40+ const nonProxyTargetMessage = await artifactStore.insertMessage({
41+ conversationId: "conv_non_proxy_target",
42+ id: "msg_non_proxy_target",
43 observedAt: nowMs - 5_000,
44 platform: "claude",
45- rawText: "missing target should not project",
46+ rawText: "non proxy delivery target should not project",
47+ role: "assistant"
48+ });
49+
50+ await artifactStore.upsertLocalConversation({
51+ automationStatus: "auto",
52+ localConversationId: "lc_shell_page",
53+ platform: "claude"
54+ });
55+ await artifactStore.upsertConversationLink({
56+ clientId: "firefox-shell-page",
57+ linkId: "link_shell_page",
58+ localConversationId: "lc_shell_page",
59+ observedAt: nowMs - 4_000,
60+ pageUrl: "https://claude.ai/chat/conv_shell_page",
61+ platform: "claude",
62+ remoteConversationId: "conv_shell_page",
63+ targetId: "tab:6",
64+ targetKind: "browser.proxy_delivery",
65+ targetPayload: {
66+ clientId: "firefox-shell-page",
67+ shellPage: true,
68+ tabId: 6
69+ }
70+ });
71+ const shellPageMessage = await artifactStore.insertMessage({
72+ conversationId: "conv_shell_page",
73+ id: "msg_shell_page",
74+ observedAt: nowMs - 4_000,
75+ platform: "claude",
76+ rawText: "shell page route should not project",
77+ role: "assistant"
78+ });
79+
80+ await artifactStore.upsertLocalConversation({
81+ automationStatus: "auto",
82+ localConversationId: "lc_missing_tab_id",
83+ platform: "claude"
84+ });
85+ await artifactStore.upsertConversationLink({
86+ clientId: "firefox-missing-tab-id",
87+ linkId: "link_missing_tab_id",
88+ localConversationId: "lc_missing_tab_id",
89+ observedAt: nowMs - 3_000,
90+ pageUrl: "https://claude.ai/chat/conv_missing_tab_id",
91+ platform: "claude",
92+ remoteConversationId: "conv_missing_tab_id",
93+ targetKind: "browser.proxy_delivery",
94+ targetPayload: {
95+ clientId: "firefox-missing-tab-id"
96+ }
97+ });
98+ const missingTabIdMessage = await artifactStore.insertMessage({
99+ conversationId: "conv_missing_tab_id",
100+ id: "msg_missing_tab_id",
101+ observedAt: nowMs - 3_000,
102+ platform: "claude",
103+ rawText: "missing tab id should not project",
104 role: "assistant"
105 });
106
107@@ -3136,8 +3200,8 @@ test("renewal projector scans settled messages with cursor semantics and skips i
108 const cursorState = await repository.getSystemState("renewal.projector.cursor");
109 assert.ok(cursorState);
110 assert.deepEqual(JSON.parse(cursorState.valueJson), {
111- message_id: missingTargetMessage.id,
112- observed_at: missingTargetMessage.observedAt
113+ message_id: missingTabIdMessage.id,
114+ observed_at: missingTabIdMessage.observedAt
115 });
116
117 const entries = readJsonlEntries(logsDir);
118@@ -3157,9 +3221,28 @@ test("renewal projector scans settled messages with cursor semantics and skips i
119 (entry) => entry.runner === "renewal.projector" && entry.stage === "message_skipped" && entry.result === "cooldown_active"
120 )
121 );
122+ const routeUnavailableEntries = entries.filter(
123+ (entry) => entry.runner === "renewal.projector" && entry.stage === "message_skipped" && entry.result === "route_unavailable"
124+ );
125 assert.ok(
126- entries.find(
127- (entry) => entry.runner === "renewal.projector" && entry.stage === "message_skipped" && entry.result === "route_unavailable"
128+ routeUnavailableEntries.find(
129+ (entry) =>
130+ entry.message_id === nonProxyTargetMessage.id
131+ && entry.route_unavailable_reason === "target_kind_not_proxy_delivery"
132+ )
133+ );
134+ assert.ok(
135+ routeUnavailableEntries.find(
136+ (entry) =>
137+ entry.message_id === shellPageMessage.id
138+ && entry.route_unavailable_reason === "shell_page"
139+ )
140+ );
141+ assert.ok(
142+ routeUnavailableEntries.find(
143+ (entry) =>
144+ entry.message_id === missingTabIdMessage.id
145+ && entry.route_unavailable_reason === "missing_tab_id"
146 )
147 );
148 assert.ok(
149@@ -3167,7 +3250,7 @@ test("renewal projector scans settled messages with cursor semantics and skips i
150 (entry) =>
151 entry.runner === "renewal.projector"
152 && entry.stage === "scan_completed"
153- && entry.cursor_after === `message:${missingTargetMessage.observedAt}:${missingTargetMessage.id}`
154+ && entry.cursor_after === `message:${missingTabIdMessage.observedAt}:${missingTabIdMessage.id}`
155 )
156 );
157
158@@ -3188,6 +3271,87 @@ test("renewal projector scans settled messages with cursor semantics and skips i
159 }
160 });
161
162+test("shouldRenew keeps route_unavailable while exposing structured route failure details", async () => {
163+ const nowMs = Date.UTC(2026, 2, 30, 10, 5, 0);
164+ const baseCandidate = {
165+ conversation: {
166+ automationStatus: "auto",
167+ cooldownUntil: null
168+ },
169+ link: {
170+ isActive: true,
171+ targetId: "tab:42",
172+ targetKind: "browser.proxy_delivery",
173+ targetPayload: JSON.stringify({
174+ clientId: "firefox-route-detail",
175+ tabId: 42
176+ })
177+ },
178+ message: {
179+ id: "msg_route_detail"
180+ }
181+ };
182+
183+ const cases = [
184+ {
185+ expectedReason: "inactive_link",
186+ link: {
187+ isActive: false
188+ }
189+ },
190+ {
191+ expectedReason: "target_kind_not_proxy_delivery",
192+ link: {
193+ targetKind: "browser.shell_page"
194+ }
195+ },
196+ {
197+ expectedReason: "shell_page",
198+ link: {
199+ targetPayload: JSON.stringify({
200+ clientId: "firefox-route-detail",
201+ shellPage: true,
202+ tabId: 42
203+ })
204+ }
205+ },
206+ {
207+ expectedReason: "missing_tab_id",
208+ link: {
209+ targetId: null,
210+ targetPayload: JSON.stringify({
211+ clientId: "firefox-route-detail"
212+ })
213+ }
214+ }
215+ ];
216+
217+ for (const [index, testCase] of cases.entries()) {
218+ const decision = await shouldRenew({
219+ candidate: {
220+ ...baseCandidate,
221+ link: {
222+ ...baseCandidate.link,
223+ ...testCase.link
224+ },
225+ message: {
226+ id: `${baseCandidate.message.id}_${index}`
227+ }
228+ },
229+ now: nowMs,
230+ store: {
231+ async listRenewalJobs() {
232+ assert.fail("route-unavailable branches should short-circuit before duplicate-job lookup");
233+ }
234+ }
235+ });
236+
237+ assert.equal(decision.eligible, false);
238+ assert.equal(decision.reason, "route_unavailable");
239+ assert.equal(decision.routeUnavailableReason, testCase.expectedReason);
240+ }
241+});
242+
243 test("renewal dispatcher sends due pending jobs through browser.proxy_delivery and marks them done", async () => {
244 const rootDir = mkdtempSync(join(tmpdir(), "baa-renewal-dispatcher-success-"));
245 const stateDir = join(rootDir, "state");
1@@ -29,6 +29,12 @@ export type RenewalProjectorSkipReason =
2 | "missing_remote_conversation_id"
3 | "route_unavailable";
4
5+export type RenewalRouteUnavailableReason =
6+ | "inactive_link"
7+ | "missing_tab_id"
8+ | "shell_page"
9+ | "target_kind_not_proxy_delivery";
10+
11 export interface RenewalProjectorCursor extends MessageScanCursor {}
12
13 export interface RenewalProjectorPayload {
14@@ -70,6 +76,7 @@ export interface RenewalShouldRenewDecision {
15 eligible: boolean;
16 existingJobId?: string | null;
17 reason: "eligible" | RenewalProjectorSkipReason;
18+ routeUnavailableReason?: RenewalRouteUnavailableReason;
19 }
20
21 export interface RenewalProjectorRunnerOptions {
22@@ -94,6 +101,11 @@ interface CursorStateValue {
23 observed_at: number;
24 }
25
26+interface RenewalRouteAvailabilityResult {
27+ available: boolean;
28+ reason: RenewalRouteUnavailableReason | null;
29+}
30+
31 export function createRenewalProjectorRunner(
32 options: RenewalProjectorRunnerOptions
33 ): TimedJobRunner {
34@@ -218,7 +230,12 @@ export async function runRenewalProjector(
35 cursor: formatCursor(cursorAfter),
36 existing_job_id: decision.existingJobId ?? null,
37 local_conversation_id: resolution.candidate.conversation.localConversationId,
38- message_id: message.id
39+ message_id: message.id,
40+ ...(decision.routeUnavailableReason == null
41+ ? {}
42+ : {
43+ route_unavailable_reason: decision.routeUnavailableReason
44+ })
45 }
46 });
47 continue;
48@@ -330,10 +347,13 @@ export async function shouldRenew(input: {
49 };
50 }
51
52- if (!hasAvailableRoute(candidate.link)) {
53+ const routeAvailability = hasAvailableRoute(candidate.link);
54+
55+ if (!routeAvailability.available) {
56 return {
57 eligible: false,
58- reason: "route_unavailable"
59+ reason: "route_unavailable",
60+ routeUnavailableReason: routeAvailability.reason ?? undefined
61 };
62 }
63
64@@ -488,13 +508,19 @@ function formatCursor(cursor: RenewalProjectorCursor | null): string | null {
65 return `message:${cursor.observedAt}:${cursor.id}`;
66 }
67
68-function hasAvailableRoute(link: ConversationLinkRecord): boolean {
69+function hasAvailableRoute(link: ConversationLinkRecord): RenewalRouteAvailabilityResult {
70 if (link.isActive !== true) {
71- return false;
72+ return {
73+ available: false,
74+ reason: "inactive_link"
75+ };
76 }
77
78 if (normalizeOptionalString(link.targetKind) !== "browser.proxy_delivery") {
79- return false;
80+ return {
81+ available: false,
82+ reason: "target_kind_not_proxy_delivery"
83+ };
84 }
85
86 const targetPayload = parseJsonRecord(link.targetPayload);
87@@ -504,11 +530,24 @@ function hasAvailableRoute(link: ConversationLinkRecord): boolean {
88 ? targetPayload.tabId
89 : parseTargetTabId(link.targetId);
90
91- if (shellPage || tabId == null) {
92- return false;
93+ if (shellPage) {
94+ return {
95+ available: false,
96+ reason: "shell_page"
97+ };
98 }
99
100- return true;
101+ if (tabId == null) {
102+ return {
103+ available: false,
104+ reason: "missing_tab_id"
105+ };
106+ }
107+
108+ return {
109+ available: true,
110+ reason: null
111+ };
112 }
113
114 function isCursorStateValue(value: unknown): value is CursorStateValue {