Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions cli/bun/src/adapters/output-budget.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { failure } from "../core/errors";
import transportLimits from "../../../shared/capabilities/transport-limits.v1.json";
export const DEFAULT_NATIVE_OUTPUT_BYTES = 67_108_864;
type Selection = { nativeOutputBytes?: string; outputContract?: string; nativeLog?: string };
export function validateNativeOutputBytes(value: string): number {
Expand All @@ -7,7 +8,7 @@ export function validateNativeOutputBytes(value: string): number {
return n;
}
export function nativeOutputBytes(input: Selection): number { return input.nativeOutputBytes===undefined?DEFAULT_NATIVE_OUTPUT_BYTES:validateNativeOutputBytes(input.nativeOutputBytes); }
export function nativeOutputLimits(input: Selection): {maxAggregateStdoutBytes:number;maxNativeCaptureBytes:number;captureEnabled:boolean}|undefined {
export function nativeOutputLimits(input: Selection): {maxRecordBytes:number;maxAggregateStdoutBytes:number;maxNativeCaptureBytes:number;captureEnabled:boolean}|undefined {
if(input.nativeOutputBytes!==undefined&&input.outputContract!=="native")throw failure("CONFIG_INVALID",{reason:"Native output bytes require native output mode."});
return input.outputContract==="native"?{maxAggregateStdoutBytes:nativeOutputBytes(input),maxNativeCaptureBytes:nativeOutputBytes(input),captureEnabled:input.nativeLog!==undefined}:undefined;
return input.outputContract==="native"?{maxRecordBytes:transportLimits.maxRecordBytes,maxAggregateStdoutBytes:nativeOutputBytes(input),maxNativeCaptureBytes:nativeOutputBytes(input),captureEnabled:input.nativeLog!==undefined}:undefined;
}
1 change: 1 addition & 0 deletions cli/bun/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ async function runOperation(
selectedHarness: selected.id,
selectedHarnessVersion: selectedStatus.detectedVersion,
...(selected.id === "codex" ? {codexCompatibility:{qualification:codexQualification(selectedStatus.detectedVersion),policy:config.values.codexCompatibility ?? "qualified"}} : {}),
...(nativeOutputLimits(config.values) ? { nativeOutputLimits: nativeOutputLimits(config.values)! } : {}),
selectedTransport: transport,
selectedAdapterId: adapterId(selected, transport),
promptPlacement: selected.id === "mock"
Expand Down
4 changes: 4 additions & 0 deletions cli/bun/src/core/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import taxonomy from "../../../shared/errors/taxonomy.v1.json" with { type: "jso
import { RunnerFailure, type RunnerErrorCode, type RunnerErrorShape } from "./types";

export function failure(code: RunnerErrorCode, details?: Record<string, unknown>): RunnerFailure {
const diagnostic = details?.transportDiagnostic as { reason?: unknown } | undefined;
if ((code === "PROTOCOL_MALFORMED" || code === "HARNESS_FAILED") && diagnostic?.reason === "record-byte-limit") {
return new RunnerFailure({ ...taxonomy.recordByteLimit, code: "HARNESS_FAILED", boundary: "process", ...(details === undefined ? {} : { details }) });
}
const definition = taxonomy.errors.find((item) => item.code === code);
if (definition === undefined) throw new Error(`Missing shared error taxonomy entry for ${code}`);
return new RunnerFailure({
Expand Down
3 changes: 3 additions & 0 deletions cli/bun/src/supervision/fake-failure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import type { ProcessSupervisionResult } from "./types";
/** Preserve bounded, runner-authored diagnostics on the test transport. */
export function fakeProtocolFailureDetails(outcome: Pick<ProcessSupervisionResult, "error" | "events">): Record<string, unknown> {
const error = outcome.error;
if (error?.code === "HARNESS_FAILED" && (error.details?.transportDiagnostic as {reason?: unknown} | undefined)?.reason === "record-byte-limit") {
return { transportDiagnostic: error.details?.transportDiagnostic, admittedRecordCount: outcome.events.length };
}
if (error?.code !== "PROTOCOL_MALFORMED" && error?.code !== "PROTOCOL_TRUNCATED") return {};
const diagnostic = error.details?.transportDiagnostic as Record<string, unknown> | undefined;
const framingReason = diagnostic?.reason;
Expand Down
16 changes: 15 additions & 1 deletion cli/bun/test/native-output-budget.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {test,expect} from "bun:test";
import Ajv2020 from "ajv/dist/2020";
import nativeOutputLimitsSchema from "../../shared/schemas/native-output-limits.schema.json";
import {mkdtemp,writeFile,rm,mkdir,readFile} from "node:fs/promises";
import {tmpdir} from "node:os";
import {join} from "node:path";
Expand All @@ -7,12 +9,24 @@ import {nativeOutputBytes,nativeOutputLimits,validateNativeOutputBytes} from "..
import {parseEntrypoint} from "../src/core/args";
import {resolveConfiguration} from "../src/core/config";
import {NativeCapture} from "../src/adapters/native-capture";
test("native output schema requires the fixed record cap for every reported budget",()=>{
const validate=new Ajv2020({strict:true}).compile(nativeOutputLimitsSchema);
for(const selection of [{outputContract:"native"},{outputContract:"native",nativeOutputBytes:"134217728",nativeLog:"/fixture/capture"}]){
const limits=nativeOutputLimits(selection)!;
expect(validate(limits)).toBe(true);
const missingCap:Record<string,unknown>={...limits};delete missingCap.maxRecordBytes;
expect(validate(missingCap)).toBe(false);
expect(validate.errors?.some(error=>error.keyword==="required"&&error.params.missingProperty==="maxRecordBytes")).toBe(true);
expect(validate({...limits,maxRecordBytes:1048577})).toBe(false);
}
expect(nativeOutputLimits({outputContract:"image-envelope"})).toBeUndefined();
});
test("native output shared bounds and mode",()=>{
expect(nativeOutputBytes({})).toBe(fixture.default);
for(const v of fixture.valid)expect(validateNativeOutputBytes(v)).toBe(Number(v));
for(const v of fixture.invalid)expect(()=>validateNativeOutputBytes(v)).toThrow();
expect(()=>nativeOutputLimits({nativeOutputBytes:"1048576"})).toThrow();
expect(nativeOutputLimits({outputContract:"native"})).toEqual({maxAggregateStdoutBytes:fixture.default,maxNativeCaptureBytes:fixture.default,captureEnabled:false});
expect(nativeOutputLimits({outputContract:"native"})).toEqual({maxRecordBytes:1048576,maxAggregateStdoutBytes:fixture.default,maxNativeCaptureBytes:fixture.default,captureEnabled:false});
expect(()=>parseEntrypoint(["--native-output-bytes","1048576","--native-output-bytes","2097152","task"])).toThrow();
});
test("native output provenance follows file environment flag precedence",async()=>{
Expand Down
118 changes: 118 additions & 0 deletions cli/bun/test/record-limit-blackbox.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { test, expect } from "bun:test";
import { mkdtemp, writeFile, readFile, unlink, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import fixture from "../../shared/fixtures/adapters/native-output.v1.json";
import transportLimits from "../../shared/capabilities/transport-limits.v1.json";
import { runCli, type CliDependencies } from "../src/cli";
import { sentinelImage } from "../src/assets/sentinel";

// The same frozen controls can also assess an independently compiled Rust CLI.
test("frozen record controls agree with authoritative transport facts", () => {
expect(fixture.recordLimits.recordLimitBytes).toBe(transportLimits.maxRecordBytes);
expect(fixture.recordLimits.error.code).toBe(transportLimits.oversizedRecordFailure);
});
for (const cell of fixture.recordLimits.cases) {
test(`record limit black box: ${cell.name}`, async () => {
const root = await mkdtemp(join(tmpdir(), "prose-record-limit-"));
const pidFile = join(root, "native.pid");
async function removeOwnedNative() {
let pid: number;
try { pid = Number(await readFile(pidFile, "utf8")); }
catch (error: any) { if (error.code === "ENOENT") return; throw error; }
if (!Number.isSafeInteger(pid) || pid <= 1) throw new Error("Invalid synthetic child PID");
try { process.kill(pid, "SIGKILL"); }
catch (error: any) { if (error.code !== "ESRCH") throw error; }
for (let count = 0; count < 100; count++) {
try { process.kill(pid, 0); await Bun.sleep(10); }
catch (error: any) { if (error.code !== "ESRCH") throw error; return; }
}
throw new Error("Synthetic native child did not disappear during failure cleanup");
}
try {
const record = JSON.stringify({ type: "item.completed", item: { type: "command_execution", id: "tool1", command: "fixture", aggregated_output: "", exit_code: 0, status: "completed" } });
const padding = cell.recordBytes - Buffer.byteLength(record);
const source = `#!${process.execPath}
const fs = require('node:fs');
if (process.argv.includes('--version')) { console.log('codex-cli 0.149.0-alpha.4.1'); }
else if (process.argv.slice(2).join(' ') === 'login status') { console.log('Logged in using ChatGPT'); }
else {
fs.writeFileSync(${JSON.stringify(pidFile)}, String(process.pid));
console.log(JSON.stringify({type:'thread.started',thread_id:'fixture'}));
console.log(JSON.stringify({type:'turn.started'}));
console.log(${JSON.stringify(record)}.replace('"aggregated_output":""', '"aggregated_output":"' + 'x'.repeat(${padding}) + '"'));
${cell.accepted ? "console.log(JSON.stringify({type:'turn.completed',usage:{input_tokens:1,output_tokens:1,cached_input_tokens:0}}));" : "setInterval(() => {},1000);"}
}
`;
await writeFile(join(root, "codex"), source, { mode: 0o700 });
const argv = ["--harness", "codex", "--auth-profile", "cached-chatgpt-login", "--output-contract", "native", "--native-output-bytes", String(cell.aggregateBytes), "--timeout", "5s", "--output", "json", "--", "execute", "fixture.md"];
let stdout = "";
let stderr = "";
const dependencies: CliDependencies = {
platform: process.platform, arch: process.arch, processCwd: root,
env: { PATH: root, HOME: root }, userConfigPath: join(root, "absent.toml"),
imageBundle: sentinelImage,
clock: { now: () => "2026-01-01T00:00:00.000Z", monotonicMs: () => performance.now() },
ids: { invocationId: () => "00000000-0000-7000-8000-000000008989" },
writeStdout: text => { stdout += text; }, writeStderr: text => { stderr += text; },
};
const code = await runCli(argv, dependencies);
expect(stderr).toBe("");
check(JSON.parse(stdout.trim()), code);
await checkCleanup();
stdout = "";
const doctorCode = await runCli(["--harness", "codex", "--auth-profile", "cached-chatgpt-login", "--output-contract", "native", "--native-output-bytes", String(cell.aggregateBytes), "cli", "doctor", "--json"], dependencies);
expect(doctorCode).toBe(0);
expect(stderr).toBe("");
expect(JSON.parse(stdout.trim()).nativeOutputLimits).toEqual({ maxRecordBytes: fixture.recordLimits.recordLimitBytes, maxAggregateStdoutBytes: cell.aggregateBytes, maxNativeCaptureBytes: cell.aggregateBytes, captureEnabled: false });
for (const binary of [process.env.PROSE_RECORD_LIMIT_RUST_BINARY, process.env.PROSE_RECORD_LIMIT_BUN_BINARY]) {
if (binary === undefined) continue;
const child = Bun.spawn([binary, ...argv], { cwd: root, env: { PATH: root, HOME: root, XDG_CONFIG_HOME: root }, stdout: "pipe", stderr: "pipe" });
const timer = setTimeout(() => child.kill("SIGKILL"), 8000);
try {
const [output, diagnostic, exit] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
expect(diagnostic).toBe("");
check(JSON.parse(output.trim()), exit);
await checkCleanup();
} finally {
clearTimeout(timer);
try { await removeOwnedNative(); }
finally {
if (child.exitCode === null) child.kill("SIGKILL");
await child.exited;
}
}
}
async function checkCleanup() {
const pid = Number(await readFile(pidFile, "utf8"));
let missing = false;
try { process.kill(pid, 0); }
catch (error: any) { if (error.code !== "ESRCH") throw error; missing = true; }
expect(missing).toBe(true);
await unlink(pidFile);
}
function check(result: any, code: number) {
expect(result.nativeOutputLimits.maxRecordBytes).toBe(fixture.recordLimits.recordLimitBytes);
expect(result.nativeOutputLimits.maxAggregateStdoutBytes).toBe(cell.aggregateBytes);
if (cell.accepted) {
expect(code).toBe(0);
expect(result.terminal.transportCompleted).toBe(true);
expect(result.semantic.status).toBe("not-applicable");
} else {
expect(code).toBe(fixture.recordLimits.error.exitCode);
for (const [key, value] of Object.entries(fixture.recordLimits.error)) expect(result.error[key]).toEqual(value);
expect(result.error.details.transportDiagnostic.reason).toBe("record-byte-limit");
expect(result.error.details.transportDiagnostic.limitBytes).toBe(fixture.recordLimits.recordLimitBytes);
expect(result.error.details.transportDiagnostic.observedBytes).toBeGreaterThan(fixture.recordLimits.recordLimitBytes);
expect(result.error.details.terminalEventObserved).toBe(false);
expect(result.terminal.transportCompleted).toBe(false);
expect(result.terminal.terminalEventObserved).toBe(false);
expect(result.semantic.status).toBe("unknown");
}
}
} finally {
try { await removeOwnedNative(); }
finally { await rm(root, { recursive: true, force: true }); }
}
}, 30000);
}
4 changes: 2 additions & 2 deletions cli/bun/test/supervision-jsonl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ describe("bounded JSONL framing", () => {
["malformed JSON", chunks('{"bad":}\n'), "PROTOCOL_MALFORMED"],
["truncated EOF", chunks('{"unfinished":'), "PROTOCOL_TRUNCATED"],
["empty record", chunks("\n"), "PROTOCOL_MALFORMED"],
["oversized record", chunks(`${JSON.stringify({ value: "x".repeat(130) })}\n`), "PROTOCOL_MALFORMED"],
["oversized record", chunks(`${JSON.stringify({ value: "x".repeat(130) })}\n`), "HARNESS_FAILED"],
["aggregate overflow", chunks(`${JSON.stringify({ a: "x".repeat(80) })}\n`, `${JSON.stringify({ b: "y".repeat(80) })}\n`), "PROTOCOL_MALFORMED"],
])("rejects %s deterministically", async (_label, stream, code) => {
const constrained = _label === "aggregate overflow" ? { ...limits, maxAggregateStdoutBytes: 150 } : limits;
Expand All @@ -61,7 +61,7 @@ import diagnosticCases from "../../shared/fixtures/transport-diagnostics.json";
for (const fixture of diagnosticCases) test(`safe transport diagnostic: ${fixture.name}`, async()=>{
let caught:any;
try {await readBoundedJsonLines(chunks(fixture.input), {...limits,maxRecordBytes:fixture.recordLimit,maxAggregateStdoutBytes:fixture.aggregateLimit},()=>{});}catch(e){caught=e;}
expect(caught.code).toBe("PROTOCOL_MALFORMED");
expect(caught.code).toBe(fixture.reason === "record-byte-limit" ? "HARNESS_FAILED" : "PROTOCOL_MALFORMED");
expect(caught.details.transportDiagnostic.reason).toBe(fixture.reason);
if("limitBytes" in fixture)expect(caught.details.transportDiagnostic.limitBytes).toBe(fixture.limitBytes);
if("observedBytes" in fixture)expect(caught.details.transportDiagnostic.observedBytes).toBe(fixture.observedBytes);
Expand Down
40 changes: 40 additions & 0 deletions cli/protocol/OWNERSHIP.md
Original file line number Diff line number Diff line change
Expand Up @@ -777,3 +777,43 @@ IMP-086 lease extension: `cli/ci/test_rehearse_release.py` for exact current 64-
IMP-086 lease extension: `cli/conformance/fixtures/adapter-host-expectations.json` and the existing leased host runner/tests for independently frozen inventory expectations across admitted POSIX hosts; keep all Codex blocked-state and full-inventory assertions.

IMP-086 lease extension: `cli/ci/test_run_local.py` solely to make the interrupt-tree fixture reap its controlled descendant and publish readiness after signal-safe setup; supervisor behavior, 130/143 exits and PID-absence assertions remain unchanged.

## IMP-089: native record limit diagnostics (October 5, 2026)

Codex `/root/record_limits` owns narrowly scoped edits on isolated branch
`codex/imp-089-record-limits`, coordinated by lead `/root`. Exact paths:
`cli/shared/errors/taxonomy.v1.json`,
`cli/shared/schemas/native-output-limits.schema.json`,
`cli/shared/schemas/runner-error.schema.json`,
`cli/shared/fixtures/native-record-limits.json`,
`cli/shared/fixtures/transport-diagnostics.json`,
`cli/shared/fixtures/adapters/native-output.v1.json`,
`cli/bun/src/core/errors.ts`, `cli/bun/src/core/types.ts`,
`cli/bun/src/supervision/jsonl.ts`, `cli/bun/src/adapters/native-capture.ts`,
`cli/bun/src/adapters/output-budget.ts`, `cli/bun/src/cli.ts`,
`cli/bun/test/supervision-jsonl.test.ts`,
`cli/bun/test/native-output-budget.test.ts`,
`cli/bun/test/record-limit-blackbox.test.ts`,
`cli/rust/crates/prose-runner-core/src/error.rs`,
`cli/rust/crates/prose-runner-core/src/runner.rs`,
`cli/rust/crates/prose-runner-core/src/config.rs`.
The agent may commit its own scoped branch after review of its diff; root owns
push, PR and integration. Shared controls precede product changes. No limit
increase, new setting, provider calls, releases, kernel semantics or Python.
IMP-089 record-limit lease extension: `cli/bun/src/supervision/fake-failure.ts`
solely to preserve diagnosed resource-limit evidence in final test-transport
receipts after honest HARNESS_FAILED classification.
IMP-089 record-limit lease extension: `cli/shared/schemas/doctor-report.schema.json`
for the optional effective native output budgets in readiness reports. Frozen
record controls are nested in the already admitted shared native-output fixture;
no new Python or architecture allowlist change is necessary.
IMP-089 record-limit lease extension: `cli/shared/capabilities/transport-limits.v1.json`
solely to reconcile the authoritative oversized-record error category with the
shared controls and both products. Numeric bounds and other failures stay fixed.
IMP-089 record-limit documentation lease: `docs/native-output-budgets.md` for
accurate fixed record-budget reporting and resource-failure recovery only.
IMP-089 mandatory Rust black-box lease: `cli/rust/crates/prose-cli/tests/native_record_limits.rs`
for provider-free compiled test-seam admission against the same frozen native
record controls. Fake Codex uses the required Bun toolchain; no Python, shell,
provider credentials or network. This supplements rather than replaces existing
framing, mapping and shared differential checks.
Loading
Loading