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/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/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/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/agent-inject: "true"
|
||||||
vault.hashicorp.com/role: hermes-agent
|
vault.hashicorp.com/role: hermes-agent
|
||||||
vault.hashicorp.com/agent-inject-secret-agent-api-key: kv/data/atlas/hermes/agent-tokens
|
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:
|
except Exception:
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
# Old dashboard versions rewrote the URL to a child without retaining the
|
# Old dashboard versions rewrote the URL to a delegated leaf without
|
||||||
# root. Recover the oldest available ancestor so those existing live links
|
# retaining its objective. Recover only the immediate objective parent.
|
||||||
# can also follow a resumed parent or a newer delegated sibling.
|
# Climbing to the oldest ancestor mixes unrelated workstreams that happen
|
||||||
|
# to share a long-lived dashboard/handoff session.
|
||||||
anchor = sid
|
anchor = sid
|
||||||
ancestor_seen = {anchor}
|
if not children.get(anchor):
|
||||||
while True:
|
|
||||||
parent = (rows_by_id.get(anchor) or {}).get("parent_session_id")
|
parent = (rows_by_id.get(anchor) or {}).get("parent_session_id")
|
||||||
if not parent or parent in ancestor_seen:
|
if parent and parent in rows_by_id:
|
||||||
break
|
anchor = parent
|
||||||
anchor = parent
|
|
||||||
ancestor_seen.add(anchor)
|
|
||||||
sid = anchor
|
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])]
|
descendants = [(root, [sid])]
|
||||||
stack = [(sid, [sid])]
|
stack = [(sid, [sid])]
|
||||||
seen = {sid}
|
seen = {sid}
|
||||||
@ -177,14 +175,22 @@ LATEST_SELECTION_AFTER = ''' children = {}
|
|||||||
# A durable parent can resume after a delegated reviewer exits. Prefer the
|
# A durable parent can resume after a delegated reviewer exits. Prefer the
|
||||||
# newest still-open member of the lineage instead of stranding the browser
|
# newest still-open member of the lineage instead of stranding the browser
|
||||||
# on the most recently created (but already finished) child.
|
# 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:
|
if active:
|
||||||
row, path = max(active, key=lambda item: (started(item[0]), len(item[1])))
|
row, path = max(active, key=lambda item: (started(item[0]), len(item[1])))
|
||||||
return row.get("id") or sid, path
|
return row.get("id") or sid, path
|
||||||
|
|
||||||
leaves = [
|
leaves = [
|
||||||
item for item in descendants
|
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]))
|
row, path = max(leaves or descendants, key=lambda item: started(item[0]))
|
||||||
return row.get("id") or sid, path
|
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 '"total_messages": total_messages' in patched
|
||||||
assert "SELECT id, parent_session_id, started_at, ended_at" 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 "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
|
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:
|
class FakeDB:
|
||||||
root = {
|
def get_session(self, session_id):
|
||||||
"id": "root",
|
return next((row for row in rows if row["id"] == session_id), None)
|
||||||
"started_at": 1.0,
|
|
||||||
"ended_at": None,
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_session(self, _session_id):
|
|
||||||
return self.root
|
|
||||||
|
|
||||||
rows = [
|
rows = [
|
||||||
{
|
{
|
||||||
"id": "root",
|
"id": "umbrella",
|
||||||
"parent_session_id": None,
|
"parent_session_id": None,
|
||||||
|
"started_at": 0.0,
|
||||||
|
"ended_at": None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "root",
|
||||||
|
"parent_session_id": "umbrella",
|
||||||
"started_at": 1.0,
|
"started_at": 1.0,
|
||||||
"ended_at": None,
|
"ended_at": None,
|
||||||
},
|
},
|
||||||
@ -1218,10 +1220,32 @@ def test_web_session_lineage_returns_to_resumed_parent():
|
|||||||
"review-2",
|
"review-2",
|
||||||
["root", "review-2"],
|
["root", "review-2"],
|
||||||
)
|
)
|
||||||
rows[1]["ended_at"] = 5.0
|
|
||||||
rows[2]["ended_at"] = 5.0
|
rows[2]["ended_at"] = 5.0
|
||||||
|
rows[3]["ended_at"] = 5.0
|
||||||
assert select_active("root", FakeDB(), rows) == ("root", ["root"])
|
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(
|
def test_api_activity_patch_coalesces_streaming_heartbeats(
|
||||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user