diff --git a/docs/decisions/0004-recipe-redelivery.md b/docs/decisions/0004-recipe-redelivery.md new file mode 100644 index 0000000000..20ee35d8c9 --- /dev/null +++ b/docs/decisions/0004-recipe-redelivery.md @@ -0,0 +1,48 @@ +# ADR-0004: Codex Recipe Knowledge Re-Delivery via load_recipe + +**Status:** Accepted +**Date:** 2026-06-27 +**Issue:** [#4051](https://github.com/TalonT-Org/AutoSkillit/issues/4051) + +## Context + +Codex auto-compaction fires at 90% of the 258K context window. When triggered, it can +destroy recipe content delivered by `open_kitchen` in the initial orchestrator response. +After compaction, the agent loses the recipe steps it needs to complete the pipeline. + +Two defenses exist: + +1. **Primary — disable compaction**: `ensure_codex_mcp_registered` writes + `model_auto_compact_token_limit = 999_999_999` (an unreachable threshold) to + `~/.codex/config.toml`. `setup_session_dir` copies this config into each session + directory, ensuring every headless session inherits the setting. + +2. **Guard — block raw recipe reads**: The `recipe_read_guard.py` PreToolUse hook + blocks `run_cmd`/`Bash` from reading recipe YAML, SKILL.md, or agent definition + files, and blocks `run_python` from calling `autoskillit.recipe.*` callables. + This prevents the agent from self-recovering from compaction loss via raw file + access — compaction loss is treated as a hard failure, not a recoverable state. + +If the primary defense is ever relaxed (e.g., Codex fixes its compaction behavior or +the context window grows), the agent needs a sanctioned channel to re-acquire recipe +knowledge without reading raw files. + +## Decision + +> **`load_recipe` (the MCP tool exposed by `server/tools/tools_recipe.py`) is the +> sanctioned channel for recipe knowledge re-delivery after context compaction.** + +If recipe content is lost, the agent must call `load_recipe` to re-acquire it. The +`recipe_read_guard` enforces that only this path is allowed — raw file access is denied. + +This is a forward obligation: when the primary defense (unreachable auto-compact limit) +is relaxed, `load_recipe` re-delivery must be tested end-to-end as the recovery path. + +## Consequences + +- `recipe_read_guard.py` error messages direct the agent to call `load_recipe`. +- No production code changes are needed today — the channel already exists and works. +- Future work relaxing `CODEX_AUTO_COMPACT_LIMIT` must add integration tests verifying + `load_recipe` re-delivery restores full pipeline execution capability. +- `test_copied_config_has_auto_compact_limit` validates the primary defense path: + `setup_session_dir` preserves the override in the copied `config.toml`. \ No newline at end of file diff --git a/docs/decisions/README.md b/docs/decisions/README.md index e3727494c4..75de7f9edc 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -3,3 +3,4 @@ - [0001-prohibit-background-subagent-execution.md](0001-prohibit-background-subagent-execution.md) — prohibit `run_in_background: true` in all skills - [0002-ban-inline-shell-scripts-from-cmd.md](0002-ban-inline-shell-scripts-from-cmd.md) — Prohibit inline shell scripts in recipe cmd fields; require externalization to .sh files or run_python callables - [0003-skill-args.md](0003-skill-args.md) — Pass skill inputs as positional arguments, not environment variables +- [0004-recipe-redelivery.md](0004-recipe-redelivery.md) — Sanctioned `load_recipe` channel for recipe knowledge re-delivery after Codex context compaction diff --git a/tests/execution/backends/test_codex_backend.py b/tests/execution/backends/test_codex_backend.py index e1a2c5044d..247a26c62c 100644 --- a/tests/execution/backends/test_codex_backend.py +++ b/tests/execution/backends/test_codex_backend.py @@ -1753,3 +1753,26 @@ def test_no_git_subdir_created(self) -> None: self._write_all_source_files() CodexBackend().setup_session_dir(self.session_dir) assert not (self.session_dir / ".git").exists() + + def test_copied_config_has_auto_compact_limit(self) -> None: + import tomllib + + from autoskillit.execution.backends import CODEX_AUTO_COMPACT_LIMIT + + (self.codex_home / "config.toml").write_text( + f"model_auto_compact_token_limit = {CODEX_AUTO_COMPACT_LIMIT}\n" + "[mcp_servers.autoskillit]\n" + ) + (self.codex_home / "auth.json").write_text("{}") + CodexBackend().setup_session_dir(self.session_dir) + data = tomllib.loads((self.session_dir / "config.toml").read_text(encoding="utf-8")) + assert data["model_auto_compact_token_limit"] == CODEX_AUTO_COMPACT_LIMIT + + def test_session_config_lacks_key_when_source_lacks_it(self) -> None: + import tomllib + + (self.codex_home / "config.toml").write_text("[mcp_servers.autoskillit]\n") + (self.codex_home / "auth.json").write_text("{}") + CodexBackend().setup_session_dir(self.session_dir) + data = tomllib.loads((self.session_dir / "config.toml").read_text(encoding="utf-8")) + assert "model_auto_compact_token_limit" not in data