Status: fixed
Decision (2026-08-02, triage with maintainer): Approved. Ticket 43's "same
key in both mcts2 trees" call is superseded — it was made on the incorrect
premise that no per-observer keying machinery existed. observed_action_key
(rust/netrunner-core/src/ai/mcts2.rs:40) already exists, already redacts
InstallCard/DrawCard/DiscardCard per observer, and its documented
contract is to mirror server/serialize.py's log redaction. The six
per-target keys from ticket 43 currently fall through it unredacted, letting
the blind observer's tree condition on an opponent's unobservable card
choice.
Keep the full per-target action_key values in the acting player's tree.
Extend observed_action_key with arms for the blind observer, derived
directly from the game's own redaction rules (serialize.py:30–77) and log
lines:
| Action | Blind observer | Observed key |
|---|---|---|
| SprintReturn | Runner | sprint_return |
| LongevityTrash | Runner | longevity_trash |
| HanseiTrash | Runner | hansei_trash |
| AnoeticTrash | Runner | anoetic_trash |
| CarnivoreTrash | Corp | carnivore_trash |
| ArchivesRecur { source: "archives_fd" } | Runner | archives_recur:archives_fd |
| ArchivesRecur { source: "archives_fu" } | Runner | archives_recur:archives_fu:{card_title} |
Design points settled at triage:
public: this codebase's own redaction rule (serialize.py:68–71)
deliberately anonymizes Carnivore's grip trashes, and observed_action_key
mirrors that rule, not paper rules. If the redaction rule is ever revisited,
that is a game-view ticket, and the observer key follows it.
source: the Runner observes which pile thecard left; only the identity is hidden.
(faceup Archives is a public zone; the log names the title,
engine.rs:2959) so aggregating would discard real information — but the
index is neither observed nor stable (each pick shifts the remaining
indices). Duplicate titles correctly share an edge: identical copies are
indistinguishable and strategically equivalent. legal.rs:204 populates
card_title for every faceup option, so the data is reliable.
actor is not the root searcher, its own tree's index keys name different
determinised cards per iteration (e.g. Corp sprint_return:{i} edges in a
Runner-root search). Redaction cannot fix this — it is inherent to
opponent modeling in single-determinisation-per-iteration IS-MCTS, exactly
as install_card:{code} edges already alias across determinised hands.
Document it; do not restructure the opponent model.
key has two halves — the actor's tree gets the full key; the observer's
tree gets what the redacted log reveals. Any future per-target key must
decide both.* Also fix ticket 43's overclaimed doc line on action_key:
own-zone indices are fixed across determinisations *when the zone owner is
the root searcher*; in the opponent tree they are approximate, like all
hidden-zone actions.
observed_action_key implements the table above; the acting player's treebehavior is unchanged.
own_zone_target_keys_are_identical_in_both_mcts2_trees test
(ai/mcts.rs:1110) is replaced with actor-vs-observer assertions: actor
gets distinct keys, blind observer gets the aggregated key, covering at
least AnoeticTrash, CarnivoreTrash, ArchivesRecur faceup (title key)
and facedown (source key).
determinisation claim.
cargo fmt, cargo clippy --workspace -- -D warnings, and
cargo test --workspace pass from rust/.
2026-08-02: Filed from review of ticket 43's implementation commit
c33d6b9. The affected test currently asserts both MCTS2 trees receive the
same full key, which encodes the private-information leak rather than the
MO-ISMCTS observation contract. Marked needs-triage because public versus
private Archives recursion and the exact observer-key design need a
maintainer decision before implementation.
2026-08-02: Triaged with the maintainer (grilling session). Approved with the observed-key table above; faceup recursion keyed by title for the observer; Carnivore follows the codebase's redaction rule (type-only for Corp); actor-tree determinisation aliasing accepted as inherent and documented; two-halves key policy recorded so ticket 45's sweep inherits it (comment appended there). Status flipped to ready-for-agent.
2026-08-02: Implemented. observed_action_key
(rust/netrunner-core/src/ai/mcts2.rs) gained the seven arms from the
table: SprintReturn / LongevityTrash / HanseiTrash / AnoeticTrash
collapse to their type-only key for a Runner observer, CarnivoreTrash
likewise for a Corp observer, and ArchivesRecur keys as
archives_recur:archives_fd (facedown, source kept) or
archives_recur:archives_fu:{card_title} (faceup, title kept, index
dropped). The acting player's tree is untouched — a Corp observer still
gets the full action_key for the HQ choices, as does a Runner observer
for Carnivore. The mcts2 module docs now state the two-halves policy
(every per-target key must decide the actor half *and* the observer half),
and action_key's doc in ai/mcts.rs no longer claims own-zone indices
are stable unconditionally — they are stable only when the zone owner is
the root searcher, with the opponent-tree aliasing called out as inherent
to single-determinisation-per-iteration opponent modeling. The leaky
own_zone_target_keys_are_identical_in_both_mcts2_trees test is replaced
by six tests covering actor-side preservation, Runner aggregation of the
four HQ choices, Corp aggregation of Carnivore, faceup title keying
(including two indices of the same title sharing an edge), facedown
source-only keying, and the untouched Done* companions. cargo fmt and
cargo clippy --workspace -- -D warnings clean; cargo test --workspace
149 passed (144 lib + 5 integration, up from 144 total); pytest 134
passed.
2026-08-02: Superseded in one detail by ticket 45's triage: the maintainer
decided the Carnivore log anonymization is cosmetic (the runner heap is
serialized ungated with full identity, serialize.py:343) and dropped it.
Per this ticket's own provision ("if the redaction rule is ever revisited …
the observer key follows it"), CarnivoreTrash becomes a public-outcome
code key (carnivore_trash:{card_code}) in both trees and the
(Corp, CarnivoreTrash) observer arm shipped here is removed by ticket 45.
Everything else in this ticket stands.