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.
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:
(session, slug) freezes the slug's current AST tokens as the session's base snapshot.The id is opaque to the server. A UUID is conventional but anything stable per session works.
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.
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"
┌──────────────┐
│ 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):
ttl_seconds default)Sessions are persisted in SQLite at ~/.local/share/oculus/data/sessions/overlays.db and survive server restarts.
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).
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:
"X-Oculus-Session" => session_id on the request header.session_id echoed back through the signal so the chain can route subsequent calls into the same overlay.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.
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.
~/.local/share/oculus/data/sessions/overlays.db) and survive restarts.(start, end, tokens) collapse on the next read — repeated writes are cheap.X-Oculus-Session reads and writes against disk normally. The header is the entire switch.