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).diagnostics.report.*— diagnostic reports: make, list, read and delete them, and open the report window. Lives in libdiagnostics. No task sends a report.
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.
Several plans run one after the other. Give --opal more than once and
each plan starts when the one before it completed, so a later plan can use
what an earlier one made. With -c the app exits after the last plan, with
exit code 0 when every plan succeeded and 1 otherwise. A value that names no
plan is skipped with a warning in the log, and counts as a failure.
One trap: 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 1 of 1 starts
--opal: plan 1 step[0] success=true message="Watchdog: 0 hot threads, heartbeat 902 ms old, …"
--opal: plan 1 step[0] output["heartbeat_age_ms"]="902"
NodeWindow: --opal plans done, exit 0
Task catalogue
Each entry gives the purpose of the task, whether it is idempotent, its scopes and its tier.
system.watchdog.status— Return live state of all watchdogs. Idempotent: yes. Scopes: (read-only). Tier:bootstrap.system.thread.list-runaway— List threads sustaining > 90 % CPU. Idempotent: yes. Scopes: (read-only). Tier:bootstrap.system.graphics.report— Report the graphics stack actually in use: backend, adapter, API version, instance layers, capability flags. Idempotent: yes. Scopes: (read-only). Tier:bootstrap.system.diagnostics.collect— Collect a diagnostic bundle of this run into one zip — and name every file it could not include. Idempotent: no (writes one archive). Scopes:system.diagnostics.read. Tier:bootstrap.diagnostics.report.create— Make a report as a draft, with optional text, files of the task's own and a screenshot. Sends nothing. Idempotent: no. Scopes:diagnostics.report.create. Tier:bootstrap.diagnostics.report.compose— Open the report window with a report, for the user to review and send. Idempotent: no. Scopes:diagnostics.report.compose. Tier:operational.diagnostics.report.list— List every report of this app. Idempotent: yes. Scopes:diagnostics.report.read. Tier:bootstrap.diagnostics.report.status— Return one report's state, attempts, error and server id. Idempotent: yes. Scopes:diagnostics.report.read. Tier:bootstrap.diagnostics.report.read— Read one text file of a report, at most 64 KiB. Idempotent: yes. Scopes:diagnostics.report.read. Tier:bootstrap.diagnostics.report.delete— Delete a report and its files. Idempotent: yes. Scopes:diagnostics.report.delete. Tier:bootstrap.system.monitor.status— Sampler health of the live Monitor activity: gap counts, and the longest freeze in ms. Idempotent: yes. Scopes: (read-only). Tier:bootstrap.system.monitor.view— Read or set the Monitor's Mix slider. Idempotent: yes. Scopes: (none). Tier:operational.system.monitor.color— The process list's color for given loads. Idempotent: yes. Scopes: (read-only). Tier:bootstrap.system.monitor.facts— What the Monitor's facts column shows: each section's badges, lines and hover card, with the NodeInfo, NodeLoad and NodeHealth JSON. Idempotent: yes. Scopes: (read-only). Tier:bootstrap.system.inventory.health— The temperatures and the health of the node's hardware, as NodeHealth JSON. Idempotent: yes. Scopes: (read-only). Tier:bootstrap.system.inventory.measure— Measure the speed of each CPU core class, in kernel operations per second per core. Idempotent: no (burns every core). Scopes: (none). Tier:owner.system.load.set— Apply synthetic CPU/MEM load to the local node. Idempotent: no. Scopes:system.load.write. Tier:operational.system.load.status— Return current synthetic load (cpu/mem/active). Idempotent: yes. Scopes: (read-only). Tier:bootstrap.system.load.clear— Drop all synthetic load to zero. Idempotent: yes. Scopes:system.load.write. Tier:operational.llm.status— Report whether OPAL can answer, and why not if it cannot. Idempotent: yes. Scopes: (read-only). Tier:bootstrap.llm.provider.list— List every configured LLM provider and its state. Idempotent: yes. Scopes: (read-only). Tier:bootstrap.llm.provider.enable— Enable or disable a provider by id. Idempotent: yes. Scopes:llm.write. Tier:operational.llm.model.list— List models, optionally scoped to one provider. Idempotent: yes. Scopes: (read-only). Tier:bootstrap.llm.model.setDefault— Set the default model by id. Idempotent: yes. Scopes:llm.write. Tier:operational.llm.permission.get— Report the permission mode the chat overlay runs under. Idempotent: yes. Scopes: (read-only). Tier:bootstrap.llm.permission.set— Set the permission mode:ask,safeorall. Idempotent: yes. Scopes: (none). Tier:owner.llm.credential.set— Set a provider's API key or credential string. Idempotent: yes. Scopes:llm.write.secret. Tier:operational.llm.provider.test— Test an online provider as the editor's Test connection does. Idempotent: yes. Scopes:llm.write. Tier:operational.llm.provider.fetchModels— Fetch an online provider's model list and add the new models. Idempotent: yes. Scopes:llm.write. Tier:operational.llm.provider.set— Set the base URL, the protocol or the self-hosted flag of an online provider. Idempotent: yes. Scopes:llm.write. Tier:operational.banter.audio.rebuild-force— Force audio device rebuild on a named node or all nodes. Idempotent: no. Scopes:banter.audio.write. Tier:operational.banter.checkpoint.save-force— Force an RL policy checkpoint write. Idempotent: no. Scopes:banter.policy.write. Tier:operational.web.fetch— Fetch one URL into an attachment. Idempotent: no (stores a new copy). Scopes: none;web.fetch.largeorweb.fetch.localon the calls in the next section. Tier:bootstrap;web-reachon those calls.
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.monitor.facts
Returns what the Monitor's facts column shows, in screen order. With the Monitor open it returns the screen's own texts. With no Monitor open it builds them from the node's inventory through the same function. With neither it fails with "no inventory on this node".
| Key | Type | Meaning |
|---|---|---|
sections |
JSON array | One entry per section: {id, title, badges: [{text, role}], lines: [text], card: [{key, value, role?, group?}]}. A role is neutral, warm, hot or unknown |
info |
JSON | The NodeInfo the facts come from |
load |
JSON | The NodeLoad |
health |
JSON | The NodeHealth |
source |
string | monitor or inventory |
system.inventory.health
Returns the node's NodeHealth: each temperature sensor with the device's own limits, the SMART health of each drive, the ECC error counts, the network link, the GPU power and slowdown reasons, and why a source gives nothing. When no screen reads the health, the task takes one reading for this call.
| Key | Type | Meaning |
|---|---|---|
health |
JSON | The NodeHealth |
system.inventory.measure
Runs the per-core speed measurement of the Monitor's Measure now button.
It burns every core for about a second per core, so it needs the owner tier.
| Name | Type | Default | Meaning |
|---|---|---|---|
force |
bool | false |
Run also on a busy machine. Without it, the task refuses on a node with work or a busy machine, and names the reason |
| Key | Type | Meaning |
|---|---|---|
classes |
JSON array | One entry per core class: {name, cpus, maxClockMhz, opsPerSecond, spreadPermille, measuredCores} |
allThreadsOpsPerSecond |
int | Every core at once, in kernel operations per second |
busyPermilleDuring |
int | The machine's busy share during the run, in parts per thousand |
kernelVersion |
int | The version of the measured calculation; numbers of two versions do not compare |
ms |
int | How long the run took |
refused |
string | Why the task did not run, when it did not |
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 |
timestamps |
bool | The GPU's time of a frame is measurable. False on every OpenGL ES device |
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. Which build, what the log says, what
the crash marker says and how the last runs ended are all mechanically knowable,
and this collects them into one zip you can attach. It makes the same
diagnostic bundle as the report window, with the trigger
opal, and writes it to a file instead of the outbox.
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 file appears in manifest.json, either in files
or in missing with a reason.
It is at bootstrap tier because the run it exists for is one that did not
come up. It reads the graphics stack and the screens on the user interface
thread, and waits at most 5 seconds for it. When that thread does not answer,
the bundle names those two files as missing.
Parameters: none.
Output keys:
| Key | Type | Meaning |
|---|---|---|
archive_path |
string | Absolute path of the zip that was written: diagnostics-<yyyyMMdd-HHmmss>.zip in the data directory |
archive_bytes |
int | Its size |
included |
string | Sources collected, comma-separated |
missing |
string | Files looked for and not found |
manifest |
string | The bundle's manifest.json, as compact JSON |
The files inside the archive, the limits and the manifest are in Diagnostic bundle.
Example:
hub --opal 'system.diagnostics.collect' -c
diagnostics.report.*
These tasks prepare a report and show it. None of them sends one. A report holds the user's data, so only the user's press of Send in the report window sends it. A user who asks OPAL to "send a report" gets the report window with the report ready. No task reads or returns the access token, and no task changes the Crash reports setting.
OPAL suggests a report when the user describes a defect, when a task failed with
an internal error, or when system.watchdog.status shows a recent crash marker.
It then calls diagnostics.report.create with a summary, optionally attaches its
own notes, and calls diagnostics.report.compose.
Every task but diagnostics.report.compose is at bootstrap tier, so a process
that is not healthy can still make and read reports. diagnostics.report.compose
needs the main window, and an app with no window refuses it with the reason
no window.
diagnostics.report.create
Make a report of this run as a draft in the outbox. Nothing is sent.
Parameters:
| Name | Type | Default | Meaning |
|---|---|---|---|
text |
string | empty | What happened, in the user's words or the task's summary |
screenshot |
bool | false |
Add a picture of the main window |
remove |
list | empty | Optional sources to leave out: logs, previous-log, crash, history, settings, screenshot, opal |
attach |
list of {name, text} |
empty | The task's own text files, put in the report's opal/ folder. At most 8 files and 1 MiB together; the manifest records the rest as cut |
Output keys: reportId, manifest (the manifest as compact JSON), path
(the bundle's file).
{"task":"diagnostics.report.create","args":{"text":"The viewport stays black after an undo.","attach":[{"name":"analysis.txt","text":"The log shows 173 failed shader bakes."}]}}
diagnostics.report.compose
Open the report window with report reportId, or with a new report made from
text when reportId is empty. The user reviews every file and decides whether
to send it.
Output keys: opened (true), reportId.
diagnostics.report.list
List every report of this app.
Output keys: reports, a JSON array with id, created, trigger,
state, size and serverId for each report, and count.
diagnostics.report.status
Parameters: reportId.
Output keys: state (draft, queued, sending, sent or failed),
attempts, error, serverId.
diagnostics.report.read
Read one text file of a report's bundle.
Parameters: reportId, and path as the manifest names it, for example
logs/run.log.
Output keys: text, bytes (the size of the whole file), truncated.
A file longer than 64 KiB gives its last 64 KiB, where a failure usually is. The
home directory reads <home>. A dump or a picture is refused, and so is a sent
report, because the app keeps no bundle for it.
diagnostics.report.delete
Delete a report and its files from this computer.
Parameters: reportId.
Output keys: deleted (true). A report that is being sent cannot be
deleted.
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. |
slot_dir |
string | The process-health folder of this run, where the heartbeat, the run record and a crash marker are written. A node uses <data>/process-health/; a standalone program such as Construct uses its own <data>/process-health/run-<k>/, so two windows do not share one. |
Example:
hub --opal 'system.watchdog.status' -c
{
"heartbeat_age_ms": "847",
"runaway_count": "0",
"base_dir": "<data>"
}
<data> stands for the data directory of the app.
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. |
mix |
int | The Mix slider, 0 to 100. |
cores |
int | The core count the process list scales by. |
machine_share |
number | The load that colors a row white: the whole machine, in cores. |
sampler |
string | real for the machine, synthetic for a test's scripted sampler. |
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.monitor.view
Reads or sets the Monitor's Mix slider, as if you moved it. The colors follow
at once, and the Monitor keeps the value for the next start. Without mix the
task only reads. When no Monitor activity is running, the task fails and says so.
Parameters:
| Name | Type | Meaning |
|---|---|---|
mix |
int, optional | The slider value, 0 to 100. |
Output keys: mix, the slider value after the call.
hub --opal 'system.monitor.view mix=30' -c
system.monitor.color
Gives the color the process list draws for a row with the loads you name, so you can check the color scale without a screenshot. Loads are in the list's percent: 100 is one core's worth, for CPU, memory and VRAM alike. The task uses the live Monitor's colors and Mix when the Monitor is open, and the list's default colors when it is not. It needs no window.
Parameters:
| Name | Type | Meaning |
|---|---|---|
cpu |
number | CPU load in percent; 100 is one core. |
mem |
number | Memory in percent of one core's worth. |
vram |
number | VRAM in percent of one core's worth. |
cores |
int, optional | The core count; the live one by default. |
Output keys:
| Key | Type | Meaning |
|---|---|---|
pixel |
string | The strip pixel, #AARRGGBB, premultiplied as the strip stores it. |
text |
string | The number text color on that row: #000000 or #ffffff. |
t_cpu, t_mem, t_vram |
number | How far each load has moved toward white: 0 up to one core, 1 at the whole machine. |
cores |
int | The core count used. |
live |
bool | true when the colors came from a running Monitor. |
hub --opal 'system.monitor.color cpu=2829 mem=985 cores=64' -c
A load of 2829 % CPU and 985 % memory on 64 cores is mostly CPU, so the row is a warm near-white with a red cast. See Reading the colors for the scale.
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.
The task needs the owner tier. The mode decides whether a plan runs without a
prompt, so the model cannot raise its own trust. A plan from another node cannot
run it.
hub --opal 'llm.permission.set mode=safe' -c
llm.provider.list
List every configured provider and its state. Parameters: none.
Output keys: providers: one entry per provider, separated by ;. Each
entry is id|name|type|enabled|status|protocol|selfHosted. The last two fields
came in 2026-10: protocol is AnthropicMessages or OpenAIChat for an online
provider and empty for the others, and selfHosted is true or false. A
reader of the five older fields still reads them in the same places.
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.
A new key is not tested yet, so the task marks the provider untested and
disables it, as the credential editor does. Run llm.provider.test next. The
same key again changes nothing.
hub --opal 'llm.credential.set provider-id=... credential=sk-...' -c
llm.provider.test
Test an online provider as Test connection in the provider editor does.
For a self-hosted server, the task first asks the server's /health, with no
key. Then it reads the model list and, for a llama.cpp server, the server's
properties. A provider that passes becomes Valid and is enabled. One that fails
becomes Invalid. The task waits up to 45 seconds for the provider.
Parameters:
| Name | Required | Default | Description |
|---|---|---|---|
provider-id |
yes | — | Provider id, from llm.provider.list. |
Output keys: id, name, result (valid or invalid), message. The
message starts with the result, then gives the test's text, for example
invalid: The server needs a key. Enter the server's key.
hub --opal 'llm.provider.test provider-id=...' -c
llm.provider.fetchModels
Fetch an online provider's model list, as Fetch models in the provider editor does, and add the models that the registry does not have. A model that the registry has gets the context size that the provider gave.
Parameters:
| Name | Required | Default | Description |
|---|---|---|---|
provider-id |
yes | — | Provider id, from llm.provider.list. |
Output keys: id, name, found, added.
hub --opal 'llm.provider.fetchModels provider-id=...' -c
llm.provider.set
Set the base URL, the protocol or the self-hosted flag of an online provider.
Give one or more of the three. Only a Custom provider can change its protocol
and its flag. The task refuses a base URL with a query, a fragment, or a user
name and password, and one that does not start with http:// or https://. A
change makes the provider untested: run llm.provider.test next. The task
creates no provider; add one in the LLM settings.
Parameters:
| Name | Required | Default | Description |
|---|---|---|---|
provider-id |
yes | — | Provider id, from llm.provider.list. |
base-url |
no | — | The base URL, with its version segment for the OpenAI protocol, for example http://192.168.1.20:8080/v1. |
protocol |
no | — | AnthropicMessages or OpenAIChat. |
self-hosted |
no | — | true for a server that you run. |
Output keys: id, name, base-url, protocol, self-hosted.
hub --opal 'llm.provider.set provider-id=... base-url=http://192.168.1.20:8080/v1 protocol=OpenAIChat self-hosted=true' -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
web.fetch
Fetch ONE web page or file into an attachment. The answer names the
attachment and lists its files. It never contains the page. Read the result
with attachment.read, starting at page.txt, and search it with
attachment.grep.
Arguments:
| Argument | Required | Meaning |
|---|---|---|
url |
yes | An absolute http:// or https:// URL, with no user name or password in it |
assets |
no | Fetch the page's own images into assets/. Default true |
allow_large |
no | Consent to a body over the soft cap, 2 MiB by default, up to the 100 MiB ceiling. The user is asked |
allow_private |
no | The host is on a local network. The user is asked |
What lands in the attachment:
| File | Holds |
|---|---|
source.txt |
The URL, the final URL after redirects, the status, the content type, the size, the time, the user agent, and each image that failed |
page.txt |
For HTML: the text, with no markup |
page.html |
For HTML: the body as received |
assets/ |
For HTML: the page's own images, at most 64 |
<name> |
For any other type, a PDF for example: the body under its own name |
Outputs: attachment, final_url, http_status, content_type,
size, files.
The two calls that ask:
| Call | The user is asked, every time | Needs |
|---|---|---|
allow_large=true |
yes | A person at this machine (web-reach tier) |
A host on a local network: loopback, private, link-local, localhost, *.local, a name with no dot, or allow_private=true |
yes | A person at this machine |
A body over the soft cap stops the first call, and the answer names
allow_large=true and the size the host declared. A host name that only a
lookup places on a local network stops the first call the same way, and the
answer names allow_private=true.
Refused, whatever the arguments:
| Refused | Why |
|---|---|
A scheme that is not http or https |
A page is fetched over the web only |
| A URL with credentials | A password is not sent on a model's say-so |
| A body over 100 MiB | The ceiling. No argument raises it |
| A public page that redirects into a local network | The approval was for the host the plan named, not a hop the page chose |
| An HTTP error, a 404 for example | An error page is not the page. Nothing is stored |
It cannot log in, submit a form, run JavaScript or follow links.
Limits of one fetch:
| Rule | What happens |
|---|---|
| Timeout | The whole transfer, redirects included, must end within the fetch timeout. A call with allow_large=true gets the large fetch timeout |
| Stall | A transfer that receives nothing for 10 s, or for half the timeout when that is shorter, stops as stalled. The partial body is discarded |
| Redirects | At most 5. One more stops the fetch, and nothing is stored |
| Text | page.txt holds the text of the first 8 MiB of HTML only, and source.txt says so. page.html keeps the full body |
| Images | The images share the size cap with the page, and get one more timeout for all of them together. A page brings at most 64 images. source.txt gives the number not fetched, and the reason |
| Image hosts | An image on a local network is not fetched, unless its host is the host the user approved. source.txt lists it as failed |
| Stop | The chat's Stop cancels a fetch at once. The answer says that the user canceled it, and nothing is stored |
Settings: the Web rows of the OPAL section of the app's settings page,
which OPAL settings in the chat's menu opens. config.set takes the
setting id, not the storage key:
| Setting id | Storage key | Default | Range |
|---|---|---|---|
opal.web.timeout |
opal-web-timeout-ms |
20 000 ms | 1 000 to 600 000 ms |
opal.web.largeTimeout |
opal-web-large-timeout-ms |
300 000 ms | 1 000 to 3 600 000 ms. A value below the fetch timeout acts as the fetch timeout |
opal.web.maxSize |
opal-web-max-kib |
2 048 KiB | 1 to 102 400 KiB |
opal.web.maskUserAgent |
opal-web-mask-user-agent |
true |
true or false |
opal.chat.permissionMode |
opal-chat-permission-mode |
ask |
ask, safe or all |
A change through config.set with schema=opal needs the owner tier:
hub --opal 'config.set schema=opal id=opal.web.timeout value=60000' -c
DNS: a host name is looked up once per fetch, and every connection to it goes to that first address. So a name cannot pass the check with one address and connect to another. Two kinds of host are not looked up:
- A literal address is its own answer.
- A name that is local by its form is local with no lookup:
localhost,*.localhost,*.local,*.home.arpa,*.internal,*.lan, and a name with no dot. The user approved that name, so Qt resolves it when it connects, and the connection is not pinned.
Example:
hub --opal 'web.fetch url=https://www.example.com/datasheet.html' -c
See also
- Give OPAL a reference — a file or a URL, from the user's side
- Audio recovery and the watchdog system
- Diagnostic bundle
- Send a bug report
- Diagnose an audio stall
- OPAL Task System