Audio recovery OPAL task reference

OPAL tasks for process-level health, runaway-thread diagnostics, and audio-recovery primitives. Split across two namespaces:

  • system.* — process-wide diagnostics (any node binary). The watchdog/thread families live in libopal; the system.load.* family lives in libnode (it drives the active node's SyntheticLoadService).
  • banter.* — BanterLab-specific recovery actions. Lives in libbanter (needs reach into the active BanterAutoTest / BanterClient).

Invoking a task from the CLI

--opal accepts three forms, and picking the wrong one fails silently — so this section exists before the catalogue rather than after it.

hub --opal 'system.watchdog.status' -c              # one task, no arguments
hub --opal 'system.load.set cpu=50' -c              # one task, key=value arguments
hub --opal '{"steps":[{"task":"…","args":{…}}]}' -c # a full plan
hub --opal '@plan.json' -c                          # a plan read from a file

-c / --close-on-completion is what makes it one-shot. Without it the node starts, runs the plan and keeps running — correct when you are driving a hub you intend to keep, wrong when you wanted a command.

Two traps, both silent:

  1. --opal '{"task":"name"}' does nothing. Anything starting with { is parsed as a full TaskPlan, and a bare {"task":…} object has no steps, so it yields an empty plan. The only trace is one line in the log — "empty plan from …" — and with -c the process then never exits, because nothing ever completed. Every example in this file used to be written that way.
  2. The short form splits on spaces. prompt=Now add tests sets prompt to "Now" and discards the rest, with no warning. Any value containing a space needs the plan form.

Results are printed to the log, one line per output key:

--opal plan completed, success= true exit= 0
--opal step[0] success=true message="Watchdog: 0 hot threads, heartbeat 902 ms old, …"
--opal step[0] output["heartbeat_age_ms"]="902"

Task catalogue

Task name Idempotent Scopes Tier Purpose
system.watchdog.status yes (read-only) bootstrap Return live state of all watchdogs.
system.thread.list-runaway yes (read-only) bootstrap List threads sustaining > 90 % CPU.
system.graphics.report yes (read-only) bootstrap Report the graphics stack actually in use: backend, adapter, API version, instance layers, capability flags.
system.diagnostics.collect no (writes one archive) system.diagnostics.read operational Collect build identity, the crash marker and process-health into one zip — and name every source it could not find.
system.monitor.status yes (read-only) bootstrap Sampler health of the live Monitor activity: gap counts, and the longest freeze in ms.
system.load.set no system.load.write operational Apply synthetic CPU/MEM load to the local node.
system.load.status yes (read-only) bootstrap Return current synthetic load (cpu/mem/active).
system.load.clear yes system.load.write operational Drop all synthetic load to zero.
llm.status yes (read-only) bootstrap Report whether OPAL can answer, and why not if it cannot.
llm.provider.list yes (read-only) bootstrap List every configured LLM provider and its state.
llm.provider.enable yes llm.write operational Enable or disable a provider by id.
llm.model.list yes (read-only) bootstrap List models, optionally scoped to one provider.
llm.model.setDefault yes llm.write operational Set the default model by id.
llm.permission.get yes (read-only) bootstrap Report the permission mode the chat overlay runs under.
llm.permission.set yes (read-only) operational Set the permission mode: ask, safe or all.
llm.credential.set yes llm.write.secret operational Set a provider's API key or credential string.
banter.audio.rebuild-force no banter.audio.write operational Force audio device rebuild on a named node or all nodes.
banter.checkpoint.save-force no banter.policy.write operational Force an RL policy checkpoint write.

Diagnostic queries are at bootstrap tier (precondition open only) so they remain usable when HUB is partially booted — the moment when diagnostics matter most.

system.graphics.report

Answers "which graphics stack is this machine actually running", so that a report saying "sluggish in graphics" arrives with the facts attached instead of needing an evening of back-and-forth to assemble.

Brings up a throwaway offscreen RHI, reads it, and tears it down. It reports and never repairs — nothing here switches a backend or changes configuration, because a task that "fixed" performance behind the operator's back would destroy the comparability the backend benchmark exists to establish.

Parameters: none.

Output keys:

Key Type Meaning
backend string Vulkan, OpenGL, D3D12, … — what was actually created, not what was preferred
adapter string GPU device name as the driver reports it
device_type string discrete, integrated, external, virtual, cpu. cpu here is the answer to most "why is it slow" questions
driver string Vendor and device IDs, hex
vulkan_api_version string The version we requested
vulkan_api_supported string The version the loader offers
vulkan_layers string Instance layers present at run time, comma-separated
max_color_attachments int Simultaneous MRT targets
rgba16f_render_target bool HDR render targets usable
rgba32f_render_target bool Full-float render targets usable
compute bool Compute and storage buffers available

The two version keys are separate on purpose. Reporting only the one in use makes a cap on our request read as a limit of the machine — which is exactly how a Vulkan 1.1 request sat unnoticed on a 1.4 loader for as long as it did.

vulkan_layers is the field that earns the task. Third-party overlay layers inject themselves into every Vulkan instance on a machine, are invisible in a bug report, are not the user's doing, and change timings. Naming them costs one query. (none) and (no Vulkan platform — layers not enumerable) are different answers and only the first is reassuring.

Example:

hub --opal 'system.graphics.report' -c

system.diagnostics.collect

The mechanical half of filing a bug report. The field report that started Windows First Run took a skilled operator an evening to assemble; which build, what the crash marker says, what process-health/ holds are all mechanically knowable, and this collects them into one zip you can attach.

It names what it could not find rather than omitting it. That is the point, not a nicety: an archive with no crash marker, and an archive assembled on a machine where the crash marker could not be read, are indistinguishable to whoever opens it later. Every source appears in MANIFEST.txt either as COLLECTED with counts or as MISSING with a reason.

Parameters: none.

Output keys:

Key Type Meaning
archive_path string Absolute path of the zip that was written
archive_bytes int Its size
included string Sources collected, comma-separated
missing string Sources looked for and not found

Inside the archive:

Entry Contents
MANIFEST.txt What was collected, what was missing and why. Written last so it can describe everything else, and included in the archive as well as the outputs — whoever opens the zip in six months will not have the task result
build-identity.txt Version, branch, commit, package version, OS, CPU. Without it a crash marker names addresses in an unknown binary
process-health/… The crash marker and its siblings. Since the marker carries a build id, this plus build-identity.txt is what makes the frames resolvable
crash-dumps/… Windows only

On the log. You will see MISSING log on every run, and that is correct rather than a bug: OctoMY applications write diagnostics to stderr and keep no rotating log of their own, so a log exists only if you redirected one. The manifest says so and says how to capture one next time — <application> 2> run.log. See Collect useful logs.

Two absences that are not the same as empty, both recorded rather than inferred: a directory that does not exist, and a directory that exists and holds nothing. On Windows the crash-dump folder is capped at ten and silently discards new dumps once full, so an empty one may mean "full" rather than "no crash" — the manifest says that too.

Example:

hub --opal 'system.diagnostics.collect' -c

system.watchdog.status

Returns the live state of ProcessHealth and ThreadRunawayDetector.

Parameters: none.

Output keys:

Key Type Meaning
heartbeat_age_ms int Milliseconds since the heartbeat thread last wrote its file. -1 if no heartbeat has been written yet.
runaway_count int Number of threads currently sustained at > 90 % CPU for ≥ 5 s.
base_dir string Per-node data directory the heartbeat is written to (useful under constellation mode).

Example:

hub --opal 'system.watchdog.status' -c
{
  "heartbeat_age_ms": "847",
  "runaway_count": "0",
  "base_dir": "/home/op/.local/share/OctoMY/hub"
}

system.monitor.status

Reports how well the Monitor activity is actually sampling. Reading /proc means waiting on the kernel, and on a loaded machine that wait can last seconds — during which the monitor takes no measurement at all. This task turns that into a number you can read from a script.

The Monitor activity owns the sampler, so the task reports active: false when the activity is not open. That is a success, not an error.

Parameters: none.

Output keys:

Key Type Meaning
active bool false when no Monitor activity is running. Every other key is absent in that case.
worker_busy bool A sample request is outstanding — the sampler thread is inside a /proc read that has not returned.
interval_ms int Configured sampling interval. 0 before the monitor has started its timer.
last_sample_age_ms int Milliseconds since the newest sample. -1 if none has been taken.
columns int History columns held (one per tick, up to 3600).
ok int Columns holding a measurement taken on time.
missing int Columns where no measurement was taken at all.
partial int Columns that arrived late: levels are valid, rates are not.
dropped int Columns discarded because they belonged to a superseded request.
longest_gap_ms int Longest unbroken run of missing columns, in milliseconds. This is the freeze duration.
recent string The newest 60 columns, one character each: . ok, M missing, P partial, D dropped.

recent is a character string rather than a JSON array because every OPAL output value is a string. One character per column stays readable in a terminal and cannot be confused with a measured zero.

Example:

hub --opal 'system.monitor.status' -c
{
  "active": "true",
  "worker_busy": "false",
  "interval_ms": "1000",
  "last_sample_age_ms": "312",
  "columns": "3600",
  "ok": "3547",
  "missing": "48",
  "partial": "5",
  "dropped": "0",
  "longest_gap_ms": "46000",
  "recent": "..........MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMP............."
}

A longest_gap_ms of 46 000 means the monitor went 46 seconds without taking a single measurement. See When a graph shows red dots for what that looks like on screen.

system.thread.list-runaway

Returns the current list of hot threads.

Parameters: none.

Output keys:

Key Type Meaning
count int Number of hot threads.
threads JSON array Each entry has tid, comm, cpu_percent, duration_ms.

Example:

hub --opal 'system.thread.list-runaway' -c
{
  "count": "1",
  "threads": "[{\"tid\":506272,\"comm\":\"VideoEncoder\",\"cpu_percent\":99.4,\"duration_ms\":42000}]"
}

On non-Linux platforms (Windows, macOS) the count is always 0 — per-thread CPU monitoring needs OS-specific support that is not yet implemented. The external heartbeat watchdog is the platform-independent safety net there.

system.load.*

Headless synthetic-load control for automated load-testing. Drives the local node's always-on SyntheticLoadService (the same engine the MonitorActivity synthetic-loads panel uses), so a load applied here shows up live in the Compute monitor's CPU/memory graphs, raises the danger-styled header return button, and locks the loads panel open — exactly as if you had dragged the sliders. Load runs independently of any activity and survives navigation; it is never persisted, so a fresh process always starts idle.

system.load.set

Apply CPU and/or MEM load to the local node.

Parameters:

Name Required Default Description
cpu no (unchanged) CPU load percent, integer 0–100. Omit to leave the CPU axis unchanged.
mem no (unchanged) MEM load percent, integer 0–100. Omit to leave the MEM axis unchanged.

Each axis is independent (like the two panel sliders): setting cpu alone leaves mem untouched. Out-of-range values are clamped to 0–100.

Output keys: cpu, mem (resulting requested percents), active ("true"/"false").

Example:

hub --opal 'system.load.set cpu=50' -c

system.load.status

Return the current synthetic load. Parameters: none. Output keys: cpu, mem, active.

hub --opal 'system.load.status' -c

system.load.clear

Drop all load to zero — the one-click off switch. Idempotent to idle. Parameters: none. Output keys: cpu ("0"), mem ("0"), active ("false").

hub --opal 'system.load.clear' -c

llm.*

Headless control of the LLM provider/model registry the OPAL chat overlay reads from — the same operations the overlay's "LLM settings" screen performs, reachable without opening it. Runs identically in hub and standalone construct: every task reads the LLMManager the active OPAL session already holds, never a Node lookup, so there is no headless-only gap between the two apps (OPAL/LLM host independence, §OLID-6 in the internal specifications).

llm.status

Report whether OPAL can answer, and why not if it cannot — the same readiness state the chat overlay's banner shows. Parameters: none.

Output keys: usable, reason, default-provider, default-model, provider-count, model-count.

hub --opal 'llm.status' -c

llm.permission.get

Report the task permission mode the chat overlay is running under. Read this before asking for something dangerous, so you know whether the plan will run without the user seeing a prompt. Parameters: none.

Output keys: mode (ask, safe or all), meaning, overlay.

overlay is false when no chat overlay is running. That is not an error: no overlay means no plan is being auto-run by one.

hub --opal 'llm.permission.get' -c

llm.permission.set

Set the permission mode. ask prompts for every plan, safe auto-runs safe tasks only, and all auto-runs every plan without asking.

Parameters: mode — one of ask, safe, all. Output keys: mode, previous.

The change is announced in the conversation and is remembered between runs, by the same route a click on the selector takes. A mode that is not one of the three is refused with the list, never guessed at — silently keeping all after a request to leave it would fail in the dangerous direction. With no overlay running the task refuses rather than writing a mode nothing would read.

hub --opal 'llm.permission.set mode=safe' -c

llm.provider.list

List every configured provider and its state. Parameters: none. Output keys: providers.

hub --opal 'llm.provider.list' -c

llm.provider.enable

Enable or disable a provider by id.

Parameters:

Name Required Default Description
id yes — Provider id, from llm.provider.list.
enabled yes true true to enable, false to disable.

Output keys: id, name, enabled.

hub --opal 'llm.provider.enable id=... enabled=true' -c

llm.model.list

List models, optionally scoped to one provider.

Parameters:

Name Required Default Description
provider-id no (all) Filter to one provider's models. Omit for every model.

Output keys: models.

hub --opal 'llm.model.list' -c

llm.model.setDefault

Set the default model by id — the headless equivalent of the readiness banner's "Configure" flow.

Parameters:

Name Required Default Description
id yes — Model id, from llm.model.list.

Output keys: id, name, provider-name.

hub --opal 'llm.model.setDefault id=...' -c

llm.credential.set

Set a provider's API key or credential string. The result never echoes the credential — check the output keys below, not the message, when scripting against this task.

Parameters:

Name Required Default Description
provider-id yes — Provider id, from llm.provider.list.
credential yes — The API key or credential string.

Output keys: id, name. Never credential.

hub --opal 'llm.credential.set provider-id=... credential=sk-...' -c

banter.audio.rebuild-force

Forces AudioInputNode::rebuildSource() and AudioOutputNode::rebuildSink() on the named node, or all nodes if no name is given.

Parameters:

Name Required Default Description
node no "" Node name. Empty string = all nodes.

Output keys:

Key Type Meaning
requested bool Always "true" when the task accepts; downstream consumers act on the [HUB-EVENT] line.

Side effects: Emits [HUB-EVENT] {"type":"audio_rebuild_requested","node":"<name>","source":"opal"}. The active BanterClient (which owns the audio engine references) picks up the request.

Example:

hub --opal 'banter.audio.rebuild-force node=Bob' -c

banter.checkpoint.save-force

Forces an RL policy checkpoint write.

Parameters: none.

Output keys:

Key Type Meaning
requested bool Always "true".

Side effects: Emits [HUB-EVENT] {"type":"checkpoint_save_requested","source":"opal"}. The active BanterAutoTest (if any) picks up the request on its next iteration boundary and writes <rlpol-path>.checkpoint. Sidecar — the named .rlpol is not touched.

Example:

hub --opal 'banter.checkpoint.save-force' -c

See also

In this section