Skip to content

fix(*): codex usage, log redaction, and two observability gaps - #347

Open
0xKT wants to merge 2 commits into
mainfrom
fix/codex_usage_and_log_hygiene
Open

fix(*): codex usage, log redaction, and two observability gaps#347
0xKT wants to merge 2 commits into
mainfrom
fix/codex_usage_and_log_hygiene

Conversation

@0xKT

@0xKT 0xKT commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Summary

Four gaps found while regression-testing the v0.1.11 fix list against main.
Three are new; the fourth is the one item that list still owed.

1. openai_codex reported zero tokens for every turn. The backend sends
usage on response.completed -- an event the SSE consumer already handled for
its finish_reason while reading nothing else from it. Empty usage made the
per-turn summary suppress itself (it renders only above zero tokens) and left
token budgeting blind. Cost stays absent by design: a plan-billed provider has
no per-token price.

2. Channel credentials reached the log file. httpx logs its request line at
INFO and the Telegram Bot API keys every route on /bot<token>/, so one gateway
run wrote 15 working bot tokens into its rotating log. All three sinks now
redact. The numeric bot id survives for debugging; ordinary URLs, ports and
paths are left untouched so the log stays usable.

3. Migration notices repeated on every load_config. The strips rewrite the
in-memory copy only, so an unmigrated file re-emits them forever -- and a gateway
loads once per cron fire. Deduped per process, alongside the existing
_warned_paths. A record the logger would drop does not count as told: the
gateway loads its config before installing a sink, and counting that dropped
first line would trade the noise for silence. A test pins this.

4. doctor now names the everos binary it resolved. Which one raven picked
was invisible from every command, and the resolver prefers the interpreter's own
directory -- what matters when PATH holds another environment's copy.

Deliberately not included: the gateway log keeps its 0644 mode. Redaction
removes the credentials; whether session content should also be unreadable to
other local users is a separate call.

Type

  • Fix
  • Feature
  • Docs
  • CI / tooling
  • Refactor
  • Other

Verification

  • uv run pytest tests/test_openai_codex_provider.py tests/test_log_redaction.py tests/test_cli_doctor_commands.py tests/test_config_loader.py tests/test_everos_server.py tests/test_cli_gateway_commands.py tests/test_cli_tui_commands.py: 234 passed.

  • make lint-python: clean (832 files).

  • uv run python scripts/coverage_gate.py diff --base-ref origin/main --threshold 90: 95.45% (42/44 changed lines).

  • Real gateway run against a live Telegram bot: the configured token appears 0
    times in the log file (15 before), and each migration line appears once (4-8
    per cron fire before).

  • Real codex turn: the summary now prints 3.7k in / 13 out tokens; it printed
    nothing before.

  • Removing the dedup guard turns its test red, so the test is not self-satisfying.

  • Relevant tests pass locally

  • Relevant lint / type checks pass locally

  • User-facing docs or screenshots are updated when needed

Risk

  • Security impact considered
  • Backward compatibility considered
  • Rollback path is clear for risky changes

Redaction rewrites log text, so a line quoting one of the masked shapes is
altered on its way to the sink; the patterns are anchored to credential syntax
and tests assert plain URLs, ports and paths pass through untouched.
_consume_sse gained a fourth return element -- both call sites are in the same
private module and updated here. Rollback is a revert of this branch.

Related Issues

N/A

Four gaps found while regression-testing the v0.1.11 fix list on main.
None of them is on that list; the one item still owed from it (doctor
not naming the resolved everos binary) is the last of the four.

1. openai_codex reported zero tokens for every turn. The backend does
   send usage, on `response.completed` -- an event the consumer already
   handled for its finish_reason and read nothing else from. With usage
   left empty the per-turn summary suppressed itself (it renders only
   when in/out tokens exceed zero) and token budgeting saw nothing. The
   cost half stays absent by design: a plan-billed provider has no
   per-token price.

2. httpx logs its request line at INFO, and the Telegram Bot API keys
   every route on /bot<token>/, so a gateway run wrote 15 working bot
   tokens into its rotating log file -- the file users attach to bug
   reports. All three sinks now redact. The bot id survives; only the
   secret half is masked, and ordinary URLs, ports and paths are left
   alone so the log stays debuggable. This is the message-body
   counterpart to the existing diagnose=False, which already kept
   tracebacks from serializing secrets.

3. Migration notices repeated on every load_config. The strips rewrite
   the in-memory copy only, so an unmigrated file re-emits them forever,
   and a gateway loads once per cron fire. Deduped per process, next to
   the existing _warned_paths precedent. A record the logger would drop
   is not counted as told: the gateway loads its config before it
   installs a sink, and counting that dropped first line would have
   turned the noise into silence.

4. doctor now names the everos binary it resolved. Which one raven
   picked was invisible from every command, and the resolver prefers the
   interpreter's own directory -- the detail that matters when PATH
   holds another environment's copy.

Verification: 234 tests across the affected modules, diff coverage
95.45% against the 90% gate, and a real gateway run confirming zero
token occurrences and one line per migration.

Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
@claude

claude Bot commented Aug 18, 2026

Copy link
Copy Markdown

Claude finished @0xKT's task in 2m 21s —— View job


Review

No blockers; this can merge.

  • Read prior review comments on this PR
  • Review diff against origin/main
  • Check correctness (bugs, edge cases, error handling, callers)
  • Check repo conventions (AGENTS.md)
  • Check test coverage / weakening
  • Post final findings

The blocker previously flagged on raven/config/loader.py:400_migrate_legacy_context_window's notify=True path logging through a raw logging.getLogger(__name__) instead of the dedup wrapper — is fixed in 3c80956. _migration_log is now a module-level _MigrationLog singleton (raven/config/loader.py:86) shared by both _migrate_config (line 397) and _migrate_legacy_context_window (line 229), so the run_stamped=True path dedupes too. New tests (test_context_window_migration_logs_once_across_both_passes, test_stamped_migration_pass_dedupes_too in tests/test_config_loader.py) exercise exactly the run_stamped=True path the prior tests skipped, and pin the "record dropped before a sink exists doesn't count as told" behavior separately.

Reviewed the rest of the diff against origin/main:

  • raven/utils/log_redaction.py + raven/cli/_log_file.py: all three loguru sinks (file, terminal, RAVEN_CLI_DEBUG stderr) now route through redacting_filter. Patterns are anchored (require ?/& immediately before the key name, Bearer , vendor prefixes, or the Telegram /bot<id>:<secret> shape), so plausible near-miss strings like token_type=bearer or ?section=token don't match — consistent with the "ordinary URLs/paths survive" test cases.
  • raven/providers/openai_codex_provider.py: _consume_sse's new 4th return value is threaded through both call sites in the same module; _convert_usage maps the Responses API shape onto the prompt_tokens/completion_tokens/cache_read_input_tokens/cache_creation_input_tokens keys _build_usage_snapshot (raven/agent/loop/main.py:1630) already expects and normalizes, matching the OpenRouter/LiteLLM total-prompt convention the docstring claims.
  • raven/cli/doctor_commands.py + raven/plugin/memory/everos/_server.py: everos_binary_path() cleanly wraps _everos_executable()'s exception into None; the "not owned" case correctly omits the new Binary: line since Raven never resolves a binary for a server it doesn't spawn.
  • Commits are Conventional-Commit formatted, ASCII-only, carry Co-authored-by, and branch name follows <type>/<snake_desc>. No asset/large files added. Test file naming follows the test_cli_<module>_commands.py / test_config_loader.py conventions — no new files needed for either change.
  • No weakened/skipped tests; new tests exercise the actual regression paths (e.g., removing the dedup guard would turn test_migration_logs_once_per_process red per the PR description).

Comment thread raven/config/loader.py
_log = _MigrationLog(_logging.getLogger(__name__))

if run_stamped:
_migrate_legacy_context_window(data, notify=True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[blocker] This call still bypasses the new per-process dedup. _migrate_legacy_context_window's notify=True branch (loader.py:223) logs via a raw logging.getLogger(__name__).info(...), not through the _MigrationLog wrapper constructed at line 392 — it never sees _log/_logged_migrations at all.

That matters because this path is exactly the one problem #3 in this PR is about: run_stamped is unstamped = _migration_version(path) < CURRENT_CONFIG_VERSION (loader.py:313), and the stamp write is best-effort (_write_migration_version, loader.py:186-197, swallows OSError). If the sidecar stamp can't be written (read-only config dir, permission issue, etc.), unstamped stays True forever, so this branch — and its logging.getLogger(__name__).info("Migrated: dropped agents.defaults.%s ...") line — fires on every single load_config() call, indefinitely. That's the identical "gateway loads once per cron fire" noise this PR sets out to fix, just for one specific migration that isn't routed through the new dedup.

The added tests (test_migration_logs_once_per_process etc.) only call _migrate_config(data) with the default run_stamped=False, so this path isn't exercised at all.

Suggest passing the dedup logger (or _logged_migrations) into _migrate_legacy_context_window as well, or moving its notify logging to go through _MigrationLog.

The dedup in the parent commit covered the migrations that log through
_migrate_config's proxy, but _migrate_legacy_context_window logs from its
own function with a raw logger and kept repeating.

One command walks that migration twice -- load_config's own read, then
the persist pass re-reading the raw file -- and the stamp that would stop
a third walk is best effort: _write_migration_version swallows OSError,
so a config directory that cannot take the stamp leaves the line firing
on every load. That is the same repeat the parent commit set out to end.

The proxy is now a module-level instance shared by both paths. The added
tests cover the twice-per-command walk and the stamped pass; the parent
commit's tests reached neither, since they all ran with run_stamped
defaulted off.

Reported by the PR reviewer on #347.

Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
@0xKT

0xKT commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: accepted, fixed in 3c80956

Finding Verdict Action
loader.py:395 context-window migration bypasses the new dedup accept Fixed in 3c80956

Verified before acting, rather than taking the reasoning at face value:

  • _migrate_legacy_context_window does log through a raw
    logging.getLogger(__name__).info(...), never touching _MigrationLog.
  • _write_migration_version does swallow OSError (debug-level only), and
    _migration_version returns 0 for an absent or unreadable stamp, so
    unstamped stays True for as long as the stamp cannot be written.
  • The new tests did all run with run_stamped defaulted off, so that path had
    no coverage. Correct.

One correction: the scope is wider than reported

The finding attributes this to the notify=True branch. The log line is not
gated by notify at all -- it sits in the for legacy_key loop and fires on
any call that finds the legacy key; notify only gates the user-facing
_migration_notices entry. There are also two call sites, not one
(loader.py:269 and loader.py:395), and the comment above the notice dedup
already states it: "one command can walk this path twice before anything is
printed".

So the repeat does not need a failed stamp write to appear. A single command
logs the line twice on the first run that migrates it; the unwritable-stamp
case is what makes it permanent. This does not change the verdict, only how
much it was already happening.

Fix

Rather than threading the logger through the signature, the proxy is now a
module-level instance (_migration_log) shared by both the in-loader
migrations and this one, so any future migration that logs from its own
function inherits the dedup instead of having to opt in.

Verify

  • uv run pytest tests/test_config_loader.py: 36 passed, including
    test_context_window_migration_logs_once_across_both_passes (the
    twice-per-command walk) and test_stamped_migration_pass_dedupes_too (the
    run_stamped=True path this finding named).
  • Mutation check: reverting the one-line routing turns both new tests red, so
    they are not self-satisfying.
  • make lint-python: clean.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; this can merge as far as I am concerned.

Independent review of 3c80956 (both commits), not a re-read of the earlier bot thread.

What I covered: the full diff against github/main, the callers of every changed signature, AGENTS.md conventions (commit grammar/ASCII, test file naming, comment rules), backward compatibility, and whether the tests actually exercise the behaviour. Full suite run locally.

Verification

  • uv run pytest -q -> 1 failed, 6440 passed, 43 skipped. The single failure is tests/test_cli_theme.py::test_bold_accent_renders_styled_not_bare, and it is not from this PR: the file is byte-identical to main, no PR-touched module is imported by it, and it fails when that file is run on its own (rich falls back to 256-colour, 38;5;221 instead of truecolor) while passing when the test is run alone -- intra-file pollution that predates this branch. The 43 skips are all import-gated optional channel SDKs plus parametrized provider cases, none of them in this diff.
  • uv run pytest -q tests/test_log_redaction.py tests/test_config_loader.py tests/test_openai_codex_provider.py tests/test_cli_doctor_commands.py tests/test_cli_log_file.py tests/test_cli_tui_logging_isolation.py -> 144 passed, 0 skipped.
  • uv run ruff check raven/ tests/ clean; ruff format --check clean.

Things I checked and could not turn into a finding

  • The dedup gap the earlier thread raised is genuinely closed: _migrate_legacy_context_window now logs through the shared module-level _MigrationLog, and the two new tests cover both the twice-per-command walk and run_stamped=True (the earlier tests reached neither). Every _log.* call in loader.py is .info, so the proxy exposing only info cannot AttributeError.
  • The isEnabledFor(INFO) guard behaves as documented: before _intercept_stdlib_logging runs, root sits at WARNING so a dropped record is not counted as told; after basicConfig(level=0, force=True) the effective level is 0 and the line reaches the file sink. setLevel/basicConfig clear the isEnabledFor cache, so the guard cannot latch.
  • Redaction is attached per sink, not once: the file sink, the terminal sink and the RAVEN_CLI_DEBUG sink each carry redacting_filter, so a record filtered out of the file sink by handler level still gets redacted on the way to stderr. Loguru runs the filter before the enqueue=True handoff, which the on-disk assertion in test_gateway_log_file_never_receives_a_live_token proves end to end. The TUI's _drop_watcher_spam matches "rust notify timeout", which redaction cannot perturb.
  • Codex usage mapping matches the convention it claims: input_tokens includes cached tokens, and both AgentLoop._build_usage_snapshot and raven/tracing/usage.normalize subtract cache_read + cache_write under the same inequality test, so nothing is double counted. _consume_sse / _request_codex are module-private and every caller (including tests) was updated for the 4-tuple.
  • MemoryInfo.binary is inserted mid-dataclass, but the only construction site is keyword-only, so no positional shift. binary is set only under owned, and _probe_memory / _render_memory_capabilities bail on the same non-everos condition, so the "not found" line can never appear for a backend nobody looked up. everos_binary_path swallows only EverosBinaryMissingError, which is all _everos_executable raises.

Worth knowing, not blocking: redaction rewrites record["message"] only. A logger.exception(...) traceback and the default stderr sink installed by raven/cli/_log_silence.py still bypass it. I looked for a concrete leak through either and did not find one in the current callers (the one URL-embedded credential in the repo, dingtalk's ?access_token= upload, logs the error interpolated into the message, so it is covered), so this is a note for later rather than a defect in this diff.

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