Skip to content

fix(gateway): multiplexed hygiene compaction no longer loses the profile secret scope (salvage #100849) - #100950

Merged
teknium1 merged 7 commits into
mainfrom
fix/hygiene-secret-scope
Sep 2, 2026
Merged

fix(gateway): multiplexed hygiene compaction no longer loses the profile secret scope (salvage #100849)#100950
teknium1 merged 7 commits into
mainfrom
fix/hygiene-secret-scope

Conversation

@teknium1

@teknium1 teknium1 commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Multiplexed-gateway session-hygiene compaction can reach the summary model again: the hygiene worker now inherits the caller's profile secret scope instead of failing closed with UnscopedSecretError and silently truncating the middle of the conversation on every pass (salvages #100849 by @MattMaximo).

Root cause: gateway/run.py hygiene ran _compress_context on a bare loop.run_in_executor(None, ...) worker. Under gateway.multiplex_profiles the profile secret scope and HERMES_HOME override are ContextVars installed per turn by _profile_runtime_scope; a bare worker starts with an empty Context, so get_secret("<PROVIDER>_API_KEY") in the summary path raised UnscopedSecretError, the summary was "unavailable", and the compressor fell through to the lossy placeholder-and-drop path. In the reporter's debug bundle every hygiene pass took that route (697→642, 645→426 messages, no LLM summary).

Changes

  • gateway/run.py: both hygiene executor hops (detached-agent path and codex app-server path) run inside copy_context().run. Default executor kept on purpose — a fence-cancelled hung summary must never occupy a gateway agent-work slot.
  • agent/context_compressor.py: UnscopedSecretError classified as a missing-credential failure → compress() aborts and preserves the session unchanged (same carve-out as 401/402/403), instead of dropping the middle window for a placeholder.
  • tools/daemon_pool.py (salvaged from fix(tools): propagate caller contextvars in DaemonThreadPoolExecutor.submit #100849, @MattMaximo): DaemonThreadPoolExecutor.submit propagates the caller's contextvars by default, so every pool consumer is safe even without propagate_context_to_thread. Docstring corrected: stdlib only does this from Python 3.14; nothing is stripped from the bundled runtime.
  • Tests: hygiene worker inherits caller ContextVars (verified to fail with the bare run_in_executor), UnscopedSecretError → access-failure class, plus the salvaged daemon-pool / secret-scope tests.

Validation

Surface Before (origin/main) After
Real get_secret("SURPLUS_API_KEY") in a run_in_executor worker, multiplex on, profile .env scope installed (/tmp/hyg_ab.py) UnscopedSecretError returns scoped value
_is_summary_access_or_quota_error(UnscopedSecretError(...)) False → truncation True → abort, session preserved
test_hygiene_worker_inherits_caller_contextvars with copy_context().run removed FAILS (sabotage check)
tests/gateway/test_codex_hygiene_compaction.py, tests/tools/test_daemon_pool.py, tests/agent/test_secret_scope.py, -k access_failure 46 passed

Note on #100849's stated mechanism: the daemon pool consumer (conversation_compression._run_compress_with_timeout) already wrapped its worker with propagate_context_to_thread; the scope was lost one layer up in gateway hygiene, which never touches the daemon pool. The pool change is kept as defense in depth for other submit() callers.

Related: #76574 (secret-scope residuals), #100697 / #100709 (unscoped-probe log noise).

Infographic

Hygiene compaction keeps profile secrets

MattMaximo and others added 7 commits September 1, 2026 21:56
…submit

Some bundled CPython runtime builds strip stdlib ThreadPoolExecutor's
copy_context() propagation, so work submitted to the daemon pool runs in a
bare context. Under the multiplexed gateway this dropped the profile
secret scope in pool workers: the context-compression timeout fence
resolved auxiliary provider keys (SURPLUS_API_KEY) with
UnscopedSecretError, silently degrading LLM compression to lossy
deterministic summaries and driving re-read loops in affected sessions.

Restore stdlib semantics in submit() by snapshotting the caller's context
and running the callable inside it (a no-op re-application on runtimes
that already propagate). Mirrors the gateway's
_run_in_executor_with_context pattern.

Tests: daemon pool worker sees caller contextvars; scoped get_secret works
in a daemon-pool worker under multiplex while scoped misses still fail
closed (no env leak).
… multiplexing

Session-hygiene compaction ran _compress_context on a bare
loop.run_in_executor(None, ...) worker. Under gateway.multiplex_profiles the
profile secret scope and HERMES_HOME override are ContextVars installed by
the per-turn _profile_runtime_scope, and a bare worker starts with an empty
Context — so the summary model's get_secret(<PROVIDER>_API_KEY) failed
closed with UnscopedSecretError on EVERY hygiene pass and compaction
silently degraded to a lossy truncation (#100849 debug bundle:
'Failed to generate context summary: get_secret(SURPLUS_API_KEY) called
with no profile secret scope active').

- gateway/run.py: run both hygiene executor hops (detached-agent path and
  codex app-server path) inside copy_context().run, keeping the default
  executor so a fence-cancelled hung summary never occupies a gateway
  agent-work slot.
- agent/context_compressor.py: UnscopedSecretError is a missing-credential
  class failure — abort and preserve the session instead of dropping the
  middle window for a placeholder summary (same carve-out as 401/402/403).
- tools/daemon_pool.py: correct the salvaged docstrings — stdlib
  ThreadPoolExecutor only propagates contextvars from 3.14; nothing is
  stripped from the bundled runtime.
- tests: hygiene worker inherits caller ContextVars (fails on bare
  run_in_executor); UnscopedSecretError classified as access failure.

Live A/B (real get_secret in a run_in_executor worker, multiplex on, profile
.env scope installed): main -> UnscopedSecretError; fixed -> scoped value.
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

ran on 6ab1250 — chore: retrigger CI (zero-job dispatch failure, auto-heal)

⚠️ Warnings

CI timings · View report · View job

Wall time 4m22s vs 1m40s (+162.0%). 3 faster, 1 unchanged.

  • Detect affected areas: -50.0s
  • OSV scan / Emit review status: -35.0s
  • OSV scan / Scan lockfiles / osv-scan: -16.0s

OSV vulnerability scan · View job

13 known vulnerabilities found in pinned dependencies.

How to fix:

Review the findings in the Security tab. Update the affected dependencies if a patched version is available.

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/gateway Gateway runner, session dispatch, delivery area/compression Context compression and continuation sessions sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages labels Sep 2, 2026
@teknium1
teknium1 merged commit c5c9aa8 into main Sep 2, 2026
44 checks passed
@teknium1
teknium1 deleted the fix/hygiene-secret-scope branch September 2, 2026 05:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/compression Context compression and continuation sessions comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants