fix(policy): reconcile session-owned action memory with game observations

This commit is contained in:
0xrsydn 2026-09-22 12:45:46 +07:00
commit bf41945ef9
9 changed files with 405 additions and 362 deletions

25
run.py
View file

@ -365,6 +365,9 @@ def main() -> int:
duplicate_waits = 0
jev_errors = 0
saw_a_run = False
policy_context = brain.PolicyContext()
pending_action = None
pending_since = time.monotonic()
for step in range(1, args.steps + 1):
try:
@ -428,9 +431,9 @@ def main() -> int:
print(json.dumps(obs, indent=2)[:900])
return finish(1, "stuck")
# Only wait when our own action actually landed and the game is still
# animating. If the action was rejected, fall through and pick a
# different one instead of waiting out the stuck counter.
# Acceptance does not prove completion. The policy context handles
# pending selections and purchases. Rejections allow another proposal
# instead of waiting out the stuck counter.
#
# NOTE: this check now happens AFTER deciding, and only suppresses a
# REPEATED action. Waiting on "state unchanged" alone blocked
@ -458,7 +461,7 @@ def main() -> int:
# must not inherit the previous step's answers in the log.
client.last = None
try:
decision = brain.decide(obs, client, deck_snapshot)
decision = brain.decide(obs, client, deck_snapshot, context=policy_context)
# A procedural action or animation wait does not establish model
# recovery. Only a successful model response resets the budget.
if client is not None and client.last is not None:
@ -478,7 +481,7 @@ def main() -> int:
# the same failing request -- measured, a DNS blip re-raised out of
# the "fallback" and killed the session.
try:
decision = brain.decide(obs, None, deck_snapshot)
decision = brain.decide(obs, None, deck_snapshot, context=policy_context)
except Exception as inner: # noqa: BLE001
trace({"step": step, "event": "fallback_error", "error": str(inner)[:160]})
print(f"[{step:03d}] fallback also failed: {inner}")
@ -499,6 +502,13 @@ def main() -> int:
print(json.dumps(obs, indent=2)[:800])
return finish(1, "no_decision")
# Bound unresolved actions even if unrelated observation fields change.
if policy_context.pending is not pending_action:
pending_action = policy_context.pending
pending_since = time.monotonic()
if pending_action is not None and time.monotonic() - pending_since > args.stuck_seconds:
return finish(1, "pending_action_timeout")
# Between turns there is nothing to do but look again.
if decision.action == "__wait__":
waits += 1
@ -508,8 +518,8 @@ def main() -> int:
# Suppress DUPLICATE actions on an unchanged state, but only for a
# bounded number of reads. Proposing a DIFFERENT action is always
# allowed, which is what makes select-then-embark and multi-purchase
# shops work. The bound matters because some actions legitimately need
# allowed here, but cannot bypass a pending policy action above.
# The bound matters because some actions legitimately need
# repeating (multi-line Ancient dialogue) and some transitions are just
# slow -- waiting forever on those stalls the run.
action_key = (decision.action, json.dumps(decision.params, sort_keys=True))
@ -543,6 +553,7 @@ def main() -> int:
"action": decision.action, "error": str(exc)[:200]})
return finish(1, "action_error")
policy_context.record_result(obs, decision, accepted=result.ok)
if not result.ok:
print(f"[{step:03d}] action rejected: {result.message}")
trace({"step": step, "event": "action_rejected",