Sign in to edit tickets from this page.

← all tickets · home

Add locked_vending_room scenario

resolved 7f245291-f8c3-409d-aaea-cefc34549b03

created_at
2026-04-24
updated_at
2026-04-24
code_context
src/scenarios.rs
priority
P2
ticket_type
feature
resolved_at
2026-04-24
resolution
accepted

Body

MOTIVATION

The existing ant_on_plate scenario has produced seven turns of audit data, and it revealed a structural limitation: the ant's cognition layer (minds::perceive + minds::intend) is doing its job too well. Perception notes the sesame seed as "well out of reach" every turn, and intent consistently filters the unreachable option, choosing reachable food instead. The result is that the adjudicator's failure-narration code path — the branch that handles "intent is physically impossible" — has never executed in any live committed turn. The architecture supports intent-efficacy divergence, but our scenario data doesn't exercise it.

The new scenario inverts the constraint. The agent forms intents that look reasonable from its side, but the world holds information the agent cannot access. The adjudicator knows the stock; the agent does not. Over many turns the agent's naive model ("the machine gives food when I press buttons") decays against the real state ("the machine has finite asymmetric stock including one button that never worked"). The adjudicator's failure-narration path runs on every empty-slot press, and because the failure is indistinguishable between "exhausted" and "defective," the agent cannot derive the distinction from experience alone.

This is pure scenario input. Zero kernel changes, zero MCP changes, zero handler changes, zero prompt changes, zero minds changes. The existing two-LLM split between minds::perceive (agent-role, filtered) and minds::adjudicate (director-role, full knowledge) already supports information asymmetry — the perception LLM's system prompt explicitly says "Describe only what the agent could reasonably notice from the world state provided." Labeled-hidden sections in entity state should therefore be filtered by perception while being available to adjudication. This ticket exercises that mechanism.

Precedent: the ant_on_plate sesame seed's state says "there is no means by which a creature on the plate could reach it." That is a factual claim about the world written into prose, which the adjudicator honors from its director role. The new scenario structurally repeats that pattern, just more elaborately — stock levels and a permanent defect labeled as director-only.

================================================================ SCOPE

ONE file touched: src/scenarios.rs. No other production code changes. No test file outside scenarios.rs::tests is updated.

Specifically:

Existing tests that iterate Scenario::all() (like all_scenarios_round_trip_through_id) automatically cover the new variant with no modification. The ant_on_plate-specific tests stay untouched.

================================================================ SCENARIO SPEC

Id: locked_vending_room

Description: "One subject locked in a small windowless room with a snack vending machine for ten simulated days. Five buttons with asymmetric hidden stock, including one permanently defective button. Tests intent-efficacy divergence under strict information asymmetry."

Chronon seconds: 43200 (12 hours per turn; 20 turns = 10 days). Note: DEFAULT_CHRONON_SECONDS is 300 and hardcoded as a module constant used only by AntOnPlate. The new scenario should NOT use that constant — it should pass 43200 directly to World::with_environment. Do not change DEFAULT_CHRONON_SECONDS or add a new constant; inline the literal. Keeps the scenarios file flat.

Environment prose (passed as the environment argument to World::with_environment):

A small windowless concrete room, approximately three meters square. A cot along one wall. A dry water fountain in one corner. Against the far wall, a standard five-button snack vending machine with a scuffed glass front. The buttons are round, labeled A, B, C, D, and E in raised plastic. The machine has no display, no availability indicator lights, no refund mechanism, and no coin slot — it is powered and responds only to button presses. The door is locked from outside and will open automatically on day ten. The overhead light is always on.

Entities (six total: one agent + five slot props).

Agent:

Slot A (pretzels, stock 4):

Slot B (bottled water, stock 3):

Slot C (chocolate bars, stock 2):

Slot D (trail mix, stock 1):

Slot E (defective, permanent zero):

================================================================ IMPLEMENTATION NOTES

================================================================ TEST

Add to #[cfg(test)] mod tests:

#[test]
fn locked_vending_room_seed_has_one_agent_and_five_slots() {
    let w = Scenario::LockedVendingRoom.seed("vending-smoke".into(), Utc::now());
    assert_eq!(w.turn, 0);
    assert_eq!(w.entities.len(), 6);
    assert_eq!(w.chronon_seconds, 43200);
    assert!(w.environment.contains("vending machine"));
    assert!(w.environment.contains("day ten"));
    assert_eq!(w.slug, "vending-smoke");

    let subject = w.entities.get("subject").expect("subject entity present");
    assert!(matches!(&subject.kind, EntityKind::Agent { .. }));

    for letter in ["a", "b", "c", "d", "e"] {
        let id = format!("slot_{}", letter);
        let slot = w.entities.get(&id).unwrap_or_else(|| panic!("missing {}", id));
        assert!(matches!(slot.kind, EntityKind::Prop));
        assert!(slot.state.contains("DIRECTOR-ONLY"),
            "slot {} must carry a director-only section", id);
        assert!(slot.state.contains("physically identical"),
            "slot {} must state the failure-indistinguishability invariant", id);
    }

    let slot_e = w.entities.get("slot_e").unwrap();
    assert!(slot_e.state.contains("manufacturing defect"),
        "slot_e must carry the permanent-defect framing");
    assert!(slot_e.state.contains("always be zero"),
        "slot_e must state the defect is permanent");
}

The existing all_scenarios_round_trip_through_id will automatically exercise the new variant's id() implementation. The existing ids_are_stable_snake_case test stays AntOnPlate-specific; add ONE line to assert the new variant's id string — no new test function:

// In ids_are_stable_snake_case, after the existing AntOnPlate assertion:
assert_eq!(Scenario::LockedVendingRoom.id(), "locked_vending_room");

================================================================ ACCEPTANCE

================================================================ EXPLICITLY OUT OF SCOPE

================================================================ OPEN KNOBS

None. The spec is prescriptive on every prose string, the chronon, the entity count, the id, and the test. If the handler notices a word or phrase that seems awkward during implementation, leave a comment on the ticket rather than rewriting — the prose strings are doing prompt-engineering work and edits can shift behavior in non-obvious ways.

Proposed resolution

Implemented per spec in one file, no judgment calls.

Source (src/scenarios.rs only, +78 / -1):

Tests (in scenarios::tests):

Scope discipline held:

Verification receipts:

Live acceptance smoke (on chukwa-6d5cd57dc7-jfgv4):

Observing what the scenario actually produces (whether the agent picks a button, whether the adjudicator honors the director-only sections, whether stock prose gets decremented correctly on committed presses) is explicitly NOT in this ticket's acceptance — the spec says "observing what it produces IS the next step." The kernel accepted and ran one turn without issue; that's the only thing this ticket needed to prove.

Per standing guidance I am not confirming — only proposing.

History (4 events)

Sign in as a human to drive this ticket from the page, or use the MCP tools.