FabricFabricHarness
Reference

Sandbox lifecycle

Auto-suspend, snapshot/fork, and sandbox refs — managing long-running compute durably.

Long-running sandboxes (AKS pods, E2B sandboxes, Daytona workspaces) cost money while they are idle. Fabric Harness provides suspend, reconnect, snapshot, restore, and fork primitives so applications can reclaim compute without losing state.

Auto-suspend on idle

Pass idleSuspendMs to init() (or any sandbox factory option). After that many milliseconds with no operation, the sandbox is suspended. The next operation transparently resumes it.

import { init } from '@fabric-harness/sdk';

const fabric = await init({
  sandbox: 'docker',
  idleSuspendMs: 5 * 60_000,           // suspend after 5 minutes idle
  autoResumeOnAccess: true,            // default — auto-resume on next call
});

Backends that don't implement suspend() / resume() ignore the option (no-op pass-through). Currently supported:

BackendNative support
daytonaSandboxYes, via stop / start
e2bSandboxYes, via pause / resume
modalSandboxNo native suspend
kubernetesSandbox / aksSandboxNo automatic scale-to-zero
local / docker / empty / virtualNot applicable

How it works

createSandboxEnv() wraps the env in a SuspendingSandboxEnv decorator when idleSuspendMs > 0 and the inner env supports suspend(). The decorator:

  1. Tracks lastAccess on every operation.
  2. Schedules a setTimeout for idleSuspendMs from the last access.
  3. Calls inner.suspend() when the timer fires.
  4. On the next op, calls inner.resume() first (auto), then dispatches.
  5. Clears the timer in cleanup().

The timer is unref()'d so it never keeps the Node process alive on its own.

Manual control

You can call session.sandbox.suspend() / resume() directly when capability is reported:

const sandbox = await session.sandbox;
if (sandbox.capabilities?.suspend) {
  await sandbox.suspend();
}

Strict mode

Set autoResumeOnAccess: false to receive a FabricError (SANDBOX_UNAVAILABLE) instead of transparent resume. Useful when you want to explicitly gate operations on suspension state.

Snapshot and fork

session.fork(label?) captures the sandbox state and returns a SandboxFork handle. Calling fork.attach() spins up an independent session whose sandbox is a fork of the captured state — writes in branch A are invisible to branch B and to the origin.

const session = await fabric.session('experiment');
await session.shell('npm install && npm test');

const fork = await session.fork('after-install');

// Run two parallel experiments from the same checkpoint:
const branchA = await fork.attach();
const branchB = await fork.attach();

await Promise.all([
  branchA.shell('node bench.js --variant fast'),
  branchB.shell('node bench.js --variant safe'),
]);

Capability

const sandbox = await session.sandbox;
if (sandbox.capabilities?.fork) {
  // session.fork() will work
}

Backend support

BackendFork supported
empty / virtualIn-memory snapshot clone
localOn-disk snapshot copied to a fresh workspace temp directory
dockerOn-disk snapshot copied to a fresh container workspace
e2bSandbox / modalSandbox (provider-managed)Uses RemoteSandboxApi.fork() when the provider supports start-from-snapshot
daytonaSandboxNo; Daytona does not expose start-from-snapshot
databricksSqlSandboxNo; SQL execution is stateless
cloudflareSandboxUse Durable Object state and R2 artifacts instead of sandbox forks

session.fork() throws FabricError { code: 'SANDBOX_UNAVAILABLE' } when the sandbox doesn't support fork.

Cleanup

Each forked session owns its own sandbox; calling branch.sandbox.cleanup() (or letting it fall out of scope) tears down only that branch. The origin is unaffected.

For local/docker, fork creates a fresh temp workspace directory — the fork's cleanup() removes it (Docker) or leaves it for caller responsibility (Local). The shared snapshotRoot is not auto-removed; that's intentional so other forks of the same snapshot remain attachable.

Sandbox refs

Sometimes you want two sessions sharing the same running sandbox — not a fork, the actual instance — for multi-agent coordination, observability shadowing, or test isolation. session.sandboxRef() returns an opaque handle; pass it to attachSandbox(ref) in another session to participate in the same sandbox without taking ownership of its lifecycle.

import { init, attachSandbox } from '@fabric-harness/sdk';

// Owner: creates the sandbox, owns cleanup.
const ownerAgent = await init({ sandbox: 'docker', cwd: '/tmp/work' });
const owner = await ownerAgent.session('orchestrator');
await owner.shell('npm install');

const ref = await owner.sandboxRef();

// Attached: shares the same sandbox, does NOT own cleanup.
const workerAgent = await init({ sandbox: attachSandbox(ref) });
const worker = await workerAgent.session('worker-1');
await worker.shell('npm test');   // runs in owner's sandbox

// worker.cleanup() is a no-op for the underlying sandbox.
// owner.cleanup() tears down for everyone.

Lifecycle rules

  • The first session to register a ref is the owner. Owner cleanup tears down the underlying sandbox.
  • Any session can attachSandbox(ref) to participate. Attached sessions are non-owning: their cleanup unregisters the attachment but doesn't touch the underlying sandbox.
  • After the owner cleanup, attached sessions see FabricError { code: 'SANDBOX_UNAVAILABLE' } on subsequent operations. The error message says "no longer alive (owner cleanup ran)" so debugging is unambiguous.
  • session.sandboxRef({ portable: true }) returns a cross-process reference when the backend supplies encodeRef(). The receiving worker registers the provider decoder and calls attachSandbox().

When to use

  • Multi-agent coordination: an orchestrator and a worker both operating on the same workspace.
  • Observability shadow: a metrics/log-collecting session attached to a primary session's sandbox.
  • Test scaffolding: a setup phase that pre-populates a sandbox and hands the ref to the test session.

When you want independent branches instead, use session.fork() (Phase B) above. Forks copy state at a point in time; refs share live state.

Continuity negotiation

Use negotiateSandboxContinuity() before selecting a recovery strategy. It reports reconnect, snapshot, restore, and fork separately, and returns modes: ['none'] when the backend cannot preserve state.

import { negotiateSandboxContinuity } from '@fabric-harness/sdk';

const continuity = negotiateSandboxContinuity(await session.sandbox);
if (continuity.reconnect) {
  const ref = await session.sandboxRef({ portable: true });
  await durableStore.put(`sandbox:${session.id}`, ref);
}

Worker rotation

Portable references include the owning tenant. A mismatched tenant is rejected before Fabric Harness calls the provider decoder. Durable workers should also use one ownership lease store shared by every replica:

import { attachSandbox } from '@fabric-harness/sdk';
import { postgresSandboxOwnershipLeaseStore } from '@fabric-harness/node';

const factory = attachSandbox(ref, {
  tenantId: principal.tenantId,
  ownership: {
    store: postgresSandboxOwnershipLeaseStore(pg),
    ownerId: process.env.HOSTNAME!,
    leaseMs: 60_000,
  },
});

const sandbox = await factory({ sessionId });

The lease is renewed at every sandbox operation. Once another worker claims an expired lease, the former worker cannot continue. Temporal activity adapters persist and recover portable references automatically when sandboxOwnership is configured.

Reference

  • SandboxEnv.suspend?(): Promise<void>
  • SandboxEnv.resume?(): Promise<void>
  • SandboxEnv.fork?(snapshot): Promise<SandboxEnv>
  • SandboxCapabilities.suspend?: boolean
  • SandboxCapabilities.resume?: boolean
  • SandboxCapabilities.fork?: boolean
  • SandboxCapabilities.reconnect?: boolean
  • SandboxFactoryOptions.idleSuspendMs?: number
  • SandboxFactoryOptions.autoResumeOnAccess?: boolean
  • FabricSession.fork(label?): Promise<SandboxFork>
  • SandboxFork.attach(options?): Promise<FabricSession>
  • FabricSession.sandboxRef(): Promise<SandboxRef>
  • attachSandbox(ref, options?): SandboxFactory - pass to init({ sandbox }).
  • negotiateSandboxContinuity(sandboxOrRef) - inspect recovery modes.
  • registerSandbox(env, options?) / unregisterSandbox(refId) — direct registry access for advanced cases.
  • SuspendingSandboxEnv (class) — the suspend/resume decorator.
  • withIdleSuspend(env, { idleSuspendMs }) — helper that wraps when applicable, returns env unchanged otherwise.