Conversation
…r's contextvars
Under gateway.multiplex_profiles the profile secret scope is a ContextVar. The
asynchronous curator review started a bare threading.Thread, which begins with an
empty contextvars context, so the fork's first get_secret("ANTHROPIC_TOKEN") was
fail-closed ("could not read this profile's ANTHROPIC_TOKEN") even though the caller
had a scope installed. Start the thread through contextvars.copy_context().run.
Adds test_review_thread_inherits_secret_scope, which installs a scope, runs the
review asynchronously and asserts the LLM pass observed it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AfnxdXwQoheA3hYjA25Exo
|
Independent review — the fix is correct and the test has teeth. One live sibling of the same root cause is still unfixed, and two other sites must not get this treatment. Verified
Repo-wide sibling scan — one more live instance
So the one-liner is right for its path and the framing is honest, but "exactly one bare Thread" is not literally true — Minor note, not a defect: Approve as-is. |
What does this PR do?
Runs the curator's asynchronous LLM review thread in a copy of the caller's
contextvarscontext, so it keeps the profile secret scope on a multiplexed gateway.run_curator_review(synchronous=False)(the default, and whatmaybe_run_curatoruses) starts a barethreading.Thread. A new thread begins with an empty contextvars context, so the profile secret scope the caller installed is gone by the time the fork callsget_secret("ANTHROPIC_TOKEN"). Undergateway.multiplex_profilesthat reader is fail-closed, so the review dies with:(verbatim from a production gateway log, 2026-09-15; the auto-transitions ran, the LLM consolidation never did.)
#116744 made the housekeeping curator tick run under each served profile's scope, which is exactly what makes this one-line drop visible: the scope is now there at the call site and lost one frame later. #111617 had classified this thread as "process-global by design" because at the time every caller was unscoped; that is no longer true after #116744.
Related Issue
Follow-up to #116744 (housekeeping runs per profile scope). No separate issue filed; happy to open one.
Type of Change
Changes Made
agent/curator.py: start thecurator-reviewdaemon thread viacontextvars.copy_context().runinstead of a bare target.tests/agent/test_curator.py:test_review_thread_inherits_secret_scopeinstalls a scope withset_secret_scope, runs the review asynchronously with a stubbed_run_llm_review, joins the thread and asserts the stub observed the scope. Red on origin/main (seen["scope"]isNone), green with the fix.How to Test
pytest tests/agent/test_curator.py -q— 38 passed.gateway.multiplex_profileswith at least one served profile), wait for the hourly curator tick or runhermes curator runfrom a scoped caller. Before: thellm: error: Hermes could not read this profile's ANTHROPIC_TOKENline above. After: the LLM consolidation pass runs.Checklist
Code
background_review.py, a different fork; fix(curator): seed the review read-mark store in the fork's context #107734 closed, read-mark store)tests/agent/test_curator.py; fullpytest tests/ -qnot run (~3 h single-process on this box)Documentation & Housekeeping
cli-config.yaml.example— N/ACONTRIBUTING.md/AGENTS.md— N/Acontextvars)🤖 Generated with Claude Code
https://claude.ai/code/session_01AfnxdXwQoheA3hYjA25Exo