hermes: scope stale activity lineage

This commit is contained in:
jenkins 2026-08-16 02:51:20 -03:00
parent 45225c41a8
commit eace0ed0df
3 changed files with 54 additions and 24 deletions

View File

@ -25,7 +25,7 @@ spec:
ai.bstein.dev/execution: Hermes Kanban with durable direct Codex and Claude Code CLI workers
ai.bstein.dev/model-policy: Jetson-assisted AUTO routing, low through xhigh, cross-provider fallback
ai.bstein.dev/placement: rpi5 preferred; Jetson deferred until state storage is available
ai.bstein.dev/config-rev: "20260815-runtime-access-boundary"
ai.bstein.dev/config-rev: "20260816-activity-lineage-scope"
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: hermes-agent
vault.hashicorp.com/agent-inject-secret-agent-api-key: kv/data/atlas/hermes/agent-tokens

View File

@ -145,20 +145,18 @@ LATEST_SELECTION_AFTER = ''' children = {}
except Exception:
return 0.0
# Old dashboard versions rewrote the URL to a child without retaining the
# root. Recover the oldest available ancestor so those existing live links
# can also follow a resumed parent or a newer delegated sibling.
# Old dashboard versions rewrote the URL to a delegated leaf without
# retaining its objective. Recover only the immediate objective parent.
# Climbing to the oldest ancestor mixes unrelated workstreams that happen
# to share a long-lived dashboard/handoff session.
anchor = sid
ancestor_seen = {anchor}
while True:
if not children.get(anchor):
parent = (rows_by_id.get(anchor) or {}).get("parent_session_id")
if not parent or parent in ancestor_seen:
break
anchor = parent
ancestor_seen.add(anchor)
if parent and parent in rows_by_id:
anchor = parent
sid = anchor
root = db.get_session(sid) or {"id": sid}
root = rows_by_id.get(sid) or db.get_session(sid) or {"id": sid}
descendants = [(root, [sid])]
stack = [(sid, [sid])]
seen = {sid}
@ -177,14 +175,22 @@ LATEST_SELECTION_AFTER = ''' children = {}
# A durable parent can resume after a delegated reviewer exits. Prefer the
# newest still-open member of the lineage instead of stranding the browser
# on the most recently created (but already finished) child.
active = [item for item in descendants if item[0].get("ended_at") is None]
# An ended objective can retain orphaned children whose process died before
# SessionDB recorded ended_at. Do not let those stale rows defeat a newer,
# completed repair/review branch. While the objective itself is live, an
# open child or the resumed parent still wins as before.
active = (
[item for item in descendants if item[0].get("ended_at") is None]
if root.get("ended_at") is None
else []
)
if active:
row, path = max(active, key=lambda item: (started(item[0]), len(item[1])))
return row.get("id") or sid, path
leaves = [
item for item in descendants
if not any(child.get("id") in seen for child in children.get(item[0].get("id"), []))
if not children.get(item[0].get("id"))
]
row, path = max(leaves or descendants, key=lambda item: started(item[0]))
return row.get("id") or sid, path

View File

@ -1160,7 +1160,9 @@ def test_web_session_activity_patch_projects_bounded_events(tmp_path: Path):
assert '"total_messages": total_messages' in patched
assert "SELECT id, parent_session_id, started_at, ended_at" in patched
assert "newest still-open member of the lineage" in patched
assert "oldest available ancestor" in patched
assert "immediate objective parent" in patched
assert "mixes unrelated workstreams" in patched
assert "orphaned children" in patched
assert 'item[0].get("ended_at") is None' in patched
@ -1179,19 +1181,19 @@ def test_web_session_lineage_returns_to_resumed_parent():
)
class FakeDB:
root = {
"id": "root",
"started_at": 1.0,
"ended_at": None,
}
def get_session(self, _session_id):
return self.root
def get_session(self, session_id):
return next((row for row in rows if row["id"] == session_id), None)
rows = [
{
"id": "root",
"id": "umbrella",
"parent_session_id": None,
"started_at": 0.0,
"ended_at": None,
},
{
"id": "root",
"parent_session_id": "umbrella",
"started_at": 1.0,
"ended_at": None,
},
@ -1218,10 +1220,32 @@ def test_web_session_lineage_returns_to_resumed_parent():
"review-2",
["root", "review-2"],
)
rows[1]["ended_at"] = 5.0
rows[2]["ended_at"] = 5.0
rows[3]["ended_at"] = 5.0
assert select_active("root", FakeDB(), rows) == ("root", ["root"])
rows[1]["ended_at"] = 9.0
rows.append(
{
"id": "orphaned-review",
"parent_session_id": "root",
"started_at": 6.0,
"ended_at": None,
}
)
rows.append(
{
"id": "accepted-review",
"parent_session_id": "root",
"started_at": 7.0,
"ended_at": 8.0,
}
)
assert select_active("review-1", FakeDB(), rows) == (
"accepted-review",
["root", "accepted-review"],
)
def test_api_activity_patch_coalesces_streaming_heartbeats(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch