Conversation
The hub refuses commands with a status and a JSON {error} body — 409 org
mismatch / unsupported agent, 503 host offline, 404 stale attachment, 413 too
long, 429 command queue full. Neither client looked at the status, so all of
that reasoning reached the operator as a click that appeared to work.
Web. post() in sessions.html and index.html parsed the body for a cmdId and
never read res.status. Both now resolve null on a refusal, having first raised
the hub's own words through one shared toast (TurmaNav.toast + refusalText in
nav.js, .toast moved to app.css so every page has it), and every caller that
painted something optimistically takes it back on that null: the "Starting…"
spinner, the renamed card, the resume that used to navigate to a session page
with nothing to open. del() gets the same contract. In chat.js the compose
button keeps its short labels — a sentence would not fit — and the reason goes
to the same toast; the model/mode/run-against chips drop their optimistic memo
instead of painting a switch that never happened.
Android. mapHistory mapped any null-bodied response, including an undecodable
error body, to Pending — so a refused /history polled 20×3s and gave up in
silence. It is now HistoryResult.Failed, ending the poll with the hub's message.
FleetViewModel.run reported every refusal as "hub unreachable" (Retrofit throws
a non-2xx, which its catch swallowed) and ChatViewModel announced "✓ kill
queued" whatever happened; both now read hubErrorMessage and drop the optimistic
pending row. hubErrorMessage gains a Response overload, worded to match the web.
XERK-30 parity: the Android side lands here, in the same change.
…st of the bottom nav Two things the end-to-end pass turned up. Driving the emulator: a refused kill from the Android Sessions screen sent the command, took the 429, worded the message — and showed nothing. Only FleetScreen and ChatScreen collected FleetViewModel.messages, so every command fired from the Sessions list (kill, rename, move, resume from the ended list) reported into a flow nobody was listening to. It now hosts a snackbar of its own, like the dashboard's. Verified on the emulator: "✗ the host's command queue is full". Driving a 390px viewport: the toast is on every page now, and at its desktop offset it sat on top of the phone bottom-nav. It lifts clear of it.
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.
Fixes XERK-264.
The hub refuses commands with a status and a JSON
{error}body — 409 orgmismatch / unsupported agent, 503 host offline, 404 stale attachment, 413 too
long, and 429 command queue full once XERK-261 lands. Neither client read the
status, so every one of those reached the operator as a click that appeared to
work: the menu closed, the card repainted, the spinner span out its TTL, and
nothing had happened.
Web
TurmaNav.toast+TurmaNav.refusalTextinnav.js(the.toastCSS moved fromsessions.htmlintoapp.css, so everypage has it). Failures only — nothing announces success through it, so a toast
on screen always means a command did not run.
post()/del()on both pages resolvenullon a refusal, having alreadytoasted the hub's own words, and callers roll back what they painted
optimistically on that null: the repo's "Starting…" spinner, the renamed card,
the pending row. A refused resume no longer navigates to a session page with
nothing to open. 401 still goes to
/login.chat.js: the compose button keeps its short, actionable labels (asentence would not fit) and the hub's reason goes to the same toast — it used
to render the bare status number. The model / mode / run-against chips drop
their optimistic memo on a refusal instead of painting a switch that never
happened. Vendored copies (
glasses,veiller) re-synced byte-identical.Android
mapHistorymapped any null-bodied response — including an error body itfailed to decode — to
Pending, so a refused/historypolled 20×3s andgave up in silence. It is now
HistoryResult.Failed, ending the poll with thehub's message.
FleetViewModel.runreported every refusal as "hub unreachable" (Retrofitthrows a non-2xx and its
catchswallowed the body);ChatViewModelannounced "✓ kill queued" whatever happened. Both read
hubErrorMessagenowand drop the optimistic pending row.
hubErrorMessagegains aResponseoverload, worded to match the web'sfallback ("the hub answered HTTP
<n>") so the same refusal reads the same onboth clients.
pass, fixed here): only Fleet/Chat collected
FleetViewModel.messages, soevery command fired from the sessions list reported into a flow nobody read.
XERK-30 parity: the Android side lands in this PR.
CLAUDE.mdgains thecross-cutting contract (both clients, one wording).
Verification
Unit —
node --test turma/tests/*.test.js: 946 pass (was 933; +13 new innav.test.js,sessions.test.js, and a newdashboard-refusal.test.jsthatdrives
index.html's own command path). AndroidtestDebugUnitTest: all green,+7 new (
MapHistoryTest,HubResponseErrorTest). glasses vitest 455 pass,veiller
bun test344 pass (vendoredchat.cjsbyte-identical).Web, driven in Chromium against a real hub (
turma/server.js+ a realheartbeat; script kept out of the repo): 13/13 checks.
SPAWN_FIELD_MAX, the hub answers413 {"error":"prompt too long"}— reads"Start failed — prompt too long" on both pages, with no stuck spinner.
dark; a command the hub ACCEPTED raises nothing.
Android, driven on the
turma228emulator against the same hub behind aproxy injecting the refusals: a confirmed kill shows "✗ the host's command queue
is full" and the session stays; opening a session whose
/historyis refusedshows "✗ the agent is offline" in ~3s after one request, instead of 20 over
60 seconds.
Not verified: the glasses client (its
hub-client.tsalready throws on anon-2xx; unchanged here) and the wide/tablet Android layout, which was reasoned
about but not driven.