From 148497f6d6ad2dd251026cc4ef2fde48ddae53b3 Mon Sep 17 00:00:00 2001 From: trymhaak <20903785+trymhaak@users.noreply.github.com> Date: Sun, 26 Jul 2026 13:43:45 -0700 Subject: [PATCH] fix(kanban): strip stale session routing from dispatched worker env MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A long-lived gateway can have platform routing (HERMES_SESSION_* / HERMES_CRON_AUTO_DELIVER_*) mirrored in os.environ from a previous turn. _default_spawn() copied that process environment verbatim into detached kanban workers, so a worker calling kanban_create treated the inherited chat/topic as its origin and auto-subscribed the child task — the task's terminal notification then woke an unrelated chat. Strip every registered session-context routing key from the worker env unconditionally (the dispatcher is detached from every conversation); board, workspace, task, branch, profile, model, and credential propagation are unchanged. Salvaged from PR #69181 (both commits squashed; the PR's second commit fixed the first's engagement-latch assumption). --- hermes_cli/kanban_db.py | 6 ++++++ tests/hermes_cli/test_kanban_db.py | 18 +++++++++++++----- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/hermes_cli/kanban_db.py b/hermes_cli/kanban_db.py index 355a7933a31d..a4b3c383df7b 100644 --- a/hermes_cli/kanban_db.py +++ b/hermes_cli/kanban_db.py @@ -8815,6 +8815,12 @@ def _default_spawn( prompt = f"work kanban task {task.id}" env = dict(os.environ) + # The dispatcher is detached from every conversation. Its worker must never + # inherit routing mirrored by a previous gateway turn, even before the first + # session binds ContextVars in this process. + from gateway.session_context import _VAR_MAP + for key in _VAR_MAP: + env.pop(key, None) # Inject HERMES_HOME so the worker reads the profile-scoped config.yaml # (fallback_providers, toolsets, agent settings, etc.) instead of the root diff --git a/tests/hermes_cli/test_kanban_db.py b/tests/hermes_cli/test_kanban_db.py index f4e552352554..0904a877b697 100644 --- a/tests/hermes_cli/test_kanban_db.py +++ b/tests/hermes_cli/test_kanban_db.py @@ -3096,17 +3096,23 @@ class TestSharedBoardPaths: assert kb.kanban_db_path() == default_home / "kanban.db" assert kb.workspaces_root() == default_home / "kanban" / "workspaces" - def test_dispatcher_spawn_injects_kanban_db_and_workspaces_root( + def test_dispatcher_spawn_injects_kanban_paths_without_stale_session( self, tmp_path, monkeypatch ): - # The dispatcher's `_default_spawn` must inject HERMES_KANBAN_DB - # and HERMES_KANBAN_WORKSPACES_ROOT into the worker env so the - # worker converges on the dispatcher's paths even when the - # `-p ` flag rewrites HERMES_HOME. + # The dispatcher must pin board paths while stripping any unrelated + # HERMES_SESSION_* identity inherited from the long-lived gateway. default_home = tmp_path / ".hermes" default_home.mkdir() self._set_home(monkeypatch, tmp_path, default_home) + from gateway import session_context as sc + + # A dispatcher can launch before the gateway binds its first session. + monkeypatch.setattr(sc, "_session_context_engaged", False) + sc.reset_session_vars() + for key in sc._VAR_MAP: + monkeypatch.setenv(key, "stale-routing-value") + captured = {} class _FakePopen: @@ -3144,6 +3150,8 @@ class TestSharedBoardPaths: ) assert env["HERMES_KANBAN_TASK"] == "t_dispatch_env" assert env["HERMES_KANBAN_BRANCH"] == "wt/t_dispatch_env" + for key in sc._VAR_MAP: + assert key not in env # ---------------------------------------------------------------------------