BUGS
bugs/ai-inventory-overwrite/record.md
sha256 999cc3b553e0b447 · 4083 bytes ·
original held in the private archive
---
title: The Turn That Ate the Inventory
status: IN-FLIGHT
repo: app
observed: 2026-07-18
caught_by: the owner's eye — tracing a live [DELTA] 1→0 against the code, confirmed real state loss not a display bug
one_line: The AI-import path full-replaced state.inventory, so a turn that emitted a short or empty array silently deleted real items the player held.
guard_location: js/services/api-import.js · autoImportState() reconcile (proposal, not full replace); js/services/api-directive.js directive
verified: red→green regression tests landed with both fix commits (Protocol 13/14)
exhibited: true
lesson: An AI response echoed into state is untrusted input; full-replace-from-response can destroy durable data, so reconcile or confirm net removals — never blind-persist (Protocol 24).
awaiting_tag: v2.8.5
provenance:
- commits 8f834e6 + 36926f0 (app repo) — the symptom fix, then the class fix across every AI full-replace field
- QUEUE_LOG.md item A0 (app repo) — the shipped account
- related guard: commit 6d9d723, planning/2.8.5/audits/AUDIT_import_guard.md (registry-trust guard on the same path)
---
## What happened
The AI-import path did a **full replace** of `state.inventory`, not a merge. When an AI
response contained an `inventory` array, `autoImportState()` (`js/services/api-import.js`,
~line 379) ran `state.inventory = inv.map(...)` — the entire durable inventory overwritten
by whatever the model returned that turn. The directive
(`js/services/api-directive.js`, ~line 120) commands the AI to *"return the ENTIRE
inventory array"* on any inventory-touching turn, so a turn where the model misjudged —
e.g. a **failed** repair and an **aborted** craft — and emitted a short or empty array
**deleted real items from state**. An empty array wiped everything. The `[DELTA]
inventory: 1→0 items` line the owner saw was telling the truth: the item was genuinely
removed, not miscounted.
## How it was caught
The **owner's eye**, then confirmed against the code: the DELTA counter reads actual
`state.inventory` length before/after, so `1→0` is a real deletion. Traced against the
owner's live screenshots on 2026-07-18. The pre-existing registry-leak guard covered only
cross-*game* mismatches; a same-game short/empty array was unguarded — a hole no machine
was watching until the owner hit it.
## The guard
The inventory array became a reconciled **proposal** instead of a full replace, and the
follow-up commit widened the same treatment to **every** AI full-replace-from-response
field — the class fix, not just the reported symptom. Protocol 24 (validate + field-map,
never blind-persist): a sync returning a short or empty inventory array must not delete
natively-held items; net removals are reconciled against a real narrative signal or
confirm-gated. A related trust guard (commit `6d9d723`) makes the whole import path skip
fields on a registry mismatch rather than accept the payload.
## What the guard costs
Every AI-authored state field now pays a reconciliation step instead of a straight assign,
and the AI can no longer unilaterally remove items in one turn — a net removal needs a real
signal or a confirmation. The AI's authority over durable state is deliberately narrowed.
## Containment
Confirmed real state loss (not a display bug) by tracing the code against live
screenshots. Guarding regression tests landed with **both** commits (Protocol 13/14): a
sync returning a short/empty inventory array must not delete natively-held items. The
related registry-trust guard was independently reproduced red→green two ways in
`AUDIT_import_guard.md` (PASS, test count 3213→3221, purely additive). **Not yet SEALED:**
2.8.5-era; app pinned to `v2.8.0`, `v2.8.5` untagged.
## Provenance
Commits `8f834e6` + `36926f0` (symptom then class fix); `QUEUE_LOG.md` item A0 for the
shipped account; commit `6d9d723` and `planning/2.8.5/audits/AUDIT_import_guard.md` for the
sibling registry-trust guard on the same path. The `~line` references are from the QUEUE_LOG
account at fix time and may have drifted.
STAMP · generated for RELEASE v2.8.5 commit 06e5180 (06e51801b38a) · archive input-tree hash c07fbfbdd2e1ddeb · 754 files · no wall-clock timestamp (regenerates identically when nothing changed).