Skip to content

fix(skillopt-sleep): surface codex auth/model/version failures instead of silently scoring 0 - #92

Merged
Yifan Yang (Yif-Yang) merged 2 commits into
microsoft:mainfrom
dmmdea:fix/skillopt-sleep-surface-codex-failures
Jun 30, 2026
Merged

Yifan Yang (Yif-Yang) merged 2 commits into
microsoft:mainfrom
dmmdea:fix/skillopt-sleep-surface-codex-failures

Conversation

@dmmdea

@dmmdea Daniel Martinez Morales (dmmdea) commented Jun 28, 2026 •

Copy link
Copy Markdown
Contributor

Problem

A nightly sleep cycle could run for a long time emitting held-out 0.000 -> 0.000 (gate reject, zero edits) — indistinguishable from "nothing to learn" — when the real cause was the codex backend returning an error (expired auth / a model unsupported on a ChatGPT account / an outdated CLI) that got scored as a failed rollout. Nothing surfaced the error, so the loop silently never learned, and the per-cycle report gave no way to tell a genuine no-improvement night from a backend that never reached the model.

Fix

CodexCliBackend (_call path)

  • Split _call into _call_once + a retry wrapper: transient empty/timeout calls are retried instead of silently returning "" (mirrors AzureOpenAIBackend's existing guard).
  • On a non-zero exec exit, surface the reason via last_call_error and return "" rather than leaking the CLI's error text as if it were a model response.
  • Fail fast (no retries) on fatal, non-recoverable errors: 401 Unauthorized, refresh_token_reused, token_expired, not supported when using Codex with a ChatGPT account, requires a newer version of Codex.

CodexCliBackend.attempt_with_tools (tool-rollout path)

  • Same treatment: a failed tool rollout now sets last_call_error (timeout / exception / non-zero exit) instead of swallowing the exception — so an auth/model/version failure on the tool path is visible in diagnostics rather than a silent empty→0. Response stays empty; the CLI error text is never returned as a "response".

CliBackend.reflect — retain last_reflect_raw so a no-edits night is diagnosable (empty/non-JSON optimizer reply vs genuinely no failures).

consolidate — ConsolidationResult now carries per-task held-out detail (response_len, response_head, hard/soft, why) + reflect_raw + call_error. Observability only; the gate's accept logic and the learning algorithm are unchanged.

cycle — write a diagnostics.json per cycle so a 0.0 night self-explains; fully try/except-wrapped so it can never break the cycle.

.gitignore — ignore the .skillopt-sleep/ runtime dir.

Why it matters

Turns a silent multi-night failure into a single-glance diagnosis: a backend auth/model/version problem now shows up immediately as a populated call_error in diagnostics.json instead of masquerading as "the gate found nothing worth adopting."

How it was tested

  • Unit: 5 new tests, all green; full tests/test_sleep_engine.py suite passes (52 passed, 1 skipped; the only 3 failures are pre-existing Windows path-separator assertions that also fail on main). Tests: _call retry-not-silent-zero; auth-error surfaced-not-scored + no-retry; tool-path failure surfaced-not-silent; holdout_detail capture; reflect_raw capture.
  • End-to-end: a capped real-backend cycle (codex + gpt-5.5) went from the prior held-out 0.000 -> 0.000 reject to 0.000 -> 1.000 accept_new_best (2 edits staged), with a populated diagnostics.json (empty call_error, real reflect_raw, non-empty responses) — i.e. the loop produced its first real learning signal once the backend was reachable.
  • Not e2e-verified: the attempt_with_tools tool-path change is covered by a unit test but was not exercised against a live failing tool-rollout end-to-end.
  • Lint: adds no new ruff violations (net −1 vs main); py_compile clean.

Review

Reviewed in a fresh, independent context against the stated intent (which surfaced the tool-path gap, now fixed).

Risk: low

Offline nightly tool; the change is error-handling hardening + read-only observability, not on any request/auth/data/money path, and does not alter the gate or learning algorithm. Worst case for a consumer is an unchanged cycle plus a diagnostics.json file.

🤖 Generated with Claude Code

…d of silently scoring 0

A nightly sleep cycle could run for weeks emitting held-out 0.0 -> 0.0 (gate reject, zero
edits), indistinguishable from "nothing to learn", when the real cause was the codex backend
returning an error (expired auth / model unsupported on the account / outdated CLI) that got
scored as a failed rollout.

backend (CodexCliBackend):
- split _call into _call_once + a retry wrapper: transient empties/timeouts are retried
  instead of silently returning "" (mirrors AzureOpenAIBackend's guard);
- on a non-zero exit, surface the reason via last_call_error and return "" rather than
  leaking the CLI error text as if it were a model response;
- fail fast (no retries) on fatal auth/model/version errors (401, refresh_token_reused,
  token_expired, "not supported when using Codex with a ChatGPT account",
  "requires a newer version of Codex").
backend (CliBackend.reflect): retain last_reflect_raw so a no-edits night is diagnosable.
consolidate: ConsolidationResult now carries per-task held-out detail (response, hard/soft,
  fail_reason) + reflect_raw + call_error.
cycle: write diagnostics.json per cycle so a 0.0 night self-explains instead of being a black box.
tests: 4 new (retry-not-silent-zero, auth-error-surfaced-not-scored, holdout-detail, reflect-raw).

Also gitignore the .skillopt-sleep/ runtime dir.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
…lout path

Follow-up from a fresh-context review of the prior commit: CodexCliBackend.attempt_with_tools
(the rollout path for tool-requiring tasks) ran codex exec inline, swallowed all exceptions,
and never set last_call_error — so an auth/model/version failure on the tool path still
produced a silent empty->0 with no diagnostic signal, the exact failure class the prior commit
fixed for the _call path. Now it surfaces timeout/exception/non-zero-exit via last_call_error
(response stays empty; never leaks the CLI error text), so a failed tool rollout shows up in
diagnostics.json. Adds a regression test.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@Yif-Yang
Yifan Yang (Yif-Yang) merged commit b9142ba into microsoft:main Jun 30, 2026
1 check was pending
Yifan Yang (Yif-Yang) added a commit that referenced this pull request Jun 30, 2026
PR #92 added a per-cycle diagnostics.json that surfaces backend stderr,
optimizer replies, and task responses so a 0.0 night is self-diagnosing.
Those free-text fields can carry credentials (e.g. a codex 401 stderr dump
containing an auth token), so persisting them verbatim was a new on-disk
leak surface.

- Add a shared redact_secrets() in staging.py and route diagnostics.json's
  call_error / reflect_raw_head / holdout_detail through it before writing.
- Redact the codex and Claude auth-error log lines too (a secondary sink
  when a file log handler is attached); last_call_error stays raw in memory
  so _AUTH_MARKERS matching is unaffected.
- Centralize _SECRET_PATTERNS in staging.py (harvest_codex now reuses them)
  and extend coverage to AWS / GitHub / Slack / Google / JWT token shapes.
- Tests: secret-shape coverage, private-key blocks, recursive/scalar
  passthrough, no over-redaction of plain prose, fail-fast auth-error log
  redaction, and an end-to-end check that diagnostics.json has no secret.

Observability-only; the gate and learning algorithm are unchanged.

Co-Authored-By: Claude <[email protected]>
Yifan Yang (Yif-Yang) pushed a commit that referenced this pull request Jul 12, 2026
…ent zero scores (#126)

* fix(skillopt-sleep): surface Claude CLI spawn failures instead of silent zero scores

In _call and attempt_with_tools, the bare 'except Exception: return ""'
swallows FileNotFoundError (e.g. bare 'claude' on Windows with npm .cmd
shim) and any other spawn failure, returning an empty string that the
trainer treats as a legitimate model response that scores 0.0 everywhere.

Now the exception is caught explicitly: last_call_error is set, a
warning is logged, and the empty-string return is preserved for
backward compatibility of the control flow. This mirrors the pattern
from #92 (codex backend) which fixed the same class of 'dead CLI
masquerades as nothing to learn' bug.

Issue: #121

* test(skillopt-sleep): add tests verifying Claude CLI spawn failures are surfaced

Add two tests to TestClaudeCliBackendBare:
- test_spawn_failure_sets_last_call_error: _call sets last_call_error
  and returns '' when subprocess.run raises FileNotFoundError.
- test_attempt_tools_spawn_failure_sets_last_call_error: same for
  attempt_with_tools.

These prove the fix from the parent commit (surface spawn failures
instead of silently scoring 0) and guard against regressions.
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.

2 participants