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",