IMJONEZZ commited on
Commit
108ca65
·
verified ·
1 Parent(s): c52514a

director: never crash the fight on a 503 or a stale lane

Browse files

Guard the LLM call in _choose (backend failure -> deterministic rng pick) and re-resolve the target lane against current state in _apply with an IllegalMove backstop. Fixes two worker crashes seen on the Space: HTTPStatusError 503 from ZeroGPU under load, and IllegalMove when the player empties the targeted lane during the slow director await.

Files changed (1) hide show
  1. scrypt/warden/director.py +60 -22
scrypt/warden/director.py CHANGED
@@ -12,7 +12,7 @@ from __future__ import annotations
12
  import random
13
  from dataclasses import dataclass, field
14
 
15
- from scrypt.engine.combat import LANES, CombatState
16
  from scrypt.inference.backend import Backend
17
 
18
  from .context import build_messages, combat_digest
@@ -129,34 +129,72 @@ class Director:
129
  'by calling the tool. Pick what stings most.'
130
  )
131
  harness = Harness(self.backend, [tool], max_steps=2, max_tokens=120)
132
- await harness.run(build_messages(frame, digest=combat_digest(state)))
 
 
 
 
 
 
133
  return picked[0] if picked else self.rng.choice(names)
134
 
135
  # ------------------------------------------------------------ applying
136
 
137
- def _apply(self, state: CombatState, action: str, args: dict) -> Intervention:
 
 
 
 
 
 
 
138
  if action == "throttle":
139
- if self.budget.try_spend("tamper_player_deck"):
140
- detail = state.throttle_player_card(args["lane"])
141
- card = state.player_row[args["lane"]].spec.id
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  return Intervention(
143
  "throttle", detail,
144
  f"you just quietly weakened the player's {card}; gloat without "
145
  "explaining exactly what you did",
146
  )
147
- if action == "reinforce":
148
- spec = self.content.card(args["card"])
149
- detail = state.reinforce_queue(args["lane"], spec)
150
- return Intervention(
151
- "reinforce", detail,
152
- f"you just scheduled an extra {spec.id} against the player",
153
- )
154
- if action == "withdraw":
155
- detail = state.withdraw_queue(args["lane"])
156
- return Intervention(
157
- "withdraw", detail,
158
- "you quietly removed a threat because the player is being crushed; "
159
- "say something that sounds like boredom, not kindness",
160
- )
161
- # Budget refused late: report nothing happened.
162
- return Intervention("none", "no-op", "")
 
 
 
12
  import random
13
  from dataclasses import dataclass, field
14
 
15
+ from scrypt.engine.combat import LANES, CombatState, IllegalMove
16
  from scrypt.inference.backend import Backend
17
 
18
  from .context import build_messages, combat_digest
 
129
  'by calling the tool. Pick what stings most.'
130
  )
131
  harness = Harness(self.backend, [tool], max_steps=2, max_tokens=120)
132
+ try:
133
+ await harness.run(build_messages(frame, digest=combat_digest(state)))
134
+ except Exception:
135
+ # A dead/slow/rude backend (e.g. the Space's ZeroGPU returning 503)
136
+ # must never crash the fight: fall back to a deterministic pick,
137
+ # exactly as author.compose and the voice path do.
138
+ return self.rng.choice(names)
139
  return picked[0] if picked else self.rng.choice(names)
140
 
141
  # ------------------------------------------------------------ applying
142
 
143
+ _NOOP = Intervention("none", "no-op", "")
144
+
145
+ def _resolve_lane(self, state: CombatState, action: str) -> int | None:
146
+ """Re-pick a legal target lane from CURRENT state. The lane chosen in
147
+ _legal_options was validated BEFORE the (slow) LLM await; the player
148
+ keeps acting during it, so by now that lane may be empty/occupied.
149
+ Re-deriving here keeps the intervention real instead of crashing on a
150
+ stale index. None = nothing legal remains for this action."""
151
  if action == "throttle":
152
+ lanes = [
153
+ i for i in range(LANES)
154
+ if state.player_row[i] is not None and state.player_row[i].power > 0
155
+ ]
156
+ return max(lanes, key=lambda i: state.player_row[i].power) if lanes else None
157
+ if action == "reinforce":
158
+ empty = [i for i in range(LANES) if state.foe_queue[i] is None]
159
+ return self.rng.choice(empty) if empty else None
160
+ if action == "withdraw":
161
+ queued = [
162
+ i for i in range(LANES)
163
+ if state.foe_queue[i] is not None and state.foe_queue[i].power >= 2
164
+ ]
165
+ return max(queued, key=lambda i: state.foe_queue[i].power) if queued else None
166
+ return None
167
+
168
+ def _apply(self, state: CombatState, action: str, args: dict) -> Intervention:
169
+ lane = self._resolve_lane(state, action)
170
+ if lane is None:
171
+ return self._NOOP # the board moved under us; nothing legal left
172
+ try:
173
+ if action == "throttle":
174
+ if not self.budget.try_spend("tamper_player_deck"):
175
+ return self._NOOP
176
+ card = state.player_row[lane].spec.id
177
+ detail = state.throttle_player_card(lane)
178
  return Intervention(
179
  "throttle", detail,
180
  f"you just quietly weakened the player's {card}; gloat without "
181
  "explaining exactly what you did",
182
  )
183
+ if action == "reinforce":
184
+ spec = self.content.card(args["card"])
185
+ detail = state.reinforce_queue(lane, spec)
186
+ return Intervention(
187
+ "reinforce", detail,
188
+ f"you just scheduled an extra {spec.id} against the player",
189
+ )
190
+ if action == "withdraw":
191
+ detail = state.withdraw_queue(lane)
192
+ return Intervention(
193
+ "withdraw", detail,
194
+ "you quietly removed a threat because the player is being crushed; "
195
+ "say something that sounds like boredom, not kindness",
196
+ )
197
+ except IllegalMove:
198
+ # Backstop: state changed between resolve and mutate. Never crash.
199
+ return self._NOOP
200
+ return self._NOOP