From 370d3b8ddb5bf06a89b63a0d6b7a1f67c156bc0f Mon Sep 17 00:00:00 2001 From: Dotta Date: Fri, 9 Oct 2026 06:19:09 -0500 Subject: [PATCH] fix(acpx): preserve commentary across steering intervals Project incremental assistant output as progress while keeping final snapshots authoritative. Verify frozen native steering intervals preserve their earlier commentary and record the failed native browser attempt and current validation scope. Co-Authored-By: Paperclip --- doc/plans/2026-10-06-hermes-native-runner.md | 33 +++++++++++++++++ .../drivers/acpx/codex-acpx-driver.test.ts | 4 +-- .../src/drivers/acpx/codex-acpx-driver.ts | 4 ++- .../transcript/native-run-events.test.ts | 36 ++++++++++++++++++- 4 files changed, 73 insertions(+), 4 deletions(-) diff --git a/doc/plans/2026-10-06-hermes-native-runner.md b/doc/plans/2026-10-06-hermes-native-runner.md index f9f60bdb84f..99608c1129d 100644 --- a/doc/plans/2026-10-06-hermes-native-runner.md +++ b/doc/plans/2026-10-06-hermes-native-runner.md @@ -1965,3 +1965,36 @@ The earlier cloud cold-start and ZIP timeouts remain failed. A separate foundation signoff-policy cancellation/reconciliation failure still needs qualification; the other current stack heads pass that shard. Full CI, review, browser controls, paid credentials and actual Daytona remain required. + +### 2026-10-09 interrupted charges and steering commentary + +An authenticated OpenRouter usage frame can report a charge before the HTTP +stream fails. The receipt now retains that verified charge in the known +subtotal while keeping request completeness and token totals unavailable. +The pinned SDK regression reproduces the original lost charge, then passes +after the fix, including a subsequent successful request. Conflicting receipts +remain unpriced. All 13 billing tests pass; the combined pinned Python suite +passes 53 tests and skips one real-Linux bubblewrap test on Mac. The runtime +manifests change only the reviewed `billing.py` entry. The derived Mac and Linux +closure pins still require fresh cloud provisioning before accepting them as +clean consumer or remote proof. + +The Mac native browser attempt using source `78c57ca` and the verified cloud +daemon shows reasoning and text before completion, then delivers the actual +Steer control to the same native turn. Native tools write the corrected file +and its authorized download matches. The attempt fails when earlier commentary +disappears after steering. Stored events retain that message; the TypeScript +ACPX driver incorrectly projects its channel as unknown. Deltas now project as +progress, matching the Rust normalizer, while the settled snapshot remains +final. All 64 driver and 174 transcript assertions pass. Fresh native browser +acceptance is still required; the failed attempt is not relabeled. + +The announcement can appear after the dashboard URL settles. The company +navigation test now retries the ordinary sidebar click with a bounded deadline, +dismissing the actual card through its normal control if it blocks navigation. +All 29 cases in the chat messaging browser file pass. UI and Runner TypeScript, +the TypeScript-only build, and UI token gates pass. No local Docker, local Rust +build, provider credential or paid model call is used. Current cloud signoff +shards pass; an isolated server Git-streaming fixture times out on one stack +head and remains failed. Fresh full CI, review, paid connection and actual +Daytona qualification remain open. diff --git a/packages/paperclip-runner/src/drivers/acpx/codex-acpx-driver.test.ts b/packages/paperclip-runner/src/drivers/acpx/codex-acpx-driver.test.ts index 353819dd755..38ab46cfb26 100644 --- a/packages/paperclip-runner/src/drivers/acpx/codex-acpx-driver.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/codex-acpx-driver.test.ts @@ -361,7 +361,7 @@ describe("Codex ACPX harness driver", () => { `${turnId}:assistant-message`, ]); expect(assistantEvents.map((event) => event.payload.channel)).toEqual([ - "unknown", + "progress", "final", ]); await expect(session.snapshot()).resolves.toMatchObject({ @@ -471,7 +471,7 @@ describe("Codex ACPX harness driver", () => { `${turnId}:assistant-message`, ]); expect(assistant.map((event) => event.payload.channel)).toEqual([ - "unknown", + "progress", "final", ]); await session.close({ reason: "channel identity verified" }); diff --git a/packages/paperclip-runner/src/drivers/acpx/codex-acpx-driver.ts b/packages/paperclip-runner/src/drivers/acpx/codex-acpx-driver.ts index ce2ba7df532..962089245af 100644 --- a/packages/paperclip-runner/src/drivers/acpx/codex-acpx-driver.ts +++ b/packages/paperclip-runner/src/drivers/acpx/codex-acpx-driver.ts @@ -1683,7 +1683,9 @@ class CodexAcpxSession implements HarnessSession { "item.delta", { kind: isReasoning ? "reasoning" : "agentMessage", - channel: isReasoning ? "summary" : "unknown", + // Output chunks are progress; the settled snapshot owns final text. + // Frozen pre-steering intervals must retain their commentary. + channel: isReasoning ? "summary" : "progress", text: output, }, { diff --git a/ui/src/components/transcript/native-run-events.test.ts b/ui/src/components/transcript/native-run-events.test.ts index 3cff4952df3..0c6c33b3cbc 100644 --- a/ui/src/components/transcript/native-run-events.test.ts +++ b/ui/src/components/transcript/native-run-events.test.ts @@ -2,7 +2,12 @@ import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; import type { HeartbeatRunEvent } from "@paperclipai/shared"; import { nativeRunEventsToTranscript } from "./native-run-events"; -import { transcriptToTaskChatItems } from "../task-chat/transcript-adapter"; +import { + paperclipRunnerTimelineItems, + settledRunChildren, + splitTranscriptAtAnchors, + transcriptToTaskChatItems, +} from "../task-chat/transcript-adapter"; const RUN_ID = "10000000-0000-4000-8000-000000000001"; @@ -74,6 +79,35 @@ function runResult(summary: string): Record { } describe("provider notice presentation", () => { + it("keeps native ACPX commentary when steering freezes its earlier interval", () => { + const entries = nativeRunEventsToTranscript([ + itemEvent(1, "item.delta", "reasoning-1", { + kind: "reasoning", channel: "summary", text: "Inspecting the current task.", + }), + itemEvent(2, "item.delta", "message-before-steer", { + kind: "agentMessage", channel: "progress", text: "I am starting the requested work.", + }), + itemEvent(3, "item.completed", "steering-ack", { + kind: "steering_acknowledgement", status: "acknowledged", text: "Steering acknowledged for the active turn.", + }), + itemEvent(4, "item.delta", "message-after-steer", { + kind: "agentMessage", channel: "progress", text: "The correction is now applied.", + }), + ]); + const [before, after] = splitTranscriptAtAnchors( + entries, Date.parse("2026-08-25T18:00:00.000Z"), [Date.parse("2026-08-25T18:00:03.000Z")], + ); + const frozen = settledRunChildren(paperclipRunnerTimelineItems(transcriptToTaskChatItems(before.entries, { + runId: RUN_ID, running: false, + }))); + expect(frozen.filter(item => item.kind === "activity_phase").map(item => item.interstitial?.text)) + .toContain("I am starting the requested work."); + const continued = transcriptToTaskChatItems(after.entries, { runId: RUN_ID, running: true }); + expect(continued.find(item => item.kind === "message")).toMatchObject({ + text: "The correction is now applied.", channel: "progress", + }); + }); + it("omits unrelated information from saved chat without removing warnings or responses", () => { const legacyNotice = { schema: "paperclip.provider.notice.v1",