Hierarchical global contexts are Psyclone AIOS’s mechanism for making a whole system change behaviour at once. Every subscription a module holds can be declared inside a named context — SoB.Alive.Awake, Scene.Dark, System.Ok — and only the subscriptions belonging to the currently active branch of the context tree are live. Switching context is a single posted message on the same sub-100µs message bus that carries everything else, so an entire pipeline can be rerouted, muted or woken in near real time without a restart, without a code change, and without a mode variable anywhere in your application logic. If you build systems that have modes — startup, normal running, degraded, error recovery, task phases, bright scene versus dark scene — this is the feature that keeps that complexity out of your code.
Why it matters
The failure mode contexts remove is the hand-rolled mode flag. In a conventional system, “behave differently when it’s dark” becomes an if in a vision module, another if in the tracker, a boolean shared through a config service, and a slow, subtle divergence between what the code does and what anyone believes it does. Nobody can answer “which code paths are live right now?” without reading every module. Under contexts, that answer is mechanical: read the PsySpec, read the active context, and you know exactly which triggers, cranks and posts are running.
The engineering consequence is that mode logic becomes declarative and inspectable rather than distributed and emergent. Behaviour variants live side by side in the spec as separate <context> blocks within the same module, sharing the module’s private data, so switching algorithms does not mean losing accumulated state. Adding a new operating mode is a spec edit and a new crank — not a refactor of every consumer downstream.
The business consequence is change velocity in systems that are expensive to redeploy. A robot, a vehicle, an industrial cell or a real-time voice AI system typically cannot be stopped to try a different processing strategy. Because a context switch is just a message, an operator action, a supervising module or a test harness can reconfigure the live dataflow of a running deployment, then switch back. Recovery behaviour, safe-mode degradation and A/B comparison of two algorithms all become configuration rather than new builds.
How it works
Contexts are hierarchical, dot-notation trees. SoB.[Alive, Dead] and SoB.Alive.[Awake, Asleep] describe one root with two levels of branching, and several roots can co-exist in the same system — for example an SoB.* tree describing an agent’s state alongside a System.Ok tree describing health. Anything you declare without an explicit context lives in the default context Psyclone.Ready, which is active for as long as the system runs. That is the same name as the message posted at start-up, and it is why a plain system with no contexts at all still behaves consistently: it is simply a system with one permanently active context.
Inside a module, a <context name="..."> block groups child <trigger>, <post>, <crank>, <retrieve>, <query> and <signal> elements. One module may hold several such blocks, each with a different crank, and all of them share the module’s private data.
Switching is done by a flag on a post: <post name="done" context="SoB.Alive.Asleep" />. Posting a context deactivates the sibling branches of the same root — here SoB.Dead, SoB.Alive.Awake and deeper branches such as SoB.Alive.Asleep.Snoring — while the ancestors SoB and SoB.Alive stay active. The switch only takes effect when a current branch is genuinely replaced; posting an already-active ancestor does nothing. A module can also react to the transition itself with <trigger name="t1" context="SoB.Alive.Asleep" />, which fires on the change (internally forcing the message type CTRL_CONTEXT_CHANGE) — the natural place for initialisation, cleanup or announcing state.
The context check sits in the delivery pipeline itself. A posted message must pass type matching (exact or wildcard), then the active-context check, then every trigger filter — maxage, from, to, tag and any <filter> children — before a crank is invoked at all. Rejected messages are discarded with no wake-up, which is why context switching costs nothing at steady state: out-of-context modules are not woken and then told to go back to sleep, they are simply never dispatched.
Work already in flight is handled explicitly rather than being killed. When the context switches away underneath a running crank, api->shouldContinue() returns false, so continuous cranks exit their loop cleanly, and a post attempted from an out-of-context crank returns POST_OUTOFCONTEXT (-3). Internal modules automatically run the crank belonging to the active context; external modules check for themselves with getCurrentTriggerContext() and contextToText(). Signals are deliberately outside this system: they bypass subscription checking entirely and are independent of contexts, which is what makes them usable as a clock in simulations.
How to use it
The whole mechanism is declarative. Below, a tracker runs a fast algorithm in a bright scene and an infra-red algorithm in a dark one; a separate scene classifier flips the system between them with a single post, and the tracker gets a chance to reinitialise on the transition.
<module name="Tracker">
<context name="Scene.Bright">
<trigger name="Input" type="input.video.raw" maxage="40" />
<crank name="fast" function="Vision::trackFast" />
<post name="Output" type="vision.track.result" />
</context>
<context name="Scene.Dark">
<trigger name="Input" type="input.video.ir" maxage="40" />
<crank name="ir" function="Vision::trackIR" />
<post name="Output" type="vision.track.result" />
</context>
<trigger name="OnDark" context="Scene.Dark" />
<crank name="reinit" function="Vision::reinit" />
</module>
<module name="SceneClassifier">
<trigger name="Frame" type="input.video.raw" interval="500" />
<crank name="classify" function="Vision::classifyScene" />
<post name="GoDark" context="Scene.Dark" />
<post name="GoBright" context="Scene.Bright" />
</module>
In practice the steps are:
- Name your context tree before writing code. Decide the roots (one per orthogonal concern) and the branch names, and keep the trees shallow enough to read.
- Move the per-mode subscriptions of each module into
<context>blocks, leaving genuinely mode-independent triggers outside them inPsyclone.Ready. - Decide which component owns each switch, and give it a
<post>with acontext=attribute per destination branch. - Add
context=triggers wherever a module needs to initialise, flush or announce on entry to a mode. - Make every continuous crank honour
while (api->shouldContinue()), saving state and returning promptly rather than treating exit as an error. - Watch it live in PsyProbe: confirm the intended branch is active and that no module is still being dispatched in a context you thought you had left.
When to use it / when not
- Use for system-wide modes. Startup and shutdown sequencing, degraded and safe modes, error recovery, task phases, sleep/wake of whole subsystems, and swapping algorithm families for the same input.
- Use for observability. Any time “which paths are live?” is a question you expect to ask during an incident, contexts make the answer readable from the spec plus one piece of runtime state.
- Do not use for per-message routing. Choosing between handlers on the content of an individual message is a job for message types, wildcards, tags and
<filter>children — not for a context switch, which is global by design. - Do not use for high-frequency toggling. A context change is a real system-wide reroute; flipping it many times a second fights the mechanism instead of using it. Prefer parameters or filters for fine-grained tuning.
- Mind in-flight work. Cranks that ignore
shouldContinue()will keep running out of context and see their posts rejected withPOST_OUTOFCONTEXT. External modules get no automatic crank selection and must inspectgetCurrentTriggerContext()themselves. - Signals are exempt. Because signals bypass subscription checking and are independent of contexts, you cannot mute a signal-driven path by switching context — design simulation clocks accordingly.
- Supervision is deterministic, not autonomous. Context switches are decided by components you write, or by the shipped Startup Supervisor’s deterministic bring-up orchestration. LLM-authored restructuring of a running system remains roadmap.
Read the docs
- User Guide 6 — Contexts: the full treatment — the context tree, switching by post, context triggers, and correct behaviour when a crank finds itself out of context.
- User Guide 3 — Core Concepts: the PsySpec, the three component kinds, the publish/subscribe bus, and why
Psyclone.Readyis both the start-up message and the default context. - User Guide 5 — Messaging & Signals: the delivery pipeline in which the context check sits, plus filters, tagging, timing attributes and guaranteed versus best-effort delivery.
- User Guide 13 — PsySpec XML Reference: element-by-element attribute tables with honest shipped / stub / roadmap status, including
<context>and thecontext=attribute on triggers and posts. - User Guide 12 — PsyProbe: inspecting a live system, which is how you verify at runtime which context branch is actually active.
- CMSDK API reference: the component-side API, including
shouldContinue(),getCurrentTriggerContext()and the post return codes.
Contexts are one half of the story; the other half is what the rerouted dataflow actually carries. See Whiteboards & dataflow for the shared-memory and retrieval side of the bus, ephemeral compute for components that come and go under a running spec, and Builders & the Startup Supervisor for deterministic bring-up orchestration of the components a context switch will later steer. All of these sit under Psyclone AIOS.
