fix(stall_watch): call herdr with the sanitized agent name (Issue #66)

herdr registers an agent under sanitize_herdr_agent_name(session), so a
session name over 32 characters or with upper-case letters got
agent_not_found and stall detection went off. stall_watch now converts the
name for every herdr call (agent get/read/prompt, watcher.wait_once); logs
and notices keep the original name, and the agent kind is still read off
the original suffix (a long name's sanitized form ends in a hash).

The conftest mock herdr gets an opt-in strict_agent_names mode that, like
the real herdr, answers an agent only under its registered name and fails
get/read with agent_not_found JSON; existing tests see no change.

Refs #66

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
GodopuandClaude Opus 5.5 committed 2026-10-24 13:47:54 +00:00
1 parent 0461a632eb
commit bb496fde78
3 files changed
+150 -5

No files matched your search

+10 -5
View File
@@ -38,6 +38,7 @@ from typing import List, Optional
from lib_py.agents.input_region import extract_input_region
from lib_py.agents.registry import agent_of_row, get_adapter
from lib_py.agents.sanitize import sanitize_herdr_agent_name
from lib_py.delegation.engine import TERMINAL_STATUSES
from lib_py.loop import watcher
@@ -156,6 +157,9 @@ class HerdrSource(object):
def __init__(self, target, agent, session=None):
self.target, self.session = target, session or None
# herdr knows the agent only by its sanitized name (#66); the kind is
# read off the original name, whose suffix a long name's hash replaces.
self.name = sanitize_herdr_agent_name(target)
self.agent = resolve_agent(agent, target)
adapter = get_adapter(self.agent) if self.agent else None
self.resident = tuple(adapter.resident_children) if adapter else ()
@@ -176,7 +180,7 @@ class HerdrSource(object):
return proc.stdout
def agent_get(self):
payload = json.loads(self._herdr("agent", "get", self.target) or "{}")
payload = json.loads(self._herdr("agent", "get", self.name) or "{}")
return (payload.get("result") or {}).get("agent") or {}
def agent_pid(self, pane_id):
@@ -199,12 +203,12 @@ class HerdrSource(object):
if not self.agent:
return None
try:
return extract_input_region(self._herdr("agent", "read", self.target), self.agent) == ""
return extract_input_region(self._herdr("agent", "read", self.name), self.agent) == ""
except ValueError:
return None
def prompt(self, text):
self._herdr("agent", "prompt", self.target, text)
self._herdr("agent", "prompt", self.name, text)
def collect(self, sig: Signals) -> Signals:
"""Fill the process and input signals of an idle agent."""
@@ -222,6 +226,7 @@ class StallWatch(object):
def __init__(self, args, source=None):
self.args = args
self.src = source or HerdrSource(args.target, args.agent, args.herdr_session)
self.herdr_name = sanitize_herdr_agent_name(args.target) # for herdr only; logs keep args.target
name = resolve_agent(args.agent, args.target)
adapter = get_adapter(name) if name else None
reliable = bool(adapter.agent_status_reliable) if adapter else True
@@ -323,7 +328,7 @@ class StallWatch(object):
until, tmo = ("idle", "done", "working"), 60000
else:
until, tmo = ("idle", "done", "blocked"), 60000
out = watcher.wait_once(self.args.target, until=until, timeout_ms=tmo, session=self.args.herdr_session)
out = watcher.wait_once(self.herdr_name, until=until, timeout_ms=tmo, session=self.args.herdr_session)
if self._wrapper_gone(): # round-2 review D2: before any notice
return RC_TERMINAL
if out.kind in (watcher.TRANSPORT_ERROR, watcher.AGENT_NOT_FOUND):
@@ -425,7 +430,7 @@ def main(argv=None) -> int:
p = argparse.ArgumentParser(description="notify-only stall watcher for one local herdr job (#30)")
p.add_argument("--job", required=True)
p.add_argument("--registry-dir", required=True)
p.add_argument("--target", required=True, help="herdr agent name of the job's session")
p.add_argument("--target", required=True, help="session name of the job (herdr is called with its sanitized form)")
p.add_argument("--agent", required=True, help="agent CLI (adapter name)")
p.add_argument("--herdr-session", default="")
p.add_argument("--expect-dispatch-token", default=None)