Recursive AI

Save and reopen a debug session

OneScope is a free, open-source viewer for simulation recordings, with a desktop window or command-line control, from Recursive AI. MIT or Apache-2.0.

An .oscope file saves replay sources, forks, cursor and layout. OneScope debugs recorded simulation steps, such as physics; a fork is a branched copy of a recording.

Terminal
dotnet tool install --global RecursiveAI.OneScope.Tool

Requires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.

The examples use --headless, which renders offscreen but still needs a graphics device and driver. On Linux, a Vulkan software renderer such as lavapipe can supply it; ONEGAME_GPU_ADAPTER=software selects software rendering. Remove --headless to open the window.

Terminal
onescope open run.oppr --headless \
  --artifacts output \
  --run 'session_save {
    "path":"s.oscope","embed":true}' \
  --run 'session_open {
    "path":"output/s.oscope"}' 

Uses run.oppr from the download; writes output/s.oscope and reopens it. Save paths are relative to --artifacts; open paths are relative to the current directory.

Unzip the example, then dotnet run.

The last command returns this session state (the machine-specific path is omitted). views: 2 means two scene-view panels were restored; it does not count replay sources.

JSON
{
  "step": 0,
  "views": 2,
  "sources": [
    {
      "source": 0,
      "label": "run.oppr",
      "module": "onephysics",
      "startStep": 0,
      "steps": 120,
      "step": 0,
      "hash": "5a09a642111ebae2"
    }
  ]
}

The full program

run.oppr is included: a generated 2D OnePhysics recording, not a capture from a user’s game. It has a ball of radius 0.5 meters starting at (0, 4), a static ground box at (0, −1), and 120 recorded steps. The ground is 20 × 1 meters. The example does not need a running game.

The C# launcher installs the published OneScope tool into obj, seeks to step 20, forks, saves, and restores the session. It also saves a second session without embedding the original file.

C#
using System.Diagnostics;
using System.Text.Json;
 
string toolDir = Path.GetFullPath("obj/onescope-tool");
string tool = Path.Combine(toolDir,
    OperatingSystem.IsWindows() ? "onescope.exe" : "onescope");
if (!File.Exists(tool))
    Run("dotnet", "tool", "install", "RecursiveAI.OneScope.Tool",
        "--version", "1.0.0", "--tool-path", toolDir);
 
var saved = Open("run.oppr",
    "seek {\"step\":20}", "fork {}",
    "session_save {\"path\":\"saved.oscope\",\"embed\":true}");
Console.WriteLine($"Saved at step: {saved[^1].GetProperty("cursor")}");
foreach (var source in saved[^1].GetProperty("sources").EnumerateArray())
    Console.WriteLine($"Source {source.GetProperty("source")}: {source.GetProperty("kind")}");
var reopened = Open("run.oppr",
    "session_open {\"path\":\"output/saved.oscope\"}");
Console.WriteLine($"Restored step: {reopened[^1].GetProperty("step")}");
Console.WriteLine($"Restored sources: {reopened[^1].GetProperty("sources").GetArrayLength()}");
var referenced = Open("run.oppr", "fork {\"step\":20}",
    "session_save {\"path\":\"referenced.oscope\",\"embed\":false}");
Console.WriteLine("Without embed: " + string.Join(", ",
    referenced[^1].GetProperty("sources").EnumerateArray()
        .Select(s => s.GetProperty("kind").GetString())));
 
string Run(string command, params string[] arguments)
{
    var start = new ProcessStartInfo(command)
    {
        RedirectStandardOutput = true, RedirectStandardError = true
    };
    foreach (string argument in arguments) start.ArgumentList.Add(argument);
    using var process = Process.Start(start)!;
    var output = process.StandardOutput.ReadToEndAsync();
    var errors = process.StandardError.ReadToEndAsync();
    process.WaitForExit();
    if (process.ExitCode != 0)
        throw new InvalidOperationException(output.Result + errors.Result);
    return output.Result;
}
JsonElement[] Open(string file, params string[] operations)
{
    var arguments = new List<string>
    {
        "open", file, "--headless", "--size", "640x480",
        "--artifacts", "output"
    };
    foreach (string operation in operations)
        arguments.AddRange(["--run", operation]);
    return Run(tool, arguments.ToArray())
        .Split('\n', StringSplitOptions.RemoveEmptyEntries)
        .Select(line => JsonDocument.Parse(line).RootElement.Clone()).ToArray();
}

dotnet run prints:

Text
Saved at step: 20
Source 0: embedded
Source 1: embedded
Restored step: 20
Restored sources: 2
Without embed: referenced, embedded

session_open replaces everything currently open; if it fails with not_found or bad_session, the current session stays unchanged.

Variations

Keep the original replay outside the session. Fork replays are still embedded:

Terminal
onescope open run.oppr --headless \
  --artifacts output \
  --run 'fork {"step":20}' \
  --run 'session_save {
    "path":"referenced.oscope",
    "embed":false}'

The saved sources’ kind values are referenced and embedded, respectively. Keep the original replay at its saved path; reopening checks its SHA-256 and refuses a changed file.

The short answer

Use OneScope’s session_save operation to write an .oscope file and session_open to restore it. This saves the debugging workspace, not just the world at one instant. Sources, fork ancestry and poke replays, the shared step cursor, and layout and camera data belong to the session. Set embed:true when the original recording should travel with it.

The distinction matters when you stop investigating a simulation and return later. A standalone replay gives you the recorded simulation again. A session also remembers which sources you had open and where you were looking. It does not save arbitrary process memory or turn a live program into a suspended executable.

How do I save a simulation debugging session?

Choose a path inside the shell’s output folder and call session_save. In the example, --artifacts output makes saved.oscope land in output/. The embed argument controls whether opened files are stored inside the session. The returned JSON lists each source and its storage kind, making it possible to check what the session depends on before moving it.

A fork always carries its own replay, including the pokes that distinguish it from its parent. Its saved parent index and fork step preserve that relationship. You do not need to export the fork separately just to retain it in this workspace. The example’s fork has no pokes.

The cursor is a simulation step, not elapsed wall-clock time. Saving at step 20 and reopening restores that cursor. Sources may have different ranges, so each source’s reported step also matters. The example returns both sources at 20, with the original still containing its full 120-step recording.

Can I reopen a session without the original replay files?

Yes, for sources saved as embedded. Their payload is the module’s replay data. A referenced source instead stores its path and a SHA-256 digest of the file at save time. The restore operation reads that path and rejects a file whose digest differs. Referencing saves duplication, but moving or replacing the original breaks that dependency.

Use embedding for a self-contained recording bundle. Use references when the files will remain available at the same paths. A session is not a package of module assemblies: the reader still needs the appropriate module to open each saved source. The bundled OnePhysics module handles this example’s .oppr files; a custom format needs its own module loaded as well.

Restore constructs each source’s history at the saved cursor. Forks reopen from their own replay and reproduce their saved hashes. Layout data is an opaque part of the file that the shell interprets; the SDK does not assign meaning to its camera or panel bytes. See the session format for the embedded and referenced record layouts.

All onescope recipes