Releases: IvanSkainet/arena-agent
Release list
v4.170.0 — python works on Windows, a failing script reports failure
48 commits since v4.169.50, none of them released — every install has
been sitting on 22 August.
Minor rather than patch because the bridge's own behaviour changed on
Windows. Anything parsing exit_code from /v1/exec/script there was
being given the wrong answer before this release.
Bridge behaviour
- A failing PowerShell script now reports failure (#249).
-File
exits 0 unless the script itself callsexit, so a script whose
command failed returnedexit_code: 0, ok: true— a failed script was
indistinguishable from a successful one. bash never had this. pythonandnodeare no longer refused on Windows (#248). The
interpreter table marked them Unix-only, soX-Arena-Interpreter: pythonanswered400 not available on Windowson hosts where python
is installed. Windows now maps topython, not the WindowsApps alias
that can open the Microsoft Store instead of running your script.
Security
- Path traversal in rollback (#243) —
time_machine.pycalled
tar.extractall()with no member validation. A snapshot containing
../../escaped.txtwrote outside the destination. Live on Python
3.10–3.13; 3.14 already refuses it via PEP 706. - SSRF-shaped probe in the diagnostic (#246) —
check_bridge.py
fetched thepublic_urlthe bridge reported, so a compromised bridge
could make it read a local file. - gitleaks stopped being blind to live source files (#178) — the
allowlist disabled every rule for listed paths, so a real committed
token passed unnoticed. - bandit now scans what we ship (#242, #244) — it covered
arena/
only, while the release also shipsscripts/,bin/andskills/.
Those held 7 HIGH and 22 MEDIUM findings the gate had never seen.
CI and tests
Two flake classes closed and ratcheted (#233, #238, #239); three gates
that were reporting success when they should not have been (#232, #234,
#240); Harden-Runner egress enforcement extended to 31 jobs from
measured audit logs; GuardDog scanning dependencies for malware rather
than only known CVEs (#228).
Full detail in CHANGELOG.md
— русская версия.
Verification
Built twice deterministically and attested from commit 07891e6e.
Acceptance run on a real Windows host against the attested bytes: 7/7.
v4.169.50 - Windows auto-update could not install a release
Windows auto-update could not install a release (#158)
Installing v4.169.49 on a real Windows host failed twice, in two distinct ways. Both defects were in the shipped updater, and neither was caught by a test — both were found by reading .arena-update-apply.log on the live machine. Together they meant Windows auto-update could not install a release unattended.
1. A pre-existing rollback directory aborted the update
[22.08.2026 20:01:46] bridge exited, starting copy
[22.08.2026 20:03:16] rollback directory unavailable
[22.08.2026 20:03:16] ERROR copy failed, restoring rollback snapshot
Not one file was copied. The bridge came back on the old version with the new release sitting unused on disk.
mkdir "<backup>" 2>NUL
if not errorlevel 1 goto :rollback_dir_ready
mkdir sets errorlevel 1 when the directory already exists. The snapshot path is derived from the version being replaced, so it is identical on every retry of the same upgrade — and apply itself prepares it before the mover runs. Any retry was guaranteed to fail closed.
Readiness is now gated on the directory existing (if exist "<dir>\."), after clearing any stale snapshot. Same lesson as v4.169.21 and schtasks: the exit code of the call meant to produce a thing is not evidence the thing exists.
2. Two relaunch helpers raced, and the old build won
The retry copied correctly, yet /health still answered 4.169.48 — with 118 seconds of uptime. New files on disk, old code in memory.
.arena-update-apply.log bridge exited, starting copy 20:11:58
.arena-restart.log fired start_hidden.vbs 20:11:59
.arena-update-apply.log copy done, launching relaunch 20:12:05
apply arms a mover that waits for the bridge PID, copies, then relaunches. A manual POST /v1/admin/update/restart armed a second detached waiter on the same PID; with nothing to copy it won by six seconds and started Python against a tree robocopy was still rewriting.
Detection deliberately does not look for the mover's files: .arena-update-apply.cmd is never deleted and the mover's own lock cleanup is best-effort, so on any host that has ever updated both can be present with no mover running. apply now publishes .arena-update-mover.pid naming the process the mover waits for, before spawning it — which also closes the window before the lock directory appears. A marker that does not name the running process is ignored, so every stale-artefact case degrades safely to arming our own helper.
3. The rollback purge was unverified
rmdir /S /Q suppresses its errors, so a locked file could leave the old snapshot in place while if exist still succeeded — producing a backup that mixes two attempts. The mover now confirms the directory is empty and aborts with a distinct log line if it is not.
Verification
Exercised against real cmd.exe on a Windows host, not only in pytest: a fresh directory, an existing one with stale content, an empty one, a path with parentheses and a space, an impossible volume, and a purge blocked by an open file handle (correctly reports dirty and stops).
Suite: 9361 passed, 36 skipped. Builds are byte-reproducible and attested with provenance + SPDX SBOM.
v4.169.49 - timing-safe credential comparison that non-ASCII input cannot crash (#61, #63)
A non-ASCII credential crashed the auth path (#61)
Issue #61 was filed against a docstring: AgentRegistry.resolve_token
claimed constant-time comparison "via hmac.compare_digest inside
_derive_agent_token" while resolving tokens with a plain dict lookup.
_derive_agent_token mints tokens with hmac.new; it compares nothing.
Applying the fix the issue suggests -- hmac.compare_digest in the
lookup loop -- would have introduced a worse bug than it fixed, because
that function raises on non-ASCII str:
TypeError: comparing strings with non-ASCII characters is not supported
Checking that hazard against the eight existing comparison sites found
it was already live. Measured against the running bridge, with no
credential at all:
GET /v1/status Authorization: Bearer <u-umlaut> -> 500
GET /v1/status Authorization: Bearer agent-<uml>-attack -> 500
GET /gui?token=%C3%BC -> 500
GET /v1/status Authorization: Bearer agent-deadbeef-... -> 401
An anonymous caller could turn any auth check into an unhandled
exception on paths whose entire job is to answer 401 -- an
error-response amplifier and a liveness bug, reachable from the public
internet whenever a tunnel is up.
The fix is arena/auth/compare.py: secrets_equal compares UTF-8
bytes, which compare_digest accepts for every input, so the
comparison stays timing-safe and becomes total. Non-string operands
(None, a dict decoded from JSON) compare False instead of raising.
Lone surrogates, which a latin-1-decoded header can carry, are encoded
with surrogatepass rather than crashing on the way in.
Applied at every credential comparison: the master-token path, the
per-user roster, both GUI login routes, the public-tunnel
acknowledgement, and the signed URL cache. arena/input_helper/ helper_server.py carries its own copy on purpose -- it runs as a
standalone script outside the package -- with a test asserting the two
agree.
And #61 proper: resolve_token now scans every registered token
without breaking early, so the number of comparisons no longer depends
on where the match sits, and the docstring describes what the code
actually does.
The unauthenticated /v1/version no longer fingerprints the host (#63)
Captured from the live bridge, with no token:
$ curl -s http://127.0.0.1:8765/v1/version
{"ok": true, "version": "4.169.48", "service": "arena-unified-bridge",
"python": "3.14.7", "platform": "Windows-10-10.0.19044-SP0",
"loopback_only": true, "deployment": {...}}
"Python 3.14.7 on Windows-10-10.0.19044-SP0" is not trivia. It names the
exact CVE set worth trying against this host -- interpreter patch level
and OS build number are the two inputs a vulnerability scanner most wants
-- and anyone who could reach the port could read it for free.
python and platform are now gated on authentication rather than
deleted. They have a real operator use, and authenticated callers already
receive both from common_status() on /v1/info and /v1/status, so
gating costs no function. An authenticated /v1/version additionally
returns the full deployment record instead of the public subset.
What deliberately did not change: version stays public. /health,
/v2/health, /, /metrics and /api-docs all publish it, installers
read it before they hold a token, and the Android status screen shows it.
Truncating it on this one route would have cost real function while
hiding nothing -- a fix that only looks like one.
The route also keeps answering anonymous callers with 200. The auth check
here reads the credential without refusing the caller: require_auth
returns 401 and counts toward the 10-failures-in-60s throttle, so using
it on a by-design-public route would have rate-limited the Android app,
which polls /v1/version with no token because it cannot read the token
file under a different UID. A probe that raises, or wiring that forgets
to supply one, falls back to the anonymous body -- the failure mode
discloses less, never more.
Two follow-ups the review surfaced, both verified by measurement rather
than argument. check_auth now returns early when the caller presented
no credential at all: it used to fall through to the roster lookup,
which is pure cost for a request that cannot match anything. And
UserStore.load_users caches an empty roster like any other -- the TTL
check previously required a non-empty dict, so a bridge with an empty
or absent users.json re-read it on every auth check (measured: 100
reads per 100 calls; 1 per 100 once a user exists). Dormant while only
authenticated routes probed auth, but /v1/version is public and
polled, so gating it would have handed anonymous callers a disk-I/O
amplifier. A damaged file is still not cached: a roster somebody is
repairing should be retried. A probe that raises is now logged at
WARNING instead of failing closed in silence.
Verification
Built twice from 13cc94496d36a50db1db65ffc032b6a291098a6d in
candidate run 32578284088;
independent builds A and B matched byte-for-byte.
arena-agent-v4.169.49.zip 46293f9153585bffab04a22da86816b10f39156e3451f8e0b02ddc909fbbdc1c
arena-agent.zip 46293f9153585bffab04a22da86816b10f39156e3451f8e0b02ddc909fbbdc1c
arena-bridge.apk 32bb7d96dad6a26c6107bfb8f8edbcc303708b39be433eff4d251b3d9b42c13b
Both ZIPs are byte-identical; the alias is a copy, not a rebuild.
Build provenance and the SPDX SBOM predicate were verified for all three
assets against the pinned release-candidate.yml signer workflow with
--source-digest bound to the exact commit and --deny-self-hosted-runners.
The check was confirmed to be meaningful rather than vacuous: verification
with a wrong source digest and with a different signer workflow both fail.
Full suite on the released commit: 9337 passed, 36 skipped.
v4.169.48 — Archive deployment provenance
Archive deployment provenance and rollback identity
Release ZIPs now carry canonical source identity: repository, exact source commit, strict release tag, and candidate workflow run. Installation writes DEPLOYED_PROVENANCE.json, binding that identity to the downloaded ZIP SHA-256 and install time. authenticated=true is reported only when the installed asset also matches official GitHub Release metadata.
Archive updates retain identified rollback trees, restore complete target shape after failure, quarantine malformed historical provenance, and remove explicitly retired release files transactionally. The obsolete root version.json marker is removed from both new archives and existing installations.
Public /v1/version exposes only a minimal deployment summary. Exact source/run/SHA and rollback identity remain on authenticated /v1/info and /v1/status.
Accepted candidate
- source commit:
9b66e745e6d83f8f6889efb8fc5045d77c829681 - candidate run:
32032140006 - ZIP SHA-256:
b88f7220db12b83d5aad2c50b670c34b0c3c286ee38efa4790527e08b701d672 - APK SHA-256:
7d75889fcd36d2b83539f1d12fc2ed6fb3aaa28064200aaed7c8eb6fb95a5281
The exact candidate passed independent A/B byte comparison, build provenance and SPDX SBOM attestations, the pinned Book of Eternity Windows contract, full CI/security matrices, and real Windows installation acceptance. The installed tree matched 1,022/1,022 release-target files; the retired root marker was absent and preserved only inside the identified rollback snapshot.
v4.169.47 - fail-closed security and exec lifecycle
Skainet Bridge v4.169.47 — fail-closed security and exec lifecycle
This release combines security scanner hardening, remote execution lifecycle cleanup, and release-test stabilization.
Security scanners now fail closed
The Security workflow no longer turns scanner execution failures into green checks. TruffleHog, OSV, Syft/Grype, Socket Firewall, DevSkim, Bandit, Semgrep, and pip-audit now use explicit exit/report contracts: missing, empty, malformed, incomplete, or unexpected reports block the gate. Finding thresholds remain explicit and separate from scanner health.
The first real fail-closed DevSkim run exposed a genuine defect: the public tunnel probe in scripts/check_bridge.py disabled TLS verification unconditionally. Public probes now use the shared strict TLS context by default; the operator's explicit insecure-TLS opt-out remains narrow and visible.
Remote exec follows the HTTP client lifetime
Buffered /v1/exec, raw /v1/exec/script, and silent waits in /v1/exec/stream now watch the underlying HTTP transport. If the remote client or proxy disconnects, the handler cancels and awaits the runner, which kills and reaps the complete process tree on Windows and POSIX. Semaphore slots, ACTIVE_PROCESSES, stream generators, and temporary scripts are cleaned on the same path.
This closes the class that previously left cmd → powershell → gh run watch alive after a tunnel-side failure.
Ship-smoke test isolation
The ship-smoke test no longer assumes an empty flight-record directory. It measures before/after history, requires an exact one-record delta, and checks that the only new record is the returned report path.
Acceptance evidence
- T40 merged-head manual Security run: all scanner jobs and
Security requiredpassed. - T44 new lifecycle module: 0/54 surviving mutants.
- Bilateral sabotage: disabling transport-close detection made the watcher test fail with
TimeoutError; restoring it returned green. - Exact PR-head Windows Python 3.14 acceptance: 19 focused client-abort/tree-kill tests passed; no
time.sleep(30)child remained. - Full local preflight and fail-closed local security gates passed.
- Release ZIP pair and Android APK are built from one exact candidate commit, independently attested with provenance and SPDX SBOM, accepted on Windows before publication, then signed and anonymously verified after publication.
v4.169.46 — attested Android APK releases
v4.169.46 — 2026-08-15
Android APK is a first-class attested release asset
The release candidate now builds arena-bridge.apk from the exact candidate
commit, verifies package name, version, Android signature, and the pinned signer
certificate, then includes the APK beside the byte-identical ZIP pair. The final
candidate checksum has three entries. APK provenance and a dedicated SPDX SBOM
are generated independently from the ZIP SBOM.
sign-release.yml now refuses a release when the APK is absent, substituted, or
not present in the accepted exact-SHA candidate. It verifies APK provenance and
SBOM, includes the APK in SHA256SUMS, creates a Sigstore certificate/signature
pair for it, and repeats anonymous post-publication download verification.
Persistent Android signing identity
Ordinary CI continues to use a disposable debug key because its APK is only a
build test. Release candidates use a persistent JKS identity stored in GitHub
Actions secrets with a DPAPI-protected operator backup. The public certificate
SHA-256 is pinned in android_app/release-signing-cert.sha256; replacing the
secret with another valid key fails the candidate.
The APK backfilled into v4.169.45 came from its exact-source CI run but used that
run's disposable debug identity. Anyone who installed that backfill must remove
it once before installing v4.169.46. APKs from v4.169.46 onward share the stable
release identity and support normal in-place upgrades. dev/bump_version.py now
also derives a monotonic Android versionCode from semver; v4.169.46 uses
41690046 instead of leaving every release at 1.
Validation
- Mandatory sabotage: removing the APK dependency from attestation makes the
release contract red; restoring it passes. - Full local preflight and security gates passed.
- PR #37 full platform matrix passed; one unrelated Windows ship-smoke absolute
history-count flake was tracked separately and its failed job passed on rerun. - Exact merged candidate run
31880691097passed ZIP A/B, release APK build,
pinned Windows game contract, provenance, ZIP SBOM, and APK SBOM jobs. - Candidate APK certificate matched the pinned fingerprint and both GitHub
attestation verifications passed.
v4.169.45 — Arena Agent Mode GM and source-bound releases
v4.169.45 — 2026-08-15
Arena Agent Mode can serve as the external Game Master
This release productizes the real client в†’ daemon в†’ ConPTY в†’ generic relay в†’
Arena Agent Mode path without adding game rules to Skainet Bridge. The Bridge
owns transport, durable lifecycle, host files, and execution; The Book of
Eternity: Reborn remains authoritative for rules, progression, validation,
canonical state, and repair semantics.
The generic relay now exposes explicit queued в†’ claimed в†’ busy в†’ replied
states through HTTP, MCP, CLI, and Dashboard surfaces. A fresh Arena session can
inspect status and explicitly resume the exact durable packet after confirming
the old session is gone. An inactive Arena session is never reported as a live
listener: unclaimed work remains queued until a real poll occurs.
Live Windows acceptance used no Codex process. Player-visible turns 4–7 passed
through the actual game client, daemon, GMBridge/ConPTY, persistent
arena-relay terminal, and Arena Agent Mode. The run proved unattended queue
persistence, fresh-session resume from canonical host state, correlated replies,
and a deliberate validation failure followed by the daemon-delivered bounded
repair. The browser extension's real inline Run control also executed
relay.status on arena.ai and inserted the result.
Independent pre-merge audit hardened malformed relay state
A green PR still contained three honesty/availability failures found by direct
sabotage:
- JSON
NaN/Infinityin the persisted listener heartbeat could fabricate
agent_polling=true; - one claimed record with a non-numeric timestamp could crash all of
relay.statusand hide healthy queued work; - an infinite MCP status limit raised
OverflowErrorinstead of returning a
bounded response.
Persisted timestamps must now be finite. Malformed claimed and reply records are
isolated without deleting their evidence or blocking healthy traffic, and MCP
limits fail to the bounded default. The sabotage tests failed on the prior code
and passed after restoration.
Source-bound deterministic release candidates
The release pipeline no longer signs arbitrary ZIP bytes that happened to be
uploaded first. Two independent CI jobs build the exact commit, compare the
archives byte-for-byte, verify canonical ZIP layout/timestamps/modes, generate an
SPDX SBOM for the shipped ZIP, and create GitHub provenance plus SBOM
attestations. Release signing requires the exact tag source digest, exact
candidate workflow, exact byte-identical public ZIP pair, and the accepted
candidate manifest before cosign signatures are produced.
The same attested bytes must pass real Windows acceptance before publication and
must remain unchanged through anonymous post-publication download and install.
Pinned cross-repository compatibility contract
A Windows workflow now checks out the exact pinned commit of The Book of
Eternity: Reborn, builds its real GMBridge, hosts the shipped generic terminal
relay in ConPTY, and exercises two multiline turn dispatches plus one validation
repair. Evidence requires exact correlation, drained queues, no partial files,
and terminated Arena/GM process trees. A freshness gate refuses scheduled and
release runs when upstream main moves beyond the reviewed compatibility pin.
Repository governance and CI efficiency
master now requires PRs and stable aggregate checks without requiring a human
approval for the single-maintainer agent workflow. Issue Forms and the PR
template make root cause, sabotage, live evidence, security impact, and
cross-repository ownership explicit. A fail-closed change classifier skips the
expensive platform matrix only for measured documentation-only diffs. The old
write-to-master version badge bot and its generated metadata were removed.
Validation
- 8,436 tests collected; the complete local suite passed.
- Configured coverage: 60.63%.
- Preflight: 23/23 gates passed; Ruff, lint debt, and quality debt remained zero.
- Bandit: 0 high/medium; Semgrep: 0 findings; pip-audit: 0 known runtime CVEs.
- PR #29 exact head: 61/61 acceptable checks, including Linux/macOS/Windows
Python matrix and the real Windows ConPTY compatibility contract. - Post-merge master checks completed with no failures.
v4.169.44 - Windows exec and restart lifecycle hardening
v4.169.44 — Windows exec and restart lifecycle hardening
This hotfix follows a live memory incident discovered after v4.169.43.
Root cause
The 44.5 GiB process was not Book of Eternity's client, daemon, ConPTY bridge, or terminal relay. It was an agent-authored PowerShell diagnostic sent through /v1/exec/script. Deep ConvertTo-Json recursively serialized the extended metadata carried by Get-Content objects. The request timed out after 90 seconds, but Windows cleanup killed only its cmd.exe shell; the PowerShell child survived for roughly three hours and accumulated 44,521.9 MB private bytes and 6,672 CPU-seconds.
Fixes
- Windows exec timeout now uses
taskkill.exe /PID <pid> /T /F, with direct kill as fallback. - Buffered and streaming runners kill and reap still-running children during cancellation, disconnect, and orderly shutdown.
- Streaming pump tasks are cancelled and awaited during abnormal teardown.
- Manual
/v1/admin/update/restartnow arms a detached WSH/CMD helper before exit, waits for the old PID, tries VBS/batch/task launchers, and verifies the port. - Restart is refused without shutting down when the helper cannot be armed.
- Windows auto-update reuses its existing mover instead of racing a second relaunch helper.
Validation
- 8,320 tests collected; bare full suite and
preflight.py --fullpassed. - Coverage: 63.07% lines / 51.65% branches.
- Bandit 0 high/medium; Semgrep 0 findings; pip-audit 0 known vulnerabilities.
- Live timeout sabotage: HTTP 408 at 3.174s; PowerShell parent PID 4552 and nested child PID 2848 were both gone; zero script orphans.
- Live manual restart: endpoint returned
relauncherPrepared=true; detached log reportedready via start_hidden.vbs; health returned automatically. - Post-sabotage memory sample: no retained process above 96.4 MB private; daemon, ConPTY, and terminal relay had zero growth.
v4.169.43 - daemon-driven terminal relay hardening
v4.169.43 — daemon-driven terminal relay hardening
This release is the first Book of Eternity stress run that preserves the real application architecture end to end: C# client request → daemon → ConPTY CLI transport → generic relay mailbox → active agent → ordinary host file writes → client validation → daemon-delivered repair → bounded agent repair. No game rules or state machine were added to Skainet Bridge.
Core fixes
- Added generic persistent
arena-relay terminalingress for ordinary lines, multiline bracketed paste, and observed Windows ConPTY raw dispatch. - Removed the Windows Console 510-character truncation failure with temporary raw VT input and incremental non-greedy chunk decoding.
- Added Ctrl+U/delayed-Enter framing, embedded newline preservation, bounded visibility echo, and per-dispatch rearming when ConPTY strips ESC paste markers.
- Changed atomic relay temp files from
.tmp-*.jsonto.partial, preventing concurrent Windows mailbox readers from causing WinError 32 duringos.replace. - Corrected the default local coverage floor so documented bare
python -m pytest -qis runnable while CI keeps its stricter Linux override. - Hardened release packaging: ignored workspace artifacts are inspected and
coverage.xmlcan no longer leak into a clean-tag archive. - Made the pyrefly quality gate fail closed when the analyzer is absent or returns malformed/empty output; fixed the Windows-only
WinDLLtyping path.
Live acceptance
- Turn 2 arrived as one exact 33,937-character daemon prompt and was accepted by the client.
- Turn 3 intentionally introduced
narrative_response_unknown_field; the daemon delivered the exact repair packet; the bounded repair was accepted and rendered. - Two sequential four-line dispatches in one terminal process preserved exact bodies and sequence 1 → 2.
- Concurrent reply long-poll + atomic write returned HTTP 200 with exactly one correlated reply; final inbox and reply depths were zero.
Validation
- 8,307 tests collected; bare full suite passed.
preflight.py --fullpassed after final changes.- Coverage: 63.07% lines / 51.61% branches (60.40% combined).
- Bandit: 0 high/medium; Semgrep: 0 findings; pip-audit: 0 known vulnerabilities across 18 runtime dependencies.
- Release ZIP: 1,148 files; extracted version and
arena-relay terminalCLI verified; generated coverage/test artifacts absent.
Full engineering journal: docs/scenarios/BOOK_OF_ETERNITY_DAEMON_E2E.md.
v4.169.42
v4.169.42 — 2026-08-13
Scheduled gitleaks was the first time the allowlist met history
The push Security scan on 433f6df6 was green. The next night's
schedule run (#31676936942) scanned 1852 commits and reported 12
leaks. All twelve were fixtures, the RFC 6455 example WebSocket nonce,
or files that no longer exist on HEAD. .gitleaks.toml only covered
the redaction tests, so a blocking scanner that looks at history had
never been asked the question it is supposed to answer.
- Expanded the allowlist to those known false-positive paths and the
RFC 6455 example keydGhlIHNhbXBsZSBub25jZQ==. - Split the
ghp_secret123fixture intest_handlers_update_parity_v4_169_39.py
into concatenation ("ghp" + "_" + "secret123"), per AGENTS.md. tests/test_gitleaks_allowlist_v4_169_42.pypins the paths, refuses a
blankettests/exemption, and sabotages both ways against a throwaway
git repo when the gitleaks binary is present. Full-historygitleaks detecton this tree is now 0 findings.
Book of Eternity is a scenario, not a second game
The vanilla host file tools are the product. Terminal signals now match
GM_Turn_Helper.ps1 byte-for-byte on disk:
ready/turn_complete.json—sessionId,requestId,turnNumber,
timestamp,status=success,filesModifiedready/turn_error.json— same ids +status=error+errorvalidation_repair_ready.json— same ids +status=success, written
only togame_state/control/(the path the C# client reads)
tests/test_boe_file_protocol_e2e_v4_169_42.py writes the official
output/* artifacts with ordinary JSON (the same bytes fs.write would
put on disk) and checks the field sets the C# validator accepts:
narrative (response, timestamp), debug (gm_thoughts_markdown,
timestamp), interface (dialogueOptions, image_prompt, timestamp).
Also: T30 (workflow timeouts/concurrency) and T31 (relay parity) remain
on master from the previous session and ship in this release.