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.
The model
Section titled “The model”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.
Authoring rule
Section titled “Authoring rule”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.
Deferred (not built here)
Section titled “Deferred (not built here)”- 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.