From ed2036c6f3872f31087cd3f74cd92e35258efde4 Mon Sep 17 00:00:00 2001
From: t
Date: Tue, 8 Sep 2026 08:35:01 +0900
Subject: [PATCH 1/2] docs: record Codex voice relay source comparison
---
devlog/_plan/260908_voice_relay/000_plan.md | 39 +++++++++++++++++++
.../_plan/260908_voice_relay/001_sources.md | 24 ++++++++++++
2 files changed, 63 insertions(+)
create mode 100644 devlog/_plan/260908_voice_relay/000_plan.md
create mode 100644 devlog/_plan/260908_voice_relay/001_sources.md
diff --git a/devlog/_plan/260908_voice_relay/000_plan.md b/devlog/_plan/260908_voice_relay/000_plan.md
new file mode 100644
index 0000000000..2675c20c84
--- /dev/null
+++ b/devlog/_plan/260908_voice_relay/000_plan.md
@@ -0,0 +1,39 @@
+# Codex voice relay follow-up
+
+Satisfy-spec HOTL loop, triggered by the maintainer's September voice source comparison request.
+Goal: carry only verified OpenCodex-owned improvements and document the client/proxy boundary.
+No local product tests, typecheck, build or installs; no release, deployment or user settings changes.
+Verification: read pinned upstream source and Aside findings; independent review; final cumulative
+remote Cross-platform CI dispatch (all lanes), followed by exact-head merge and fetched dev tree proof.
+Local product verification is NOT RUN by explicit user instruction. Git diff/document inspection is
+allowed but does not certify runtime behavior. No latency or live audio improvement is claimed.
+Stop: audited no-change conclusion, or required corrections landed with fresh remote evidence.
+Outcomes: DONE, evidence-backed NOOP, or explicit unmet external gate. No invented time/cost budget;
+existing tools/credentials only, bounded individual probes, no new services or installs.
+Escalation: unresolved maintainer objection, missing external authority, or unavailable required CI.
+
+## Ordered work phases
+
+1. wp1: source research and audited roadmap (documents only).
+2. wp2: scoped relay correction and adjacent regression coverage; depends on wp1.
+3. wp3: publish the documented contract, final cumulative CI, and merge; depends on wp2.
+
+Existing owners: `src/server/live.ts`, `src/server/index.ts`, `tests/server/server-live.test.ts`,
+`docs-site/src/content/docs/guides/codex-integration.md`, `structure/04_transports-and-sidecars.md`.
+No new production abstraction, endpoint or dependency. Preserve preexisting worktree documents.
+Manual two-PR chain: relay implementation/tests, then integration documentation. User explicitly
+requests final-tip-only product CI, overriding per-layer local/full-suite defaults. Automatic
+redundant product CI on these task PRs may be cancelled; it is never counted as passing evidence.
+Use merge commits to preserve stack ancestry, retarget the child only after the parent lands,
+and recheck the current dev tree before final merge. Required checks remain truthful.
+
+Security working material is kept only in ignored scratch per AGENTS.md. The detailed audited
+roadmap resides in `.tmp/voice-0908/010_runtime.md` and `.tmp/voice-0908/020_delivery.md` until
+publication of the fix; it is intentionally not copied into this public planning directory.
+
+## Roadmap audit and lock
+
+Independent plan and security audit: PASS, no blockers. The implementation will preserve view
+bounds and original frame delivery. Diagnostic replacement-character flags are not evidence of
+which peer introduced malformed text. Existing logs are outside this prospective logging change.
+The roadmap is locked for wp2; final runtime evidence remains due in wp3, on the cumulative tree.
diff --git a/devlog/_plan/260908_voice_relay/001_sources.md b/devlog/_plan/260908_voice_relay/001_sources.md
new file mode 100644
index 0000000000..b5a6c2658f
--- /dev/null
+++ b/devlog/_plan/260908_voice_relay/001_sources.md
@@ -0,0 +1,24 @@
+# Source comparison
+
+Pinned upstream: openai/codex b01c3986fd2e79b8a477a08d81430f52f22bc0dc (2026-09-07 UTC).
+The local corpus is `/Users/jun/Developer/codex`; its 120 and 121 upstream checkouts had older
+working heads, so the named commit was fetched without modifying their worktrees.
+
+- https://github.com/openai/codex/commit/1b53f6a44eff890b5169bde8d3bd5b12b8766946:
+ local voice helper offer/answer, ordered oai-events data channel and UDP/TCP transport.
+- https://github.com/openai/codex/commit/b01c3986fd2e79b8a477a08d81430f52f22bc0dc:
+ feature-gated TUI voice commands, captions, handoff answer delivery and lifecycle cleanup.
+- `codex-rs/codex-api/src/endpoint/realtime_call.rs` at the pinned head:
+ backend JSON and API multipart call creation, Frameless `/live`, AVAS `/realtime/calls`.
+- OpenCodex `src/server/live.ts` already implements these call-create and sideband shapes;
+ `src/server/index.ts` transparently relays frames and bounds pending queues and teardown.
+- `tests/server/server-live.test.ts` already covers call creation, protocol headers, pool identity,
+ sideband joins and frame delivery. Existing implementation is reused, not duplicated.
+
+Fast-tier display text and local audio negotiation do not demonstrate a proxy latency gain.
+The TUI merge date does not establish when a desktop binary shipped. Live microphone/audio
+verification is outside the automated evidence gathered here.
+
+The Fast-tier metadata commit is 0e0f55fc4ec9308840e54ceba1f1f1dc9547380f,
+2026-09-04T00:12:18Z; it changes only `codex-rs/models-manager/models.json`.
+It describes the supported service tier, not OpenCodex voice transport performance.
From d14f75b49fb08583998b265b2e5bf12ad83b1097 Mon Sep 17 00:00:00 2001
From: t
Date: Tue, 8 Sep 2026 08:37:13 +0900
Subject: [PATCH 2/2] fix(voice): keep sideband diagnostics free of frame
content
---
src/server/live.ts | 27 ++++-------
structure/04_transports-and-sidecars.md | 10 +++++
tests/server/server-live.test.ts | 60 ++++++++++++++++++++++---
3 files changed, 73 insertions(+), 24 deletions(-)
diff --git a/src/server/live.ts b/src/server/live.ts
index 91caec1f66..6d983ce0aa 100644
--- a/src/server/live.ts
+++ b/src/server/live.ts
@@ -86,39 +86,29 @@ export const LIVE_CLIENT_PROTOCOL_HEADERS = [
*
* When `OCX_LIVE_FRAME_LOG` is set to a file path, every relayed sideband frame appends one
* JSONL record: direction, frame kind, byte length, and whether the payload contains U+FFFD.
- * Privacy: full frame payloads are never written — only when U+FFFD is present, a short
- * excerpt around the first replacement character is included so the corruption point can be
- * attributed (upstream vs relay vs client). Disabled entirely when the env var is unset.
+ * Privacy: no frame content is written, including excerpts around replacement characters.
+ * For binary frames, U+FFFD may also be introduced by UTF-8 decoding; the flag alone does not
+ * identify the source of corruption. Disabled entirely when the env var is unset.
*/
export const LIVE_FRAME_LOG_ENV = "OCX_LIVE_FRAME_LOG";
-const LIVE_FRAME_LOG_CONTEXT_CHARS = 24;
-
-function fffdContext(text: string): string | undefined {
- const idx = text.indexOf("\uFFFD");
- if (idx < 0) return undefined;
- const start = Math.max(0, idx - LIVE_FRAME_LOG_CONTEXT_CHARS);
- const end = Math.min(text.length, idx + LIVE_FRAME_LOG_CONTEXT_CHARS);
- return text.slice(start, end);
-}
-
export function logLiveSidebandFrame(dir: "c2u" | "u2c", data: unknown): void {
const logPath = process.env[LIVE_FRAME_LOG_ENV];
if (!logPath) return;
try {
let kind: "text" | "binary" = "binary";
let bytes = 0;
- let context: string | undefined;
+ let fffd = false;
if (typeof data === "string") {
kind = "text";
bytes = Buffer.byteLength(data);
- context = fffdContext(data);
+ fffd = data.includes("\uFFFD");
} else if (data instanceof ArrayBuffer) {
bytes = data.byteLength;
- context = fffdContext(new TextDecoder().decode(new Uint8Array(data)));
+ fffd = new TextDecoder().decode(new Uint8Array(data)).includes("\uFFFD");
} else if (ArrayBuffer.isView(data)) {
const view = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
bytes = data.byteLength;
- context = fffdContext(new TextDecoder().decode(view));
+ fffd = new TextDecoder().decode(view).includes("\uFFFD");
} else {
return;
}
@@ -127,8 +117,7 @@ export function logLiveSidebandFrame(dir: "c2u" | "u2c", data: unknown): void {
dir,
kind,
bytes,
- fffd: context !== undefined,
- ...(context !== undefined ? { context } : {}),
+ fffd,
};
appendFileSync(logPath, `${JSON.stringify(record)}\n`);
} catch {
diff --git a/structure/04_transports-and-sidecars.md b/structure/04_transports-and-sidecars.md
index 256bd1eae5..678f1ee93f 100644
--- a/structure/04_transports-and-sidecars.md
+++ b/structure/04_transports-and-sidecars.md
@@ -1778,3 +1778,13 @@ The field is omitted when no classified recovery result exists, and existing com
branches that return the original target failure keep that response.
`recovery_unavailable` includes cache/singleflight capacity and does not prove an
upstream request was attempted. No retry or broader envelope acceptance is enabled.
+
+## Voice diagnostic metadata
+
+`src/server/live.ts` owns optional `OCX_LIVE_FRAME_LOG` diagnostics for both sideband directions.
+The JSONL schema contains only `ts`, `dir`, `kind`, `bytes`, and `fffd`. It never stores frame
+content or transcript excerpts, and logging failures do not affect transparent frame delivery.
+Binary detection decodes only the supplied buffer view; malformed UTF-8 can itself produce U+FFFD,
+so the flag does not identify the peer responsible for corruption. Existing diagnostic files are
+not rewritten. Audio devices, WebRTC media negotiation, captions and spoken handoff delivery remain
+client responsibilities.
diff --git a/tests/server/server-live.test.ts b/tests/server/server-live.test.ts
index 16ee4c7946..f6d4da8916 100644
--- a/tests/server/server-live.test.ts
+++ b/tests/server/server-live.test.ts
@@ -1237,10 +1237,12 @@ test("sideband relay preserves multibyte UTF-8 frames byte-identically in both d
// The env-gated frame forensic log (OCX_LIVE_FRAME_LOG) records per-frame metadata and
// U+FFFD presence without writing full payloads — the attribution tool for multibyte
// transcript corruption reports.
-test("sideband frame log records direction, kind, and U+FFFD context without full payloads", async () => {
+test("sideband frame log preserves delivery without recording damaged or clean text", async () => {
const frameLogPath = join(TEST_DIR, "frames.jsonl");
+ const previousFrameLog = process.env.OCX_LIVE_FRAME_LOG;
process.env.OCX_LIVE_FRAME_LOG = frameLogPath;
const FFFD_TEXT = "가볍게 ��기핼봐요";
+ const received: string[] = [];
const upstream = Bun.serve({
port: 0,
@@ -1293,7 +1295,8 @@ test("sideband frame log records direction, kind, and U+FFFD context without ful
client.addEventListener("open", () => {
client.send("clean-frame");
});
- client.addEventListener("message", () => {
+ client.addEventListener("message", event => {
+ received.push(String(event.data));
acks += 1;
if (acks >= 2) {
clearTimeout(timer);
@@ -1316,23 +1319,70 @@ test("sideband frame log records direction, kind, and U+FFFD context without ful
expect(u2cFffd).toBeDefined();
expect(u2cFffd.kind).toBe("text");
expect(u2cFffd.bytes).toBeGreaterThan(0);
- expect(u2cFffd.context).toContain("�");
+ expect(received).toContain(FFFD_TEXT);
expect(c2uClean).toBeDefined();
expect(c2uClean.fffd).toBe(false);
- // Full payloads must never be logged — only short FFFD context excerpts.
+ // Even a short damaged transcript must not be persisted as diagnostic context.
for (const line of lines) {
+ expect(Object.keys(line).sort()).toEqual(["bytes", "dir", "fffd", "kind", "ts"]);
expect(JSON.stringify(line)).not.toContain("clean-frame");
+ expect(JSON.stringify(line)).not.toContain(FFFD_TEXT);
}
client.close();
} finally {
- delete process.env.OCX_LIVE_FRAME_LOG;
+ if (previousFrameLog === undefined) delete process.env.OCX_LIVE_FRAME_LOG;
+ else process.env.OCX_LIVE_FRAME_LOG = previousFrameLog;
globalThis.WebSocket = RealWebSocket;
await server.stop(true);
await upstream.stop(true);
}
});
+test("frame diagnostics retain only metadata for text, binary, and bounded views", async () => {
+ const { logLiveSidebandFrame } = await import("../../src/server/live");
+ const previousFrameLog = process.env.OCX_LIVE_FRAME_LOG;
+ const frameLogPath = join(TEST_DIR, "frame-metadata.jsonl");
+ const damagedText = "private-voice-�";
+ const encoded = new TextEncoder().encode(damagedText);
+ const padded = new TextEncoder().encode("�safe�");
+ const frames: Array<{ data: unknown; kind: string; bytes: number; fffd: boolean }> = [
+ { data: damagedText, kind: "text", bytes: 17, fffd: true },
+ { data: encoded.buffer, kind: "binary", bytes: 17, fffd: true },
+ { data: Buffer.from(encoded), kind: "binary", bytes: 17, fffd: true },
+ // Replacement characters outside this view must not affect the flag or byte count.
+ { data: new Uint8Array(padded.buffer, 3, 4), kind: "binary", bytes: 4, fffd: false },
+ { data: new DataView(padded.buffer, 3, 4), kind: "binary", bytes: 4, fffd: false },
+ { data: "한글", kind: "text", bytes: 6, fffd: false },
+ { data: new Uint8Array([0xff]), kind: "binary", bytes: 1, fffd: true },
+ ];
+ try {
+ process.env.OCX_LIVE_FRAME_LOG = frameLogPath;
+ for (const frame of frames) logLiveSidebandFrame("u2c", frame.data);
+ logLiveSidebandFrame("c2u", { privateText: damagedText });
+ const raw = readFileSync(frameLogPath, "utf8");
+ const records = raw.trim().split("\n").map(line => JSON.parse(line));
+ expect(records).toHaveLength(frames.length);
+ records.forEach((record, index) => {
+ const expected = frames[index]!;
+ expect(record).toEqual({
+ ts: expect.any(String), dir: "u2c", kind: expected.kind,
+ bytes: expected.bytes, fffd: expected.fffd,
+ });
+ expect(Number.isNaN(Date.parse(record.ts))).toBe(false);
+ });
+ for (const content of [damagedText, "safe", "한글", "�"]) expect(raw).not.toContain(content);
+ delete process.env.OCX_LIVE_FRAME_LOG;
+ logLiveSidebandFrame("c2u", damagedText);
+ expect(readFileSync(frameLogPath, "utf8")).toBe(raw);
+ process.env.OCX_LIVE_FRAME_LOG = TEST_DIR;
+ expect(() => logLiveSidebandFrame("c2u", damagedText)).not.toThrow();
+ } finally {
+ if (previousFrameLog === undefined) delete process.env.OCX_LIVE_FRAME_LOG;
+ else process.env.OCX_LIVE_FRAME_LOG = previousFrameLog;
+ }
+});
+
// ── /readyz: per-server readiness gate ────────────────────────────────────────
// /healthz remains the immediate liveness signal (with only bounded capability
// metadata); /readyz is the stricter gate that reflects the post-startup Codex sync