From 62f0cfd90274d42e19564a084902933d0ab6c776 Mon Sep 17 00:00:00 2001 From: teknium <127238744+teknium1@users.noreply.github.com> Date: Thu, 4 Jun 2026 06:13:56 -0700 Subject: [PATCH] fix(kanban-dashboard): use context-local board pin in specify/decompose endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dashboard specify and decompose endpoints run as sync FastAPI threadpool handlers and pinned the active board by mutating the process-global HERMES_KANBAN_BOARD env var. Two concurrent requests for different boards race on that shared global and cross-write — the same bug class as the CLI path (#38323), now using the scoped_current_board() contextvar introduced by the CLI fix. --- plugins/kanban/dashboard/plugin_api.py | 29 ++++++++++---------------- 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/plugins/kanban/dashboard/plugin_api.py b/plugins/kanban/dashboard/plugin_api.py index 7cc814dcf..0a49172a8 100644 --- a/plugins/kanban/dashboard/plugin_api.py +++ b/plugins/kanban/dashboard/plugin_api.py @@ -38,7 +38,6 @@ from __future__ import annotations import asyncio import json import logging -import os import sqlite3 import time from dataclasses import asdict @@ -1620,10 +1619,12 @@ def specify_task_endpoint( """ board = _resolve_board(board) # Pin the board for the duration of this call so the specifier module - # (which calls ``kb.connect()`` with no args) hits the right DB. - prev_env = os.environ.get("HERMES_KANBAN_BOARD") - try: - os.environ["HERMES_KANBAN_BOARD"] = board or kanban_db.DEFAULT_BOARD + # (which calls ``kb.connect()`` with no args) hits the right DB. Use a + # context-local override rather than mutating the process-global + # HERMES_KANBAN_BOARD env var — this endpoint runs in FastAPI's + # threadpool, so two concurrent requests for different boards would + # otherwise race on the shared env var and cross-write (issue #38323). + with kanban_db.scoped_current_board(board or kanban_db.DEFAULT_BOARD): # Import lazily so a missing auxiliary client at import time # doesn't break plugin load. from hermes_cli import kanban_specify # noqa: WPS433 (intentional) @@ -1632,11 +1633,6 @@ def specify_task_endpoint( task_id, author=(payload.author or None), ) - finally: - if prev_env is None: - os.environ.pop("HERMES_KANBAN_BOARD", None) - else: - os.environ["HERMES_KANBAN_BOARD"] = prev_env return { "ok": bool(outcome.ok), @@ -2233,19 +2229,16 @@ def decompose_task_endpoint( can take minutes on reasoning models. """ board = _resolve_board(board) - prev_env = os.environ.get("HERMES_KANBAN_BOARD") - try: - os.environ["HERMES_KANBAN_BOARD"] = board or kanban_db.DEFAULT_BOARD + # Context-local board pin (see specify endpoint above): this sync + # endpoint runs in FastAPI's threadpool, so mutating the process-global + # HERMES_KANBAN_BOARD env var would let concurrent requests for + # different boards race and cross-write (issue #38323). + with kanban_db.scoped_current_board(board or kanban_db.DEFAULT_BOARD): from hermes_cli import kanban_decompose # noqa: WPS433 (intentional) outcome = kanban_decompose.decompose_task( task_id, author=(payload.author or None), ) - finally: - if prev_env is None: - os.environ.pop("HERMES_KANBAN_BOARD", None) - else: - os.environ["HERMES_KANBAN_BOARD"] = prev_env return { "ok": bool(outcome.ok),