re: Stage 02 is lost at ~11 minutes, and the pilot's survival rule is what guarantees it

First session whose deliverable was the mission's ENDING rather than a
measurement (mission_run.sh, 500 s, hull of every entity at 2 Hz). The ACROPOLIS
is untouched to t=170 s then falls at ~53 HP/s with no let-up, reaching zero at
t=640-720 s — so "no mission completed" is not an artifact of the 240 s
time-boxes, and not of the 600 s cap on a blocking tool call. A longer session
would only watch the loss arrive.

Attributing the damage by co-presence, exactly two classes are ever near the
asset: e007 turrets (8483) and e010 bombers (8334). pilot.py treats turrets as
keep-out zones at 2500 units and never as targets — the rule that made it
survive — so roughly half the escort damage comes from the one class it is
designed to avoid. Survival and the objective are in direct conflict and the
pilot resolves it entirely for survival: WARSHIPS 0000, WARPLANES 0009,
REMAINING OB rising 004 -> 008, our hull untouched at 1500/1500 with 120
missiles spent. That is unspent risk budget, not a good run.

Also corrects launch_mission.sh: a harness-tracked BACKGROUND task does not keep
the display alive (lost 11 s in, at the turn boundary) — the
one-blocking-foreground-call rule stands.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-10 20:57:43 +00:00
parent 530555de9f
commit f4d59c5783
4 changed files with 152 additions and 1 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

@@ -0,0 +1,89 @@
# Why Stage 02 is never won — the escort sinks at ~11 minutes (2026-08-10)
**Status: ✅ measured, one 500 s run.** The standing open item since 2026-07-29 was
"no mission completed". This is the first session whose deliverable was the
*ending* rather than a measurement, and it settles why: **the mission is lost
before it can be won, and the pilot's survival policy is what guarantees it.**
Run: `tools/re-capture/mission_run.sh 500 mission01` — boot → Stage 02 in flight →
`pilot.py` (escort-weighted targeting, target commitment, guided missiles) for
500 s, with every entity's hull sampled at 2 Hz and a screenshot every 30 s.
Artifacts at `/sylph-home/re/mission01/` (9 MB `mission.jsonl`, not committed).
## The escort's decay is linear, and it ends the mission
| t (s) | ACROPOLIS hull | % |
|---|---|---|
| 0160 | 25000 | 100 % |
| 180 | 24510 | 98.0 |
| 280 | 20279 | 81.1 |
| 380 | 13799 | 55.2 |
| 480 | 8541 | 34.2 |
| 485 (end) | 8182 | 32.7 |
Untouched until **t ≈ 170 s**, then **≈53 HP/s** with no let-up — so the asset
reaches zero at **t ≈ 640 s**, and the whole-run average rate puts it at 722 s.
Either way the escort is dead at **1012 minutes**, and "the ACROPOLIS is sunk"
is a defeat condition ([mission-escort-state](mission-escort-state.md)).
This also retires a suspicion: the 240 s time-box of earlier runs was *not*
hiding a win, and the ~500 s ceiling of a single blocking tool call is **not**
the binding constraint. A longer session would simply watch the loss arrive.
## What is actually killing it — and the conflict that follows
Attributing damage by co-presence (which hostiles are within 3000 units of the
asset in the sample where its hull drops, damage split evenly among the classes
present — suggestive, not per-shot proof), only **two** classes are ever near it:
| class | samples present | attributed damage |
|---|---|---|
| `UN_e007_ADAN_Turret` | 200 | 8483 |
| `UN_e010_ADAN_Attacker_S` | 194 | 8334 |
Roughly half the damage comes from **turrets** — and `pilot.py` treats turrets as
**keep-out zones at 2500 units, never as targets**. That rule is not arbitrary: a
turret is what shot down every pilot before 2026-07-30, and it is why the craft
now survives. But it means **the policy that keeps the pilot alive also
guarantees the escort dies.** Survival and the objective are in direct conflict,
and the pilot currently resolves it entirely in favour of survival.
The HUD at t≈485 s says the same thing from the game's side:
- `YOU KILLED WARSHIPS` **0000** — not one warship in 500 s, across every run ever;
- `YOU KILLED WARPLANES` **0009** — fighters only;
- `REMAINING OB` **004 → 008** — objectives are being *added* by waves faster than
any are cleared, so the pilot is not touching the objective set at all;
- SHIELD and ARMOR bars full, hull **1500/1500**, 120 missiles spent.
![Stage 02 at t≈485 s](captures/mission01-t485.png)
## The conclusion that matters
The pilot optimises the wrong thing. It maximises survival and fighter kills;
the mission scores **objectives** and **the escort**, and the fighter population
(134 → 92) is close to irrelevant to both. An untouched 1500/1500 hull at the
moment the escort passes 33 % is not a good run — it is **unspent risk budget**.
Concretely, for the next attempt, in priority order:
1. **Turrets near the asset must become targets**, not keep-out zones — accepting
hull damage is the only way to cut ~50 % of the incoming escort damage. The
keep-out rule should be scoped to turrets that are *not* threatening the
asset, rather than applied globally.
2. **Engage warships.** `WARSHIPS 0000` forever means the objective class has
never been attacked; `REMAINING OB` rising is the scoreboard saying so.
3. Re-check whether the escort damage rate actually falls once turrets die —
that is the experiment that tells us whether (1) is sufficient or whether the
bombers need dedicated intercept too.
## Method note, learned the hard way
A harness-tracked **background** task does *not* protect the display: the same
run launched in the background lost Xvfb 11 s in, at the turn boundary
(`skip_intro` exit 3, "DISPLAY LOST"). The comment in `launch_mission.sh` saying
the script may be run as a tracked background task is **wrong**; the
one-blocking-foreground-call rule still stands, which caps a single attempt at
the tool's 600 s timeout. And do not pipe a long run through `tail` — the first
attempt printed nothing because `timeout` killed the pipeline before it flushed;
the on-disk artifacts are what survived.

View File

@@ -19,7 +19,9 @@ alive(){ ps -o pid=,stat= -C xenia_canary 2>/dev/null | awk '$2 !~ /^Z/ {print $
# that actually bought was the opposite: a process nothing owns is a process # that actually bought was the opposite: a process nothing owns is a process
# nothing keeps alive, and both were being reaped a couple of minutes in — the # nothing keeps alive, and both were being reaped a couple of minutes in — the
# long-standing "Xvfb and the emulator die on their own every few minutes" note. # long-standing "Xvfb and the emulator die on their own every few minutes" note.
# Run this whole script as ONE tracked background task and leave Xvfb, openbox # MEASURED WRONG 2026-08-10: a harness-tracked BACKGROUND task does not protect
# them either — the display was lost 11 s in, at the turn boundary. Run this
# whole script as ONE BLOCKING FOREGROUND call and leave Xvfb, openbox
# and xenia as its children: they then live exactly as long as the session does. # and xenia as its children: they then live exactly as long as the session does.
# `nohup` still shields them from a stray HUP; the exit-status wrapper means a # `nohup` still shields them from a stray HUP; the exit-status wrapper means a
# death is reported with the server's own account instead of being inferred. # death is reported with the server's own account instead of being inferred.

60
tools/re-capture/mission_run.sh Executable file
View File

@@ -0,0 +1,60 @@
#!/usr/bin/env bash
# Attempt to COMPLETE a mission and record how it ends.
#
# Every previous flight session was time-boxed to 240 s to measure something
# (escort hull, lethality, ship placement) and none ever reached a mission
# outcome — "no mission completed" has been the standing open item. Unit
# definitions are instantiated per stage, so story progress is the only thing
# that grows unit coverage past 21/110, and that needs a WIN, not a survival.
#
# So this run is deliberately long and its only deliverable is the ENDING:
# screenshots throughout, every entity's hull sampled, and the pilot log kept
# whole (never tail-piped — a frozen log tail is what mission-end looks like
# from outside, and tailing throws away the transition).
#
# Runs as ONE tracked background task with Xvfb/openbox/xenia as plain nohup
# children — see docs/re/session-lifetime notes; do NOT setsid anything.
#
# Usage: mission_run.sh [flight_seconds] [tag]
set -u
export HOME=/sylph-home/re SDL_AUDIODRIVER=dummy DISPLAY=:98
export PYTHONPATH=/sylph-home/.local/lib/python3.12/site-packages
SD="$(cd "$(dirname "$0")" && pwd)"
SECS="${1:-900}"
TAG="${2:-mission}"
SHOTS=/sylph-home/re/shots
OUT="/sylph-home/re/$TAG"
mkdir -p "$SHOTS" "$OUT"
"$SD/launch_mission.sh" fly || { echo "BOOT FAILED"; exit 1; }
python3 "$SD/entities2.py" self 0x130 "$OUT/cfg.json" || { echo "BIND FAILED"; exit 1; }
echo "=== initial entity table ==="
python3 "$SD/mission_state.py" scan "$OUT/cfg.json"
# A screenshot every 30 s for the WHOLE run: the outcome card (MISSION COMPLETE
# / GAME OVER) is on screen only briefly, so sampling must not stop early.
( n=$(( SECS / 30 + 4 ))
for i in $(seq 1 "$n"); do
printf '%s SHOT %03d\n' "$(date +%s)" "$i" >> "$OUT/shots.log"
screenshot "$SHOTS/$TAG-$(printf %03d "$i").png" >/dev/null 2>&1
sleep 30
done ) &
SHOTTER=$!
date +%s > "$OUT/t0"
python3 "$SD/mission_state.py" watch "$OUT/cfg.json" "$SECS" 2 "$OUT/mission.jsonl" \
> "$OUT/mission.log" 2>&1 &
WATCHER=$!
python3 "$SD/pilot.py" "$OUT/cfg.json" "$SECS" > "$OUT/pilot.log" 2>&1
PILOT_RC=$?
wait $WATCHER 2>/dev/null
kill $SHOTTER 2>/dev/null
screenshot "$SHOTS/$TAG-end.png" >/dev/null 2>&1
cp -f "$SHOTS/$TAG-end.png" "$OUT/end.png" 2>/dev/null
echo "PILOT_RC=$PILOT_RC"
echo "--- last 5 pilot lines ---"; tail -5 "$OUT/pilot.log"
echo "--- shots: $(ls "$SHOTS/$TAG-"*.png 2>/dev/null | wc -l) ---"
echo "MISSION RUN DONE ($TAG, ${SECS}s)"