router: the Public Share refusal names which of your settings declined (#1201, #1252) - #1256
Merged
Merged
Conversation
peerOnlyMissNote had three arms and publicGateFor collapsed four causes into one zero value, so a turn refused because the operator's own Public Share posture is off came back as "no public machine is reachable right now" — not vague but false, since the grant acquirer releases every held grant while the posture is off. publicGate now records WHICH switch refused (never consented / switched off / main-agent turns / sub-agent turns), and the settings arms are ordered ahead of the reachability arm. Consent is tested before mode because EffectiveMode already folds "never consented" into "off" before the policy reaches the router. The reason is taken from the gate the attempt actually used rather than re-read from the policy, which is republished under an atomic pointer while a selection runs. The consumer's own Public Share floor is counted separately from the operator's routing floor: two settings, two commands, and folding them would send an operator to a switch they did not set. It deliberately does not feed the SizeFloorError wrapper. The refusal also leads with the entry that declined instead of with the mesh. The person picked one /model row; "no mesh peer is available (...); local state=..." answered with two Waired-internal facts about a machine the turn was never going to run on. Related: the operator's routing floor no longer answers for a public-only turn at all. That floor disqualified this host's engine, which this route was never going to use, so `waired worker set --min-model-size` is the wrong switch to name — narrowing waired-agent#1128's floor-first order by this one case. Separately (waired-agent#1252), ModelIsArriving keyed on this host's local model state even on branches that refuse to run here, so a host midway through a download answered every peer-only, pinned and public refusal with 503 + Retry-After and the reason never reached the client. ModelNotReadyError now states whether local arrival is evidence at all, false by default so a future branch that forgets cannot restore #788 silently. Refs #1201, #1252 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJUUSmAfbRdjER1seDdjXm Signed-off-by: gen16k <gen16k@users.noreply.github.com>
The page promised the entry names "which of the two reasons it was", and the two it named were the two that are not about the operator's own settings. It also implied the posture-off case could not arise, because the picker leaves the row out — but a session keeps the entry it last picked, and the pre-#1185 spelling is still routed. troubleshooting gains two rows in the table that is already keyed on how the message starts: one for the settings that decline, one for the two world-state reasons that are not a fault. Refs #1201 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJUUSmAfbRdjER1seDdjXm Signed-off-by: gen16k <gen16k@users.noreply.github.com>
|
📘 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 6, 2026
… every page in both languages (#1264) Restructures docs.waired.ai around the order a person actually meets Waired, and rewrites every page from zero in both languages against a saved corpus of human-written documentation (Tailscale and Discord for English; LINE Developers, SmartHR, and the JTF style guide for Japanese). Owner request of 2026-09-06. ## Structure - 32 page pairs become 56, in eight sidebar groups that follow the journey: Get started → Install and set up → Use it from your tools → Models and routing → The Waired app and console → Fix a problem → How it works → Reference. Every group and item has a Japanese sidebar label (the ja sidebar was all English before). - The four longest pages are split so a reader lands on the thing they came for: first-run becomes Sign in / Set up in the browser / Set up in the terminal / Servers and auth keys / Run setup again; troubleshooting becomes a hub plus six symptom pages; the CLI reference becomes a hub plus five command-group pages; the Claude Code guide gains How turns are routed / Subagents / The status line. - New pages for things that had none: Meet the Waired app, How Waired chooses a model, Choose which computer answers (routing), Share this computer, Notices, The Status… report. - URLs the product prints are unchanged (`/quickstart/`, `/public-share/`, `/reference/cli/`, `/reference/model-catalog/`, `/reference/install-options/`). Moved pages get 301s in both `astro.config.mjs` and `firebase.json`: `/getting-started/first-run/` → `/getting-started/sign-in/`, `/guides/models/` → `/guides/choose-a-model/`, `/guides/public-share/` → `/public-share/`, and their `/ja/` twins. The first two of those were already 404s on the live site. - The `what-is-waired` figure is now HTML and CSS instead of an SVG, so it follows the site theme and is readable in both languages. ## Prose - English: one voice (Tailscale's), the reader is "you", UI actions are *select* / *press* / *run*, UI labels are `**bold**`, product output is quoted verbatim, no dash chains or parentheticals. - Japanese: 敬体, no space between Japanese and Latin characters (LINE style), [操作対象] / 「表示文字列」 / screen names without brackets, dictionary-form task headings. The 2026-09-06 owner decisions are recorded in `docs-site/TRANSLATION.md` §Register, next to the 106 existing term rulings, all of which are applied. - Japanese paragraphs are written on one line. Browsers drop a soft line break between two Japanese characters but render one next to a Latin character as a space, so a wrapped source cannot keep the no-space rule. The rule and its reason are in TRANSLATION.md. - Retired vocabulary is gone from both trees: the AI / your AI, graphics card, graphics memory, AI engine, speed check, under-spec, AI model, tray (outside the program name `waired-tray`), and the bare 「窓」 for context window. - The owner-ruled public wording on what reaches Anthropic (waired#1313) is on the Claude Code guide and the privacy page, with the fail-closed decision cited. ## Kept in step with main Three commits landed on `main` while this branch was open and are carried into the pages that now hold the text: Qwen3.8 Flash Next and the never-chosen Qwen3.5 122B (#1259, on How Waired chooses a model), the `none here` form of the local window line (#1258, on the Claude Code troubleshooting page and the coding-tools CLI reference), and the Public Share refusal wording (#1256, in the base). Two in-flight PRs were coordinated with their sessions and are written into these pages ahead of them, so their rebases keep the text: the lighter variants of Qwen3.8 27B and Qwen3.6 35B-A3B and the catalog memory-column sentence (#1268, which lands first), and the app labels that the fix for #1269 introduces (`Sign in…`, `Pause Waired` / `Resume Waired`, and the `○ Paused` top line). The app's Disconnect and `waired pause` are one switch, so the pause page now has one section for it. Until #1269 lands the app still shows `Sign in...`, `Disconnect`, and `○ Disconnected`. ## Guards and tests - New `scripts/ci/docs-links-check.mjs` resolves every root-relative link and `#anchor` in `dist/` and runs after the build in `deploy-docs.yml`. Astro only fails on a bad sidebar slug; body links to renamed pages built green and 404'd before. - `i18n:report`: 56 pairs, 0 out of sync. `i18n-pair-guard` and `docs-surface-guard`: OK against main. - Two Go test comments cite the pages that now hold the sentences they quote (`internal/router/reason_lines_test.go`, `internal/gui/tray/status_report_test.go`). No pinned string changed. `go test ./internal/router/ ./internal/gui/tray/` passes. ## Screenshots Four of the five captures are real and included: - `setup-wizard-progress.png` and `setup-wizard-progress-ja.png`, taken with Playwright against the development console during a real 1.9 GB download, masked in the DOM (device renamed to `my-desktop`, account chip and pre-release banner hidden). - `app-ready.png` and `app-not-signed-in.png`, taken on a Mac at 2× in the dark appearance: the menu opened and read through System Events, captured on its own rectangle, and the account row repainted in the image so no real address ships. `CAPTURES.md` records the method. `claude-code-statusline.png` stays a labelled placeholder until a release prints the `⚡` segment the docs quote (0.0.3-rc5 prints it without the prefix); tracked in #1270. Taking the signed-out capture found that the app's **Sign out…** on macOS signs nothing out; filed as #1269. The fix for it also changes the app's `Sign in...` label to `Sign in…`, so the docs quote that form; until it lands the app shows three periods. ## Why one PR The plan allowed two. Get-started and install pages link forward to guide, troubleshooting, and reference pages that only exist in the new structure, so landing the first half alone would have shipped 404s. Review by group is still possible: each group is one sidebar section, and each commit is one slice. Refs waired-ai/waired#1314 Fixes #1254 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_019puN41reeo6o9gXkiRTbBr
This was referenced Sep 22, 2026
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.
A turn refused on the Waired public share entry told the operator the
opposite of what had happened. The
/modelpicker hides the row when PublicShare is off, but the id still arrives — a session keeps whichever entry it
last picked, and the pre-#1185 spelling is still routed for exactly that
reason — and the refusal came back as:
Nobody was unreachable. The operator's own posture was off, and while it is
off the grant acquirer releases every held grant, so the map holds no provider
and the reachability arm fires. Not vague — false.
Why it read that way
peerOnlyMissNotehad three arms, andpublicGateForcollapsed fourcauses into one zero
publicGate: policy unwired, mode off, never consented,and the per-class
Main/Subtoggles. All four produceauto == false, sothe arm that names the posture was unreachable for them, and they fell through
to the two arms that describe the world.
A fifth cause was invisible too: peers dropped by the consumer's own Public
Share size floor were not counted anywhere —
publicShortfall.belowFloorcounts only the operator's
Inputs.MinModelSize.What this does
publicGaterecords which switch refused. Five explicit returns frompublicGateFor, with consent tested before mode:EffectiveModealreadyfolds "never consented" into "off" before the policy reaches the router, so a
mode-first order leaves that case permanently unnameable. The reason is taken
from the gate the attempt actually used rather than re-read from the policy —
the policy is republished under an atomic pointer while a selection runs, and
a
waired public use --offlanding in between would explain the refusal witha posture that did not cause it. (Same hazard
effectivePrefavoids byreading the routing preference once and handing it down.)
The settings arms come before the reachability arm. That ordering is the
defect. Reachability still comes before the auto comparison, which the old code
already had right: with nobody lending, no comparison ran, so "none was better"
would be equally untrue.
The consumer's size floor is its own shortfall. Two floors, two settings,
two commands —
waired public use --min-model-sizeandwaired worker set --min-model-size.buildMeshCandidatesnow returns a namedmeshDropstally rather than a bare int, and the public count deliberatelydoes not feed the
SizeFloorErrorwrapper, which names the operator'scommand. It is also kept off
publicGate, even though the pointer travels forfree:
pinnedNodeCandidatesshares that gate and discards its floor count, soa gate-carried counter would silently absorb pin-pass drops.
The refusal leads with the entry that declined. The operator picked one row
in
/model;no mesh peer is available … local state="ready"answered withtwo Waired-internal facts about a machine the turn was never going to run on.
ModelNotReadyErrorgainsPublicShare, andError()a third arm on the samediscriminant it already branches on.
local statestays everywhere else —#828 put it there deliberately, on branches where it is the next fact an
operator reads.
The operator's routing floor stands aside on a public-only turn. It
disqualified this host's engine, which this route never intended to use, so
waired worker set --min-model-sizeis the wrong switch to send someone to.Owner ruling 2026-09-06, narrowing #1128's floor-first order by this one case
and recorded in
docs/decisions/20260906/0410-the-public-entry-answers-for-its-own-refusal.md.The wording reuses only vocabulary
waired public statusalready prints(
Use public nodes,Consented,Smallest model accepted,Main agent,Sub agents) and names the command that changes each setting, per ownerruling 2026-09-06. Nothing new is coined.
Also fixes #1252 — the reason could be replaced by a retry
meshMissstamps the error with this host's local model state, andModelIsArrivingread nothing but that field. On the branches that refuse torun here — peer-only, public-only, pinned — a host midway through a download
therefore answered every refusal with
503 overloaded_error+Retry-After: 30. The client retries, the reason is never shown, and theretries end only when an unrelated download does. That is #788's defect
reintroduced through a field that means something different on the branch that
set it, and it would have made this PR's wording invisible on exactly the hosts
most likely to hit it.
ModelNotReadyError.LocalArrivalAnswersnow states whether local arrival isevidence at all. False by default, on purpose: the inverted polarity would
have touched two production sites instead of six, but its zero value is the
unsafe direction, and a future never-consults-local branch that forgets the
flag would silently restore the ten-silent-503s failure. The safe direction's
worst case is a 400 for a genuinely downloading local model, which #1180
already ruled is an acceptable end for a turn.
ModelIsArrivinghas four readers and they move together (#740): the Anthropicsurface (503 → 400 fail-closed), the OpenAI surface (503 → 404
model_not_served),selectionErrorReason/selectionStatus, and management's/explaindry run.Tests
internal/router/public_refusal_note_test.go(new):publicGateFor's tableincluding the consent-before-mode row; every arm of the reason function plus
an assertion that no two causes produce the same sentence; the two arms
that had no test at all before; the public floor counted as its own
shortfall and not as the operator's; and the floor-ordering ruling.
released (what a posture-off host actually looks like, and the shape that
produced the false sentence) and a grant still held (the propagation window).
Checked by mutation: restoring the old arm order fails three subtests.
internal/router/model_not_ready_test.go:ModelIsArrivingper constructor,and a
SelectK-level pair where peer-preferred must still answer true —that row is the regression guard for the whole change.
internal/gateway/public_share_refusal_test.go(new): the body a clientactually receives, asserted verbatim, plus the absence of
Retry-After.Nothing pinned that before — the router tests stop at
err.Error().internal/gateway/selection_error_test.gogains the row where the samearriving state answers 404 because the branch never looked locally.
This PR inverts an existing assertion.
public_only_test.go's"posture off admits nothing at all"asserted onlyerr != nil, which is howthe wrong sentence shipped; it is now a table over the causes, each of which
must name its own switch and must not say "reachable". Its sibling keeps its
two rows and gains a note that "the two ways" was a count taken before this.
Test-after rather than test-first: the branch table could not be written until
the measurement below established which arm the posture-off case actually
lands on.
Checks
go build ./...,go test ./...,gofmt -l,golangci-lint run --max-same-issues=0(cache cleaned first), the decision-logguard,
i18n-pair-guard.sh, andnpm run i18n:check(32 pairs in sync).Notes for reviewers
ModelNotReadyErroris an exported type with exported fields. Aconstruction outside this repository takes
LocalArrivalAnswers: falseandanswers 400 where it used to answer 503 — the fail-closed direction, but
worth a grep on the monorepo side.
/anthropic/v1/modelsstilladvertises
waired/publicunconditionally, including on a host with PublicShare off. The picker drops it; the model list does not, because the
intercept has to be able to route an id a client still holds. Routing and
advertising are separate questions and the second one has other readers.
routing=pinned) gets the router: a peer-only refusal borrows this host's download and answers 503 instead of saying why #1252 status fix but no reasontable; its note stays the fixed string it was.
Fixes #1201
Fixes #1252
Refs waired-ai/waired#1313
🤖 Generated with Claude Code
https://claude.ai/code/session_01HJUUSmAfbRdjER1seDdjXm