Boot-time env report + fix the env reference

The bot is spawned by the server/GUI, so it inherits the SERVER's
environment. The user could not tell whether their exports reached the
bot, so print a one-shot greppable report at boot:

  grep '^\[env\]' /tmp/modularbot_stdout.log

Section A prints every TR_*/GUN_* this process actually received, the
count vs the total env size, a loud warning when nothing matched, and
the process identity (pid/ppid, cwd, self command line, and the PARENT
command line) so the spawn trap is obvious. Section B prints the
resolved effective value of every documented knob with its source
(env|default), including clamps and the rack's empty-set fallback.
Build identity (NimVersion, compile date/time, binary path/size/mtime)
pins the exact artifact. Suppress with TR_ENV_REPORT=0.

docs/env_reference.md: add the missing GUN_SHOTLOG_PATH,
GUN_SELECTOR_MINOBS/FLOOR/POOL/RANK/SHRINK/SEED, TR_ENV_REPORT and
-d:TM_NCLAUSES; record the measured TR_TMHORIZON_WINDOW verdict; and
add a prominent 'Did my env vars actually reach the bot?' section with
the boot report, the /proc/PID/environ no-code check, the correct GUI
launch recipe, and how to prove the trap deliberately.
This commit is contained in:
2026-09-23 08:21:25 +02:00
parent 7f6ccfb015
commit 9bf3005850
3 changed files with 453 additions and 1 deletions
+99 -1
View File
@@ -9,6 +9,94 @@ Every knob the bot reads. **All are read ONCE at process start.**
An unset (or empty, or unparseable) value means **the shipped default** — a typo
can never silently change behaviour, it warns on stderr and falls back.
Every process prints a one-shot, greppable boot report to stdout; see
[Did my env vars actually reach the bot?](#did-my-env-vars-actually-reach-the-bot)
below. `TR_ENV_REPORT=0` suppresses it.
---
## Did my env vars actually reach the bot?
**The spawn trap:** the bot is spawned by the **server/GUI**, so it inherits the
**server's** environment — exporting a variable in an unrelated terminal does
nothing, because that terminal is not the bot's parent.
### 1. Ask the bot itself (boot-time report)
Every process prints a one-shot, greppable report to **stdout**
(`/tmp/modularbot_stdout.log` when launched by the GUI, or the GUI/server
console). Recover just the report with:
```sh
grep '^\[env\]' /tmp/modularbot_stdout.log
```
It has three parts, all behind `[env] === ENVIRONMENT ... ===`:
- **A. raw process environment** — every `TR_*`/`GUN_*` this process *actually*
received, sorted, followed by
`raw: N TR_*/GUN_* of M total environment variables`. It then prints the
process identity: `pid`/`ppid`, the cwd, the bot's own command line, and the
**parent's command line**. The parent command line is the conclusive proof of
which process spawned the bot (the server/GUI — not your shell). If nothing
matched it prints a loud
`[env] WARNING: this process has NO TR_*/GUN_* variables - the env you exported did NOT reach the bot.`
- **B. effective values** — for every knob, the value the bot will *actually*
use and whether it came from `env` or the shipped `default`, including the
effects of parsing, clamping and fallback (e.g. `TR_TMHORIZON_NSTATES` is
clamped to `2..4096`; `TR_RACK_*` shows the empty-set fallback). Where a value
is only resolvable later, it is labelled `(raw - not resolved here)` rather
than guessed.
- **build identity** — `NimVersion`, compile date/time, and the binary path +
size + mtime, so a stale binary is obvious.
Suppress the report with `TR_ENV_REPORT=0`.
### 2. The no-code check (authoritative)
`/proc/PID/environ` is the environment **at exec time** — exactly what the bot
started with, before it read anything:
```sh
for p in $(pgrep -f ModularBot); do
echo "== $p"
tr '\0' '\n' < /proc/$p/environ | grep -E '^(TR_|GUN_)'
done
```
### 3. Launching the GUI with knobs
Export in the **same shell** that launches the GUI/server, then start it from
that shell. Because the launcher **appends** to the log, clear it first so you
read only the new run:
```sh
rm -f /tmp/modularbot_stdout.log
export TR_POWER_ENERGY_MIN=1.0 TR_RACK_PATTERN=off
./start-gui.sh # whatever launches the server/GUI
grep '^\[env\]' /tmp/modularbot_stdout.log
```
### 4. Prove the trap deliberately
Terminal A — where you *exported*, but never launched anything:
```sh
export TR_POWER_ENERGY_MIN=1.0
```
Terminal B — where you launch the GUI:
```sh
./start-gui.sh
grep '^\[env\]' /tmp/modularbot_stdout.log | grep WARNING
# [env] raw: 0 TR_*/GUN_* of ... total environment variables
# [env] WARNING: this process has NO TR_*/GUN_* variables - the env you exported did NOT reach the bot.
```
Terminal A's variable never reached the bot because the bot's parent is the
server, not terminal A. Fix it by exporting in terminal B before launching.
---
## The ones you'll actually use live
@@ -22,6 +110,7 @@ can never silently change behaviour, it warns on stderr and falls back.
| `TR_RAM_LOG` | off | `1` = log ram on/off with the reason |
| `TR_MOVEMENT_LOG` | off | `1` = log movement band/class changes |
| `TR_TMHORIZON_LOG` | off | `1` = let the horizon TM gun log its thinking per shot |
| `TR_ENV_REPORT` | `1` | print the boot-time `[env]` process-environment report to stdout; `0` suppresses it |
---
@@ -32,6 +121,7 @@ can never silently change behaviour, it warns on stderr and falls back.
| `TR_RACK_<GUN>` | `PATTERN=both`, all others `off` | membership: `both` \| `1v1` \| `melee` \| `off` |
| `GUN_RACK_DISABLE` | empty | comma-separated gun **ids** to remove entirely (older mechanism; the gun never even spawns virtual bullets) |
| `GUN_STATS_PATH` | `/tmp/gun_stats.jsonl` | where the per-round per-gun stats are written |
| `GUN_SHOTLOG_PATH` | `/tmp/shot_log.jsonl` | where the per-shot JSONL (Task A) is written |
`<GUN>` names: `HEADON LINEAR TSETLIN CIRCULAR GUESSFACTOR PATTERN WALLBOUNCE ACCEL
STOPSHOT DISPLACE AVGLEAD DECAYGF KNN TMSELECT TMPATTERN TMHORIZON`.
@@ -56,8 +146,14 @@ If **every** gun is `off`, the rack falls back to admitting all of them.
| `GUN_SELECTOR_MODE` | `relative` | tie-band model: `relative` (scale-aware) or `absolute` (legacy fixed bars) |
| `GUN_SELECTOR_WINDOW` | `100` | rolling window (ticks) for the fitness rates |
| `GUN_SELECTOR_TIE` | `0.20` | tie band width: tied if `rate >= bestRate*(1-this)` |
| `GUN_SELECTOR_FLOOR` | `0.25` | relative floor: fire HeadOn only if best rate < this × its peak |
| `GUN_SELECTOR_MINOBS` | `50` | min observations before a gun×bin may compete (clamped to ≥1) |
| `GUN_SELECTOR_POOL` | `on` | pool the gun's power bins when computing its rate |
| `GUN_SELECTOR_RANK` | `mean` | window statistic: `mean` \| `wilson` \| `ucb` \| `thompson` \| `shrunk` |
| `GUN_SELECTOR_SHRINK` | `20.0` | empirical-Bayes shrink strength (`shrunk` rank only) |
| `GUN_SELECTOR_DWELL` | `10` | ticks the incumbent gun is held before it may be displaced |
| `GUN_SELECTOR_MARGIN` | `0.05` | a challenger must beat the incumbent by this fraction to displace it |
| `GUN_SELECTOR_SEED` | unset | integer seed for the tie-break RNG; unset = time+pid per process |
| `GUN_SELECTOR_TIEBREAK` | `off` | `off` (shipped) / `point` / `pointCommit`. Measured **negative** on real hit rate; opt-in only |
| `GUN_SELECTOR_POINT_TIE` | `0.5` | margin used by the `point` tie-break |
@@ -130,7 +226,7 @@ only path that converts.
| `TR_TMHORIZON_BIG_MULT` | `1.5` | multiplier when the predicted correction is BIG |
| `TR_TMHORIZON_NSTATES` | `64` | **automata inertia** — the "mood". Lower = adapts faster, higher = more stubborn (2…4096) |
| `TR_TMHORIZON_RESET_ON_TARGET` | on | wipe the model when the target changes (no-op in 1v1) |
| `TR_TMHORIZON_WINDOW` | `0` | sliding window: retrain on only the most recent N samples. `0` = keep everything |
| `TR_TMHORIZON_WINDOW` | `0` | sliding window: retrain on only the most recent N samples. `0` = keep everything. **ON is MEASURED HARMFUL live: 26.5% vs 49.0% round wins, p=0.036 — keep it at `0`.** |
| `TR_TMHORIZON_RESET_DROP` | `0` | change detection: if rolling accuracy falls this many points below its own peak, treat it as "the enemy changed" and re-learn |
| `TR_TMHORIZON_ACCURVE` | off | `1` = log the rolling accuracy periodically |
| `TR_TMHORIZON_RETRAIN_EVERY` | `50` | samples between windowed retrains |
@@ -156,6 +252,7 @@ bearing-only, so a purely radial offset is invisible. Measured byte-identical on
| variable | default | what it does |
|---|---|---|
| `TR_VBULLET_ADMIT_ONLY` | `1` | only admitted guns spawn virtual bullets. **Turning it on gave +68% tick rate** (87→146 ticks/s); `0` restores the old behaviour |
| `TR_ENV_REPORT` | `1` | print the boot-time `[env]` report to stdout; `0` suppresses it |
| `TR_RECORD_WORLDSTATE` | off | append every tick's WorldState to `/tmp/worldstate_record.jsonl` for offline replay |
| `TR_RADAR_FORCE_SPIN` | off | force the old stateless full-spin melee radar (for A/B on one binary) |
| `TR_RADAR_SCANLOG` | off | append per-round radar scan/coverage JSON |
@@ -181,6 +278,7 @@ bearing-only, so a purely radial offset is invisible. Measured byte-identical on
| `-d:TMH_STALE_MAX=N` | 8 | a sample is dropped if the enemy was not seen this recently |
| `-d:TM_S_DEF=x` | `"3.0"` | `guns/tm_pattern.nim` specificity (`s`). **Higher = LONGER clauses**, measured. `s=1.0` is degenerate |
| `-d:TM_NSTATES=N` | 64 | `guns/tm_pattern.nim` |
| `-d:TM_NCLAUSES=N` | 40 | `guns/tm_pattern.nim` — clauses per class |
## Adding / finding knobs