Tune Spiral with Helix
Tuning is the process of adjusting the domain’s cost model — delay costs, noSoln outsourcing costs,
travel factors, option times, quality settings — until Spiral’s decisions match the operation’s
intent. Helix gives you a repeatable loop for it: replay a recorded situation, watch what Spiral
decides and why, adjust the domain, replay again, compare. This guide walks that loop end to end.
(Prerequisites: an admin login, a client dev environment — see
Develop a client extension — and one or more recorded
.run.json files in the client’s tests/ tree.)
1. Replay a run file
Section titled “1. Replay a run file”A run file (<name>.run.json) is a recorded input stream: every command that reached the CRUD,
in order, with its Spiral synchronisation tag. Replaying one reproduces the recorded situation
exactly.
From the landing screen choose Run File (shown when run files exist and you have the admin
permission), then pick a file from the picker — it lists the client’s tests/ directory
(HELIX_TESTS_DIR overrides the location).
Two modes:
- Continuous (default): the file streams through at full speed. The application is read-only
while loading (the top bar shows progress, e.g.
1240 of 3800 (32%)) and unlocks automatically when the final tag completes. - Single step (tick the checkbox in the picker): the file advances one tagged command at a time, and the application stays fully editable throughout — this is the tuning mode.
The run-file library — capture, organise, batch-test
Section titled “The run-file library — capture, organise, batch-test”Run files are managed from the Run File Console on the landing screen (browse them under
/run-files; the tests base is HELIX_TESTS_DIR, or <HELIX_CLIENT_PATH>/tests). Their folder
encodes a lifecycle:
current/— captures from the live dev system. While a writable node runs, the crud→Spiral command stream can be captured to a new run file, so a situation you just reproduced by hand becomes replayable.new/— a capture promoted as a candidate regression (a situation worth keeping).regression/— the accepted baseline suite.
Two ways to make a run file, and the second is the one that matters after a crash:
- Capture writes the current position into a new file and records every command sent after it.
- New run file from logs builds one from the live
crud2spiral.log— everything this crud has sent Spiral since it launched. Use this when Spiral has died: there is no live optimiser left to capture from, but the log still holds the exact stream that killed it, and the file replays standalone.
Both rewrite each include filename to be relative to tests/.., which is where the harness runs Spiral
from, so the file works outside Helix without editing.
The console also batch-runs the testSpiral harness against the running binary and shows a live
pass/fail tally on the landing page. Each file’s recent results come from its <name>.audit.json —
one JSON object per line, {start} when a run begins and {start,end,result} when it finishes. The
end time is what marks a run complete; a trailing record without one is a run still in progress. The
console reads the last N of those (N is yours to set) and colours each card’s edge accordingly — green
for a clean history, aqua when it passes now but has failed recently, and a distinct colour per failure
kind (result check, crash, timeout, slow). Keep the file you tuned against in regression/ (see step 6).
One harness, one binary
Section titled “One harness, one binary”testSpiral is a helixCore component, shipped with the core and present in the image at
/app/helixCore/testSpiral — one place to maintain it. A client repo does not commit a copy. Pull the
harness the image actually carries:
./helix/runtime/helixctl harness # → tests/testSpiral + tests/testSpiralRunBoth land in tests/, where the repo’s .gitignore ignores them, so they can never be committed and can
never drift from the core. That is the point: a committed copy has to be hand-refreshed on every check-in
and quietly tests against an older harness than the core ships the first time someone forgets, while a
symlink into the Helix tree only resolves when the two trees sit side by side — never inside the container,
where only the client’s tests/ is mounted. Pulling has neither failure mode.
Re-run helixctl harness after taking a new core. If a client has no copy at all, Helix falls back to the
shipped one automatically.
Point both Helix and the harness at the same engine with SPIRAL_EXE:
export SPIRAL_EXE=~/Honeywick/Helix/helixCore/spiral.l64This matters when you are tuning against a new Spiral build. Unset, the two fall back differently —
Helix to helixCore/spiral.l64, the harness to release/spiral.l64 — so a build can pass every test and
still not be what Helix executed. Helix probes the binary it actually runs for its version, so the About
dialog reports the truth either way.
2. Step through the interesting part
Section titled “2. Step through the interesting part”In single-step mode the dispatch top bar (the slot that normally shows progress/errors) becomes a release button showing the next waiting command:
▶ tag 1041 — {"add":{"project":{"id":20003,"stops":{...Hover it to see the complete command; click it to release that command for processing. The next tagged command then takes its place. Untagged lines (comments, configuration) flow through without stopping. While Spiral is paused the button dims and shows ⛔ — a released command couldn’t process, so resume Spiral first.
This lets you bring the replay to precisely the moment you care about — the project whose allocation
looked wrong, the surge that triggered outsourcing — then examine the world before letting the next
command land. Because the app is not read-only, you can open editors, change values and probe
between steps. (The parked command is also available programmatically: GET /run-files/next echoes
it; POST /run-files/step releases it.)
3. Review the costs — Full / Developer mode and auto-allocate
Section titled “3. Review the costs — Full / Developer mode and auto-allocate”Switch to Full or Developer mode from the top-bar settings menu (☰ → Mode: Full / Mode: Developer). Normal mode is the operator view; the tuning detail only shows in these modes.
Auto-allocate asks Spiral to re-place a stop freely and — crucially — to explain itself:
right-click a stop row (or Ctrl+right-click its map marker) and choose Auto Allocate. Helix
sends a force coerce with rid: 0 (recalculate the resource; did: 0 for desk assignment) and
the tagged reply comes back as a Spiral reply dialog:
- Warnings are translated through the domain’s
helix.warningtemplates — the operator-facing reasons a placement was refused or penalised. simpleSolns(Full/Developer only) is Spiral’s raw solution set — the candidate resources it considered with their solution data, shown as an expandable JSON tree. This is the primary cost-review surface: compare the candidates Spiral weighed and what separated them.
Per stop, the Schedule card in the stop editor shows what the current solution costs bought:
dep / arv / fin, travel (tvl/dst), the solnTier the solution landed in, cbt
(Full/Developer), and Spiral’s info messages. Developer mode additionally shows each tree node’s
stop id (and flags #in≠out when an entity’s recorded stop id has drifted from the live document —
useful when correlating with the raw stream).
The Developer-mode CRUD dialogs (every outgoing request and raw reply shown for confirmation) are useful when you want to see exactly what a UI action sends to Spiral.
4. Inspect the runtime domain in the JSON editor
Section titled “4. Inspect the runtime domain in the JSON editor”Open the JSON Editor (☰ → JSON Editor). The entity buttons include the Define set — Stop
Parameters, Resource Parameters, Customer Parameters, Contract Parameters, Topography — each seeded
from the live reference.define of the running system. This is the runtime domain as Spiral
currently sees it: resolved values, clone chains, the lot. The Dispatch Stream Viewer below it
dumps the whole live in-memory database, reference included.
Use it to:
- confirm what a parameter actually resolved to at runtime (clone chains can surprise),
- copy the current
stopParamsblock as the starting point for your tuned version.
Note — the Define verb does not apply live. The editor lets you modify the JSON, but an interactive
definepost is rejected by the CRUD (define not available in crud mode): the domain reaches a running Spiral only through file replay or a restart. Validation therefore happens by replay, not by posting the editor’s buffer — see the next step.
The Tune panel — edit the domain file in place
Section titled “The Tune panel — edit the domain file in place”In Full or Developer mode on a solo system, the auto-allocate Spiral reply dialog offers
a Tune… button (admin login required). It opens the client domain file itself — comments and
all — as a navigable tree, positioned at the current stop’s stopParams entry:
- Follow the clone chain: a
clonereference is a link — clicking it jumps to the referenced definition and pushes your position onto a stack (the breadcrumb shows the chain, e.g.booked-2 ▸ appt-2 ▸ immediate); ⬅ Back returns. This makes it quick to find where an inherited cost actually comes from. - Edit values in place: click any value, type, Enter. Edits are applied surgically to the file text, so the file’s comments and layout are preserved — only the value you changed is touched.
- Reload Spiral saves the file and restarts Spiral exactly as the landing page’s restart does:
the domain is re-read from disk and the app’s parameters refresh, so the next auto-allocate runs
under the tuned values. (A one-off
.bakof the original file is kept alongside.)
The panel also opens from the entity editors in Full/Developer mode (solo): the resource editor
has Tune buttons beside its Type and Contract fields (landing on the resourceParams /
contractParams definition), and the project/response-point editor has Tune… in its footer
(the root stop’s stopParams definition). One tuning window exists at a time — opening a new
target retargets it, confirming first if you have unsaved edits.
Structural changes (adding keys or entries) still belong in your editor or the JSON editor’s
define flow — the Tune panel is for the tuning loop: costs, times, factors.
5. Validate the change
Section titled “5. Validate the change”Two equivalent validation routes; both end with replaying the same run file and comparing decisions:
- Domain-file route (recommended): edit the client domain file
(
<CLIENT>.domain.json— e.g. adjust adelay.cost, anoSoln.costStop, an optiont). In the dev environment the domain is read live from the mounted repo, so the change applies on the next load — reload the same run file and step to the same moment. - Run-file route: for a self-contained experiment, copy the tuned
defineblock (from the JSON editor) into a copy of the run file, ahead of the recorded commands — replay applies defines in-stream, so the recorded traffic runs under the modified domain without touching the real file.
Compare before/after: the same auto-allocate on the same stop — did the candidate ranking change as
intended? Did solnTier improve? Did the warnings disappear? Single-stepping makes the comparison
precise because you can halt at the identical tag in both runs.
6. Land the change in the client runtime
Section titled “6. Land the change in the client runtime”Once the values are settled, make the domain-file edit permanent:
- Dev: already done — the dev loop reads
<CLIENT>.domain.jsonlive from your repo (dev loop); commit it to your client repo. - Production: rebuild and ship the client bundle —
helixctl release <version> --as bundlepacks the extension and the domain config, thenhelixctl deploy+ run on the node (build & deploy).
Keep the run file you tuned against in the client’s tests/ tree — it becomes the regression
baseline for the next tuning round.
Related
Section titled “Related”- Develop a client extension — the dev loop, domain-driven UI, deploy
- stopParams — the cost-model attributes (delay, noSoln, quality, appt …)
- Scenarios — scripted demonstrations (as opposed to recorded replays)
- errorObject / perfObject — Spiral’s monitoring stream