Files
hermes-agent/evals/postmortem/forensics/delegation.py
T
Teknium a8ca904922 feat(evals): post-mortem harness — forensics lanes + live A/B + review probes for the #102117 run fixes
evals/postmortem/ turns the one-off audit behind tracking issue #103563 into
something anyone with a Hermes state.db copy (and optionally rotated
agent.log*) can run on their own fan-out:

  forensics/   common.py discovers the run tree (root = most descendants,
               compression-rollover children excluded so cost buckets stay
               disjoint), fits pricing from estimated_cost_usd, and five lanes
               recompute the OBSERVED figures: tokens (buckets, depth/duration
               shares, context reconstruction, excess-cache-write proxy, cap
               replay), logcalls (per-call cache behaviour from agent.log with
               coverage printed first; strict and loose plateau definitions
               reported separately), delegation (timeouts, orphaned children,
               polling hours, batch-join withheld child-hours, truncated
               summaries), tools (hardline blocks, foreground refusals,
               whole-file rewrites), goal_loop (nudges, parked barrier), rework
               (public-surface drop at PR open + post-open commit inventory).
               Every figure is labeled OBSERVED or MODELED.
  live_ab/     the per-PR A/Bs (real code paths, fake providers, temp
               HERMES_HOME), paths from argv.
  review_probes/ the independent /review's probes, credited and adapted; each
               reproduced a round-1 defect and the fixed head must pass it.
  run.py       runs the offline probes against one or two checkouts and prints
               PASS/FAIL side by side (--live adds the ones that spend cents).
  tests/       synthetic-DB smoke test for the lanes and runner.

On the run's DB the lanes reproduce the tracking issue's population exactly
(1,394 sessions, 93,284 calls, $19,302.59; cache_write $11,159.76) and on
main vs an integration checkout of the 13 PRs the runner shows every probe
FAIL -> PASS (two guard-only probes pass on both, noted in run.py).

The trajectories are deliberately not shipped: the DB holds 51,956 home
paths, 5,341 e-mails, private IPs, chat ids and real-shaped credentials in
tool output. The lane reports and recomputed JSON are in a secret gist
linked from #103563.
2026-09-05 09:13:10 -07:00

116 lines
6.5 KiB
Python

"""Lane 2: delegation behaviour — nested-delegate timeouts, orphaned children, polling cost, batch-join
delivery delay, and summary truncation. All OBSERVED from ``messages`` in the run tree.
python -m evals.postmortem.forensics.delegation --db state_copy.db
Reproduces: how many ``delegate_task`` tool results were deadline timeouts and in how many sessions;
how many nested calls ever returned a result; what those sessions spent after their first timeout
(lifetime cost, with the caveat that it includes real work); total ``sleep N`` seconds; child results
withheld by batch joins (concurrent child-hours, NOT critical path); and how many parent-side summaries
carried the truncation footer.
"""
from __future__ import annotations
import collections
import json
import re
import statistics
from typing import Any, Dict, List
from evals.postmortem.forensics.common import Run
_TIMEOUT = re.compile(r"timed out after ([\d.]+)s")
_SLEEP = re.compile(r"\bsleep\s+(\d+)")
_TRUNC = re.compile(r"\[SUMMARY TRUNCATED\]|middle omitted|trimmed to protect the parent")
def main(argv=None) -> int:
run = Run.from_args(argv, (__doc__ or "").split("\n\n")[0])
parent_of = {s: run.sessions[s].get("parent_session_id") for s in run.in_run}
children_of: Dict[str, List[str]] = collections.defaultdict(list)
for s, p in parent_of.items():
if p:
children_of[p].append(s)
orchestrators = [s for s in run.in_run if children_of.get(s)]
timeouts_by_sess: Dict[str, int] = collections.Counter()
delegate_results = delegate_ok = 0
sleep_seconds_after_timeout = 0
first_timeout_ts: Dict[str, float] = {}
truncated_summaries = total_summaries = 0
for sid in orchestrators:
for m in run.messages(sid, "role, tool_name, content, tool_calls, timestamp"):
if m["role"] == "tool" and (m.get("tool_name") or "") == "delegate_task":
c = m.get("content") or ""
delegate_results += 1
if _TIMEOUT.search(c) and "delegate_task" in c:
timeouts_by_sess[sid] += 1
first_timeout_ts.setdefault(sid, float(m.get("timestamp") or 0))
elif '"status"' in c and ("completed" in c or "summary" in c):
delegate_ok += 1
total_summaries += 1
if _TRUNC.search(c):
truncated_summaries += 1
if m["role"] == "user" and (m.get("content") or "").startswith("[ASYNC DELEGATION"):
# background batches re-enter as a user-role completion block, one entry per child
c = m["content"]
entries = c.count("--- ✓ TASK") + c.count("--- ✗ TASK") + c.count("--- ⚠ TASK") or (1 if "COMPLETE" in c else 0)
total_summaries += entries
truncated_summaries += c.count("[SUMMARY TRUNCATED]")
if m["role"] == "assistant" and sid in first_timeout_ts and float(m.get("timestamp") or 0) >= first_timeout_ts[sid]:
for mt in _SLEEP.finditer(m.get("tool_calls") or ""):
sleep_seconds_after_timeout += int(mt.group(1))
timeout_sessions = list(timeouts_by_sess)
nested_timeout_sessions = [s for s in timeout_sessions if run.depth[s] >= 1]
lifetime_cost = sum(run.cost(s) for s in timeout_sessions)
lifetime_cache_write = sum(float(run.sessions[s].get("cache_write_tokens") or 0) for s in timeout_sessions) * run.price_per_token["cache_write_tokens"]
# batch-join delivery delay: children of one parent dispatched within 60 s of each other = one batch
withheld: Dict[int, List[float]] = collections.defaultdict(list)
for p, kids in children_of.items():
kids = sorted(kids, key=lambda k: float(run.sessions[k].get("started_at") or 0))
batch: List[str] = []
def flush(batch: List[str]) -> None:
if len(batch) < 2:
return
ends = [float(run.sessions[k].get("ended_at") or 0) for k in batch]
last = max(ends)
withheld[run.depth.get(str(p), 0)].extend(last - e for e in ends if e)
for k in kids:
if batch and float(run.sessions[k].get("started_at") or 0) - float(run.sessions[batch[-1]].get("started_at") or 0) > 60:
flush(batch); batch = []
batch.append(k)
flush(batch)
report: Dict[str, Any] = {
"observed": {
"orchestrators": len(orchestrators),
"delegate_task_results": delegate_results,
"delegate_task_timeouts": sum(timeouts_by_sess.values()),
"sessions_with_timeouts": len(timeout_sessions),
"nested_sessions_with_timeouts": len(nested_timeout_sessions),
"delegate_task_results_that_carried_a_result": delegate_ok,
"sleep_hours_after_first_timeout": round(sleep_seconds_after_timeout / 3600, 1),
"timeout_sessions_lifetime_cost_usd": round(lifetime_cost, 2),
"timeout_sessions_lifetime_cache_write_usd": round(lifetime_cache_write, 2),
"lifetime_cost_caveat": "whole-session spend; includes legitimate work after the timeout, and its cache writes overlap the tokens lane's excess proxy",
"summaries_truncated": truncated_summaries, "summaries_total": total_summaries,
"batch_join_withheld_child_hours_by_parent_depth": {d: round(sum(v) / 3600, 1) for d, v in sorted(withheld.items())},
"batch_join_withheld_minutes_median_by_parent_depth": {d: round(statistics.median(v) / 60, 1) for d, v in sorted(withheld.items()) if v},
"withheld_caveat": "concurrent child-hours held back from the parent; not critical-path time",
}
}
path = run.write("delegation.json", report)
o = report["observed"]
print(f"[delegation] {o['orchestrators']} orchestrators; delegate_task results {o['delegate_task_results']:,}: timeouts {o['delegate_task_timeouts']} in {o['sessions_with_timeouts']} sessions "
f"({o['nested_sessions_with_timeouts']} nested); carried a result: {o['delegate_task_results_that_carried_a_result']}")
print(f"[delegation] after first timeout: sleep {o['sleep_hours_after_first_timeout']} h; those sessions' lifetime ${o['timeout_sessions_lifetime_cost_usd']:,} (cache writes ${o['timeout_sessions_lifetime_cache_write_usd']:,}; includes real work)")
print(f"[delegation] summaries truncated {o['summaries_truncated']}/{o['summaries_total']}; batch-join withheld child-hours by parent depth {o['batch_join_withheld_child_hours_by_parent_depth']}")
print(f"[delegation] wrote {path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())