- commit
- 943b477
- parent
- 8ac471c
- author
- codex@macbookpro
- date
- 2026-04-01 17:23:21 +0800 CST
feat: confirm proxy delivery downstream status
11 files changed,
+622,
-129
Raw patch view.
1diff --git a/apps/conductor-daemon/src/browser-types.ts b/apps/conductor-daemon/src/browser-types.ts
2index 6da61fe1759ed2da1119acbb6c7b65067b64af06..d7ab05d50e012bd94682779de1fc65b9cf2fd82c 100644
3--- a/apps/conductor-daemon/src/browser-types.ts
4+++ b/apps/conductor-daemon/src/browser-types.ts
5@@ -101,7 +101,16 @@ export interface BrowserBridgeActionResultTargetSnapshot {
6 requested_platform: string | null;
7 }
8
9+export interface BrowserBridgeDeliveryAckSnapshot {
10+ confirmed_at: number | null;
11+ failed: boolean;
12+ level: 0 | 1 | 2 | 3;
13+ reason: string | null;
14+ status_code: number | null;
15+}
16+
17 export interface BrowserBridgeActionResultItemSnapshot {
18+ delivery_ack: BrowserBridgeDeliveryAckSnapshot | null;
19 ok: boolean;
20 platform: string | null;
21 restored: boolean | null;
22diff --git a/apps/conductor-daemon/src/firefox-ws.ts b/apps/conductor-daemon/src/firefox-ws.ts
23index a4a3844b7633d9c90eaba1ea47f2757d30874311..0190a19e5415f42475b68c58f243f59e70299f1b 100644
24--- a/apps/conductor-daemon/src/firefox-ws.ts
25+++ b/apps/conductor-daemon/src/firefox-ws.ts
26@@ -14,6 +14,7 @@ import {
27 type FirefoxBridgeRegisteredClient
28 } from "./firefox-bridge.js";
29 import type {
30+ BrowserBridgeDeliveryAckSnapshot,
31 BrowserBridgeActionResultItemSnapshot,
32 BrowserBridgeActionResultSnapshot,
33 BrowserBridgeActionResultSummarySnapshot,
34@@ -476,6 +477,7 @@ function normalizeActionResultItem(
35 const runtimes = readShellRuntimeArray(record.shell_runtime ?? record.shellRuntime, platform);
36
37 return {
38+ delivery_ack: normalizeDeliveryAck(record.delivery_ack ?? record.deliveryAck),
39 ok: readOptionalBoolean(record, ["ok"]) !== false,
40 platform: platform ?? runtimes[0]?.platform ?? null,
41 restored: readOptionalBoolean(record, ["restored"]),
42@@ -485,6 +487,29 @@ function normalizeActionResultItem(
43 };
44 }
45
46+function normalizeDeliveryAck(value: unknown): BrowserBridgeDeliveryAckSnapshot | null {
47+ const record = asRecord(value);
48+
49+ if (record == null) {
50+ return null;
51+ }
52+
53+ const level = readOptionalInteger(record, ["level"]);
54+ let normalizedLevel: 0 | 1 | 2 | 3 = 0;
55+
56+ if (level === 1 || level === 2 || level === 3) {
57+ normalizedLevel = level;
58+ }
59+
60+ return {
61+ confirmed_at: readOptionalTimestampMilliseconds(record, ["confirmed_at", "confirmedAt"]),
62+ failed: readOptionalBoolean(record, ["failed"]) === true,
63+ level: normalizedLevel,
64+ reason: readFirstString(record, ["reason"]),
65+ status_code: readOptionalInteger(record, ["status_code", "statusCode"])
66+ };
67+}
68+
69 function buildActionResultSummary(
70 record: Record<string, unknown> | null,
71 results: BrowserBridgeActionResultItemSnapshot[],
72diff --git a/apps/conductor-daemon/src/index.test.js b/apps/conductor-daemon/src/index.test.js
73index 3c157909b17255443dd6ea984cc0f92857d320e6..16589f17b4a8868a7b82a536c33d3cff0905683e 100644
74--- a/apps/conductor-daemon/src/index.test.js
75+++ b/apps/conductor-daemon/src/index.test.js
76@@ -1915,6 +1915,7 @@ function sendPluginActionResult(socket, input) {
77 const results =
78 input.results
79 ?? shellRuntime.map((runtime) => ({
80+ delivery_ack: input.deliveryAck ?? null,
81 ok: true,
82 platform: runtime.platform,
83 restored: input.restored ?? false,
84@@ -1953,6 +1954,64 @@ function sendPluginActionResult(socket, input) {
85 );
86 }
87
88+function buildDeliveryAck(statusCode, overrides = {}) {
89+ const resolvedStatusCode = Number.isFinite(Number(statusCode)) ? Number(statusCode) : null;
90+ return {
91+ confirmed_at: overrides.confirmed_at ?? overrides.confirmedAt ?? 1710000000250,
92+ failed: overrides.failed ?? resolvedStatusCode !== 200,
93+ level: overrides.level ?? (resolvedStatusCode == null ? 0 : 1),
94+ reason:
95+ overrides.reason
96+ ?? (resolvedStatusCode != null && resolvedStatusCode !== 200 ? `downstream_status_${resolvedStatusCode}` : null),
97+ status_code: resolvedStatusCode
98+ };
99+}
100+
101+function buildProxyDeliveryActionResult(options = {}) {
102+ const platform = options.platform ?? "claude";
103+ const shellRuntime = options.shell_runtime ?? [buildShellRuntime(platform)];
104+ const deliveryAck = options.deliveryAck ?? null;
105+ const results = options.results ?? shellRuntime.map((entry) => ({
106+ delivery_ack: deliveryAck,
107+ ok: true,
108+ platform: entry.platform,
109+ restored: false,
110+ shell_runtime: entry,
111+ skipped: null,
112+ tab_id: entry.actual.tab_id
113+ }));
114+
115+ return {
116+ accepted: options.accepted ?? true,
117+ action: "proxy_delivery",
118+ completed: options.completed ?? true,
119+ failed: options.failed ?? false,
120+ reason: options.reason ?? null,
121+ received_at: options.receivedAt ?? 1710000000300,
122+ request_id: options.requestId ?? "proxy-delivery-result",
123+ result: {
124+ actual_count: shellRuntime.filter((entry) => entry.actual.exists).length,
125+ desired_count: shellRuntime.filter((entry) => entry.desired.exists).length,
126+ drift_count: shellRuntime.filter((entry) => entry.drift.aligned === false).length,
127+ failed_count: results.filter((entry) => entry.ok === false).length,
128+ ok_count: results.filter((entry) => entry.ok).length,
129+ platform_count: shellRuntime.length,
130+ restored_count: results.filter((entry) => entry.restored === true).length,
131+ skipped_reasons: results.map((entry) => entry.skipped).filter(Boolean)
132+ },
133+ results,
134+ shell_runtime: shellRuntime,
135+ target: {
136+ client_id: options.clientId ?? `firefox-${platform}`,
137+ connection_id: options.connectionId ?? `conn-firefox-${platform}`,
138+ platform,
139+ requested_client_id: options.clientId ?? `firefox-${platform}`,
140+ requested_platform: platform
141+ },
142+ type: "browser.proxy_delivery"
143+ };
144+}
145+
146 function createBrowserBridgeStub() {
147 const calls = [];
148 const buildActionDispatch = ({
149@@ -4003,35 +4062,16 @@ test("renewal dispatcher sends due pending jobs through browser.proxy_delivery a
150 connectionId: "conn-firefox-chatgpt",
151 dispatchedAt: nowMs,
152 requestId: "proxy-dispatch-1",
153- result: Promise.resolve({
154- accepted: true,
155- action: "proxy_delivery",
156- completed: true,
157- failed: false,
158- reason: null,
159- received_at: nowMs + 50,
160- request_id: "proxy-dispatch-1",
161- result: {
162- actual_count: 1,
163- desired_count: 1,
164- drift_count: 0,
165- failed_count: 0,
166- ok_count: 1,
167- platform_count: 1,
168- restored_count: 0,
169- skipped_reasons: []
170- },
171- results: [],
172- shell_runtime: [],
173- target: {
174- client_id: input.clientId || "firefox-chatgpt",
175- connection_id: "conn-firefox-chatgpt",
176- platform: input.platform,
177- requested_client_id: input.clientId || "firefox-chatgpt",
178- requested_platform: input.platform
179- },
180- type: "browser.proxy_delivery"
181- }),
182+ result: Promise.resolve(buildProxyDeliveryActionResult({
183+ clientId: input.clientId || "firefox-chatgpt",
184+ connectionId: "conn-firefox-chatgpt",
185+ deliveryAck: buildDeliveryAck(200, {
186+ confirmed_at: nowMs + 40
187+ }),
188+ platform: input.platform,
189+ receivedAt: nowMs + 50,
190+ requestId: "proxy-dispatch-1"
191+ })),
192 type: "browser.proxy_delivery"
193 };
194 }
195@@ -4118,7 +4158,14 @@ test("renewal dispatcher sends due pending jobs through browser.proxy_delivery a
196 assert.equal(conversation.cooldownUntil, nowMs + 60_000);
197 assert.equal(conversation.updatedAt, nowMs);
198 assert.ok(entries.find((entry) => entry.stage === "job_attempt_started"));
199- assert.ok(entries.find((entry) => entry.stage === "job_completed" && entry.result === "attempt_succeeded"));
200+ assert.ok(
201+ entries.find(
202+ (entry) =>
203+ entry.stage === "job_completed"
204+ && entry.result === "attempt_succeeded"
205+ && entry.details?.downstream_status_code === 200
206+ )
207+ );
208 } finally {
209 artifactStore.close();
210 rmSync(rootDir, {
211@@ -4153,35 +4200,16 @@ test("renewal dispatcher adds inter-job jitter before consecutive dispatches and
212 connectionId: "conn-firefox-chatgpt",
213 dispatchedAt,
214 requestId: `proxy-dispatch-jitter-${dispatchTimes.length}`,
215- result: Promise.resolve({
216- accepted: true,
217- action: "proxy_delivery",
218- completed: true,
219- failed: false,
220- reason: null,
221- received_at: dispatchedAt + 5,
222- request_id: `proxy-dispatch-jitter-${dispatchTimes.length}`,
223- result: {
224- actual_count: 1,
225- desired_count: 1,
226- drift_count: 0,
227- failed_count: 0,
228- ok_count: 1,
229- platform_count: 1,
230- restored_count: 0,
231- skipped_reasons: []
232- },
233- results: [],
234- shell_runtime: [],
235- target: {
236- client_id: input.clientId || "firefox-chatgpt",
237- connection_id: "conn-firefox-chatgpt",
238- platform: input.platform,
239- requested_client_id: input.clientId || "firefox-chatgpt",
240- requested_platform: input.platform
241- },
242- type: "browser.proxy_delivery"
243- }),
244+ result: Promise.resolve(buildProxyDeliveryActionResult({
245+ clientId: input.clientId || "firefox-chatgpt",
246+ connectionId: "conn-firefox-chatgpt",
247+ deliveryAck: buildDeliveryAck(200, {
248+ confirmed_at: dispatchedAt + 4
249+ }),
250+ platform: input.platform,
251+ receivedAt: dispatchedAt + 5,
252+ requestId: `proxy-dispatch-jitter-${dispatchTimes.length}`
253+ })),
254 type: "browser.proxy_delivery"
255 };
256 }
257@@ -4291,6 +4319,162 @@ test("renewal dispatcher adds inter-job jitter before consecutive dispatches and
258 }
259 });
260
261+test("renewal dispatcher classifies downstream proxy_delivery statuses for retry and terminal failure", async () => {
262+ const rootDir = mkdtempSync(join(tmpdir(), "baa-renewal-dispatcher-http-status-"));
263+ const stateDir = join(rootDir, "state");
264+ const artifactStore = new ArtifactStore({
265+ artifactDir: join(stateDir, ARTIFACTS_DIRNAME),
266+ databasePath: join(stateDir, ARTIFACT_DB_FILENAME)
267+ });
268+ const nowMs = Date.UTC(2026, 2, 30, 12, 30, 0);
269+ const browserCalls = [];
270+ const runner = createRenewalDispatcherRunner({
271+ browserBridge: {
272+ proxyDelivery(input) {
273+ browserCalls.push(input);
274+ const statusCode = input.conversationId === "conv_dispatch_http_retry" ? 429 : 401;
275+
276+ return {
277+ clientId: input.clientId || "firefox-chatgpt",
278+ connectionId: "conn-firefox-chatgpt",
279+ dispatchedAt: nowMs,
280+ requestId: `proxy-http-${statusCode}`,
281+ result: Promise.resolve(buildProxyDeliveryActionResult({
282+ clientId: input.clientId || "firefox-chatgpt",
283+ connectionId: "conn-firefox-chatgpt",
284+ deliveryAck: buildDeliveryAck(statusCode, {
285+ confirmed_at: nowMs + 10
286+ }),
287+ platform: input.platform,
288+ receivedAt: nowMs + 25,
289+ requestId: `proxy-http-${statusCode}`
290+ })),
291+ type: "browser.proxy_delivery"
292+ };
293+ }
294+ },
295+ now: () => nowMs,
296+ retryBaseDelayMs: 1,
297+ retryMaxDelayMs: 1
298+ });
299+
300+ try {
301+ const definitions = [
302+ {
303+ conversationId: "conv_dispatch_http_retry",
304+ expectedStatus: "pending",
305+ jobId: "job_dispatch_http_retry",
306+ lastError: "downstream_status_429",
307+ localConversationId: "lc_dispatch_http_retry",
308+ messageId: "msg_dispatch_http_retry",
309+ pageTitle: "HTTP Retry Renewal",
310+ tabId: 41
311+ },
312+ {
313+ conversationId: "conv_dispatch_http_fail",
314+ expectedStatus: "failed",
315+ jobId: "job_dispatch_http_fail",
316+ lastError: "downstream_status_401",
317+ localConversationId: "lc_dispatch_http_fail",
318+ messageId: "msg_dispatch_http_fail",
319+ pageTitle: "HTTP Fail Renewal",
320+ tabId: 42
321+ }
322+ ];
323+
324+ for (const definition of definitions) {
325+ await artifactStore.insertMessage({
326+ conversationId: definition.conversationId,
327+ id: definition.messageId,
328+ observedAt: nowMs - 60_000,
329+ platform: "chatgpt",
330+ rawText: `${definition.conversationId} renewal message`,
331+ role: "assistant"
332+ });
333+ await artifactStore.upsertLocalConversation({
334+ automationStatus: "auto",
335+ localConversationId: definition.localConversationId,
336+ platform: "chatgpt",
337+ updatedAt: nowMs - 30_000
338+ });
339+ await artifactStore.upsertConversationLink({
340+ clientId: "firefox-chatgpt",
341+ linkId: `link_${definition.localConversationId}`,
342+ localConversationId: definition.localConversationId,
343+ observedAt: nowMs - 30_000,
344+ pageTitle: definition.pageTitle,
345+ pageUrl: `https://chatgpt.com/c/${definition.conversationId}`,
346+ platform: "chatgpt",
347+ remoteConversationId: definition.conversationId,
348+ routeParams: {
349+ conversationId: definition.conversationId
350+ },
351+ routePath: `/c/${definition.conversationId}`,
352+ routePattern: "/c/:conversationId",
353+ targetId: `tab:${definition.tabId}`,
354+ targetKind: "browser.proxy_delivery",
355+ targetPayload: {
356+ clientId: "firefox-chatgpt",
357+ conversationId: definition.conversationId,
358+ pageUrl: `https://chatgpt.com/c/${definition.conversationId}`,
359+ tabId: definition.tabId
360+ }
361+ });
362+ await artifactStore.insertRenewalJob({
363+ jobId: definition.jobId,
364+ localConversationId: definition.localConversationId,
365+ maxAttempts: 2,
366+ messageId: definition.messageId,
367+ nextAttemptAt: nowMs,
368+ payload: "[renewal] http confirmation",
369+ payloadKind: "text"
370+ });
371+ }
372+
373+ const { context, entries } = createTimedJobRunnerContext({
374+ artifactStore
375+ });
376+ const result = await runner.run(context);
377+ const retryJob = await artifactStore.getRenewalJob("job_dispatch_http_retry");
378+ const failedJob = await artifactStore.getRenewalJob("job_dispatch_http_fail");
379+
380+ assert.equal(result.result, "ok");
381+ assert.equal(result.details.retried_jobs, 1);
382+ assert.equal(result.details.failed_jobs, 1);
383+ assert.equal(browserCalls.length, 2);
384+ assert.equal(retryJob.status, "pending");
385+ assert.equal(retryJob.attemptCount, 1);
386+ assert.equal(retryJob.lastError, "downstream_status_429");
387+ assert.equal(retryJob.nextAttemptAt, nowMs + 1);
388+ assert.equal(failedJob.status, "failed");
389+ assert.equal(failedJob.attemptCount, 1);
390+ assert.equal(failedJob.lastError, "downstream_status_401");
391+ assert.equal(failedJob.nextAttemptAt, null);
392+ assert.ok(
393+ entries.find(
394+ (entry) =>
395+ entry.stage === "job_retry_scheduled"
396+ && entry.result === "downstream_status_429"
397+ && entry.details?.downstream_status_code === 429
398+ )
399+ );
400+ assert.ok(
401+ entries.find(
402+ (entry) =>
403+ entry.stage === "job_failed"
404+ && entry.result === "downstream_status_401"
405+ && entry.details?.downstream_status_code === 401
406+ )
407+ );
408+ } finally {
409+ artifactStore.close();
410+ rmSync(rootDir, {
411+ force: true,
412+ recursive: true
413+ });
414+ }
415+});
416+
417 test("renewal dispatcher success writes cooldownUntil and blocks projector from projecting follow-up messages during cooldown", async () => {
418 const rootDir = mkdtempSync(join(tmpdir(), "baa-renewal-dispatcher-cooldown-chain-"));
419 const stateDir = join(rootDir, "state");
420@@ -4316,35 +4500,16 @@ test("renewal dispatcher success writes cooldownUntil and blocks projector from
421 connectionId: "conn-firefox-claude",
422 dispatchedAt: nowMs,
423 requestId: "proxy-chain-1",
424- result: Promise.resolve({
425- accepted: true,
426- action: "proxy_delivery",
427- completed: true,
428- failed: false,
429- reason: null,
430- received_at: nowMs + 50,
431- request_id: "proxy-chain-1",
432- result: {
433- actual_count: 1,
434- desired_count: 1,
435- drift_count: 0,
436- failed_count: 0,
437- ok_count: 1,
438- platform_count: 1,
439- restored_count: 0,
440- skipped_reasons: []
441- },
442- results: [],
443- shell_runtime: [],
444- target: {
445- client_id: input.clientId || "firefox-claude",
446- connection_id: "conn-firefox-claude",
447- platform: input.platform,
448- requested_client_id: input.clientId || "firefox-claude",
449- requested_platform: input.platform
450- },
451- type: "browser.proxy_delivery"
452- }),
453+ result: Promise.resolve(buildProxyDeliveryActionResult({
454+ clientId: input.clientId || "firefox-claude",
455+ connectionId: "conn-firefox-claude",
456+ deliveryAck: buildDeliveryAck(200, {
457+ confirmed_at: nowMs + 40
458+ }),
459+ platform: input.platform,
460+ receivedAt: nowMs + 50,
461+ requestId: "proxy-chain-1"
462+ })),
463 type: "browser.proxy_delivery"
464 };
465 }
466diff --git a/apps/conductor-daemon/src/local-api.ts b/apps/conductor-daemon/src/local-api.ts
467index 6a38fb96e58e810295d62436a914fd549b09a4e2..7b22f74ca768d50324d67014faccc2b9183925c0 100644
468--- a/apps/conductor-daemon/src/local-api.ts
469+++ b/apps/conductor-daemon/src/local-api.ts
470@@ -2945,6 +2945,16 @@ function serializeBrowserActionResultItemSnapshot(
471 snapshot: BrowserBridgeActionResultItemSnapshot
472 ): JsonObject {
473 return compactJsonObject({
474+ delivery_ack:
475+ snapshot.delivery_ack == null
476+ ? undefined
477+ : compactJsonObject({
478+ confirmed_at: snapshot.delivery_ack.confirmed_at ?? undefined,
479+ failed: snapshot.delivery_ack.failed,
480+ level: snapshot.delivery_ack.level,
481+ reason: snapshot.delivery_ack.reason ?? undefined,
482+ status_code: snapshot.delivery_ack.status_code ?? undefined
483+ }),
484 ok: snapshot.ok,
485 platform: snapshot.platform ?? undefined,
486 restored: snapshot.restored ?? undefined,
487diff --git a/apps/conductor-daemon/src/renewal/dispatcher.ts b/apps/conductor-daemon/src/renewal/dispatcher.ts
488index 884ff0cb865f74f19eed0ef8c9b09082b4e0f2c6..71bf8ae30bc4c972ba6ecd6b37a0d32d3090c583 100644
489--- a/apps/conductor-daemon/src/renewal/dispatcher.ts
490+++ b/apps/conductor-daemon/src/renewal/dispatcher.ts
491@@ -11,7 +11,8 @@ import type {
492 import type {
493 BrowserBridgeActionDispatch,
494 BrowserBridgeActionResultSnapshot,
495- BrowserBridgeController
496+ BrowserBridgeController,
497+ BrowserBridgeDeliveryAckSnapshot
498 } from "../browser-types.js";
499 import type { TimedJobRunner, TimedJobRunnerResult, TimedJobTickContext } from "../timed-jobs/index.js";
500 import { DEFAULT_RENEWAL_EXECUTION_TIMEOUT_MS } from "../execution-timeouts.js";
501@@ -82,16 +83,28 @@ interface RenewalDispatchContext {
502
503 interface RenewalDispatchOutcome {
504 dispatch: BrowserBridgeActionDispatch;
505+ deliveryAck: BrowserBridgeDeliveryAckSnapshot;
506 result: BrowserBridgeActionResultSnapshot;
507 }
508
509 interface RenewalExecutionFailure {
510+ deliveryAck: BrowserBridgeDeliveryAckSnapshot | null;
511 errorCode: string | null;
512 message: string;
513 result: string;
514 timeoutMs: number | null;
515 }
516
517+class RenewalProxyDeliveryError extends Error {
518+ readonly deliveryAck: BrowserBridgeDeliveryAckSnapshot | null;
519+
520+ constructor(message: string, deliveryAck: BrowserBridgeDeliveryAckSnapshot | null = null) {
521+ super(message);
522+ this.name = "RenewalProxyDeliveryError";
523+ this.deliveryAck = deliveryAck;
524+ }
525+}
526+
527 interface RenewalDispatcherRunnerOptions {
528 browserBridge: BrowserBridgeController | null;
529 executionTimeoutMs?: number;
530@@ -453,10 +466,13 @@ export async function runRenewalDispatcher(
531 attempt_count: attemptCount,
532 client_id: delivery.dispatch.clientId,
533 connection_id: delivery.dispatch.connectionId,
534+ downstream_delivery_level: delivery.deliveryAck.level,
535+ downstream_status_code: delivery.deliveryAck.status_code,
536 job_id: job.jobId,
537 message_id: job.messageId,
538 proxy_request_id: delivery.dispatch.requestId,
539- received_at: delivery.result.received_at
540+ received_at: delivery.result.received_at,
541+ status_confirmed_at: delivery.deliveryAck.confirmed_at
542 }
543 });
544 } catch (error) {
545@@ -487,6 +503,9 @@ export async function runRenewalDispatcher(
546 result: failureResult.result,
547 details: {
548 attempt_count: attempts,
549+ downstream_delivery_level: failure.deliveryAck?.level ?? undefined,
550+ downstream_reason: failure.deliveryAck?.reason ?? undefined,
551+ downstream_status_code: failure.deliveryAck?.status_code ?? undefined,
552 error_code: failure.errorCode,
553 error_message: failure.message,
554 job_id: job.jobId,
555@@ -501,6 +520,9 @@ export async function runRenewalDispatcher(
556 result: failureResult.result,
557 details: {
558 attempt_count: attempts,
559+ downstream_delivery_level: failure.deliveryAck?.level ?? undefined,
560+ downstream_reason: failure.deliveryAck?.reason ?? undefined,
561+ downstream_status_code: failure.deliveryAck?.status_code ?? undefined,
562 error_code: failure.errorCode,
563 error_message: failure.message,
564 job_id: job.jobId,
565@@ -607,17 +629,53 @@ async function dispatchRenewalJob(
566 timeoutMs: input.timeoutMs
567 });
568 const result = await dispatch.result;
569+ const deliveryAck = resolveRenewalDeliveryAck(result);
570
571 if (result.accepted !== true || result.failed === true) {
572- throw new Error(normalizeOptionalString(result.reason) ?? "browser proxy delivery failed");
573+ throw new RenewalProxyDeliveryError(
574+ normalizeOptionalString(result.reason) ?? "browser proxy delivery failed",
575+ deliveryAck
576+ );
577+ }
578+
579+ if (deliveryAck == null) {
580+ throw new RenewalProxyDeliveryError("downstream_status_missing");
581+ }
582+
583+ if (deliveryAck.level < 1 || deliveryAck.status_code == null || deliveryAck.failed === true || deliveryAck.status_code !== 200) {
584+ throw new RenewalProxyDeliveryError(
585+ buildRenewalDeliveryFailureResult(deliveryAck),
586+ deliveryAck
587+ );
588 }
589
590 return {
591 dispatch,
592+ deliveryAck,
593 result
594 };
595 }
596
597+function resolveRenewalDeliveryAck(
598+ result: BrowserBridgeActionResultSnapshot
599+): BrowserBridgeDeliveryAckSnapshot | null {
600+ for (const item of result.results) {
601+ if (item.delivery_ack != null) {
602+ return item.delivery_ack;
603+ }
604+ }
605+
606+ return null;
607+}
608+
609+function buildRenewalDeliveryFailureResult(deliveryAck: BrowserBridgeDeliveryAckSnapshot): string {
610+ if (deliveryAck.status_code != null) {
611+ return `downstream_status_${deliveryAck.status_code}`;
612+ }
613+
614+ return normalizeOptionalString(deliveryAck.reason) ?? "downstream_status_missing";
615+}
616+
617 async function applyFailureOutcome(
618 artifactStore: Pick<ArtifactStore, "updateRenewalJob">,
619 job: RenewalJobRecord,
620@@ -932,6 +990,12 @@ function resolveJobLogPath(existing: string | null, logDir: string | null, now:
621 }
622
623 function isRetryableFailure(message: string): boolean {
624+ const downstreamStatusCode = parseDownstreamStatusCode(message);
625+
626+ if (downstreamStatusCode != null) {
627+ return downstreamStatusCode === 429 || downstreamStatusCode >= 500;
628+ }
629+
630 return ![
631 "invalid_payload",
632 "missing_local_conversation",
633@@ -939,6 +1003,16 @@ function isRetryableFailure(message: string): boolean {
634 ].includes(message);
635 }
636
637+function parseDownstreamStatusCode(message: string): number | null {
638+ const match = /^downstream_status_(\d{3})$/u.exec(message.trim());
639+
640+ if (match == null) {
641+ return null;
642+ }
643+
644+ return Number(match[1]);
645+}
646+
647 function isPlainRecord(value: unknown): value is Record<string, unknown> {
648 return typeof value === "object" && value != null && !Array.isArray(value);
649 }
650@@ -1081,7 +1155,32 @@ function readErrorTimeoutMs(error: unknown): number | null {
651 : null;
652 }
653
654+function readErrorDeliveryAck(error: unknown): BrowserBridgeDeliveryAckSnapshot | null {
655+ if (!isPlainRecord(error)) {
656+ return null;
657+ }
658+
659+ const candidate = error.deliveryAck;
660+
661+ if (!isPlainRecord(candidate)) {
662+ return null;
663+ }
664+
665+ const level = candidate.level;
666+ const statusCode = candidate.status_code;
667+ const confirmedAt = candidate.confirmed_at;
668+
669+ return {
670+ confirmed_at: typeof confirmedAt === "number" && Number.isFinite(confirmedAt) ? confirmedAt : null,
671+ failed: candidate.failed === true,
672+ level: level === 1 || level === 2 || level === 3 ? level : 0,
673+ reason: typeof candidate.reason === "string" ? candidate.reason : null,
674+ status_code: typeof statusCode === "number" && Number.isFinite(statusCode) ? statusCode : null
675+ };
676+}
677+
678 function normalizeExecutionFailure(error: unknown): RenewalExecutionFailure {
679+ const deliveryAck = readErrorDeliveryAck(error);
680 const errorCode = readErrorCode(error);
681 const timeoutMs = readErrorTimeoutMs(error);
682 const message = toErrorMessage(error);
683@@ -1089,6 +1188,7 @@ function normalizeExecutionFailure(error: unknown): RenewalExecutionFailure {
684 switch (errorCode) {
685 case "action_timeout":
686 return {
687+ deliveryAck,
688 errorCode,
689 message,
690 result: "browser_action_timeout",
691@@ -1096,6 +1196,7 @@ function normalizeExecutionFailure(error: unknown): RenewalExecutionFailure {
692 };
693 case "request_timeout":
694 return {
695+ deliveryAck,
696 errorCode,
697 message,
698 result: "browser_request_timeout",
699@@ -1103,6 +1204,7 @@ function normalizeExecutionFailure(error: unknown): RenewalExecutionFailure {
700 };
701 default:
702 return {
703+ deliveryAck,
704 errorCode,
705 message,
706 result: message,
707diff --git a/docs/api/control-interfaces.md b/docs/api/control-interfaces.md
708index b18c57729af3437ea30188d40a801de0c95843d3..c74587ed93bf5d74fecc5e2c8d19eebcf4c4b7fb 100644
709--- a/docs/api/control-interfaces.md
710+++ b/docs/api/control-interfaces.md
711@@ -101,6 +101,7 @@ browser/plugin 管理约定:
712 - 如果没有活跃 Firefox bridge client,会返回 `503`
713 - 如果指定了不存在的 `clientId`,会返回 `409`
714 - `POST /v1/browser/actions` 会等待插件回传结构化 `action_result`,返回 `accepted` / `completed` / `failed` / `reason` / `target` / `result` / `shell_runtime`
715+- 对 `browser.proxy_delivery` 一类动作,`action_result.results[*]` 现在可额外带 `delivery_ack`,用于表达下游确认层级;首版已稳定提供 Level 1 `status_code`
716 - `GET /v1/browser` 会同步暴露当前 `shell_runtime` 和每个 client 最近一次结构化 `action_result`
717 - `GET /v1/browser` 的 `policy` 视图也会带 `stale_lease` 默认阈值,以及 target 级别的 `last_activity_*`、`stale_sweep_count`、`last_stale_sweep_*` 诊断字段
718 - browser request 的限流窗口、退避和熔断状态现在会持久化到 `artifact.db`;daemon 重启后 `GET /v1/browser` 会恢复上次的风险控制位置,`in_flight / waiting` 仍只代表当前进程内的运行态
719diff --git a/docs/api/firefox-local-ws.md b/docs/api/firefox-local-ws.md
720index 597c8501acb5006b58aa56d3c89e8ef9ae52680d..53b4895930b48e84c1a378ab2525cb287eb20e3e 100644
721--- a/docs/api/firefox-local-ws.md
722+++ b/docs/api/firefox-local-ws.md
723@@ -240,6 +240,13 @@ WS 地址直接由 `BAA_CONDUCTOR_LOCAL_API` 派生,不单独引入新的环
724 {
725 "platform": "claude",
726 "ok": true,
727+ "delivery_ack": {
728+ "level": 1,
729+ "status_code": 200,
730+ "failed": false,
731+ "reason": null,
732+ "confirmed_at": 1760000012500
733+ },
734 "restored": true,
735 "tab_id": 321,
736 "shell_runtime": {
737@@ -264,6 +271,7 @@ WS 地址直接由 `BAA_CONDUCTOR_LOCAL_API` 派生,不单独引入新的环
738 补充:
739
740 - `browser.inject_message` / `browser.send_message` 这类 delivery 动作也会复用同一结构化 `action_result`
741+- `browser.proxy_delivery` 现在会在 `results[*].delivery_ack` 里补充下游确认层级;首版固定回传 Level 1 HTTP 状态码,不在 proxy 内等待完整 SSE 结束
742 - 当插件侧 delivery adapter fail-closed 时,`reason` 会带稳定前缀 `delivery.<code>:`,例如 `delivery.page_not_ready:`、`delivery.selector_missing:`、`delivery.send_not_confirmed:`
743 - 这类失败表示浏览器没有确认 inject / send 已完成,server 不应把该轮交付误记为成功
744
745diff --git a/plans/STATUS_SUMMARY.md b/plans/STATUS_SUMMARY.md
746index c0325ea4407816e793b6b0591b1489ebd70775ae..e8b11b46457647a62ade7fdd3141fa9bd1344cc1 100644
747--- a/plans/STATUS_SUMMARY.md
748+++ b/plans/STATUS_SUMMARY.md
749@@ -51,6 +51,7 @@
750 - BAA normalize / parse 现在按 block 做错误隔离,单个坏 block 不再中断整批合法指令
751 - timed-jobs JSONL 日志现在已改为异步写入,减少 tick 周期内的同步 IO 阻塞
752 - browser request 风控状态现在会持久化到 `artifact.db`,重启后会恢复限流/退避/熔断窗口,并回收遗留执行锁与 `running` renewal job
753+ - `proxy_delivery` 结果现在会异步补齐 Level 1 下游 HTTP 状态码;renewal dispatcher 仅在 `200` 时标记 `done`,`429/5xx` 改为 `retry`
754
755 ## 当前已纠正的文档/代码不一致
756
757@@ -71,30 +72,27 @@
758
759 **当前下一波任务:**
760
761-1. `T-S069`:proxy_delivery 成功语义增强
762-2. `T-S068`:ChatGPT proxy send 冷启动降级保护
763-3. `T-S065`:policy 配置化
764-4. `T-S067`:Gemini 正式接入 raw relay 支持面
765-5. `OPT-004`:Claude final-message 更稳 fallback
766-6. `OPT-009`:renewal 模块重复工具函数抽取
767+1. `T-S068`:ChatGPT proxy send 冷启动降级保护
768+2. `T-S065`:policy 配置化
769+3. `T-S067`:Gemini 正式接入 raw relay 支持面
770+4. `OPT-004`:Claude final-message 更稳 fallback
771+5. `OPT-009`:renewal 模块重复工具函数抽取
772
773 并行需要持续关注:
774
775-- `T-S068`、`T-S069` 都会碰 delivery 路径,尽量不要并行修改同一批文件
776+- `T-S068` 继续会碰 delivery 路径,尽量避免与同批 delivery 收口项并行改同一批文件
777
778 **并行优化项:**
779
780-1. `T-S069`
781- 为 proxy_delivery 补齐下游 HTTP 状态码回传,提升成功语义正确性
782-2. `T-S068`
783+1. `T-S068`
784 给 ChatGPT proxy send 补冷启动保护,避免插件重载后首批 delivery 直接失败
785-3. `T-S065`
786+2. `T-S065`
787 让 policy 白名单配置化,为后续 automation control 指令扩面铺路
788-4. `T-S067`
789+3. `T-S067`
790 把 Gemini 提升到正式 raw relay 支持面,减少 helper/proxy mix 带来的脆弱性
791-5. `OPT-004`
792+4. `OPT-004`
793 为 Claude final-message 增加更稳的 SSE fallback
794-6. `OPT-009`
795+5. `OPT-009`
796 renewal 模块重复工具函数抽取,减少重复逻辑
797
798 **已关闭的优化项:**
799@@ -147,8 +145,8 @@ Phase 1(浏览器主链)、Artifact 静态服务,以及 timed-jobs + 续
800 - `Gemini` 当前仍不是 `/v1/browser/request` 的正式 raw relay 支持面;`@browser.gemini` 走 helper / proxy mix,仍需依赖最近观测到的真实请求上下文
801 - ChatGPT proxy send 仍依赖最近捕获的真实发送模板;如果 controller 刚重载且还没观察到真实发送,会退回同页 DOM fallback
802 - Claude 的 `organizationId` 当前仍依赖最近观测到的 org 上下文,不是完整的多页多 org 精确映射
803-- `proxy_delivery` 当前的成功语义是”请求已派发到目标页面上下文”,不是”下游 AI 已完整回复”
804+- `proxy_delivery` 当前已补齐 Level 1 HTTP 接受确认,但仍不是“下游 AI 已完整回复”;Level 3 仍依赖后续 final-message 链路
805 - ChatGPT root message / mapping 结构如果后续变化,final-message 提取启发式仍需跟进
806 - recent relay cache 是有限窗口;极老 replay 超出窗口后,仍会落回 conductor dedupe
807 - `status-api` 继续保留为显式 opt-in 兼容层,不是当前删除重点
808-- 以上几项风险现已分别拆成 `T-S067`、`T-S068`、`T-S069` 跟踪
809+- 以上几项风险现主要拆成 `T-S067`、`T-S068` 跟踪;`T-S069` 已完成首版 Level 1 收口
810diff --git a/plugins/baa-firefox/controller.js b/plugins/baa-firefox/controller.js
811index 8dc04981ecf13bd39e3f015a66046d40fd998e24..6b315e7be86432426ee34022910651b6e0fac98c 100644
812--- a/plugins/baa-firefox/controller.js
813+++ b/plugins/baa-firefox/controller.js
814@@ -4395,6 +4395,41 @@ function collectPluginActionShellRuntime(results = [], platform = null) {
815 );
816 }
817
818+function normalizeDeliveryAckLevel(value) {
819+ const numeric = Number(value);
820+
821+ if (!Number.isInteger(numeric)) {
822+ return 0;
823+ }
824+
825+ return Math.min(3, Math.max(0, numeric));
826+}
827+
828+function normalizePluginDeliveryAck(value) {
829+ if (!isRecord(value)) {
830+ return null;
831+ }
832+
833+ const confirmedAt = Number.isFinite(Number(value.confirmedAt))
834+ ? Number(value.confirmedAt)
835+ : Number.isFinite(Number(value.confirmed_at))
836+ ? Number(value.confirmed_at)
837+ : null;
838+ const statusCode = Number.isFinite(Number(value.statusCode))
839+ ? Number(value.statusCode)
840+ : Number.isFinite(Number(value.status_code))
841+ ? Number(value.status_code)
842+ : null;
843+
844+ return {
845+ confirmed_at: confirmedAt,
846+ failed: value.failed === true,
847+ level: normalizeDeliveryAckLevel(value.level),
848+ reason: trimToNull(value.reason) || null,
849+ status_code: statusCode
850+ };
851+}
852+
853 function buildPluginActionResultPayload(actionResult, options = {}) {
854 const requestId = trimToNull(options.requestId) || trimToNull(actionResult?.requestId);
855 if (!requestId) return null;
856@@ -4411,8 +4446,10 @@ function buildPluginActionResultPayload(actionResult, options = {}) {
857 : targetPlatform
858 ? buildPlatformRuntimeSnapshot(targetPlatform)
859 : null;
860+ const deliveryAck = normalizePluginDeliveryAck(entry?.delivery_ack || entry?.deliveryAck);
861
862 return {
863+ delivery_ack: deliveryAck,
864 ok: entry?.ok !== false,
865 platform: targetPlatform,
866 restored: typeof entry?.restored === "boolean" ? entry.restored : null,
867@@ -4833,14 +4870,33 @@ function buildGeminiAutoRequest(prompt) {
868 }
869
870 function createPendingProxyRequest(id, meta = {}) {
871+ let ackSettled = false;
872 let settled = false;
873+ let ackResolveFn = null;
874+ let ackRejectFn = null;
875 let timer = null;
876 let resolveFn = null;
877 let rejectFn = null;
878+ const ackResponse = new Promise((resolve, reject) => {
879+ ackResolveFn = resolve;
880+ ackRejectFn = reject;
881+ });
882 const response = new Promise((resolve, reject) => {
883 resolveFn = resolve;
884 rejectFn = reject;
885 });
886+ void ackResponse.catch(() => {});
887+ void response.catch(() => {});
888+
889+ const finishAck = (callback, value) => {
890+ if (ackSettled) return false;
891+ ackSettled = true;
892+ callback(value);
893+ return true;
894+ };
895+
896+ const normalizePendingError = (error) =>
897+ error instanceof Error ? error : new Error(String(error || "proxy_failed"));
898
899 const finish = (callback, value) => {
900 if (settled) return false;
901@@ -4851,20 +4907,34 @@ function createPendingProxyRequest(id, meta = {}) {
902 return true;
903 };
904
905+ const rejectAll = (error) => {
906+ const nextError = normalizePendingError(error);
907+ finishAck(ackRejectFn, nextError);
908+ return finish(rejectFn, nextError);
909+ };
910+
911 timer = setTimeout(() => {
912- finish(rejectFn, new Error(`${platformLabel(meta.platform || "claude")} 代理超时`));
913+ rejectAll(new Error(`${platformLabel(meta.platform || "claude")} 代理超时`));
914 }, Math.max(1_000, Number(meta.timeoutMs) || PROXY_REQUEST_TIMEOUT));
915
916 const entry = {
917 id,
918 ...meta,
919+ ackResponse,
920 response,
921 resolve(value) {
922+ finishAck(ackResolveFn, value);
923 return finish(resolveFn, value);
924 },
925+ resolveAck(value) {
926+ return finishAck(ackResolveFn, value);
927+ },
928 reject(error) {
929- const nextError = error instanceof Error ? error : new Error(String(error || "proxy_failed"));
930- return finish(rejectFn, nextError);
931+ return rejectAll(error);
932+ },
933+ rejectAck(error) {
934+ const nextError = normalizePendingError(error);
935+ return finishAck(ackRejectFn, nextError);
936 }
937 };
938
939@@ -6555,6 +6625,14 @@ function handlePageSse(data, sender) {
940 if (!pending.streamOpened || data.open === true) {
941 pending.streamOpened = true;
942 pending.streamId = streamId;
943+ pending.resolveAck({
944+ error: status != null && status >= 400 ? `upstream_status_${status}` : null,
945+ id: pending.id,
946+ method: data.method || pending.method || null,
947+ ok: status != null ? status < 400 : data.error == null,
948+ status,
949+ url: data.url || pending.path || null
950+ });
951 wsSend({
952 type: "stream_open",
953 id: pending.id,
954@@ -6605,6 +6683,20 @@ function handlePageSse(data, sender) {
955 }
956
957 if (data.error) {
958+ if (!pending.streamOpened) {
959+ if (status != null) {
960+ pending.resolveAck({
961+ error: data.error,
962+ id: pending.id,
963+ method: data.method || pending.method || null,
964+ ok: false,
965+ status,
966+ url: data.url || pending.path || null
967+ });
968+ } else {
969+ pending.rejectAck(new Error(data.error));
970+ }
971+ }
972 const streamError = new Error(data.error);
973 streamError.streamReported = true;
974 wsSend({
975@@ -6684,6 +6776,15 @@ function handlePageProxyResponse(data, sender) {
976 }
977
978 if (pending.responseMode === "sse" && (data.error || (Number.isFinite(data.status) && data.status >= 400))) {
979+ pending.resolveAck({
980+ body: typeof data.body === "string" ? data.body : (data.body == null ? null : JSON.stringify(data.body)),
981+ error: data.error || `upstream_status_${data.status}`,
982+ id: data.id,
983+ method: data.method || pending.method || null,
984+ ok: false,
985+ status: Number.isFinite(data.status) ? data.status : null,
986+ url: data.url || pending.path || null
987+ });
988 const message = data.error || `upstream_status_${data.status}`;
989 const streamError = new Error(message);
990 streamError.streamReported = true;
991@@ -6699,6 +6800,15 @@ function handlePageProxyResponse(data, sender) {
992 return;
993 }
994
995+ pending.resolveAck({
996+ body: typeof data.body === "string" ? data.body : (data.body == null ? null : JSON.stringify(data.body)),
997+ error: data.error || null,
998+ id: data.id,
999+ method: data.method || pending.method || null,
1000+ ok: data.ok !== false && !data.error,
1001+ status: Number.isFinite(data.status) ? data.status : null,
1002+ url: data.url || pending.path || null
1003+ });
1004 pending.resolve({
1005 id: data.id,
1006 ok: data.ok !== false && !data.error,
1007@@ -7103,6 +7213,9 @@ function buildClaudeDeliveryProxyRequest(message, target) {
1008 async function runProxyDeliveryAction(message) {
1009 const platform = trimToNull(message?.platform);
1010 const planId = trimToNull(message?.plan_id || message?.planId);
1011+ const timeoutMs = Number.isFinite(Number(message?.timeout_ms || message?.timeoutMs))
1012+ ? Number(message?.timeout_ms || message?.timeoutMs)
1013+ : PROXY_REQUEST_TIMEOUT;
1014
1015 if (!platform) {
1016 throw new Error("proxy_delivery 缺少 platform");
1017@@ -7127,20 +7240,63 @@ async function runProxyDeliveryAction(message) {
1018 messageText: message?.message_text || message?.messageText,
1019 sourceAssistantMessageId: message?.assistant_message_id || message?.assistantMessageId
1020 });
1021-
1022- await postProxyRequestToTab(target.tab.id, {
1023- id: `${planId}:proxy_delivery`,
1024- ...request,
1025+ const proxyRequestId = `${planId}:proxy_delivery`;
1026+ const pending = createPendingProxyRequest(proxyRequestId, {
1027+ method: request.method,
1028+ path: request.path,
1029 platform,
1030- response_mode: "sse",
1031- source: "proxy_delivery"
1032+ responseMode: "sse",
1033+ tabId: target.tab.id,
1034+ timeoutMs
1035 });
1036
1037+ try {
1038+ await postProxyRequestToTab(target.tab.id, {
1039+ id: proxyRequestId,
1040+ ...request,
1041+ platform,
1042+ response_mode: "sse",
1043+ source: "proxy_delivery"
1044+ });
1045+ } catch (error) {
1046+ pending.reject(error);
1047+ throw error;
1048+ }
1049+
1050+ const deliveryAck = await pending.ackResponse.then(
1051+ (response) => {
1052+ const statusCode = Number.isFinite(Number(response?.status)) ? Number(response.status) : null;
1053+ const reason = trimToNull(response?.error)
1054+ || (statusCode != null && statusCode !== 200 ? `downstream_status_${statusCode}` : null);
1055+
1056+ return {
1057+ confirmed_at: Date.now(),
1058+ failed: reason != null || statusCode !== 200,
1059+ level: statusCode == null ? 0 : 1,
1060+ reason,
1061+ status_code: statusCode
1062+ };
1063+ },
1064+ (error) => ({
1065+ confirmed_at: Date.now(),
1066+ failed: true,
1067+ level: 0,
1068+ reason: trimToNull(error?.message) || String(error || "downstream_ack_failed"),
1069+ status_code: Number.isFinite(Number(error?.statusCode)) ? Number(error.statusCode) : null
1070+ })
1071+ );
1072+ addLog(
1073+ deliveryAck.failed ? "warn" : "info",
1074+ `proxy_delivery 下游确认:${platformLabel(platform)} status=${deliveryAck.status_code ?? "-"} level=${deliveryAck.level} reason=${deliveryAck.reason || "-"}`,
1075+ false
1076+ );
1077+
1078 return {
1079 action: "proxy_delivery",
1080 platform,
1081 results: [
1082 {
1083+ delivery_ack: deliveryAck,
1084 ok: true,
1085 platform,
1086 shell_runtime: buildDeliveryShellRuntime(platform),
1087diff --git a/tasks/T-S069.md b/tasks/T-S069.md
1088index 7461c5619a6f38949dd95b6d252bc8f9496b8281..ebb066e8db1bfc6cfc4fa6dfda54b34ff4b70db9 100644
1089--- a/tasks/T-S069.md
1090+++ b/tasks/T-S069.md
1091@@ -2,10 +2,10 @@
1092
1093 ## 状态
1094
1095-- 当前状态:`待开始`
1096+- 当前状态:`已完成`
1097 - 规模预估:`L`
1098 - 依赖任务:`T-S060`
1099-- 建议执行者:`Claude / Codex`
1100+- 建议执行者:`Codex`
1101
1102 ## 直接给对话的提示词
1103
1104@@ -85,22 +85,41 @@
1105
1106 ### 开始执行
1107
1108-- 执行者:
1109-- 开始时间:
1110+- 执行者:`Codex`
1111+- 开始时间:`2026-04-01 16:56:18 CST`
1112 - 状态变更:`待开始` → `进行中`
1113
1114 ### 完成摘要
1115
1116-- 完成时间:
1117+- 完成时间:`2026-04-01 17:21:11 CST`
1118 - 状态变更:`进行中` → `已完成`
1119 - 修改了哪些文件:
1120+ - `plugins/baa-firefox/controller.js`
1121+ - `apps/conductor-daemon/src/browser-types.ts`
1122+ - `apps/conductor-daemon/src/firefox-ws.ts`
1123+ - `apps/conductor-daemon/src/local-api.ts`
1124+ - `apps/conductor-daemon/src/renewal/dispatcher.ts`
1125+ - `apps/conductor-daemon/src/index.test.js`
1126+ - `docs/api/control-interfaces.md`
1127+ - `docs/api/firefox-local-ws.md`
1128+ - `tasks/T-S069.md`
1129+ - `tasks/TASK_OVERVIEW.md`
1130+ - `plans/STATUS_SUMMARY.md`
1131 - 核心实现思路:
1132+ - 在 Firefox 插件侧给 `proxy_delivery` 增加异步下游确认跟踪,不阻塞请求派发;等拿到 HTTP 响应头后,把 `delivery_ack.level/status_code` 回填到结构化 `action_result.results[*]`
1133+ - 在 daemon 侧扩展 `action_result` 数据模型与读面序列化,为后续 Level 2/3 预留 `delivery_ack.level` 扩展位
1134+ - renewal dispatcher 改为基于 `delivery_ack` 判定结果:`200 -> done`,`429/5xx -> retry`,其他非 `200` 下游状态记为失败,并在日志里显式写出下游状态码
1135 - 跑了哪些测试:
1136+ - `pnpm install`
1137+ - `node --check plugins/baa-firefox/controller.js`
1138+ - `pnpm -C apps/conductor-daemon build`
1139+ - `pnpm -C apps/conductor-daemon test`
1140
1141 ### 执行过程中遇到的问题
1142
1143--
1144+- 新 worktree 初始没有 `node_modules`,先执行了 `pnpm install` 后才可跑 `tsc` 和 `node --test`
1145
1146 ### 剩余风险
1147
1148--
1149+- 本轮只实现到 Level 1(HTTP 状态码确认);Level 2/3 的“流已开始 / 完整回复已结束”仍需后续复用 SSE / final-message 链路继续补齐
1150+- renewal dispatcher 已消费 `delivery_ack`,但其他仍只看 proxy 派发层语义的调用方若要升级到“下游已接受”或“完整回复完成”,还需要后续分别接入
1151diff --git a/tasks/TASK_OVERVIEW.md b/tasks/TASK_OVERVIEW.md
1152index 92c49c8d73f13cf58338edfc6b6ef8a1de730f46..19aa3aa28123b37a3a83f941364e181bf9121dcf 100644
1153--- a/tasks/TASK_OVERVIEW.md
1154+++ b/tasks/TASK_OVERVIEW.md
1155@@ -49,6 +49,7 @@
1156 - timed-jobs JSONL 日志现在已改为异步写入,减少 tick 周期内的同步 IO 阻塞
1157 - 系统级 automation pause 已落地:`system paused` 会同时阻断 live BAA 指令和 timed-jobs 主链,且不会覆盖各对话原有 `pause_reason`
1158 - browser request 风控状态现在会持久化到 `artifact.db`,重启后会恢复限流/退避/熔断窗口,并清理遗留执行锁与 `running` renewal job
1159+ - `proxy_delivery` 结果现在会异步补齐 Level 1 下游 HTTP 状态码;renewal dispatcher 仅在 `200` 时标记 `done`,`429/5xx` 改为 `retry`
1160
1161 ## 当前已确认的不一致
1162
1163@@ -84,12 +85,12 @@
1164 | [`T-S063`](./T-S063.md) | normalize / parse 错误隔离 | S | 无 | Codex | 已完成 |
1165 | [`T-S064`](./T-S064.md) | timed-jobs 异步日志写入 | S | 无 | Codex | 已完成 |
1166 | [`T-S066`](./T-S066.md) | 风控状态持久化 | M | T-S060 | Codex | 已完成 |
1167+| [`T-S069`](./T-S069.md) | proxy_delivery 成功语义增强 | L | T-S060 | Codex | 已完成 |
1168
1169 ### 当前下一波任务
1170
1171 | 项目 | 标题 | 类型 | 状态 | 说明 |
1172 |---|---|---|---|---|
1173-| [`T-S069`](./T-S069.md) | proxy_delivery 成功语义增强 | task | 待开始 | 至少补齐下游 HTTP 状态码回传,避免“已派发”直接等于成功 |
1174 | [`T-S068`](./T-S068.md) | ChatGPT proxy send 冷启动降级保护 | task | 待开始 | 减少插件重载后首批 delivery 直接失败或退回 DOM fallback |
1175 | [`T-S065`](./T-S065.md) | policy 配置化 | task | 待开始 | 为自动化控制指令和后续扩面提供策略入口 |
1176 | [`T-S067`](./T-S067.md) | Gemini 正式接入 raw relay 支持面 | task | 待开始 | 把 `@browser.gemini` 提升到稳定 raw relay 支持面 |
1177@@ -142,7 +143,6 @@
1178
1179 ### P1(并行优化)
1180
1181-- [`T-S069`](./T-S069.md)
1182 - [`T-S068`](./T-S068.md)
1183 - [`T-S065`](./T-S065.md)
1184 - [`T-S067`](./T-S067.md)
1185@@ -182,11 +182,11 @@
1186
1187 ## 当前主线判断
1188
1189-Phase 1(浏览器主链)、Artifact 静态服务,以及 timed-jobs + 续命主线都已完成收口。`T-S060`、`T-S061`、`T-S062`、`T-S063`、`T-S064`、`T-S066` 已经落地。当前主线已经没有 open bug blocker,下一步是:
1190+Phase 1(浏览器主链)、Artifact 静态服务,以及 timed-jobs + 续命主线都已完成收口。`T-S060`、`T-S061`、`T-S062`、`T-S063`、`T-S064`、`T-S066`、`T-S069` 已经落地。当前主线已经没有 open bug blocker,下一步是:
1191
1192-- 先收口 `T-S069`
1193-- 再做 `T-S068`、`T-S065`
1194-- 最后再推进 `T-S067`
1195+- 先做 `T-S068`
1196+- 再做 `T-S065`
1197+- 最后推进 `T-S067`
1198 - `OPT-004`、`OPT-009` 继续保留为 open opt
1199
1200 ## 现在该读什么