Skip to content

shell: survive a box gateway that is not there (BUG-CV-01, BUG-CV-02) - #155

Merged
pythonlearner1025 merged 1 commit into
mainfrom
shell-resilience-cv
Sep 1, 2026
Merged

shell: survive a box gateway that is not there (BUG-CV-01, BUG-CV-02)#155
pythonlearner1025 merged 1 commit into
mainfrom
shell-resilience-cv

Conversation

@pythonlearner1025

Copy link
Copy Markdown
Member

Canary workspace zesty-swan booted a box whose cloudflared connector carried
zero ready connections. Every /workspaces/<ws>/webapp/7445/* call answered 530
for more than seven minutes. Two defects followed from that one dead tunnel.

BUG-CV-01 (critical) — the whole webapp went blank

The shell painted for about five seconds, then body.innerText.length went 87
to 0 on two loads out of two. The console held Failed to fetch dynamically imported module: /assets/SessionSurface-*.js after a run of
ERR_INSUFFICIENT_RESOURCES. The chunk itself was fine (200, 3 583 733 bytes).

Cause: no box poll carried a deadline, and the platform poller ran on a 500 ms
setInterval that fired whether or not the previous read had answered. The
browser ran out of sockets, the lazy SessionSurface import rejected, and no
error boundary covered the rejection — so React unmounted the tree. The degraded
path already worked; nothing could reach it.

Both halves are fixed.

  • lody/SurfaceLoadBoundary.tsx catches the rejection and renders the notice
    the surface already had. A Try again button builds a fresh lazy(), because
    React re-throws a rejected payload forever. A failed chunk is now a message,
    never an empty body.
  • box-gateway-health.ts gives every box read a 10 s deadline through one
    helper. The platform poller schedules its next read when the previous one
    lands, and stretches to 30 s while the box is known unreachable. The terminal
    reconnect ceiling rises from 5 s to 30 s.

BUG-CV-02 (major) — the footer said workspace running

CloudApp.tsx rendered machine lifecycle state alone, so a machine that boots
behind a dead connector reads as healthy.

Every box read now reports what it saw. Three consecutive tunnel failures (502,
504, 530, or a transport error) settle the shell on unreachable; one answer
clears it. A cold daemon's own 503 counts as reachable, because it came from the
box. shell/workspace-status-line.ts folds the signal in and the footer reads
workspace running · box unreachable.

No new poll and no new control-plane API. The evidence is the requests the shell
already sends.

Tests

packages/webapp/test/shell-gateway-resilience.test.tsx (11 cases): a rejected
chunk renders the degraded notice and keeps the container non-empty; the retry
mounts the surface; every box read carries a deadline; three 530s change the
surfaced status to workspace running · box unreachable; a cold 503 does not;
the caller's own abort reports nothing; the platform poller makes one read in
thirty seconds against a box that never answers.

Gates

  • npm run typecheck — pass
  • npm run lint:gate — pass (74 anti-slop, 0 blitz-house, unchanged)
  • BLITZDEV_MANAGED=1 npm test — pass. Two real-daemon suites
    (lody-sharing-relay, lody-worktree-session) failed on the first run and
    passed on retry; they are the known daemon-harness flake.

🤖 Generated with Claude Code

https://claude.ai/code/session_013voh4zgczP2jmPrS2ERrav

A canary box booted with a cloudflared connector carrying zero ready
connections. Every /webapp/7445/* call answered 530 for more than seven
minutes. Two defects followed from that one dead tunnel.

BUG-CV-01 (critical): the whole document went blank. Every box poll ran
without a deadline, and the platform poller ran on a 500 ms setInterval
that fired whether or not the previous read had answered. The browser ran
out of sockets, the lazy SessionSurface import rejected with
ERR_INSUFFICIENT_RESOURCES, and no boundary covered the rejection — so
React unmounted the tree. Two halves are fixed:

- SurfaceLoadBoundary catches the rejection and renders the degraded
  notice the surface already had, plus a Try again that builds a fresh
  lazy(). A failed chunk is now a message, never an empty body.
- box-gateway-health.ts gives every box read a 10 s deadline, and the
  platform poller schedules its next read when the previous one lands.
  The terminal reconnect ceiling rises from 5 s to 30 s.

BUG-CV-02 (major): the footer read "workspace running" throughout, because
CloudApp rendered machine lifecycle state alone. Those same polls now
report what they saw; three consecutive tunnel failures settle the shell
on "unreachable", one answer clears it, and the footer reads
"workspace running · box unreachable". No new poll and no new control-plane
API: the evidence is the requests the shell already sends.

Tests: packages/webapp/test/shell-gateway-resilience.test.tsx.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013voh4zgczP2jmPrS2ERrav
@pythonlearner1025
pythonlearner1025 merged commit 6a6cb18 into main Sep 1, 2026
10 checks passed
pythonlearner1025 added a commit that referenced this pull request Sep 1, 2026
The redesign branched at #110, three hours before #112 landed
`settings-surface.css` — the `cfg-` canon CLAUDE.md now names as the one
system for every settings-shaped screen. Both sides restyled the same
surfaces, so the resolution keeps the redesign's STRUCTURE and main's
VOCABULARY: primitives, the org switcher, the surface ladder and the
danger-zone/About headings survive, expressed in `cfg-` classes rather than
in a second set of headings, fields, dividers and fact lists.

Per file:

- settings.css — the redesign's ladder and its `.settings-switch-*` shape
  stay; every rule whose markup moved to the canon is gone (definition list,
  appearance wrapper, `.settings-field*`, `.settings-danger*`, `.settings-note`,
  `.settings-credential-section`, `.connect-field__label`). The account page
  keeps its red frame around the canon's `.cfg-danger`.
- workspace-details-dialog.css — the ladder's raised cards stay; the heading,
  field and fact-list rules the canon owns are gone, and #114's credential
  import and #112's volume meter come across whole. The settings form's
  machine-type select re-targets `.cfg-field` and takes the one control height.
- SettingsPage.tsx — main's `.cfg-meta` profile facts and `.cfg-section`
  appearance block, plus the redesign's Organizations section, re-headed
  with `.cfg-section-head` / `.cfg-title`.
- MembersPanel.tsx — the redesign's named "Danger zone" over main's
  `.cfg-danger` / `.cfg-danger-action` zone.
- UsagePanel.tsx — the redesign's `SettingsSwitch` (it writes on change, which
  is what the primitive is for) with main's `.cfg-help` notes.
- WorkspaceSettingsTab.tsx — main's `.cfg-field--inline` checkbox, NOT
  `SettingsSwitch`: that row is part of a draft the Save below sends, and the
  primitive's own contract forbids a Save beside it. `cfg-actions` and
  `cfg-meta` as main has them, with the redesign's "About" title.
- WorkspaceDetailsDialog.tsx — main's `.cfg-section` credentials tab and its
  comment field, with the redesign's red Revoke.
- CloudApp.tsx — the redesign cuts the desktop statusline; #155's BUG-CV-02
  sentence keeps one desktop surface, because a box the browser cannot reach
  has no other affordance. The bar now shows on desktop when the terminal
  needs its sign-in pair OR when the gateway is unreachable.

Also carried across the version gap: `settings-surface.css` gains the disabled
state the redesign gave its danger button, the preview fixtures gain the fields
main added to `MachineView`, `WorkspaceCredentialView` and `CatalogEntryView`,
the preview gallery passes `refreshWorkspaces`, and two tests drop props the
redesign removed.

Gates: typecheck and lint:gate pass at baseline (74 anti-slop, 0 house).
`npm test` passes except `webapp/test/lody-worktree-composer.test.tsx`, which
fails the same way on origin/main alone — a pre-existing failure, not this
merge's.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhdxNAYjV97JTv2pddcJiN
pythonlearner1025 added a commit that referenced this pull request Sep 2, 2026
…ing it — seam patch 15 (#173)

* docs(lody): declare seam patch 15 — the host owns connectivity

BlitzOS surfaces connectivity itself: the shell footer's left slot carries
`workspace running · box unreachable` from `workspace-status-line.ts`, behind
the gateway probe in `box-gateway-health.ts`. Lody, mounted inside that shell,
tells the same story again in different words from a narrower vantage.

This declares the patch that stops it, ahead of the edit, as CLAUDE.md
requires: one optional prop, `hideConnectionStatus`, framed for any embedding
host that reports connectivity itself. It names every surface it takes, every
surface it deliberately leaves alone and why, and the five it found dark
already.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhdxNAYjV97JTv2pddcJiN

* lody: apply seam patch 15 and gate every connection surface off v1-scope

One optional prop, `hideConnectionStatus`, on the four levels that draw the
connection: the status resolver, the chat surface, the session page and the
file viewer, plus the chat landing for the mobile home's banner. Every hunk
defaults to today's behaviour and no upstream call site passes it.

Our side is a sixth flag in `v1-scope.ts`, `connectionStatus`, and two props in
`router.tsx`. The flag's row says what the other five do not: this is an
ownership boundary, not a "not in v1" cut. `shell/workspace-status-line.ts`
already reports reachability for the whole workspace.

`machine-removed` keeps its chip. It blocks sending, and the footer says
nothing about it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhdxNAYjV97JTv2pddcJiN

* test(lody): pin every connection surface dark, and pin ours still lit

Each cut is asserted both ways, with the underlying state ACTIVE in both: an
offline browser and an offline machine resolve a chip for a host that reports
nothing itself, and resolve null for the host BlitzOS is. The info bar renders
"You are offline. Reconnect to sync." in the first case and not in the second;
the catch-up spinner and the mobile pill get the same treatment.

`machine-removed` is asserted to SURVIVE the suppression, because it blocks
sending and the footer says nothing about it.

The last block is PR #155 from this side: the probe settles on unreachable, the
sentence changes, and `CloudApp.tsx` still renders it. Taking Lody's story away
is only correct while ours is still told.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhdxNAYjV97JTv2pddcJiN

* docs(lody): cite seam patch 15's kept surfaces by key, not by line

Two claims in the "leaves alone" list were wrong. There is no "Disconnected
from sync server" string anywhere in the vendored tree, and `isOffline` is a
field of `SessionListRow` that no renderer reads rather than something our rail
computes. Both are corrected.

The rest now cite i18n keys instead of line numbers. Every line number in that
list belonged to a surface this patch does NOT touch, so nothing keeps them
honest as the files move; the keys are what a merge agent can grep.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhdxNAYjV97JTv2pddcJiN

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
pythonlearner1025 added a commit that referenced this pull request Sep 2, 2026
…orm (#176)

`the platform snapshot poller > never has two reads in flight against a
box that does not answer` timed out at 5 s on main. The poller is not the
cause. `platform.tsx` has not changed since #155 landed it, and the
single-flight invariant holds: with a fetch that never answers, the hook
issues one read and 30 s of fake time add none.

The timed region was wrong. The test imported `../src/lody/platform.js`
INSIDE its own `it()`, and that module pulls Convex and the vendored Lody
providers behind it. Measured on a loaded box: 7 720 ms to import, 1 ms
for the property. The 5 s timeout fired on module weight.

The import is now static, so the transform lands in the file's import
phase where no test timeout governs it. The assertion is unchanged:
one read, thirty seconds later.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.

2 participants