diff --git a/apps/server/src/orchestration-v2/WireProjection.test.ts b/apps/server/src/orchestration-v2/WireProjection.test.ts index 6379cad45dbd0..95d3fcde7f401 100644 --- a/apps/server/src/orchestration-v2/WireProjection.test.ts +++ b/apps/server/src/orchestration-v2/WireProjection.test.ts @@ -175,7 +175,9 @@ describe("orchestration V2 wire projection", () => { }); it("truncates detail at a UTF-8 boundary without changing the source", () => { - const progress = "a".repeat(32_767) + "😀" + "x".repeat(100_000); + const marker = "\n… output truncated for transport"; + const prefixBytes = 32_768 - Buffer.byteLength(marker); + const progress = "a".repeat(prefixBytes - 1) + "😀" + "x".repeat(100_000); const item = { ...base, type: "subagent" as const, @@ -184,13 +186,17 @@ describe("orchestration V2 wire projection", () => { driver: ProviderDriverKind.make("codex"), providerInstanceId: ProviderInstanceId.make("codex"), childThreadId: null, - prompt: "Inspect code", + prompt: "漢".repeat(12_000), result: null, progress, }; const projected = projectTurnItemForWire(item); + expect(projectTurnItemForWire(projected)).toEqual(projected); + expect( + Buffer.byteLength(projected.type === "subagent" ? projected.prompt : ""), + ).toBeLessThanOrEqual(32_768); expect(projected.type === "subagent" ? projected.progress : null).toBe( - "a".repeat(32_767) + "\n… output truncated for transport", + "a".repeat(prefixBytes - 1) + marker, ); expect(item.progress).toBe(progress); }); diff --git a/apps/server/src/orchestration-v2/WireProjection.ts b/apps/server/src/orchestration-v2/WireProjection.ts index 1495a3effa677..c4916546cf8e1 100644 --- a/apps/server/src/orchestration-v2/WireProjection.ts +++ b/apps/server/src/orchestration-v2/WireProjection.ts @@ -26,6 +26,8 @@ const MAX_WIRE_CHECKPOINT_FILES = 200; const MAX_DETAIL_STRING_BYTES = 32_768; const MAX_DYNAMIC_VALUE_BYTES = 16_384; const MAX_ON_DEMAND_BYTES = 256 * 1024; +const TRUNCATION_MARKER = "\n… output truncated for transport"; +const TRUNCATION_MARKER_BYTES = Buffer.byteLength(TRUNCATION_MARKER, "utf8"); function truncateDetail( value: string | undefined, @@ -37,12 +39,14 @@ function truncateDetail( ) { return value; } + // The marker counts toward the cap, so projecting truncated text again leaves it unchanged. + const budget = maxBytes - TRUNCATION_MARKER_BYTES; // UTF-8 needs at least one byte per UTF-16 unit; encode only a potentially fitting prefix. - const prefix = Buffer.from(value.slice(0, maxBytes), "utf8") - .subarray(0, maxBytes) + const prefix = Buffer.from(value.slice(0, budget), "utf8") + .subarray(0, budget) .toString("utf8") .replace(/\uFFFD$/u, ""); - return `${prefix}\n… output truncated for transport`; + return `${prefix}${TRUNCATION_MARKER}`; } /** Bound stored diffs to the client wire limit to avoid retaining invisible data in SQLite and heap. */ diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index 4781fcf78dde6..9b97c61260580 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -813,7 +813,7 @@ export const subscribeOrchestrationV2Thread = Effect.fn("ws.orchestrationV2.subs Stream.map((stored) => ({ kind: "event" as const, sequence: stored.sequence, - event: projectDomainEventForWire(stored.event), + event: stored.event, })), coalesceThreadLiveStream, Stream.mapError(