Skip to content

Background systems & the scheduler

Companion to ADR-0045. The authoritative API contract lives with the code in runtime/src/sched.h; this doc is the model and the authoring rule, not a second API reference.

The scheduler lets a nOSh subsystem run in the background — advance on a clock regardless of the active screen — and survive power-off. It is the heartbeat layer beside the event bus: the bus fans out “X happened”; the scheduler makes X happen on a clock. nOSh-internal C; no cart FFI in v1.

A background system holds its own state, the scheduler advances it, and it announces results on the bus. What it does to the foreground (ambient → soft → hard intrusion) is built on top and is out of scope here.

One fixed table of entries on a virtual clock. A periodic background system and a one-shot deferred tick are the same mechanism — register either, cancel by handle, and the host drives the clock once per frame. The verbs are sched_every / sched_after / sched_cancel / sched_tick / sched_catchup; see sched.h for signatures and the fixed sizing/clamp constants.

Live ticking and power-off catch-up are one code path. Every callback gets a dt_ms — the elapsed virtual time since it last fired. While powered on that’s a small per-frame delta; at boot sched_catchup advances the clock by the offline gap in one jump, so an entry that fell behind fires once with the whole gap as dt (coalesced — no replaying thousands of missed ticks). The device sources the gap from the RTC-synced clock (monotonic time resets every boot), and the jump is clamped so a clock fault can’t over-advance an economy-touching system.

The desktop emulator ticks live only. The device host also calls sched_catchup() at boot — that wiring is the Platform Eng track.

The one thing that bites if you get it wrong: a callback must tolerate a large dt_ms. Write the step as a function of elapsed time, not “one fire = one step”:

/* GOOD — integrates dt; correct at 250 ms (live) or 10 h (catch-up). */
static void heat_decay(uint32_t dt_ms, void *ctx) {
Heat *h = ctx;
uint32_t decay = (uint32_t)((uint64_t)h->rate_per_hour * dt_ms / 3600000u);
h->level = (decay >= h->level) ? 0 : h->level - decay;
}
/* BAD — assumes one fire == one step; loses all but one step on catch-up. */
static void heat_decay_wrong(uint32_t dt_ms, void *ctx) {
(void)dt_ms; ((Heat *)ctx)->level -= 1;
}

Then the short list: run at a coarse cadence (1–4 Hz, not the frame rate); no malloc, return promptly; a callback may register/cancel entries (including itself) but must not re-enter sched_tick/sched_catchup; and route any UDS/economy write through the sanctioned deck_set_* mutators (which publish on the bus and honor the integrity cores), never poke state directly.

  • Foreground hard intrusion (a background system seizing the active screen) and soft world-changes — their own ADR when a consumer needs them.
  • A cart-facing FFI. v1 is nOSh-internal.