openmixer — generated API reference
    Preparing search index...

    Interface MixerServerOptions

    Options for MixerServer. Either supply an adapter or let it be loaded from config.

    interface MixerServerOptions {
        adapter?: MixerAdapter;
        adapters?: {
            configPath: string;
            registrations?: readonly AdapterTypeRegistration[];
            watchPresence?: boolean | { intervalMs?: number; retryBackoffMs?: number };
        };
        align?: { hz?: number; source: AlignPairSource };
        autosaveDebounceMs?: number;
        catalog?: Catalog;
        clipCapturePollMs?: number;
        clock?: ClockControl;
        clockPollMs?: number;
        config: {
            adapter: "x32" | "midas" | "roland" | "software";
            device: { host: string; options: Record<string, unknown>; port?: number };
            rig: {
                catalog?: string;
                console?: string;
                demoSources?: boolean;
                gig?: string;
                preset?: "bare" | "demo" | "console";
            };
            web: { host: string; manualDir?: string; port: number };
        };
        configuredBoxModel?: () => Promise<string | undefined>;
        configuredHardware?: () => Promise<ConfiguredHardware>;
        convergence?: { heartbeatMs?: number };
        coreLostPollMs?: number;
        coreLostProbe?: () => boolean;
        deviceProfileDoor?: DeviceProfileDoor;
        deviceRateProbe?: (
            references: readonly ClockReference[],
        ) => Promise<readonly DeviceRateReading[]>;
        discovery?: { autoStart?: boolean; provider: DiscoveryProvider };
        followerRateProbe?: () => Promise<readonly FollowerDeviceReading[]>;
        graphSource?: GraphSource;
        insertSuitability?: InsertSuitabilityPolicy;
        io?: IoEnumerator;
        journalBound?: number;
        logger?: boolean;
        manualSiteCandidates?: readonly string[];
        measurement?: {
            catalogPath?: string;
            nodeBin?: string;
            spawn?: MeasurementSpawn;
            toolPath?: string;
        };
        nativeLinkDoor?: NativeLinkDoor;
        network?: { settingsFile?: string; sources?: NetworkValueSources };
        nodeXrunProbe?: () => Promise<readonly NodeXrunCount[]>;
        now?: () => number;
        onCoreLost?: () => void;
        patchbay?: {
            appStreams?: "strip" | "park";
            exclusive?: boolean;
            hotplug?: boolean;
            hotplugMs?: number;
        };
        pdc?: PdcControl;
        perf?: { enabled?: boolean };
        persistence?: {
            channelConfigsDir?: string;
            dir?: string;
            patchesDir?: string;
            scenesDir?: string;
            sessionsDir?: string;
            settingsFile?: string;
            shippedSessions?: boolean;
        };
        plugins?: {
            catalog: Catalog;
            host: PluginHost;
            insertPointFor?: InsertPointResolver;
            portConnector?: PortConnector;
        };
        quantum?: QuantumControl;
        reacScenePollMs?: number;
        reacSceneSettleReplayMs?: readonly number[];
        realHardwareDefaults?: boolean;
        record?: {
            laneOf: (channel: ChannelId) => number | undefined;
            node: () => RecordNode | undefined;
        };
        resources?: {
            capability?: ChannelCapabilityReader;
            chain?: ResourceHandle<ChainOrder>;
            chainProcessor?: ResourceHandle<ChainProcessorState>;
            channels?: () => readonly ChannelId[];
            dcaMembers?: ResourceHandle<DcaMembers>;
            directPath?: ResourceHandle<SourceDirectPaths>;
            headAmp?: ResourceHandle<HeadAmpRowOrAbsent>;
            headAmpKnownFields?: (
                id: ResourceId,
            ) => ReadonlySet<keyof HeadAmp> | undefined;
            headAmpPort?: (id: ResourceId) => string | undefined;
            headAmpResource?: HeadAmpResource;
            matrixPoint?: ResourceHandle<MatrixPointRowState>;
            migrateHeadAmpPort?: (
                from: SourcePatch,
                to: SourcePatch,
                capability: HeadAmpCapability,
            ) => boolean;
            mixMinus?: ResourceHandle<MixMinusExcluded>;
            muteGroupActive?: ResourceHandle<MuteGroupActiveState>;
            muteGroupMembers?: ResourceHandle<MuteGroupMembers>;
            nativeReadback?: NativeReadbackSource;
            patchInput?: ResourceHandle<PatchInputState>;
            patchOutput?: ResourceHandle<PatchOutputState>;
            patchOutputLeg?: ResourceHandle<PatchOutputLegState>;
            prefix?: string;
            profile?: () => ConsoleProfile;
            projectHeadAmpPort?: (
                patch: SourcePatch,
                fields?: ReadonlySet<keyof HeadAmp>,
            ) => ProjectionResult;
            reacDesired?: ReacDesiredConfig;
            recallSafeMask?: () => RecallSafeMask;
            refusedRivalMasters?: () => ReadonlyMap<string, RefusedRivalMaster>;
            registry: ResourceRegistry;
            restoreChannelHeadAmp?: (
                id: ResourceId,
                values: Partial<HeadAmp> | undefined,
            ) => void;
            seedHeadAmpPort?: (
                patch: SourcePatch,
                value: HeadAmp,
                capability: HeadAmpCapability,
            ) => boolean;
            send?: ResourceHandle<SendRowState>;
        };
        rmeTotalMix?: RmeTotalMixControl;
        rta?: { hz?: number; source: RtaSource };
        rtLoad?: { headroom?: RtHeadroomConfig; source: RtLoadSource };
        structural?: StructuralDsp;
        talkback?: { generators?: TalkbackGeneratorBackend };
        telemetry?: { hz?: number; source: LatencyReportSource };
        touched?: TouchedStore;
        vsc?: {
            laneOf: (channel: ChannelId) => number | undefined;
            node: () => VscNode | undefined;
        };
        writeDriverPolicyDropIns?: boolean;
        xrunPollMs?: number;
        xrunProbe?: () => Promise<XrunProbeResult>;
    }
    Index
    adapter?: MixerAdapter

    Pre-built adapter (skips loadAdapter). Used by tests to inject the mock and by embedders that already hold an adapter instance.

    adapters?: {
        configPath: string;
        registrations?: readonly AdapterTypeRegistration[];
        watchPresence?: boolean | { intervalMs?: number; retryBackoffMs?: number };
    }

    The declarative adapter framework. When supplied, the server runs an AdapterManager (loads config/adapters.{yaml,json}, instantiates enabled adapters, persists runtime changes) and serves the /adapter* REST entities (adapter-admin-rows.ts) off it.

    Type Declaration

    • ReadonlyconfigPath: string

      Path to the adapter config file.

    • Optional Readonlyregistrations?: readonly AdapterTypeRegistration[]

      Registered adapter types (factory + label + settingsSchema).

    • Optional ReadonlywatchPresence?: boolean | { intervalMs?: number; retryBackoffMs?: number }

      The control-surface presence watchdog (SurfaceWatchdog): probes every probeable entry (~2 s) so an enabled surface auto-connects when its device appears and detaches cleanly when it vanishes. On by default when any registration carries a probe; false disables it, an object tunes it.

    align?: { hz?: number; source: AlignPairSource }

    The SMART ALIGNMENT measurement door (2026-08-31-smart-alignment.md): the PAIRED complex spectra two lanes give up in one snapshot pass. Its own option rather than a field of rta because it is a different reading — complex, of a PAIR, at one tap — even though it shares the analyser's rings and its demand ledger. A console without it still serves the /align row and the panel; the measurement simply refuses no-capture, which is the honest answer and a visible one.

    Type Declaration

    • Optional Readonlyhz?: number

      Measurement rate in Hz. Default 2 (spec §4).

    • Readonlysource: AlignPairSource
    autosaveDebounceMs?: number

    Debounce window (ms) for the live-session autosave (operator #51): a console-mutating commit schedules a write of the live console under the store's reserved live-autosave id; further mutations within the window collapse into the same write (timed from the last one). Also written once, synchronously, on MixerServer.stop so a graceful shutdown never loses the tail of a show. Only relevant when persistence is configured. Default AUTOSAVE_DEBOUNCE_MS.

    catalog?: Catalog

    The console's plugin CATALOG — what it can rack, whether or not anything is attached to rack it with. /plugins and /plugins/{uri} are rows over this, and the rack door's plugins.values narrows it; a console that loads a catalog but has no plugin host still knows what it holds, and says so. plugins supplies it too, for a console whose host and catalog arrive together; this option wins where both are given.

    clipCapturePollMs?: number

    Clip capture poll period (ms): how often the adapter's read-and-clear over flags are read into the /channel/{kind}/{index}/clip latches (2026-08-29-clip-latch.md §6.4). Runs regardless of demand — a latch catches what nobody is metering. Default CLIP_CAPTURE_INTERVAL_MS; 0 disables the poll.

    clock?: ClockControl

    Clock control for the clock.set verb (the config-zone quantum + sample-rate lever). Explicit injection always wins (tests inject a fake so the suite never touches the real system clock). Left unset, this resolves to a REAL PwMetadataClock — which shells out to pw-metadata -n settings 0 clock.force-quantum|force-rate and can re-clock the running graph — only when realHardwareDefaults is true; otherwise it resolves to NULL_CLOCK, which touches nothing.

    clockPollMs?: number

    Live-clock poll period (ms) for the honest-rate reflection (issue #159): every this-often the server re-reads the graph's LIVE clock.rate / quantum via the clock control's ClockControl.refreshLive and re-broadcasts clock.state when it changed — so a rate change made out-of-band (a PipeWire restart into a new persistent default.clock.rate) shows up in the UI without an operator action. Default CLOCK_POLL_INTERVAL_MS; 0 disables the poll (tests that don't want a background timer). The one-shot boot seed (which reads the live rate before the first client connects) runs regardless.

    config: {
        adapter: "x32" | "midas" | "roland" | "software";
        device: { host: string; options: Record<string, unknown>; port?: number };
        rig: {
            catalog?: string;
            console?: string;
            demoSources?: boolean;
            gig?: string;
            preset?: "bare" | "demo" | "console";
        };
        web: { host: string; manualDir?: string; port: number };
    }

    Validated configuration.

    configuredBoxModel?: () => Promise<string | undefined>

    The box model the rig's reac adapter entry PINS (boxModel: s0808), for the reconciliation that says so when the wire disagrees with it.

    A pin is the one thing here that is neither observed nor a preference: it is a stored copy of a fact the box states about itself, and on 2026-08-05 the copy said s1608 while an S-0808 was attached, with nothing anywhere comparing the two. Read once at start; a failed read is "nothing pinned", never a guess. Absent by default, so a server without it simply has nothing to contradict.

    configuredHardware?: () => Promise<ConfiguredHardware>

    What the RIG is configured for, independent of any session — read fresh on each missing-hardware diff (the composition root reads the operator's reac adapter entries, which is where a box expectation is authored).

    This is the seam behind the rig defect: every other expectation is scoped to the RESTORED SESSION, so a box the operator had configured — with reac-pw actively probing for it — went missing in silence whenever the show happened not to patch it. Absent by default: a server without this option keeps the pre-change, session-only behaviour. Must never throw; a failed read reports "nothing configured", never a phantom box.

    convergence?: { heartbeatMs?: number }

    The convergence instrument (task #48): the console-wide revision counter every frame and REST body is stamped with, the /telemetry/convergence heartbeat row published while watched, and the corrections log clients report their heals into. Always on when the console has a resource registry; this option only tunes it.

    Type Declaration

    • Optional ReadonlyheartbeatMs?: number

      Heartbeat publish period, ms. One default: 1000 (~1 Hz). 0 disables the pump (the counter and the rows stay live — only the periodic publish stops).

    coreLostPollMs?: number

    Core-lost watchdog poll period (ms) for engine reconnect-on-disconnect. Every this-often the server asks coreLostProbe whether the shared native PipeWire client has lost its daemon connection; on the first true it fires onCoreLost exactly once (and stops polling). Default CORE_LOST_POLL_INTERVAL_MS (~0.5 s → exit within ~1 s of the daemon going away); 0 disables the watchdog (tests that don't want a background timer, non-PipeWire deployments).

    coreLostProbe?: () => boolean

    The connection-health probe the core-lost watchdog polls. Defaults to the native nativeCoreLost (reads the atomic latched by mix_host.c on a core -EPIPE). Tests inject a fake to drive the watchdog without a real daemon disconnect.

    deviceProfileDoor?: DeviceProfileDoor

    The native device-profile door (amendment 2026-09-15, later the same evening) — the LIVE half of the device-exclusivity policy, beside the drop-in's boot half. Defaults to deviceProfileDoor over loadPwGraph, which answers NO_BACKEND with no addon. Tests inject a fake card so the actuation and its read-back are exercised with no daemon.

    deviceRateProbe?: (
        references: readonly ClockReference[],
    ) => Promise<readonly DeviceRateReading[]>

    The per-device native-rate probe (amendment 2026-09-15): every ALSA device the exclusivity policy currently sees, each with its own rate menu. Defaults to probeDeviceRates. Sampled on the SAME cadence writeDriverPolicyConfigs already regenerates the drop- ins on (device-set change, not a bare timer) — tests inject a fake so a bare MixerServer never shells out.

    discovery?: { autoStart?: boolean; provider: DiscoveryProvider }

    Network discovery. Requires adapters (so addAsAdapter has a manager). The provider is consumed through the core DiscoveryProvider interface so the server builds before @freemixer/discovery lands.

    Type Declaration

    • Optional ReadonlyautoStart?: boolean

      Begin scanning on start. Default false (UI triggers discovery.scan).

    • Readonlyprovider: DiscoveryProvider
    followerRateProbe?: () => Promise<readonly FollowerDeviceReading[]>

    The follower-rate probe (2026-09-14): every actively-running sink's own native rate set. Defaults to probeFollowerRates. Sampled on the SAME cadence as xrunProbe — tests inject a fake so the suite never touches the real system graph.

    graphSource?: GraphSource

    The graph-patchbay GraphSource behind the REST /patchbay/* routes the web-ui Graph tab reads. Defaults to a PipeWireGraphSource (live pw-dump / pw-link), constructed lazily on first request so a host without PipeWire pays nothing at startup. Tests inject a fake to avoid shelling out.

    insertSuitability?: InsertSuitabilityPolicy

    Role-aware insert-suitability configuration (issue #339) — the per-role latency budgets, their enforcement, and any per-bus role/budget override. Unset roles fall back to the documented defaults in @freemixer/core (DEFAULT_LATENCY_BUDGET_MS), so a deployment that says nothing still gets a monitor bus that refuses what would ruin it.

    Optional live-I/O enumerator for the patchbay (patchbay.list). Defaults to a PipeWireGraphProvider (a pw-dump graph read: every audio node — external apps, DAWs, ALSA devices — with friendly names) whenever a structural surface is present. Tests inject a fake to avoid shelling out.

    journalBound?: number

    How many gestures the undo JOURNAL holds before the oldest drops off the bottom. Default JOURNAL_BOUND (512, argued there). Configurable because a bound is a judgement about a show's length, and because an arm that proves the ring is bounded must be able to reach the bound without making 512 gestures.

    logger?: boolean

    Inject a logger setting; defaults to Fastify's default logger off.

    manualSiteCandidates?: readonly string[]

    Where to look for the prerendered documentation site, in order. Defaults to defaultManualSiteCandidates — the packaged install, then the workspace build. A layout seam, not a knob: it is what lets a test exercise the INSTALLED branch of the derivation without a real /usr/share/openmixer/manual on the machine.

    measurement?: {
        catalogPath?: string;
        nodeBin?: string;
        spawn?: MeasurementSpawn;
        toolPath?: string;
    }

    The local plugin analysis (issue #342). Every field is a seam for tests or an unusual layout; a normal deployment sets none of them and the paths resolve from the installed @freemixer/catalog package.

    Type Declaration

    • Optional ReadonlycatalogPath?: string

      The shipped catalog to merge under local measurements. Default: the package's.

    • Optional ReadonlynodeBin?: string

      The Node binary that runs it. Default: this process's.

    • Optional Readonlyspawn?: MeasurementSpawn

      Subprocess seam, so tests can drive a run without spawning anything.

    • Optional ReadonlytoolPath?: string

      lv2-measure.mjs. Default: next to the installed catalog package's data/.

    nativeLinkDoor?: NativeLinkDoor

    The native door /patchbay/link and /patchbay/unlink dispatch through (patchbay-native-link.ts, nativeLinkDoor). Defaults to the real @freemixer/pipewire-native client. Tests inject a fake so the resolve/create/destroy seam is exercised with no daemon — a test that touches the real one belongs in that module's own private-daemon suite, never a plain unit test (lane-discipline §4).

    network?: { settingsFile?: string; sources?: NetworkValueSources }

    Runtime-network settings surface (the Setup menu's Network panel): where config.set persists overrides, plus the per-field provenance the boot-time resolver computed (network-config.ts). Without a settingsFile (and no persistence dir to derive one from) config.set is rejected; config.get always works.

    Type Declaration

    • Optional ReadonlysettingsFile?: string

      The network-settings JSON file (default <persistence.dir>/network-settings.json).

    • Optional Readonlysources?: NetworkValueSources

      Which layer won each field at boot (persisted/cli/env/default), for the Setup panel.

    nodeXrunProbe?: () => Promise<readonly NodeXrunCount[]>

    The per-node xrun probe (2026-09-14): pw-top's ERR column, kept per node rather than summed. Defaults to probeNodeXruns. Sampled on the same cadence as xrunProbe.

    now?: () => number

    The clock the server reads for timestamps that BECOME STATE — today the tap-tempo detector, whose taps are differenced into a tempo the console then holds. Defaults to Date.now; the six-site convention (telemetry-rows.ts, rta-rows.ts, clock-drift-probe.ts, core's convergence.ts) is the same shape.

    Time that MEASURES is deliberately not routed through here: performance.now() around a request, the perf histograms. Those observe the machine and should keep observing the real one. Only time whose value the console goes on to SERVE needs a seam, and it needs one because otherwise the test of a derivation is a test of the scheduler — which is how a 400 ms sleep that took 683 ms was reported as a wrong tempo (#153).

    onCoreLost?: () => void

    What to do when the core-lost watchdog detects the daemon connection is gone. Defaults to a clean process.exit(1) so systemd restarts the engine into a fresh connection (we do NOT attempt an in-process reconnect+re-export — too risky mid-graph). Tests inject a fake to observe the trigger without exiting the test runner.

    patchbay?: {
        appStreams?: "strip" | "park";
        exclusive?: boolean;
        hotplug?: boolean;
        hotplugMs?: number;
    }

    Type Declaration

    • Optional ReadonlyappStreams?: "strip" | "park"

      The SEED of the app-stream policy (/patchbay/exclusive appStreams) on a console with no persisted settings. Default 'park' — safe mode, the operator's "by default do not patch" (spec 2026-09-15 §6). Where a settings file exists, the file wins.

    • Optional Readonlyexclusive?: boolean

      The SEED of the exclusive-takeover preference (/patchbay/exclusive wanted) on a console with no persisted settings — a test desk, or a first boot with no state dir. Default true (coexistence spec, amendment 2026-08-28: the console takes the sound card when it comes up, and the operator switches it off from Setup). Where a settings file exists, the file wins: this is a default, never an override.

    • Optional Readonlyhotplug?: boolean

      Disable the hot-plug poll (default enabled).

    • Optional ReadonlyhotplugMs?: number

      Poll period in ms (default 2000).

    pdc?: PdcControl

    Plugin-delay-compensation control for /console/pdc. Defaults — for the built-in software adapter — to the live SoftwareMixer recorded at build time, so a plain software deployment can toggle PDC without extra wiring. Tests inject a fake. Absent (and no software mixer), the row registers no instance.

    perf?: { enabled?: boolean }

    Engine-seam performance telemetry (task #72): count + latency histograms for every REST resource GET/PATCH/watch (also the meter/telemetry/rta SSE fan-out) and the /patchbay/* routes, exposed at GET /telemetry. Recording is a single performance.now() pair plus an allocation-free PerfHistogram.record per seam call. MEASURED (not guessed) with packages/server/bench/perf-telemetry-overhead.mjs: the raw record() call costs ~25 ns, and enabling it end-to-end (REST dispatch + SSE fan-out, headless mock rig) moved the p50 of a 2000-request burst by less than 1% — within run-to-run noise, well under the task's ">1% → OFF by default" gating rule. Default disabled anyway: this is a deliberate opt-in (new + unproven in a live show), not a response to a measured cost — a disabled registry's record() is one boolean check regardless.

    persistence?: {
        channelConfigsDir?: string;
        dir?: string;
        patchesDir?: string;
        scenesDir?: string;
        sessionsDir?: string;
        settingsFile?: string;
        shippedSessions?: boolean;
    }

    Type Declaration

    • Optional ReadonlychannelConfigsDir?: string

      Explicit channel-config dir (overrides the dir-derived default).

    • Optional Readonlydir?: string

      Parent dir; sessions go in <dir>/sessions, scenes in <dir>/scenes, patches in <dir>/patches, configs in <dir>/channel-configs.

    • Optional ReadonlypatchesDir?: string

      Explicit standalone-patch dir (overrides the dir-derived default).

    • Optional ReadonlyscenesDir?: string

      Explicit scene dir (overrides the dir-derived default).

    • Optional ReadonlysessionsDir?: string

      Explicit session dir (overrides the dir-derived default).

    • Optional ReadonlysettingsFile?: string

      Explicit patchbay-settings file (the persisted ignore set, operator #83). Defaults to <dir>/patchbay-settings.json. When neither this nor dir is set the ignore set is in-memory only (session-scoped) — it will not survive a restart.

    • Optional ReadonlyshippedSessions?: boolean

      Whether an EMPTY session store is seeded with the shipped sessions (session-templates.ts) so a console with no history comes up as a real minimal show rather than a bare allocation. Default true — that is the product's answer to a first boot (task #33).

      Set false where the composition DECLARES ITS OWN SHOW and a shipped one would overwrite what it just said: the engine↔UI matrix's probe console dimensions itself deliberately (see __conformance__/engine-ui-matrix/facts.ts), and a suite whose subject is "a console with nothing saved" needs nothing saved to mean exactly that.

    plugins?: {
        catalog: Catalog;
        host: PluginHost;
        insertPointFor?: InsertPointResolver;
        portConnector?: PortConnector;
    }

    Type Declaration

    • Readonlycatalog: Catalog

      Installed-plugin catalog for URI + param validation.

    • Readonlyhost: PluginHost

      Connected mod-host client (shared with the audio-engine adapter, ideally).

    • Optional ReadonlyinsertPointFor?: InsertPointResolver

      Per-strip insert-point resolver (defaults to the stereo capture/playback seam).

    • Optional ReadonlyportConnector?: PortConnector

      Optional pw-link-backed port connector for PipeWire-named ports.

    quantum?: QuantumControl

    Quantum control for /console/quantum (the latency lever). Defaults — for the built-in software adapter — to the live SoftwareMixer recorded at build time, so a plain software deployment can drive live/floor quantum without extra wiring. Tests inject a fake. Absent (and no software mixer), the row registers no instance.

    reacScenePollMs?: number

    Remote head-amp scene-watch poll period (ms). Every this-often the server refreshes the adapter's remote head-amp fingerprint (MixerAdapter.refreshSourceCapsSignature); on a change — a reac-pw restart, a stagebox swap, a box width/capability change — it re-advertises capabilities to every open surface AND re-applies the recorded head-amp scene (phantom / pad / SENS) so the device always matches what the UI shows (the scene restore: "if we have phantom on, send phantom"). Default DEFAULT_FABRIC_POLL_MS; 0 disables (tests without a background timer); inert on an adapter without the refresh seam (no REAC fabric).

    reacSceneSettleReplayMs?: readonly number[]

    Follow-up scene re-apply delays (ms after a fabric change). Defaults to DEFAULT_SETTLE_REPLAY_MS — the settle window a power-cycled box needs before it accepts head-amp records (rig 2026-08-19). [] disables the follow-ups; tests inject short delays to observe the retry without waiting out the real window.

    realHardwareDefaults?: boolean

    EXPLICIT opt-in, THE ONE SWITCH: whether the device-global controls this server always carries — clock and rmeTotalMix — default to their REAL, hardware-touching ACTUATORS (PwMetadataClock: pw-metadata/pw-dump reads and a live re-clock; AmixerRmeTotalmix: amixer cset against ALSA card 0) when the caller injects neither. Default false/absent: an unconfigured boot gets NULL_CLOCK (fully inert — the clock control has no separable actuator) for clock, and a REAL RmeTotalMixController over the inert NULL_RME_TOTAL_MIX_ACTUATOR for rmeTotalMix — its in-memory intent tracking (owned/trims/cells) still moves on a PATCH, so a desk that injects nothing still answers a broadcast battery honestly; only the amixer exec is gated.

    Audit item 8 (2026-09-14/15): wireClock() used to construct a real PwMetadataClock() UNCONDITIONALLY whenever options.clock was absent, and start()'s seedClockState() runs on every boot with no test action required — its drift-recovery legs (reassertPersistedRate/reassertPersistedQuantum) call the real setRate/requestQuantum the instant a stale managed drop-in disagreed with the live driver, i.e. a PLAIN SERVER TEST RE-CLOCKED THE OPERATOR'S DESK with no clock.* verb ever invoked. Only console-rig.ts's real startConsole sets this true; every conformance desk and unit test leaves it off, so the class of incident that also wrote the operator's real WirePlumber drop-ins (below) cannot recur from an ordinary test boot — see test-harness-fake-by-default-conformance.test.ts.

    record?: {
        laneOf: (channel: ChannelId) => number | undefined;
        node: () => RecordNode | undefined;
    }

    MULTITRACK RECORD (2026-07-16-recording-vsc.md §2): the native node a take is captured from. Supplied exactly as rtLoad's source is — the console owns what to record and where it lands, the node owns the capture. Absent means no transport: /record has no instance, which is the honest way to say a console with no engine cannot record.

    Type Declaration

    • ReadonlylaneOf: (channel: ChannelId) => number | undefined

      A channel's lane on that node. Supplied by the composition rather than resolved here, because the composition ALREADY owns that rule (console-rig.ts's laneIndexOf) and a take that captured a different lane from the one the console's own EQ and dynamics write would be a file of somebody else's audio with nothing about it looking wrong.

    • Readonlynode: () => RecordNode | undefined
    resources?: {
        capability?: ChannelCapabilityReader;
        chain?: ResourceHandle<ChainOrder>;
        chainProcessor?: ResourceHandle<ChainProcessorState>;
        channels?: () => readonly ChannelId[];
        dcaMembers?: ResourceHandle<DcaMembers>;
        directPath?: ResourceHandle<SourceDirectPaths>;
        headAmp?: ResourceHandle<HeadAmpRowOrAbsent>;
        headAmpKnownFields?: (
            id: ResourceId,
        ) => ReadonlySet<keyof HeadAmp> | undefined;
        headAmpPort?: (id: ResourceId) => string | undefined;
        headAmpResource?: HeadAmpResource;
        matrixPoint?: ResourceHandle<MatrixPointRowState>;
        migrateHeadAmpPort?: (
            from: SourcePatch,
            to: SourcePatch,
            capability: HeadAmpCapability,
        ) => boolean;
        mixMinus?: ResourceHandle<MixMinusExcluded>;
        muteGroupActive?: ResourceHandle<MuteGroupActiveState>;
        muteGroupMembers?: ResourceHandle<MuteGroupMembers>;
        nativeReadback?: NativeReadbackSource;
        patchInput?: ResourceHandle<PatchInputState>;
        patchOutput?: ResourceHandle<PatchOutputState>;
        patchOutputLeg?: ResourceHandle<PatchOutputLegState>;
        prefix?: string;
        profile?: () => ConsoleProfile;
        projectHeadAmpPort?: (
            patch: SourcePatch,
            fields?: ReadonlySet<keyof HeadAmp>,
        ) => ProjectionResult;
        reacDesired?: ReacDesiredConfig;
        recallSafeMask?: () => RecallSafeMask;
        refusedRivalMasters?: () => ReadonlyMap<string, RefusedRivalMaster>;
        registry: ResourceRegistry;
        restoreChannelHeadAmp?: (
            id: ResourceId,
            values: Partial<HeadAmp> | undefined,
        ) => void;
        seedHeadAmpPort?: (
            patch: SourcePatch,
            value: HeadAmp,
            capability: HeadAmpCapability,
        ) => boolean;
        send?: ResourceHandle<SendRowState>;
    }

    The console's resource layer (epic #436): the ONE ResourceRegistry its entities were registered into, served over HTTP by a RestRouter this server mounts.

    The registry arrives already populated because the composition root is the only place that can build the entities — the native mixer stages a projection writes to live there, not here. Mounting is this server's job for the mirror-image reason: it owns Fastify and the client fan-out, so it is the only place a REST write can reach every open surface.

    Absent leaves no /api surface at all rather than an empty one, which is the honest answer for an embedding that declared no entities.

    Type Declaration

    • Optional Readonlycapability?: ChannelCapabilityReader

      The channel's LIVE capability row — the authority on what a channel can carry. The clipboard's paste asks it per target (#57); absent, a paste falls back to the contract's kind column, which is the same table this row's own features derive from.

    • Optional Readonlychain?: ResourceHandle<ChainOrder>

      The processor-CHAIN entities' typed handles. When present, chain.reorder, chain.swapEqDyn, chain.processor.enable and chain.processor.plugin write through them — one engine door, one declared order — and each verb still answers its own chain.processors dialect, read back from that same store. Absent, the verbs keep the structural path: a desk without the entities loses nothing.

    • Optional ReadonlychainProcessor?: ResourceHandle<ChainProcessorState>
    • Optional Readonlychannels?: () => readonly ChannelId[]

      Every channel the console holds a strip for — what a composition change re-projects.

    • Optional ReadonlydcaMembers?: ResourceHandle<DcaMembers>

      The DCA MEMBERS entity's typed handle (VCA nesting: members + parents). When present, dca.members writes through it — the same structural runtime dca.fader already reads/writes. Absent, the verb keeps the structural path.

    • Optional ReadonlydirectPath?: ResourceHandle<SourceDirectPaths>

      The /directPath/{source} entity's typed handle — ONE source's open direct outputs. When present, directPath.add/directPath.remove still await the engine call directly (as before), then patch the row of the source they named: the diff the row computes against that same settled state is empty, so the entity's own broadcast is exact, no path is opened or closed twice, and no other source's row is touched.

    • Optional ReadonlyheadAmp?: ResourceHandle<HeadAmpRowOrAbsent>

      The head-amp entity's typed handle — the SENS-capable GAIN door (headAmpDoor) and the session-capture reads go through it. The phantom/pad/sens verbs patch the registry by path instead: one record, one projection queue, and a refusal (no patched input, a control the input does not declare) THROWN back to the client rather than falling to MixerEngine.setPhantom/setPad/setSens — the engine's adapter write is a second, independently-ordered queue onto the same preamp, and on a registry console its strip write is a fact no hardware honours. With no resources at all the engine path stands: it is the demo/mock console's one store.

    • Optional ReadonlyheadAmpKnownFields?: (id: ResourceId) => ReadonlySet<keyof HeadAmp> | undefined

      The KNOWN-set read beside the handle (phase D2): which of the three head-amp fields the entity actually knows for a channel's physical input — undefined where the channel resolves to no instance. Session capture persists exactly this set, so a never-touched preamp persists nothing and a restore has nothing to assert onto it. Wired together with headAmp, from the same entity.

    • Optional ReadonlyheadAmpPort?: (id: ResourceId) => string | undefined

      The PHYSICAL INPUT a channel's head-amp resolves to — the identity of the ONE record, read beside the handle from the same entity (audit D4). A head-amp belongs to the port, so a write on one channel IS a write on every channel that splits that port: this is how the fan-out finds the other legs and publishes the owner's value for each of them. Absent, only the addressed channel is published — what a desk with no splits, and a console with no entity, do anyway.

    • Optional ReadonlyheadAmpResource?: HeadAmpResource

      The head-amp entity's CONCRETE instance — beside headAmp's erased handle, for the one caller that needs more than read/patch-by-channel: /source/{id}/headAmp (source-rows.ts) resolves a source to whichever channel currently sources its physical input, which needs HeadAmpResource.portOf and HeadAmpResource.capabilities, neither of which a ResourceHandle carries. The row still reaches audio through this SAME object's own patch/project — a second address, never a second store. Absent wherever headAmp is, for the same reason.

    • Optional ReadonlymatrixPoint?: ResourceHandle<MatrixPointRowState>

      The matrix-crosspoint entity's typed handle. matrix.point writes through it when present.

    • Optional ReadonlymigrateHeadAmpPort?: (from: SourcePatch, to: SourcePatch, capability: HeadAmpCapability) => boolean

      The port-identity drift rescue (HeadAmpResource.migrateKeptPort, §6d amendment, #836/#835) — carry a KEPT value from a box's old port name onto its new one on the rejoin edge, before the masked sweep drives it. Absent, no console this old builds ever migrated one: no entity layer, nothing to carry.

    • Optional ReadonlymixMinus?: ResourceHandle<MixMinusExcluded>

      The mix-minus exclude entity's typed handle. mixminus.exclude writes through it when present.

    • Optional ReadonlymuteGroupActive?: ResourceHandle<MuteGroupActiveState>

      The mute-group's HOLD handle. When present, muteGroup.set patches through it instead of calling the structural door directly; absent, the verb keeps the structural path. The group's LABEL needs no handle here — its only door is /channel/muteGroup/{index}/name.

    • Optional ReadonlymuteGroupMembers?: ResourceHandle<MuteGroupMembers>

      The mute-group MEMBERS entity's typed handle. When present, muteGroup.assign writes membership through it — the engine's one runtime, with the row's declared ripples re-projecting exactly the entering/leaving members' composed mutes — and muteGroup.set reads the group's members from it to scope its re-projection to the held set. Absent, both verbs keep the structural path: a desk without the entity loses nothing.

    • Optional ReadonlynativeReadback?: NativeReadbackSource

      The ?verify=1 reading — §6c query-and-compare made available to a rig operator with curl. Present only where there is a native node to ask (native-lane-readback.ts); absent on the software, mock and demo paths, and then a ?verify=1 GET answers the ordinary row body with no native in it. Absence is a fact, never a fabricated reading.

    • Optional ReadonlypatchInput?: ResourceHandle<PatchInputState>

      The patch-EDGE entities' typed handles — a channel's input source set and a bus's output destination set. When present, every patchbay.* crosspoint verb still writes the engine's ONE ledger through the structural controller (which is what settles the graph and returns the frame), and then REPROJECTS the row: the entity re-reads that same settled ledger and its fan-out speaks both dialects. Absent, the verbs broadcast their own frames as before.

    • Optional ReadonlypatchOutput?: ResourceHandle<PatchOutputState>
    • Optional ReadonlypatchOutputLeg?: ResourceHandle<PatchOutputLegState>
    • Optional Readonlyprefix?: string

      Mount prefix. Default /api.

    • Optional Readonlyprofile?: () => ConsoleProfile

      The declared appliance this console runs as — the CEILING every allocation, live or proposed, narrows through. Absent where the deployment declared none, which reads as the unrestricted OPENMIXER_PROFILE: a profile is a restriction, so "none declared" is honestly "nothing narrowed", never "unknown".

    • Optional ReadonlyprojectHeadAmpPort?: (patch: SourcePatch, fields?: ReadonlySet<keyof HeadAmp>) => ProjectionResult

      DRIVE the head-amp a physical input already holds onto the box, with no channel in the path (HeadAmpResource.projectPort) — how a determination reaches metal and how an already-known box's appearance gets the DESK's values back (spec §8). Channel-keyed projection cannot do this job: it enumerates strips, and the ports a sixteen-input box exposes to nothing are exactly the preamps still holding someone else's phantom.

    • Optional ReadonlyreacDesired?: ReacDesiredConfig

      The REAC segments' DESIRED config store (reac-desired-config.ts) — the ONE home of what the operator asserted on /reac/segment/{name} (2026-08-26-reac-runtime-config.md §3, "the SESSION owns desired config… the daemon persists nothing"). Composed in the rig beside the member row that writes it, and handed here so the session snapshot carries it. Absent on a console with no REAC fabric — nothing to remember.

    • Optional ReadonlyrecallSafeMask?: () => RecallSafeMask

      The console-wide recall-safe mask, DERIVED from the per-channel /recallSafe rows. Handed to the scene store, which asks it on every recall and holds no copy. Absent on a console built without the rows: nothing is safed, and a recall applies whole.

    • Optional ReadonlyrefusedRivalMasters?: () => ReadonlyMap<string, RefusedRivalMaster>

      Every currently-refused rival master, keyed by the recognition key its OWN mac would build (reac-segment-row.ts's refusedRivalKeyOf, over the same native graph scan every other reacSegments accessor in console-rig.ts reads). Forwarded to /stagebox's roster (buildStageboxRosterRow) so a bound entry can say WHY it reads absent, rather than the composition root reaching into the registry a second time. Absent on a console with no native PipeWire graph, which answers no refusedRivalMaster field on any entry.

    • Readonlyregistry: ResourceRegistry
    • Optional ReadonlyrestoreChannelHeadAmp?: (id: ResourceId, values: Partial<HeadAmp> | undefined) => void

      HOLD a restored trio the entity could not place, because the channel resolved to no physical input when the show landed — HeadAmpResource.restoreChannelHeadAmp. Wired from the same entity as the two above, and for the same reason they are: a show restores in one pass and a stagebox arrives on its own clock, so without it the port a box-fed channel lands on is first touched by a projection and seeded with the HOUSE FLOOR (#673).

    • Optional ReadonlyseedHeadAmpPort?: (patch: SourcePatch, value: HeadAmp, capability: HeadAmpCapability) => boolean

      DETERMINE a physical input's head-amp before any channel has patched to it (HeadAmpResource.seedPort) — the door a never-seen stagebox's arrival writes through (spec §8: a preamp param is never undetermined). Absent, no console this old builds ever seeded a port this way, and HeadAmpLifecycleController.adopt skips it: no entity layer, nothing to determine.

    • Optional Readonlysend?: ResourceHandle<SendRowState>

      The send entity's typed handle. When present, send.set / send.remove / send.tap and the send row's assign door all write membership through it — one ledger, the row's existence-by-value at level 0 standing in for send.remove. Absent, every one of those verbs keeps the structural path.

    rmeTotalMix?: RmeTotalMixControl

    RME Babyface Pro TotalMix ownership control for the rme.* verbs (the config-zone RME routing menu). Explicit injection always wins (tests inject a fake so the suite never shells out to amixer). Left unset, this resolves to a REAL RmeTotalMixController — full in-memory intent tracking, persisting to <stateDir>/rme-totalmix.json when persistence names a dir — wrapping a real AmixerRmeTotalmix actuator (amixer cset against ALSA card 0) only when realHardwareDefaults is true; otherwise the controller wraps the inert NullRmeTotalMixActuator, so the SAME state-tracking behaviour (a PATCH still moves an observable fact) runs with zero real hardware I/O. Always present (the lever is device-global — it drives the RME card directly, not the adapter).

    rta?: { hz?: number; source: RtaSource }

    Real-time analyzer (RTA / spectrum). When supplied, the server polls the engine's RtaSource while clients are subscribed and broadcasts a spectrum frame (raw FFT bins, for the standalone panel) + an rta frame (compact 0..1 log-bins, for the EQ-curve overlay) per channel each poll — the channels the operator ARMED (/channel/{kind}/{index}/rta's armed) plus whatever any surface is DISPLAYING, MAIN among them like any other lane. Handles rta.subscribe/unsubscribe.

    Type Declaration

    • Optional Readonlyhz?: number

      Poll rate in Hz. Default 10.

    • Readonlysource: RtaSource
    rtLoad?: { headroom?: RtHeadroomConfig; source: RtLoadSource }

    The engine's RT load meter (task #121): the native mixer's own measured busy-per-quantum reading. When supplied, GET /telemetry carries an rt section, GET /telemetry/rt answers the same one-shot reading (rt-load-resource.ts), and GET /telemetry/rt?watch=1 rides it live. Each surfaced reading also carries the core headroom verdict (level + the honest 'engine at N% of its real-time budget' message) assessed with headroom (defaults: warn > 60%, refuse non-essential DSP adds > 80% — see core RT_HEADROOM_*).

    Type Declaration

    structural?: StructuralDsp

    The structural-DSP surface — the live software mixer's buses / sends / DCA / matrix / output layer. When supplied, the server serves the send (send-row.ts), DCA-membership (dca-members-row.ts) and matrix-point (matrix-point-row.ts) REST entities off it (driving it + re-projecting on every change), and the session capture/restore round-trips that state; otherwise those entities are unavailable and the session keeps them empty. The software adapter exposes it; hardware-console adapters do not.

    talkback?: { generators?: TalkbackGeneratorBackend }

    Talkback / test-signal generators. When a structural surface is present the server runs a TalkbackGeneratorManager (the built-in oscillator + white/pink noise, published as patchable NATIVE PipeWire source nodes — never subprocesses) and serves the /talkback/* REST entities (talkback-rows.ts) off it. The backend is injected at the server boundary (NativeGeneratorHost from @freemixer/pipewire-native in production, a fake in tests); omitted — the addon absent / non-native host — the generators degrade to not-running without ever crashing.

    Type Declaration

    • Optional Readonlygenerators?: TalkbackGeneratorBackend

      The native generator-node backend (default: none — generators report not-running).

    telemetry?: { hz?: number; source: LatencyReportSource }

    Latency telemetry. When supplied, /telemetry/latency exists and the telemetry tick polls the engine's LatencyReportSource on each tick (telemetry-rows.ts). The tick itself runs on every console, for as long as a stream declares any of its rows (/telemetry/latency, /telemetry/rt, /telemetry/process); this option only adds the latency row to it.

    Type Declaration

    touched?: TouchedStore

    The ephemeral fader-touch ledger — shared with the resource layer's /surface/touch/{kind}/ {index} row (surface-touch-row.ts), which is composed BEFORE this server (same reason every other row's engine door is a call-time accessor). Absent = this server owns its own (the pre-resource-layer default, and every bare test server).

    vsc?: {
        laneOf: (channel: ChannelId) => number | undefined;
        node: () => VscNode | undefined;
    }

    VIRTUAL SOUNDCHECK (2026-07-16-recording-vsc.md §3): the native node a take is REPLAYED through. The recorder's block read backwards, and deliberately the same shape — the console decides which take and which channels, the node owns the playback. Absent means no soundcheck: /vsc has no instance, which is the honest way to say a console with no engine cannot replay one.

    laneOf is the SAME resolver record is given, and it has to be: a take is played back into the lanes it was captured from, and a soundcheck feeding channel 4's recording into channel 5's strip would be somebody else's audio with nothing about it looking wrong.

    writeDriverPolicyDropIns?: boolean

    EXPLICIT opt-in: write the generated WirePlumber drop-ins (driver ring depth + device exclusivity, driver-ring-policy.ts) into the REAL ~/.config/wireplumber/ wireplumber.conf.d/ whenever the graded clock candidates move. Default false/absent — every conformance desk and unit test boots with this OFF, mirroring realHardwareDefaults (a separate switch: a desk could in principle want a real, moving clock reading without also writing config — the incident that forced this flag needed both off). Only console-rig.ts's real startConsole sets this true — see an internal spec §"the fix" and the 2026-09-14 23:25 device-exclusivity ruling.

    xrunPollMs?: number

    Xrun-health poll period (ms) — how often xrunProbe is sampled into the delta /clock/warnings judges (issue #657). Default XRUN_POLL_INTERVAL_MS; 0 disables the poll (tests that don't want a background timer).

    xrunProbe?: () => Promise<XrunProbeResult>

    The xrun-health probe (issue #657): pw-top's ERR column, the effective quantum/rate, and who is asking for the largest buffer on the graph. Defaults to probeXrunHealth (shells out through the hardened exec runner). Tests inject a fake so the suite never touches the real system graph.