On this page
The SDK owns the reusable native dispatcher and framed stdio transport. The composing application or native host supplies its identity, version and services. Product prompts, model selections, filesystem locations, host privileges and application policy remain with that owner.
import {createCoreRuntime} from 'arcane-os/core/runtime';
import {startCoreStdio} from 'arcane-os/core/stdio';
import catalogStore from './catalog-store.mjs';
const runtime = createCoreRuntime({
application: {id: 'moon-cheese-catalog', name: 'Moon Cheese Catalog'},
version: '1.0.0',
services: [{
name: 'catalog',
methods: {
'catalog.save': {
lifetime: 'service',
async handle(record) {
// catalogStore is supplied by this application's native owner.
return catalogStore.save(record);
}
}
},
async drain() {
await catalogStore.flush();
},
async dispose() {
await catalogStore.close();
}
}]
});
const transport = startCoreStdio({runtime});
await transport.closed;
The example's store is an application dependency, not an SDK-created database.
The runtime passes each request's parameters to its registered handler
unchanged. It preserves complete results and diagnostic errors; it neither
selects product data nor supplies replacement results.
Registration and startup
createCoreRuntime({application, version, services}) creates a dispatcher.
Supply the application's real descriptor and host release version. The built-in
app.current and version.current methods return those values; system.ping
returns {ok:true}. A ping establishes dispatcher responsiveness, not model,
service or application readiness.
runtime.registerService({name, methods, start, drain, dispose}) registers one
native service. Method names are complete RPC names, such as catalog.save.
A method is either a handler function or {handle, lifetime}. A handler function
has request lifetime by default. Duplicate service or method names, including
the three built-in methods, are reported rather than replacing another owner.
Registration is a native composition API, not a browser command.
Registration returns {ready, current}. Reading ready starts that service and
returns its shared startup promise; current() exposes its state and actual
error. runtime.start() starts all registered services independently and returns
the current dispatcher state without waiting for them. A service registered
after startup starts independently as well. A method waits only for its own
service's startup before invoking its handler. State errors use the shared
complete diagnostic serialization; the startup promise retains its original
rejection value.
Each lifecycle hook receives {application, service, emit, getService}. Each request handler
receives (parameters, {application, service, emit, getService, requestId, clientRequestId, coreRequestId, signal}), with
this bound to its service definition. A hook or handler may publish a complete
service-owned event through emit(event, data).
A request handler's emit places its owning request ID in the event frame's
optional top-level requestId, leaving data unchanged. Multi-connection hosts
can route those frames to the requesting connection even when different clients
choose the same application streamId. Lifecycle-hook emit and public
runtime.emit remain uncorrelated service-wide events. Retain that service
context when an application intentionally publishes a change to every client;
request-context events belong to the originating request. The ordinary Core
client still delivers the complete original event data to its listeners.
Native service composition
await runtime.getService(name) and await context.getService(name) return the
actual registered service definition after that service's shared startup
promise. A lookup can start its selected dependency before runtime.start();
it waits for that dependency alone, with no renderer, transport request or
all-service readiness barrier. Concurrent lookups reuse the same startup and
return the same object. Applications keep their service dependencies acyclic.
Use the returned service's documented native members, such as
getONNXRuntime(), for same-process
composition. RPC handlers remain owned by the dispatcher; calling entries in
service.methods directly does not supply request tracking or cancellation.
No service lookup is exposed over the browser protocol.
An unregistered name rejects with CORE_SERVICE_UNAVAILABLE. Startup failure
rejects with the original error. New lookups reject with CORE_CLOSING once
shutdown begins; an already accepted lookup still settles with its startup.
Lookup provides readiness, not a lifetime lease or ownership transfer. Retain
each acquired native owner according to its own documented cleanup contract,
and use that retained handle during disposal rather than requesting a new one.
Independent services still dispose concurrently.
State and frames
runtime.current() returns dispatcher state, application, version, per-service
state/errors and active request identities, methods and lifetimes.
runtime.subscribe(listener, {emitCurrent:true, signal}) immediately replays the
current state and then observes changes through the canonical SDK event owner.
It returns an unsubscribe function. runtime.onFrame(listener, {signal})
observes outgoing protocol frames and also returns an unsubscribe function.
Attach transport listeners before starting the runtime to observe initial frames.
The shared arcane-os/core/contracts module owns CORE_PROTOCOL (arcane/1),
CoreError and serializeCoreError. Requests are:
const result = {
protocol: 'arcane/1',
type: 'request',
id: 'request-1',
method: 'catalog.save',
parameters: {name: 'Lunar cheddar', notes: 'Keep the complete supplied text.'}
};
await runtime.handle(frame) returns and emits the correlated response:
{protocol, type:'response', id, ok:true, result, time} or
{protocol, type:'response', id, ok:false, error, time}. Framing and method
availability errors remain observable; unavailable services are not replaced
with browser behavior.
A transport that assigns internal dispatch IDs may call
runtime.handle(frame, {contextRequestId, contextClientRequestId}). Each supplied
metadata value must be a nonempty string. Handler identities are:
| Field | Value |
|---|---|
requestId |
contextRequestId ?? frame.id, preserving the transport's established handler contract |
clientRequestId |
contextClientRequestId ?? contextRequestId ?? frame.id, exposing the actual incoming client identity |
coreRequestId |
frame.id, the actual internal dispatch identity |
Ordinary handle(frame) exposes all three equal to frame.id. Existing callers
that supply only contextRequestId also receive that value as clientRequestId.
Stdio and both IPC listener forms always supply the actual client ID separately.
Stdio and the borrowed listener preserve it as requestId too; the owning
shared-host adapter retains its established internal requestId contract.
The supplied frame.id still owns active-request state, cancellation lookup,
response correlation and the request event's top-level envelope. It distinguishes
simultaneous requests whose clients selected equal IDs. Parameters, results and
event data remain unchanged; these identity fields are separate context metadata.
A request with no registered handler retains code METHOD_NOT_ALLOWED and
adds the exact method, its first dotted namespace, and a reason.
core-namespace-unavailable means the dispatcher has neither a service with
that name nor any registered method in that namespace. Otherwise the reason is
core-method-unavailable; the built-in system, app, and version
namespaces also count as present. This is a dispatch lookup result before any
handler or permission decision, not a capability-discovery call. A read-only
or partially registered service therefore never appears wholly absent.
Handler/startup/permission errors remain unchanged and acquire none of these
lookup markers. Generic or older METHOD_NOT_ALLOWED errors do not establish
namespace absence. An optional browser adapter may recognize the exact absent
namespace and method according to its own documented contract; the dispatcher
does not select or create another implementation.
Events have {protocol, type:'event', event, data, time}:
core.ready: the dispatcher is connected; service/model readiness is separate.core.state: the current complete runtime state.core.service.state: a service's name, state and serialized error when present.core.error: a lifecycle or transport diagnostic.
A newly connected document may send
{protocol:'arcane/1', type:'control', control:'runtime.replay'}. The runtime
publishes one current snapshot: core.ready only when the dispatcher is ready,
then core.state, followed by core.service.state for every registered service.
Complete service errors and active request state are retained. Replay does not
start or restart services, dispatch a request, poll readiness or create a second
lifecycle owner. The runtime completes that snapshot before publishing lifecycle
changes triggered reentrantly by its listeners. A draining runtime reports its
draining state rather than announcing readiness.
Service states are registered, starting, ready, failed, draining and
closed. Dispatcher states are created, ready, draining and closed.
Complete diagnostics belong in the host's developer-facing error handling, not
in ordinary chat history or a fabricated conversation turn.
Cancellation and shutdown
The request.cancel control names requestId; requests.cancelAll applies to
active request-lifetime operations. Cancellation is cooperative. The handler's
signal is aborted. An otherwise successful cancelled operation returns
REQUEST_ABORTED after its owned work settles; a handler's own rejection remains
the actual reported error. If startup is still pending, cancellation is checked before
the handler runs. A service that does not finish or respond to cancellation is
still unfinished; the SDK does not report a forced timeout as successful cleanup.
Use lifetime:'service' for accepted work, such as a save, that must survive a
renderer cancellation or disconnect. The dispatcher does not abort these
operations through request cancellation controls or its own shutdown. The
service still owns its persistence, queueing and failure semantics. A composing
host or native service can abort its own lifetime signal during shutdown;
in-flight native inference can therefore be cancelled even when the dispatcher
retains a service-lifetime response. Service lifetime does not promise survival
of native host shutdown.
runtime.close() is idempotent and retains the same completion promise. It stops
accepting new requests, cancels request-lifetime operations, waits for accepted
responses, then drains and disposes services. Independent services close
concurrently; each service's startup, drain and dispose remain ordered.
Shutdown errors remain complete and reject the close promise. A failed drain
does not prevent the service's dispose hook from running.
The final closed state is published before event subscriptions are released.
current() remains available afterward; the disposed runtime is not restarted.
The host must observe completion before terminating Core. The SDK does not call
process.exit(), kill a host or claim durability after an operating-system crash
or a host-forced termination.
Stdio transport
startCoreStdio({runtime, input, output, onError}) attaches a runtime and starts
it. Streams default to process.stdin and process.stdout; diagnostics default
to console.error, leaving stdout for protocol frames. It returns
{runtime, closed, close}. Observe closed for final completion or failure.
Frames use the existing Content-Length header followed by UTF-8 JSON. Length
is transport-local framing only. encodeCoreFrame(frame) and
createCoreFrameDecoder(onFrame) are also exported for host adapters. The
decoder accepts fragmented or multiple frames through push(chunk);
finish() reports an incomplete final frame rather than dropping it.
Requests dispatch concurrently. Outgoing frames preserve their queued order and
wait for each writable-stream callback. Input EOF starts runtime shutdown, waits
for accepted service work and queued output, and resolves closed afterward.
An input stream closed without its normal EOF event also initiates this drain;
an already ended or destroyed input is handled when attached. Transport failures
reach onError and reject closed. The transport detaches
its listeners and pauses its input; it does not end caller-owned output streams
or take ownership of the host process.
Stdio routes responses and request-context events only for its own requests.
request.cancel, requests.cancelAll and runtime.replay are connection-scoped;
they do not cancel or replay another attached connection's work. The protocol
owner gives every accepted request a fresh internal ID and restores its client
ID on responses, event envelopes and stdio's own core.state entries. Handler
context.requestId and complete application payloads keep their original values.
Uncorrelated runtime/service events remain shared.
startCoreListener({runtime,endpoint,onError}) from arcane-os/core/host can
attach additional local clients to this same runtime. Its explicit close()
closes only that listener; stdio EOF still closes the runtime and drains all
accepted service work, then the listener closes automatically. Observe the
listener's closed as well as the stdio owner's closed to join both transport
lifecycles. See existing-runtime listeners.
Platform and delivery boundary
These modules use portable Node and JavaScript facilities for Windows, Linux and macOS native hosts. Android requires its native transport/host adaptation; the module is not a Node runtime embedded inside a WebView. Browser callers use the separate Core client.
The dispatcher/transport sources are one part of Core extraction. They do not by themselves replace existing native host bundles, platform adapters, provider loading, packaging or OS composition. Those owners must adopt the selected published SDK boundary and preserve their real shutdown and service lifecycles.