Wanderland

using-oculus-sessions

A practical guide to making session-isolated edits against oculus-api. If you can drive it from a browser fetch, you can drive it from anything else that speaks HTTP — including a Ruby Wanderland::Boundary.

The engine internals (snapshot data structures, op compaction, virtual-fence re-annotation) live at oculus-session-overlay. This node is the developer-facing how-to.

The Contract

Every HTTP call to oculus-api may carry an X-Oculus-Session: <id> header. FastAPI middleware binds the id to a request-scoped contextvar, and reads or writes that touch a node's tokens then route through the session overlay store instead of disk:

The id is opaque to the server. A UUID is conventional but anything stable per session works.

Identity Sourcing

Where the session id comes from is the caller's choice:

Caller Convention
oculus-fence web component crypto.randomUUID(), persisted in localStorage under lantern.fence-session.v1, scoped by data-session-scope
Ruby boundary env var, input arg, or a UUID generated at the start of the chain
curl / scripts --header "X-Oculus-Session: $UUID"

A scope key (__global__, goose-pong-game-1, etc.) is just a hash key into a per-scope id store; the server only ever sees the UUID it eventually receives.

Quick Start (curl)

SID=$(uuidgen)

# First peek under SID — server seeds the base snapshot from disk on first touch.
curl -H "X-Oculus-Session: $SID" \
     "https://i.loss.dev/api/oculus/peek/some-node?path=section.yaml.score"

# A poke under the same SID writes into the overlay, not the file on disk.
curl -H "X-Oculus-Session: $SID" -H "Content-Type: application/json" \
     -X POST "https://i.loss.dev/api/oculus/poke/some-node" \
     -d '{"path":"section.yaml.score","value":42,"context":"session test"}'

# A second peek replays the op — returns 42 even though disk is unchanged.
curl -H "X-Oculus-Session: $SID" \
     "https://i.loss.dev/api/oculus/peek/some-node?path=section.yaml.score"

# A peek WITHOUT the header reads the file directly — original score.
curl "https://i.loss.dev/api/oculus/peek/some-node?path=section.yaml.score"

Snapshot Lifecycle

┌──────────────┐
│  disk node   │   source of truth on read with no session header
└──────┬───────┘
       │ first touch under (S, X)
       ▼
┌──────────────────────────┐
│  base snapshot @ t=0     │   frozen AST tokens for slug X under session S
└──────┬───────────────────┘
       │ append-only
       ▼
┌──────────────────────────┐
│  op log [op_1, op_2…]    │   token-range splices: tokens[start:end] = updated
└──────┬───────────────────┘
       │ replay on read
       ▼
┌──────────────────────────┐
│  effective tokens        │   what the caller sees under X-Oculus-Session: S
└──────────────────────────┘

States (per session, used by the /sessions admin page):

Sessions are persisted in SQLite at ~/.local/share/oculus/data/sessions/overlays.db and survive server restarts.

Admin Endpoints

GET    /api/oculus/overlay-sessions                       list (filter ?state=active|idle|expired)
GET    /api/oculus/overlay-sessions/{sid}                 raw snapshots + op log
GET    /api/oculus/overlay-sessions/{sid}/state           effective tokens per touched slug
POST   /api/oculus/overlay-sessions/{sid}/reset           drop snapshots + ops; id stays reusable
DELETE /api/oculus/overlay-sessions/{sid}                 hard purge
POST   /api/oculus/overlay-sessions/purge                 bulk: { filter: 'expired' } or { ids: [...] }

Lantern fronts a human view at /sessions (built on the same data via the /api/oculus/overlay-sessions endpoints).

Tutorial: Session-Aware Ruby Boundary

A Wanderland::Boundary that calls a fence under a session overlay. Drop into lib/sprout_engine/boundaries/oculus_fence_session.rb and register on a chain.

# frozen_string_literal: true
require "net/http"
require "uri"
require "json"
require "securerandom"

module SproutEngine
  module Boundaries
    class OculusFenceSession
      include Wanderland::Boundary

      IDENTITY = Wanderland::Identity.new(
        id: "boundary:oculus_fence_session",
        name: "OculusFenceSession",
        roles: [:boundary],
        type: :service,
        scopes: [:grid]
      ).freeze

      boundary :oculus_fence_session,
        identity: IDENTITY,
        capabilities: [:fence_execution, :session_overlay],
        description: "Execute an oculus fence under a session overlay; first call under (session, slug) seeds the snapshot, subsequent calls splice into it",
        env: {
          OCULUS_BASE: "https://i.loss.dev",
          OCULUS_USER: nil,
          OCULUS_PASS: nil
        },
        input_shape: {
          args: {
            fence_id:   true,
            params:     true,
            session_id: false   # optional — generated if omitted
          }
        },
        output_shape: {
          fence_id:   true,
          session_id: true,
          result:     true
        }

      def call(input)
        env        = input["context"]["env"]
        fence_id   = input.find_or_fail("args.fence_id")
        params     = input.find_or_fail("args.params")
        session_id = input.dig("args", "session_id") || SecureRandom.uuid

        uri = URI("#{env["OCULUS_BASE"]}/api/oculus/fences/#{fence_id}/execute")
        req = Net::HTTP::Post.new(uri,
          "Content-Type"     => "application/json",
          "X-Oculus-Session" => session_id
        )
        req.basic_auth(env["OCULUS_USER"], env["OCULUS_PASS"]) if env["OCULUS_USER"]
        req.body = JSON.generate(params: params)

        res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|
          http.request(req)
        end
        body = JSON.parse(res.body)

        Wanderland::Signal.ok(
          fence_id:   fence_id,
          session_id: session_id,
          result:     body
        )
      end
    end
  end
end

The two lines that turn this into a session-aware boundary:

A chain that places two pieces under the same session overlay reuses the id explicitly:

chain:
  - oculus_fence_session:
      args:
        fence_id: place-piece
        params:     { piece: "knight", at: "e4" }
        session_id: "game-7-2026-05-02"
  - oculus_fence_session:
      args:
        fence_id: place-piece
        params:     { piece: "pawn",   at: "d5" }
        session_id: "game-7-2026-05-02"

The first call seeds the snapshot for place-piece's touched slug; the second splices on top. Disk stays untouched until a future commit operation folds the ops back to the base — see the engine spec for the gap.

Reset, Inspect, Purge

A boundary that clears its session mid-chain hits the admin endpoints with the same id:

def reset_session(env, session_id)
  uri = URI("#{env["OCULUS_BASE"]}/api/oculus/overlay-sessions/#{session_id}/reset")
  req = Net::HTTP::Post.new(uri)
  req.basic_auth(env["OCULUS_USER"], env["OCULUS_PASS"]) if env["OCULUS_USER"]
  Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") { |h| h.request(req) }
end

reset keeps the id reusable — the next touch reseeds the snapshot from current disk. DELETE purges the id outright. Both shapes show up on /sessions immediately.

Notes