The /model rows come from a documented setting, and say which computer they mean (#1185, #1177) - #1241
Merged
Conversation
|
📘 Docs preview — the preview channel for this PR has been deleted now that it is closed. |
gen16k
added a commit
that referenced
this pull request
Sep 5, 2026
…the page (#1215) Every ja page carried a sourceHash in its frontmatter — a digest of the English page it was translated from — and i18n:check compared it. That is a derived value stored in a versioned file: two PRs touching one English page always rewrote it to two different values and always conflicted, on that line and nothing else, while the prose merged cleanly. Measured on main over the 60 days before this: 219 of 622 commits touch a ja page, 90% of them rewrite the hash, and 35 of 144 overlapping PR pairs share a ja file. At one or two docs lanes that is a cost per collision. At today's lane count it is a condition on landing — #1198 held 12 of the 32 ja pages for 2 h 19 m and went CONFLICTING four times, each within nine minutes of a competing PR merging, and the answer of the day was to hold everyone else's merges. It is not only wide docs PRs: #1237, a gateway change that touches three docs pages incidentally, spent two of its four force-pushes on the same line. This PR itself went CONFLICTING 2 m 40 s after it was opened, before its own CI finished, on one line of ja/troubleshooting.md. Removing the stored value removes the class. The freshness question moves to the diff, where nothing is stored and nothing conflicts: a PR that changes an English page changes its Japanese one too, enforced by scripts/ci/i18n-pair-guard.sh in docs-guard.yml, with a `translation-not-needed: <reason>` line in the PR body for the English edit that genuinely needs no translation. Replayed over every commit since the mirror was enforced (#147), that line would have been needed six times across 405 page pairs — three terminology sweeps where an English phrase changed and the Japanese sentence was already right — and nothing else in the repository's history would have failed the rule. A merge driver was considered and does not work: a custom driver only runs where git config sets it, and GitHub computes mergeability server-side, so the PR still goes DIRTY and still has to be rebased. i18n:check keeps the two questions that are about the tree and cannot be answered from a diff: is there a ja page at all, and do the two sides still have the same shape (#678, #1011). That comparison now runs on every pair, always, instead of only on pairs whose hashes already agreed — and that gate was worth less than it looked, because on the path a stale pair actually takes, --accept wrote the hash before it ever reached the refusal, recording a lost heading as intentional and reporting it only on the next run. --accept and i18n:accept are gone with the value they wrote. So is the "may be out of date" notice in PageTitle.astro and the second copy of the digest it carried: deploy-docs.yml runs i18n:check before the build in the same job, so a page the notice would fire on never reaches the site, and it has never rendered on docs.waired.ai since it was added alongside the check in #147. The rule and its opt-out are written where a contributor already reads about the sibling one — CONTRIBUTING.md next to docs-not-needed:, and CLAUDE.md §Documentation, whose "npm run i18n:check gates it" was true of freshness before this change and is not any more. Sequenced directly with the lanes that had docs PRs open (#1235, #1237 first; #1241 after), because this touches all 32 ja pages at once. Fixes #1215 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BgrcRiYj339QxzD3b6KijQ
…r they mean (#1185, #1177) The Waired rows were written straight into Claude Code's own discovery cache. Not by choice: discovery is credential-gated and waired supplies none, so the picker's fallback read of that file was the only way the rows appeared at all. The price was a canary that re-measured somebody else's private file shape every release. Claude Code v2.1.242 documents `modelPicker`, which says the same thing out loud. The rows go there, in the user's own settings.json — not managed settings: it needs no elevation, and because Claude Code takes the whole lineup from the highest source that sets the key and never merges two, an organisation's managed lineup outranks waired's whole instead of being silently merged into. The same rule is why a lineup that is not ours is left alone rather than replaced, classified the way modelsetting.go already classifies a default model. The rows gained a second line, which is the part people will notice: a `modelPicker` row has a description and the picker renders it. The per-peer rows no longer fold the model name into the label — the computer names the row, the model describes it. The ids lost their `claude-` and `anthropic-` heads. Those existed to pass discovery's id filter, and `modelPicker` filters nothing. The one other thing the heads decided was that CLAUDE_CODE_MAX_CONTEXT_TOKENS is honoured only for ids that do NOT start with `claude-`, which is why the local id alone was spelled `anthropic-`. That arrangement started costing something once 2.1.26x began enforcing an assumed window on catalog-unknown ids: every `claude-`-headed row ran in a 200k session and put a notice on screen saying the id is not in the model catalog. With no head at all the variable applies to every row and the notice is gone. It is a compaction hint either way; what actually refuses a too long prompt is this gateway's own 400. Every row whose side declares a 1M window now has a `[1m]` twin, and a twin is offered only where a node declares it — a tier is a promise about the serving node, and a twin with nothing behind it is a menu entry whose selection fails. There has been no 1M row at all since the crossing to Anthropic was removed; this is how it comes back, fail closed. RequiredWindowForRequest honours the tier on node-naming rows now, which it deliberately did not before: picking "Waired local (1M context)" IS the demand, and serving it at 200k would be the surprise. inference off, not only where there is no engine. rc5 found it offered on a machine that answers nothing, and picking it failed every turn. SubsystemStateDisabled is that intent on the wire. Every previous spelling still routes, per-peer ids included, and the new test says so by name — a session keeps the id it was started with. The canary stops measuring a file waired no longer writes. It measures the two things that would break this quietly instead: that the lineup waired writes parses, and that the window variable still applies to a non-`claude-` id. Asymmetric on purpose — a notice on our spelling fails, no notice on the `claude-` control only warns, because upstream dropping the enforcement costs waired nothing but leaves the predicate unexercised. One thing is worse than before and the docs say so. The cache was read after SessionStart hooks ran, so a hook could refresh it for the session about to start. Settings are read before the watch that would notice a hook's write is armed, so a row written by the hook first appears in the next session. Measured, not assumed: a synchronous hook write and writes at 1s, 2s and 3s are all missed; 6s and 15s land. That is a race, not a contract, so the hook does not try to win it. Only the per-peer rows and the presence of the public and 1M rows move with the mesh. Fixes #1185 Fixes #1177 Refs waired-ai/waired#1313 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJUUSmAfbRdjER1seDdjXm Signed-off-by: gen16k <gen16k@users.noreply.github.com>
gen16k
force-pushed
the
feat/1185-model-picker-rows
branch
from
September 5, 2026 18:37
460e42e to
11c86f8
Compare
This was referenced Sep 5, 2026
Merged
gen16k
added a commit
that referenced
this pull request
Sep 12, 2026
…he same thing (#1308, #1310, #1272, #1274) (#1325) * claude: remember every hook command waired has written `waired claude _models-cache write --from-managed` was renamed to `waired claude _picker write --from-managed` in #1185/#1241, and nothing was left that recognised the old spelling. A marker identifies OUR entry, so a rename with no forwarding address stranded every SessionStart entry written before it: `waired claude disable` — and so `uninstall.sh --clean`, which runs it — left the entry on disk, and a re-enable appended a second entry beside it instead of replacing it. The stale command names a subcommand that no longer exists, so it fails at every Claude Code session start (silently on the Unixes, one line of stderr per session on Windows). Measured on three hosts running 0.0.3-rc6; the fixture is the byte-exact string left on an Ubuntu host by `uninstall.sh --clean`. Removal and replacement now read a marker LIST per hook — today's plus every retired one — the shape the retired Stop hook (fallbackHookMarker) already had for exactly this reason. Refs #1308 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VB9qaNLWCAdpnoMNW9ZY1H Signed-off-by: gen16k <gen16k@users.noreply.github.com> * uninstall: tear the Claude integration down while the agent is still up `waired claude disable` tells its own CLAUDE_CODE_MAX_CONTEXT_TOKENS from an operator's by resolving this host's context window, and it resolves it by asking the running agent. macOS stopped the LaunchDaemon first and ran the teardown third, so the window was always 0 there, the key was always kept, and an uninstalled machine went on steering every Claude Code session that started on it. Linux and Windows already had the right order; this moves macOS onto it. waired-agent#1174 anticipated exactly this and added a warning for it -- which all three branches then discarded, macOS and Linux with `2>/dev/null` and Windows with `2>$null | Out-Null`. The warning was written for the uninstall transcript, so let it reach the transcript. The per-user Windows call keeps its quiet form: unelevated, it can only report a permission it was never going to have. Refs #1308, #1174 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VB9qaNLWCAdpnoMNW9ZY1H Signed-off-by: gen16k <gen16k@users.noreply.github.com> * sign-out: take local inference off the Claude gateway The :9472 listener is built at boot -- it has to answer before enrollment -- and the local-inference handler is published once, at activation. Sign-out had no way to unpublish it: SetLocalInference(nil) returned early and nothing else wrote the pointer. So a signed-out daemon kept serving out of a torn-down session, and the gateway's per-request lazy start brought the inference engine back up, while `sudo waired doctor` said local inference was offline and :9473 refused connections. Three surfaces, three answers, all measured on one macOS host after `sudo waired logout`. The engine itself already stops on sign-out (the session context runs ollama.Stop/vllm.Stop and teardown waits on it) -- measured, not read off the types, which is what waired-agent#1274 asked for. What was missing was anything stopping it from being started again. intercept gains Deps.LocalServing, a probe rather than a second Set call because the answer belongs to the switchboard and changes without this package hearing about it. A Waired id then fails with the sign-out reason in the existing waired_cannot_serve shape -- 400, because Claude Code retries 5xx ten times before showing anything -- and /v1/models stops splicing Waired ids into the picker, so /model offers nothing that cannot run. `waired doctor` stops telling a signed-out computer its turns will go to another of its computers. It has no others; it is off the mesh. Fixes #1274 Refs #1310 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VB9qaNLWCAdpnoMNW9ZY1H Signed-off-by: gen16k <gen16k@users.noreply.github.com> * status: stop reading "I cannot look" as "this computer is signed in" The system state dir is 0700 root (SYSTEM+Administrators DACL'd on Windows) whether or not anything is enrolled inside it, so an unelevated caller gets fs.ErrPermission either way. `waired status` turned that one error into "This computer is signed in system-wide" -- measured on macOS in the same minute `sudo waired status` said "Not signed in." on the same machine, right after `sudo waired logout`. Absent, unreadable and enrolled are three states, which is what #1005 established for doctor and #1269 for the app. The answer for an unreadable directory comes from something that actually observed the enrollment: the background service, over the management socket, which is bound 0666 precisely so an unprivileged caller can reach it -- and which `waired doctor` already asks. Three outcomes now: it says signed in (today's wording), it says signed out (the plain "Not signed in" message the caller already had), or it does not answer (say that, rather than pick one). `waired auth status` and doctor's state-directory row take the same argument; doctor's only consumer of that classification is its daemon-is-enrolled branch, so its wording is unchanged. Refs #1272, #1310 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VB9qaNLWCAdpnoMNW9ZY1H Signed-off-by: gen16k <gen16k@users.noreply.github.com> * sign-out: take the Waired rows out of /model, and say what stays Owner ruling 2026-09-12: close the local side of the Claude gateway on sign-out, and stop offering Waired models in /model. The rows live in the user's own ~/.claude/settings.json since #1185, so removing them needs no elevation -- which is what makes this compatible with sign-out asking for none (docs/decisions/20260907/0230). `waired logout` drops them for the invoking user; the SessionStart hook, which would otherwise rewrite them at the next Claude Code launch from a base URL that is still in managed settings, now asks the daemon first and clears instead. Only an explicit "signed out" clears: a daemon that did not answer is what every machine looks like for a moment at boot. Sign-out then says what it deliberately left behind -- the Claude Code integration itself, because changing it needs administrator rights -- and that this computer stays in the device list as signed out. That last line is the `-1` on the next sign-in, told before it happens instead of found afterwards. Also corrects runLogout's own doc comment, which claimed the preserved row is recoverable via `waired init`. It is not: the control plane matches an enrolling device on its machine key, and sign-out deletes secrets/machine.key. Coming back as the same device is filed as #1323. Refs #1310, #1308 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VB9qaNLWCAdpnoMNW9ZY1H Signed-off-by: gen16k <gen16k@users.noreply.github.com> * logs: bound the service-log child in both directions `waired logs` shelled out to journalctl / log / Get-WinEvent with no process group and no WaitDelay. CommandContext's kill then reached only the direct child, and CombinedOutput went on waiting for every inherited pipe writer to close -- so one surviving grandchild held the command open for as long as it lived. The command a person runs when something is already wrong is the worst one to hang. Own process group plus a Cancel that signals the group on the Unixes (the treatment the engine spawner and the HF downloader already give their children), and a 2s WaitDelay on all three. The unix test spawns a grandchild that outlives the shell: without the fix it hung for the full 30s bound, with it the read returns at cancellation. This is the in-tree half of waired-agent#1308's macOS "log stream" orphan. The orphan itself is NOT one of ours -- nothing in this repo runs `log stream`, `waired logs` runs `log show`, and its predicate is `process == "waired-agent"` rather than the substring match the reported process carried. Noted on the issue. Refs #1308 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VB9qaNLWCAdpnoMNW9ZY1H Signed-off-by: gen16k <gen16k@users.noreply.github.com> * installtest: make the uninstall legs subjects of the Claude file None of the three uninstall legs asserted anything about managed-settings.json except Windows, and Windows only checked that the file was absent -- against a host this same build had enrolled, so its SessionStart hook carried today's command and the leftover the issue is about could not appear. That is why macOS shipped an uninstall that ran `claude disable` after stopping the daemon, and why a pre-rename hook survived --clean on every OS. All three legs now plant the pre-rename shape (`_models-cache`, byte for byte as measured on a real host) before the uninstall, the way this harness already plants a staging directory and the Vulkan machine env vars for the same reason: an assert over a victim nothing created is vacuous. macOS additionally asserts the ORDER -- the Claude teardown before the service teardown -- which is the half the file contents cannot show on a runner with no engine to resolve a context window from. Refs #1308 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VB9qaNLWCAdpnoMNW9ZY1H Signed-off-by: gen16k <gen16k@users.noreply.github.com> * intercept: fold the one-line wrapper into WriteCannotServe Its only call site now passes a reason, so the no-argument form was unused. The contract it documented (400, the error type, the escape hatch) moves onto the function that still exists. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VB9qaNLWCAdpnoMNW9ZY1H Signed-off-by: gen16k <gen16k@users.noreply.github.com> * docs: say what a sign-out stops, and what it deliberately leaves The CLI reference told people `waired status` "reports that the computer is signed in system-wide and stops there", and the troubleshooting page said it "rather than guess, tells you the device is enrolled" -- which is the guess waired-agent#1272 is about. Both now describe what it actually does: ask the background service and say what the service says, including "cannot tell" when neither source is available. The logout section gains what sign-out stops (local inference, the /model rows) and the two things it deliberately does not touch: the Claude Code integration, because changing it needs administrator rights and signing out does not ask for any, and the device-list entry, which is where the "-1" on the next sign-in comes from. The decision record is the answer to waired-agent#1274, with the measurement it asked for: the engine already stops on sign-out; what was missing was anything stopping it being started again. Refs #1274, #1310, #1272 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VB9qaNLWCAdpnoMNW9ZY1H Signed-off-by: gen16k <gen16k@users.noreply.github.com> * statehint: name the Windows half of the channel too The comment described the unix socket's 0666 mode as if it were the whole story; the Windows named pipe grants Interactive Users, which is what makes the same unelevated read work there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VB9qaNLWCAdpnoMNW9ZY1H Signed-off-by: gen16k <gen16k@users.noreply.github.com> --------- Signed-off-by: gen16k <gen16k@users.noreply.github.com> Co-authored-by: gen16k <gen16k@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The Waired rows of Claude Code's
/modelpicker move from Claude Code's private discovery cache to the documentedmodelPickersetting, the reserved ids are re-spelled, every row whose side declares a 1M window gets a[1m]twin, and the local row is dropped where local inference is turned off.Decision
docs/decisions/20260903/0333§5 (publication) anddocs/decisions/20260906/0330(this PR's own ruling). Lane L97 of waired-ai/waired#1312.What changed
Publication.
internal/integration/claudecode/modelpicker.gowrites amodelPickerlineup into the user's own~/.claude/settings.json, classified absent / ours / foreign / unreadable before it is touched — the same shapemodelsetting.gouses for a default model. Foreign is left alone and reported: Claude Code takes the whole lineup from the highest source that sets the key and never merges two, so writing over one deletes it. User scope rather than managed for the same reason and one more — it needs no elevation, so it can follow the mesh at session start, and an organisation's managed lineup outranks it whole.Gone:
gatewaycache.go,cmd/waired/claude_models_cache.go,scripts/ci/canary-cache-schema.py,anthropic_models_cache_sync_test.go, andCLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERYfrom managed settings (scrubbed on write, and only the value waired itself wrote). The SessionStart hook stays; its command is_picker write --from-managednow and the marker is a prefix, so older forms are replaced automatically. The gateway keeps serving/v1/modelsfor the OpenCode surface.Ids.
waired/waired/local/waired/peer/waired/peer-<node>/waired/public, and<id>[1m]. Every previous spelling still routes —TestEveryPreviousSpellingStillRoutesnames each one, and per-peer ids map onto the current prefix by slug so one resolver covers both.1M twins. Offered only where a node declares a 1M window; none for public share, because this host learns someone else's window only when it answers.
RequiredWindowForRequesthonours the tier on node-naming rows now, which it deliberately did not before — the comment said naming a node must not also make demands of it, and that is still true of the bare row, which still returns 0. A twin is a different row, only ever offered where the node declares the window, so picking it IS the demand.#1177.
localServesreadsSubsystemStateDisabledas well as "no engine at all". rc5 found the row offered on a machine whose operator had turned local inference off; every turn on it failed.Measured against Claude Code 2.1.261, not assumed
docs/knowledges/20260906/0340-the-model-picker-measured-again.mdhas the full record. Three things corrected an earlier on-device measurement:A
modelPickerrow has a description and the picker renders it as the second line; a row without one readsCustom model (<id>). The note inpeerdirectives.gosaying there was no description field and every row read "From gateway" was about the private cache. The per-peer rows stop folding the model into the label.CLAUDE_CODE_MAX_CONTEXT_TOKENSis honoured only for ids that do not start withclaude-(!id.toLowerCase().startsWith("claude-")). That is what the old heads were really deciding, and why the local id alone was spelledanthropic-. It went uncosted until 2.1.26x began enforcing an assumed window on catalog-unknown ids: with aclaude-head the variable is ignored, the session runs at an assumed 200k, and the picker puts"claude-waired-peer" isn't described by this version's model catalog…on screen. With no head, the variable applies and the notice is gone.A SessionStart hook's write to settings.json does not reach the session it starts. Settings are read at startup and the watch is armed after; the cache was read after hooks ran. Measured: synchronous, 1 s, 2 s and 3 s all missed; 6 s and 15 s land. A race, not a contract, so the hook does not try to win it — the rows a session sees are the ones written before it started, and the docs say so. Only the per-peer rows and the presence of the public and 1M rows move with the mesh.
Also confirmed and not used:
behavesAson a picker row suppresses the catalog notice without changing the id on the wire, but appears in no public doc (settings-reference, managed-settings, sub-agents, llm-gateway-protocol, model-config all return zero). Depending on it would cut against the point of this lane. Reasoning in the decision record.The canary now measures something that can still break
It stopped testing a file waired no longer writes. Two legs against the real binary instead: (a) the lineup waired writes parses — a row Claude Code cannot read is dropped silently, so its settings warning is the only signal; (b)
CLAUDE_CODE_MAX_CONTEXT_TOKENSstill applies to a non-claude-id. Asymmetric: a notice on our spelling fails, no notice on theclaude-control only warns, since upstream dropping the enforcement costs waired nothing but leaves the predicate unexercised.Run locally against 2.1.261 — every check including both new legs passes.
Also fixed, found on the way
internal/integration/adapter.goclaimed~/.claude/settings.json is NEVER touched. Untrue since the status line shipped; it now names the three keys waired owns and the ownership rule.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFICbecause docs-site documented it as a cause of missing entries. That guidance was about the cache read and is gone from the page, so the check goes with it.docs/decisions/20260820/0400's supersede note recorded the write target moving; it now also records that its own measurement (the cache is read after the hook) inverted.Checks
go build ./...,go test ./...,gofmt -l,golangci-lint run --max-same-issues=0,bash -n+shellcheck -xon the canary,npm run i18n:check(32 pairs in sync),npm run buildon docs-site. Rebased on main after #1234.Not yet verified on a real waired host — the picker measurements above were taken against the real
claudewith a stub gateway, butwaired claude enable→/modelon a machine running the daemon is the after-check, and it is the next thing I do.Fixes #1185
Fixes #1177
Refs waired-ai/waired#1313
🤖 Generated with Claude Code
https://claude.ai/code/session_01HJUUSmAfbRdjER1seDdjXm