Skip to content

fix(worker): unblock Linux onboarding, stop silent staking failures, recover model names - #168

Merged
marinom2 merged 4 commits into
mainfrom
fix/linux-worker-onboarding
Jul 29, 2026
Merged

fix(worker): unblock Linux onboarding, stop silent staking failures, recover model names#168
marinom2 merged 4 commits into
mainfrom
fix/linux-worker-onboarding

Conversation

@marinom2

Copy link
Copy Markdown
Owner

Linux worker onboarding could not complete, and two failure modes reported themselves as success. Found by auditing the flow on a real Ubuntu 24.04 box (RTX 5060 Ti 16GB), then re-reviewed adversarially — 24 candidate findings, 16 confirmed, 8 refuted and dropped.

The two that mattered most

Install could never finish on Linux. The Ollama and Docker installers ran as a bare root-requiring curl | sh under set -e, spawned from Tauri with no tty. It either aborted the whole installer or hung forever on a sudo prompt nobody could see. Now uses the sudo -npkexec ladder this file already had, and fails with a copy-pasteable command instead of hanging.

A reverted addSupportedModel was reported as success. cast send --gas-limit exits 0 even on a status-0 receipt, so a revert printed ✓ model added on-chain, the loop reached ✅ worker online, and the UI went green — over a staked worker serving nothing. We no longer send when the estimate reverts, and confirm with isEligible() before claiming success.

Model names and sizes

Seven of ten catalog models rendered as raw 66-char hashes. A model's on-chain identity is keccak256(tag) and the registry stores no name, so registrations made without a tag string leave the indexer echoing the id into name. keccak is one-way, but a known-tag set can be inverted — lib/model-catalog.ts recovers all ten live testnet models, and keeps known: false as a first-class state so an unrecovered id is never treated as a servable tag.

Sizes were inferred by regex over the display name, and the branch at hardware.ts:171 lacked a paramsB > 0 guard, so every unparseable name — including a hash — was asserted at exactly 8GB "Standard". Sizes now come from measurements taken on real hardware:

model download measured resident
gemma4:e2b 7.2 GB 1.7 GB (MoE — only active experts stay resident)
qwen3-embedding:0.6b 0.6 GB 2.3 GB
llama3-8b 4.7 GB 5.0 GB
qwen3-vl:8b 6.1 GB 5.7 GB
gpt-oss:20b 13.8 GB 12.7 GB

Download size is not resident size, and scaling it is off by 4x for MoE models — which is why the estimate is now only a fallback.

Also fixed

  • pkill -f matched the installer's own command line and SIGTERMed the shell running it (rc 143, everything after skipped)
  • diagnoseFailure told a staked operator to add funds they already had
  • the Windows path still staked for a model it never pulled (bash got the gate in the first pass; PowerShell didn't)
  • the picker's Apply button could be permanently dead from a stale closure — decision logic extracted to a pure module, because the unit environment is node and cannot import JSX, which is why this had no coverage
  • update-models.tsx compared against a VRAM figure that was always 0

Verification

tsc --noEmit clean · 673 tests pass · eslint clean · next build succeeds · bash -n parses all 18 generated scripts including the 663-line installer · the ten catalog ids are pinned in tests against the live registry, so a mistyped tag fails CI.

Behaviour on mainnet (llama3-8b, llama3-70b) is unchanged.

marinom2 added 4 commits July 29, 2026 08:49
The Linux worker flow could not complete, and two failure modes were reported
to the user as success. Fixes, in severity order:

P0 install could never finish on Linux. The Ollama and Docker installers were
invoked as a bare root-requiring `curl | sh` under `set -e`, from a Tauri
process with no tty. It either aborted the whole installer or hung forever on
an invisible sudo prompt. Both now use the sudo -n -> pkexec ladder this file
already used for the systemd drop-in, folded into one escalation so the user
sees a single polkit prompt, and fail loudly with a copy-pasteable command
instead of hanging.

P0 a reverted addSupportedModel was reported as success. `cast send
--gas-limit` exits 0 even on a status-0 receipt, so a revert printed
"model added on-chain" and the UI reached "worker online" over a staked worker
serving nothing. We no longer send when the gas estimate reverts, and we
confirm with isEligible() before claiming success.

P1 seven of ten catalog models rendered as raw 66-char hashes. A model's
on-chain identity is keccak256(tag) and the registry stores no name, so a
registration made without its tag string leaves the indexer echoing the id
into `name`. keccak is one-way, but a known-tag set can be inverted: the new
lib/model-catalog.ts recovers all ten live testnet models, and keeps
`known: false` as a first-class state so an unrecovered id is never treated as
a servable tag.

P1 model sizes were inferred by regex over the display name, and the branch at
hardware.ts:171 lacked a `paramsB > 0` guard, so every unparseable name -
including a hash - was asserted at exactly 8GB "Standard". That under-stated
qwen3-coder-next (51.7GB) and gpt-oss:120b (65.4GB) by 6-8x, and the named
gemma4:e2b (7.2GB) by nearly 2x. Sizes now come from measured catalog data.

P1 the picker dropped `id` and keyed selection on the display name, so a hash
could be selected, staked for, and passed to `ollama pull` and `cast keccak`,
which would hash it a second time into an id the registry rejects. It now
carries the id, keys on it, and excludes unrecoverable models from selection.

P2 preflight told Linux users Ollama would be installed "via brew" and still
concluded the install was safe; a failed `ollama pull` warned and continued on
to stake and register; and diagnoseFailure had no pattern for privilege errors,
the single most likely Linux failure.

Verified: tsc --noEmit clean, 590 tests pass, next build succeeds, eslint clean.
The ten catalog ids are pinned in tests against the live registry.
Separate from the worker fixes so it can be reviewed (or dropped) on its own.

Lint has been unrunnable on this repo independently of any change here. PR #161
(09c625b) bumped eslint 8 -> 10 and eslint-config-next 15 -> 16 together; the
revert in dbe1cff rolled the production group back to Next 15 but left the dev
group on the new majors. That combination cannot execute:

  - ESLint 10 dropped eslintrc support, but `next lint` from Next 15 still
    drives that API, so lint failed on "Unknown options: useEslintrc,
    extensions, ..." before reading a single file.
  - ESLint 10 also removed context.getFilename(), which the newest published
    eslint-plugin-react (7.37.5, pulled in transitively) still calls, and its
    peer range stops at ^9.7.

Pinning eslint to ^9 and eslint-config-next to ^15 realigns the dev group with
the production Next version, and the flat config replaces the eslintrc that
ESLint 10 would have rejected anyway. `lint` now runs eslint directly rather
than through the deprecated `next lint` wrapper.

Coverage floors are recalibrated, NOT relaxed in substance. The same dependabot
bump moved @vitest/coverage-v8 v2 -> v4, which swapped v8-to-istanbul for
AST-aware remapping. It attributes branches in never-executed files far more
completely, so an unchanged suite that previously reported ~78% branches now
reports ~42%. The old floors are not comparable to the new measurement. These
are the honest v4 numbers with ~1pt of slack. The large uncovered surfaces are
sdk/src/cli.ts, sdk/src/worker.ts, lib/tauri.ts and lib/use-encrypted-inference.ts,
all near 0%; covering those is the way to raise these floors.
…ith measurements

Two problems, both found by exercising the code rather than reading it.

resolveModel was not idempotent, and the pipeline resolves twice. lib/subgraph
resolves what the indexer sent and stores the result; the picker then resolves
that stored value again. The old check asked only whether the string LOOKED
like a 32-byte digest, and the placeholder from the first pass
("unnamed 0x1234abcd…") does not, so the second pass accepted it as a genuine
tag and returned { tag: "unnamed 0x1234abcd…", known: true }. A placeholder
marked servable is precisely what this module exists to prevent: it can be
selected, staked for, and passed to `ollama pull`, and hashing it yields an id
the registry never issued.

The fix uses the one thing we can actually prove. Because id = keccak256(tag),
a claimed name can be verified against its id instead of trusted on shape. So
a name that hashes to the id is the real tag - which also means a
correctly-registered model we have never seen still resolves, rather than
being discarded for not being in the catalog - and anything else falls back to
inverting the id, yielding known:false when we cannot recover it. Resolution
is now stable under repetition, and a name/id mismatch resolves by id rather
than believing a column that cannot be true.

The VRAM figures were guesses and one was badly wrong. Sizes came from the
Ollama manifest download scaled for overhead, which is a poor proxy for what
actually occupies the card: gemma4:e2b is a 7.2GB download that sits at 1.7GB
resident, because only a mixture-of-experts model's active experts stay on the
GPU. The estimate said ~9GB - a 5x over-reservation that would wrongly
disqualify machines it fits comfortably, and it landed the model in the wrong
tier as well. Measured on an RTX 5060 Ti 16GB via Ollama's /api/ps with each
model loaded and answering:

  qwen3-embedding:0.6b  2.3GB (est. was 2.6)
  qwen3-vl:8b           5.7GB (est. was 5.4)
  gemma4:e2b            1.7GB (est. was 9.0)
  gpt-oss:20b          12.7GB (est. was 11.9)

All three of the small models were confirmed co-resident at 100% GPU with no
CPU spill, so the numbers reflect a real serving set, not a single-model best
case. The header now states that download size is not resident size and says
why, so the next person does not re-derive the same wrong shortcut.

Also drops two scratch test files a tooling pass left behind
(tests/unit/zz-scratch-probe.test.ts, tests/unit/zzdump.test.ts).

tsc clean, 595 tests pass, eslint clean, next build succeeds.
…pass

A multi-agent review of b6de6ec turned up defects the type checker, the unit
suite and the Next build all pass over. Each is verified against the generated
shell rather than the TypeScript that emits it.

The teardown could kill the installer running it. pgrep/pkill -f match the
FULL command line, and the whole install script IS the command line of the
shell executing it. So the "is Docker Desktop wedged?" probe always matched
itself, and the pkill that followed SIGTERMed its own shell - rc 143, with
everything after it silently skipped. The probe now matches a macOS-only
binary path and the kill goes to explicit pids from pgrep, never a blanket
pattern.

A staked worker was told to add funds it already had. diagnoseFailure keyed
insufficient-funds on a substring that also appears in unrelated revert text,
so a model-registration revert surfaced as "top up your wallet". The
recogniser now anchors on the actual emitted strings, and says plainly whether
anything was staked - that sentence is the difference between a 30-second fix
and a support thread.

The Windows path could still stake for a model it never pulled. The bash side
gained a presence gate in the first pass; the PowerShell side kept the old
warn-and-continue, so the failure just moved platform.

The picker's Apply button could be permanently dead. It captured the selection
in a closure that a whitelist refetch replaced, so after the fetch resolved the
handler wrote a stale set - or nothing. The decision logic moved out to
model-picker-logic.ts, which is pure and therefore testable: the unit
environment is node and cannot import JSX at all, which is why this class of
bug had no coverage.

Trust in a model name is now carried, not re-derived. lib/subgraph marks a row
`unnamed` when the id could not be inverted, instead of leaving every consumer
to re-infer it from the label - re-inference is what let a placeholder round-trip
into a servable tag (fixed in f8d3d65). sdk/src/subgraph.ts gets the same
treatment and sdk-consistency.test.ts now fails if the two copies drift.

Verified: tsc clean, 673 tests pass, eslint clean, next build succeeds, and
`bash -n` parses all 18 generated scripts including the 663-line installer.
@vercel

vercel Bot commented Jul 29, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
lightnode Ready Ready Preview, Comment Jul 29, 2026 7:21am

Request Review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant