feat(state): scope card evidence and policy memory by reported run identity

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

View file

@ -4,6 +4,7 @@
```text
run.py observe, execute, report results, record the session
run_state.py reported run identity and scoped combat-pile evidence
brain.py entry point, dispatch, navigation, shops, minigames
policy/
__init__.py package marker; no registration or initialization
@ -13,8 +14,9 @@ policy/
facts.py observation parsing and combat calculations
```
`brain.decide()` remains the policy entry point. The runner owns one
`PolicyContext` per session and reports action results to it.
`brain.decide()` remains the policy entry point. The runner owns a `RunContext`,
which holds policy memory and replaces it on reported run changes. The runner
reports action results through that context.
`brain.Decision` and `brain.PolicyContext` remain available as direct imports
of the shared types, so the runner interface does not change.
@ -47,8 +49,8 @@ These are plain modules, not a plugin framework or class hierarchy. Navigation,
shops, and minigames stay together until a further split helps development.
The extraction does not change game decisions, confidence gates, fallbacks,
state reconciliation, or recording. Run identity and persistent-deck provenance
remain separate work.
state reconciliation, or recording. The subsequent
[run-state pass](RUN_STATE.md) adds reported identity and card-evidence provenance.
## Extraction verification

117
docs/RUN_STATE.md Normal file
View file

@ -0,0 +1,117 @@
# Run identity and card-evidence provenance
## What is available
The vendored mod exposes `current_run.run_id` through `GET /api/v1/compendium`.
Its format includes save scope, profile, and start time. The seed describes
content; it is not the identity of an attempt.
The live player serializer exposes hand, draw, discard, and exhaust piles during
combat. It does not expose the persistent run deck. The compendium exposes a
save path, but the runner does not follow that path or read the save file.
Sources checked:
- `vendor/STS2MCP/McpMod.Compendium.cs`: `BuildCurrentRunContext`.
- `vendor/STS2MCP/McpMod.StateBuilder.cs`: `BuildPlayerState`.
## Ownership and identity
`run.py` owns one `run_state.RunContext` per invocation. The context owns the
current `PolicyContext` and optional combat-pile evidence. The context performs
no network or file operations.
The runner requests identity after each stable in-run observation, before
proposing an action. Menu, game-over, unknown, and overlay observations do not
trigger this request. Dry-run still performs identity reads.
This deliberately uses the existing full compendium endpoint. It adds one local
HTTP read per stable in-run step. A smaller identity endpoint would reduce cost,
but this pass does not modify the mod or add a polling cache with stale identities.
Live latency has not been measured.
A usable identity requires `is_in_progress: true` and a nonempty string `run_id`.
Missing or malformed identity remains unknown. The runner never substitutes a
seed, character name, or floor number as an identity.
- A different reported run ID resets policy memory, duplicate-action guards,
rejection counts, and card evidence.
- `--stop-on-run-end` stops before acting in the replacement run, with reason
`run_changed`. This reports a run boundary, not victory or defeat.
- Observed menu/game-over transitions clear run evidence and active-run policy memory.
- Unknown states and overlays preserve pending-action guards.
- Missing identity or a transport error discards card evidence but retains action
guards. Erasing accepted toggles during a metadata outage could toggle them again.
- The last known ID is retained privately across a metadata outage, so recovery
with a different ID can still reset state. Decision rows use null during the outage.
### Identity is reported, not atomic
The mod derives identity from save metadata. The observation and compendium are
separate requests. They are not an atomic snapshot, and save metadata can lag.
The runner cannot prove that both responses describe exactly the same instant.
A live observation-level run ID would provide a stronger association.
Do not infer exact attribution or game-action completion from this identity alone.
Externally switching runs while the bot acts remains a coordination risk.
## Card evidence
The runner no longer reads or writes `deck.json`. Existing files remain untouched.
No card evidence survives a runner restart. Unscoped legacy input to
`facts.deck_context` becomes `unknown`.
A combat-pile snapshot is retained only when a reported run ID is available.
Its provenance includes:
- `source: combat_piles`;
- reported `run_id`;
- `observed_step` within the current session;
- observed act and floor;
- `persistent_deck: false`;
- which pile lists were present;
- freshness: `observed_combat_piles` or `historical_combat_piles`.
The counts retain visible temporary cards, including Status cards. Filtering out
Status cards would not reconstruct the persistent deck: generated Attack cards,
temporary upgrades, and unavailable card data would still remain problems.
Pile-list presence does not establish complete coverage.
Evidence can carry into immediate reward decisions in the same reported room.
Opening a card reward or skipping it preserves that limited evidence. Other
accepted non-combat actions discard it because they can change cards or leave the
room. Rejected requests do not establish such a change.
A room change, missing room coordinates, identity failure, new reported run ID,
or observed run exit discards evidence. The policy receives `unknown` when no
scoped evidence remains. This reduces available synergy context intentionally;
it is safer than presenting stale combat piles as the current persistent deck.
An exact persistent-deck source is still unavailable. This pass labels that
limitation; it does not implement a complete deck tracker or a save-file parser.
## Minimal trace additions
Decision rows now include reported `run_id`, `run_seed`, and available
`deck_provenance`. The provenance describes available macro-policy evidence;
it does not mean every handler used it. Combat policy uses the combat observation directly.
Identity changes and identity transport failures have separate trace events.
Full observation/action linkage, unique capture names, session identity, and
session finalization remain recording work.
## Validation
- 279 existing-script assertions passed: 68 facts, 110 policy, and 101 runner.
This includes two upgrade-selection assertions added concurrently.
- Runner integration flows cover same-seed run replacement, single-run stopping,
metadata outages, temporary cards, provenance, mutation invalidation, room
boundaries, unknown rooms, and restart/cache isolation.
- Eighteen temporary whole-process scenarios passed with real HTTP clients and
loopback fixtures. Five exercise the new identity/provenance behavior.
- The offline audit replayed 346 stored observations without policy exceptions.
- Dataset integrity passed for 37 runs, 1,052 decisions, and 346 observations.
- Python compilation and shell syntax checks passed.
No live game actions, paid model calls, dependencies, vendor edits, or dataset
rewrites were required. Temporary probes were not added to the repository.

View file

@ -107,7 +107,7 @@ Combat and selection policy have since been extracted without changing execution
behavior. See [the current policy structure](../POLICY.md). No plugin framework
or class hierarchy was added.
The next state work is run identity and deck provenance. Recording should then
link observations, proposals, action attempts, results, and reconciliation.
[Run identity and card-evidence provenance](../RUN_STATE.md) have since been added.
Recording should next link observations, proposals, action attempts, results, and reconciliation.
Session finalization, capture-name collisions, and actual policy-gate metadata
remain separate work.