Skip to content

fix(browser): one Browser app + host-capable Pi serves streamed sessions#1285

Merged
jaylfc merged 2 commits into
devfrom
feat/unify-browser-remove-streamed-app
Jun 21, 2026
Merged

fix(browser): one Browser app + host-capable Pi serves streamed sessions#1285
jaylfc merged 2 commits into
devfrom
feat/unify-browser-remove-streamed-app

Conversation

@jaylfc

@jaylfc jaylfc commented Jun 21, 2026

Copy link
Copy Markdown
Owner

Two related fixes so there is one Browser app that actually works on the Pi (Jay flagged both: the duplicate app, and 'the Pi isn't capable').

1. Remove the duplicate streamed app

The Browser app already has a per-tab Proxy/Streamed toggle (BrowserModeToggle) that attaches the active tab to a real Neko/WebRTC Chromium session, rendered by the same LiveBrowserView the standalone StreamedBrowserApp imported. So 'Browser (Streamed)' was pure duplication. Removed the registry entry + the component + the stale vitest exclude. The recent neko fixes (#1223 sandbox, #1228/#1230 backend) live in LiveBrowserView + /api/browser/sessions, which the toggle uses, so they are preserved.

2. Fix 'the Pi isn't capable' (no_capable_node)

POST /api/browser/sessions (what the toggle hits) only called pick_browser_node, which by design returns Tier-2 worker nodes and never the host. So a 16GB Pi with no browser workers always returned no_capable_node, even though there is a host path (start_on_host via browser_container_runner, RAM floor 6GB) that the /sessions/mine route already uses. The create path now uses the canonical resolve_browser_target (explicit worker -> host if RAM-capable -> best worker) and starts on the host, mirroring the mine path. The host runtime is already wired in lifespan (wire_browser_runtime).

Tests

2 new: capable host (16GB, no workers) places on host (201, node=host, start_on_host called); non-capable host (4GB) + no workers -> 409. Existing browser session + manager + registry suites green; compileall clean.

Guardrails: no new auth, no DB migration, no installer/boot.

Summary by CodeRabbit

  • Refactor

    • Consolidated browser app functionality into a unified "Browser" app; the separate "Browser (Streamed)" app option has been removed.
  • New Features

    • Enhanced session placement logic to intelligently start sessions on the host when capable, with fallback to worker nodes.
  • Bug Fixes

    • Added proper error handling returning a "no capable node" response when session placement targets are unavailable.

Two related fixes so there is a single Browser app that works on the Pi:

1. Remove the duplicate 'Browser (Streamed)' app. The Browser app already has
   a per-tab Proxy/Streamed toggle (BrowserModeToggle) that attaches the tab to
   a real Neko/WebRTC session via the same LiveBrowserView the standalone app
   imported, so StreamedBrowserApp was pure duplication. Drops the registry
   entry, deletes the component, removes the stale vitest exclude.

2. Fix 'the Pi isn't capable': POST /api/browser/sessions only called
   pick_browser_node (Tier-2 workers, never the host), so a 16GB host with no
   browser workers returned no_capable_node. It now uses resolve_browser_target
   (explicit worker -> host if RAM-capable -> best worker) and starts on the
   host via browser_container_runner, mirroring the already-working
   /sessions/mine path. 2 tests: capable host places on host; non-capable host +
   no workers -> 409. Browser + registry suites green.
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@jaylfc, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 24 minutes and 52 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2b3ee4fb-e804-462f-b207-e9b7ef5dbc3e

📥 Commits

Reviewing files that changed from the base of the PR and between 8138c1d and 474aa02.

📒 Files selected for processing (2)
  • tests/test_routes_browser_sessions.py
  • tinyagentos/routes/browser_sessions.py
📝 Walkthrough

Walkthrough

Removes the StreamedBrowserApp frontend component, its test suite, and the streamed-browser OS app registry entry. The app registry is updated with a comment noting that proxy vs. streamed behavior is now a per-tab toggle. On the backend, create_session is rewritten to resolve placement via resolve_browser_target, supporting direct host startup alongside the existing worker path.

Changes

Browser Session Consolidation

Layer / File(s) Summary
Remove StreamedBrowserApp and registry entry
desktop/src/apps/StreamedBrowserApp/index.ts, desktop/src/registry/app-registry.ts, desktop/vite.config.ts
Removes the StreamedBrowserApp re-export, removes the "streamed-browser" app manifest entry from the OS registry (adds a comment that proxy vs. streamed is a per-tab toggle), and removes the Vitest quarantine exclusion for the deleted test file.
Rewrite create_session with host/worker placement
tinyagentos/routes/browser_sessions.py
create_session now calls resolve_browser_target(cluster, host_hw, explicit_node=body.node) and branches: host path offloads DNS via asyncio.to_thread and calls start_on_host; worker path calls start_on_worker; both paths return 409 {"error": "no_capable_node"} when no valid target is found. Prior worker-only logic is removed.
Session placement tests
tests/test_routes_browser_sessions.py
Adds TestCreateSessionHostPlacement with two async tests: one asserts start_on_host is awaited and start_on_worker is not on a capable host; the other asserts HTTP 409 / no_capable_node when the host is incapable and no workers exist.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • jaylfc/taOS#633: Modifies StreamedBrowserApp.tsx for ?device=mobile|desktop handling — directly overlaps with this PR's removal of that same component.
  • jaylfc/taOS#944: Introduces BrowserModeToggle that consumes POST /api/browser/sessions and its 409 { "error": "no_capable_node" } response, which this PR reshapes in create_session.
  • jaylfc/taOS#1202: Both PRs modify the Vitest exclude list in desktop/vite.config.ts — this PR removes the StreamedBrowserApp.test.tsx exclusion while the other restructures the quarantine list.

Poem

🐇 Hop hop, the streamed app is gone today,
One browser tab now rules the display.
The host gets a place, the worker too,
resolve_browser_target sorts the queue.
No more quarantine for tests that fled —
The rabbit tidied up and went to bed. 🌙

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(browser): one Browser app + host-capable Pi serves streamed sessions' accurately summarizes the main changes: removing the duplicate StreamedBrowserApp and enabling hosts to serve streamed sessions.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/unify-browser-remove-streamed-app

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.

@gitar-bot

gitar-bot Bot commented Jun 21, 2026

Copy link
Copy Markdown

Note

Your trial team has used its Gitar budget, so automatic reviews are paused. Upgrade now to unlock full capacity. Comment "Gitar review" to trigger a review manually.
Learn more about usage limits

Code Review 👍 Approved with suggestions 0 resolved / 1 findings

Consolidates browser streaming into a single app and enables host-capable Raspberry Pi nodes for session hosting. Ensure session records are properly cleaned up if the worker lookup fails post-creation.

💡 Edge Case: Orphaned session record when worker lookup fails after create

📄 tinyagentos/routes/browser_sessions.py:129-143

In the new create_session, the session record is now created (mgr.create_session(...) at line 130) before the worker is resolved. In the worker branch, cluster.get_worker(node) can return None (line 145-147), which returns a 409 no_capable_node but leaves a freshly-created session row persisted in the store with no container ever started (an orphan in pending/idle state).

This is a behavior change from the previous code, where the worker is None check ran before create_session, so no orphan was created. resolve_browser_target returns ("worker", node) based on pick_browser_node / list_browser_nodes, and there's a small TOCTOU window (or stale node listing) where get_worker can then return None.

Impact is minor (rare race, leftover idle record), but over time these orphans can accumulate. Consider resolving the worker before calling create_session, or cleaning up the session record before returning 409. Note the same orphan exists for the BrowserWorkerError/502 path, but that was pre-existing.

Resolve the worker before creating the session record so a failed lookup returns 409 without leaving an orphan.
kind, node = target
worker = None
if kind == "worker":
    worker = cluster.get_worker(node)
    if worker is None:
        return JSONResponse({"error": "no_capable_node"}, status_code=409)

mgr = request.app.state.browser_sessions
session = await mgr.create_session(
    "user", user_id, body.url, body.profile or "default"
)
vol = f"taos-browser-{session['id']}"
auth_token = getattr(request.app.state, "browser_worker_auth_token", None)
try:
    if kind == "host":
        ...
    else:
        session = await mgr.start_on_worker(session["id"], node=node, worker_url=worker.url, profile_volume=vol, auth_token=auth_token)
🤖 Prompt for agents
Code Review: Consolidates browser streaming into a single app and enables host-capable Raspberry Pi nodes for session hosting. Ensure session records are properly cleaned up if the worker lookup fails post-creation.

1. 💡 Edge Case: Orphaned session record when worker lookup fails after create
   Files: tinyagentos/routes/browser_sessions.py:129-143

   In the new `create_session`, the session record is now created (`mgr.create_session(...)` at line 130) **before** the worker is resolved. In the worker branch, `cluster.get_worker(node)` can return `None` (line 145-147), which returns a 409 `no_capable_node` but leaves a freshly-created session row persisted in the store with no container ever started (an orphan in `pending`/`idle` state).
   
   This is a behavior change from the previous code, where the `worker is None` check ran *before* `create_session`, so no orphan was created. `resolve_browser_target` returns `("worker", node)` based on `pick_browser_node` / `list_browser_nodes`, and there's a small TOCTOU window (or stale node listing) where `get_worker` can then return `None`.
   
   Impact is minor (rare race, leftover idle record), but over time these orphans can accumulate. Consider resolving the worker before calling `create_session`, or cleaning up the session record before returning 409. Note the same orphan exists for the `BrowserWorkerError`/502 path, but that was pre-existing.

   Fix (Resolve the worker before creating the session record so a failed lookup returns 409 without leaving an orphan.):
   kind, node = target
   worker = None
   if kind == "worker":
       worker = cluster.get_worker(node)
       if worker is None:
           return JSONResponse({"error": "no_capable_node"}, status_code=409)
   
   mgr = request.app.state.browser_sessions
   session = await mgr.create_session(
       "user", user_id, body.url, body.profile or "default"
   )
   vol = f"taos-browser-{session['id']}"
   auth_token = getattr(request.app.state, "browser_worker_auth_token", None)
   try:
       if kind == "host":
           ...
       else:
           session = await mgr.start_on_worker(session["id"], node=node, worker_url=worker.url, profile_volume=vol, auth_token=auth_token)

Options

Display: compact → Showing less information.

Comment with these commands to change:

Compact
gitar display:verbose         

Important

Your trial ends in 6 days — upgrade now to keep code review, CI analysis, auto-apply, custom automations, and more.

Was this helpful? React with 👍 / 👎 | Gitar

Comment on lines 129 to +143
mgr = request.app.state.browser_sessions
session = await mgr.create_session(
"user", user_id, body.url, body.profile or "default"
)
vol = f"taos-browser-{session['id']}"
auth_token = getattr(request.app.state, "browser_worker_auth_token", None)
try:
session = await mgr.start_on_worker(
session["id"],
node=node,
worker_url=worker.url,
profile_volume=f"taos-browser-{session['id']}",
auth_token=auth_token,
)
if kind == "host":
runner = request.app.state.browser_container_runner
# _connecting_host_ip does a blocking DNS lookup; offload it so the
# event loop is not stalled while the host is resolved.
nat1to1_ip = await asyncio.to_thread(_connecting_host_ip, request)
session = await mgr.start_on_host(
session["id"], profile_volume=vol, runner=runner, nat1to1_ip=nat1to1_ip
)

@gitar-bot gitar-bot Bot Jun 21, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Edge Case: Orphaned session record when worker lookup fails after create

In the new create_session, the session record is now created (mgr.create_session(...) at line 130) before the worker is resolved. In the worker branch, cluster.get_worker(node) can return None (line 145-147), which returns a 409 no_capable_node but leaves a freshly-created session row persisted in the store with no container ever started (an orphan in pending/idle state).

This is a behavior change from the previous code, where the worker is None check ran before create_session, so no orphan was created. resolve_browser_target returns ("worker", node) based on pick_browser_node / list_browser_nodes, and there's a small TOCTOU window (or stale node listing) where get_worker can then return None.

Impact is minor (rare race, leftover idle record), but over time these orphans can accumulate. Consider resolving the worker before calling create_session, or cleaning up the session record before returning 409. Note the same orphan exists for the BrowserWorkerError/502 path, but that was pre-existing.

Resolve the worker before creating the session record so a failed lookup returns 409 without leaving an orphan.:

kind, node = target
worker = None
if kind == "worker":
    worker = cluster.get_worker(node)
    if worker is None:
        return JSONResponse({"error": "no_capable_node"}, status_code=409)

mgr = request.app.state.browser_sessions
session = await mgr.create_session(
    "user", user_id, body.url, body.profile or "default"
)
vol = f"taos-browser-{session['id']}"
auth_token = getattr(request.app.state, "browser_worker_auth_token", None)
try:
    if kind == "host":
        ...
    else:
        session = await mgr.start_on_worker(session["id"], node=node, worker_url=worker.url, profile_volume=vol, auth_token=auth_token)

Was this helpful? React with 👍 / 👎

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
tests/test_routes_browser_sessions.py (1)

116-126: ⚡ Quick win

Consider asserting create_session was not called on early 409.

When resolve_browser_target returns None, the route returns 409 before mgr.create_session is invoked. Adding an assertion strengthens the test by verifying no orphan session is created on this path.

         assert resp.status_code == 409
         assert resp.json()["error"] == "no_capable_node"
+        mgr.create_session.assert_not_called()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_routes_browser_sessions.py` around lines 116 - 126, The
test_create_409_when_no_host_and_no_workers method should verify that when a 409
response is returned due to no capable nodes, the mgr.create_session method is
not invoked. Add an assertion after the existing resp.status_code and
resp.json() assertions to call assert_not_called() on the mgr.create_session
mock, ensuring that no orphan session is created when the route returns early
with a 409 error.
tinyagentos/routes/browser_sessions.py (1)

119-154: 💤 Low value

Orphan session on TOCTOU race when worker disappears.

If resolve_browser_target returns ("worker", node) but cluster.get_worker(node) returns None at line 145-147 (worker deregistered between the two calls), the session created at line 130-132 is left orphaned in pending status. This is a rare race, but worth noting.

Consider either:

  1. Moving session creation after the worker lookup succeeds, or
  2. Deleting the orphan session before returning 409.

Given the rarity of this race and that pending sessions can be garbage-collected later, this is low-priority.

♻️ Optional fix: clean up orphan session
         else:
             worker = cluster.get_worker(node)
             if worker is None:
+                await mgr.delete_session(session["id"])
                 return JSONResponse({"error": "no_capable_node"}, status_code=409)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/routes/browser_sessions.py` around lines 119 - 154, There is a
race condition where a session is created by calling mgr.create_session() before
verifying the worker exists. If cluster.get_worker(node) returns None (worker
was deregistered between the resolution and lookup), the session is left
orphaned. Move the mgr.create_session() call to after the worker is successfully
retrieved in the worker branch, so the session is only created once the worker
is confirmed to exist. Alternatively, if you need to keep session creation
early, capture the session id before the worker check and call a cleanup method
on mgr to delete the orphan session before returning the 409 error for
no_capable_node.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/test_routes_browser_sessions.py`:
- Around line 116-126: The test_create_409_when_no_host_and_no_workers method
should verify that when a 409 response is returned due to no capable nodes, the
mgr.create_session method is not invoked. Add an assertion after the existing
resp.status_code and resp.json() assertions to call assert_not_called() on the
mgr.create_session mock, ensuring that no orphan session is created when the
route returns early with a 409 error.

In `@tinyagentos/routes/browser_sessions.py`:
- Around line 119-154: There is a race condition where a session is created by
calling mgr.create_session() before verifying the worker exists. If
cluster.get_worker(node) returns None (worker was deregistered between the
resolution and lookup), the session is left orphaned. Move the
mgr.create_session() call to after the worker is successfully retrieved in the
worker branch, so the session is only created once the worker is confirmed to
exist. Alternatively, if you need to keep session creation early, capture the
session id before the worker check and call a cleanup method on mgr to delete
the orphan session before returning the 409 error for no_capable_node.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 93d1efe2-f75f-4f04-88df-58a84b0062c2

📥 Commits

Reviewing files that changed from the base of the PR and between 9b200e6 and 8138c1d.

📒 Files selected for processing (7)
  • desktop/src/apps/StreamedBrowserApp/StreamedBrowserApp.test.tsx
  • desktop/src/apps/StreamedBrowserApp/StreamedBrowserApp.tsx
  • desktop/src/apps/StreamedBrowserApp/index.ts
  • desktop/src/registry/app-registry.ts
  • desktop/vite.config.ts
  • tests/test_routes_browser_sessions.py
  • tinyagentos/routes/browser_sessions.py
💤 Files with no reviewable changes (4)
  • desktop/src/apps/StreamedBrowserApp/index.ts
  • desktop/vite.config.ts
  • desktop/src/apps/StreamedBrowserApp/StreamedBrowserApp.tsx
  • desktop/src/apps/StreamedBrowserApp/StreamedBrowserApp.test.tsx

…1285)

create_session created the session record before resolving the worker, so a
failed get_worker in the worker branch returned 409 while leaving an orphaned
session row in pending/idle. Move the worker lookup ahead of create_session.
Test: worker-lookup failure 409s and create_session is never called.
@jaylfc jaylfc merged commit be4cff2 into dev Jun 21, 2026
7 of 8 checks passed
@github-project-automation github-project-automation Bot moved this from Todo to Done in TinyAgentOS Roadmap Jun 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

1 participant