You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A delegated child can lose the parent session ContextVar when execution crosses an inner ThreadPoolExecutor boundary. The child then resolves HERMES_SESSION_KEY through the process environment, where the value may belong to another concurrently active session. If the child starts a background terminal process, that foreign key is persisted in ProcessSession and in the completion event. The completion can consequently enter the wrong session.
This is a narrow, implementation-level report about the delegated-child producer path. It is not a claim that every wrong-session notification has the same cause, and it does not include private logs or debug-upload data.
Summary
The currently affected runtime checkout has two bare executor submissions in tools/delegate_tool.py: the per-child timeout executor and the multi-child batch executor. The outer async-delegation workers already propagate context, but these inner boundaries do not. A standard-library probe shows that a bare submission reads a foreign process-environment value while contextvars.copy_context().run(...) preserves the parent value.
The upstream main ref checked during this review (91937a6dc3ffbbe2f3be91a500f0ecf962c4cf53) already contains copy_context().run at the corresponding timeout and batch sites. The affected runtime checkout is older/different, so this report should be treated as an affected-checkout/release-regression and regression-coverage follow-up rather than as a claim that current upstream main is still unfixed.
Observed Behavior
In a multi-session run, a background completion from one session was observed entering another session. The source path explains how this can happen:
A delegated child crosses an unwrapped executor boundary.
The child context has no value for the session-key ContextVar.
Session-key resolution falls back to os.environ["HERMES_SESSION_KEY"].
The resulting key is stored in ProcessSession.session_key and copied into the completion event.
A consumer that maps the key to a live session can deliver the completion or wake-up to the wrong session.
Expected Behavior
A delegated child must retain the true initiating session context across both inner executor boundaries. Any background process it creates should store the true parent session key in ProcessSession and in its completion event. Completion output and any follow-up delivery should be accepted only by that initiating session.
Minimal Reproduction (executor mechanism)
This platform-independent probe uses only the Python standard library and deliberately sets a different process-environment value:
A background completion can contaminate an unrelated conversation.
Completion output can be visible to, or cause a follow-up wake-up in, the wrong live session.
The core persists the wrong route datum at process creation, so a downstream consumer cannot reliably reconstruct the true parent from session_key alone.
Concurrent sessions can therefore experience cross-session delivery and potentially act on unrelated process results.
Upstream comparison: main=91937a6dc3ffbbe2f3be91a500f0ecf962c4cf53 (contains the two context wrappers; not the affected checkout)
OS: Linux 6.8.0-136-generic
Python: 3.11.15
Surface: multi-session execution with delegated children and background terminal completion notifications
Affected components: Agent Core / delegation, Tools / terminal and process registry, Gateway session context
Messaging platform: N/A; the executor mechanism is platform-independent
Debug Report
No debug-upload link is included. This is a source-level isolation defect with a self-contained standard-library reproduction; no private logs, credentials, or session data are required for the mechanism probe.
Source Pointers
On the affected checkout:
tools/delegate_tool.py:2006-2017: _run_with_thread_capture enters the child conversation, then :2017 calls _timeout_executor.submit(_run_with_thread_capture) without a context wrapper.
tools/delegate_tool.py:2646-2679: the batch path calls executor.submit(_run_single_child, ...) at :2673-2679 without a context wrapper. A single child still reaches the unwrapped per-child timeout executor.
tools/approval.py:202-214: get_current_session_key() checks approval context and then calls get_session_env.
gateway/session_context.py:303-326: a bound context value wins, but an unset value falls back to os.environ for CLI/cron compatibility.
tools/terminal_tool.py:2448-2454: the terminal background-process path reads the current session key.
tools/process_registry.py:90-119: ProcessSession stores session_key; :717-724 writes the passed key at spawn; :1142-1174 copies it into a type=completion event.
For comparison, upstream main at 91937a6dc3ffbbe2f3be91a500f0ecf962c4cf53 has:
tools/delegate_tool.py:2188-2192: contextvars.copy_context() followed by _child_context.run for the timeout worker.
tools/delegate_tool.py:3005-3013: a per-child contextvars.copy_context() followed by child_context.run for the batch worker.
Relationship to Existing Issues and PRs
#60426 and #52447 are the broad, user-visible wrong-session background-notification trackers. This report is a narrower producer-side implementation follow-up: it identifies the inner delegated-child executor boundary that can create the wrong session_key, rather than proposing another generic queue-consumer routing fix. It is related to both, but is not a duplicate of their symptom descriptions.
#65407 (and its open #65409) covers residual readers that prefer process-global environment values over already-bound task-local ContextVars. This report is distinct: the inner worker has no copied ContextVar in the first place, so the existing unbound-context fallback is reached. The proposed scope is to propagate the context at the two delegate call sites, not to remove the intentional CLI/cron fallback globally.
#71472 is the closest overlap. Its requested two-site context propagation is closed as implemented on upstream main, and the current upstream source confirms both wrappers. This draft is not a competing root-cause report or a claim of a new fix; it records that the affected checkout still lacks those sites and asks for release/deployment readback plus call-site regression coverage. If version-specific tracking is not useful to maintainers, this draft should be closed in favor of fix(delegate): propagate session ContextVars across the subagent executor boundary #71472.
#75856 is merged and addresses delegated-child SESSION_ID clobbering during child construction. That is related session-identity hardening, but it does not replace propagation of the session-key ContextVar across the inner timeout/batch executor submissions or verify ProcessSession.session_key completion routing.
Proposed Fix / Regression Test
The minimal implementation direction is to snapshot the parent context before each inner submit and invoke the target through context.run (or an equivalent context-propagation wrapper), at both the timeout and batch sites. Preserve the executor's non-interactive delegated-child approval callback semantics; do not replace it with a wrapper that can reintroduce prompt callbacks on a worker thread. Do not remove the process-environment fallback wholesale because CLI/cron compatibility intentionally uses it when no session ContextVar is bound.
Please add a call-site regression matrix that:
Sets a true parent session ContextVar and a deliberately different HERMES_SESSION_KEY environment value.
Exercises a single delegated child that reaches the timeout executor, creates a background process, and asserts ProcessSession.session_key and the completion event equal the true parent.
Exercises the batch executor with at least two children and makes the same assertion for every child; cover the batch executor and each child's timeout executor.
Runs two concurrent parent contexts with deliberately swapped environment values and asserts that no child or completion event crosses parents.
Verifies that approval callback installation/cleanup and timeout/cancellation behavior remain unchanged.
Evidence and Limitations
The affected checkout source shows both bare submissions and the fallback chain described above.
The standard-library probe was executed and produced the output shown above; the propagated wrapper retained true-parent while the bare worker resolved env-foreign.
GitHub REST API readback confirmed the issue/PR states above and confirmed that upstream main contains both copy_context().run sites.
This is not a full UI or end-to-end cross-session replay. The probe proves the executor/context mechanism, while the source chain explains how it reaches ProcessSession and completion routing. It does not by itself prove that every wrong-session incident has this exact cause.
No private paths, credentials, debug links, profile names, or incident-specific session data are included.
Bug Description
A delegated child can lose the parent session
ContextVarwhen execution crosses an innerThreadPoolExecutorboundary. The child then resolvesHERMES_SESSION_KEYthrough the process environment, where the value may belong to another concurrently active session. If the child starts a background terminal process, that foreign key is persisted inProcessSessionand in the completion event. The completion can consequently enter the wrong session.This is a narrow, implementation-level report about the delegated-child producer path. It is not a claim that every wrong-session notification has the same cause, and it does not include private logs or debug-upload data.
Summary
The currently affected runtime checkout has two bare executor submissions in
tools/delegate_tool.py: the per-child timeout executor and the multi-child batch executor. The outer async-delegation workers already propagate context, but these inner boundaries do not. A standard-library probe shows that a bare submission reads a foreign process-environment value whilecontextvars.copy_context().run(...)preserves the parent value.The upstream
mainref checked during this review (91937a6dc3ffbbe2f3be91a500f0ecf962c4cf53) already containscopy_context().runat the corresponding timeout and batch sites. The affected runtime checkout is older/different, so this report should be treated as an affected-checkout/release-regression and regression-coverage follow-up rather than as a claim that current upstreammainis still unfixed.Observed Behavior
In a multi-session run, a background completion from one session was observed entering another session. The source path explains how this can happen:
ContextVar.os.environ["HERMES_SESSION_KEY"].ProcessSession.session_keyand copied into the completion event.Expected Behavior
A delegated child must retain the true initiating session context across both inner executor boundaries. Any background process it creates should store the true parent session key in
ProcessSessionand in its completion event. Completion output and any follow-up delivery should be accepted only by that initiating session.Minimal Reproduction (executor mechanism)
This platform-independent probe uses only the Python standard library and deliberately sets a different process-environment value:
Observed output:
Impact
session_keyalone.Environment
v0.19.0; affected checkoutHEAD=651c0931debf86e75129b4c781f2669e41fdfd42main=91937a6dc3ffbbe2f3be91a500f0ecf962c4cf53(contains the two context wrappers; not the affected checkout)Debug Report
No debug-upload link is included. This is a source-level isolation defect with a self-contained standard-library reproduction; no private logs, credentials, or session data are required for the mechanism probe.
Source Pointers
On the affected checkout:
tools/delegate_tool.py:2006-2017:_run_with_thread_captureenters the child conversation, then:2017calls_timeout_executor.submit(_run_with_thread_capture)without a context wrapper.tools/delegate_tool.py:2646-2679: the batch path callsexecutor.submit(_run_single_child, ...)at:2673-2679without a context wrapper. A single child still reaches the unwrapped per-child timeout executor.tools/approval.py:202-214:get_current_session_key()checks approval context and then callsget_session_env.gateway/session_context.py:303-326: a bound context value wins, but an unset value falls back toos.environfor CLI/cron compatibility.tools/terminal_tool.py:2448-2454: the terminal background-process path reads the current session key.tools/process_registry.py:90-119:ProcessSessionstoressession_key;:717-724writes the passed key at spawn;:1142-1174copies it into atype=completionevent.For comparison, upstream
mainat91937a6dc3ffbbe2f3be91a500f0ecf962c4cf53has:tools/delegate_tool.py:2188-2192:contextvars.copy_context()followed by_child_context.runfor the timeout worker.tools/delegate_tool.py:3005-3013: a per-childcontextvars.copy_context()followed bychild_context.runfor the batch worker.Relationship to Existing Issues and PRs
session_key, rather than proposing another generic queue-consumer routing fix. It is related to both, but is not a duplicate of their symptom descriptions.main, and the current upstream source confirms both wrappers. This draft is not a competing root-cause report or a claim of a new fix; it records that the affected checkout still lacks those sites and asks for release/deployment readback plus call-site regression coverage. If version-specific tracking is not useful to maintainers, this draft should be closed in favor of fix(delegate): propagate session ContextVars across the subagent executor boundary #71472.SESSION_IDclobbering during child construction. That is related session-identity hardening, but it does not replace propagation of the session-key ContextVar across the inner timeout/batch executor submissions or verifyProcessSession.session_keycompletion routing.Proposed Fix / Regression Test
The minimal implementation direction is to snapshot the parent context before each inner
submitand invoke the target throughcontext.run(or an equivalent context-propagation wrapper), at both the timeout and batch sites. Preserve the executor's non-interactive delegated-child approval callback semantics; do not replace it with a wrapper that can reintroduce prompt callbacks on a worker thread. Do not remove the process-environment fallback wholesale because CLI/cron compatibility intentionally uses it when no session ContextVar is bound.Please add a call-site regression matrix that:
HERMES_SESSION_KEYenvironment value.ProcessSession.session_keyand the completion event equal the true parent.Evidence and Limitations
true-parentwhile the bare worker resolvedenv-foreign.maincontains bothcopy_context().runsites.ProcessSessionand completion routing. It does not by itself prove that every wrong-session incident has this exact cause.