Universes
A Universe is a folder of Spaces that share assets, recordings and tools. A Space is one world: a folder of readable files, the database Studio runs it from, and a git history that grows with every save.
01Overview
Universe, Space, Instance
A Space is one world: a scene with its parts, scripts, lighting and data, kept in one folder. A Universe is a folder of related Spaces plus the assets, recordings and experiment results they share. Everything inside a Space is an instance: a part, a folder, a model, a script, a light.
| Level | What it is | Where it lives |
|---|---|---|
| Eustress folder | Holds every Universe | Documents/Eustress/ |
| Universe | Related Spaces and what they share | Universe1/, a folder with a Spaces/ folder |
| Space | One world, its database and its history | Universe1/Spaces/Space1/ |
| Instance | One object in the world | A folder with an _instance.toml, or a record in the database |
Files, Database, History
A Space keeps its world in three places, and each has one job:
- Files are the readable form: service settings, scripts and
_instance.tomldocuments you can open in any editor. - The WorldDb (
world.fjalldb/) is the form Studio runs from. Files are copied into it, and the scene is built from it. - Git history is a commit of the Space folder each time you save.
The three are not copies of one another. Some edits exist only in the database, and the database folder is never committed, so it is worth knowing where each change goes. The rest of this page traces it.
A Place to Rehearse
The idea behind Universes is that a world should be as safe to change as a codebase: branch it, try the change, measure the result, then keep it or throw it away. Today that loop runs on git branches of a Space's files, experiment runs that save their results, and extra engines that run variants side by side. Branching the database itself exists in the storage library and is not yet wired into Studio; What's Next covers it.
02On Disk
The Eustress Folder
Every Universe lives in one Eustress folder, Documents/Eustress. On Windows that is the local %USERPROFILE%\Documents folder,
not a OneDrive-redirected copy, because OneDrive rewrites file metadata and
fights the file watcher. Set EUSTRESS_WORKSPACE to use another
folder. When the folder is empty, Studio creates Universe1 with
one Space, Space1.
Documents/Eustress/
.eustress/
engine.port port of the last engine to start
instances/<pid>.json one record per running engine
Universe1/ a Universe
.eustress/ shared by the Universe's Spaces
Spaces/
Space1/ a Space
Space2/
Universe2/Studio remembers the last Space you opened (last_space_path in ~/.eustress_engine/settings.json) and opens it again on the next
launch.
Inside a Universe
A Universe is any folder in the Eustress folder that holds a Spaces/ folder. Create one with File > New Universe... (Ctrl+Shift+N). Its .eustress/ folder holds what
its Spaces share, so every Space in it sees the same default meshes, recordings
and experiment results:
engine.port bridge port of the engine working in this Universe
lsp.port Rune language server port
assets/parts/ default part meshes: block.glb, ball.glb, ...
assets/meshes/ shared meshes
knowledge/recordings/<Space>/ simulation recordings, one JSON file per run
experiments/ results saved by run_experiment
telemetry.jsonl live watchpoint values, one line per second
runtime-snapshot.json play state and sim values, 4 times a second
sim-commands.jsonl commands queued by the MCP sim toolsInside a Space
A Space is a folder under Spaces/. File > New Space (Ctrl+N) creates one with 19 service folders, a Baseplate and a
Welcome Cube:
Workspace/ parts, models and folders
_service.toml
Baseplate/_instance.toml
WelcomeCube/_instance.toml
Lighting/ Sun, Moon, Sky, Atmosphere
SoulService/ MaterialService/ DataService/ ... 19 services in all
src/
space.toml name, author, version
simulation.toml simulation clock settings
header.bin world id, engine and schema versions
world.fjalldb/ the WorldDb, created on first open
.git/ history, created on first save
.gitignore
.eustress/
project.toml settings.toml sync.toml publish.toml ...
view.toml the 2D or 3D view the Space opens in
local/ user-local state, never committed
trash/ deleted instances, recoverable
exports/instances/ database contents exported as TOML
last_reconcile when files were last read into the databaseDeleting a file-backed instance in the Explorer moves its folder into .eustress/trash/ instead of erasing it. Perspective explains view.toml, and Simulation explains the clock settings
and recordings.
An Instance File
On disk, an instance is a folder named after it that holds an _instance.toml. This is the Welcome Cube every new Space starts
with, a 4 m cube resting on the Baseplate:
[metadata]
class_name = "Part"
archivable = true
created = "2026-09-22T17:04:11.482019300+00:00"
last_modified = "2026-09-22T17:04:11.482019300+00:00"
[asset]
mesh = "parts/block.glb"
scene = "Scene0"
[transform]
position = [0.0, 2.0, 0.0]
rotation = [0.0, 0.0, 0.0, 1.0]
scale = [4.0, 4.0, 4.0]
[properties]
color = [0.388, 0.706, 1.0, 1.0]
transparency = 0.0
reflectance = 0.2
anchored = true
can_collide = true
locked = falseLengths are meters. A file can name a different authoring unit with unit under [metadata] (for example cm or ft), and the loader converts its position and scale to
meters once, as it loads.
03The WorldDb
One Database per Space
Each Space has one database, world.fjalldb/, opened when the
Space opens. It is a Fjall store: a log-structured merge tree that appends
writes to a journal and compacts them in the background, so a Space can grow
past the point where one file per part stays practical. Eustress builds Fjall from
its own copy in the repository, wrapped by the eustress-fjall crate,
and everything reaches it through the WorldDb trait in eustress-worlddb.
Beside it, header.bin identifies the world. It starts with the
bytes EUSWORLD and records a world id, the version of the engine
that last wrote it and the schema version of the data inside.
Some edits exist only in the database, and world.fjalldb/ is excluded from git. Back it up with the rest of the Space, and never
delete it to force a reload: Studio would rebuild it from the files and
every edit that lived only in the database would be gone. The .gitignore Studio writes says the same thing.
Partitions
Inside, the data is split into partitions, independent key spaces that compact separately:
| Partition | What it holds |
|---|---|
tree | Every file of the Space, keyed by its path relative to the Space folder, plus #bin records for simple instances |
entities | Binary instance cores, keyed by the spatial cell they sit in |
entities_uuid | The same cores, keyed by instance UUID |
path_to_uuid, uuid_to_path | Lookups between file paths and UUIDs |
class_index | Which instances belong to each class |
mutations | The causal op-log |
datasets, timeseries | Data Platform datasets and recorded series |
voxels | Voxel terrain chunks |
datastore, datastore_ord | Laid out for DataStore values; scripts do not write here yet |
meta | The commit counter, the op-log sequence and the schema version |
Opening a Space
Opening a Space runs the same steps every time:
- Studio opens
world.fjalldb/and writesheader.binif the Space has none. - On the first open the database is empty, so Studio copies every file of the Space into
tree, keyed by relative path. It skips.eustress/,.git/,world.fjalldb/and other dot folders, and leaves the files where they are. - On every later open it reconciles: each
.toml,.rune,.luau,.soulor.mdfile changed since the time stored in.eustress/last_reconcileis copied in, and a.tomldeleted from disk is removed fromtreeas long as its folder still exists. This runs on a background thread while the scene loads, and the changes are applied to the scene as they arrive. - The scene is built from the database, not from the files: services, folders and instances from
tree, binary parts fromentities. A Space with more than 100,000 binary parts streams them around the camera instead of loading them all. - While the Space is open, the file watcher copies every file you save in another editor into
treeand applies it to the live scene.
If the database cannot be opened at all, Studio reads the files directly, so a Space always opens. Building covers streaming large Spaces.
The reconcile checks every file in the Space folder. On a Space with
hundreds of thousands of files, set EUSTRESS_SKIP_DISK_SCANS=1 to skip it. Files edited while Studio was closed are then ignored until
you clear the variable and reopen the Space.
Where an Edit Lands
Where a change is stored depends on how you made it:
| Change | Stored as | In git |
|---|---|---|
| A part from Toolbox > Primitives with no Folder or Model selected, in Edit mode | A binary core in entities, with no file | No |
| A part from the Insert menu or Insert Object..., or from Primitives with a Folder or Model selected | A new folder with an _instance.toml | Yes |
| A Part an agent creates over MCP while the engine runs | A binary core in entities, with no file | No |
| Save a simple part: no children, a built-in shape | A #bin record in tree; the file keeps its earlier values | No |
| Save a part that has children or a custom mesh | Its _instance.toml, rewritten on disk | Yes |
| Edit a file in another editor | The file, copied into tree by the watcher or the next open | Yes |
A binary core is a compact record of one part that sits at the top level of the
Workspace. Its path, such as Workspace/__bin_Part_<id>/_instance.toml, exists only in the database. Parts with children, custom meshes, scripts and GUI
elements always keep a real folder, because a single record cannot hold their
children or a relative mesh path.
04History
Every Save Is a Commit
Ctrl+S (File > Save Space) writes the scene,
then runs git add -A and commits in the Space folder with the
message manual save <timestamp>. Git runs on a background thread,
so the editor never waits on it, and the first save runs git init. When nothing changed since the last commit, no commit is made. The File menu
shows the time of the last snapshot.
Autosave does the same on a timer, every 300 seconds by default, with the
message autosave <timestamp>. A successful autosave shows the
toast Auto-saved (git); a failed one shows an error instead of
failing quietly. Commits carry your Eustress user name when you are signed in.
Otherwise they use the repository's own identity, which Studio sets to Eustress Engine Autosave when it creates the repository.
{
"auto_save_enabled": true,
"auto_save_interval": 300.0
}The interval is in seconds. Change these two keys, with Studio closed, to turn autosave off or change its pace.
What Git Captures
A commit holds the files in the Space folder, which is not the whole Space.
Autosave adds world.fjalldb/, .eustress/trash/ and *.bak-* to the Space's .gitignore, and a new Space
already ignores .eustress/local/.
| Part of the Space | In git |
|---|---|
| Services, scripts and GUI definitions | Yes, as files |
| Parts from the Insert menu or Insert Object, and parts with children or custom meshes | Yes, as files |
| Binary cores: parts from Toolbox > Primitives at the top level, and Parts agents create over MCP | No, database only |
Saved changes to simple parts (#bin records) | No, database only |
world.fjalldb/ | Never |
To put binary parts into history, export them first. The MCP tool export_instances_toml writes every binary core as an ordinary _instance.toml under .eustress/exports/instances/, and the next save commits that folder. The loader and the file watcher both
skip .eustress/, so an export is never loaded back as a second
copy of the scene.
{ "tool": "export_instances_toml", "arguments": { "layout": "single_file" } }
// layout "folders" (default): one loadable <Name>_<id>/_instance.toml per core
// layout "single_file": one instances.toml, easier to read and diff
// limit: 5000 by default, 200000 at most; class and region narrow the exportGoing Back
History is plain git, so any git client reads it. To bring back an earlier version of a file, check it out of an older commit:
cd ~/Documents/Eustress/Universe1/Spaces/Space1
git log --oneline
git checkout <commit> -- Workspace/WelcomeCube/_instance.tomlStudio treats the restored file like any other edit made outside the editor:
the file watcher applies it while the Space is open, and the reconcile reads it
in on the next open. A checkout restores files only. Binary cores and #bin records are in no commit, so they keep their current state.
05Branching
Git Branches
A branch of a Space is a git branch of its folder. You can use any git client, or
let an agent use the git tools of the MCP server
and of Studio's Workshop panel. They run in the Space's own repository, the
nearest .git at or above the Space folder.
| Tool | What it does |
|---|---|
git_branch | list, create, switch, delete or merge (merges with --no-ff) |
git_commit | Stages all changes, or the files you list, and commits |
git_status, git_diff, git_log | Read the working tree and recent history |
feedback_diff | Diffs two branches, commits or tags, optionally as a summary |
Experiments
run_experiment turns a what-if into a recorded run. It sets the
simulation values you give it, runs the simulation for duration_s simulated seconds, waits for the run to finish (300 s of wall time at most, by
default), summarizes the telemetry and saves the result as JSON in the
Universe's .eustress/experiments/ folder. With create_branch it first creates and switches to a git branch named exp/<name>-<timestamp>.
{
"tool": "run_experiment",
"arguments": {
"name": "high_voltage_4v3",
"sim_values": { "cell_voltage": 4.3 },
"duration_s": 60,
"time_scale": 100,
"create_branch": true
}
}list_experiments lists saved results, newest first, and compare_runs shows the change in every metric between two of them
(latest and latest-1 work as names). Merge the
winning branch with git_branch and delete the rest. Simulation covers the clock, time compression
and watchpoints these runs use.
Switching branches changes the files. State that lives only in world.fjalldb/ stays as it is on every branch. The sim_values are applied to the running simulation and are
not files either, so git diff does not show them; the
experiment JSON records them under config. Put a design
change you want to compare in a file, and it travels with the branch.
Variants in Parallel
To run variants at the same time, give each one its own Space and its own
engine. eustress open <space> --headless starts a windowless
engine on a Space and prints its process id and bridge port; run each variant's
experiment against that engine, then compare the saved results. CLI & Headless covers the commands, and the next
sections cover how the engines find each other.
06The Op-Log
What It Records
Git records states; the causal op-log records the events between them. It lives
in the mutations partition as an append-only list, one record per
create or delete, numbered by a sequence the database assigns and that survives
restarts. Every record carries:
| Field | Meaning |
|---|---|
seq | Position in the log |
tx_id | Transaction the change belongs to |
ts_nanos | Wall-clock time in nanoseconds |
actor | Who caused it: User, Script:<name>, Mcp:<tool>, Importer, FileWatcher or System |
op | Create, Update or Delete |
class, uuid, rel_path | Which instance, and its path in the Space |
has_before, has_after | Whether the record holds the instance before and after the change |
Today the log captures binary cores: created from Toolbox > Primitives or through
the bridge, deleted in the editor, through the bridge or by undo, and recorded as System. It also captures new _instance.toml files that
appear while the Space is open, recorded as FileWatcher. Property
edits, moves and new GUI elements are not
recorded yet, and no record carries a before-image, so the log can say what was
created and deleted but cannot yet rewind it.
Reading It
Read the tail of the log from a running engine with the oplog_tail MCP tool, the oplog.tail bridge method, or
the CLI. The default is the last 50 records, the maximum 1,000, oldest first:
eustress bridge --universe ~/Documents/Eustress/Universe1 oplog --limit 20{
"count": 1,
"mutations": [
{
"seq": 42,
"tx_id": 7,
"ts_nanos": 1790000000000000000,
"actor": "System",
"op": "Create",
"class": "Part",
"uuid": "9f2c4e1ab7d34c0e8a61f0b2c3d4e5f6",
"rel_path": "Workspace/__bin_Part_00000000a1b2c3d4/_instance.toml",
"has_before": false,
"has_after": true,
"parent_tx": null,
"reason": null
}
]
}07Engines & Tools
Several Engines
Every running engine, windowed or headless, opens a bridge on 127.0.0.1 and writes its port to engine.port in its
Universe's .eustress/ folder, with a copy in the Eustress folder.
That file has one slot, so with two engines in one Universe it names only one of
them. Each engine therefore also writes a record of its own, keyed by process id:
{
"pid": 18244,
"port": 53120,
"kind": "headless",
"space": "C:\\Users\\me\\Documents\\Eustress\\Universe1\\Spaces\\VariantA",
"universe": "C:\\Users\\me\\Documents\\Eustress\\Universe1",
"started_at": "2026-09-22T18:02:41.512934100+00:00"
}eustress instances lists these records and removes any whose port no
longer answers, eustress bridge --pid <pid> ... drives one engine by
its record, and eustress close shuts engines down cleanly by pid, by
Space or all at once.
Fjall keeps no lock between processes, and nothing stops two engines from opening the same Space, so nothing coordinates their writes to its database. Give every engine its own Space, and close the engine on a Space before an offline tool writes to its database.
Inspect Without the Engine
eustress-space reads a Space's database without linking the engine.
It takes a Space folder or its world.fjalldb/ and works on the
binary instance cores:
eustress-space open <space> # world id, engine version, core count, bounds, classes
eustress-space verify <space> # validates every core; exits non-zero if any fail
eustress-space export <space> [--out <dir>] # one readable .instance.toml per coreExport writes to export_toml/ inside the given path unless you pass --out. The tool is built from its crate in the repository: cargo run -p eustress-space -- open <space>.
Repair Tools
Two more command-line tools repair a Space's database while no engine has it open:
purge_tree_pathremoves every record matching--matchfrom every partition: thetreekey and its#bintwin, the identity lookups and both cores. It is a dry run unless you pass--apply, and with it every value is first copied to.eustress/trash/db-purge-<unix-seconds>/.reseed-space-subtreecopies one folder's.tomlfiles from disk intotreeand drops their#binrecords, for when the database holds an older version of files you trust.
cargo run -p eustress-worlddb --bin purge_tree_path -- --space <space> --match Aureole
cargo run -p eustress-worlddb --bin purge_tree_path -- --space <space> --match Aureole --apply
cargo run -p eustress-engine --bin reseed-space-subtree -- --space <space> --subdir Workspace08What's Next
Database Branches
The storage library already contains the branch that git cannot provide. A BranchHandle forks any database in constant time: writes go to an
in-memory overlay, reads fall through to the parent, commit replays
the overlay into the parent and discard drops it. Branches nest, and batch_rollout runs many of them forward in parallel with no
rendering and returns a digest of each. Both are tested in eustress-worlddb, and nothing in Studio, the CLI or the MCP server
calls them yet. Wiring them in will let a what-if fork the whole Space, database
included, and throw the losers away at no cost to the original.
Complete History
Two gaps separate history from the whole Space. The op-log will record property
edits and new GUI elements, attribute each change to the agent or person that made
it, and keep before-images, which lets the replay and rewind functions the storage
library already has step a Space back. And a versioned export of the database will
let a commit carry the parts that live only in world.fjalldb/.
Branch it. Run it. Keep what works.