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),