← Blog · Pattern Library · SimHub

Sim Cartridges: Portable State for Stateless Systems

kody-w · March 2025 · Pattern Data

In 1977, Atari shipped the VCS with a radical idea: the game doesn't live in the console. It lives in a cartridge you plug in. Eject it. Carry it. Plug it into a different console. Same game.

Your production system needs the same thing.

The State Problem

Modern systems are obsessed with being "stateless." APIs are stateless. Functions are stateless. Containers are stateless. This is great for scaling but terrible for continuity.

Where does the state go? Into databases, caches, message queues — distributed across infrastructure that's expensive, fragile, and hard to reason about. Want to move your system to a new machine? Good luck migrating all that state. Want to compare two runs? You'd need to diff two databases.

A sim cartridge inverts this. The state is a file. A single, flat, self-describing JSON file that contains everything needed to resume the system on any machine, in any environment, at any time.

What Goes in the Cartridge

{
  "_format": "mars-barn-cartridge",     // Self-identifying
  "version": 1,                         // Schema version
  "id": "MBC-LZ4K2-A1B2",             // Unique run ID
  "created": "2025-07-09T14:30:00Z",

  // 1. CONFIGURATION (what was chosen)
  "config": {
    "arch": "engineer",                 // Policy selection
    "lispy": "adaptive_governor",       // Which program
    "lispyCode": "(begin ...)",         // The actual code
    "simSpeed": 2
  },

  // 2. FULL STATE (the organism right now)
  "state": {
    "sol": 147,
    "alive": true,
    "o2": 42.3,
    "crew": [{ "name": "Chen", "hp": 72, "alive": true }, ...],
    "modules": ["greenhouse_dome", "solar_farm"],
    "alloc": { "h": 0.25, "i": 0.4, "g": 0.35 }
  },

  // 3. MEMORY (trajectory + decisions)
  "echoHistory": [ /* last 100 echo frames */ ],
  "echoInertia": { "o2_velocity": -0.3, ... },
  "taskHistory": [ /* every decision */ ],
  "reflexHistory": [ /* reflex fire log */ ],

  // 4. SCORING (how good is this run)
  "score": {
    "total": 15200,
    "grade": "B",
    "breakdown": [["Survival", 8000], ["Crew", 2000], ...]
  }
}

The Three Properties

1. Self-Describing

The _format and version fields mean any system can look at the file and know what it is. No external schema registry. No API to query. The cartridge tells you what it is when you read it.

2. Complete

The cartridge contains everything needed to resume. Not a reference to state — the actual state. Not a pointer to the echo history — the actual history. You can take this file to a plane with no internet and resume the simulation.

3. Diffable

Because it's JSON, you can diff two cartridges. "What changed between save A and save B?" is just diff a.json b.json. You can version cartridges in git. You can build a timeline of how a system evolved by comparing sequential saves.

Three Tiers of Persistence

The cartridge pattern naturally creates a three-tier persistence hierarchy:

TIER 1: Auto-Save (localStorage)
  ├── Automatic, every N ticks
  ├── Trimmed (last 30 echoes, not 100)
  ├── Recovery prompt on page load
  └── Volatile — browser clears, it's gone

TIER 2: Manual Export (.cartridge.json file)
  ├── Explicit user action (Ctrl+S or button)
  ├── Full fidelity (100 echoes, full history)
  ├── Portable — email it, Dropbox it, git it
  └── Permanent — it's a file on their disk

TIER 3: Leaderboard Upload (SimHub)
  ├── Public submission with colony name
  ├── Validated against frame hashes
  ├── Ranked by score
  └── Competitive — everyone can see it

Each tier is backed by the same serialization function. The only difference is where the bytes go and how aggressively they're trimmed.

Anti-Cheat by Design

When combined with the Public Frame Ledger, cartridges become tamper-evident. The cartridge contains the frame hashes it consumed. The leaderboard can verify those hashes against the public ledger.

// Verify a cartridge against the public frame ledger
function validateCartridge(cartridge, frameLedger) {
  // Check that the frames this cartridge claims to have consumed
  // actually match the public ledger
  for (const echo of cartridge.echoHistory) {
    const publicFrame = frameLedger[echo.frame];
    if (publicFrame && echo.frameHash !== publicFrame._hash) {
      return { valid: false, reason: 'Frame hash mismatch at sol ' + echo.frame };
    }
  }
  return { valid: true };
}

You can't modify the environmental conditions your colony faced because those conditions are publicly hashed. Your score is your management, not your luck.

Beyond the Sim

WHERE CARTRIDGES TRANSFER

AI Agents: Serialize agent state + memory + tool call history. Transfer an agent from one machine to another mid-conversation. Compare two agents' decision logs side by side.

DevOps: Snapshot an entire deployment's state — not just the config, but the runtime state, metrics history, and incident response log. Replay it in staging.

Game Design: Save states that include not just player position but the game world's dynamic state, NPC memories, and narrative branches taken.

Scientific Computing: Checkpoint a simulation with full state + parameter history. Resume on a different cluster. Compare runs by diffing cartridges.

The Datasette Connection

Simon Willison's Datasette has the same philosophy: the data file IS the application. A SQLite database is a complete, portable, self-contained unit of data that you can deploy anywhere.

A sim cartridge is the same idea applied to runtime state. The JSON file IS the running system, frozen at a point in time. Deploy it to any compatible runtime and it resumes.

The cartridge is not a save file. It's a portable organism. Eject it, carry it, plug it back in. The organism wakes up exactly where it was.

Implementation in 30 Lines

function exportCartridge() {
  const cartridge = {
    _format: 'my-system-cartridge',
    version: 1,
    id: generateId(),
    created: new Date().toISOString(),
    config: { ...myConfig },
    state: JSON.parse(JSON.stringify(myState)),  // deep clone
    history: myHistory.slice(-100),               // bounded
    score: computeScore()
  };
  
  const blob = new Blob([JSON.stringify(cartridge, null, 2)], 
    { type: 'application/json' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = `run-${cartridge.id}.cartridge.json`;
  a.click();
  URL.revokeObjectURL(url);
}

function importCartridge(file) {
  const reader = new FileReader();
  reader.onload = (e) => {
    const cartridge = JSON.parse(e.target.result);
    if (cartridge._format !== 'my-system-cartridge') throw new Error('Wrong format');
    myState = cartridge.state;
    myConfig = cartridge.config;
    myHistory = cartridge.history;
    resume();
  };
  reader.readAsText(file);
}

The Sim Cartridge is Pattern 05 in the Rappter Pattern Library.


State is not something you store. It's something you carry.