{
  "openapi": "3.1.0",
  "info": {
    "title": "Simcraft execution API",
    "version": "0.1.0",
    "summary": "Reference execution API for batch runs and interactive sessions.",
    "description": "The HTTP + WebSocket API served by `simcraft-server` and implemented by the private `crates/simcraft-server` package. POST an OSDL document to start a batch run or interactive session. Poll its status and stream the typed event stream over WebSocket. The IDE's run panel and `@simcraft/client` use this API against local and remote servers. A hosted execution service is expected to expose the same surface.\n\nDocuments and events follow the published schemas: model documents per [`spec/schemas/osdl.schema.json`](spec/schemas/osdl.schema.json), event envelopes per [`spec/schemas/osdl.events.schema.json`](spec/schemas/osdl.events.schema.json). Results follow the results JSON contract in [`docs/contracts.md`](docs/contracts.md), shared byte-for-byte with the CLI and the WASM engine. The interactive session lifecycle is documented in [`docs/interactive-session-protocol.md`](docs/interactive-session-protocol.md).\n\nThe reference server enables permissive CORS and has no authentication. It is built for local and trusted-network use. Runs and sessions are held in memory and are not persisted across restarts.",
    "license": {
      "name": "Apache-2.0",
      "identifier": "Apache-2.0"
    }
  },
  "servers": [
    {
      "url": "/api",
      "description": "Same-origin hosted endpoint"
    },
    {
      "url": "http://localhost:8420",
      "description": "Local `simcraft-server` (default port 8420)"
    }
  ],
  "paths": {
    "/v0/health": {
      "get": {
        "operationId": "health",
        "summary": "Liveness and version",
        "description": "Always returns `ok: true` with the server version. Use it to discover whether a Simcraft server is listening before offering remote execution.",
        "responses": {
          "200": {
            "description": "The server is up.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthResponse"
                },
                "example": {
                  "ok": true,
                  "version": "0.1.0"
                }
              }
            }
          }
        }
      }
    },
    "/v0/libraries": {
      "get": {
        "operationId": "libraries",
        "summary": "Registered component library definitions",
        "description": "The library definitions (per `spec/schemas/osdl.library.schema.json`) registered in this server's engine, verbatim. A plugin distribution (see the Plugin API section of [`docs/contracts.md`](docs/contracts.md)) reports its additional libraries here, which is how tooling such as the IDE palette discovers plugin components at runtime.",
        "responses": {
          "200": {
            "description": "A JSON array of library documents.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "description": "One OSDL library document: `{\"osdl\": \"0.1\", \"library\": {...}}`."
                  }
                },
                "example": [
                  {
                    "osdl": "0.1",
                    "library": {
                      "namespace": "des",
                      "name": "Discrete Event Components",
                      "version": "0.1.0",
                      "components": [
                        "\u2026"
                      ]
                    }
                  }
                ]
              }
            }
          }
        }
      }
    },
    "/v0/validate": {
      "post": {
        "operationId": "validateDocument",
        "summary": "Validate a document without running it",
        "description": "Validates one OSDL document for schema structure, semantic diagnostics, and buildability against the registered libraries. It never accepts execution options or starts a run. Returns `200` with the diagnostics array. An empty array means the document is valid.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ValidateRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Validation diagnostics (empty when valid).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Diagnostic"
                  }
                }
              }
            }
          },
          "422": {
            "description": "The request shape is invalid or contains fields other than `document`."
          }
        }
      }
    },
    "/v0/runs": {
      "post": {
        "operationId": "createRun",
        "summary": "Start a run",
        "description": "Prepares the posted run before accepting it. Preparation validates the requested limits against server maximums, validates the OSDL document and selected experiment, applies parameter overrides, and builds the execution plan. A `201` response means the worker was created and the run is retained. Poll `GET /v0/runs/{id}` for completion and subscribe to `GET /v0/runs/{id}/events` for the live event stream.\n\nThe server has 128 run slots. Preparing requests, runs available through the status and event endpoints, and still-running workers deleted from lookup occupy these slots. Completed and failed runs keep their slot until deletion or expiry. A later `POST` removes completed or failed runs that have been retained for more than 3,600 seconds. Capacity does not evict a non-expired run.\n\nDeterminism: identical `(document, experiment, parameters, seed, replication)` produce byte-identical results, the same as a local CLI run. `trace` does not enter that tuple: it selects what the run streams, never what it computes.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RunRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The run was accepted and its worker was created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RunCreated"
                },
                "example": {
                  "runId": "r1",
                  "trace": "off"
                }
              }
            }
          },
          "400": {
            "description": "Preparation failed validation. Causes include a requested limit above the server maximum, a zero limit, an invalid document, an unavailable component library, an invalid experiment or parameter override, or an execution plan that exceeds a requested resource limit. No worker is created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RunValidationErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "The request shape is invalid or contains an unknown field."
          },
          "429": {
            "description": "All 128 run slots are occupied. Retry after deleting a retained run, after an active worker exits following deletion, or when a completed or failed run is old enough for the retry to expire it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": "run limit reached"
                }
              }
            }
          },
          "500": {
            "description": "The preparation task could not complete, the worker thread could not be created, or the run identifier counter is exhausted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "preparationFailed": {
                    "value": {
                      "error": "run preparation failed"
                    }
                  },
                  "identifierExhausted": {
                    "value": {
                      "error": "run identifier exhausted"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v0/runs/{id}": {
      "get": {
        "operationId": "getRun",
        "summary": "Poll run status and results",
        "description": "Returns the run's current state. Results appear in the shared results JSON shape after successful completion. Execution failures and caught worker panics produce `status: error`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/RunId"
          }
        ],
        "responses": {
          "200": {
            "description": "The current state of the run.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RunState"
                }
              }
            }
          },
          "404": {
            "description": "No run with this id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": "no such run"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteRun",
        "summary": "Delete a run",
        "description": "Removes the run from status and event lookup immediately. Deleting an active run does not cancel its worker. The worker keeps its run slot until it exits, and an event stream connected before deletion can continue until the worker exits. Deleting a completed or failed run releases its slot.",
        "parameters": [
          {
            "$ref": "#/components/parameters/RunId"
          }
        ],
        "responses": {
          "204": {
            "description": "The run was deleted."
          },
          "404": {
            "description": "No run with this id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": "no such run"
                }
              }
            }
          }
        }
      }
    },
    "/v0/runs/{id}/events": {
      "get": {
        "operationId": "streamRunEvents",
        "summary": "Stream the typed event stream (WebSocket)",
        "description": "WebSocket upgrade. What the socket carries is fixed when the run is created. A run with `trace` `off` or `collect` streams `sim.started`, `sim.progress`, `sim.completed`, `sim.error`, `sim.aborted`, and `param.changed` only, at most 100 progress frames per replication, and its replay buffer holds a few thousand frames. A run with `trace` `live` streams every envelope it produces. There is no per-subscriber selection: the run decides, so every subscriber reads the same stream.\n\nOn connect the server replays the buffered envelopes, then streams live. The replay buffer is bounded to the newest 50,000 frames. A subscriber that connects late to an event-heavy run receives a trace that starts mid-run. Event `seq` values restart at `1` for each replication. A complete trace starts with `run.scenario` 0, `run.replication` 1, and `seq` 1. Any other first-envelope values identify a dropped prefix. Subscribers connected from the start miss nothing. Each envelope is one text frame of JSON per [`spec/schemas/osdl.events.schema.json`](spec/schemas/osdl.events.schema.json). `run.experiment`, `run.replication`, and `run.scenario` are set on every event.\n\nNormal execution finishes with a terminal `sim.completed`, `sim.error`, or `sim.aborted` frame. Multi-replication runs contain interior `sim.completed` frames, and the socket stays open across them. A caught worker panic records `status: error` but bypasses normal terminal-frame emission. The server then closes the socket with code `1000` and reason `run completed`.\n\nA subscriber that falls behind the live broadcast is resynchronized from the replay buffer: the server resends only the frames that subscriber has not received, so its trace carries no gap and no duplicate. A subscriber that falls behind by more than the buffer retains cannot be resynchronized, and the server closes the socket with code `1008` and reason `trace cut: replay buffer no longer covers this subscriber`. Deleting the run does not close the sockets already streaming it; they run on until the run completes.",
        "parameters": [
          {
            "$ref": "#/components/parameters/RunId"
          }
        ],
        "responses": {
          "101": {
            "description": "Switching protocols: the connection upgrades to a WebSocket carrying `EventEnvelope` text frames."
          },
          "404": {
            "description": "No run with this id (plain-text body).",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                },
                "example": "no such run"
              }
            }
          }
        }
      }
    },
    "/v0/sessions": {
      "post": {
        "operationId": "createSession",
        "summary": "Start an interactive session",
        "description": "Prepares one selected scenario and replication, creates its worker, and waits for the initial paused snapshot. A successful session starts at simulation time 0 with speed 1.0 on root branch `b1`. The default server retains at most 8 sessions and 32 branches per session through its `maxBranches` policy. Completed and errored sessions remain retained for 3,600 seconds unless deleted.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SessionRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The worker published its initial paused snapshot.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionCreated"
                },
                "example": {
                  "sessionId": "s1"
                }
              }
            }
          },
          "400": {
            "description": "Preparation, validation, or trajectory selection failed.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/RunValidationErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    }
                  ]
                }
              }
            }
          },
          "403": {
            "description": "Interactive sessions are disabled by host policy.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": "interactive sessions disabled"
                }
              }
            }
          },
          "422": {
            "description": "The request shape is invalid or contains an unknown field."
          },
          "429": {
            "description": "All interactive session slots are occupied.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": "session limit reached"
                }
              }
            }
          },
          "500": {
            "description": "The preparation task or worker could not start, a component panicked while the engine initialized, or the session identifier counter is exhausted. A panic is reported as `session worker panicked: <message>`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v0/sessions/{id}": {
      "get": {
        "operationId": "getSession",
        "summary": "Get an interactive session snapshot",
        "parameters": [
          {
            "$ref": "#/components/parameters/SessionId"
          }
        ],
        "responses": {
          "200": {
            "description": "The current session snapshot.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionSnapshot"
                }
              }
            }
          },
          "404": {
            "description": "No session with this id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": "no such session"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteSession",
        "summary": "Delete an interactive session",
        "description": "Removes the session from lookup and disconnects its worker.",
        "parameters": [
          {
            "$ref": "#/components/parameters/SessionId"
          }
        ],
        "responses": {
          "204": {
            "description": "The session was deleted."
          },
          "404": {
            "description": "No session with this id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": "no such session"
                }
              }
            }
          }
        }
      }
    },
    "/v0/sessions/{id}/commands": {
      "post": {
        "operationId": "commandSession",
        "summary": "Control an interactive session",
        "description": "Applies one tagged command and returns the immediate post-command snapshot. `runTo`, `runToEnd`, `seek`, and `switchBranch` targets that require replay return promptly with state `playing`; the worker advances in bounded bursts. A rebuilt seek to time zero returns state `paused`. Branch-switch replay finishes to the target tip. During that replay `pause` is a no-op, `setSpeed`, `switchBranch`, and `stop` apply, and every other command returns the unchanged snapshot with a transient `error` field. Observe asynchronous target arrival, completion, or failure through `session.status` frames or `GET /v0/sessions/{id}`. `seek` and `switchBranch` can rebuild a completed or errored session. Other commands return its final snapshot unchanged. Parameter-name, parameter-value, branch-id, and branch-limit validation failures return status 200 with the unchanged snapshot and a transient `error` field.",
        "parameters": [
          {
            "$ref": "#/components/parameters/SessionId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SessionCommand"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The immediate post-command session snapshot.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionSnapshot"
                }
              }
            }
          },
          "404": {
            "description": "No session with this id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": "no such session"
                }
              }
            }
          },
          "409": {
            "description": "The session worker is unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": "session worker unavailable"
                }
              }
            }
          },
          "422": {
            "description": "The command shape is invalid or contains an unknown field."
          }
        }
      }
    },
    "/v0/sessions/{id}/events": {
      "get": {
        "operationId": "streamSessionEvents",
        "summary": "Stream interactive session events (WebSocket)",
        "description": "WebSocket upgrade. The server replays buffered simulation envelopes, then streams live simulation envelopes and `session.status` frames. Status frames are emitted on state, generation, or active-branch transitions and at a 100 ms cadence while state is `playing`, including turbo advancement. Status frames bypass the replay buffer, so late subscribers read `GET /v0/sessions/{id}` for the current snapshot. The buffer retains the newest 50,000 simulation event frames. Event `seq` restarts at 1 after a replay generation change. A live parameter edit keeps the generation and emits `param.changed` in the current trace. The socket stays open across completed and error states because `seek` and `switchBranch` can revive the session. It ends when the client disconnects or delivery fails, and closes with code 1000 and reason `stream closed` when the session is deleted or expires. A subscriber that falls behind the live broadcast is resynchronized from the replay buffer: the server resends only the event frames that subscriber has not received, so its trace carries no gap and no duplicate, but it does miss the status frames sent while it was behind. A subscriber that falls behind by more than the buffer retains cannot be resynchronized, and the server closes the socket with code `1008` and reason `trace cut: replay buffer no longer covers this subscriber`. Clearing the buffer, which `seek` backward and `switchBranch` do, cuts a subscriber that was still behind in the previous generation.",
        "parameters": [
          {
            "$ref": "#/components/parameters/SessionId"
          }
        ],
        "responses": {
          "101": {
            "description": "Switching protocols: the connection carries `EventEnvelope` and `SessionStatusFrame` text frames."
          },
          "404": {
            "description": "No session with this id (plain-text body).",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                },
                "example": "no such session"
              }
            }
          }
        }
      }
    },
    "/v0/runs/{id}/scenarios/{scenario}/replications/{replication}/events": {
      "get": {
        "summary": "Fetch one replication's collected trace",
        "operationId": "getReplicationEvents",
        "description": "Returns the envelopes a single replication recorded during a `collect` run, as NDJSON. The first line is a header object carrying `runId`, `scenario`, `replication`, `cap`, `envelopes`, `discarded`, `truncated`, `from` and `count`; every line after it is one envelope. `envelopes` is the total the replication recorded and `count` is how many this response carries, so a client renders its position as `from + 1` to `from + count` of `envelopes`. Truncation is reported before the payload it describes. One replication of the packaged M/M/1 example records about 80,000 envelopes and 17 MB, so a reader pages with `from` and `limit` rather than fetching a whole replication. Available once the run has finished. A run started `off` or `live` recorded nothing and answers 409.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "scenario",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 0
            },
            "description": "Sweep scenario index, 0 when the experiment declares no sweep."
          },
          {
            "name": "replication",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 1
            },
            "description": "Replication number, counting from 1."
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            },
            "description": "Index of the first envelope to return, counting from 0. A `from` past the end returns an empty window whose header reports `from` as the total, so a client paging forward discovers the end without an error."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 5000
            },
            "description": "Envelopes to return. Absent returns every envelope after `from`, which is what the endpoint served before windows existed. A value above 5000 is clamped rather than refused, and the header reports how much was returned."
          }
        ],
        "responses": {
          "200": {
            "description": "The replication's trace as NDJSON, header line first.",
            "content": {
              "application/x-ndjson": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "No such run, scenario or replication."
          },
          "409": {
            "description": "The run did not collect a trace, or has not finished."
          },
          "410": {
            "description": "The collected trace was displaced by a newer collecting run."
          }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "RunId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Run identifier returned by `createRun`. Opaque; do not parse.",
        "schema": {
          "type": "string"
        }
      },
      "SessionId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Session identifier returned by `createSession`. Opaque; do not parse.",
        "schema": {
          "type": "string"
        }
      }
    },
    "schemas": {
      "HealthResponse": {
        "type": "object",
        "required": [
          "ok",
          "version"
        ],
        "additionalProperties": false,
        "properties": {
          "ok": {
            "const": true
          },
          "version": {
            "type": "string",
            "description": "Server version (the `simcraft-server` crate version)."
          }
        }
      },
      "RunRequest": {
        "type": "object",
        "required": [
          "document"
        ],
        "additionalProperties": false,
        "properties": {
          "document": {
            "$ref": "#/components/schemas/OsdlDocument"
          },
          "experiment": {
            "type": "string",
            "description": "Name of the experiment to run. Defaults to the first experiment in the document."
          },
          "parameters": {
            "type": "object",
            "additionalProperties": {
              "type": [
                "number",
                "integer",
                "boolean",
                "string"
              ]
            },
            "description": "Model parameter overrides by name. Applied after the experiment's fixed `parameters` and after sweep values, the same precedence as the CLI's `--param`."
          },
          "seed": {
            "type": "integer",
            "minimum": 0,
            "description": "RNG seed override (unsigned 64-bit). When neither this nor the experiment provides a seed, `0` is used."
          },
          "emit_state_events": {
            "type": "boolean",
            "description": "Include `state.changed` envelopes for state-store writes. A run with `trace` `off` streams none of them, and a `collect` run records them instead of streaming them: `state.changed` is not a lifecycle event."
          },
          "emitStateEvents": {
            "type": "boolean",
            "description": "Camel-case alias for `emit_state_events`."
          },
          "trace": {
            "type": "string",
            "enum": [
              "off",
              "live",
              "collect"
            ],
            "default": "off",
            "description": "What the run does with the envelopes it mints. `off`, the default, mints run lifecycle envelopes only (`sim.started`, `sim.progress`, `sim.completed`, `sim.error`, `sim.aborted`, `param.changed`), which is what a batch run needs for a progress bar and a result, and it lets replications run in parallel. `live` mints every envelope and streams it in run order as the run produces it, which is what a timeline can be rebuilt from live; ordering the stream funnels every replication through one channel, so a `live` run does not run its replications in parallel. `collect` mints every envelope, streams the lifecycle ones, and records the rest per replication for a client to fetch once the run finishes, which keeps the parallel speed and the full trace at the cost of holding the trace in memory. The setting is fixed at creation and applies to every subscriber."
          },
          "limits": {
            "$ref": "#/components/schemas/RunLimits",
            "description": "Optional per-run maximums. Each supplied value must be at least 1 and no greater than the matching server maximum. Omitted fields inherit the server maximum."
          },
          "trace_cap": {
            "type": "integer",
            "minimum": 1,
            "maximum": 1000000,
            "default": 100000,
            "description": "Envelopes each replication records in `collect` mode. A replication that mints more keeps its oldest `trace_cap` envelopes and reports the rest as `discarded`. Ignored in `off` and `live`."
          },
          "traceCap": {
            "type": "integer",
            "minimum": 1,
            "maximum": 1000000,
            "default": 100000,
            "description": "Camel-case alias for `trace_cap`."
          }
        }
      },
      "SessionRequest": {
        "type": "object",
        "required": [
          "document"
        ],
        "additionalProperties": false,
        "properties": {
          "document": {
            "$ref": "#/components/schemas/OsdlDocument"
          },
          "experiment": {
            "type": "string",
            "description": "Experiment name. Defaults to the first experiment in the document."
          },
          "parameters": {
            "type": "object",
            "additionalProperties": {
              "type": [
                "number",
                "integer",
                "boolean",
                "string"
              ]
            },
            "default": {},
            "description": "Model parameter overrides by name."
          },
          "seed": {
            "type": "integer",
            "minimum": 0,
            "description": "Root RNG seed override. The experiment seed is used when omitted, then 0."
          },
          "scenario": {
            "type": "integer",
            "minimum": 0,
            "default": 0,
            "description": "Zero-based index in the prepared sweep."
          },
          "replication": {
            "type": "integer",
            "minimum": 0,
            "maximum": 4294967295,
            "default": 1,
            "description": "One-based replication selection. Zero is rejected during preparation."
          },
          "emitStateEvents": {
            "type": "boolean",
            "default": false,
            "description": "Include `state.changed` envelopes for state-store writes."
          },
          "limits": {
            "$ref": "#/components/schemas/RunLimits",
            "description": "Optional per-session maximums. Omitted fields inherit the server maximum."
          }
        }
      },
      "SessionCreated": {
        "type": "object",
        "required": [
          "sessionId"
        ],
        "additionalProperties": false,
        "properties": {
          "sessionId": {
            "type": "string",
            "description": "Opaque session identifier for status, commands, deletion, and events."
          }
        }
      },
      "SessionSnapshot": {
        "type": "object",
        "required": [
          "state",
          "generation",
          "time",
          "duration",
          "speed",
          "events",
          "branchId",
          "branches"
        ],
        "additionalProperties": false,
        "properties": {
          "state": {
            "type": "string",
            "enum": [
              "paused",
              "playing",
              "completed",
              "error"
            ]
          },
          "generation": {
            "type": "integer",
            "minimum": 1,
            "description": "Replay generation. Starts at 1 and increases when seek rebuilds the run or any `switchBranch` command rebuilds its target."
          },
          "time": {
            "type": "number",
            "description": "Playhead position. During playback this is the paced virtual clock and can sit between event times. Seek and runTo park it at their target when the trajectory remains live."
          },
          "duration": {
            "type": "number",
            "description": "Selected experiment duration."
          },
          "speed": {
            "type": "number",
            "description": "Simulation-time units advanced per real second during paced playback."
          },
          "events": {
            "type": "integer",
            "minimum": 0,
            "description": "Advisory dispatched-event counter derived from the trajectory event sequence for the current generation. It resets when the generation increases. Each dispatched event increments it, including simultaneous events dispatched by one time-granularity step."
          },
          "branchId": {
            "type": "string",
            "description": "Identifier of the active branch."
          },
          "branches": {
            "type": "array",
            "description": "Branch tree over the shared prepared document, scenario, replication, and seed.",
            "items": {
              "$ref": "#/components/schemas/BranchSummary"
            }
          },
          "results": {
            "$ref": "#/components/schemas/Results",
            "description": "Single-trajectory results. Present after successful completion."
          },
          "error": {
            "type": "string",
            "description": "Execution error message when state is `error`. A command reply can contain a transient validation error without changing the stored snapshot."
          }
        }
      },
      "BranchSummary": {
        "type": "object",
        "required": [
          "id",
          "parentId",
          "forkTime",
          "forkSeq",
          "edits",
          "tipTime"
        ],
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^b[1-9][0-9]*$",
            "description": "Session-local branch identifier."
          },
          "parentId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Parent branch identifier. The root branch uses null."
          },
          "forkTime": {
            "type": "number",
            "description": "Playhead position where this branch forked."
          },
          "forkSeq": {
            "type": "integer",
            "minimum": 0,
            "description": "Envelope sequence boundary where this branch forked."
          },
          "edits": {
            "type": "integer",
            "minimum": 0,
            "maximum": 4294967295,
            "description": "Number of entries in the branch's cumulative parameter edit log."
          },
          "tipTime": {
            "type": "number",
            "description": "Latest published playhead position for this branch."
          }
        }
      },
      "SessionCommand": {
        "description": "One interactive control command, tagged by `command`.",
        "oneOf": [
          {
            "type": "object",
            "required": [
              "command"
            ],
            "additionalProperties": false,
            "properties": {
              "command": {
                "const": "pause"
              }
            }
          },
          {
            "type": "object",
            "required": [
              "command"
            ],
            "additionalProperties": false,
            "properties": {
              "command": {
                "const": "resume"
              }
            }
          },
          {
            "type": "object",
            "required": [
              "command"
            ],
            "additionalProperties": false,
            "properties": {
              "command": {
                "const": "step"
              },
              "granularity": {
                "type": "string",
                "enum": [
                  "time",
                  "event"
                ],
                "default": "time"
              },
              "count": {
                "type": "integer",
                "minimum": 0,
                "maximum": 4294967295,
                "default": 1,
                "description": "Number of step calls. Zero is treated as one."
              }
            }
          },
          {
            "type": "object",
            "required": [
              "command",
              "time"
            ],
            "additionalProperties": false,
            "properties": {
              "command": {
                "const": "runTo"
              },
              "time": {
                "type": "number",
                "description": "Inclusive simulation-time target."
              }
            }
          },
          {
            "type": "object",
            "required": [
              "command",
              "time"
            ],
            "additionalProperties": false,
            "properties": {
              "command": {
                "const": "seek"
              },
              "time": {
                "type": "number",
                "description": "Target clamped to the session duration. Forward targets use turbo. Backward targets and seeks on completed or errored sessions rebuild the run in a new generation."
              }
            }
          },
          {
            "type": "object",
            "required": [
              "command"
            ],
            "additionalProperties": false,
            "properties": {
              "command": {
                "const": "runToEnd"
              }
            }
          },
          {
            "type": "object",
            "required": [
              "command",
              "multiplier"
            ],
            "additionalProperties": false,
            "properties": {
              "command": {
                "const": "setSpeed"
              },
              "multiplier": {
                "type": "number",
                "description": "Positive finite values replace the current speed. Other values leave it unchanged."
              }
            }
          },
          {
            "type": "object",
            "required": [
              "command",
              "name",
              "value"
            ],
            "additionalProperties": false,
            "properties": {
              "command": {
                "const": "setParameter"
              },
              "name": {
                "type": "string",
                "description": "Declared model parameter name."
              },
              "value": {
                "oneOf": [
                  {
                    "type": "number"
                  },
                  {
                    "type": "boolean"
                  }
                ],
                "description": "Numeric or boolean override value. Booleans resolve numerically as 1 or 0."
              }
            },
            "description": "Pauses first, creates and activates a child branch, and applies the override at the current envelope sequence. The generation does not change. The engine emits `param.changed`. Unknown names, invalid values, and the 32-branch default `maxBranches` limit return the unchanged snapshot with a transient error."
          },
          {
            "type": "object",
            "required": [
              "command",
              "branchId"
            ],
            "additionalProperties": false,
            "properties": {
              "command": {
                "const": "switchBranch"
              },
              "branchId": {
                "type": "string",
                "description": "Existing session-local branch identifier."
              }
            },
            "description": "Rebuilds the target branch in a new generation, schedules its cumulative edit log, and replays by envelope sequence to its recorded tip. Replay uses bounded bursts. Until replay reaches the tip, `pause` is a no-op and commands that would continue from the replay position are refused with a transient `error` field."
          },
          {
            "type": "object",
            "required": [
              "command"
            ],
            "additionalProperties": false,
            "properties": {
              "command": {
                "const": "stop"
              }
            }
          }
        ]
      },
      "SessionStatusFrame": {
        "type": "object",
        "description": "Current snapshot sent on state, generation, or active-branch transitions and at a 100 ms cadence while state is `playing`, including turbo advancement. Status frames are not retained in the replay buffer.",
        "required": [
          "type",
          "payload"
        ],
        "additionalProperties": false,
        "properties": {
          "type": {
            "const": "session.status"
          },
          "payload": {
            "$ref": "#/components/schemas/SessionSnapshot"
          }
        }
      },
      "RunLimits": {
        "type": "object",
        "description": "Requested per-run resource limits. Omitted fields inherit the matching server maximum. The documented defaults and maximums describe the default server configuration.",
        "additionalProperties": false,
        "properties": {
          "componentsMax": {
            "type": "integer",
            "minimum": 1,
            "maximum": 4096,
            "default": 4096,
            "description": "Maximum built components."
          },
          "connectionsMax": {
            "type": "integer",
            "minimum": 1,
            "maximum": 16384,
            "default": 16384,
            "description": "Maximum model connections."
          },
          "roundsMax": {
            "type": "integer",
            "minimum": 1,
            "maximum": 1024,
            "default": 1024,
            "description": "Maximum built resolution rounds."
          },
          "roundParticipantsMax": {
            "type": "integer",
            "minimum": 1,
            "maximum": 16384,
            "default": 16384,
            "description": "Maximum participants across built resolution rounds."
          },
          "scenariosMax": {
            "type": "integer",
            "minimum": 1,
            "maximum": 4096,
            "default": 4096,
            "description": "Maximum sweep scenarios."
          },
          "replicationsMax": {
            "type": "integer",
            "minimum": 1,
            "maximum": 1024,
            "default": 1024,
            "description": "Maximum replications per scenario."
          },
          "runsMax": {
            "type": "integer",
            "minimum": 1,
            "maximum": 16384,
            "default": 16384,
            "description": "Maximum scenario-replication runs."
          },
          "calendarEntriesMax": {
            "type": "integer",
            "minimum": 1,
            "maximum": 1000000,
            "default": 1000000,
            "description": "Maximum queued calendar entries per replication."
          },
          "dispatchesMax": {
            "type": "integer",
            "minimum": 1,
            "maximum": 10000000,
            "default": 10000000,
            "description": "Maximum calendar dispatches per replication."
          },
          "dispatchesSameTimeMax": {
            "type": "integer",
            "minimum": 1,
            "maximum": 1000000,
            "default": 1000000,
            "description": "Maximum consecutive dispatches at one simulation time."
          },
          "entitiesMax": {
            "type": "integer",
            "minimum": 1,
            "maximum": 10000000,
            "default": 10000000,
            "description": "Maximum created entities per replication."
          },
          "outputPointsMax": {
            "type": "integer",
            "minimum": 1,
            "maximum": 10000000,
            "default": 10000000,
            "description": "Maximum recorded scalar output points per replication."
          }
        }
      },
      "ValidateRequest": {
        "type": "object",
        "required": [
          "document"
        ],
        "additionalProperties": false,
        "properties": {
          "document": {
            "$ref": "#/components/schemas/OsdlDocument"
          }
        }
      },
      "OsdlDocument": {
        "type": "object",
        "description": "A complete OSDL model document, valid against the model schema [`spec/schemas/osdl.schema.json`](spec/schemas/osdl.schema.json) (`https://osdl.dev/schemas/0.1/osdl.schema.json`). Every component type used must be available to the engine."
      },
      "TraceCap": {
        "type": "integer",
        "minimum": 1,
        "maximum": 1000000,
        "description": "Envelopes each replication records in `collect` mode. Present only for a collecting run."
      },
      "TraceRetention": {
        "type": "string",
        "enum": [
          "pending",
          "retained",
          "dropped"
        ],
        "description": "Retention state of the collected traces. Present only for a collecting run."
      },
      "RunCreated": {
        "type": "object",
        "required": [
          "runId",
          "trace"
        ],
        "additionalProperties": false,
        "properties": {
          "runId": {
            "type": "string",
            "description": "Opaque run identifier for the status and events endpoints."
          },
          "trace": {
            "type": "string",
            "enum": [
              "off",
              "live",
              "collect"
            ],
            "default": "off",
            "description": "What the run does with the envelopes it mints. `off`, the default, mints run lifecycle envelopes only (`sim.started`, `sim.progress`, `sim.completed`, `sim.error`, `sim.aborted`, `param.changed`), which is what a batch run needs for a progress bar and a result, and it lets replications run in parallel. `live` mints every envelope and streams it in run order as the run produces it, which is what a timeline can be rebuilt from live; ordering the stream funnels every replication through one channel, so a `live` run does not run its replications in parallel. `collect` mints every envelope, streams the lifecycle ones, and records the rest per replication for a client to fetch once the run finishes, which keeps the parallel speed and the full trace at the cost of holding the trace in memory. The setting is fixed at creation and applies to every subscriber."
          },
          "traceCap": {
            "$ref": "#/components/schemas/TraceCap"
          }
        }
      },
      "RunState": {
        "description": "Current state of a run, discriminated by `status`. Every variant carries `trace`. A collecting run also carries `traceCap` and `traces`. A completed collecting run carries `traceCut` when its cap truncated at least one replication.",
        "oneOf": [
          {
            "type": "object",
            "required": [
              "status",
              "trace"
            ],
            "additionalProperties": false,
            "properties": {
              "status": {
                "const": "running"
              },
              "trace": {
                "type": "string",
                "enum": [
                  "off",
                  "live",
                  "collect"
                ],
                "default": "off",
                "description": "What the run does with the envelopes it mints. `off`, the default, mints run lifecycle envelopes only (`sim.started`, `sim.progress`, `sim.completed`, `sim.error`, `sim.aborted`, `param.changed`), which is what a batch run needs for a progress bar and a result, and it lets replications run in parallel. `live` mints every envelope and streams it in run order as the run produces it, which is what a timeline can be rebuilt from live; ordering the stream funnels every replication through one channel, so a `live` run does not run its replications in parallel. `collect` mints every envelope, streams the lifecycle ones, and records the rest per replication for a client to fetch once the run finishes, which keeps the parallel speed and the full trace at the cost of holding the trace in memory. The setting is fixed at creation and applies to every subscriber."
              },
              "traceCap": {
                "$ref": "#/components/schemas/TraceCap"
              },
              "traces": {
                "$ref": "#/components/schemas/TraceRetention"
              },
              "traceCut": {
                "type": "string",
                "description": "Summary of the first replication whose collected trace exceeded traceCap. Present after a truncated collecting run completes."
              }
            }
          },
          {
            "type": "object",
            "required": [
              "status",
              "results",
              "trace"
            ],
            "additionalProperties": false,
            "properties": {
              "status": {
                "const": "completed"
              },
              "results": {
                "$ref": "#/components/schemas/Results"
              },
              "trace": {
                "type": "string",
                "enum": [
                  "off",
                  "live",
                  "collect"
                ],
                "default": "off",
                "description": "What the run does with the envelopes it mints. `off`, the default, mints run lifecycle envelopes only (`sim.started`, `sim.progress`, `sim.completed`, `sim.error`, `sim.aborted`, `param.changed`), which is what a batch run needs for a progress bar and a result, and it lets replications run in parallel. `live` mints every envelope and streams it in run order as the run produces it, which is what a timeline can be rebuilt from live; ordering the stream funnels every replication through one channel, so a `live` run does not run its replications in parallel. `collect` mints every envelope, streams the lifecycle ones, and records the rest per replication for a client to fetch once the run finishes, which keeps the parallel speed and the full trace at the cost of holding the trace in memory. The setting is fixed at creation and applies to every subscriber."
              },
              "traceCap": {
                "$ref": "#/components/schemas/TraceCap"
              },
              "traces": {
                "$ref": "#/components/schemas/TraceRetention"
              },
              "traceCut": {
                "type": "string",
                "description": "Summary of the first replication whose collected trace exceeded traceCap. Present after a truncated collecting run completes."
              }
            }
          },
          {
            "type": "object",
            "required": [
              "status",
              "error",
              "trace"
            ],
            "additionalProperties": false,
            "properties": {
              "status": {
                "const": "error"
              },
              "error": {
                "type": "string",
                "description": "Execution error message. A caught worker panic starts with `run worker panicked:`."
              },
              "trace": {
                "type": "string",
                "enum": [
                  "off",
                  "live",
                  "collect"
                ],
                "default": "off",
                "description": "What the run does with the envelopes it mints. `off`, the default, mints run lifecycle envelopes only (`sim.started`, `sim.progress`, `sim.completed`, `sim.error`, `sim.aborted`, `param.changed`), which is what a batch run needs for a progress bar and a result, and it lets replications run in parallel. `live` mints every envelope and streams it in run order as the run produces it, which is what a timeline can be rebuilt from live; ordering the stream funnels every replication through one channel, so a `live` run does not run its replications in parallel. `collect` mints every envelope, streams the lifecycle ones, and records the rest per replication for a client to fetch once the run finishes, which keeps the parallel speed and the full trace at the cost of holding the trace in memory. The setting is fixed at creation and applies to every subscriber."
              },
              "traceCap": {
                "$ref": "#/components/schemas/TraceCap"
              },
              "traces": {
                "$ref": "#/components/schemas/TraceRetention"
              },
              "traceCut": {
                "type": "string",
                "description": "Summary of the first replication whose collected trace exceeded traceCap. Present after a truncated collecting run completes."
              }
            }
          }
        ]
      },
      "Results": {
        "type": "object",
        "description": "The shared results JSON, identical to the stdout of `simcraft run`/`sweep` and the return of the WASM engine's `run` (see [`docs/contracts.md`](docs/contracts.md)).",
        "required": [
          "osdl",
          "model",
          "experiment",
          "scenarios"
        ],
        "properties": {
          "osdl": {
            "const": "0.1"
          },
          "model": {
            "type": "string",
            "description": "Model name from the document."
          },
          "experiment": {
            "type": "string",
            "description": "Name of the experiment that ran."
          },
          "scenarios": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Scenario"
            },
            "description": "One per sweep cross-product point; a single element with empty `parameters` when the experiment has no sweep."
          }
        }
      },
      "Scenario": {
        "type": "object",
        "required": [
          "index",
          "parameters",
          "replications"
        ],
        "properties": {
          "index": {
            "type": "integer",
            "minimum": 0,
            "description": "Position in the sweep cross product."
          },
          "parameters": {
            "type": "object",
            "additionalProperties": {
              "type": [
                "number",
                "integer",
                "boolean",
                "string"
              ]
            },
            "description": "Only the swept parameter values for this scenario; empty when there is no sweep."
          },
          "replications": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Replication"
            }
          },
          "aggregate": {
            "type": "object",
            "additionalProperties": {
              "type": "object",
              "additionalProperties": {
                "type": "object",
                "additionalProperties": {
                  "$ref": "#/components/schemas/AggregateStat"
                }
              }
            },
            "description": "Per output key, recorder type, and statistic: the cross-replication aggregate. Timeseries recorders are not aggregated."
          }
        }
      },
      "Replication": {
        "type": "object",
        "required": [
          "replication",
          "seed",
          "outputs"
        ],
        "properties": {
          "replication": {
            "type": "integer",
            "minimum": 1,
            "description": "1-based replication number."
          },
          "seed": {
            "type": "integer",
            "minimum": 0,
            "description": "The derived RNG seed this replication ran with."
          },
          "outputs": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/Output"
            },
            "description": "Keyed by the output's `as` alias if set, else its state path."
          }
        }
      },
      "Output": {
        "description": "Recorder results for one declared output, keyed by recorder type.",
        "type": "object",
        "anyOf": [
          { "required": ["summary"] },
          { "required": ["final"] },
          { "required": ["timeseries"] }
        ],
        "additionalProperties": false,
        "properties": {
          "summary": {
            "type": "object",
            "required": [
              "stats"
            ],
            "additionalProperties": false,
            "properties": {
              "stats": {
                "type": "object",
                "additionalProperties": {
                  "type": "number"
                },
                "description": "The requested statistics (default `[\"mean\"]`) plus always `last` and `count`. Percentile keys: `p5`\u2026`p99`, `median`."
              }
            }
          },
          "final": {
            "type": "object",
            "required": [
              "value"
            ],
            "additionalProperties": false,
            "properties": {
              "value": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "Value at the end of the run."
              }
            }
          },
          "timeseries": {
            "type": "object",
            "required": [
              "times",
              "values"
            ],
            "additionalProperties": false,
            "properties": {
              "times": {
                "type": "array",
                "items": {
                  "type": "number"
                }
              },
              "values": {
                "type": "array",
                "items": {
                  "type": "number"
                }
              }
            }
          }
        }
      },
      "AggregateStat": {
        "type": "object",
        "required": [
          "mean",
          "std",
          "min",
          "max",
          "n"
        ],
        "additionalProperties": false,
        "properties": {
          "mean": {
            "type": "number"
          },
          "std": {
            "type": "number"
          },
          "min": {
            "type": "number"
          },
          "max": {
            "type": "number"
          },
          "n": {
            "type": "integer",
            "description": "Number of replications aggregated."
          }
        }
      },
      "EventEnvelope": {
        "type": "object",
        "description": "One event on the typed simulation event stream, sent as a single WebSocket text frame. The envelope shape is defined by [`spec/schemas/osdl.events.schema.json`](spec/schemas/osdl.events.schema.json) (`https://osdl.dev/schemas/0.1/osdl.events.schema.json`). Every envelope carries `v`, `seq`, `time`, `type`, and `source`; over this transport `run.experiment`, `run.replication`, and `run.scenario` are always set."
      },
      "ErrorResponse": {
        "type": "object",
        "required": [
          "error"
        ],
        "additionalProperties": false,
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable error message."
          }
        }
      },
      "RunValidationErrorResponse": {
        "type": "object",
        "required": [
          "error",
          "diagnostics"
        ],
        "additionalProperties": false,
        "properties": {
          "error": {
            "type": "string",
            "const": "validation failed"
          },
          "diagnostics": {
            "type": "array",
            "minItems": 1,
            "items": {
              "$ref": "#/components/schemas/Diagnostic"
            }
          }
        }
      },
      "Diagnostic": {
        "type": "object",
        "description": "One validation finding, as emitted by the core validator (also the CLI's `validate --format json` element shape).",
        "required": [
          "severity",
          "code",
          "target",
          "message"
        ],
        "properties": {
          "severity": {
            "type": "string",
            "enum": [
              "error",
              "warning"
            ]
          },
          "code": {
            "type": "string",
            "description": "Stable machine-readable code, e.g. `core.unknown-type`."
          },
          "target": {
            "description": "What the finding is about: the model, a component, a connection, or a parameter."
          },
          "message": {
            "type": "string"
          }
        }
      }
    }
  }
}
