Ephemeral Compute

Ephemeral compute is the property that makes a Psyclone AIOS system something other than a fixed pipeline: a component does not have to exist for the lifetime of the system. Most processing modules are one-shot — a trigger fires, the module’s crank function borrows a thread from the pool, does its work, posts a result and returns the thread. Components can also be created after start-up that were never in the PsySpec at all, baked into the live system by a Builder. If you are evaluating whether a runtime can grow, shrink and reshape itself while a real-time control loop keeps running, this is the mechanism to look at.

A one-shot module borrows a pool thread, posts its result and releases it; private data persists.
A one-shot module borrows a pool thread, posts its result and releases it; private data persists.

Why it matters

The failure mode this removes is the restart. In a conventional service graph, adding a processing stage, swapping an algorithm or standing up a test harness means editing configuration, rebuilding and restarting the process — and in a real-time system, a restart is a loss of state, a loss of calibration and a gap in the control loop. Psyclone separates what exists from what is running: components are created and destroyed against a live message bus, and the bus does not care who is listening at any given instant.

The second consequence is cost. A thread per component is the naive design, and it does not scale: threads are expensive, and hundreds of mostly-idle ones are worse than useless. Because a one-shot crank holds its thread only for the duration of one message, a system can declare far more components than it has cores, and pay only for the ones actually processing. Psyclone maintains the spare-thread pool automatically in both execution styles, so the choice between them is a code-shape decision rather than a capacity-planning exercise.

Third, it makes fault isolation cheap. Components are grouped into spaces, and a space is an OS process; if a space crashes, the owning node restarts it without taking the system down. External modules — code living in your own executable, linked against CMSDK and attached to the running system — are tolerated the same way: the process can die, be restarted and reattach. The business consequence is deployment risk: adding instrumentation, a recorder or a replacement analysis module to a running system, then removing it again, shortens the loop between “we think this is the problem” and “we have measured the problem” without a maintenance window.

How it works

Every component in Psyclone has at least one crank — a function called when a trigger fires. The same declaration supports two execution styles, and which one you get is decided entirely by how the crank is written. A one-shot crank runs once per trigger, returns, and its thread goes back to the pool; it is the default choice and ideal for stateless processing. A continuous crank never returns while the system runs: it loops on while (api->shouldContinue()) with a timeout on waitForNewMessage(…) so it can also do periodic work each cycle. Continuous cranks own their thread, which pays for itself when initialisation is heavy or in-memory state is large, but many of them mean many threads and a possible performance cost.

Because a one-shot crank borrows a pool thread per trigger firing, the same crank function can be executing on several threads at once for different messages. This is the single most important thing to internalise: do not assume one-at-a-time execution, and guard any shared or static state — including read-modify-write on private data — against concurrent access.

State that must outlive an invocation goes into private data: any component can save a binary blob to persistent storage with setPrivateData and read it back with getPrivateDataCopy. That is what lets a one-shot module keep state between invocations. Supply a mimetype and the blob appears in the component’s Data tab in PsyProbe.

The reason components can appear and disappear without rewiring anything is the indirection between names and message types. Crank code refers only to trigger and post names; the PsySpec binds those names to dot-notation message types. A <post> publishes without knowing who receives it, subscriptions match exactly or by wildcard, and messages nobody subscribes to are simply discarded unless the post sets a ttl. A component that vanishes therefore causes no error at the sender; posting returns the number of messages delivered, and code that cares can check POST_NOSPEC or POST_OUTOFCONTEXT.

Name indirection: crank code names channels, the PsySpec binds them to message types.
Name indirection: crank code names channels, the PsySpec binds them to message types.

Creating components at runtime is the job of the node-local Builder, shipped in 2.2. Every node has one. You hand it a recipe — the same PsySpec XML grammar, parsed by the same code path, including <include> expansion and %variable% substitution — and it bakes it step by step while the node is live, creating components and running <cli> steps. Progress comes back over the ordinary message bus on two correlated streams keyed by a RecipeID: Psyclone.Builder.Bake.Status carries percentages, summaries and the terminal outcome, while Psyclone.Builder.Bake.Data carries the step output, lossless and in order — so a dashboard can subscribe to the first and never touch the second. A Supervisor decides what to bake and can steer a bake in flight. Normal start-up already runs through the built-in Startup Supervisor, so this is shipped machinery you are using whether or not you drive it yourself.

How to use it

A recipe is a PsySpec fragment submitted to a live Builder. This one adds a capture module on a named edge node and runs a warm-up step:

<psyspec name="AddCamera">
  <module name="Camera7" node="Edge3">
    <crank name="CameraCapture" />
  </module>
  <cli name="Warmup" node="Edge3" retryable="yes">./warmup.sh Camera7</cli>
</psyspec>

Node pinning matters here. A step with no node= only runs on a config node. When a Supervisor dispatches a recipe to a specific node it stamps each step with that node’s name for you — but a hand-written recipe for a remote node must pin every step with node="…" or the step is silently skipped.

  1. Decide the execution style per component. Write a plain one-shot crank unless the component has heavy initialisation or large persistent in-memory state; only then keep the thread with a shouldContinue() loop.
  2. Make one-shot cranks thread-safe. Assume concurrent invocations of the same function and protect shared state; push anything that must survive an invocation into setPrivateData.
  3. Place components in spaces deliberately: group those that can share fate, isolate the crash-prone ones, and remember a space costs a process and IPC.
  4. To create components at runtime, write a Supervisor as an ordinary continuous crank module that owns a Supervisor object: bind it with initForCrank(node, name), dispatch with startup(targetNode, recipeXML), pump every triggered message into processMessage(msg), and check bakeSucceeded(rid). Declare it as a normal <module> with triggers on Psyclone.Builder.Bake.* — the bare <supervisor> tag is parsed and ignored today.
  5. Steer or abort with sendControl(recipeID, "pause"|"start"|"cancel"|"skip"|"retry"). Verbs apply at the next step boundary; cancel is immediate, even mid-<cli>. A read-only dashboard is just a component subscribing to the same streams.

When to use it / when not

Use it forCaveat or limit
Stateless, high-fan-out processing: one-shot cranks that scale with message rate rather than with declared component countThe same crank runs concurrently on several pool threads. Static and shared state must be guarded; there is no implicit serialisation.
Adding instrumentation, recorders or a replacement analysis stage to a running systemThere is no dry-run or spec validate mode today, and the parser is permissive — unknown or mistyped attributes are silently ignored. Rehearse recipes in staging.
Test harnesses and simulation runs built on demand, then torn downSupervisors are plain code. The LLM tier that would author or regenerate module code is roadmap, not shipped — supervised, not autonomous.
Components hosted in your own process via an external space, attached with PsySpace::connect(sysid)The <executable> auto-start element is parsed and stored but not launched (roadmap, in progress). Start external processes yourself, and monitor with isConnected() / hasShutdown().
Cross-node bring-up, where a Supervisor on one node dispatches recipes to another node’s BuilderDispatch works; getting the reverse Status/Data streams back across a node boundary relies on subscription sync and is verified single-node while multi-node hardening continues. Cohort messaging and the system-status API are not built.
Long-lived components with expensive setup — a device driver, a large model in memoryMake these continuous and accept the thread cost. Do not use one-shot cranks to hide re-initialisation work.
Moving a component between machinesThe migration attribute is parsed but not yet acted on; runtime migration is a stub. Plan placement in the spec instead.

Read the docs

  • User Guide 4 — Modules: cranks and libraries, the one-shot versus continuous decision, the concurrency warning, private data, parameters, <setup>, passthrough modules, Python cranks and external modules.
  • User Guide 3 — Core Concepts: the PsySpec, the three component kinds, the publish/subscribe bus, the POST_* return codes and what happens between start-up and Psyclone.Ready.
  • User Guide — Builders & Supervisors: recipes, the two-stream .status/.data protocol, control verbs, and a worked custom Supervisor crank with its honest limits.
  • User Guide 8 — Catalogs: the query/reply interaction, the built-in File, Data, Replay and Request Store catalogs, and how to write your own.
  • System Guide 11 — Roadmap Features: the Shipped / Roadmap / Stub status table. Read this before assuming a tag does something — several are parsed-and-ignored by design.
  • System Guide 12 — Running in Production: space and node restart semantics, the shared-memory ABI rule that forces all nodes onto one build, and the known operability gaps. API signatures are in the CMSDK reference.

Ephemeral components only make sense alongside the machinery that keeps the dataflow stable while they come and go. See Whiteboards & Dataflow for the typed bus and shared short-term memory a departing module leaves its results in, Builders & Supervisor for the full runtime-bake protocol, and Global Contexts for switching whole sets of active components at once. The overview is on the Psyclone AIOS page.