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).
  • 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, safe or all. 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.large or web.fetch.local on the calls in the next section. Tier: bootstrap; web-reach on 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