Migrate GobboNet API to Go for cross-platform support - #2
Conversation
tested these setups:
* config discovery
* remote mode
* local mode (llama-cpp process supervision)
* example GGUF files
* legacy auth migration
* portable layout
* upstream API keys
Every fix below is something that exercise found and
the unit tests had missed.
supervisor: do not leak llama-server's children
terminateGroup derived the group id with Getpgid(child.Pid), which
fails with ESRCH once the reaper has Wait()ed the child -- while the
rest of the group is still running -- and then signalled the dead PID.
stop() compounded it by returning early whenever the leader had already
exited, which is exactly the crash and rollback path. An orphan is
reparented to init, so it disappears from any walk of our descendants
while still holding GPU memory; the next model then fails to allocate a
backend buffer with nothing pointing at the cause.
Capture the group id at launch and sweep by id, confirming the group is
empty with kill(-pgid, 0) rather than trusting the leader's exit.
models: honour the real context length under the hard overrides
The five filename overrides (llama3, mistral-small, mistral-nemo,
granite, command-r) returned before MaxCtx was assigned, so the most
common models all reported the 131072 placeholder. Against a server
launched with --ctx-size 8192 the UI offered a window llama.cpp
rejects on every request past the real limit.
models: exclude multimodal projectors from the model list
Vision models ship mmproj-*.gguf beside the weights. llama-server takes
a projector via --mmproj; handed one as --model it refuses to load, so
listing it offers a choice that can only fail and looks like a broken
swap.
models: normalise family to the vocabulary chat.html indexes
family is looked up in the stop-string table by exact match, so
architecture-derived values like qwen2 and phi3 missed every time and
the model's turn markers were rendered to the user as content.
config: keep GGUFs out of ~/.config
defaults() and the generated TOML both pinned model_dir to "./models",
which resolves against the config directory -- the XDG violation
GO_CONFIG_SPLIT.md calls out. Default to <data_dir>/models; a portable
install opts back in with a relative path.
config: stop `config set` eating its own documentation
The uncomment pattern allowed leading whitespace and replaced the first
match, so setting a key whose only occurrence was an indented prose
example rewrote the explanation instead of adding a setting.
proxy: put our CORS headers on proxied responses
ModifyResponse stripped llama.cpp's copies and added none, so /llm,
/search and /embed answered with no CORS at all while the preflight for
those same paths answered "*". A browser told the preflight passed and
then handed a response with no Allow-Origin blocks the result.
Three changes for consistency rather than because a test failed:
- config get/set accept --config like every other subcommand. Without
it, `config set` edited a different file from the one the next
`serve --config` reads.
- A missing llm_api_key_file is fatal to serve and check but no longer
blocks config get, matching where a missing server_exe is reported.
- The local degraded path guesses architecture from the filename the
same way the remote path already did.
Adds 15 tests pinning each of these.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>.
Testers need to run this without a toolchain, and a bug report is only actionable if it names the build that produced it. internal/version carries an identity stamped in at link time. It defaults to "dev" rather than a plausible-looking number, so an unstamped build can never be mistaken for a distributed one. Three places report it, because a tester may have any one of them to hand: `gobbonet version`, the startup banner, and /health-fileserver -- the last so a build identity can be copied out of a browser with no terminal open. Full() also reports the revision the Go toolchain embeds on its own from the real repository state. That is a cross-check rather than a source of truth: disagreement with the ldflags value means the stamp is stale. It carries the toolchain and platform too, which is the first thing to look at when one tester behaves differently from the rest. build-release.sh cross-compiles linux/amd64, linux/arm64, windows/amd64, darwin/arm64 and darwin/amd64, and bundles each binary with web/ and the docs into an archive under dist/ with a SHA256SUMS. CGO_ENABLED=0 and -trimpath, so each one is static and unpacks ready to run. The script refuses to build from a dirty tree. A sha that does not describe the code inside the binary is worse than no sha at all: the bug gets filed against a commit that does not contain it and cannot be reproduced. --allow-dirty overrides and marks the version -dirty, so it stays visible everywhere the version is reported. It bundles web/chat.html and deliberately not the repo-root chat.html, which is still the older Windows-lineage copy without the /llm/jobs client. Both the script and GO_SERVER.md say so, so the fork cannot be shipped by accident. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Collapses the "installed -> running" dialog sequence into a single finish-page checkbox. The hardware probe, model recommendation and model download that launch.bat asked for at a C:\> prompt are now wizard pages, and gobbonet.exe starts llama-server itself, so the checkbox is one Exec with no console window in between. llama.cpp is bundled rather than downloaded. launch.bat documents that "cmd -> temp .ps1 with Bypass -> downloads an executable archive" is what behavioral AV reads as malware staging, and an unsigned installer fetching a zip of .exe files is the same shape with a worse parent process. A .gguf is inert data, so the model download stays online -- it is also the only file too large to bundle. The model catalogue is generated, not copied. launch.bat holds it in three places (menu echo lines, the inline PowerShell $min table and recommendation ladder, and the MODEL_CHOICE download blocks); gen-catalog.py parses all three into models.ini, which NSIS reads with native ReadINIStr. build-installer.sh regenerates it every build so it cannot lag launch.bat. Download integrity mirrors launch.bat's policy exactly: HuggingFace serves an LFS pointer instead of the model on failure, and it arrives as a clean HTTP 200. Hash mismatch is fatal, an unparseable pointer warns, and a sub-1GB file is fatal as the backstop. hardware-probe.ps1 gains an optional -IniPath that writes a flat [hardware] INI alongside hardware.json, so NSIS can read the probe without a JSON parser. Additive: hardware.json is written either way and existing callers are unaffected. The launch.exe / launchLAN.exe C shims are dropped. They existed to locate the install folder and ShellExecute a .bat; gobbonet.exe is a real executable, so the shortcuts point straight at it. Compiles clean under NSIS 3.08 (Elodine used 3.09). Not yet run on Windows. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A directory named vendor/ at a Go module root is reserved by the toolchain: `go build` refused with "not marked as explicit in vendor/modules.txt" for every dependency once the llama.cpp bundle landed there. It now lives at installer/vendor/llama-cpp, next to the build script that consumes it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
v1.5 split the frontend from a single chat.html into chat.html plus 24 js/ and 15 css/ files. The fork carried its own copy of the frontend under web/, which worked while that was one file and stopped working at 39: a copy goes stale the moment upstream touches any of them, the server keeps serving the stale copy, and nothing reports a problem. web/ is now assembled by stage-web.sh from the repo-root frontend and is no longer committed. The root stays the single source of truth, so an upstream merge lands in one place. stage-web.sh refuses to build a web root whose file count disagrees with what chat.html actually references -- a forgotten <script> tag fails the build instead of producing a blank page with console errors. A checkout that has never run it still works: detectWebRoot() falls through to the working directory and serves the root directly. Also collapses the two release version literals into a VERSION file. They had drifted to 1.3 in build-release.sh and 1.4 in build-installer.sh -- under a comment claiming the latter "matches build-release.sh's scheme" -- while the tree carried upstream's 1.5.1 frontend, so a tester's report would have named a build that did not describe what they were running. VERSION_QUAD is padded to four components rather than suffixed ".0.0", which on a three-part version produced "1.5.1.0.0" and made makensis abort. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DjDjeRg7H6bUEuhZLVMsnk
The /llm/jobs wire contract belongs to fileserver.ps1, which defined it. This
port had changed the poll payload to send raw UTF-8 in a `chunk` string,
saving the 33% base64 overhead.
That saving is real and irrelevant. js/03-generation.js reads chunk_b64 and no
other key, and it does not error on an unrecognised payload: it finds no
chunk_b64, leaves `offset` where it was, computes drained = offset >= size as
false, and polls a perfectly healthy job forever. The reply never appears, the
user watches a spinner, and nothing is logged on either side. Verified against
the stock v1.5 frontend before changing it back.
The rune-alignment logic in read() goes with it. It existed only because Go's
JSON encoder turns a split UTF-8 character into U+FFFD; base64 has no such
hazard, and the client already decodes with TextDecoder(..., {stream: true}),
which rejoins characters split across chunks. It also carried a stall of its
own: a window containing no complete character returned empty and left `next`
at `from`, so bytes that were not UTF-8 at all -- which base64 carries
perfectly -- would freeze the offset forever while the job streamed on. read()
now returns a plain byte range, exactly as the PowerShell poll branch does.
conformance_test.go pins the field name, because the failure mode is a silent
hang rather than a test failure. The jobs tests now assert the property that
replaced alignment: read() must always advance `next`, whatever the bytes are.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DjDjeRg7H6bUEuhZLVMsnk
v1.5 replaced hardware-probe.ps1 wholesale: 552 lines and 13 functions became
1,961 and 40, first-hit-wins GPU detection became multi-source evidence
merging with credibility scoring, and the parameter block changed. Its header
documents four real bugs the rewrite fixes, including the floor-division that
under-reported every GPU by one tier and the dxdiag parse that reported ~1.6
million GB of VRAM. Worth taking.
It has no -IniPath, which the NSIS wizard calls. -EmitEnv does not substitute:
it writes bare KEY=VALUE lines and ReadINIStr needs a [section] header. So
this is a port onto a different file rather than a re-apply of the old 42-line
block -- new payload shape, new helper conventions.
The sanitiser is separate from ConvertTo-EnvSafe on purpose. ! and % are
delayed-expansion hazards in batch and must be stripped there, but they are
ordinary characters to the INI API and stripping them mangles real GPU names.
What breaks an INI read is a newline, a ';', and leading/trailing space.
Ordering matters and is the one subtle part: the substitution must run BEFORE
ConvertTo-SafeAscii, which deletes everything outside \x20-\x7E including CR
and LF -- so running it first welds the words either side of a line break
together ("Radeon RX\r\n7800 XT" -> "Radeon RX7800 XT"). Caught by the
self-test assertion written for it.
A failed INI write exits 1, unlike -EmitEnv's warn-and-continue. launch.bat
can fall back to parsing hardware.json; the installer cannot, which is the
whole reason the file exists. And its failure is not a visible error but a
wrong recommendation: with vram_gb unset the wizard reads 0, matches no rung
on the VRAM ladder, falls through to the catalogue default and offers a model
the machine cannot load. Exiting non-zero puts it on its "could not read this
machine's hardware" path, which offers the catalogue unfiltered and says so.
Three sanitiser cases are covered by -SelfTest, which needs no hardware and no
installer. Not yet run on Windows.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DjDjeRg7H6bUEuhZLVMsnk
RunProbe invoked $INSTDIR\hardware-probe.ps1 from the probe page, which sits at Page custom ProbePageCreate -- three pages before MUI_PAGE_INSTFILES, where Section SecMain writes that file. Nothing had been written to $INSTDIR yet, so the probe could never run: nsExec returned non-zero and every install silently took the "could not read this machine's hardware" branch, offering the catalogue unfiltered. It looks like a normal outcome on a machine with an unrecognised GPU, which is why it would have shipped. .onInit now extracts the probe into $PLUGINSDIR alongside models.ini, and the page runs that copy with its outputs there too. SecMain still installs its own copy for launch.bat, and copies hardware.json into $INSTDIR so the first launch inherits the probe rather than re-running it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DjDjeRg7H6bUEuhZLVMsnk
INDEX.md was a structural index of chat.html by line number -- renderMessages() at 7848 and so on. Every one of those died with the split. It is not replaced with new line numbers: each extracted module records the range it came from in its own header, so the file is now a translation table from any stale line reference into the module that holds it, plus the load-order hazards. For finding a symbol, grep across js/ is faster and always correct. AGENTS.md described an 11,402-line single-file frontend in three places. GO_SERVER.md claimed web/chat.html was the bundled fork and the root copy the older lineage -- exactly backwards now -- and documented the `chunk` framing and the rune alignment that went with it. GO_MIGRATION_INVENTORY.md recorded "Drop base64 framing -- Adopt" as a decision; it is marked REVERSED with what it cost, since the reasoning was sound and the conclusion still wrong. Also documents the platform gap plainly in GO_SERVER.md: the server is fully cross-platform, but hardware detection and guided model download are PowerShell and batch, so Linux and macOS have no first-run path. States the three ways out and that current state is "leave it". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DjDjeRg7H6bUEuhZLVMsnk
Upstream shipped 1.5.4 (error 500 fix) and 1.5.8 (power-user QOL) while this branch was out for review. The frontend half merges untouched -- this branch has never edited chat.html, js/, css/ or the launcher scripts, so all 2,116 lines of upstream's changes arrive as-is and stage-web.sh picks them up. The server half does not merge, because on this branch the server is Go. Three behaviours landed in fileserver.ps1 that gobbonet has to grow separately, and they follow in their own commits: - GET/POST /perf, which the new settings panel in js/02-model.js calls - the 8080 -> 9066 and 11434 -> 11437 port moves - job supersede-instead-of-429 Only .gitignore conflicted: upstream added .gobbonet-port next to the entries this branch had already restructured. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two port defaults moved in 1.5.8, both to stop squatting where the
neighbours already live.
llm_url 11434 -> 11437. 11434 is Ollama's default. On a machine with
Ollama installed the launcher found something answering there, concluded
llama-server was already up, skipped starting its own, then found
nothing healthy and restarted -- a loop caused entirely by the shared
port.
listen_port 8080 -> 9066 ("gobb" on a keypad). 8080 is the single most
contended port on a developer machine, and on Windows the dynamic ranges
Hyper-V, WSL2 and Docker Desktop reserve can swallow it, which presents
as a bind failure netstat cannot account for.
Existing installs do not move: config.toml is written with explicit
values on first run, so only a fresh install sees the new defaults.
Not adopted: upstream's .gobbonet-port file and its silent clamp to
1024-32767. The port is already a config.toml key that the installer
writes and `gobbonet config set listen_port` edits, and a second file
saying the same thing is a second thing to disagree. The clamp is worse
than the error it replaces -- a config that asks for a port and quietly
gets a different one is the failure this branch keeps removing. Out of
range still errors; the reasoning behind 32767 is in the generated
file's comments where someone choosing a port will read it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Upstream 1.5.8 gave context size, GPU layers and KV cache type a UI. js/02-model.js GETs /perf to fill the panel, POSTs to save, and then posts the already-loaded model to /swap-model to apply -- deliberately reusing the hot-swap path so there is one restart mechanism with one lock and one status feed. Without this endpoint that panel is dead UI on the Go server, so the wire shape here is upstream's verbatim, camelCase and all, because one frontend file drives both servers. The storage is not upstream's. fileserver.ps1 takes its baseline from GEMMA_* environment variables that launch.bat recomputes from hardware every run, and overrides them with .gobbonet-perf.json. Here config.toml IS the baseline -- installer/gobbonet.nsi writes ctx_size and kv_cache_type into it from hardware.ini, and off Windows a human writes them by hand -- so the override goes in a perf.toml beside it and reset deletes that file. Writing into config.toml instead would have been simpler and wrong: the first save destroys the probe's numbers, after which "reset to auto" can only mean the compiled-in 16384/99/q8_0. That is precisely backwards on the machines that need reset most. A 6GB card probed down to ctx 8192 would be reset UP to 16384 and stop loading, with the button that broke it labelled "put it back how it was". Two deliberate divergences beyond storage: A perf.toml that is unparseable or out of range stops the server with a message naming the file and the value, where upstream warns and silently falls back to auto. This file is only ever written by code that validates first, so a bad one means a hand edit -- and running settings the user did not choose, having noticed they did not choose them, is the kind of quiet substitution this port keeps removing. Load() does not apply the overlay; only the serve path does. `config get ctx_size` therefore keeps reporting the file that `config set ctx_size` writes. A getter and a setter that disagree about which file they mean would be worse than either layer on its own. Supervisor tuning moves out of the fixed Options into a mutex-guarded Tuning that SetTuning replaces and BuildArgs reads once per launch, so a save landing mid-assembly cannot pair one model's context size with another's cache type. /active-model.json now reports the live context rather than the startup one, or the client would keep budgeting prompts against a window llama-server is no longer running. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Past the concurrency cap the jobs manager answered 429. Both halves of that were wrong, and upstream fixed both in 1.5.8. llama-server is launched with one slot, so a cap of four only bought a queue it could not serve. The reported symptom was the giveaway: press Stop, send again, and the new generation sat behind a request that was still running because llama-server had not noticed the disconnect yet. Do it a few more times and four stacked generations fought over one slot until they all drained. And a 429 to someone who just pressed Send is a refusal, when what they plainly want is the new generation and not the old one. So: cancel the oldest live work, wait for it to actually let go, then dispatch. The wait is the part that matters -- llama.cpp frees its slot when it notices the disconnect, and dispatching before that happens queues the new request behind a generation nobody is reading, which is precisely the stall this removes. Job.done is closed by the worker after the response body is closed, so waiting on it means the socket is gone rather than merely the context being cancelled. Three differences from upstream's version: The wait is 5s, not 2.5s. Upstream picked 2.5s because PowerShell's accept loop is single-threaded and the whole server stalls for the duration -- it is a budget for collateral damage, not for the teardown. Nothing else blocks here, so the bound comes from what is being waited on. Reaching it now means something is genuinely wrong. Only enough work is shed to get under the cap, oldest first, rather than every live worker unconditionally. At the default cap of 1 that is identical; above it, shedding everything would throw away generations that had room to run. Ordering is a monotonic seq, because startedAt is unix seconds and two jobs in the same second would sort arbitrarily. Supersede happens after the body is validated, not before. Upstream sheds first and parses second, which lets a malformed request from a buggy client kill a generation the user is reading. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
GO_SERVER.md gains the two sections the new code needs: Runtime tuning (what /perf is, why the override is a perf.toml beside config.toml rather than a rewrite of it, and why a bad one stops the server where upstream's warns), and the supersede half of Generation jobs. Ports get their reasoning recorded next to the discovery order, including what this deliberately does not adopt -- .gobbonet-port and the silent clamp. Also finishes the VERSION-file note left over from the last commit: version.go and the Releases section still described a hardcoded "1.3-go-" stamp that cc715ef replaced with the VERSION file. AGENTS.md documents the whole project, PowerShell included, so its diagram and security table carry the new port numbers with a note on why they moved. GO_MIGRATION_INVENTORY.md is a record of a comparison made at a point in time, so /perf is noted as arriving after it rather than retrofitted into a table of two implementations that never had it. The line counts in AGENTS.md and INDEX.md were stale the moment the merge landed -- fileserver.ps1 1,169 -> 1,967, launch.bat 1,687 -> 2,257, chat.html 906 -> 950, and eleven js/ modules. INDEX.md already says this is exactly the failure mode of a hand-maintained structural index; refreshing them keeps that argument honest until something generates them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two saves landing together could interleave between reading the current tuning and publishing the new one, so perf.toml and the running server ended up describing different settings. The file is the half that survives a restart, which means the disagreement outlives the request that caused it -- the panel shows one context size, the next boot uses another, and nothing reports either. A separate write mutex rather than holding the RWMutex across the whole POST, so a GET is never blocked behind a disk write. The test drives eight concurrent saves and compares what the server reports against what a fresh ApplyPerf reads back off disk. It fails reliably on the unserialised version. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
hardware-probe.ps1 is ~2,000 lines, not ~1,400 -- the figure was right when the platform-coverage section was written and upstream has grown the file since. It is load-bearing there: it is the argument for why porting hardware detection to Go is its own piece of work. stage-web.sh copies 40 files, not 39. 24 js + 15 css is 39, and chat.html makes forty. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Updated to 1.5.8The frontend half merged untouched. This branch has never edited The server half doesn't merge, because on this branch the server is Go. Three things you shipped in
the big one. Port moves 8080 to 9066 and 11434 to 11437. Cooperates with Ollama Supersede instead of 429 cancel the running generation, wait for it to let go, then dispatch. Your comment about the old value describing the bug rather than defending it was the useful part; I ported the reasoning, not just the number. design changesPerf overrides live in a Supersede happens after the request body is validated, not before. No not ported: the have a look aroundGo is not my language of choice, but "single binary, everything included" makes it match the GobboNet philosophy. dependencies
Everything else is stdlib. |
css/01-tokens.css tells the user to drop atkinson-hyperlegible.woff2 into fonts/ themselves -- the font is not in the repo. On the PowerShell server that works, because it serves the repo root. Here it did not: stage-web.sh copies only chat.html, js/ and css/, so a font the user had correctly installed never reached web/ and 404d forever, with no symptom beyond a face that silently never applied. Absent stays a supported state (font-display: swap falls through to the monospace stack, which is why upstream can ship without the file), so fonts/ is staged when present rather than required. Also corrects the '39 files' count in two comments -- the frontend has been 40 files since 1.5.8.
fileserver.ps1 spools each generation to <root>/.jobs/. The .sse file holds the model's reply transcript, so a stray 'git add -A' can commit chat output. .gitignore already covers conversations/, .gobbonet-state.json and the other runtime files; this one was missed. SECURITY.md notes that uninstalling removes the job spool — this keeps it out of version control in the meantime. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> (cherry picked from commit 427f88e)
Follow-up to the cherry-picked ignore, not a correction of it: the line is right and needed. But this branch replaces fileserver.ps1's runtime half, and internal/jobs/ holds spools in memory rather than on disk, so a reader here would otherwise find an ignore for a directory nothing in the Go tree ever creates and reasonably assume it went stale. The ignore stays regardless of what happens to this PR -- fileserver.ps1 is still the server on main.
I incorporated changes from @TheAmericanMaker 's commentary on #16 |
|
y'all are amazing <3 |
Keeping the Go port in step with the rest of the queuereviewing our user's bug reports and PRs, searching for findings that would also apply to the Go port. The port barely touches your files
Because the Go port is almost entirely 100% additive (just copying the GobboNet API into a language that will run outside of Windows), there's not any conflict with upstream. Every open PR, merged into both basesThrowaway worktree,
Across nine open PRs, the port's total merge cost is one trivial A clean merge is not a safe mergeexample: #6 merges into today's 1.5.8 ( Verified with Six of the eight PRs are based at Review of open PRs#6 -- one line. As above. Design is otherwise sound and minimal. #5 -- misses a third door. #4 -- one line to revert. #4 and #5 go together #5 stops #3 -- one line, and it is not the sanitiser itself. What is wrong is that #7 -- stale, recommend closure. 1.5.8 already rewrote the URL-ACL block, Change (2) also doesn't do what it says: Change (3), dropping the LAN rules for the loopback-only #8 -- accept. I verified both digests independently against the publishing Separate note: #11 -- the headline claim doesn't hold. The frontend still hard-gates on an #15 -- one line. Independent of any PR: the base64 search-proxy payload at What the port needs, per PRFive of the eight need nothing from the Go port:
Three need real work, all of it on my side of the fence:
The process from hereWhen one of these merges, mirroring is: merge Happy to rebase this PR onto whatever lands, in whatever order suits you. It |
VERSION said 1.5.1 while the tree carried upstream's 1.5.8 frontend -- the ports at launch.bat:134 and :160 are 11437 and 9066, which are 1.5.8's. So every stamped build reported 1.5.1-go-<sha>, naming a release it was not built from, from `gobbonet version`, the startup banner and /health-fileserver at once. This is the second time. build-release.sh's own comment records the first: two separate literals drifted to 1.3 and 1.4 while the tree carried 1.5.1. Consolidating them into one file stopped the literals disagreeing with each other. It did nothing about the file disagreeing with upstream, which is the failure that actually reaches a tester. The release half is upstream's number, not ours, so a test now holds VERSION to the nearest upstream release tag reachable from HEAD and prints the correct value when it does not match. The tag is the check and not the source. Deriving VERSION from it would need a clone with tags fetched, which a release build cannot assume, and deriving it from js/01-config.js's CHAT_HTML_BUILD is worse: that string was '2026-05-16-nemo-strict-template-fix' at v1.5, v1.5.1 and v1.5.3, and only started carrying a version number at 1.5.4. Where there is no tag to read -- a shallow clone, an export -- the test skips and names what it did not verify, rather than passing quietly. Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Assisted-by: John McCardle <mccardle.john@gmail.com>
@neoliminal - pin SHA-256 of engine + embed model (#8), strip run-flags from transferred state (#5), sanitize GGUF metadata reaching generated .cmd (#3), escapeHtml fallback (#4), firewall / URL ACL scoping (#7). @wizzense - fall back to loopback when the LAN bind is denied (#10). @jmccardle - Linux wine catch, roadmaps for Linux (#2). @DawidKorach - stable port assignments, llama health registration in the CMD (#14, #15). Co-authored-by: neoliminal <john.kipling.lewis@gmail.com> Co-authored-by: wizzense <37890504+wizzense@users.noreply.github.com> Co-authored-by: John McCardle <mccardle.john@gmail.com> Co-authored-by: Dawid Korach <dawidk6@gmail.com>
Upstream shipped the contributor patches this fork submitted -- the batch and cmd argument sanitisers, the card-code neutralisation on restore, the purge that actually clears IndexedDB, the CSS.escape and escapeJsAttr corrections -- so the frontend arrives here already carrying them and the merge is a clean fast-forward of files this branch never touched. Two upstream changes need a Go-side answer and get one in the commits that follow: web search no longer runs as a separate process on 11435 (fileserver.ps1 serves /search itself), and the model catalogue in launch.bat changed shape, which installer/models.ini is generated from.
Upstream deleted the search relay: a hidden-window PowerShell started with -EncodedCommand that bound 11435 and forwarded authenticated requests to ollama.com. fileserver.ps1 now makes that call itself. The Go port never started that process at all, so its default search_url of 127.0.0.1:11435 pointed at nothing on every install and /search answered 502 whenever a user switched search on. Pointing search_url at the API is the whole port -- the existing proxy already strips our session cookie and forwards the browser's own Authorization, which is exactly what Handle-Search does by hand. /search/health is the one thing a plain proxy cannot do: the client probes it before every search and the API has no such route. It is answered here, and reports configuration rather than reachability -- an unauthenticated probe cannot tell "offline" from "bad key", and the search request itself reports the real failure a moment later.
1.6.0 clamps it in identify-model.ps1, at the source, because the value is raw UTF-8 out of a downloaded GGUF and lands in a `set "MODEL_ID=..."` line of a .cmd the launcher CALLs. This program cannot be injected that way -- llama-server is exec'd with an argv slice, and the record reaches disk through encoding/json -- but the arch string still becomes rec.ID and rec.Family for every model that falls through to the generic branches, and both travel to the browser in active-model.json. Clamping at the same place keeps the port readable against its original and leaves one choke point rather than a rule each consumer has to remember.
1.6.0 moved the $min VRAM table and the vram -> pick ladder out of an inline `powershell -Command` one-liner in launch.bat and into hw-recommend.ps1. gen-catalog.py parsed them out of launch.bat, so it did what it is built to do and refused to emit a catalogue rather than one with no VRAM warnings in it. Regenerated models.ini follows the release: Gemma 4 E4B replaces Gemma 3 4B at slot 1, Qwen3.5 9B replaces Llama 3.1 8B at 4, Qwen3.6 35B-A3B replaces Qwen3 30B at 6 (gate raised to 24 GB, default context dropped to 8192 so the KV cache fits beside 22 GB of weights), and slots 7 and 8 pick up upstream's fixes for two URLs that 404'd. All ten resolve on HuggingFace as of this commit. Also cross-checks $min against launch.bat's PICK_MIN list. They are the same gate maintained twice in two languages, upstream's own comment says they must agree, and slots 9 and 10 had no PICK_MIN at all until this release -- so the menu warned about a threshold the download did not enforce, in the direction that lets a 19 GB model onto an 8 GB card.
The startup banner printed "[OK] serving on http://..." and then, if the bind failed, an error underneath it. That reads as a server that started and then broke, which is the wrong thing to go looking for. Listen() now binds first and Serve() takes the listener. An in-use port gets the explanation launch.bat gained in 1.6.0: it is usually a GobboNet from an earlier run, closing a window does not always stop it, a reboot clears it -- which is why "custom ports do not work" and "I rebooted and it fixed itself" are the same report. Naming the holding PID is the one part not ported; it needs Get-NetTCPConnection and has no portable equivalent. Windows reports this as WSAEADDRINUSE rather than EADDRINUSE, so the check covers both.
installer/vendor/llama-cpp is fetched by hand, and the copy on this machine turns out to be the CPU-only Windows asset: same filenames as the Vulkan one, minus ggml-vulkan.dll. An installer built from it would have set gpu_layers 99, offered the same 16 GB models, and run every one of them on the processor -- no error at any layer, just a machine that takes a minute to answer, which reads as "this software is slow" rather than "this build has no GPU backend". The check is one file test, and LLAMA_BACKEND=cpu is there for anyone building a CPU-only installer deliberately. No installer is published from this tree until a Vulkan engine is in place.
TheAmericanMaker
left a comment
There was a problem hiding this comment.
The commits in this range represent a clean, robust, and security-conscious update that brings the Go backend and installer in lockstep with the upstream 1.6.0 release. All behavioral contracts and security constraints are well-tested and documented.
|
Two live reports on Discord line up with something concrete in here — extracted the actual v1.6 "How do I start the LAN launcher on Linux?" — there isn't a separate one, and that's the actual bug. "It says it's open on an IP but I can't connect" — checked Two real, scoped fixes if useful:
Didn't have a Go toolchain handy to build and verify a patch, so posting the diagnosis rather than guessing at one — happy to build it if it'd help. |
gobbonetis a single distributable binary that replaces the API logic infileserver.ps1andlaunch.bat. With the GobboNet API itself implemented in Go, we continue using PowerShell for Windows setup functions, while the API itself makes the GobboNet front-end available on Linux and Mac.This branch also includes my attempt at an NSIS installer which uses the powershell scripts as steps in the install wizard.
Modifications to Front-End Facing Code
chat.html,js/,css/,launch.bat,fileserver.ps1are unmodified. Linux, Windows, and Mac get the same API.hardware-probe.ps1is given a new optional parameter,-IniPath, because NSIS has no JSON parser.The new argument makes
hardware-probe.ps1something that can be used in the installer: the wizard can suggest models at setup, and the selected models can be downloaded with a progress bar and no additional setup. My goal was a "start GobboNet" checkbox at the end of the installer, which already hasllama.cppand at least one model ready to run.GO_MIGRATION_INVENTORY.mdrecords every behaviour carried over fromfileserver.ps1, including the 1.4-to-1.5 re-porting changesIdeas worth borrowing even if my PR can't be merged
CGO_ENABLED=0,-trimpath;build-release.shcross-compiles linux/amd64, linux/arm64, windows/amd64, darwin/arm64 and darwin/amd64 and bundles each withweb/and aSHA256SUMS.gobbonet version, the startup banner, and/health-fileserver. A user or tester can copy a build identity out of a browser without even using command prompt. The git commit would show if they're on a prerelease or release build.launch.batalready notes this). The.ggufdownload stays online (inert data, and too large to bundle) and the Go version borrowslaunch.bat's integrity policy exactly: hash mismatch fatal, LFS-pointer detection, sub-1GB backstop.~/.config/gobbonet,~/.local/share/gobbonet), with the Windows layout —config.tomlnext tolaunch.bat— still last in the discovery order, so a portable install works unchanged.Known gaps
Overview for review
internal/server/conformance_test.go,internal/jobs/llama-serverlifecycle)internal/supervisor/internal/config/,GO_CONFIG_SPLIT.mdinstaller/gobbonet.nsi,installer/gen-catalog.py,installer/README.mdGO_SERVER.md,GO_MIGRATION_INVENTORY.md