fix(runner): preserve canonical reasoning delta privacy

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-09-28 14:30:34 -05:00
1 parent a589381f90
commit a978d1bc54
2 files changed
+49 -3

No files matched your search

@@ -32,6 +32,7 @@ import type {
PrpTerminalState,
} from "../protocol/replay-contract.js";
import { executeNativeSession } from "../native-session-runtime.js";
import { redactCapabilityEvidenceData } from "./evidence-redaction.js";
import { NativeSessionCloseUnrecoverableError } from "../contracts/native-session-backend.js";
import { parsePaperclipQuestionSet } from "../contracts/question-set.js";
import { ACPX_CREDENTIAL_BINDING_ENV, createAcpxCredentialBinding, createAcpxSidecarHostEnvironment } from "../drivers/acpx/environment.js";
@@ -1922,6 +1923,35 @@ it("binds a durable semantic result to the active provider turn", () => {
});
});
it.each(["summary", "detail"])("preserves canonical reasoning %s deltas without publishing assistant text", (channel) => {
// Exact shape emitted by the Rust ACPX projector, with synthetic private text.
const payload = { provider: "acpx", itemId: "reason-1", kind: "reasoning", channel,
providerMethod: "runtime.event", text: "PRIVATE_REASONING_SENTINEL" };
const method = runnerdCanonicalNotificationMethod("item.delta", payload);
expect(method).toBe(channel === "detail" ? "item/reasoning/textDelta" : "item/reasoning/summaryTextDelta");
const params = rehydrateRunnerdDeltaNotification(payload, "provider-thread", "provider-turn");
expect(params).toMatchObject({ threadId: "provider-thread", turnId: "provider-turn", itemId: "reason-1" });
const evidence = redactCapabilityEvidenceData("provider_event", { method, params });
expect(evidence).toEqual({ event: "reasoning_delta" });
expect(JSON.stringify(evidence)).not.toContain("PRIVATE_REASONING_SENTINEL");
});
it("classifies each coalesced canonical delta without mixing reasoning into assistant output", () => {
const notifications = expandRunnerdCanonicalNotifications("item/agentMessage/delta", {
coalescedCount: 3,
events: [
{ kind: "reasoning", channel: "summary", text: "PRIVATE_REASONING_SENTINEL" },
{ kind: "agentMessage", channel: "progress", text: "Visible progress" },
{ kind: "reasoning", channel: "detail", text: "PRIVATE_DETAIL_SENTINEL" },
],
}, "item.delta");
expect(notifications.map((entry) => entry.method)).toEqual([
"item/reasoning/summaryTextDelta", "item/agentMessage/delta", "item/reasoning/textDelta",
]);
expect(notifications.filter((entry) => entry.method === "item/agentMessage/delta").map((entry) => entry.params.text))
.toEqual(["Visible progress"]);
});
it("restores provider identity and streamed text from a canonical delta", () => {
expect(rehydrateRunnerdDeltaNotification({ text: "Reading Gmail", itemId: "message-1", turnId: "controller-turn" }, "root-thread", "provider-turn"))
.toMatchObject({ threadId: "root-thread", turnId: "provider-turn", delta: "Reading Gmail", itemId: "message-1" });
@@ -1325,10 +1325,21 @@ export function unseenRunnerdCommittedEvents<
export function expandRunnerdCanonicalNotifications(
method: string,
input: unknown,
eventType?: string,
): Array<{ method: string; params: Record<string, unknown> }> {
const payload = record(input);
if (!Array.isArray(payload.events)) return [{ method, params: payload }];
return payload.events.map((event) => ({ method, params: record(event) }));
const events = Array.isArray(payload.events) ? payload.events : [payload];
return events.map((event) => {
const params = record(event);
// A coalesced envelope can contain both reasoning and visible messages.
// Classify each payload after expansion, never using the envelope's kind.
return {
method: eventType === "item.delta"
? runnerdCanonicalNotificationMethod(eventType, params) ?? method
: method,
params,
};
});
}
export function runnerdCanonicalNotificationMethod(
@@ -1341,6 +1352,11 @@ export function runnerdCanonicalNotificationMethod(
if (eventType === "session.goal.snapshot" && payload.goal === null) {
return undefined;
}
if (eventType === "item.delta" && payload.kind === "reasoning") {
return payload.channel === "detail"
? "item/reasoning/textDelta"
: "item/reasoning/summaryTextDelta";
}
return (
{
"turn.started": "turn/started",
@@ -6007,7 +6023,7 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
: event.eventType === "provider.event"
? unwrapRunnerdProviderNotifications(eventPayload)
: canonicalMethod
? expandRunnerdCanonicalNotifications(canonicalMethod, eventPayload)
? expandRunnerdCanonicalNotifications(canonicalMethod, eventPayload, event.eventType)
: [];
for (const payload of notifications) {
const method = payload.method;