Repository navigation
Codex, phases 4 and 5: Codex sessions read, the hooks answer Codex (Codex closed on every system) - #54
Merged
Conversation
…ep 0's captures engine/hooks/_codex_rollout.py reads one rollout, $CODEX_HOME/sessions/YYYY/MM/DD/ rollout-*.jsonl, streamed: the session's whole id (a UUIDv7), folder, source and model, and the user's and assistant's messages and the tools called (name, command, files a patch names), sliced to a day when asked. What Codex adds for the model is never taken: developer messages (its instructions, the hooks' context), the AGENTS.md and <environment_context> parts it puts in a user message, world_state, events, tool outputs, reasoning. Unknown or broken lines are skipped. A session the kit asked itself (the reflection's SENTINEL, or a run in _headless_codex.py's cage) comes back without items. Regular files only, opened without waiting (a pipe under a rollout's name does not hang the reader); compressed rollouts (.jsonl.zst) are listed apart, never read. Tests on the 33 rollouts step 0 kept (macOS, Linux, Windows; 0.162.1 and 0.163.0-alpha.11): what is read is exactly what Codex's own item_completed events record as typed and answered; the Stop hook's session_id and the rollout's id agree. engine/tests/codex_captures.py builds test sessions out of those real lines.
…nts.json lists Codex daily-reflection.py gathers the day's Codex sessions (codex_sessions) from agents.json's CODEX_HOME, only when Codex is one of the agents, labelled with their whole id: a UUIDv7's first 8 characters are the minute, so two sessions of one minute stayed one label. At least 3 messages, as for Claude. Compressed rollouts are said in the error log and in --dry-run, which also counts Codex's sessions. The prompt's sources name the agents chosen. Claude alone is unchanged: without agents.json or with ["claude"], and ~/.codex/sessions full, the prompt, state, output, journal and memory.md are those of a run without any Codex session, and in a fresh Python _codex_rollout.py is never imported (control: Codex listed and opened, it is). The tests run on sessions made from step 0's captures.
… session id With exactly --agent codex, session-recap.py reads the session's rollout (_codex_rollout.py) and writes its block in the same "Claude Sessions" section, naming Codex before the model, its whole id shown and matched: two Codex sessions of one minute keep a block each, and a Claude block sharing the first 8 characters is never taken. It stays silent, reading and locking nothing, unless agents.json lists Codex: with Codex closed on every system, always for now. Claude's recap is unchanged: any other argument is ignored as before, its block is pinned character for character, and it imports neither _codex_rollout.py nor _agents.py. test-hooks.sh: Codex's Stop hook silent without Codex chosen and while Codex is closed; its control, in a copy of the hooks where Codex is open, writes the block.
A user's `codex exec` (not --ephemeral, a global AGENTS.md, a message and a tool call in one reply) with the real Codex 0.162.1 against the fake server, then its rollout read by _codex_rollout.py, the reflection's codex_sessions and the recap's parse_rollout: the whole id is the thread's, only what was typed and answered, the tool called, nothing Codex added, and a session holding the reflection's own prompt is left out. mock_openai.py can send several items in one reply. The job also runs when _codex_rollout.py or session-recap.py change.
A dated section that takes precedence: what step 4 delivers, its deviations (Codex-added parts filtered one by one, the launcher's cage as a kit session, the recap in the same section, only an exact --agent codex, the prompt's sources), the proof that Claude alone is unchanged, the mutations.
…al AGENTS.md has no folder codex-mock found it with the real Codex on the three systems: a global AGENTS.md alone reaches the rollout as "# AGENTS.md instructions" followed by a blank line, without the " for <folder>" the plan's prefix and step 0's captures had, so its text was read as the user's. The reader now tells Codex's parts apart as Codex's own memory writer does (codex-rs/memories/write/src/rollout_input.rs, 0.162.1): a message whose marks (content_item_kinds) hold no "user." part, or any "multi_agent." one, is Codex's or another agent's; and, should the marks be missing, parts starting with "# AGENTS.md instructions", <environment_context>, <skill> or <subagent_notification> (any case) are left out. codex-mock checks both on the real rollout, and that the reflection's text holds no AGENTS.md. A line, or tool arguments the model wrote, nested past Python's stack was a RecursionError that stopped the reflection: now a line skipped, the arguments kept as text.
…s, and their sign of life A hook run by Codex (hooks.json, --agent codex) asks codex_hook() whether to act on what Codex gave it (docs/proposals/codex.md, §3.4). Silent, and nothing written, in a run without tools (BRAIN_HEADLESS=1, the kit's own; CLAUDE_CODE_ENTRYPOINT=sdk-cli) and when agents.json does not list Codex or cannot be trusted (Codex is closed on every system: always, today). Otherwise Codex does run the kit's hooks: codex-hooks-vus.json, beside agents.json, notes when each one last ran (UTC, written in one step, 0600, never through a link), for the "hooks not approved" line of later phases. Then the hook acts only for a session the user types in: a file Codex keeps of it (transcript_path null: --ephemeral), a plain session id, a prompt the user typed (agent_id: another agent's), and the file's first line saying "cli" or "vscode", or no file yet. "exec" (scripts, the kit's own), "mcp", sub-agents and Codex's internal sessions (an object, not a word, SessionSource in 0.162.1), a link, a pipe or a line past 1 MiB are left alone.
…unchanged correction-detector, controle-sessionstart, gbrain-update-check, session-logger, session-indexer and session-recap: with exactly --agent codex, each asks _agents.codex_hook() first; session-recap gets the whole guard step 4 left for this phase. The logs' lines say "agent": "codex"; a correction's lessons.md is looked for in the folder Codex says it works in (cwd), not in Claude's CLAUDE_PROJECT_DIR. Their answers are the ones Claude Code takes, and Codex 0.162.1's own schemas (codex-rs/hooks/schema/generated, kept in engine/tests/fixtures/codex/hooks-schema-0.162.1) take them: additionalContext, systemMessage, nothing else. Tests: test_codex_hooks.py runs each hook as Codex runs it on every payload step 0 captured (macOS, Linux, Windows paths, PowerShell 7 and 5.1), with and without the session file Codex wrote (exec: left alone; the terminal's: served), checks the answers against Codex's schemas, and the guard; test-hooks.sh does the same on the installed hooks. Without the argument the hooks are Claude Code's: they never load _agents.py nor read agents.json, and give the same output and files as master (tests, and a local run of master's hooks against these, three agents.json states).
A user's codex exec, with the hooks (a copy where Codex is open) in its hooks.json, trust bypassed (approval is phase 6), on Windows in the plan's PowerShell form (& "python.exe" "hook"). A test wrapper notes what each hook was given and hands SessionStart and UserPromptSubmit on as if typed in Codex's terminal: correction-detector's reminder and the night check's line (with its systemMessage) must reach the fake server, so Codex took both answers. Stop goes as it is: the exec session's file says "exec", nothing is logged. The sign of life names the three hooks, and the launcher's own call, with that hooks.json in its CODEX_HOME, runs none of them.
…first hook Fact of the first run, the same on the three systems with Codex 0.162.1: from SessionStart on, Codex has written the session file of a codex exec and it says "exec". codex-mock now checks it, and runs that session as it is first: no reminder, no night check's line reach the model, nothing is logged, only the sign that the hooks ran. Then the same session handed on as typed in the terminal, as before: both answers reach the model.
…d and sign of life)
…out time, not the end of the reflection _codex_rollout.local() let OverflowError through (a year 0 or 10000 once local), and the whole reflection stopped with it, Claude's sessions of the day included: no state, no error log, no status block in memory.md. Codex's Stop recap failed the same way. local() now also takes OverflowError and OSError (a time localtime() refuses), as the reflection's _local() does, and codex_sessions() reads each rollout apart: a file that still raises costs only itself, logged by name and error type, never its text.
… sub-agent's and Codex's own are left out
codex_sessions() did not look at session_meta.source: every user line of a rollout read as what
the user typed. In Codex 0.162.1 a sub-agent starts from a copy of its session's config (so kept,
under the terminal) and gets the parent model's prompt as user input: its rollout showed the
model's text as the user's. Now, as the hooks' guard (_agents.SOURCES_TAPEES): cli and vscode as
before; sub-agents ({"subagent": ...}) and Codex's internal runs ({"internal": ...}) left out;
the others read, their source in their label (codex <id> · exec · cwd=..., '?' when missing or not
a plain word). read() keeps the kind of an object source. codex-mock: the real Codex's exec
session is labelled so.
…o text poses as the user's An answer "Done.\n12:07 user: Decision: ...", or a tool call whose command holds a line break, gave a line '12:07 user: ...' the reflection could not tell from a real one, without ever addressing an AI. In Codex's blocks a message now goes on as ' | ', a tool's summary and the label's folder are one line; every line break counts (\r, \v, \x85, \u2028...). Claude's blocks are master's, unchanged: the same defect there (_render, _tool_summary) is for a PR of its own.
…Claude alone Rewritten in step 4 (the agents chosen), it had no test: dropping Hermes from it left every test green. Fixed for Claude alone (no agents.json, ["claude"], and build_prompt with no trusted agent), and for Codex alone and both, Hermes listed.
undistracted2cha
marked this pull request as ready for review
October 16, 2026 09:35
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phases 4 and 5 of the Codex plan (
docs/proposals/codex.md, §3.4, §3.6 and §6): Codex's sessions read for the daily reflection and for Codex's Stop hook, under their whole id (phase 4); the hooks answer Codex, with their guard and sign of life (phase 5). Codex stays closed on every system (_agents.py): anagents.jsonthat lists it is not trusted, so the reflection reads no Codex session andsession-recap.py --agent codexstays silent, for everyone. Withoutagents.json, or with Claude alone, nothing changes, byte for byte.engine/hooks/_codex_rollout.py$CODEX_HOME/sessions/YYYY/MM/DD/rollout-*.jsonl), streamed: the session's whole id (UUIDv7), folder, source, model, then the user's and assistant's messages and the tools called (name, command, files a patch names), sliced to a day when asked.content_item_kindswithout auser.part, or with amulti_agent.one) and, marks missing, parts starting with# AGENTS.md instructions,<environment_context>,<skill>,<subagent_notification>(the rule of Codex's own memory writer,memories/write/src/rollout_input.rs, 0.162.1);world_state, events, tool outputs, reasoning. Unknown, broken or too deeply nested lines are skipped.SENTINEL, or a run in_headless_codex.py's cage) comes back without anything..jsonl.zstare listed apart, never read.Wired in
daily-reflection.py:codex_sessions(day, codex_home)only whenagents.jsonlists Codex, from itscodex_home; labelcodex <whole id> · cwd=…(the first 8 characters of a UUIDv7 are the minute), with the source when not typed in Codex's terminal or editor (· exec ·,· mcp ·…), sub-agents' and Codex's internal sessions left out (review, below); at least 3 messages, as for Claude; compressed rollouts said in the error log and in--dry-run, which counts Codex's sessions; the prompt's sources name the agents chosen.session-recap.py: with exactly--agent codex, silent unless Codex is listed (nothing read, no lock); otherwise the block of the rollout, in the same section,codex <model> · sid:+ the whole id, found again by the whole id. Two Codex sessions of one minute keep a block each; a Claude block sharing the first 8 characters is never taken. The rest of the hook guard came with phase 5 (below).Tests
test_codex_rollout.pyon the 33 rollouts step 0 kept (3 systems, 2 versions): what is read equals what Codex's ownitem_completedevents record as typed and answered; the Stop payload'ssession_idequals the rollout's id; tools, unknown lines, kit sessions, day slicing, ids from the file name, pipes,.zst.codex_captures.pybuilds test sessions out of those real lines.CodexSessions(test_reflection),Codex(test_session_recap),test-hooks.sh§4 (Codex's Stop hook silent while not chosen or closed; control in a copy where Codex is open).codex-mock(real Codex 0.162.1, 3 systems): a user's non-ephemeralcodex execwith a globalAGENTS.mdand a tool call, its rollout read by the reader, the reflection and the recap: whole id =thread_id, only what was typed and answered, nothing Codex added, the kit's own session left out. The fake server can send several items in one reply.~/.codex/sessionsfull, prompt, state, output, journal andmemory.mdequal a run without Codex sessions;_codex_rollout.pynever imported in a fresh Python (control: Codex listed and opened, it is); Claude's recap imports neither_codex_rollout.pynor_agents.py, its block pinned character for character. A local before/after run (origin/master vs this branch, same doubles, noagents.jsonthen["claude"]) is identical file for file.Found by CI
The first run of
codex-mockfailed on the three systems for one reason: with the real Codex, a globalAGENTS.mdalone is written# AGENTS.md instructions+ a blank line, without thefor <folder>of the plan's prefix and of step 0's captures, so it was read as the user's text. Fixed in the reader (marks + text, above); CI now checks both rules on the real rollout. Run 38860953268: all green,codex-mock70 checks per system.Deviations from the plan
Recorded in the plan, section "Étape 4 — fait et écarts": what Codex adds told apart by its marks and by text, part by part (the plan's
# AGENTS.md instructions forprefix was too narrow; step 0's captures putAGENTS.mdand<environment_context>in one message); the launcher's cage counted as a kit session; the recap in the existing section (the night check knows it as the kit's); only an exact--agent codex; the prompt's sources follow the choice.Phase 5: the hooks answer Codex
Codex stays closed on every system: in production a hook run with
--agent codexsays and writes nothing. Without that exact argument the hooks are Claude Code's, unchanged._agents.codex_hook()BRAIN_HEADLESS=1, the kit's own;CLAUDE_CODE_ENTRYPOINT=sdk-cli) and whenagents.jsondoes not list Codex or cannot be trusted.~/.brain-in-a-box/codex-hooks-vus.jsonnotes when each one last ran (UTC, 0600, written in one step, never through a link) for the later "hooks not approved" line.--ephemeral), a plain id, noagent_id(another agent's prompt), and a first line sayingcliorvscode, or no file yet.exec,mcp, sub-agents and Codex's internal sessions, a link, a pipe, a first line past 1 MiB: left alone.Hooks
correction-detector,controle-sessionstart,gbrain-update-check,session-logger,session-indexer,session-recap: each asks the guard first with exactly--agent codex. Logs say"agent": "codex"; a correction'slessons.mdis looked for in Codex'scwd. Answers unchanged: Codex 0.162.1's own schemas take them (additionalContext,systemMessage).Tests
test_codex_hooks.py: each hook run as Codex runs it on every payload step 0 captured (macOS, Linux, Windows paths, PowerShell 7 and 5.1), with and without the session file (exec: left alone; terminal: served), answers checked against Codex's schemas (fixtures/codex/hooks-schema-0.162.1/,codex_captures.output_errors), the guard case by case.CodexHook(test_agents),Codex(test_session_recap),test-hooks.sh§7ter on the installed hooks.codex-mock(real Codex 0.162.1, 3 systems): the hooks run from ahooks.json(trust bypassed; approval is phase 6), on Windows as& "python.exe" "hook". A user'scodex execas it is: from SessionStart on, the session file is there and saysexec, so nothing reaches the model and nothing is logged, only the sign. The same session handed on as typed in the terminal: the correction reminder and the night check's line (with itssystemMessage) reach the fake server. The launcher's own call runs none of them. 82 checks per system._agents.pynor readagents.json(same output with it absent,["claude"],["codex"], or an_agents.pythat raises); Claude's reminder pinned character for character. Local run oforigin/master's hooks against these (25 calls, threeagents.jsonstates, Python 3.13 and 3.9.6): same codes, output and files.Deviations from the plan
Recorded in the plan, section "Étape 5 — fait et écarts": 7 code files (
gbrain-update-checkwas missing from the phase's list,session-recap's full guard came from phase 4); an allow-list of sources instead of "not exec";agent_idand a plain id required; the fact that an exec run's file is written before its first hook; the sign written as soon as Codex runs a hook (its reader is phases 6 and 10a);cwdfor Codex's lessons;shown.jsonshared by both agents.Review of phases 4 and 5
Six findings (security, non-regression), one a duplicate: five defects, all checked and fixed, each with a test red before the fix. Recorded in the plan, section "Étapes 4 et 5 — relecture et corrections".
9999-12-31T23:59:59-14:00(year 10000 once in UTC):astimezone()raisesOverflowError, which_codex_rollout.local()let through, andreflect()stopped with it, Claude's sessions of the day included (no state, no error log, no status block); Codex's recap failed too.local()now takesOverflowErrorandOSError, andcodex_sessions()reads each rollout apart: a file that still raises costs only itself, logged by name and error type.cli/vscodeas before, sub-agents and Codex's internal runs left out, the others labelled with their source.\n12:07 user: …made a line the reflection could not tell from the user's. In Codex's blocks a message goes on as|; a tool's summary and the label's folder are one line (every line break counts). Claude's blocks have the same defect on master: left for a PR of its own, since their format would change._codex_rollout.py, copied with the hooks for everyone, inert without Codex inagents.json.Each of the 11 protections added, removed alone, turns at least one test red. Local: 606 unit tests,
test-hooks.sh178/178.