mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 02:17:31 +00:00
* fix(browser): negotiate a larger tunnel flow-control window for high-latency links Embedded-browser tabs routed through a paired runtime (e.g. another Mac over Tailscale) were capped by a fixed 256 KB per-stream flow-control window. That window also bounds per-stream throughput to window / RTT: on a ~90 ms link the ceiling is ~2.9 MB/s per connection regardless of real bandwidth, while on a sub-millisecond LAN the same window allows ~256 MB/s, so the limit is invisible locally and dominant remotely. The window value is validated symmetrically on both ends (each side rejects a credit grant above its own constant), so simply raising the constant would break browsing between mismatched Orca versions. Instead negotiate it: each side advertises its maximum (client in the attach params, host in the ready event) and both use min(own, peer), falling back to the legacy 256 KB window whenever either side does not advertise. Advertising peers settle on a 2 MB window (~8x the high-RTT ceiling). Retained buffers and the outbound memory budget scale to cover the larger window. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(browser): track granted receive credit and scale the session retained limit Review fixes for the window negotiation: both ends still initialized receiveCredit to the legacy 256 KB constant while announcing the negotiated window, so a peer legitimately using the announced 2 MB window tripped credit validation — the client closed the tunnel and the session treated the traffic as a protocol violation. Initialize credit to zero and assign it where the grant is announced, so tracking always matches the announcement; a conformance test now drives a full negotiated window in both directions unsettled. Also scale the per-session retained-byte limit with the negotiated window (16 windows, floored at the previous fixed 8 MB) so concurrent streams on a slow link keep their stream-count headroom instead of being shed at four. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(browser): scale the pending-chunk cap with the negotiated window The 256-chunk pending caps paired with the legacy 256 KB window at 1 KiB frames. Under a negotiated 2 MB window, more than 256 small unsettled frames fit inside the byte credit, and the receiver treated that permitted traffic as an overflow — the session closed on the destination write path and the client fenced the whole tunnel on the receive path. Derive the cap from the maximum window at the same 1 KiB-per-chunk pairing (2048), keeping the flood bound for tinier frames. The conformance regression now drives a full negotiated window of 1 KiB frames in both directions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(browser): size claim capacity to the byte budget at chunk granularity Each retained chunk consumes one claim, so the fixed 16,384-claim host limit exhausted at eight stalled 2 MB windows of 1 KiB frames — 16 MiB of a 64 MiB byte budget — and shed the next stream as a receive-buffer overflow. Derive the claim caps from the byte budgets at the same 1 KiB-per-chunk pairing the pending caps use, so claim exhaustion never precedes byte exhaustion for in-protocol frames. Claims are counters, not stored entries, so the higher caps retain no extra memory. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(browser): bound larger tunnel windows with connection-level credit Fixes #38053 Rework the window negotiation onto a window.v2 capability: the client advertises network.browserTunnel.window.v2 and the host echoes it on ready. v2 tunnels add a connection window per direction (16 MiB to the client, 4 MiB to the host) acknowledged on receipt over stream 0, so the queued bytes on either WebSocket stay well below its overflow cap however many streams grow, and a stalled reader cannot starve other streams. Older peers keep the legacy 256 KiB / 256-chunk bounds. Streams start at the legacy 256 KiB window and grow toward 2 MiB by the bytes their reader consumes, drawing on the connection window. Growth is charged only while a stream holds unread data: it is released when the reader drains and recharged when data arrives again, so idle keep-alive sockets don't hold a fresh download at legacy speed. Connection acknowledgements are sent after the Data frame is dispatched, so a synchronous peer cannot reorder its next frame ahead. Tradeoff: because idle streams are not charged, an idle grown stream that wakes and then stalls can exceed the growth pool by its own growth. The per-stream memory budgets still fail that stream rather than the tunnel. Co-authored-by: jeky22 <38278199+jeky22@users.noreply.github.com> * fix(browser): keep grown tunnel windows charged until a stream is genuinely idle Fixes #38053 Growth was released whenever a reader drained, and Chromium drains at once, so the connection pool barely bounded growth: 16 grown downloads that stalled together reached the 32 MiB client budget and were reset, and 8 stalled uploads passed the host's 12 MiB retained limit. Growth now stays charged while a stream holds it. A stream gives its charge up only once it is idle (drained, nothing received for 1 s) and another stream needs the pool, so kept-alive sockets still don't slow a fresh download. A woken idle stream is charged again; while that overcommits the pool, consuming streams withhold credit to shrink back toward the legacy window. Zero-credit updates are not sent. * fix(browser): charge each tunnel frame a credit floor and complete empty writes Fixes #38053 The WebSocket queues cap frames as well as bytes, so a connection window spent on 1 KiB frames could queue 4,096 frames and close a paired transport capped at 2,048. Each DATA frame now costs at least 8 KiB of connection credit on both sides, bounding queued data frames to 2,048 toward the client and 512 toward the host. An empty write found no credit to spend and waited forever, blocking every later write; it now completes at once. --------- Co-authored-by: jeky22 <38278199+jeky22@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
41 lines
2.5 KiB
TypeScript
41 lines
2.5 KiB
TypeScript
export const BROWSER_GOOGLE_COOKIE_CLEAR_CAPABILITY = 'browser.google-cookie-clear.v1' as const
|
|
// Advertise only with an available offscreen backend so remote-owned pages cannot fall back to local desktop tabs.
|
|
export const BROWSER_HEADLESS_RUNTIME_CAPABILITY = 'browser.headless.v1' as const
|
|
export const BROWSER_IDENTITY_RUNTIME_CAPABILITY = 'browser.identity.v1' as const
|
|
export const BROWSER_SCREENCAST_RUNTIME_CAPABILITY = 'browser.screencast.v1' as const
|
|
export const BROWSER_CERTIFICATE_TRUST_RUNTIME_CAPABILITY = 'browser.certificate-trust.v1' as const
|
|
// Older hosts drop tabCreate.page; trust preallocated IDs only when advertised.
|
|
export const BROWSER_TAB_CREATE_KNOWN_ID_RUNTIME_CAPABILITY =
|
|
'browser.tab-create-known-id.v1' as const
|
|
export const BROWSER_CLIENT_HOST_RUNTIME_CAPABILITY = 'browser.clientHost.v1' as const
|
|
export const BROWSER_CLIENT_PAGE_METADATA_RUNTIME_CAPABILITY =
|
|
'browser.clientHost.pageMetadata.v1' as const
|
|
export const BROWSER_CLIENT_AUTOMATION_RUNTIME_CAPABILITY =
|
|
'browser.clientHost.automation.v1' as const
|
|
// Why: without it a client-placed browser.upload would resolve remote paths on the desktop filesystem, so uploads fail closed instead.
|
|
export const BROWSER_CLIENT_FILE_CHANNEL_RUNTIME_CAPABILITY =
|
|
'browser.clientHost.fileChannel.v1' as const
|
|
export const BROWSER_NETWORK_TUNNEL_RUNTIME_CAPABILITY = 'network.browserTunnel.v1' as const
|
|
export const BROWSER_NETWORK_EXECUTION_HOSTS_RUNTIME_CAPABILITY =
|
|
'network.browserTunnel.executionHosts.v1' as const
|
|
// Larger stream windows bounded by connection-level credit; the host echoes it on ready to opt in.
|
|
export const BROWSER_NETWORK_TUNNEL_WINDOW_V2_CAPABILITY =
|
|
'network.browserTunnel.window.v2' as const
|
|
|
|
// Older hosts drop expectUrl and would act unguarded, so the CLI refuses the flag without this.
|
|
export const BROWSER_EXPECT_URL_RUNTIME_CAPABILITY = 'browser.expect-url.v1' as const
|
|
|
|
// Headless, identity and certificate trust are host-conditional, so getStatus adds them itself.
|
|
export const BROWSER_RUNTIME_CAPABILITIES = [
|
|
BROWSER_EXPECT_URL_RUNTIME_CAPABILITY,
|
|
BROWSER_GOOGLE_COOKIE_CLEAR_CAPABILITY,
|
|
BROWSER_SCREENCAST_RUNTIME_CAPABILITY,
|
|
BROWSER_TAB_CREATE_KNOWN_ID_RUNTIME_CAPABILITY,
|
|
BROWSER_CLIENT_HOST_RUNTIME_CAPABILITY,
|
|
BROWSER_CLIENT_PAGE_METADATA_RUNTIME_CAPABILITY,
|
|
BROWSER_CLIENT_AUTOMATION_RUNTIME_CAPABILITY,
|
|
BROWSER_CLIENT_FILE_CHANNEL_RUNTIME_CAPABILITY,
|
|
BROWSER_NETWORK_TUNNEL_RUNTIME_CAPABILITY,
|
|
BROWSER_NETWORK_EXECUTION_HOSTS_RUNTIME_CAPABILITY
|
|
] as const
|