Sign in to edit tickets from this page.

← all tickets · home

Model adjustments to support the read-only web UI

resolved 8894dd80-582c-404b-bf86-a7032120d88c

created_at
2026-04-23
updated_at
2026-04-24
code_context
src/mcp.rs, src/kernel.rs, src/persistence.rs
priority
P2
ticket_type
chore
resolved_at
2026-04-24
resolution
accepted

Body

MOTIVATION

The read-only web UI at /w/:slug/... (session page, turn page, entity page) sits atop the MCP data surface. After an investigation of the current handler shapes, three small model-layer adjustments would let the view layer build each page in one MCP call instead of two or three, without scanning or reconciling data in the view.

This parent ticket is the bundle. The actual work lives in three child tickets that can proceed in PARALLEL. There is no must-wait-for edge between them (no depends_on) — only the shared parent pointing here, so progress is visible in one place.

Once all three children are resolved, this parent can be accepted with cascade=false (nothing to cascade; the children are already terminal).

================================================================ THE THREE GAPS

Gap 1 — list_turns rows don't carry event counts or touched entities

Today each row in handle_list_turns gives turn_num, turn_ref, simulation_time, and entity_count (count of world entities, not events). The session page wants:

turn 3 · 2026-04-23T… · 4 events · touched: ant

To get event counts today, the view would call get_events once per turn (N+1) or scan the audit log in the view layer. Both are wasteful when the kernel already logged the data at turn_complete time.

Fix: each row gains events_emitted: u32 and entities_touched: Vec<String>. Both computed server-side from the audit log, filtered to the committed attempt for that turn.

Gap 2 — get_turn doesn't return that turn's events

The turn page's point is the event stream — perception → intent → adjudication, with adjudication_rejected inline if retries fired. Today handle_get_turn returns world state only; the view would also call get_events(from_turn=N, to_turn=N, include_failed=true).

Fix: get_turn gains optional include_events: bool arg. When true, response gains events: [...] containing every audit event whose turn field matches, ordered by _seq. Default false to preserve existing callers; change is strictly additive.

Gap 3 — adjudication events don't expose per-entity state transitions

The entity page wants a clean timeline of state changes for a specific entity. Today the view would have to call entity_history, filter to intent_adjudicated events, then reconstruct each state change by loading the turn payload and diffing. That's a lot of reconstruction for information the kernel had in hand at mutation time.

Fix: intent_adjudicated and adjudication_rejected (no — rejected events don't mutate; only the accepted adjudication) gain an entity_transitions: [{entity_id, state_before, state_after}] field alongside the existing entities_touched. Captured in apply_adjudication before overwriting, threaded through PendingAuditEvent::Adjudication, serialized in log_adjudication. Pure addition. No field renamed, no behavior changed.

================================================================ FOUR THINGS THAT LOOK LIKE GAPS BUT AREN'T

Listed so the handlers working the children don't overreach:

  1. get_world is already complete. slug, simulation_time, turn, environment, full entity HashMap. The session page reads this as-is. No restructure needed.

  2. Entity shape is already legible. id, name, state, kind. Agents carry goal + memory inline on EntityKind::Agent. View walks the HashMap and renders. No flattening, no new view-model struct.

  3. Turn ordering is fine. Zero-padded filenames (turn_000042) sort correctly; list_turns returns them in that order.

  4. Pagination is fine. list_turns has since, from_turn, to_turn, limit, desc. More than the view needs.

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

================================================================ PARALLELIZATION MAP

Child A (Gap 1): src/mcp.rs::handle_list_turns. No kernel change, no event shape change.

Child B (Gap 2): src/mcp.rs::handle_get_turn. No kernel change, no event shape change.

Child C (Gap 3): src/kernel.rs::apply_adjudication, src/kernel.rs::PendingAuditEvent::Adjudication, src/persistence.rs::log_adjudication. No MCP-layer change.

Coupling:

No depends_on edges between children. Only parent = this ticket.

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

This parent is acceptable when all three children are resolved. Cascade not needed at acceptance time — children close themselves.

A post-hoc sanity check the caller will run:

Proposed resolution

All three model-adjustment children are implemented, tested, merged, and live in production. This parent bundle can close once the caller accepts each child.

Children summary:

Cross-cutting receipts:

Mid-stream correction worth flagging (already fixed before merge): Gap 3's initial implementation captured state_before at mutation time instead of turn-start, which was wrong when agent_state_after preceded an entity_mutations entry for the same entity. The new live-router test caught it; fix was a pre-apply snapshot HashMap read in apply_adjudication. This is documented in the Gap 3 proposed_resolution.

Parallelization note: the caller's parent ticket specified these as independent, no depends_on edges. The handler (me) ran three subagents concurrently in separate git worktrees (feat/list-turns-rollup, feat/get-turn-events, feat/entity-transitions) off a shared base. A fourth unrelated subagent (ticket-UI polish) ran simultaneously in the main tree. One orthogonal issue surfaced: the base commit df9d8ce (slug refactor) didn't compile in isolation because a stray 2-arg issue_token call site in server.rs was committed without the matching oauth.rs signature change, which was sitting uncommitted in my working tree. Landed that fix as its own commit 6da7649 feat(oauth): track consumer + redirect_uri on persisted access tokens so the base buildable and the three branches could verify. That commit is orthogonal to this parent but is noted here for the paper trail.

Production smoke confirms all three Gaps working together:

Per standing guidance I am not confirming this ticket — only proposing. The three children are also in proposed_resolution; accepting them will leave this parent as the last item in your caller_queue. No cascade-on-accept needed since the children close independently.

Follow-up unlock: the read-only /w/:slug/... web UI effort the parent was gating is now unblocked — all three data-shape changes the UI needed are live.

History (3 events)

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