Skip to content

feat: let the owner exempt an agent's jobs - #3

Merged
fus3r merged 3 commits into
mainfrom
feat/agent-bypass
Sep 28, 2026
Merged

fus3r merged 3 commits into
mainfrom
feat/agent-bypass

Conversation

@fus3r

@fus3r fus3r commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

Supported path and problem

Coding agents such as Claude Code and Codex are told to run long jobs under train-guard. The owner sometimes wants one agent's jobs to run at full speed whatever the power and temperature, for example a short verification on battery, without turning the policy off for every other job. train-guard had no way to tell which agent started a job.

Behavior before and after

  • Before: every guarded job follows the policy.
  • After: run records the agent session that starts a job, from --agent ID or else CLAUDE_CODE_SESSION_ID, then CODEX_THREAD_ID, which Claude Code and Codex export to the commands they run. attach records an agent only from an explicit --agent, since the session that attaches did not start the job. Restart specifications keep the original agent. A job whose agent the owner lists in <state>/ignored-agents runs full with the live-only reason agent_ignored. The policy still decides every cycle, so its thermal cooldown stays true to the pack: once the agent leaves the list, a job whose pack got hot stays paused until it cools to temp_resume_c. The supervisor rereads the list on every cycle, as a regular UTF-8 file (a BOM is accepted) of at most 64 KiB; invalid lines are skipped and reported by line number. An unreadable list exempts no agent, is journaled once per distinct error, and is reported by status, list and doctor. status shows each guard's agent and the ignored agents; the JSON outputs gain agent, agent_ignored and ignored_agents.

The override reason is a separate OverrideReason, not a DecisionReason member: the native kernel numbers DecisionReason in order and its differential test requires the policy to produce every member. simulate and sweep outputs are byte-identical to main for nominal, compare and bounded runs.

Reproduction

Live, on a MacBook on battery with a temporary TRAIN_GUARD_HOME: run --name probe -- sleep 120 from a Claude Code session recorded its session id; the job was suspended (stop, battery_disabled); after listing the id it resumed within one poll with full/agent_ignored; after removing it, it was suspended again with stop/battery_disabled.

Validation that ran

pytest on Python 3.13.7 and 3.9.6 (246 passed), ruff check, ruff format --check, mypy trainguard, branch coverage 90.59%, compileall, build, mkdocs build --strict, git diff --check, the native kernel tests, and the release workflow's source-archive retest and wheel smoke test run locally.

Review

An independent review of the first commit found no process-safety defect and two design problems, both fixed in the second commit: attach credited the target to the session running it, and the override cleared the thermal cooldown, so a hot pack could resume gentle too early once the agent left the list.

Limits

The exemption changes the workload policy only; macOS thermal protection is unaffected. Jobs started before this change have no agent. Windows was not run locally; CI covers it.

run and attach record the agent session that starts a job, from --agent or
the CLAUDE_CODE_SESSION_ID or CODEX_THREAD_ID variable, and restart specs
keep it. A job whose agent the owner lists in ignored-agents runs full with
the reason agent_ignored, whatever the power and temperature, and follows the
policy again once the agent leaves the list. The reason is live-only, so
replay, sweeps and the native kernel keep their outputs and protocols.
attach no longer credits the session running it, since someone else started
the process it guards; it records an agent only from --agent. For an ignored
agent the policy still decides every cycle, so its thermal cooldown is kept
and a job taken off the list stays paused until the pack cools, as replay
shows; decision events record the policy's own decision under policy.

The ignore list is read without blocking on a FIFO, as a regular UTF-8 file
of at most 64 KiB with an optional byte order mark, and invalid lines are
skipped and reported by line number. A malformed CLAUDE_CODE_SESSION_ID falls
through to CODEX_THREAD_ID. Status marks an agent as listed and shows the
ignored agents only when the list or agents are in use.
Path.write_text translates newlines on Windows, so the CRLF line became CR CR LF and the invalid entry moved to line 6. Writing the bytes keeps the file exactly as a Windows editor saves it.
@fus3r
fus3r merged commit f884a20 into main Sep 28, 2026
14 checks passed
@fus3r
fus3r deleted the feat/agent-bypass branch September 28, 2026 07:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant