Virtual time and asynchronous work

Look up clock modes, duration grammar, public transition results, idle scope, and committed-failure recovery.

Every instance owns its clock. This checked public test shows the complete status(), advance(), idle(), and cleanup shape:

test/clock-transition.test.ts
import { createTestRuntime } from "localhost2137/testing";
import { expect, it } from "vitest";
import config from "../localhost.config.js";

it("reports one exact instance-clock transition", async () => {
	const runtime = await createTestRuntime({ config, port: 0, storage: "temporary" });
	try {
		const instance = await runtime.createInstance();
		try {
			expect(await instance.clock.status()).toEqual({
				mode: "pinned",
				now: "2026-01-01T00:00:00.000Z",
			});

			const advanced = await instance.clock.advance("2h");
			expect(advanced).toMatchObject({
				from: "2026-01-01T00:00:00.000Z",
				mode: "pinned",
				to: "2026-01-01T02:00:00.000Z",
			});
			expect(advanced.advanceId.length).toBeGreaterThan(0);

			await instance.idle();
			expect(await instance.clock.status()).toEqual({
				mode: "pinned",
				now: "2026-01-01T02:00:00.000Z",
			});
		} finally {
			await instance.destroy();
		}
	} finally {
		await runtime.close();
	}
});
pnpm --filter @localhost2137/example-getting-started test

The clock moves only because the test calls advance("2h"). idle() does not move it.

Choose a mode

The checked config imported above sets clock: { mode: "pinned", startAt: "2026-01-01T00:00:00.000Z" }.

ClaimModeConsequence
Plugin work at an exact emulator instant{ mode: "pinned", startAt }Time stays at the RFC 3339 startAt value until explicit advancement.
Plugin timestamps must follow machine time{ mode: "real" }Time follows wall time plus a persisted offset added by advances.
Application timers or Date.now()Neither controls themGive the application its own clock seam or completion boundary.
Provider-hosted schedulingNeither executes itKeep a provider-facing check.

Plugin code sees instance time only through its context clock. Code that reads Date.now(), an SDK clock, or another process still sees that source. Task tracking is a separate concern: it controls which work the runtime waits for and which failures it surfaces, not which clock that work reads.

A pinned clock stays fixed across daemon restarts. A real clock keeps moving and retains its explicit offset. New and reset worlds start from config; an existing persistent world retains its current clock after restart.

Duration grammar

AcceptedMilliseconds
1ms1
2s2000
3m180000
4h14400000
30d2592000000
2w1209600000

Durations are positive whole values without leading zeroes. 01s, 1.5h, 0s, -1d, and 1month are rejected. The parsed result must fit a safe integer of milliseconds, and the resulting clock must fit the JavaScript Date range. Month and year units do not exist.

Advance adds a duration; it never sets an absolute timestamp. Two successful advance("30d") calls move pinned time by exactly 60 days. In real mode, they add 60 days to the persisted offset while wall time continues to pass. Each successful window has a new advanceId.

What success means

Awaited callEstablishedNot established
clock.status()Current instance mode and RFC 3339 timeAny other clock source
clock.advance(duration)Window committed; each configured service reconciled and acknowledged it; tracked reconciliation work settledApplication queues, SDK timers, external systems
idle()Currently registered work for that instance settled or failedFuture scheduled retries, application jobs, untracked promises

An advance commits { advanceId, from, to }, presents it to configured services in stable order, waits for tracked work started during reconciliation, and then records acknowledgements. One 30d call is one window; a plugin may produce zero, one, or many effects in it. Two 15d calls are two windows and are equivalent only when that plugin documents and tests the invariant.

A plugin without onTimeAdvanced still observes the new clock, but the runtime invents no service behavior. A retry stored for a future virtual instant is not currently tracked work; idle() can settle the failed attempt while the retry remains scheduled. Advance to the plugin's documented deadline to run it.

For callback-driven application effects, first await the operation or clock transition, then await the application's own database, queue, or job boundary. A second idle() does not extend runtime tracking into application code. Callbacks and webhooks shows an actual receiver loop.

Recover a committed failure without moving twice

This is the exact tested control envelope when the clock committed but reconciliation remains pending. Runtime-generated correlationId and advanceId values differ in a real process.

tested control response
{
  "error": {
    "code": "INSTANCE_MUTATION_COMMITTED",
    "correlationId": "adapter-correlation",
    "details": {
      "advanceId": "advance_safe",
      "from": "2026-01-01T00:00:00.000Z",
      "mode": "pinned",
      "reconciliationPending": true,
      "to": "2026-01-01T00:00:01.000Z"
    },
    "message": "The clock moved, but time reconciliation remains pending."
  }
}
Error stateMeaningNext action
reconciliationPending: trueAt least one service has not acknowledged the committed window.Correct the service/config cause. Restart resumes persistent pending work; the same duration in the running daemon resumes the original window.
reconciliationPending: falseReconciliation finished, but a durability check failed after commit.Inspect clock and service state. Do not repeat the duration as repair.
Another error codeThe committed-mutation contract does not apply.Read current status before deciding whether anything moved.

A different duration is rejected while reconciliation is pending. If a service owing an acknowledgement was removed, restore its prior config and restart. Do not reset or destroy the world to hide the pending transition.

The public error does not expose the underlying cause. The CLI prints the generic message and boundary correlation ID; ControlApiError also exposes the safe window details. Inspect available delivery/plugin evidence, config, and state rather than retrying from memory.

Diagnose a failing scenario covers the first broken boundary. What determinism means separates clock control from repeatability; Provider compatibility limits what the observed transition can claim.