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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,13 @@ run, `scripts/impasse_run.py set-model --backend codex <name>` to persist, or th
`IMPASSE_CODEX_MODEL` / `IMPASSE_CLAUDE_MODEL` env var. Precedence: flag > env > persisted > default.
Pinning a model *different* from the host's buys a little extra independence *within* a rung (a different model, same provider).

**Execution speed (Fast mode):** Codex has a **Fast mode** service tier (**codex-only**) that trades
a **higher credit cost** for faster serving. It's off by default (`standard`). Set it the same way as
the model: `--speed fast` per run, `scripts/impasse_run.py set-speed fast` to persist (clear with
`--clear`), or the `IMPASSE_CODEX_SPEED` env var. Precedence: flag > env > persisted > default
(`standard`). It's **independent of `--effort`**, and — like the model and effort — the host can set
it for you conversationally ("always use fast mode"). The claude backend has no speed knob.

**Fast checks (`--raw`):** for a quick, low-stakes look at your own work, `review --raw` returns the
reviewer's findings and skips the verify → reconcile → escalate protocol (and doesn't record). The
findings are **unverified** — the host hasn't checked them — so use the full protocol when it matters.
Expand Down
27 changes: 26 additions & 1 deletion SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ backend is the cross-provider reviewer). The host is auto-detected (`IMPASSE_HOS
python3 "$IMPASSE_ROOT/scripts/impasse_run.py" review \
--kind <code|document|decision|research|data|other> \
--instruction-file <instr.txt> --artifact-file <artifact> \
[--backend auto|codex|claude] [--model <name>] [--approve-send <endpoint>] [--effort none|low|medium|high|xhigh] [--wall 300] [--idle 300]
[--backend auto|codex|claude] [--model <name>] [--approve-send <endpoint>] [--effort none|low|medium|high|xhigh] [--speed standard|fast] [--wall 300] [--idle 300]
```
It returns JSON: on success, `response` is the reviewer's **untrusted** structured output;
on failure, a `failure` with a `code`
Expand Down Expand Up @@ -221,6 +221,31 @@ backend is the cross-provider reviewer). The host is auto-detected (`IMPASSE_HOS
claude backend has no effort knob — nothing resolves for it and any result that reaches backend
resolution reports `effort: null`. **Scale `--wall` to the resolved effort** (see Timeouts
above) — raising effort without raising the wall trades findings for timeouts.

**Speed (Fast mode).** A separate **codex-only** service-tier knob, **independent of effort**.
Precedence: `--speed <standard|fast>` (this run) > `IMPASSE_CODEX_SPEED` env > persisted default
(`impasse_run.py set-speed <standard|fast>`, clear with `--clear`) > **`standard`** (Fast mode
**off**, the default). `fast` turns Codex **Fast mode** on — roughly ~1.5× faster serving at a
**higher credit cost** — via `-c service_tier="fast" -c features.fast_mode=true`; `standard`/unset
add nothing. Values are allowlisted at every entry; a bad `IMPASSE_CODEX_SPEED` is a structured
`backend_error` naming the var, not a traceback. The claude backend has no speed knob — nothing
resolves for it and it reports `speed: null`. A codex run always reports the resolved `speed`
(`standard` or `fast`) alongside `model` and `effort`. Speed and effort compose freely (e.g. high
effort **and** fast mode).

**Letting the operator choose model / effort / speed interactively.** These three reviewer knobs
share one rule: when the operator asks to choose or change any of them (or you offer), the runner
can't prompt, so present the options yourself with **`AskUserQuestion`** — for **speed**, offer
`standard` vs `fast` and note it is **codex-only** and that `fast` costs more credits; for
**effort**, the `none|low|medium|high|xhigh` scale (also codex-only); for **model**, a short
curated candidate list plus an "other" free-text choice (Codex has no model-list command; a bad
model fails with a clear 400). Then map their answer to scope: a **per-run** request ("review at
high effort with fast mode") becomes `--effort` / `--speed` (and `--model`) on **that run**; a
**persistent** request ("always use fast mode", "default my reviewer to <model>") becomes the
matching **`set-*`** command (`set-speed` / `set-effort` / `set-model`, clear with `--clear`). The
precedence is the same for all three — per-run flag > `IMPASSE_*` env > persisted `set-*` default >
the backend default — and **effort and speed are codex-only** (the claude backend reports both as
`null`).
4. **Treat `response` as partially validated.** The runner confirms it's JSON with the required
top-level fields; full schema validation runs in CI (`tests/validate_schemas.py`), not at
runtime. Don't rely on fields the runner didn't check without validating them yourself.
Expand Down
18 changes: 18 additions & 0 deletions docs/backends/codex.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ codex exec --json --output-last-message <file> \
--sandbox read-only --color never --skip-git-repo-check --ephemeral \
--ignore-user-config --ignore-rules \
[-m <model>] [-c model_reasoning_effort="low"] \
[-c service_tier="fast" -c features.fast_mode=true] # only when --speed fast (Fast mode ON) \
"<reviewer instruction + the reviewer-response schema>" # artifact piped on stdin, then EOF
```

Expand Down Expand Up @@ -103,6 +104,23 @@ with it (see SKILL.md Timeouts). The claude backend has no effort equivalent: no
for it (an `IMPASSE_CLAUDE_EFFORT` is ignored, never an error) and it reports `effort: null`.
The argv builder re-checks the allowlist before interpolating into `-c` (defense in depth).

## Fast mode / service tier

A separate codex-only knob, **independent of reasoning effort**: `--speed fast` selects Codex's
**Fast mode** service tier — roughly ~1.5× faster serving at a **higher credit cost** — by adding
`-c service_tier="fast" -c features.fast_mode=true`. `standard` (or unset) is the default and adds
no flags (Fast mode off). Precedence mirrors effort: `--speed` (per run) > `IMPASSE_CODEX_SPEED`
env > a persisted default (`impasse_run.py set-speed <standard|fast>`, stored in `settings.json`
beside the model/effort defaults) > `standard`. The two-value allowlist (`standard|fast`) is
enforced at every entry point — the CLI flag and `set-speed` by argparse choices, `set_default_speed`
on write, `get_default_speed` again on read (a hand-edited `settings.json` can't smuggle a bad
value), and the env var at resolution: an invalid `IMPASSE_CODEX_SPEED` fails as a structured
`backend_error` naming the variable, never a traceback. The argv builder re-checks the allowlist
before interpolating into `-c` (defense in depth, so a future caller can't inject config syntax).
The review result reports the resolved value in `speed` (`standard` or `fast`); the claude backend
has no speed knob — an `IMPASSE_CLAUDE_SPEED` is ignored, never an error, and it reports `speed:
null`. Speed and effort compose freely (e.g. high effort with fast mode).

## Failure handling (limits & outages)

On an API error Codex exits non-zero and puts the real error — `{"type":"error"|"turn.failed", …}`
Expand Down
27 changes: 27 additions & 0 deletions scripts/impasse_lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,11 @@ def _run_dir(run_id: str) -> str:
# runner (per-run validation) and the settings store (set-effort validation) share one source.
ALLOWED_EFFORT = ("none", "low", "medium", "high", "xhigh")

# Codex execution-speed / service-tier allowlist. "fast" == Fast mode ON (a higher serving tier at
# higher credit cost); "standard" is the default (Fast mode OFF). Independent of reasoning effort.
# Same one-source discipline as ALLOWED_EFFORT: shared by the runner and the set-speed settings store.
ALLOWED_SPEED = ("standard", "fast") # codex service tier; "fast" == Fast mode ON. Default OFF.


# --- Persisted settings (a small config store, e.g. the operator's default reviewer model) ------

Expand Down Expand Up @@ -676,6 +681,15 @@ def get_default_effort(backend: str) -> str | None:
return e if e in ALLOWED_EFFORT else None


def get_default_speed(backend: str) -> str | None:
"""The persisted default execution speed (service tier) for a backend, or None. Lower precedence
than a per-run --speed and than IMPASSE_CODEX_SPEED — see impasse_run.review(). A hand-edited
value outside ALLOWED_SPEED is dropped here (fail safe on the read path); set_default_speed
refuses to write one."""
s = _get_default_setting("default_speed", backend)
return s if s in ALLOWED_SPEED else None


def _settings_lock():
"""An interprocess lock guarding the settings read-modify-write, so two hosts (e.g. a Claude Code
and a Codex host sharing one config dir) can't lose an update via interleaved read-modify-replace
Expand Down Expand Up @@ -753,6 +767,19 @@ def set_default_effort(backend: str, effort: str | None) -> None:
_set_default_setting("default_effort", backend, effort)


def set_default_speed(backend: str, speed: str | None) -> None:
"""Persist (speed set) or clear (speed None) the default execution speed / service tier for a
backend. Refuses a value outside ALLOWED_SPEED — never persist something the runner would reject.
Only codex HAS a service-tier/Fast-mode knob, so a non-null write for any other backend is refused
at the library level too (not just the CLI); clearing (speed=None) is allowed for any backend so a
legacy persisted value can always be removed (migration path)."""
if speed is not None and speed not in ALLOWED_SPEED:
raise ValueError(f"speed must be one of {sorted(ALLOWED_SPEED)}")
if speed is not None and backend != "codex":
raise ValueError(f"only the codex backend has a service-tier/Fast-mode knob (got backend={backend!r})")
_set_default_setting("default_speed", backend, speed)


def fsync_dir(path: str) -> None:
"""fsync a directory so a preceding os.replace into it is durable across a crash. Best-effort:
not every platform/filesystem supports it (Windows raises), so failures are swallowed."""
Expand Down
Loading
Loading