Files
hermes-agent/evals/update_obligation_identity.py
Teknium 89c85b8466 fix(update): settle stale receipt warnings from matching live gateways
Reconcile receipt-only restart obligations at the shared warning/catch-up
predicate, requiring every historical runtime/profile identity to have a
current live gateway successor. Preserve missing and unknown obligations,
non-gateway identities, and independently authoritative pending markers.

Keep failed receipts unchanged instead of recording an unverified success.
Live isolated two-process A/B reproduces the warning on base and settles
it after the fix; stale, unknown, and missing-profile controls still warn.

Reported-by: duanzhiwei0315
Inspired-by: zengzheqing (#104295), RootZ3n (#100249)
2026-09-07 08:20:46 -07:00

84 lines
3.0 KiB
Python

"""Disposable process/state-file A/B: run with the checkout path as argv[1].
No services are discovered or restarted. Both children write runtime status
through production code; only their temporary version fields are altered for
negative controls. Exit 1 on base, exit 0 when current successors settle.
"""
import json, os, pathlib, subprocess, sys, tempfile, time
repo = pathlib.Path(sys.argv[1])
sys.path.insert(0, str(repo))
root = pathlib.Path(tempfile.mkdtemp(prefix="obligations-live-"))
os.environ["HOME"] = str(root)
os.environ["HERMES_HOME"] = str(root / ".hermes")
from hermes_cli import update_cmd_fleet as fleet, update_receipt as receipts
print("MODULE", fleet.__file__)
sha = fleet._current_checkout_sha()
children = []
rows = []
try:
for name in ["alpha", "beta"]:
home = root / ".hermes" / "profiles" / name
home.mkdir(parents=True)
code = "import sys,time; sys.path.insert(0,sys.argv[1]); from gateway.status import write_runtime_status; write_runtime_status(gateway_state='running'); print('ready',flush=True); time.sleep(120)"
p = subprocess.Popen(
[sys.executable, "-c", code, str(repo)],
cwd=repo,
env={**os.environ, "HERMES_HOME": str(home)},
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
children.append(p)
assert p.stdout.readline().strip() == "ready", p.stderr.read()
d = root / ".hermes/logs/update_receipts"
d.mkdir(parents=True)
receipt = {
"outcome": "failed",
"plan": {
"runtimes": [
{"kind": "gateway", "profile": name, "code_sha": "old", "pid": 1}
for name in ["alpha", "beta"]
]
},
}
(d / "latest.json").write_text(json.dumps(receipt))
def observe(label):
row = {
"case": label,
"fleet": receipts.collect_fleet_versions(),
"pending": fleet._pending_fleet_restart_needed(),
}
rows.append(row)
print(json.dumps(row))
observe("current-successors")
home = root / ".hermes/profiles/beta"
state = home / "gateway_state.json"
payload = json.loads(state.read_text())
payload["code_sha"] = "old"
state.write_text(json.dumps(payload))
observe("stale-beta")
payload["code_sha"] = None
state.write_text(json.dumps(payload))
observe("unknown-beta")
children[1].terminate()
children[1].wait(timeout=10)
observe("missing-beta-with-current-alpha")
assert rows[1]["pending"] and rows[2]["pending"] and rows[3]["pending"]
print("VERDICT:", "FIXED" if not rows[0]["pending"] else "REPRODUCED")
assert json.loads((d / "latest.json").read_text()) == receipt
assert not rows[0]["pending"], (
"Current identity-matched successors must settle the warning"
)
finally:
for p in children:
if p.poll() is None:
p.terminate()
p.wait(timeout=10)
print("CLEANUP", [p.returncode for p in children])