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). Thewatchdog/threadfamilies live in libopal; thesystem.load.*family lives in libnode (it drives the active node'sSyntheticLoadService).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:
--opal '{"task":"name"}'does nothing. Anything starting with{is parsed as a fullTaskPlan, and a bare{"task":…}object has nosteps, so it yields an empty plan. The only trace is one line in the log — "empty plan from …" — and with-cthe process then never exits, because nothing ever completed. Every example in this file used to be written that way.- The short form splits on spaces.
prompt=Now add testssetspromptto"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
- Audio recovery and the watchdog system
- Diagnose an audio stall
- OPAL Task System