Cartridge Lifecycle
How the nOSh runtime detects, mounts, registers, hot-swaps, and unmounts a cartridge. Single source for the complete lifecycle — referenced by orchestration.md (mission flow), deck-state.md (which fields get touched on each event), and cipher-voice.md (grammar merge on insertion).
adr/ADR-0019— the SD-card-via-USB-MSC physical model that drives the lifecycle events.adr/ADR-0017— Pico 2 coprocessor; per ADR-0019, no longer touches the cart bus (event source isudevon the Pi).adr/ADR-0006—.kn86container format the runtime parses on insertion.software/api-reference/grammars/coprocessor-protocol.md— UART frames the runtime emits to the Pico on session boundaries.
Lifecycle states
Section titled “Lifecycle states”A cartridge moves through five states:
ABSENT → MOUNTED → REGISTERED → ACTIVE → UNMOUNTING → ABSENT| State | Trigger | Runtime action |
|---|---|---|
| ABSENT | No cart present in the slot | Mission board generates contracts from cartridge_history alone (no cart-specific templates available). |
| MOUNTED | udev add event from the SD-via-USB-MSC bridge | Filesystem mounted; .kn86 header read; runtime validates format, versions, section bounds, and declared capability grants. |
| REGISTERED | Header valid + capability grants parsed | Fe cart source parsed and tree-walked (no bytecode); (cart-init) runs; cart-scoped handlers and runtime contributions are registered. |
| ACTIVE | First mission contract using this cart’s capability begins | Cart program code (KEC Lisp, ADR-0001) handles runtime events through the host FFI surface; cart save file is opened for read/write. |
| UNMOUNTING | udev remove event OR Hot Swap initiated | Phase chain serialized to deck-state SRAM (then flash); cart save file flushed and closed; grammar overlay rolled back; phase handlers unregistered; coprocessor session closed (if any). |
Transitions are runtime-driven; cartridges never observe state changes directly — they see only their own handler invocations.
Insertion (ABSENT → MOUNTED → REGISTERED)
Section titled “Insertion (ABSENT → MOUNTED → REGISTERED)”- Operator inserts the SD-card cartridge sled into the slot. The push-push socket engages the SD card’s contacts. (Per ADR-0019: the card-detect switch fires the lifecycle event.)
- The internal USB hub’s SD bridge IC enumerates the card as a USB mass-storage device.
- The Pi’s kernel emits a
udevadd event. The nOSh runtime subscribes to this event. - The runtime mounts the filesystem read-only at a known mount point (e.g.,
/mnt/cart). - The runtime opens the
.kn86container per ADR-0006. Header validation:- Magic bytes (
KN86) - Format version (
2) - Required NoshAPI and Fe VM versions
- Code (Lisp source) and static-data section bounds
- Optional CRC-32 checksum
CART_CAPABILITIESstatic-data block, if present
- Magic bytes (
- If the header is invalid, the runtime surfaces an in-fiction error on the bare-deck terminal (
> CARTRIDGE FORMAT NOT RECOGNIZED) and remains in MOUNTED state until the cart is removed. - If the header is valid, registration proceeds:
- The runtime opens a per-cart Fe arena and binds the host-specific FFI surface.
- The code section is parsed and tree-walked. It holds raw
.lspsource text; AOT bytecode is a deferred future (see ADR-0004 / ADR-0006 2026-06-14 amendments), not a v0.1 artifact. (cart-init)is invoked when defined. Lisp carts use it to register handlers, seed state, draw initial UI, and install grammar/input contributions.- Capability grants from
CART_CAPABILITIESare recorded for later FFI gates. - Mission board and CIPHER-LINE contributions refresh from the registered cart scope.
The runtime never auto-launches a contract on insertion — the operator chooses what to do next from the mission board.
Activation (REGISTERED → ACTIVE)
Section titled “Activation (REGISTERED → ACTIVE)”A cart becomes ACTIVE the first time a mission contract requiring its capability begins:
- Operator selects a contract from the mission board.
- Runtime resolves the first phase’s required capability against the loaded carts.
- If the required cart is REGISTERED, runtime calls its phase handler with the deserialized phase chain context.
- Cart program code runs in a per-cart Fe VM context. In
kn86emu, that context is bound to the full NoshAPI/SystemState surface. Inkn86-nosh-tb, it is bound to the constrained cell API and semantic input callbacks. - Cart’s per-save file at
/save/<cart_id>.savon the cart’s SD is opened for read/write. - Cart contributes events to the Cipher event ring via
cipher-push-event(ADR-0015) when the host surface exposes that primitive.
Multiple carts can be REGISTERED simultaneously (rare in practice — the slot holds one cart at a time, but a recently-removed cart’s templates can stay parsed transiently). Only one cart is ACTIVE at any moment.
Hot Swap (ACTIVE → UNMOUNTING → MOUNTED → REGISTERED → ACTIVE)
Section titled “Hot Swap (ACTIVE → UNMOUNTING → MOUNTED → REGISTERED → ACTIVE)”Multi-phase missions that span capabilities require physical cartridge swaps. The runtime formalizes this as Hot Swap — a tactical pause where the operator exchanges modules to pick up new capabilities. Hot Swap mechanics are detailed in software/cartridges/modules/ice-breaker.md where they serve as a core decision framework.
Phase chain protocol
Section titled “Phase chain protocol”Phase 1: ICE BREAKER (NETWORK INTRUSION) ↓ phase completes → runtime serializes intermediate state to phase_chain ↓ runtime displays: > PHASE 2 REQUIRES: FORENSIC ACCOUNTING ↓ > INSERT: BLACK LEDGER ↓ operator physically swaps cartridgePhase 2: BLACK LEDGER (FORENSIC ACCOUNTING) ↓ runtime reads phase_chain, passes context to cartridge handler ↓ phase completes → runtime updates phase_chain ↓ runtime displays: > CONTRACT COMPLETE ↓ > PAYOUT: 4,800 ¤ ↓ > REPUTATION: +12Debrief: Cipher voice summaryWhat happens during a swap
Section titled “What happens during a swap”- The active phase completes. The cart’s phase handler returns control to the runtime.
- The runtime writes the phase chain to deck state (SRAM). Includes: phase index, intermediate results (data extracted, evidence gathered, contacts mapped), accumulated threat modifiers, and partial payout accrued.
- The runtime displays the swap prompt with the next required capability.
- The operator physically removes the current cartridge.
udevremove event fires; runtime executes the UNMOUNTING sequence (see below) and returns to ABSENT. - The operator inserts the next cartridge. Runtime executes the insertion sequence; new cart reaches REGISTERED.
- Runtime verifies the new cart declares and is granted the required capability. If not, surfaces
> CARTRIDGE DOES NOT PROVIDE: <CAPABILITY> / RE-INSERT: <expected_module>. - Runtime calls the new cart’s phase handler with the deserialized phase chain context.
- Phase begins. Operator works in the new capability domain with full context from the previous phase.
Swap timeout
Section titled “Swap timeout”If the operator does not insert a valid cartridge within 5 minutes, the runtime offers two options:
- Suspend — phase chain preserved in flash; suspended missions can be resumed on next boot.
- Abandon — reputation penalty, partial payout for completed phases.
Diegetic swap surface
Section titled “Diegetic swap surface”The physical act of swapping a cartridge is a diegetic event, not a limitation to apologize for. The swap prompt is in-fiction:
> NETWORK TRACE RECOVERED> FINANCIAL RECORDS IDENTIFIED IN EXTRACTED DATA> ANALYSIS REQUIRES FORENSIC ACCOUNTING CAPABILITY> INSERT MODULE: BLACK LEDGER> PHASE CHAIN ACTIVE — DO NOT POWER DOWNThe display flickers briefly when the cartridge is removed (2–3 frames of noise, matching the original device’s behavior). The boot sequence for the new cartridge is abbreviated — no full splash screen, just: > MODULE LOADED: BLACK LEDGER / PHASE 2 OF 3 / RESUMING...
Removal (ACTIVE → UNMOUNTING → ABSENT)
Section titled “Removal (ACTIVE → UNMOUNTING → ABSENT)”Whether triggered by Hot Swap or by the operator simply pulling the cart, removal follows the same sequence:
udevremove event fires.- Runtime serializes any in-flight phase chain state to the deck state (SRAM, then flash).
- Cart’s per-save file (
/save/<cart_id>.savon the cart’s SD) is flushed and closed. Save format continues to be ADR-0006 tagged static data; the storage backend is filesystem read/write per ADR-0019, replacing ADR-0013’s MBC5-SRAM model. - The cart’s grammar overlay is rolled back from the CIPHER-LINE grammar tables.
- Phase handlers are unregistered from the dispatch table.
- Coprocessor session is closed (if the cart held one — see
coprocessor-protocol.mdsession-control frames). - Filesystem is unmounted.
- Runtime returns to ABSENT.
cartridge_history is never cleared — once a cart has been registered on this deck, the bit stays set for the deck’s lifetime.
Per-cart save data
Section titled “Per-cart save data”Per ADR-0019, save state lives as a file on the cart’s own SD card filesystem:
- Path:
/save/<cart_id>.savon the cart’s mounted volume - Format: ADR-0006 tagged static data (unchanged at the FFI surface from ADR-0013)
- Lifecycle: opened for read/write at ACTIVE; flushed and closed at UNMOUNTING
- Cross-cart fields (handle, credits, reputation) live on the device’s microSD per Universal Deck State, not on the cart
The cart_save / cart_load NoshAPI primitives are unchanged at the FFI surface; only the underlying storage moves from MBC5 SRAM bank-switching to filesystem read/write.
Edge cases
Section titled “Edge cases”- Cart removed mid-phase without a Hot Swap prompt. Runtime treats this as an unsafe removal; phase chain still serializes, but Cipher voice surfaces an
:anomalousevent (“the deck remembers the removal”). On re-insert, runtime offers Resume from saved phase chain. - Cart with corrupted save file. Runtime falls back to a fresh save; original is renamed to
<cart_id>.sav.corruptand a Cipher event fires. - Cart whose
.kn86header references a missing capability. Header rejected at registration; cart stays in MOUNTED state. Bare deck terminal shows the error. - Multiple inserts/removes in rapid succession.
udevevent coalescing in the runtime: each MOUNTED → UNMOUNTING transition must complete before the next event is processed. - Power-off during ACTIVE. Low-voltage interrupt drives the same UNMOUNTING sequence with a tightened budget. Phase chain preservation takes priority over save flushing.
Emulator Hot Swap simulation (GWP-319)
Section titled “Emulator Hot Swap simulation (GWP-319)”The desktop emulator has no physical cartridge slot. To exercise the same lifecycle on a host keyboard, kn86-emulator/src/hot_swap.c simulates eject and insert events that drive cartridge_fsm.c directly.
Keyboard chords
Section titled “Keyboard chords”| Chord | Action |
|---|---|
Ctrl+E | Eject the currently loaded cartridge (calls hot_swap_eject). FSM walks to ABSENT, phase chain serializes if active. |
Ctrl+I | Insert the cartridge configured via --swap-cart (calls hot_swap_insert(NULL)). FSM walks ABSENT → MOUNTED → REGISTERED. |
The chords are intercepted in main.c before host-key forwarding, so the underlying E / I keys don’t double-fire as KN86_KEY_NIL.
CLI flag
Section titled “CLI flag”kn86emu --cart cart-a.kn86 --swap-cart cart-b.kn86Boots with cart-a.kn86 mounted; pressing Ctrl+E then Ctrl+I swaps to cart-b.kn86. The combined hot_swap_swap() API is also available for tests.
Faithfulness to the device
Section titled “Faithfulness to the device”hot_swap.c drives cart_fsm_on_cart_inserted / cart_fsm_on_cart_ejected directly rather than going through the bus (the bus rejects re-entrant publish, which would drop the FSM’s secondary notifications). Downstream consumers — CIPHER eject countdown, mission board, save subsystem, future provenance wiring — subscribe to CART_FSM_STATE_CHANGED, CART_FSM_RESUME_HINT, CART_FSM_DIFFERENT_CART, CART_FSM_SWAP_OFFER, and CART_FSM_UNSAFE_PULL, so the same transition events fire whether the source is a udev event on the prototype or Ctrl+E on the emulator. This module is emulator-only; on the device, a udev subscriber will publish the same FSM dispatches.
See kn86-emulator/src/hot_swap.h for the contract and kn86-emulator/tests/test_hot_swap.c for the FSM-faithful coverage.