Skip to content

Codex code-mode truncates open_kitchen to 10K despite 54.5K configured ceiling #4300

Description

@Trecek

Summary

Full-mode open_kitchen payloads must reach the orchestrator untruncated — recipe payloads are allowed to be very large; delivering them whole is the product requirement. On Codex code-mode sessions this does not hold: a remediation orchestrator session on 2026-07-20 received only ~10,000 of 43,857 tokens of its open_kitchen response. Most of the recipe body never reached the model, and the errors/diagram/suggestions fields were cut entirely; the tail of the response (ingredients_table, orchestration_rules, stop_step_semantics, deferred_guards, closing metadata) survived the middle-out cut — by serialization-order luck, not by design.

Observed Incident (direct log evidence)

Codex rollout log: ~/.codex/sessions/2026/07/20/rollout-2026-07-20T07-08-43-019f7fdb-8f14-7200-88ad-225434150b08.jsonl (interactive remediation orchestrator, started 2026-07-20T14:08:43Z).

Call 1 (14:08:56Z) — code-mode exec script:

const r = await tools.mcp__autoskillit__open_kitchen({name:"remediation"});

Harness-rendered result (verbatim):

Warning: truncated output (original token count: 43857)

with a middle-out cut marker …33857 tokens truncated… — exactly 10,000 tokens kept. Head: start of recipe content. Tail: ingredients_table, orchestration_rules, stop_step_semantics, deferred_guards, closing metadata (verified byte positions in the visible output: cut marker 20,079; ingredients_table 23,790; orchestration_rules 26,161; stop_step_semantics 30,096). Lost in the cut: errors, diagram, suggestions, and most of content (the recipe body). Which fields survive is a function of serialization order and payload size — nothing guarantees it.

No max_output_tokens was passed — verified against the full rollout: the string's only occurrence in the entire session is the advisory intake-discipline digest text inside the prompt.

Call 2 (14:09:05Z) — the model improvised a recovery call open_kitchen({name:"remediation", ingredients_only:true}) (15,195 tokens, also truncated to ~10K, but the metadata fields survived). The visible "double open_kitchen call" is a symptom of this bug, not a defect itself.

Root Cause

Every autoskillit-side budget layer passed the payload intentionally, and the one layer that cut it is outside all of them:

Layer Value Result
MCP response backstop exemption — RESPONSE_BACKSTOP_EXEMPTION_REGISTRY["open_kitchen"] (src/autoskillit/core/types/_type_constants_registries.py:197-199) 186,000 bytes ~175 KB payload within ceiling → served whole
Codex transport ceiling — CODEX_TOOL_OUTPUT_TOKEN_LIMIT (src/autoskillit/execution/backends/_codex_config.py:34, ((186_000+3)//4)+8_000) written by autoskillit init to ~/.codex/config.toml as tool_output_token_limit = 54500, deliberately sized for the open_kitchen exemption (ADR-0005 "Per-Repo Ceiling Guidance") 54,500 tokens 43,857 < 54,500 → should fit
Codex code-mode path model-declared max_output_tokens governs; config ceiling is not the operative bound (ADR-0005, docs/decisions/0005-output-budget-protocol.md: "code-mode models (gpt-5.6-sol) honor model-declared max_output_tokens unclamped — for those sessions the ceiling is not a hard cap") model declared nothing (no max_output_tokens anywhere in the rollout) → an exactly-10,000-token bound governed the output. The enforcing mechanism lives in Codex's code-mode harness, outside this repo, and is unconfirmed — the figure matches the digest's advisory 10,000, but whether it is a harness default or model-side behavior cannot be proven from the rollout

ADR-0005 documented the code-mode bypass in the upward direction (unclamped intake → context blowouts, #4272/#4280). This incident is the complementary downward direction: when the model passes no max_output_tokens, an effective ~10K bound governs — below the configured 54,500 ceiling that was specifically engineered so open_kitchen would fit. The only autoskillit-side control on this path is the advisory digest line "Never pass max_output_tokens above 10000" (CODEX_INTAKE_DISCIPLINE_DIGEST, src/autoskillit/core/types/_type_constants.py:174) — prompt-level, unenforced, and its 10,000 figure is itself far below full-mode open_kitchen size.

Ruled-out alternative explanations (verified during adversarial review): (a) a per-session CODEX_HOME/config override — the interactive launcher calls build_interactive_cmd without add_dirs (src/autoskillit/cli/session/_session_launch.py:109-117), so the CODEX_HOME override branch in src/autoskillit/execution/backends/codex.py never fires for orchestrator sessions; the active config was ~/.codex/config.toml with tool_output_token_limit = 54500. (b) an autoskillit-side truncation hook — no code in server/ or execution/ touches MCP response bytes for Codex sessions.

Impact

  • The orchestrator ran its pipeline with most of the recipe body (~3/4 of the payload) missing from context and the validity signals (errors, suggestions, diagram) cut entirely — deviation risk. The orchestration rules and stop-step semantics survived this particular cut, but only because they serialize near the tail; the middle-out cut guarantees nothing about which fields survive at other payload sizes.
  • Structural for large recipes: full-mode remediation is ~43.9K tokens vs. the observed ~10K delivery bound, and ADR-0005's measured maximum full-mode payload is 183,103 bytes (≈45.8K tokens by the 4-byte heuristic) — so any Codex-backend session opening a recipe whose full-mode payload exceeds ~10K tokens is affected. Small bundled recipes (e.g. consolidate-health-reports.yaml, 1,952 bytes) plausibly fit under the bound. Codex is not the default interactive backend (AgentBackendConfig.backend defaults to "claude-code", src/autoskillit/config/_config_dataclasses.py:607); the defect applies to sessions explicitly launched on the Codex backend.
  • Secondary waste: forced recovery double-call; validity information (errors, suggestions) invisible on the first call.

Prior Manifestations — 4th occurrence of this failure class

# Boundary Fix
#1803 Fleet dispatch paying full-recipe context cost PR #1827ingredients_only
#4089 Deferred-recall resending full content PR #4092 — stripping on recall path
#4253 Claude Code ~100KB persistence gate truncating open_kitchen PR #4259 — 186KB backstop exemption
#4272 Codex unbounded intake (upward direction) PR #4277 / ADR-0005 — output budget protocol (advisory on code-mode path)
this Codex code-mode default output bound (downward direction) — untracked until now

Related open issues (opposite failure direction, same design weakness): #4272, #4280. Distinct kitchen-tracker defect in the same area: #4293.

Invariant (acceptance criteria)

  1. A Codex interactive orchestrator session receives the complete full-mode open_kitchen payload (content, orchestration_rules, stop_step_semantics, errors, diagram, suggestions, ingredients_table) in one call, untruncated, for every bundled recipe.
  2. The invariant is regression-gated: an end-to-end assertion (analog of ADR-0005's live large-output probe) that covers the code-mode default (downward) direction, not only the unclamped (upward) one.

Feasibility note: criterion 1 is achievable relative to the configured ceilings — ADR-0005's measured maximum full-mode payload (183,103 bytes, remediation all-truthy) sits under the 186,000-byte exemption, and ≈45.8K tokens sits under the 54,500-token config ceiling — provided the fix makes that ceiling (or better) the operative bound on the code-mode path. Until then, the bound to beat is the observed 10,000 tokens, not 54,500: note that even the recovery payload (ingredients_only=true, 15,195 tokens) exceeded 10K and was truncated too.

Candidate Fix Directions (not prescriptive)

  1. Enforce, don't advise: ensure code-mode MCP calls for backstop-exempted tools carry an effective max_output_tokensCODEX_TOOL_OUTPUT_TOKEN_LIMIT, rather than relying on the prompt-level digest. Caveat — likely upstream-dependent: autoskillit currently has no control surface over the code-mode harness (zero code-mode references anywhere in src/autoskillit/; the only knobs it writes are config.toml keys — and ADR-0005 states tool_output_token_limit does not clamp code mode — plus advisory prompt text). Unless Codex exposes such a mechanism, this direction requires an upstream change (cf. ADR-0006: "Route (c) — upstream pre-truncation integration — is the ideal end state but outside this repo's control"). In-repo, the enforceable options are direction 2 and hardened advisory prompting.
  2. Or serve to fit: a Codex-code-mode-sized full-mode serving (compact variant or chunked delivery) that stays under the path's operative bound.
  3. Either way, extend ADR-0005's release gates so the open_kitchen-fits-the-transport relationship is pinned for the code-mode path specifically.

Investigation

Prior investigation completed interactively. See below for root cause analysis.

Investigation: Double open_kitchen Call + Warning Suggestions in the Second Response (Codex Orchestrator)

Date: 2026-07-20
Scope: Why a Codex-backend remediation orchestrator session called open_kitchen twice (full mode, then ingredients_only=true), and whether the "error" content in the second response indicates a defect
Mode: Standard

Summary

The pasted transcript came from Codex-backend orchestrator session 019f7fdb-8f14-7200-88ad-225434150b08 (started 2026-07-20 14:08:43 UTC, rollout log at ~/.codex/sessions/2026/07/20/rollout-2026-07-20T07-08-43-019f7fdb-8f14-7200-88ad-225434150b08.jsonl). The orchestrator prompt prescribes exactly one full open_kitchen(name='remediation') call. The model made that call, but the ~43,857-token response was middle-out truncated to ~10,000 tokens by the Codex code-mode exec harness ("Warning: truncated output (original token count: 43857)" / "…33857 tokens truncated…"). The cut removed the errors, diagram, and suggestions fields and most of the recipe content, leaving the model unable to verify recipe validity; the response tail — ingredients_table, orchestration_rules, stop_step_semantics, deferred_guards, and closing metadata — survived the middle-out cut (verified byte positions in the visible output: cut marker 20,079; ingredients_table 23,790; orchestration_rules 26,161; stop_step_semantics 30,096). The second call with ingredients_only=true was the model's improvised recovery — a smaller payload (15,195 tokens, also truncated but with all metadata fields surviving). The "error" in the second response is actually "errors": [] (empty — no errors) plus two WARNING-severity advisory suggestions that were present in both responses but only visible in the second, because the first response's truncation cut them out. Neither warning blocks anything; one is systemic advisory noise, the other flags a real but repo-wide routing gap.

Root Cause

Question 1 — the double call. Not by design for the interactive flow, and not a tool-contract bug: the second call was rational model-improvised recovery from harness truncation.

  • The session received the standard interactive orchestrator prompt (_build_orchestrator_prompt, src/autoskillit/cli/_prompts_orchestrator.py:112-123): "FIRST ACTION … Call open_kitchen(name='remediation')" — a single call, with a pre-rendered ingredients table already embedded in the prompt ("RECIPE INGREDIENTS — USE THESE EXACT NAMES"). Both facts confirmed present in the rollout log.
  • Both response modes return the same underlying payload; ingredients_only=true merely strips {"content", "orchestration_rules", "stop_step_semantics"} (strip_ingredients_only_keys, src/autoskillit/server/_misc.py:234-239; applied at src/autoskillit/server/tools/tools_kitchen.py:1053-1054, 1179-1180). The full-mode response already contains ingredients_table, diagram, suggestions, errors. There is no server-side reason to call twice.
  • What actually happened (direct rollout-log evidence): call 1 at 14:08:56 returned 43,857 tokens; the Codex code-mode exec harness middle-out truncated it to ~10,000 tokens, keeping the head (start of recipe content) and tail (which happened to include the complete ingredients_table, orchestration_rules, stop_step_semantics, deferred_guards, and the closing metadata) while cutting 33,857 tokens from the middle — including errors, diagram, and suggestions. Call 2 at 14:09:05 (ingredients_only:true) returned 15,195 tokens, also truncated, but small enough that errors/diagram/suggestions/ingredients_table all survived.
  • Why the harness truncated: both server-side guards were satisfied — the ~175KB payload is within the RESPONSE_BACKSTOP_EXEMPTION_REGISTRY["open_kitchen"] ceiling of 186,000 bytes (src/autoskillit/core/types/_type_constants_registries.py:191-202) and below CODEX_TOOL_OUTPUT_TOKEN_LIMIT = 54,500 (_codex_config.py:34, ADR-0005). The exec call passed no max_output_tokens at all — verified: the string's only occurrence in the entire rollout is the advisory digest text in the prompt (CODEX_INTAKE_DISCIPLINE_DIGEST, src/autoskillit/core/types/_type_constants.py:174). The ~10K bound is therefore the Codex code-mode harness's default output cap for script output when the model requests nothing larger. This is the flip side of the behavior ADR-0005 (lines 108–111) documents — model-declared max_output_tokens is honored unclamped: the bound is model-discretionary in both directions. Ask for too much and context blows out (the earlier 019f6b5a incident); ask for nothing and the default cap silently drops 3/4 of the payload (this incident).

Question 2 — the "error" in the second response. There is no error. "errors": [] is empty; errors is reserved for structural/parse/route failures (src/autoskillit/recipe/_api.py:370-431, 556-587). The visible items are two WARNING-severity suggestions emitted by semantic rules that run on every open_kitchen (via serve_recipeload_and_validaterun_semantic_rules, _api.py:475):

  1. required-ingredient-no-default on task (src/autoskillit/recipe/rules/rules_inputs.py:292-318, WARNING): fires because task is required: true with no default and not hidden. Every bundled recipe's primary task-type ingredient shares this exact shape (implementation.yaml, planner.yaml, all research*.yaml, etc.) — it is the intentional, universal convention, since "what to investigate" has no sensible default. Advisory noise for this ingredient category.
  2. file-writing-skill-missing-context-limit on rectify and dry_walkthrough (src/autoskillit/recipe/rules/rules_worktree.py:178-227, WARNING): factually correct — remediation.yaml rectify (lines 289–315) and dry_walkthrough (lines 334–351) invoke write_behavior: always skills with no on_context_limit route. But the identical gap exists in implementation.yaml's plan/verify steps, while both recipes DO wire on_context_limit: retry_worktree for implement (remediation.yaml:380, implementation.yaml:339). A real, systemic, low-priority gap — not a remediation-specific regression.

Neither warning affects valid (compute_recipe_validity, registry.py:256-265, counts only ERROR severity) or gates anything downstream. The reason they seemed to appear only in the second call: they were in the truncated middle region of the first response.

Affected Components

  • ~/.codex/sessions/2026/07/20/rollout-2026-07-20T07-08-43-019f7fdb….jsonl: primary evidence — both calls, truncation warnings, cut markers [SUPPORTED]
  • src/autoskillit/server/tools/tools_kitchen.py:765-1268: open_kitchen implementation; ingredients_only stripping at 1053-1054, 1179-1180 [SUPPORTED]
  • src/autoskillit/server/_misc.py:234-239: strip_ingredients_only_keys — strips only content/orchestration_rules/stop_step_semantics [SUPPORTED]
  • src/autoskillit/recipe/_api.py:188-660: load_and_validate — builds errors/suggestions/ingredients_table on every call [SUPPORTED]
  • src/autoskillit/recipe/rules/rules_inputs.py:292-318, rules_worktree.py:178-227: the two WARNING rules [SUPPORTED]
  • src/autoskillit/recipes/remediation.yaml:38-40, 289-315, 334-351, 380: task ingredient; rectify/dry_walkthrough (no on_context_limit); implement (has it) [SUPPORTED]
  • src/autoskillit/cli/_prompts_orchestrator.py:112-123: single-call FIRST ACTION prescription [SUPPORTED]
  • src/autoskillit/core/types/_type_constants.py:174 (CODEX_INTAKE_DISCIPLINE_DIGEST), _type_constants_registries.py:191-202 (backstop exemption), _codex_config.py:34 (Codex transport limit): the three layers whose interplay allowed the truncation [SUPPORTED]

Data Flow

open_kitchen(name)serve_recipeRecipeRepository.load_and_validate (repository.py:87-124_api.py:188) builds the full result dict (content ≈ 40K tokens, errors, diagram, suggestions, valid, ingredients_table, orchestration_rules, …) → optional strip_ingredients_only_keys → MCP response (within the 186KB server-side exemption budget) → Codex code-mode exec harness (tools.mcp__autoskillit__open_kitchen(...) inside a JS exec call) → middle-out truncation to ~10K tokens → model context. The truncation boundary is the only layer that cut the payload; every server-side layer passed it intentionally.

Test Gap Analysis

  • No test exercises a full-mode open_kitchen response traversing the Codex code-mode exec harness — the four output-budget layers are each tested in isolation; the end-to-end "full recipe payload reaches the orchestrator on any backend in one call" invariant has no test.
  • tests/recipe/test_bundled_recipes_all_truthy.py filters suggestions to severity == "error" only, so warnings on bundled recipes pass silently and accumulate; no zero-warning or allowlist assertion exists for remediation.yaml.
  • Each open_kitchen mode is tested independently (tests/server/test_tools_kitchen_gate_features.py:119-163, test_tools_kitchen_envelope.py); no sequential two-call workflow test exists (closest is test_tools_kitchen_cache_poison.py, which is cache-integrity framed).

Similar Patterns

The implement step's on_context_limit: retry_worktree route (remediation.yaml:380, implementation.yaml:339) is the established pattern the warning rule asks for; recipe authors applied it where stranded writes are most costly (worktree mutation) and left plan/markdown-writing steps (rectify, dry_walkthrough, plan, verify) unrouted. For payload-size collisions, the established mitigations are per-path: ingredients_only (dispatch), deferred-recall stripping (Codex pre-reveal), RESPONSE_BACKSTOP_EXEMPTION_REGISTRY (Claude Code disk gate) — each fixed one boundary at a time.

Design Intent Findings

Historical Context

Prior fixes found — this is the fourth manifestation of the same underlying weakness in ~2.5 months, each at a different boundary:

  1. Fleet dispatch: L3 session should auto-discover recipe ingredients without loading full recipe #1803/PR Fleet dispatch L3 session should auto-discover recipe ingredients without loading full recipe #1827 (a2f066d39) — fleet dispatch paying full recipe cost → ingredients_only.
  2. Double open_kitchen context cost: recall re-sends full recipe; Codex amplifiers #4089/PR [FIX] Double open_kitchen Context Cost — Pre-Reveal / Deferred-Recall Architectural Immunity #4092 (fbfb3589e) — deferred-recall resending full content → stripping applied.
  3. open_kitchen response exceeds Claude Code MCP output cap — step graph truncated from orchestrator context, causing out-of-order step execution (review_approach before rectify) #4253/PR Server-authoritative payload delivery for open_kitchen output cap #4259 (1e2670ec7) — Claude Code ~100KB disk-persistence gate truncating open_kitchen to a 2KB preview → 186KB backstop exemption.
  4. This incident — Codex code-mode exec harness truncating the full-mode response at the advisory ~10K-token bound, which no prior fix covers. No existing GitHub issue tracks it (searches for "open_kitchen truncation", "codex truncat" returned nothing relevant; Stale pipeline tracker from unclosed kitchen silently disables step-completion marking — false DEPENDENCY UNMET aborts pipeline; _fmt_run_skill drops error field #4293 is a distinct kitchen-tracker defect).

ADR-0005 itself frames recipe-payload/boundary collisions as "the recurring failure class behind #4253, #2819, #2564, #3938."

This is a recurring pattern — consider running /rectify for architectural immunity after resolving the immediate issue.

An operational consequence worth noting: the Codex orchestrator in session 019f7fdb proceeded with only ~10K of 44K tokens of recipe context — most of the recipe body never reached the model (the orchestration rules, ingredients table, and deferred_guards survived in the truncated tail — by serialization-order luck, not by design), which still raises deviation risk for the rest of that pipeline run.

External Research

Not applicable — all evidence was local (session rollout logs, source, git history, GitHub issues via gh). Prior internal investigation of Codex context behavior (session 12326fb2, 2026-07-20 06:48, re: Codex session 019f6b5a) established that code-mode honors model-supplied max_output_tokens without an upper clamp — the unbounded intake failure direction. That does not conflict with this incident: here no max_output_tokens was supplied and the harness's default cap applied. Same root design (model-discretionary bound, no enforced config-level control), opposite symptom.

Scope Boundary

Investigated: the Codex rollout log for the session in question; open_kitchen implementation and both response modes; the two semantic rules and their severities; remediation.yaml ground truth vs. other bundled recipes; orchestrator/kitchen prompt builders; introducing commits for all mechanisms; the four output-budget layers; GitHub issue history; test coverage.
Not yet explored: Codex-source confirmation of the exact default that produced the ~10K cut in this specific session (model-declared max_output_tokens per the digest vs. a harness default — the digest rule matches the observed bound, but the model's encrypted reasoning prevents direct confirmation); whether other MCP tool responses in that session were similarly truncated; downstream behavior of session 019f7fdb after the user's task was provided.

Recommendations

  1. File a tracking issue for the real defect: full-mode open_kitchen responses exceed what the Codex code-mode exec harness will deliver, so the Codex interactive orchestrator can never receive the complete recipe payload in one call. The fix direction suggested by ADR-0005's own analysis: an enforced (config/harness-level) transport bound or max_output_tokens floor for MCP calls in code mode — analogous to the ADR-0006 shell_capture_hook pattern — rather than the current prompt-level advisory digest; or per-backend serving that keeps the full-mode payload under the harness bound (e.g., a compact full-mode variant for Codex).
  2. Do not "fix" the double call itself — it was rational recovery and is cheap relative to the alternative (operating blind). Fixing the truncation removes the trigger.
  3. Warnings: treat required-ingredient-no-default on task-type ingredients as advisory noise (optionally exempt task-category ingredients from the rule, or accept the warning); the file-writing-skill-missing-context-limit findings are legitimate — if addressed, add on_context_limit routes to rectify/dry_walkthrough (and implementation.yaml plan/verify) following the existing implement → retry_worktree pattern, as a deliberate decision rather than warning suppression.
  4. Test hardening: an end-to-end assertion that bundled full-mode payloads fit every backend's delivery bound (the invariant ADR-0005 gestures at), and an allowlist-based zero-new-warnings test for bundled recipes so new WARNING-rule firings surface at PR time instead of at kitchen-open time.

Confidence Levels

  • Second call caused by first-response truncation (sequence + truncation warnings + cut field inventory): SUPPORTED
  • Single-call design for interactive flow; ingredients_only intended for dispatch: SUPPORTED
  • "errors": [] + WARNING suggestions is normal, non-blocking output: SUPPORTED
  • Warnings present in both responses; hidden in the first by the middle cut: SUPPORTED
  • Missing on_context_limit on rectify/dry_walkthrough is real but systemic: SUPPORTED
  • No explicit max_output_tokens on the exec calls; the ~10K cut is the harness default cap: SUPPORTED (verified against the full rollout log)
  • The exact Codex-source constant implementing that default cap: NEEDS-EVIDENCE (lives in Codex source, outside this repo)
  • Model's motivation for the recovery call (recover the cut errors/suggestions/diagram validity signals): NEEDS-EVIDENCE (inferred from the truncation warning; reasoning not inspectable)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugExisting behavior is brokenrecipe:remediationRoute: investigate/decompose before implementationstagedImplementation staged and waiting for promotion to main

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions