mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
fix(kanban): key the worker-session retag per board, not per database
The retag gate was global, so once one board reclaimed its legacy rows a second board on the same state.db never got swept. Key the state_meta gate on the workspaces root and skip reopening state.db on every spawn via an in-process set. Align the dispatcher-spawn test with the worker's own `kanban` source tag and cover the per-board gate.
This commit is contained in:
parent
805c483ca5
commit
c2872cf53b
4 changed files with 41 additions and 9 deletions
|
|
@ -8841,12 +8841,19 @@ def _resolve_worker_cli_toolsets(hermes_home: Optional[str]) -> Optional[list[st
|
|||
return None
|
||||
|
||||
|
||||
_retagged_workspace_roots: set[str] = set()
|
||||
|
||||
|
||||
def _retag_legacy_worker_sessions(workspaces_root_path: str) -> None:
|
||||
"""Reclaim pre-tag worker rows in state.db so they leave the session lists.
|
||||
|
||||
Best-effort and gated to run once per database — a dispatcher tick must
|
||||
never fail because a session DB was busy or missing.
|
||||
Best-effort and gated — the durable ``state_meta`` gate lives in
|
||||
``retag_kanban_worker_sessions``; the in-process set keeps a busy
|
||||
dispatcher from reopening state.db on every spawn just to read it. A
|
||||
dispatcher tick must never fail because a session DB was busy or missing.
|
||||
"""
|
||||
if workspaces_root_path in _retagged_workspace_roots:
|
||||
return
|
||||
try:
|
||||
from hermes_state import SessionDB
|
||||
|
||||
|
|
@ -8855,6 +8862,7 @@ def _retag_legacy_worker_sessions(workspaces_root_path: str) -> None:
|
|||
db.retag_kanban_worker_sessions(workspaces_root_path)
|
||||
finally:
|
||||
db.close()
|
||||
_retagged_workspace_roots.add(workspaces_root_path)
|
||||
except Exception as exc:
|
||||
_log.debug("kanban worker: legacy session retag skipped (%s)", exc)
|
||||
|
||||
|
|
|
|||
|
|
@ -7758,16 +7758,17 @@ class SessionDB(SessionSearchMixin, SessionSchemaMixin, SessionPortabilityMixin)
|
|||
too. Identified by cwd under the board's workspaces root — a path only
|
||||
the dispatcher ever runs a session in.
|
||||
|
||||
Runs once per database (``state_meta`` gate) and returns the number of
|
||||
rows retagged.
|
||||
Gated per workspaces root (``state_meta``) so each board reclaims its
|
||||
own rows exactly once. Returns the number of rows retagged.
|
||||
"""
|
||||
if self.get_meta("kanban_worker_source_retagged") == "1":
|
||||
return 0
|
||||
|
||||
prefix = str(workspaces_root).rstrip("/\\")
|
||||
if not prefix:
|
||||
return 0
|
||||
|
||||
gate = f"kanban_worker_source_retagged:{prefix}"
|
||||
if self.get_meta(gate) == "1":
|
||||
return 0
|
||||
|
||||
def _do(conn):
|
||||
cursor = conn.execute(
|
||||
"UPDATE sessions SET source = 'kanban' "
|
||||
|
|
@ -7777,7 +7778,7 @@ class SessionDB(SessionSearchMixin, SessionSchemaMixin, SessionPortabilityMixin)
|
|||
# Read rowcount before set_meta reuses this cursor for its INSERT,
|
||||
# which would otherwise overwrite it with the meta write's count.
|
||||
retagged = cursor.rowcount or 0
|
||||
self.set_meta("kanban_worker_source_retagged", "1", cursor=cursor)
|
||||
self.set_meta(gate, "1", cursor=cursor)
|
||||
return retagged
|
||||
|
||||
return self._execute_write(_do)
|
||||
|
|
|
|||
|
|
@ -792,6 +792,9 @@ class TestSharedBoardPaths:
|
|||
):
|
||||
# The dispatcher must pin board paths while stripping any unrelated
|
||||
# HERMES_SESSION_* identity inherited from the long-lived gateway.
|
||||
# The one exception is HERMES_SESSION_SOURCE, which the dispatcher
|
||||
# re-sets to its own `kanban` tag AFTER the strip — a value it owns,
|
||||
# never one inherited from whatever the gateway last routed.
|
||||
default_home = tmp_path / ".hermes"
|
||||
default_home.mkdir()
|
||||
self._set_home(monkeypatch, tmp_path, default_home)
|
||||
|
|
@ -842,6 +845,11 @@ 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:
|
||||
if key == "HERMES_SESSION_SOURCE":
|
||||
# Re-set by the dispatcher, so what matters is that it carries
|
||||
# the worker's own tag rather than the inherited routing value.
|
||||
assert env[key] == "kanban"
|
||||
continue
|
||||
assert key not in env
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ def test_retag_reclaims_legacy_worker_rows(db, tmp_path):
|
|||
assert sources == {"legacy": "kanban", "legacy2": "kanban", "mine": "cli"}
|
||||
|
||||
|
||||
def test_retag_runs_once_per_database(db, tmp_path):
|
||||
def test_retag_runs_once_per_workspaces_root(db, tmp_path):
|
||||
"""The state_meta gate keeps the retag off every subsequent spawn."""
|
||||
workspaces = tmp_path / "kanban" / "workspaces"
|
||||
db.create_session(session_id="legacy", source="cli", cwd=str(workspaces / "t_a"))
|
||||
|
|
@ -104,3 +104,18 @@ def test_retag_runs_once_per_database(db, tmp_path):
|
|||
assert db.retag_kanban_worker_sessions(str(workspaces)) == 0
|
||||
row = db._conn.execute("SELECT source FROM sessions WHERE id = 'later'").fetchone()
|
||||
assert row[0] == "cli"
|
||||
|
||||
|
||||
def test_retag_gate_is_per_board(db, tmp_path):
|
||||
"""A second board's workspaces root still gets its own sweep.
|
||||
|
||||
The gate is keyed on the root, so reclaiming board A must not convince the
|
||||
dispatcher that board B's legacy rows were already handled.
|
||||
"""
|
||||
board_a = tmp_path / "kanban" / "boards" / "a" / "workspaces"
|
||||
board_b = tmp_path / "kanban" / "boards" / "b" / "workspaces"
|
||||
db.create_session(session_id="a1", source="cli", cwd=str(board_a / "t_a"))
|
||||
db.create_session(session_id="b1", source="cli", cwd=str(board_b / "t_b"))
|
||||
|
||||
assert db.retag_kanban_worker_sessions(str(board_a)) == 1
|
||||
assert db.retag_kanban_worker_sessions(str(board_b)) == 1
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue