baa-conductor


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 ## 现在该读什么