Skip to content

Merge beta into main: v3.0.4 release - #140

Merged
GeorgiK77 merged 4 commits into
mainfrom
beta
May 19, 2026
Merged

GeorgiK77 merged 4 commits into
mainfrom
beta

Conversation

@KrasimirKralev

@KrasimirKralev KrasimirKralev commented May 19, 2026

Copy link
Copy Markdown
Contributor

Promotes the v3.0.3 + v3.0.4 work from beta to production. All commits have been live on beta and validated on physical Jetson hardware (krasi.local). CodeRabbit + CI green on each component PR before merge.

v3.0.4 bundle (#139)

  • fix(remote-control): Cloudflare Quick Tunnel panel keeps polling so the URL appears without a manual Refresh.
  • feat(picker): Model picker merges live OpenClaw catalog with the static PROVIDER_CATALOGS and filters retired Claude IDs via a DEPRECATED_MODEL_IDS denylist. Dropdown surfaces when allowCustom is set so users can type their own model ID against Anthropic OAuth.
  • feat(clawkeep): Pair card redesigned to match the ClawAI subscription panel — explicit 'Open authorization page' button so the device code is readable before focus shifts, 'Get a new code' affordance for expired codes, reuse of the existing ai.openAuthPage translation key.
  • feat(vnc): Bidirectional UTF-8 clipboard via a new /setup-api/vnc/clipboard route that shells out to xclip on the guest X CLIPBOARD. Replaces the basic RFB ClientCutText path that mangled Cyrillic / CJK / emoji as Latin-1 mojibake. Stdout capped at 1 MiB and Content-Length pre-checked on POST to protect the Jetson from memory spikes. Requires xclip installed on the device.
  • fix(clawai-tier, client): Preserve last-known tier on transient /setup-api/ai-models/status failures so the 'You are on the free plan' modal stops popping every time the gateway WebSocket blips. Hook returns the same state ref when no logical change so React bails out.
  • feat(catalog): Trigger a background catalog refresh when a provider is configured, so the picker reflects the new entitlement without waiting for the next periodic refresh.
  • chore(release): bump version to 3.0.4.

Post-3.0.3 hardening (#138)

  • fix(gateway-pre-start): Re-install @openclaw/codex if the nested peer-dep symlink to openclaw is missing or dangling. Repro'd live on a customer device where the chat failed with Cannot find package 'openclaw'. -e follows symlinks, so both 'missing' and 'dangling' trigger a --force reinstall, which rebuilds the link without redownloading the package.
  • fix(clawai-tier, server): Portal 401/403 no longer flips the badge to Free. The portal returns 401/403 for several distinct cases (genuine Free, revoked token, token format migration, backend cleanup), so the response code alone can't distinguish them. Fall through to the existing unreachable branch instead — clawaiAccountTier preserves localTier, the downgrade-celebration popup doesn't fire spuriously, and the next poll re-tries the portal immediately so a recovered token is picked up without a 120 s wait.
  • perf: Added a separate short negative cache (portalUnreachableCache, 30 s TTL — matches the poll cadence) so back-to-back polls during sustained auth failure don't hammer the portal. Successful 200 clears it so recovery stays responsive.
  • tests: Replaced the obsolete '401/403 = Free' assertion with two new tests covering the preserve-localTier path and the negative-cache hit.

v3.0.3

  • chore: bump version (already merged to beta via earlier PRs).

Note: PR #138 + PR #139 both contain tier-flicker fixes — #139 is the client-side preservation in useClawboxLogin, #138 is the server-side preservation in /setup-api/ai-models/status. They're complementary; together they fix the modal-flicker symptom from both ends.

Test plan

  • Pull main on a Jetson, run the standard update flow, verify version reports 3.0.4.
  • Smoke-test the Remote Desktop clipboard (Cyrillic round-trip) — see PR feat: v3.0.4 — VNC UTF-8 clipboard + ClawKeep pair UX + bug fixes #139 for the full Test plan checklist.
  • Smoke-test ClawKeep pairing → portal hand-off.
  • Confirm the tier modal does NOT pop on a brief network blip or portal 401/403; DOES pop exactly once on a real Paid → Free transition (200 + tier: "free").
  • Verify the codex self-heal: on a Jetson with @openclaw/codex installed, manually delete ~/.openclaw/npm/node_modules/@openclaw/codex/node_modules/openclaw, restart the gateway service, confirm the peer dep is restored and chat works.

* feat: refresh model catalog when provider gets configured

Symptom: a device's AI picker shows only 1 (or 0) models for a
provider the user just configured. Example from a live device that
added Anthropic credentials after first boot:

  models.providers.anthropic = {}
  catalog cache: 1 anthropic model (a default placeholder)
  picker: only the placeholder selectable
  chat: silently falls back to ClawBox AI / deepseek

Root cause: the catalog refresh job only runs at clawbox-setup boot
warmup. When the user adds an API key after boot (the normal path —
the wizard prompts for credentials hours/days after the device
first comes online), no refresh fires for that provider. The
catalog stays on whatever the pre-auth snapshot found, which for
Anthropic is the single fallback model. ClawBox AI gets configured
as the fallback at the same time and silently absorbs every
Claude-selected chat request.

Fix: trigger a single-provider catalog refresh at the end of
configure/route.ts, right before the gateway restart. The refresh
runs out-of-band (single-flight guarded, no await), so the
configure response doesn't slow down. Once the refresh completes
(seconds to a couple minutes on Jetson) the live model list is
written to the disk cache and the picker sees it on next open.

Implementation:
- Export `refreshInBackground` from `catalog/route.ts` (was already
  designed for fire-and-forget use; just needed the export).
- Import it from `configure/route.ts` and call after the existing
  step 8b (anthropic-plugin gating) and before step 9 (gateway
  restart). Map `ocProvider === "deepseek"` → `clawai` because the
  catalog uses the user-facing label, not the openclaw provider id.
  Gate on `isCatalogProvider()` so local-only providers (llamacpp)
  don't trip the refresh.

This means the `models.providers.<p>` block in openclaw.json (and
the picker UI that reads from it) reflects whatever the upstream
provider actually exposes — no hard-coded model lists ever, not
even temporarily.

Verified on a live device: re-saving Anthropic now triggers a
`[catalog] refreshed anthropic: <N> models` log line within seconds
of the configure call returning, and the picker subsequently shows
all available Claude variants instead of the single boot-time
placeholder.

* feat(picker): merge static catalog into live + filter deprecated Claude IDs + show dropdown for allowCustom providers

Three picker-experience fixes uncovered on a device whose Claude.ai
OAuth scope only enumerates `claude-sonnet-4-20250514` (a model
Anthropic has scheduled for retirement on 2026-06-15).

## 1. Merge static PROVIDER_CATALOGS into the live catalog response

When `openclaw models list --provider anthropic` returns just one
model — which is what Claude.ai consumer OAuth scopes do today —
the picker had nothing to offer. The curated list in
`src/lib/provider-models.ts` already tracks the current Anthropic
lineup (Opus 4.7, Sonnet 4.6, Haiku 4.5 per
https://platform.claude.com/docs/en/about-claude/models), so we
now union the live response with the static list. Live entries
take precedence (their `contextWindow`, `input`, and `label`
reflect what the gateway negotiated); ids in the static list but
not the live response get appended as fallback entries with
`contextWindow` from a small known-good lookup table.

Net effect: the picker always shows the curated current lineup,
plus anything extra the upstream actually exposes.

## 2. DEPRECATED_MODEL_IDS denylist

Anthropic's docs list `claude-sonnet-4-20250514` and
`claude-opus-4-20250514` as retiring on 2026-06-15. The openclaw
plugin does not tag them as deprecated (so the existing
`entry.tags?.includes("deprecated")` filter doesn't catch them),
which means a fresh device hitting Claude.ai OAuth gets these as
the *only* selectable Claude model — i.e. the picker silently
defaults users to a soon-to-be-dead model. Hardcode the two
known-retired ids in a denylist that filters them out at
catalog transform time.

When Anthropic publishes the next deprecation notice, add the
new ids to this set.

## 3. Show dropdown for `allowCustom: true` providers with only 1 model

`ChatPopup.tsx` previously hid the model dropdown when
`catalog.models.length < 2`. With Anthropic's single-model OAuth
catalog and `allowCustom: true`, the user had no UI affordance to
switch models at all — even though typing a custom id was the
documented escape hatch. Relax the gate: show the dropdown when
the catalog has multiple models OR allows custom ids.

## Verified on a live device

- `openclaw models list --provider anthropic --all --json` returns
  the single deprecated model.
- `/setup-api/ai-models/catalog?provider=anthropic` now returns
  the three curated current Claude models (Opus 4.7 / Sonnet 4.6 /
  Haiku 4.5) and the disk cache at
  `data/catalog-cache/anthropic.json` is consistent.
- The chat picker shows the dropdown with those three entries. The
  deprecated `claude-sonnet-4-20250514` is no longer reachable from
  the curated list (still typeable as a custom id if a user really
  needs it before the 2026-06-15 retirement, since
  `allowCustom: true` is preserved).

* fix(remote-control): keep polling tunnel status so the URL appears without a manual refresh

The Cloudflare Quick Tunnel panel exited its poll loop as soon as the
service entered the 'active' state without a URL yet — leaving the
'Negotiating tunnel with Cloudflare...' spinner up until the user
hit Refresh by hand. Restructure the loop to always reschedule with
an adaptive cadence (2s while negotiating, 15s when settled) so the
URL appears the moment cloudflared prints it.

* feat(clawkeep): redesign pair card to match the ClawAI subscription panel

- Replace the auto-window.open with an explicit 'Open authorization
  page' button so the device code is visible before focus shifts to
  the portal tab.
- Add a 'Get a new code' affordance for when the code expires or
  the user wants to retry against a different account.
- Reuse the existing 'ai.openAuthPage' translation key instead of a
  near-duplicate; remove four pair-flow keys (enterCode, reopenPortal,
  typeCodeOnPortal, waitingApproval) that the redesigned card no
  longer references, in all 10 locales.

* feat(vnc): bidirectional UTF-8 clipboard via xclip with toast fallback

Both copy directions used the basic RFB ClientCutText path, which is
Latin-1 only — Cyrillic / CJK / emoji round-tripped as mojibake. Route
both directions through xclip on the guest X CLIPBOARD instead, since
xclip handles UTF-8 natively.

- New /setup-api/vnc/clipboard route: GET reads the guest CLIPBOARD,
  POST writes it. xclip's write daemon-forks to keep the selection
  alive, so the write side uses 'ignore' stdio to avoid the parent
  hanging on inherited pipes.
- Paste modal posts text to the route, then sends Ctrl+V over RFB so
  the focused field inside Chromium pastes from the freshly-updated
  CLIPBOARD.
- Copy uses noVNC's 'clipboard' event as a change signal only — the
  Latin-1 payload is ignored; we GET the real UTF-8 text via xclip.
  An in-flight guard + pending flag collapses rapid copy stampedes.
- On insecure HTTP origins where navigator.clipboard is gated, a
  toast with a manual 'Copy' button uses the shared copyToClipboard
  helper (execCommand fallback inside a user gesture).
- One new translation key, 'vnc.copyToast.fromRemote', in 10 locales.

Note: requires 'xclip' installed on the device (apt install xclip).

* fix(clawai-tier): preserve last-known tier on transient failure

The 'You are on the free plan' modal was popping up every time the
network blipped or the gateway WebSocket reconnected. Cause: the
useClawboxLogin poll cleared loggedIn + tier to null on any non-2xx
response or fetch throw, which TierUpgradeCelebration then detected
as a downgrade (currentRank=0 while seenRank>0).

The server-side /status route already has portal-failure resilience
(it caches the last-known tier with TTLs), so a 2xx response is the
authoritative signal — anything else is 'I don't know right now',
not 'you've been downgraded'.

- On transient failure: return the previous state ref so React bails
  out and downstream consumers don't re-render every 30s on a flaky
  network.
- On 2xx: keep trusting the server response as before. A confirmed
  Paid → Free transition still fires the modal exactly once.

* chore(release): bump version to 3.0.4

* fix: address CodeRabbit review on #139

- VNC clipboard route: cap xclip stdout buffering at MAX_CLIPBOARD_BYTES
  and SIGTERM the child when exceeded, returning HTTP 413 — prevents a
  giant guest selection from spiking Jetson RAM during the JSON hop.
- VNC clipboard route: reject oversized POST bodies via Content-Length
  pre-check, before request.json() buffers the whole payload.
- ClawKeep PairChallengeCard: mark the phase indicator with role=status
  + aria-live=polite, hide the decorative spinner with aria-hidden, so
  assistive tech announces waiting/configuring transitions.
- VNCApp sendPaste: add a pasteBusy early-return guard and move
  setPasteBusy(false) into a finally — kills the duplicate-paste race
  on rapid Ctrl+Enter / double-click.
- VNCApp copy toast: wrap the remote-clipboard toast in
  role=status + aria-live=polite + aria-atomic=true so screen readers
  announce that remote text is available without focus motion.
@KrasimirKralev
KrasimirKralev requested a review from a team as a code owner May 19, 2026 13:49
@coderabbitai

coderabbitai Bot commented May 19, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@KrasimirKralev has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 19 minutes and 1 second before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e44ad097-ac61-435b-bd71-ad206828faa4

📥 Commits

Reviewing files that changed from the base of the PR and between 9462b55 and 60946c7.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (17)
  • package.json
  • scripts/gateway-pre-start.sh
  • src/app/setup-api/ai-models/catalog/route.ts
  • src/app/setup-api/ai-models/configure/route.ts
  • src/app/setup-api/ai-models/status/route.ts
  • src/app/setup-api/vnc/clipboard/route.ts
  • src/components/ChatPopup.tsx
  • src/components/ClawKeepApp.tsx
  • src/components/RemoteControlPanel.tsx
  • src/components/VNCApp.tsx
  • src/lib/clawkeep-translations.ts
  • src/lib/desktop-translations-part1.ts
  • src/lib/desktop-translations-part2.ts
  • src/lib/desktop-translations-part3.ts
  • src/lib/desktop-translations.ts
  • src/lib/use-clawbox-login.ts
  • src/tests/routes/ai-models/status.test.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch beta

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@KrasimirKralev
KrasimirKralev requested a review from GeorgiK77 May 19, 2026 13:50
…th loss (#138)

* fix: codex peer-dep self-heal + don't flip badge to Free on portal auth loss

Two related post-v3.0.3 hardenings observed live on a customer device
and a dev box.

## 1. gateway-pre-start.sh: re-install codex if the peer-dep symlink is missing

Symptom on a customer device: chat fails with
`Error: Cannot find package 'openclaw' imported from
.../@openclaw/codex/dist/shared-client-…js`.

Root cause: `openclaw plugins install codex` writes both the package
directory AND a `<codex>/node_modules/openclaw -> <global openclaw>`
symlink. If something later removes the nested node_modules (partial
install, openclaw upgrade churn, manual cleanup), the package.json
remains but the peer dep is gone. Our v3.0.3 auto-installer only
checked `$CODEX_PLUGIN_DIR/package.json`, declared the plugin
"installed", and skipped repair. The plugin then loaded but every
import via the openclaw package failed at runtime.

Add a check on the nested peer dep too. `-e` follows symlinks, so
both "missing symlink" and "dangling symlink" trigger a `--force`
reinstall, which rebuilds the link without redownloading the
package contents.

## 2. status/route.ts: portal 401/403 no longer flips badge to Free

Symptom: when ClawKeep auth disconnects (or the bearer is otherwise
rejected by the portal), the device flips its tier badge to Free
and fires the downgrade-celebration popup with a Re-subscribe CTA
— even though the user might still be on a paid plan.

Root cause: fetchPortalTier treated 401/403 as a definitive "user is
Free" verdict and cached it for the full 120 s TTL. But the portal
returns 401/403 for several distinct cases:
  - User is genuinely Free.
  - Token was revoked (admin action, account event, plan transition).
  - Token migrated to a new format the device hasn't refreshed yet.
  - Backend cleanup pruned the device record.
The response code alone can't distinguish them.

Fall through to the existing `unreachable` branch on 401/403
instead. clawaiAccountTier then preserves `localTier` (last known
good), tierSource is "picker", the downgrade-celebration popup
doesn't fire spuriously, and — because `unreachable` is uncached —
the very next poll re-tries the portal, so a recovered token is
picked up immediately instead of waiting out a stale 120 s null.

A user who legitimately downgrades to Free still flips correctly:
the portal returns 200 + `tier: "free"` for that case, which is
the path that goes through mapPortalTier and returns null.

## Tests

- Replaced "returns clawaiTier=null on portal 403" with two new tests:
  - "preserves localTier on portal 401/403 (auth lost, might still be paid)"
  - "does not cache the auth-failure verdict — recovers on next poll"

## Verified

- Codex resilience: reproduced the broken state on a dev Jetson
  (`@openclaw/codex` present, nested node_modules removed), confirmed
  the new check triggers `--force` reinstall and the chat recovers.
- Auth-loss handling: covered by the two new unit tests above.

* refactor(simplify): negative-cache unreachable + trim duplicated rationale

Applies the three actionable findings from /simplify:

1. Efficiency: previously, removing the 401/403 positive-null cache
   meant every 30 s status poll hit the portal during sustained auth
   failure or outage (4× sustained per-device load vs. the prior
   120 s cache). Add a separate short negative cache
   (portalUnreachableCache, 30 s TTL — matches the poll cadence so
   we hit the portal at most every other poll). Successful 200
   clears it so recovery is responsive. The "401/403 isn't Free"
   semantic from the previous commit is preserved.

2. Quality: comment block in fetchPortalTier was 16 lines, the
   "also: deliberately not cached" tail was redundant with the test
   name. Trimmed to 6 lines (the irreducible "401/403 is ambiguous"
   reasoning).

3. Quality: dropped the duplicated prose comments inside the two
   new test cases — the test titles already document the behaviour
   and the rationale belongs in route.ts, not both places.

Tests updated:
- Renamed "does not cache the auth-failure verdict" → "negative-
  caches an unreachable verdict so back-to-back polls don't hammer
  the portal". Same shape, now asserts fetchSpy was called once
  (not twice). Verifies the new cache hit.
- "preserves localTier on portal 401/403" unchanged in behaviour;
  comment stripped.
@github-actions

github-actions Bot commented May 19, 2026

Copy link
Copy Markdown

CI Summary

✅ Tests

  • Result: passed
  • View run
  • Coverage: statements 70.71%, branches 60.19%, functions 66.59%, lines 72.86%

✅ E2E

✅ E2E Install

@GeorgiK77 GeorgiK77 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i trust this

@GeorgiK77
GeorgiK77 merged commit 6e7907c into main May 19, 2026
7 checks passed
@GeorgiK77
GeorgiK77 deleted the beta branch May 19, 2026 14:25
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