# SessionHandle

> Talos next · Since 0.6

The run itself, rather than anything in the world.

Reached in a behaviour as `ctx.session`.

## Signature

```ts
export interface SessionHandle {
  /**
   * Starts the scene over.
   *
   * The request is recorded and acted on when the tick ends, never in the middle of one: the
   * behavior asking is running inside the world it wants replaced, and half the scene must not see
   * the new one while the other half sees the old. At the end of the tick the runtime rebuilds from
   * the scene as loaded — frame 0, every behavior's `state` and per-instance state gone (a restart
   * is not a hot reload), controllers back at their spawns, the HUD cleared, the event bus empty —
   * and the next tick runs against that.
   *
   * Assets are not reloaded, so a restart costs no upload and no load time. The player's own key
   * bindings survive it: restarting is asking to play again, not to give the keys back. Calling it
   * twice in one tick is one restart.
   *
   * The same call works in editor Play, in `argus run` and in an exported build, because all three
   * step the same world.
   */
  restart(): void

  /**
   * Asks to leave the game — the "Quit" on a title screen or a pause menu.
   *
   * Like {@link SessionHandle.restart} it is recorded and acted on when the tick ends, and the
   * tick that asked completes first. What happens next is up to whoever is running the game, not
   * the game: `argus run` with a window, and an exported build, close the window and exit with
   * status 0; the editor's Play stops and hands the editor back, never closing it; a headless run
   * and a managed session keep running, because the session's owner decides when it ends. Every
   * host records it the same way — a `session-quit` event carrying `{ tick }` on the event
   * stream (`GET /events`) — so an agent or the playtester sees the game asked to go. Calling it
   * twice in one tick is one quit.
   *
   * It is not a save: nothing is written for the game, so persist anything the player should keep
   * with `ctx.save` before calling it.
   */
  quit(): void

  /**
   * Plays a different scene of this project, in the same process.
   *
   * `path` is relative to the content root this session was opened from — the project root for
   * `argus run <project>` and for an exported build, the revision's verified snapshot for the
   * editor's Play — spelled with forward slashes, as `scenes/credits.scene.json`. It is never
   * resolved against the authored tree, so a running game reads exactly the files it shipped with,
   * and it may not leave that root: an absolute path, a `..`, a backslash separator, the wrong
   * case, or a symbolic link that leads outside are all refused.
   *
   * A refusal — a path that breaks a rule, a scene that is not there, a scene that does not
   * validate, a behaviour this build did not compile — reaches the caller as a `ctx.log` line and
   * a `scene-load-failed` event carrying `{ scene, code, message }`, and **the scene that is
   * running keeps running**, under its own name and still watched. Nothing is half-switched.
   *
   * A session opened on a checked build is also refused a scene **that build could not resolve**,
   * by name and before the file is opened: a scene naming a file the snapshot does not hold, a
   * document the contract refuses, a behaviour the build did not compile, or a scene whose module
   * name belongs in this build to a different scene's compiled file. Those are the same four
   * problems that stop an export composing, so what a build cannot ship it cannot switch to, and
   * the event carries the build's own word for it in `reason`. A run with no build behind it has
   * no such verdict and is as lenient about a switch as it was about the first load.
   *
   * Like {@link SessionHandle.restart} it lands when the tick ends, never inside one, and it is a
   * restart with a different scene: frame 0, every behavior's `state` and per-instance state gone,
   * the entity store, the physics world, the HUD and the event bus fresh. The renderer and
   * everything uploaded to it, the parsed assets, the time scale, the pause and the player's own
   * key bindings are carried across, so geometry two scenes share is not uploaded twice.
   *
   * Calling it twice in one tick is one switch, to the scene the **last** call named, and a
   * `loadScene` supersedes a {@link SessionHandle.restart} asked for in the same tick: the scene
   * being restarted is the scene being left.
   */
  loadScene(path: string): void

  /**
   * Holds the world from the next tick until {@link SessionHandle.resume}: physics, controllers,
   * behaviours' `update`, animation, particles, the camera rig and `ctx.time.now` stop, while the
   * UI, input, `"ui-activated"` into `onEvent`, audio and each behaviour's `pausedUpdate` (called
   * in place of `update`) run on. A screen declared `pausesWorld: true` holds the world the same
   * way while it is on the stack. A flag, not a count; a restart or a scene switch starts running.
   */
  pause(): void

  /** Lets the world go from the next tick, unless a `pausesWorld` screen still holds it. */
  resume(): void

  /** Whether the world is held, by {@link SessionHandle.pause} or by a `pausesWorld` screen. */
  paused(): boolean

  /**
   * The scene running right now, spelled the way {@link SessionHandle.loadScene} takes it.
   *
   * What a behavior shared between scenes reads to find out which one it is in, and what a menu
   * compares against before switching to a scene it is already playing. A world with no scene file
   * behind it answers with an empty string rather than inventing a path.
   */
  scene(): string
}
```

Import: `import type { SessionHandle } from "@argus/api"`

## Members

| Member | Signature | Description |
|---|---|---|
| `restart` | `restart(): void` | Starts the scene over. The request is recorded and acted on when the tick ends, never in the middle of one: the behavior asking is running inside the world it wants replaced, and half the scene must not see the new one while the other half sees the old. At the end of the tick the runtime rebuilds from the scene as loaded — frame 0, every behavior's `state` and per-instance state gone (a restart is not a hot reload), controllers back at their spawns, the HUD cleared, the event bus empty — and the next tick runs against that. Assets are not reloaded, so a restart costs no upload and no load time. The player's own key bindings survive it: restarting is asking to play again, not to give the keys back. Calling it twice in one tick is one restart. The same call works in editor Play, in `argus run` and in an exported build, because all three step the same world. |
| `quit` | `quit(): void` | Asks to leave the game — the "Quit" on a title screen or a pause menu. Like {@link SessionHandle.restart} it is recorded and acted on when the tick ends, and the tick that asked completes first. What happens next is up to whoever is running the game, not the game: `argus run` with a window, and an exported build, close the window and exit with status 0; the editor's Play stops and hands the editor back, never closing it; a headless run and a managed session keep running, because the session's owner decides when it ends. Every host records it the same way — a `session-quit` event carrying `{ tick }` on the event stream (`GET /events`) — so an agent or the playtester sees the game asked to go. Calling it twice in one tick is one quit. It is not a save: nothing is written for the game, so persist anything the player should keep with `ctx.save` before calling it. |
| `loadScene` | `loadScene(path: string): void` | Plays a different scene of this project, in the same process. `path` is relative to the content root this session was opened from — the project root for `argus run <project>` and for an exported build, the revision's verified snapshot for the editor's Play — spelled with forward slashes, as `scenes/credits.scene.json`. It is never resolved against the authored tree, so a running game reads exactly the files it shipped with, and it may not leave that root: an absolute path, a `..`, a backslash separator, the wrong case, or a symbolic link that leads outside are all refused. A refusal — a path that breaks a rule, a scene that is not there, a scene that does not validate, a behaviour this build did not compile — reaches the caller as a `ctx.log` line and a `scene-load-failed` event carrying `{ scene, code, message }`, and **the scene that is running keeps running**, under its own name and still watched. Nothing is half-switched. A session opened on a checked build is also refused a scene **that build could not resolve**, by name and before the file is opened: a scene naming a file the snapshot does not hold, a document the contract refuses, a behaviour the build did not compile, or a scene whose module name belongs in this build to a different scene's compiled file. Those are the same four problems that stop an export composing, so what a build cannot ship it cannot switch to, and the event carries the build's own word for it in `reason`. A run with no build behind it has no such verdict and is as lenient about a switch as it was about the first load. Like {@link SessionHandle.restart} it lands when the tick ends, never inside one, and it is a restart with a different scene: frame 0, every behavior's `state` and per-instance state gone, the entity store, the physics world, the HUD and the event bus fresh. The renderer and everything uploaded to it, the parsed assets, the time scale, the pause and the player's own key bindings are carried across, so geometry two scenes share is not uploaded twice. Calling it twice in one tick is one switch, to the scene the **last** call named, and a `loadScene` supersedes a {@link SessionHandle.restart} asked for in the same tick: the scene being restarted is the scene being left. |
| `pause` | `pause(): void` | Holds the world from the next tick until {@link SessionHandle.resume}: physics, controllers, behaviours' `update`, animation, particles, the camera rig and `ctx.time.now` stop, while the UI, input, `"ui-activated"` into `onEvent`, audio and each behaviour's `pausedUpdate` (called in place of `update`) run on. A screen declared `pausesWorld: true` holds the world the same way while it is on the stack. A flag, not a count; a restart or a scene switch starts running. |
| `resume` | `resume(): void` | Lets the world go from the next tick, unless a `pausesWorld` screen still holds it. |
| `paused` | `paused(): boolean` | Whether the world is held, by {@link SessionHandle.pause} or by a `pausesWorld` screen. |
| `scene` | `scene(): string` | The scene running right now, spelled the way {@link SessionHandle.loadScene} takes it. What a behavior shared between scenes reads to find out which one it is in, and what a menu compares against before switching to a scene it is already playing. A world with no scene file behind it answers with an empty string rather than inventing a path. |

## Example

The tested fixture `fixtures/recipes/jump-pad-and-elevator/scenes/behaviors/camera-zone.ts`.

```ts
import type { Behavior } from "@argus/api"

/**
 * A trigger volume that retunes the camera while the player is inside it.
 *
 * The engine has no camera-zone concept and needs none: a volume, a `trigger` component and this.
 * Here the player's behaviour owns the camera (it re-sends its params when the view is turned), so
 * the zone tells it what to change rather than calling `ctx.camera.set` itself. In a scene where
 * nothing else writes the camera, the zone can call `ctx.camera.set(kind, params)` directly.
 */
interface Props {
  /** `ctx.camera.set` params to merge in on the way in, and on the way out. */
  inside?: Record<string, unknown>
  outside?: Record<string, unknown>
}

export default {
  onTrigger(ctx, trigger) {
    const params = trigger.entering ? ctx.props.inside : ctx.props.outside
    if (params) ctx.events.emit("camera-zone", { target: trigger.other.id, params })
  },
} satisfies Behavior<Record<string, never>, Props>
```
