hermes: scope stale activity lineage
This commit is contained in:
parent
45225c41a8
commit
eace0ed0df
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user