Skip to content

feat(alb): saas compatible connection multiplexing and metrics#1399

Merged
Rod-Christensen merged 31 commits into
developfrom
feat/alb
Jul 10, 2026
Merged

feat(alb): saas compatible connection multiplexing and metrics#1399
Rod-Christensen merged 31 commits into
developfrom
feat/alb

Conversation

@Rod-Christensen

@Rod-Christensen Rod-Christensen commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Scope

108 files, +4,938 / −2,597 vs develop. This is a focused, cohesive "alb" feature: SaaS-compatible connection multiplexing, subprocess metrics self-reporting, decoupling apps from auth, and per-request RequestContext threading. The subsystems below are interdependent and reviewed together.

The ai unit-test suite is migrated to the refactors below and is green: ./builder ai:test1329 passed, 0 failed.

Post-review hardening (in this branch). After a full review of the branch, the 12 correctness findings it surfaced — all of them this branch's own regressions — were fixed in-branch rather than deferred. Those fixes are folded into the sections below (subscription/gate migration, ordered event delivery, permission consistency, dashboard pagination, deep-link gate, billing warmup/idle, path validation). The PYTHONASYNCIODEBUG=1 instrumentation flagged in the first review pass has been disabled.


1. DECOUPLING APPS FROM AUTH (the AccountInfo diet)

What changed

  • AccountInfo no longer carries apps: list[AppManifestEntry] or credits: dict
  • AccountInfo does carry a slim subscriptions: dict[str, str] (appId → AppStatus) — status only, no seats/pricing/stripe. This is the authoritative entitlement source used by the run/deploy gate and the shell's subscribe prompt; it flows to ConnectResult via to_connect_result() and is re-sent on every apaext_account push, so cached getAccountInfo() stays fresh.
  • ConnectResult (wire format) drops apps/credits and gains subscriptions in Python and TypeScript SDKs
  • ServerInfoResult drops apps — the probe now returns a single home app entry
  • New AccountInfo.to_pod_context() returns a slim dict for inter-pod forwarding (now includes waitlisted)
  • New server endpoint: rrext_account_me?subcommand=desktop returns the user's desktop apps on demand
  • Both Python and TS SDKs add account.get_desktop() / account.getDesktop()
  • Shared AppStatus enum + isActiveStatus()/ACTIVE_APP_STATUSES (subscribed/trialing/free) promoted into the TS SDK (mirrored in Python), reused by the gate, SettingsPage, and useSubscriptions

Why it changed

The auth handshake was doing too much work. Every login triggered a cascade of DB queries (desktop apps, subscriptions, credit wallets) that serialized into ConnectResult. This blocked the login response, inflated the payload, and meant every broadcast_account_update re-serialized the full app catalog to every connected client.

By moving the app manifest to a separate fetch-after-auth call, login becomes a near-pure identity operation. The one piece kept on AccountInfo is the lightweight subscriptions status map — it is tiny (appId → status string) and every consumer that gates on "is this app active" needs it to be reactive, so it rides the existing apaext_account push instead of a separate fetch.

OSS impact

OSS authenticate() no longer builds apps=[...]; it now populates subscriptions as all apps 'free' (an active status), so every subscription check passes on OSS without per-component saas capability branches. The probe returns home: self._home_app. The previously-NotImplementedError OSS desktop subcommand is now implemented, returning all apps with onDesktop=true so OSS mirrors the SaaS desktop.

Files: packages/ai/src/ai/account/models.py, packages/ai/src/ai/account/oss/__init__.py, packages/ai/src/ai/account/base.py, packages/ai/src/ai/modules/task/commands/cmd_public.py, packages/client-python/src/rocketride/types/client.py, packages/client-typescript/src/client/types/client.ts, packages/client-python/src/rocketride/account.py, packages/client-typescript/src/client/account.ts, apps/vscode/src/shared/util/subscriptionGate.ts


2. APP REGISTRY (shell-ui architecture)

What changed

  • New apps/shell-ui/src/hooks/AppRegistryContext.tsx — single source of truth for registered apps
  • WorkspaceProvider no longer accepts an apps prop; reads from useAppRegistry()
  • useSubscriptions reads subscription status from AccountInfo.subscriptions (reactive on apaext_account), not from the registry. The registry keeps only desktop membership (onDesktop) + the MF entry — subscription and desktop placement are independent (an app can be subscribed but off-desktop), so the desktop-derived registry is the wrong source for entitlement.
  • mergeApps splits full vs lean incoming entries: full entries (with an MF entry URL) register as remotes; a lean apaext_desktop membership push (id + onDesktop only) now patches onDesktop onto the existing registry row by id instead of being dropped by the entry filter
  • Bootstrap registers only the home app from probe (not the full catalog)
  • Shell exports AppRegistryProvider and useAppRegistry for downstream consumers

Why it changed

Previously the app list was assembled from three sources (probe, ConnectResult.apps, runtime merges) via a fragile useMemo in Shell.tsx, with races between probe results and post-auth merges. The registry centralizes membership; entitlement moves to the authoritative AccountInfo.subscriptions map so the two concerns don't fight.

OSS impact

OSS shell behavior is unchanged functionally — rocketride.hello is registered from probe, and after auth the desktop fetch returns all OSS apps. Custom shell consumers that imported WorkspaceProvider with an apps prop must wrap with AppRegistryProvider instead.

Files: apps/shell-ui/src/hooks/AppRegistryContext.tsx, apps/shell-ui/src/components/layout/Shell.tsx, apps/shell-ui/src/workspace/WorkspaceContext.tsx, apps/shell-ui/src/workspace/useWorkspaceState.ts, apps/shell-ui/src/hooks/useSubscriptions.ts, apps/shell-ui/src/views/settings/SettingsPage.tsx, apps/shell-ui/src/bootstrap.tsx, apps/shell-ui/src/lib/appLoader.ts, apps/shell-ui/src/index.ts


3. DEEP LINK GATE + BRANDED LOADING

What changed

  • LoadingScreen gains app-branded (icon + name + spinner) and deep-link-pending (spinner only) modes
  • New DeepLinkGate wraps ShellLayout — holds the loading screen during deep-link navigation, but releases on any terminal condition: target MF remote loaded, target load errored (appLoadErrors[sessionAppId]), the shell fell back to the default app, or a 15s safety timeout. (Previously it released only on success, so a deep-linked app that 404s/times-out/isn't in the catalog stranded the branded spinner forever and the error+Retry UI never rendered.)
  • Shell pre-fetches the target app's name/icon from rrext_public_catalog before starting auth
  • Sidebar hides "Home" when hideAppSwitcher is true (vanity domain mode)

Why it changed

Deep-linking via ?appId=someApp previously flashed home-ui before the target loaded, and could hang indefinitely on a bad app id. The gate resolves the target before rendering, shows the requested app's branding, and always terminates.

OSS impact

Neutral — the infrastructure is available for self-hosted single-app deployments.

Files: apps/shell-ui/src/components/layout/LoadingScreen.tsx, apps/shell-ui/src/components/layout/Shell.tsx, apps/shell-ui/src/hooks/AppRegistryContext.tsx, apps/shell-ui/src/components/layout/Sidebar.tsx


4. DESKTOP UPDATE EVENT CHANNEL

What changed

  • Server can push apaext_desktop DAP events (separate from apaext_account) for "app added/removed from my desktop" — carries desktop membership only, not subscription status
  • Shell-ui ConnectionManager routes it to shell:desktopUpdate; VSCode does the same and DeployManager forwards it
  • ShellConnectionEventMap gains the shell:desktopUpdate entry

Why it changed

Previously any desktop change triggered a full broadcast_account_update (re-auth, rebuild AccountInfo, push to all connections). The dedicated event sends only the desktop delta, and only to the affected user. Subscription changes stay on apaext_account; desktop membership rides apaext_desktop — the two are kept distinct on purpose (see §2).

OSS impact

In OSS, desktop membership is static (all apps always on desktop), so this channel is inert; the routing is present in both connection managers with minimal overhead.

Files: apps/shell-ui/src/connection/connection.ts, apps/vscode/src/connection/connection.ts, apps/vscode/src/connection/deploy-manager.ts, packages/shared-ui/src/types/shell.ts


5. REQUEST CONTEXT (ctx) THREADING

What changed

  • New RequestContext dataclass: account_info, conn_id, source
  • DAPConn.on_receive() is factored into three overridable hooks — _track_message (per-message bookkeeping), _pre_dispatch_gate (returns False to reject), and _build_ctx (per-request RequestContext) — with behavior-preserving base implementations. TaskConn overrides only those three (auth gate, inbound-metric counters, pod-_ctx extraction) instead of copy-pasting the whole dispatch method.
  • DAPConn gains has_permission(), verify_permission(), verify_auth() (moved from TaskConn)
  • Every command handler across all command mixins (cmd_task, cmd_account, cmd_app, cmd_debug, cmd_deploy, cmd_misc, cmd_data, cmd_cprofile, cmd_monitor, cmd_public, cmd_store) updated to (request, ctx)
  • self._account_info references in handlers replaced with ctx.account_info
  • Task lookup consolidated on get_task_control_by_token() + TaskServer.verify_task_access(control, ctx, ...)

Permission-model consistency (post-review fixes)

  • resolve_team_permissions now honors the same sys.admin/internal bypass as resolve_task_permissions before the membership check, so an admin / pod-internal caller is no longer denied team-scoped commands (on_execute, on_rrext_get_tasks, on_rrext_dashboard).
  • to_pod_context() now forwards waitlisted, so a forwarded ctx can't deserialize waitlisted=False and slip a waitlisted user past verify_auth in pod mode.
  • _build_ctx guards the forwarded-_ctx account lookup (raw_ctx.get('account_info')), so a malformed/pre-auth forward yields account_info=None instead of a KeyError that escapes on_receive and silently drops the message.

Why it changed

In the pod architecture the caller's identity arrives via _ctx injected by the orchestrator; reaching for self._account_info (the orchestrator's service credential in pod mode) was wrong. Threading ctx makes the caller identity explicit regardless of topology. For OSS (single-process) ctx is built from connection state, so behavior is identical.

Does this break the shipped OSS server? — No (verified)

There are two independent dispatch hierarchies and each is internally consistent:

  • Server-side (changed): dap_conn.py has the only _call_method override in ai; it calls method(message, ctx) and every resolvable handler accepts (request, ctx) (cprofile/process handlers default ctx=None).
  • Client-side (untouched): the client dap_base.py base still calls method(message); the message-only on_* methods all live on DAPClient subclasses dispatched by it.

The two never cross, so no ctx-passing dispatch reaches a 2-arg handler and vice-versa. The breakage is external-only (downstream custom DAPConn subclasses / command mixins) and fails loud (TypeError). The in-repo migration is complete and covered by the updated tests.

Files: packages/ai/src/ai/account/models.py, packages/ai/src/ai/common/dap/dap_conn.py, packages/ai/src/ai/modules/task/task_conn.py, packages/ai/src/ai/modules/task/task_server.py, packages/ai/src/ai/account/oss/__init__.py, packages/ai/src/ai/modules/data/data_conn.py, and all cmd_*.py files


6. EVENT BROADCAST — ordered per-connection delivery (post-review fix)

What changed

  • broadcast_task_event no longer spawns an unretained asyncio.create_task(_send()) per connection. Each connection now owns a bounded outbound asyncio.Queue + a lifelong drain task; broadcast does put_nowait onto each subscribed connection's queue and returns O(1).
  • Overflow policy by event type: SSE (stream chunks) → disconnect the slow consumer (dropping chunks corrupts the stream); idempotent events (SUMMARY/DASHBOARD/status) → drop-oldest. Queue + drain task are torn down on disconnect.
  • New CONST_CONN_OUT_QUEUE_MAX.

Why it changed

The old fire-and-forget had two bugs: (1) per-connection ordering was lost — event N+1's task could beat N's and corrupt SSE reconstruction; (2) asyncio holds only weak refs to bare tasks, so a task could be GC'd before running and the event was silently dropped. The per-connection FIFO queue preserves order and owns its task (no GC drop); a slow WebSocket stalls neither other connections nor the parent event loop.

Files: packages/ai/src/ai/modules/task/task_server.py, packages/ai/src/ai/modules/task/commands/cmd_monitor.py, packages/ai/src/ai/constants.py


7. METRICS OVERHAUL (subprocess self-reporting)

Node-side coupling is self-contained in this PR. The add_time()add_value()/set_value() rename moves together with all its emitters in packages/ai/src/ai/common/models/*. The nodes/ tree needs no change (the only metrics-touching node, llamaparse, uses metrics.event(), unchanged). No cross-PR nodes dependency.

What changed

  • task_metrics.py: removed all psutil/pynvml parent-side sampling; the class is now billing-only
  • New ProcessReporter (packages/ai/src/ai/web/metrics/process_reporter.py) — daemon thread inside the subprocess sampling CPU (getrusage/psutil), RSS, GPU VRAM (pynvml)
  • Two-protocol split: >MET* for live dashboard metrics, >USG* for billing
  • >USG* is now emitted both at object boundaries (taskhook) AND periodically (CONST_BILLING_REPORT_INTERVAL = 15s) from ProcessReporter, so an idle pipeline that holds GPU/RAM but pushes no traffic (e.g. a chat that's never messaged) still accrues billing instead of billing 0
  • Warmup/model-load excluded from billing: the billing baseline is captured on the first real usage sample after service-up (parent-side deferred baseline), so CPU/mem burned loading a model before the service is up isn't billed
  • Removed the dead avg_* task-metric fields (never written, consumed nowhere) from the SDK types, vscode types, and the engine's zeroing
  • transport_stdio.py: >USG* handler parses billing payload → apaevt_status_tokens
  • metrics.py: add_time()add_value(); new set_value(); report format {timers, counters, events}{values, events}
  • TASK_TOKENS model: fixed fields → ConfigDict(extra='allow') (dynamic keys)
  • Billing report loop centralized in task_server.py
  • CONST_METRICS_SAMPLE_INTERVAL/CONST_METRICS_STOP_TIMEOUT removed; CONST_PROCESS_REPORT_INTERVAL = 1.0 added

Why it changed

Parent-side psutil polling sampled the child from the outside — inaccurate for GPU workloads, blind to per-model consumption. Self-reporting from inside the subprocess is accurate (direct getrusage/GPU context), lower overhead, and enables per-component billing. The two-protocol split separates high-frequency live telemetry (>MET*) from cumulative billing (>USG*).

OSS impact

High. The metrics format change (timers/counters → flat values) is a wire-level breaking change; code consuming raw >MET* or TASK_TOKENS fields by name breaks. add_time()add_value() affects custom nodes.

Files: packages/ai/src/ai/modules/task/task_metrics.py, packages/ai/src/ai/web/metrics/process_reporter.py, packages/ai/src/ai/web/metrics/metrics.py, packages/ai/src/ai/web/metrics/taskhook.py, packages/ai/src/ai/common/dap/transport_stdio.py, packages/ai/src/ai/constants.py, packages/ai/src/ai/modules/task/task_server.py, packages/ai/src/ai/modules/task/task_engine.py, packages/ai/src/ai/common/models/*, apps/vscode/src/shared/types/taskStatus.ts


8. DASHBOARD PAGINATION + RELOCATION

What changed

  • on_rrext_dashboard removed from cmd_misc.py and rebuilt in cmd_monitor.py with real pagination, sorting, and state filtering (the previous version ignored request['arguments'] entirely and returned an unpaginated dump with tasks_total = len(returned))
  • Request shape: { tasks: { offset, limit, sort_by, sort_order, state_filter }, connections: { ... } }; state_filter maps running/completed to task states; totals are computed on the filtered list before slicing; limit:0 omits the section
  • Response: tasks/connections optional; tasks_total/connections_total added; DashboardOverview gains completedTasks, totalTasks, eaasNodes
  • Both SDKs kept in sync: TS getDashboard() and Python get_dashboard() accept the pagination params; Python DASHBOARD_* types mirror the TS shape
  • Monitor tabs null-safe with ?? []

Why it changed

The old dashboard dumped all tasks/connections unpaginated — wasteful and slow for servers with hundreds of historical tasks. Pagination + sorting lets the monitor UI page on demand; relocation co-locates it with the rest of the monitoring infrastructure.

OSS impact

getDashboard() stays backward-compatible (no args = defaults), but consumers that assumed tasks/connections are always present arrays must handle the optional fields.

Files: packages/ai/src/ai/modules/task/commands/cmd_misc.py, packages/ai/src/ai/modules/task/commands/cmd_monitor.py, packages/client-typescript/src/client/types/dashboard.ts, packages/client-python/src/rocketride/types/dashboard.py, packages/client-python/src/rocketride/mixins/dashboard.py, packages/client-typescript/src/client/client.ts, packages/shared-ui/src/modules/server/MonitorView.tsx, packages/shared-ui/src/modules/server/components/OverviewTab.tsx, packages/shared-ui/src/modules/server/components/ConnectionsTab.tsx


9. SDK HARDENING (DAPException, transport guards, auto-chunking, path validation)

What changed

  • DAPException replaces RuntimeError/Error for failed DAP calls; carries file/lineno from the server trace
  • Transport null-guards in the Python client (send(), request(), disconnect())
  • attach() fix: desired-state upgrade-only (prevents downgrade from authenticated to attached)
  • DataPipe.write() auto-chunks at 512KB in both SDKs
  • validateStorePath() rejects absolute paths AND Windows drive-letter paths (C:\… / C:/…) — those don't start with a slash and : is a legal POSIX path char, so they previously slipped through; the fix is mirrored in the Python SDK validator (kept in sync per the cross-SDK rule)

Why it changed

RuntimeError lost the server-side trace; DAPException preserves it. Transport guards prevent crashes during reconnection races. The attach() fix prevents desired-state reset breaking reconnection. Auto-chunking prevents WebSocket frame overflow. The drive-letter reject closes a path-validation gap.

OSS impact

except RuntimeError still catches DAPException (subclass) in Python. Auto-chunking and the attach()/path fixes are transparent bugfixes.

Files: packages/client-python/src/rocketride/core/exceptions.py, packages/client-python/src/rocketride/core/dap_client.py, packages/client-python/src/rocketride/mixins/connection.py, packages/client-python/src/rocketride/client.py, packages/client-python/src/rocketride/mixins/data.py, packages/client-python/src/rocketride/mixins/store.py, packages/client-typescript/src/client/client.ts, packages/client-typescript/src/client/exceptions/index.ts


10. STORE PROVIDER LAZY IMPORTS

What changed

  • store_providers/__init__.py no longer eagerly imports FilesystemStore/MemoryStore/S3Store/AzureBlobStore — just a docstring explaining the lazy-import rationale
  • StoreCommands (cmd_store.py) is ctx-threaded with the rest of the command mixins (§5)

Why it changed

Eager imports triggered depends() for every backend on every startup. Lazy imports load only the selected backend's dependencies.

OSS impact

Direct imports like from ai.account.store_providers import S3Store break; use from ai.account.store_providers.s3 import S3Store. This import was internal — Store.create() handles backend selection.

Files: packages/ai/src/ai/account/store_providers/__init__.py, packages/ai/src/ai/modules/task/commands/cmd_store.py


11. MISCELLANEOUS

  • ConnectionManager singleton: stored on globalThis via Symbol.for() to survive MF module duplication
  • Replayable events: shell:loginRequest/shell:subscribe buffered if no listener and replayed on registration
  • Store eager init: Store.create() called in __init__/start() instead of lazy first-access
  • Proxy-mode device lock: make_device_lock() replaces threading.Lock() in 6 vision models — nullcontext() in proxy mode, since the lock is only needed for local GPU access
  • Debug scaffolding removed: [TIMING] perf-counter blocks in task_engine/task_server, and [on_rrext_monitor]/[set_monitor] argument/permission dumps in cmd_monitor
  • Dead code dropped: TaskMetrics.pid param (stored, never read)
  • asyncio debug mode disabled: the PYTHONASYNCIODEBUG=1 block (ai/__init__.py) and loop.set_debug(True) / slow_callback_duration = 0.1 (eaas.py) — temporary instrumentation from an earlier diagnostic pass — are now commented out so they don't ship

BREAKING CHANGES

All rows below are changes introduced by this PR (against origin/develop).

# Change What breaks Migration Risk
1 ConnectResult.apps removed Code reading connectResult.apps after auth Use client.account.getDesktop() / get_desktop() HIGH
2 ConnectResult.credits removed Code reading connectResult.credits Fetch via billing endpoint MEDIUM
3 ServerInfoResult.apps removed Code reading probe apps for pre-auth catalog Use rrext_public_catalog; probe returns single home field MEDIUM
4 ConnectResult.subscriptions added Additive; consumers gain an entitlement map Read subscriptions[appId] (∈ subscribed/trialing/free = active) LOW
5 All on_* handler signatures → (request, ctx) External-only — every in-repo handler is migrated (verified, §5). Breaks downstream custom DAPConn subclasses / command mixins overriding on_* Add ctx to the overriding handler; fails loud (TypeError) HIGH (external)
6 metrics.add_time()add_value() Custom nodes calling metrics.add_time() Rename to add_value() MEDIUM
7 Metrics report format {timers, counters}{values} Code parsing raw >MET*/>USG* output or TASK_STATUS.tokens fields by name Use flat values; TASK_TOKENS fields are dynamic HIGH
8 TASK_TOKENS fixed fields → dynamic extra='allow' Code accessing .cpu_utilization/.cpu_memory/.gpu_memory by name Access dict-style / iterate keys MEDIUM
9 Dashboard response: tasks/connections now optional Code destructuring { tasks, connections } without null checks Add ?? [] / .get('tasks', []) LOW
10 DashboardOverview new fields TS code constructing DashboardOverview (unlikely for consumers) Add completedTasks, totalTasks, eaasNodes LOW
11 store_providers/__init__.py no longer re-exports backends from store_providers import S3Store Import from submodule: from store_providers.s3 import S3Store LOW
12 WorkspaceProvider no longer accepts apps prop Custom shell integrations passing apps Wrap with AppRegistryProvider LOW
13 get_task_control()get_task_control_by_token() Internal callers Rename + use verify_task_access() for permission checks LOW

RISK ASSESSMENT BY AREA

Area Risk Rationale
Auth/ConnectResult diet High Every SDK consumer is affected; missing apps/credits return undefined/KeyError rather than erroring at the call site. The new subscriptions map is additive.
ctx threading High (external only) Most pervasive change by surface area, but the shipped server is internally consistent (both dispatch hierarchies match their handlers — §5). Only downstream overrides are affected, and they fail loud. Covered by tests.
Metrics overhaul High Wire-format + billing-integration change. Affects dashboard, VS Code, any custom metrics consumer. Warmup exclusion + periodic >USG* change what gets billed (verify billing totals).
Event delivery Medium Per-connection queue is new machinery on the hot SSE path; overflow policy differs by event type. Ordering and no-GC-drop are the fixes it exists to provide.
App Registry Medium App-lifecycle refactor in the shell; entitlement now off-registry (on AccountInfo.subscriptions). appsRef synchronous update mitigates ensureApp/desktop-fetch/switchApp races; the desktop-fetch dedupe + deep-link gate timeout close the remaining ones.
Dashboard pagination Low Additive; old callers (no args) get defaults; UI null-guarded.
SDK hardening Low DAPException extends RuntimeError; transport guards + path-validation reject are strictly defensive; auto-chunking transparent.

NOTES FOR THE REVIEWER

  • Billing behavior changed intentionally: idle pipelines now bill (periodic >USG*), and model-load warmup no longer bills (deferred baseline). Worth a sanity check against expected ledger totals for a long-model-load task and for a chat that's opened but never messaged.
  • One review finding was deliberately not "fixed": a proposed shared live↔billing metric key-name map was declined — the live keys (cpu_memory_mb, an RSS snapshot) and billing keys (cpu_memory, cumulative GB-seconds) are different quantities, and billing keys are DB-driven (metrics_conversions), so there is no genuine 1:1 map to centralize.

Summary by CodeRabbit

  • New Features
    • Deep-link aware loading screen and gated shell startup (with timeout), showing the target app’s icon/name when available.
    • Centralized app registry for on-demand app loading and desktop catalog refresh.
    • Desktop app/icon labeling added to account and billing views; settings now uses the authoritative subscription state.
  • Bug Fixes
    • Improved deep-link/home navigation and app-switch fallback behavior.
    • Stronger permission enforcement across task actions and monitoring.
  • Billing & Metrics
    • Updated token/usage reporting to support dynamic metric keys and improved cumulative billing telemetry.

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a shell app registry and deep-link loading flow, refactors AI task DAP handlers to use per-request context and centralized authorization, replaces task metric sampling with subprocess billing snapshots, and updates client/shared UI contracts for subscriptions, dashboards, app labels, and dynamic token data.

Changes

Shell-UI app registry and deep-link loading

Layer / File(s) Summary
App registry and app loader
apps/shell-ui/src/hooks/AppRegistryContext.tsx, apps/shell-ui/src/lib/appLoader.ts, apps/shell-ui/rsbuild.config.mts, apps/shell-ui/src/connection/connection.ts
Adds a shell app registry with desktop refresh and on-demand app resolution, preserves app fields during mapping, shares rocketride in Module Federation, and routes apaext_desktop updates into shell:desktopUpdate.
Home probe and loading gate
apps/shell-ui/src/bootstrap.tsx, apps/shell-ui/src/components/layout/Shell.tsx, apps/shell-ui/src/components/layout/LoadingScreen.tsx
Bootstrap now reads a home manifest entry, Shell fetches deep-link app metadata and wraps rendering in a timeout gate, and LoadingScreen renders branded/deep-link variants.
Workspace registry integration
apps/shell-ui/src/workspace/WorkspaceContext.tsx, apps/shell-ui/src/workspace/useWorkspaceState.ts
Workspace sources apps from the registry context and uses registry-derived names when identifying the active app.
Sidebar, subscriptions, and settings
apps/shell-ui/src/components/layout/Sidebar.tsx, apps/shell-ui/src/hooks/useSubscriptions.ts, apps/shell-ui/src/index.ts, apps/shell-ui/src/views/settings/SettingsPage.tsx, apps/vscode/src/shared/util/subscriptionGate.ts, packages/shared-ui/src/types/shell.ts
Sidebar hides or disables home actions when the switcher is hidden, subscription state uses registry desktop membership plus entitlement maps, and the subscription gate checks cached app subscriptions.
VS Code desktop app labels
apps/vscode/src/connection/connection.ts, apps/vscode/src/connection/deploy-manager.ts, apps/vscode/src/providers/AccountProvider.ts, apps/vscode/src/providers/views/Account/AccountWebview.tsx, apps/vscode/src/providers/views/types.ts
Desktop events are forwarded into shell:desktopUpdate, and account init now carries minimal desktop app labels into the billing webview.

Server DAP RequestContext and permission refactor

Layer / File(s) Summary
Context and account contracts
packages/ai/src/ai/account/models.py, packages/ai/src/ai/account/base.py, packages/ai/src/ai/account/oss/__init__.py, packages/ai/src/ai/account/store_providers/__init__.py
Adds RequestContext, reshapes AccountInfo around subscriptions and pod context, extends account dispatch signatures, and changes OSS account behavior to synthesize subscriptions and a home app.
DAPConn request pipeline
packages/ai/src/ai/common/dap/dap_conn.py
Adds request-context-aware dispatch, auth gating, permission checks, and task-token helpers to the base DAP connection.
TaskConn context-driven auth and routing
packages/ai/src/ai/modules/task/task_conn.py
Threads RequestContext through task auth, debug routing, identify/ping, and task lookup while removing local permission helpers.
TaskServer access and broadcast
packages/ai/src/ai/modules/task/task_server.py
Centralizes task access checks, adds token/project lookup helpers, eager store creation, broadcast updates, and queued task-event delivery.
Typed DAP handlers with ctx
packages/ai/src/ai/modules/task/commands/cmd_account.py, cmd_app.py, cmd_cprofile.py, cmd_data.py, cmd_debug.py, cmd_deploy.py, cmd_misc.py, cmd_public.py, cmd_store.py, cmd_task.py, packages/ai/src/ai/modules/data/data_conn.py
All command handlers switch to typed DAP requests/responses with RequestContext, and task/app/account delegation is updated to use the new context-aware server/account APIs.
Monitor queue and dashboard
packages/ai/src/ai/modules/task/commands/cmd_monitor.py
Adds per-connection outbound queuing, context-based monitor permissions, and a paginated dashboard command with label helpers.
Asyncio debug comments
packages/ai/src/ai/__init__.py, packages/ai/src/ai/eaas.py
Adds commented-out asyncio debug scaffolding with no runtime behavior change.

Task billing and metrics pipeline rework

Layer / File(s) Summary
MetricsManager add_value/set_value
packages/ai/src/ai/web/metrics/metrics.py
Replaces add_time with add_value and set_value, and merges reports into a flat values map.
Inference call sites
packages/ai/src/ai/common/models/**/*.py
Model and OCR/vision metrics calls switch from add_time to add_value.
ProcessReporter and stdout protocol
packages/ai/src/ai/web/metrics/process_reporter.py, packages/ai/src/ai/web/metrics/taskhook.py, packages/ai/src/ai/common/dap/transport_stdio.py, packages/ai/src/ai/constants.py
Adds a daemon process reporter that emits live >MET* and cumulative >USG* snapshots, wires it into task lifecycle hooks, and teaches the transport to parse the new messages.
TaskMetrics billing ledger and audit
packages/ai/src/ai/modules/task/task_metrics.py, packages/ai/src/ai/modules/task/task_engine.py
Task metrics now ingest subprocess usage snapshots, compute billing tokens, write ledger entries, and update task engine status handling for direct status/token payloads.
Dynamic token/status schemas
apps/vscode/src/shared/types/taskStatus.ts, packages/client-python/src/rocketride/types/task.py, packages/client-typescript/src/client/types/task.ts, docs/agents/ROCKETRIDE_OBSERVABILITY.md
Token and task status schemas move to dynamic token keys, remove average utilization fields, and add event_type.

Client SDK updates

Layer / File(s) Summary
DAP types and traceable exceptions
packages/client-python/src/rocketride/__init__.py, packages/client-python/src/rocketride/core/exceptions.py, packages/client-python/src/rocketride/types/*, packages/client-typescript/src/client/exceptions/index.ts, packages/client-typescript/src/client/types/client.ts
Adds DAP request/response/event types and extends DAPException with file/line trace data.
Subscriptions and desktop apps
packages/client-python/src/rocketride/types/client.py, packages/client-python/src/rocketride/account.py, packages/client-typescript/src/client/types/client.ts, packages/client-typescript/src/client/account.ts
Connect results drop apps/credits and add subscriptions; app statuses become typed; desktop apps are fetched through getDesktop().
Client behavior and dashboard API
packages/client-python/src/rocketride/client.py, packages/client-python/src/rocketride/core/dap_client.py, packages/client-python/src/rocketride/mixins/*, packages/client-typescript/src/client/client.ts
Failures raise DAPException, identify/reconnect preserve app identity, data writes chunk at 512 KB, store paths reject Windows drive letters, and dashboards accept pagination options.
Dashboard and task schemas
packages/client-python/src/rocketride/types/dashboard.py, packages/client-typescript/src/client/types/dashboard.ts, packages/client-python/src/rocketride/types/task.py, packages/client-typescript/src/client/types/task.ts
Adds dashboard pagination types/totals and updates task schema types for dynamic tokens and event_type.

Shared-UI dashboard and token components

Layer / File(s) Summary
Dynamic token rendering
packages/shared-ui/src/components/tokens/Tokens.tsx
Token data becomes a dynamic key/value shape and renders per-key totals and bars.
Connection and billing display
packages/shared-ui/src/modules/account/components/BillingPanel.tsx, packages/shared-ui/src/modules/server/MonitorView.tsx, packages/shared-ui/src/modules/server/components/ConnectionsTab.tsx, packages/shared-ui/src/modules/server/components/OverviewTab.tsx
Billing rows show app icons, missing task/connection arrays default to empty lists, and connection displays prefer appName over client SDK names.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Shell
  participant DeepLinkGate
  participant WorkspaceProvider
  participant LoadingScreen
  User->>Shell: navigate with ?appId=
  Shell->>Shell: fetch app metadata via rrext_public_catalog
  Shell->>DeepLinkGate: render with loadingAppInfo
  DeepLinkGate->>WorkspaceProvider: watch loadedApps/appLoadErrors
  WorkspaceProvider-->>DeepLinkGate: app loaded or error
  DeepLinkGate->>LoadingScreen: show branded loading until resolved
  DeepLinkGate-->>Shell: render children
Loading
sequenceDiagram
  participant ProcessReporter
  participant Stdout as "Stdout (>MET*/>USG*)"
  participant TransportStdio
  participant TaskEngine
  participant TaskMetrics
  ProcessReporter->>Stdout: emit >MET* (live) and >USG* (cumulative)
  Stdout->>TransportStdio: parse protocol message
  TransportStdio->>TaskEngine: apaevt_status_metrics / apaevt_status_tokens
  TaskEngine->>TaskMetrics: merge_subprocess_usage(payload)
  TaskMetrics->>TaskMetrics: _update_tokens() via billing rates
Loading

Possibly related PRs

Suggested labels: module:client-python, module:vscode, module:ai, module:ui, module:client-typescript

Suggested reviewers: jmaionchi, stepmikhaylov

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately reflects the main SaaS connection multiplexing and metrics focus of the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/alb

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

packages/client-typescript/src/client/client.ts

Oops! Something went wrong! :(

ESLint: 9.39.4

TypeError: expand is not a function
at Minimatch.braceExpand (/node_modules/.pnpm/minimatch@3.1.5/node_modules/minimatch/minimatch.js:271:10)
at Minimatch.make (/node_modules/.pnpm/minimatch@3.1.5/node_modules/minimatch/minimatch.js:180:33)
at new Minimatch (/node_modules/.pnpm/minimatch@3.1.5/node_modules/minimatch/minimatch.js:156:8)
at doMatch (/node_modules/.pnpm/@eslint+config-array@0.21.2/node_modules/@eslint/config-array/dist/cjs/index.cjs:422:13)
at match (/node_modules/.pnpm/@eslint+config-array@0.21.2/node_modules/@eslint/config-array/dist/cjs/index.cjs:756:11)
at /node_modules/.pnpm/@eslint+config-array@0.21.2/node_modules/@eslint/config-array/dist/cjs/index.cjs:772:10
at Array.some ()
at pathMatches (/node_modules/.pnpm/@eslint+config-array@0.21.2/node_modules/@eslint/config-array/dist/cjs/index.cjs:767:44)
at /node_modules/.pnpm/@eslint+config-array@0.21.2/node_modules/@eslint/config-array/dist/cjs/index.cjs:1368:8
at FlatConfigArray.forEach ()

packages/client-typescript/src/client/types/dashboard.ts

Oops! Something went wrong! :(

ESLint: 9.39.4

TypeError: expand is not a function
at Minimatch.braceExpand (/node_modules/.pnpm/minimatch@3.1.5/node_modules/minimatch/minimatch.js:271:10)
at Minimatch.make (/node_modules/.pnpm/minimatch@3.1.5/node_modules/minimatch/minimatch.js:180:33)
at new Minimatch (/node_modules/.pnpm/minimatch@3.1.5/node_modules/minimatch/minimatch.js:156:8)
at doMatch (/node_modules/.pnpm/@eslint+config-array@0.21.2/node_modules/@eslint/config-array/dist/cjs/index.cjs:422:13)
at match (/node_modules/.pnpm/@eslint+config-array@0.21.2/node_modules/@eslint/config-array/dist/cjs/index.cjs:756:11)
at /node_modules/.pnpm/@eslint+config-array@0.21.2/node_modules/@eslint/config-array/dist/cjs/index.cjs:772:10
at Array.some ()
at pathMatches (/node_modules/.pnpm/@eslint+config-array@0.21.2/node_modules/@eslint/config-array/dist/cjs/index.cjs:767:44)
at /node_modules/.pnpm/@eslint+config-array@0.21.2/node_modules/@eslint/config-array/dist/cjs/index.cjs:1368:8
at FlatConfigArray.forEach ()

packages/shared-ui/src/modules/server/components/OverviewTab.tsx

Oops! Something went wrong! :(

ESLint: 9.39.4

TypeError: expand is not a function
at Minimatch.braceExpand (/node_modules/.pnpm/minimatch@3.1.5/node_modules/minimatch/minimatch.js:271:10)
at Minimatch.make (/node_modules/.pnpm/minimatch@3.1.5/node_modules/minimatch/minimatch.js:180:33)
at new Minimatch (/node_modules/.pnpm/minimatch@3.1.5/node_modules/minimatch/minimatch.js:156:8)
at doMatch (/node_modules/.pnpm/@eslint+config-array@0.21.2/node_modules/@eslint/config-array/dist/cjs/index.cjs:422:13)
at match (/node_modules/.pnpm/@eslint+config-array@0.21.2/node_modules/@eslint/config-array/dist/cjs/index.cjs:756:11)
at /node_modules/.pnpm/@eslint+config-array@0.21.2/node_modules/@eslint/config-array/dist/cjs/index.cjs:772:10
at Array.some ()
at pathMatches (/node_modules/.pnpm/@eslint+config-array@0.21.2/node_modules/@eslint/config-array/dist/cjs/index.cjs:767:44)
at /node_modules/.pnpm/@eslint+config-array@0.21.2/node_modules/@eslint/config-array/dist/cjs/index.cjs:1368:8
at FlatConfigArray.forEach ()


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.

@github-actions github-actions Bot added docs Documentation builder labels Jul 1, 2026
@github-actions github-actions Bot removed the docs Documentation label Jul 4, 2026
@Rod-Christensen Rod-Christensen changed the title Feat/alb feat(alb) Jul 4, 2026
@Rod-Christensen Rod-Christensen changed the title feat(alb) feat(alb): saas compatible connection multiplexing and metrics Jul 4, 2026
@github-actions github-actions Bot added module:client-python Python SDK and MCP client module:vscode VS Code extension module:ai AI/ML modules module:ui Chat UI and Dropper UI module:client-typescript docs Documentation and removed builder labels Jul 4, 2026
Rod-Christensen and others added 16 commits July 7, 2026 12:58
… and security hardening

Decompose the EAAS monolith into five pods (Orchestrator, Account, Balancer,
Deploy, EAAS) communicating via DAP WebSocket + Redis shared state.

Pod architecture:
- Extract StoreCommands mixin for file storage operations
- DAPConn.accept() for server-side WebSocket handling
- RequestContext on all on_* handlers: (self, request, ctx)
- DAPConn._call_method passes ctx to handlers
- TaskConn.on_receive builds ctx from _ctx args (pod) or _account_info (OSS)

Event delivery (WebSocket, not Redis pub/sub):
- _forward_task_event injects event_type, project_id, source into event body
- SSE events always sent on internal connections (no subscription churn)
- broadcast_task_event no longer publishes to Redis

Security hardening:
- Centralized verify_task_access(control, ctx, require) on TaskServer
- Pure lookups: get_task_control_by_token, get_task_control_by_project
- resolve_task_permissions returns full permissions for internal/sys.admin
- Fixed cmd_cprofile: was not verifying team ownership of target task
- All get_task() callsites now pass ctx

Client SDK (Python + TypeScript):
- DAPRequest/DAPResponse/DAPEvent TypedDicts
- event_type field on TASK_STATUS
- connect(): _desired_state set to 'authenticated' before attach()
- attach(): no longer downgrades _desired_state
- Python: fixed _debug_message → debug_message in reconnect

Task engine:
- Zero metrics after stop_monitoring() audit but before final status broadcast

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rop, and download

Introduces the explorer-ui micro-frontend — a multi-tab file browser and viewer
for the RocketRide server store. Previously, files stored on the server could
only be accessed programmatically via the SDK. This gives users a visual way to
browse, preview, upload, download, and manage their store files directly from
the RocketRide web UI.

Three content modes determine how each file type is loaded and displayed:
- `inline` — text read as a string (plain text, markdown, JSON)
- `link` — presigned URL for native browser streaming with Range/seek (video, audio)
- `blob` — data fetched via presigned URL, served as a local blob: URL so
  cross-origin content works in iframes and img elements (images, PDF, docx, xlsx)

Each file type has a self-contained viewer in its own file:
- VideoViewer / AudioViewer — call fsGetUrl directly for streaming URLs
- ImageViewer / PdfViewer — render from blob URLs (no CORS issues)
- DocxViewer — Word document rendering via docx-preview library
- SpreadsheetViewer — Excel/CSV rendering via SheetJS (xlsx) library
- MarkdownViewer — shared MarkdownRenderer with GFM, syntax highlighting
- JsonViewer — pretty-printed with syntax highlighting via MarkdownRenderer
- TextViewer — editable textarea with monospace font
- BinaryViewer — fallback for unsupported types

- Drag files/folders within the tree to move them between directories
- Drop files from the OS to upload (chunked 4MB writes via fsOpen/fsWrite/fsClose)
- Visual drop-target highlighting (brand-colored outline) on directories
- Fully backward-compatible: new optional onMove/onUpload props on IExplorerProps;
  existing hosts (VS Code, pipe-builder) that don't pass them are unaffected

- Download menu item with BxDownload icon in the file kebab menu
- Injected via the existing fileActions prop so it only appears in explorer-ui
- Uses fsGetUrl to get a presigned URL, triggers browser download via <a> element

- New DAP command `fs_geturl` returns a time-limited URL for accessing a store file
- S3 backend: presigned URL via generate_presigned_url
- Azure backend: SAS token URL
- Local filesystem backend: JWT-signed URL pointing at a new /task/fetch endpoint
- RR_SIGNING_KEY env var added to .env.template for the JWT signing secret
- Python SDK: new fs_get_url method on the store mixin
- TypeScript SDK: new fsGetUrl method on RocketRideClient

- Viewers are self-contained components in a viewers/ subdirectory, keeping
  ExplorerApp.tsx as a thin dispatcher (~30 lines in FilePane)
- Video/audio viewers own their URL lifecycle (call fsGetUrl directly) so they
  get native browser streaming with Range request support for seeking
- Blob-mode files (images, PDF, docx, xlsx) are fetched by the VFS store layer
  and served as local blob: URLs to avoid cross-origin restrictions in iframes
- The shared Explorer component gains drag-drop via optional props, preserving
  backward compatibility with all existing hosts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add Monaco code editor with theme-aware syntax highlighting for 80+
file types, a streaming hex viewer with range-based fetching for large
files, and an "Open with…" submenu on the file context menu to switch
between compatible viewers per file type.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The fs_geturl subcommand was added to TaskCommands but the test's
expected command set was not updated, causing CI to fail.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…flict

The <47 ceiling on cryptography fought with the 49.x installed by the
base build, making uv race to downgrade a locked .pyd on Windows CI.
Drop the ceiling, keep the >=46.0.7 security floor, and relax exact
pins in node-level requirements to >= so they no longer conflict.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Security:
- Remove hardcoded RR_SIGNING_KEY default from .config
- Validate and clamp expires_in (max 1h) in file_store.get_url()
- Fail fast when RR_BASE_URL is unset instead of falling back to localhost
- Defer RR_BASE_URL when port=0 until the real port is resolved
- Check fetch response.ok before creating blob URLs in store.ts
- Only cache HTTP 206 responses in HexViewer range fetcher
- Reject absolute paths in TS SDK validateStorePath (match Python SDK)

Stability:
- Reset viewer state (url/error/html) on file switch in Audio, Video,
  Docx, Spreadsheet, and Hex viewers to prevent stale content flashing
- Handle empty files in HexViewer instead of permanent loading spinner
- Close upload handles on failure via try/finally in ExplorerSidebar
- Ref-count shared blob URLs so closing one pane doesn't revoke another's
- Guard against moving a directory into itself in Explorer handleDrop

Quality:
- Remove unused vfs state/effect and createStoreVfs import in sidebar
- Add binary category mappings for common binary extensions in mediaTypes
- Use registry default (HexViewer) for binary files instead of hardcoded
- Add wrap="off" to TextViewer textarea for horizontal scrolling
- Make "Open with…" submenu keyboard-accessible (focus/blur, aria attrs)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Transitive deps (mcp, huggingface_hub) pull cryptography 49.x during
server:setup-pip. Later, nodes/requirements.txt (>=46.0.7,<47) forces
a downgrade — which fails on Windows when the .pyd is locked by the
running engine. Fix by installing cryptography<47 upfront so no
downgrade is needed. Restore the <47 ceiling in node requirements
and bump state key to V9 to force re-install.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Problem: verify_auth() checked for a Zitadel `sub` field on AccountInfo
that was never populated by any authentication path. This caused "Zitadel
authentication required" errors for ALL credential types — rr_* API keys,
PKCE, and access tokens — on every SaaS handler (16 callsites across
app_handler, account_handler, saas_handler, billing_rates_handler).

Root cause: The `sub` claim (Zitadel subject) is used at authentication
time to look up the user in the database, then discarded. AccountInfo
stores `userId`, not `sub`. The check was testing for a field that never
existed on the model.

Fix (verify_auth): Check `userId` instead of `sub`. Every auth path
(rr_* key lookup, PKCE code exchange, access token validation) populates
userId. Task-scoped credentials (pk_*) intentionally do NOT get userId
so verify_auth correctly rejects them from user-scoped handlers like
desktop_add, billing, org management, etc.

Fix (pk_* credential scope): Previously, pk_* connections received
userId, userToken, and org context from the task owner — giving them
far more identity than a public data pipe credential should have. Now
pk_* gets only AccountInfo(auth=credential) with no userId, no
userToken, no org. This means:
- verify_auth() rejects pk_* from all user-scoped handlers
- resolve_task_permissions() returns ['task.data'] based on the auth
  prefix, which is correct for data pipe operations
- tk_* (private task token) retains full user context as before

Fix (set_monitor permissions): Added _verify_monitor_permissions() that
checks specific permissions for the requested EVENT_TYPE bitmask before
accepting the subscription. Previously set_monitor only checked if perms
was non-empty (truthy), so a pk_* user with only ['task.data'] could
subscribe to SUMMARY, FLOW, etc. — the events would be silently dropped
at delivery time but the subscription was wastefully accepted. Now:
- Non-SSE event types (SUMMARY, FLOW, DETAIL, etc.) require task.monitor
- SSE event type requires task.data
This mirrors the delivery-time check in send_task_event but rejects
upfront with a clear PermissionError instead of silently discarding.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…polling

The EAAS parent previously ran a _metrics_monitor_loop that polled each
task subprocess externally via psutil for CPU%, RSS, and GPU VRAM. At
~100ms per psutil call, this takes 100+ seconds per cycle for 1000 tasks
-- completely unusable at scale.

This refactor moves all metrics collection INTO the subprocess:

Protocol changes:
- >MET* repurposed for live dashboard metrics (cpu_percent, cpu_memory_mb,
  gpu_memory_mb) emitted every ~1s by a new ProcessReporter daemon thread
- >USG* new protocol for cumulative billing values (flat dict keyed to
  metrics_conversions DB rate names), replaces old >MET* billing role

Subprocess side:
- New ProcessReporter (process_reporter.py): daemon thread samples
  getrusage (Linux) or psutil (Windows fallback) for CPU, /proc/self/statm
  for RSS, pynvml for GPU VRAM. Emits >MET* for dashboard, accumulates
  cpu_compute (CPU-ms) and cpu_memory (GB-sec) for billing via >USG*
- MetricsManager: add_time renamed to add_value, new set_value for
  absolute cumulative values, report() merges timers+counters into flat dict
- taskhook.py: wires ProcessReporter start/stop, emits >USG* instead of >MET*

Parent side:
- transport_stdio.py: >MET* -> apaevt_status_metrics, >USG* -> apaevt_status_tokens
- task_engine.py: handles both events, updates _status.metrics from >MET*
- task_metrics.py: psutil removed entirely, _update_tokens() is single
  generic loop matching subprocess values against DB rates, billing gate
  uses baseline snapshots at set_service_up(True)
- task_server.py: _metrics_monitor_loop replaced with _billing_report_loop

Data model:
- TASK_TOKENS: Pydantic BaseModel with extra='allow', dynamic keys matching
  DB rate names. Client computes total.
- TASK_METRICS: field names unchanged for backward compat
- Tokens.tsx: renders dynamic keys with formatLabel (gpu_compute -> "GPU Compute")

Performance: parent goes from 100s/cycle to <1ms/cycle for 1000 tasks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New Module Federation app for comprehensive server stress testing via
the TypeScript client SDK. Found and fixed multiple server bugs during
development (identify, deploy.list, dashboard on orchestrator).

Architecture:
- Multi-tab interface with independent test sessions per tab
- Per-tab state (config, phases, view) persisted to editor viewState
- All editors stay mounted (display:none pattern like rocket-ui) so
  running tests survive tab switches
- Sidebar nav uses request/confirm pattern: sidebar requests view change,
  active tab applies and confirms. Prevents cross-tab state corruption.
- Actions (launch/abort/pause/clear/reset) keyed by editorId so sidebar
  dispatches to the correct tab's engine

Test engine (engine.ts):
- 6 phases: API sweep, multi-socket stress, backpressure flood, pipe
  streaming, chaos injection, concurrent API hammer
- API sweep now tests 15+ previously-skipped methods via SweepContext
  (pipeline token, log name, service name passed between method calls)
- Pure timing monitor layer (monitor.ts): begin/end only, no outcome
  interpretation. Caller decides pass/fail via apiMonitor.recordError()

Dashboard panels:
- MetricsPanel: live passed/failed/ops/ping with color thresholds
- PhaseProgress: live phase status on dashboard (separate from settings)
- SwarmPanel: pipeline grid visualization
- LatencyPanel: log-scale sparkline chart
- ApiCoveragePanel: per-method stats table
- SettingsPanel: config + phase selector + reset (no action buttons)
- PhaseList: pure checkboxes for settings (no status indicators)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Client SDK improvements found during test-ui stress testing:

- DAPException now extends RuntimeError (Python) for backward compat,
  includes file/lineno properties extracted from server-side trace
- Python client.call() and TS client.call() throw DAPException instead
  of generic RuntimeError/Error, preserving server stack trace info
- Python dap_client: None guards on _transport for disconnect(), _send(),
  request(), connect() to prevent NoneType errors during connection churn
- TS DAPException: file/lineno properties extracted from dapResult.trace

These fixes prevent crashes during rapid connect/disconnect cycling and
provide better error diagnostics (server file:line in error messages).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Changes found during test-ui stress testing:

- ai/__init__.py: PYTHONASYNCIODEBUG=1 and asyncio logger handler for
  detecting event loop blocking during concurrent pipeline operations
- eaas.py: loop.set_debug(True) and slow_callback_duration=0.1 to flag
  callbacks taking >100ms (identified psutil as the main offender)
- account/oss/__init__.py: handle_account accepts ctx=None to match
  the DAP handler signature contract
- data_conn.py: ctx parameter fixes for data connection handlers

These are diagnostic aids that helped identify the metrics polling
bottleneck (100ms per psutil call * N tasks blocking the event loop).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…hboard

Move desktop apps out of AccountInfo/ConnectResult into a dedicated
`rrext_account_me?subcommand=desktop` endpoint. This removes the
biggest payload from the auth handshake (apps+credits), cutting
ConnectResult size and eliminating the query cascade that slowed login.

Shell-ui changes:
- AppRegistryProvider: single source of truth for registered apps,
  grows incrementally from probe -> desktop fetch -> catalog browsing
- DeepLinkGate: branded loading screen with app icon for ?appId= deep
  links, prevents home-ui flash while target MF remote loads
- LoadingScreen: two modes (rocket mark vs app-branded spinner)
- WorkspaceContext: reads from AppRegistry instead of ConnectResult.apps
- useSubscriptions: derives desktop state from AppRegistry, not identity
- appLoader: spread server entry to preserve all manifest fields
- bootstrap: registers only the home app from probe, not full catalog
- connection.ts: routes apaext_desktop events to shell:desktopUpdate

Server changes:
- AccountInfo.to_pod_context(): slim 6-field dict for inter-pod _ctx
- Probe returns only home app entry (not full public catalog)
- Store eagerly created at startup instead of lazy first-access
- store_providers/__init__.py: lazy per-backend imports to avoid
  unnecessary pip dependency checks
- cmd_monitor: adds completedTasks/totalTasks/eaasNodes to overview
- Dashboard (orchestrator): full pagination, sorting, state filtering,
  cluster-wide Redis connection listing
- Orchestrator connection: _pod_context built at auth for _forward(),
  _forward_public() for unauthenticated commands
- TaskStartedEvent.state typed as int (matches TASK_STATE enum values)

SDK changes:
- Python/TS: add account.get_desktop() method
- Remove apps/credits from ConnectResult and ServerInfoResult types
- TS dashboard types: add pagination params and enriched response shape

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The dashboard's connection display was broken in several ways:
- identify() overwrote _client_info['name'] (SDK identity) with the
  app-level name, so you could never see both simultaneously
- clientInfo was always {} on the orchestrator because on_auth never
  captured clientName/clientVersion from the auth request arguments
- messagesIn/Out, monitors, attachedTasks, and lastActivity were
  hardcoded to 0/empty on orchestrator connections (Redis had no data)
- The UI fell back to showing raw UUIDs (clientId) when names were empty
- The ping heartbeat in test-ui used a raw WebSocket via Web Worker
  that silently died and was never identified in the dashboard

Changes:

Server (OSS + orchestrator):
- Add _app_name field (mutable, set by rrext_identify) separate from
  _client_info (immutable, set at auth time from clientName/clientVersion)
- rrext_identify now accepts appName (preferred) or clientName (legacy
  back-compat) and writes to _app_name instead of _client_info
- OSS dashboard response includes appName field
- Orchestrator on_auth captures clientName/clientVersion into _client_info
- Orchestrator adds send()/on_receive() overrides to track messagesIn/Out,
  lastActivity (mirrors TaskConn pattern)
- Orchestrator _register_connection writes app_name to Redis
- Orchestrator heartbeat flush (_refresh_connection_ttls) writes dynamic
  stats (counters, monitors, subscribed_keys, client_info) alongside TTL
- Orchestrator _load_connections_from_redis reads all dynamic fields back,
  resolves monitor labels and attachedTasks via O(subscriptions) task_table
  lookups instead of O(tasks) full scan

Client SDKs (TypeScript + Python):
- identify() renamed param to appName, stores locally in _appName/_app_name
- identify() sends both appName and clientName for back-compat with older
  servers that only read clientName
- Re-sends identify automatically on reconnect after _resubscribeAllMonitors
- DashboardConnection type includes optional appName field
- DashboardConnectionUpdated event type added

Shell UI:
- WorkspaceContext passes app registry to useWorkspaceState for name lookup
- useWorkspaceState sends identify with display name on initial load (F5)
  and on app switch, waiting for registry to populate before sending
- Fallback chain in OverviewTab and ConnectionsTab: appName -> clientInfo
  -> Conn #N (removed clientId/UUID from chain)
- ConnectionDetailModal shows "Application" and "Client SDK" as separate
  rows

Test UI:
- Engine createClient/createPool identify as "Test - {label}"
- Ping heartbeat rewritten from raw WebSocket Web Worker to plain
  RocketRideClient — simpler, automatically identified as "Test - ping",
  and no silent death from Worker loading failures
- Removed hardcoded identify('test-ui') — shell workspace handles it

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The File Explorer store backend (fs_geturl, presigned/SAS URLs, the
/task/fetch endpoint) exists on both feat/alb and develop because develop
received it via the squash-merged File Explorer PR (#1356), independent of
feat/alb's original explorer commits. develop's copy is the more complete
one: it carries the browser-compat / cross-site download work that landed
during that PR's review, which never flowed back to feat/alb. Resolving the
merge conflicts naively toward feat/alb would have regressed that work.

Defer to develop's hardened version (feat/alb had nothing newer here):
- account/file_store.py, store.py, store_providers/s3, store_providers/azure:
  restore download_name + Content-Disposition: attachment support, filename
  sanitisation, S3 ResponseContentDisposition, Azure blob-client-derived base
  URL (custom/sovereign/Azurite endpoints), so download filenames survive
  cross-origin cloud URLs where the <a download> hint is ignored
- modules/task/fetch.py: JWT exp enforcement + malformed-path -> 400
- client-python mixins/store.py: fs_get_url(download_name=...) parameter

Hand-merge where feat/alb's architecture and develop's feature both matter:
- commands/cmd_store.py: keep feat/alb's RequestContext threading on every
  fs_* handler (on_rrext_store(request, ctx), _get_file_store(ctx),
  verify_permission('task.store', ctx)) AND graft develop's download_name
  into _store_fs_geturl
- client-typescript client.ts: keep feat/alb's 512 KB write auto-chunking,
  appName reconnect, and DAPException; adopt develop's
  fsGetUrl(path, expiresIn, downloadName?) signature

task_conn.py needed no change: it already carried develop's StoreCommands
registration; only the import line differed and it keeps feat/alb's ctx form.

Also syncs pnpm-lock.yaml to add dompurify, which the explorer-ui package.json
(brought to develop's version in the previous commit) now declares.

Verified: all touched Python compiles and passes ruff; the call chain stays
consistent (_store_fs_geturl -> _get_file_store(ctx).get_url(path, expires_in,
download_name=...) matches file_store.get_url's signature).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rewrites

The feat/alb source changes (connection-stats enrichment, appName/clientInfo
split, app/auth decoupling, paginated dashboard, desktop endpoint) reshaped
the DAP command layer and several modules, but the test suite was not updated
alongside them. `builder ai:test` failed with 119 failures + 20 errors against
1171 passing. These changes bring the tests back in line with current source
behavior. No source files were touched — the source is the intended behavior;
the tests were lagging.

WHY each group changed:

- ctx threading (bulk of the failures): every DAP handler now receives a
  per-request `RequestContext` (`ai.account.models.RequestContext`) carrying
  caller identity, and reads `ctx.account_info` instead of `self._account_info`.
  Tests called handlers with the old signature, so they raised
  "missing 1 required positional argument: 'ctx'". Added a `_ctx()` helper per
  file and pass a ctx built from the same account each test sets up. Affects
  test_task_conn, test_cmd_task/public/debug/account_app/misc/monitor,
  test_account_helpers, test_dap_conn.

- moved permission helpers: `resolve_team_permissions` / `resolve_task_permissions`
  moved to `ai.account.models`. Tests patched them on `task_conn`, which no longer
  defines them (AttributeError). Retargeted patches to the module where each name
  is now looked up (dap_conn / cmd_task / cmd_monitor / task_server).

- task-access moved to the server: team-membership checks migrated from `task_conn`
  into `TaskServer.verify_task_access`, and task lookup is via
  `get_task_control_by_token`. Rewrote the affected get_task / request / monitor /
  get_token tests to drive access through the server instead of patching the old
  in-conn permission resolution.

- dashboard/monitor helpers relocated: `_resolve_monitor_label`,
  `_build_monitors_list`, and `on_rrext_dashboard` moved from `MiscCommands` into
  `MonitorCommands` (cmd_monitor). The `time` patch moved with them. Retargeted
  the cmd_misc tests to MonitorCommands / cmd_monitor.

- TaskServer surface changes: `push_account_update` was renamed to
  `broadcast_account_update`; `broadcast_task_event` now dispatches sends
  fire-and-forget via `asyncio.create_task` (so tests await a scheduler tick
  before asserting); the `store` property is eager, not lazy; and
  `_build_task_account_info` returns a bare AccountInfo for `pk_*` tokens (org
  context only for `tk_*`). Tests updated to match.

- web/metrics rewrite: `MetricsManager.report()` now returns a flat
  `{'values', 'events'}` shape (timers+counters merged) and `add_time` was
  replaced by `add_value(dict)`. Rewrote the metrics tests to the new API.
  Likewise the stdio transport `met` message now carries a flattened metrics
  body rather than `{'metrics': {...}}`.

- task_metrics full rewrite: TaskMetrics no longer samples CPU/memory/GPU via
  psutil/pynvml (that responsibility moved to the task engine's `>MET*` path);
  the class is now billing-only. The 20 tests that patched module-level `psutil`
  and exercised the removed sampling/monitoring loop errored at setup. Deleted
  those and wrote new tests covering the current API: subprocess-usage merge,
  DB-rate token conversion, billing gate + baselines, and billing-report state.

Result: builder ai:test -> 1313 passed, 122 skipped, 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rod-Christensen and others added 14 commits July 7, 2026 12:58
Removes apps/test-ui from feat/alb. The stress/chaos testing app was added
here alongside unrelated dashboard/decoupling work, inflating this PR by ~7.8k
lines / 36 files. It is a self-contained leaf app (nothing imports it) and does
not depend on any feat/alb-only source changes — every symbol it consumes from
shell-ui and the rocketride SDK already exists on develop — so it belongs on its
own branch off develop rather than riding this PR.

Also drops the 'apps/test-ui' entry from pnpm-workspace.yaml and regenerates
pnpm-lock.yaml so the workspace no longer references the removed package.

The app is re-added on branch feat/test-ui (off develop) in a separate PR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The asyncio debug aids that surfaced the psutil sampling bottleneck (now
fixed by the metrics overhaul) were still active in two places and would
ship to production:

- ai/__init__.py: set PYTHONASYNCIODEBUG=1 at import time (affects every
  process that imports the ai package) plus an [ASYNCIO-DEBUG] stderr logger
  and a print() proof-of-run line.
- ai/eaas.py: called loop.set_debug(True) with slow_callback_duration=0.1
  on every SaaS server start.

asyncio debug mode adds real per-callback overhead to every async operation,
so it must not be on by default. Both blocks are commented out rather than
deleted so the instrumentation stays one uncomment away when needed, and each
block now carries a NOTE that BOTH must be uncommented together for asyncio
debug mode to take full effect (the env var enables coroutine-level checks;
loop.set_debug enables the running-loop checks + slow-callback logging).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…+ periodic >USG)

WS6.1 + billing-gap fix. Two related billing defects in the subprocess
self-reporting metrics:

1. Warmup billed. task_metrics.set_service_up() snapshotted the billing
   baseline from _subprocess_usage at the service-up instant, but >USG* is only
   emitted at object boundaries (after service-up), so that snapshot was empty
   and the entire model-load/warmup window got billed on the first >USG*.

2. Idle pipelines never billed. >USG* was emitted ONLY at object boundaries and
   task end, so a pipeline that is up but idle (e.g. a chat holding models in
   GPU/RAM with no traffic) accrued real resource cost but emitted no >USG* —
   it was billed only when a message finally passed through, or not at all if it
   received none. With fix (1)'s deferred baseline, a no-traffic chat would even
   bill zero (its only >USG*, at task end, became the baseline).

Fixes (parent + subprocess, no engine change):
- task_metrics: capture the baseline as a "startup delta" at/after service-up —
  snapshot current usage on service-up and, if empty, defer to the FIRST >USG*
  received after service-up (that first cumulative carries all of startup, so it
  nets to zero; only later growth bills).
- process_reporter: emit a cumulative >USG* on the billing cadence
  (CONST_BILLING_REPORT_INTERVAL), not just at object boundaries. Idle GPU/RAM
  reservation is now billed, and the deferred baseline lands at ~service-up
  (warmup only) instead of at the first object.

Together: warmup excluded, idle reservation charged, first post-service-up
snapshot is the startup delta.

Tests: added test_baseline_deferred_to_first_usg_after_service_up and a new
test_process_reporter.py covering the periodic-emit interval gate. ai:test 1316
passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The deferred-baseline comments in task_metrics still claimed >USG* is only
emitted at object boundaries, but ProcessReporter now also emits it on the
billing cadence. Reword: at the service-up instant the first >USG* (periodic
or object-boundary) may simply not have arrived yet, which is why the baseline
capture is deferred when _subprocess_usage is still empty. Comment-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
WS6.2. avg_cpu_percent / avg_cpu_memory_mb / avg_gpu_memory_mb were computed by
the parent-side psutil monitoring loop that the metrics overhaul deleted. Since
then nothing computes them — task_engine only ever wrote them to 0 on task
completion — and no UI consumes them (the status charts read the live/peak
fields). Remove the dead surface:

- task_engine: drop the avg_* zeroing block (keep current + peak).
- client-python TASK_METRICS and client-typescript TaskMetrics: drop the fields.
- vscode taskStatus.ts: drop the fields.
- ROCKETRIDE_OBSERVABILITY.md: drop the avg_* line from the metrics shape.

The engine-side status model never statically declared them (they were only
ever set dynamically to 0), so no engine change is needed. ai:test 1316 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…, guard _ctx

WS3 — three permission-model correctness fixes exposed by the ctx refactor:

- WS3.1: resolve_team_permissions() lacked the sys.admin/internal full-access
  bypass that resolve_task_permissions() has, and raises on no membership. So a
  sys.admin or pod 'internal' caller with no team membership was DENIED on
  team-scoped commands (on_execute, on_rrext_get_tasks, on_rrext_dashboard) via
  has_permission/verify_permission, while task-scoped access was granted — the
  two surfaces disagreed. Add the same bypass before the membership check.

- WS3.2: AccountInfo.to_pod_context() omitted `waitlisted`, so a forwarded pod
  ctx deserialized it to False and verify_auth() would serve a waitlisted user
  in pod mode. Forward `waitlisted`.

- WS3.3: TaskConn.on_receive built the pod ctx via AccountInfo(**raw_ctx[
  'account_info']), which raises KeyError (escaping on_receive and silently
  dropping the message) if a truthy _ctx lacks account_info. Use .get() and
  treat a missing account_info as unauthenticated (account_info=None).

Tests: added resolve_team_permissions bypass/raise cases and a to_pod_context
waitlisted case to test_account_helpers. ai:test 1320 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dropped events)

WS2. broadcast_task_event spawned an unretained asyncio.create_task(_send())
per connection, which had two bugs: (1) per-connection ordering was lost —
event N+1's task could run before N's, corrupting SSE stream reconstruction;
(2) asyncio keeps only weak refs to bare tasks, so a task could be
garbage-collected before running, silently dropping the event. High-rate SSE
flows through this path.

Fix — one bounded outbound queue + drain task per connection (MonitorCommands):
- broadcast_task_event now calls conn.enqueue_task_event() (O(1), non-blocking)
  instead of sending inline, so a slow WebSocket never head-of-line-blocks the
  parent event loop.
- A single drain task per connection delivers in FIFO order via send_task_event
  (which applies the subscription/permission filter, so non-subscribers drain
  instantly and only a genuinely slow subscriber backs up) -> ordering preserved,
  no GC-drop (the task is owned by the connection).
- Overflow policy on a full queue (CONST_CONN_OUT_QUEUE_MAX=1000): SSE streams
  disconnect the slow consumer (dropping chunks would corrupt the stream);
  idempotent snapshots (status/summary/dashboard) drop-oldest / keep-latest.
- Lifecycle: drain lazily starts on first enqueue (covers every registration
  path incl. the pod/orchestrator conn) and is cancelled in
  _dapbase_on_disconnected.

Error/permission isolation moved from broadcast into the drain. Tests updated:
broadcast now asserts enqueue-per-connection; added drain ordering, permission
skip, error-survival, and both overflow policies. ai:test 1323 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
WS4. on_rrext_dashboard advertised a paginated contract (both SDKs send
{tasks,connections}: {offset,limit,sort_by,sort_order,state_filter}) but the
handler ignored request arguments entirely — it returned ALL accessible tasks
and connections and set tasks_total/connections_total to the returned length,
so pagination/sort/filter were client-only wire dressing.

Server (cmd_monitor.on_rrext_dashboard):
- Parse per-section params from request arguments.
- tasks: apply state_filter (running vs completed), then sort + slice.
- connections: sort + slice.
- *_total = pre-slice count matching the (state-)filtered set (for the UI).
- A section with limit=0 is omitted from the body; absent limit returns all.
- overview stays a GLOBAL snapshot over all accessible tasks (computed before
  the filter/pagination), so activeTasks/completedTasks/totalTasks are unchanged.
- New _sort_key (None-safe, mixed-type) + _paginate helpers.

Python SDK (parity with the TS SDK, which already had the contract):
- DASHBOARD_OVERVIEW gains completedTasks / totalTasks / eaasNodes.
- New DASHBOARD_PAGE_PARAMS + DASHBOARD_REQUEST; DASHBOARD_RESPONSE made
  total=False with tasks_total / connections_total; exported from types.
- get_dashboard(tasks=None, connections=None) forwards the sections (empty ->
  unpaginated, unchanged behaviour).

Tests: added offset/limit, sort-desc, state_filter (+ global-overview), and
limit=0-omits cases. ai:test 1327 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "Event Types Supported" section listed PASSIVE / ACTIVE / DEBUG, none of
which are real EVENT_TYPE members — the code filters on SUMMARY / DETAIL / TASK
/ SSE / FLOW / OUTPUT / DEBUGGER / DASHBOARD / BILLING (+ NONE/ALL). Replace the
stale conceptual grouping with the actual bitmask flags. Comment-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…te (WS1 core)

WS1 (part 1 — server + SDKs + gate). The PR removed ConnectResult.apps, but the
vscode subscriptionGate still read info.apps[].appStatus -> always undefined on
SaaS -> isSubscribed() returned false for every app -> run and deploy were
paywalled for genuinely subscribed users. It also used the desktop apps as the
source, which is wrong: appStatus and onDesktop are independent, so a
subscribed-but-off-desktop app would be denied.

Fix: carry a slim per-app subscription status on AccountInfo (the full
entitlement, not the desktop subset) and gate on it.

- models.py: add AccountInfo.subscriptions {appId -> AppStatus}. It flows to
  ConnectResult via to_connect_result() (model_dump) and is re-sent on every
  apaext_account push, so clients need no new command / cache / invalidation.
  Full billing detail (plan/price/seats/credits) still comes from
  rrext_account_billing; this map is status-only.
- OSS (account/oss): mark every catalog app 'free' at auth (an active status),
  and add the 'desktop' subcommand (all apps, onDesktop=true) so OSS mirrors the
  SaaS desktop. OSS therefore never paywalls.
- SDK types: subscriptions on ConnectResult (Python dict[str,str]; TS
  Record<string, AppStatus>). New shared AppStatus type + ACTIVE_APP_STATUSES +
  isActiveStatus() in the TS SDK (active = subscribed | trialing | free);
  AppManifestEntry.appStatus typed as AppStatus.
- Gate (vscode subscriptionGate): keep the client/info/capabilities('saas')
  short-circuit (OSS never gated), then isActiveStatus(info.subscriptions?.[appId]).

SaaS populates AccountInfo.subscriptions in the proprietary account layer (not
in this repo). Tests: OSS authenticate marks apps 'free'; OSS desktop subcommand
returns all apps free+onDesktop. ai:test 1329 passed; client-typescript builds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lt.apps

WS1 display half — completes the app-source migration after ConnectResult.apps
and ServerInfoResult.apps were removed. Consumers now read from the authoritative
sources instead of the deleted embedded manifest.

shell-ui:
- useSubscriptions.getStatus/isSubscribed read AccountInfo.subscriptions (the
  slim appId -> AppStatus map on the cached ConnectResult, reactive on
  apaext_account) instead of the desktop-derived registry appStatus. Subscription
  and desktop membership are independent (an app can be subscribed but off-desktop),
  so the registry is the wrong source for entitlement. The registry keeps only
  desktop membership (onDesktop) + the MF entry.
- AppRegistryContext.mergeApps splits full vs lean incoming entries: full entries
  (with an MF entry URL) register as remotes as before; lean entries (an
  apaext_desktop membership push carrying only id + onDesktop) now patch onDesktop
  onto the EXISTING registry row by id, instead of being silently dropped by
  registerAndMapApps's entry filter. New registrations still require entry.
- SettingsPage reads the subscribed decision from useSubscriptions
  (AccountInfo.subscriptions) and pulls the pipeBuilder app entry (for the
  shell:subscribe checkout) from the app registry, replacing the removed
  getAccountInfo().apps lookup.

vscode:
- AccountProvider.sendInitialData fetches account.getDesktop() and sends a minimal
  {id, name, icon}[] appLabels list on account:init, replacing the removed
  authUser.apps/profile.apps. AccountWebview stores it and passes it as BillingPanel's
  apps prop (used only to resolve appId -> display name/icon on billing rows; the
  subscription/plan/price data comes from the separate BillingDetail fetch and is
  unaffected).

shared-ui:
- BillingPanel now renders the per-app icon (previously declared but unused) next
  to the app name on each subscription row.

Verified: shell-ui:build and vscode:build both compile clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…up, debug scaffolding)

WS7.1 — store-path validation misses Windows drive-letter absolutes.
validateStorePath (TS) and _validate_relative_path (Python) rejected leading
"/" and "\\" but not "C:\..." / "C:/..." — those don't start with a slash and
":" is a legal POSIX path char, so a drive-letter absolute slipped through to
the server. Both SDKs now reject a leading drive-letter (kept in sync per the
cross-SDK rule).

WS7.2 — TaskConn.on_receive duplicated ~40 lines of DAPConn.on_receive.
Factored the dispatch loop in DAPConn into three overridable hooks — _track_message
(per-message bookkeeping), _pre_dispatch_gate (returns False to reject), and
_build_ctx (per-request RequestContext) — with behavior-preserving base
implementations (no-op / allow-all / connection-state ctx). TaskConn now overrides
only those three hooks instead of copy-pasting the whole method: _track_message
bumps the inbound counters, _pre_dispatch_gate enforces the auth gate, _build_ctx
extracts the forwarded _ctx (pod mode) or falls back to connection state. Identical
observable behavior; the message-counter, auth gate, and pod-ctx KeyError guard are
all preserved.

WS7.3 — removed debug scaffolding. Deleted the [TIMING] perf-counter blocks
sprinkled through task_engine.start_task and task_server (and their _t/_t_total
locals), and the [on_rrext_monitor]/[set_monitor] argument/permission dumps in
cmd_monitor. These were temporary instrumentation, not diagnostics worth keeping.

WS7.5 — dropped the dead TaskMetrics.pid parameter. It was stored but never read;
removed the constructor arg, the assignment, the docstring line, the task_engine
call-site kwarg, and the two test references.

WS7.4 (shared live/billing key-name map) intentionally skipped: the premise doesn't
hold. The "live" keys (cpu_memory_mb = an RSS snapshot in MB) and "billing" keys
(cpu_memory = cumulative GB-seconds) are different quantities, not two names for one
metric, and the billing keys are driven by the metrics_conversions DB table rather
than a fixed enum — so there is no real one-to-one map to centralize. Adding one
would misrepresent the relationship.

Verified: ai:test 1329 passed; ruff clean; client-typescript builds.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
WS5.1 — deep-link loading gate could spin forever. DeepLinkGate only released
when loadedApps[sessionAppId] became truthy, so a deep-linked app that 404s,
times out, or isn't in the catalog left the branded spinner up permanently and
ShellLayout's error + Retry UI never rendered. The gate now releases on any
terminal condition: target descriptor loaded, target load errored
(appLoadErrors[sessionAppId]), the shell fell back to the default app
(activeAppId === defaultAppId), or a 15s safety timeout elapsed.

WS5.2 — desktopFetchRef race. refreshDesktop nulled desktopFetchRef.current
unconditionally after its await, so under concurrent fetches the first
completion cleared a ref still pointing at a second, in-flight promise —
ensureApp would then skip the wait and miss apps still arriving. Now
refreshDesktop dedupes (returns the in-flight promise if one exists) and only
nulls the ref when its own promise is still the current one (finally + identity
check).

WS5.3 — mount double-fetch. The provider's mount effect fired refreshDesktop
when already authenticated, and the shell:login listener fired it too — a login
racing mount (or a StrictMode double-mount) double-fetched the desktop. Guarded
the mount-time fetch with initialFetchDoneRef so the initial fetch runs once;
the dedupe (WS5.2) covers the concurrent case and shell:login still refreshes on
genuine future logins.

Verified: shell-ui:build compiles clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The feat/alb history was groomed (apps/test-ui and apps/explorer-ui filtered
out; develop owns them) and rebased onto develop with -X theirs, which
auto-resolves conflicting hunks in favor of the replayed commits. Three
develop-side changes were overridden in the process and are restored here,
byte-identical to the verified develop+feat/alb merge tree
(alb-develop-merge-backup):

- apps/vscode/src/providers/views/types.ts: re-add the
  checkout:validatePromoResult / checkout:redeemPromoResult members to
  AccountHostToWebview (develop's promo flow) alongside ALB's authUser /
  appLabels fields.
- .config: restore develop's RR_SIGNING_KEY guidance text (landed with the
  explorer PR).
- pnpm-lock.yaml: restore develop-side lockfile entries.

After this commit the tree is identical to the known-good merge result.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Rod-Christensen
Rod-Christensen marked this pull request as ready for review July 7, 2026 20:10

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 22

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
packages/client-python/src/rocketride/core/exceptions.py (1)

67-85: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Docstring example is now stale after adding file/lineno.

The class docstring still teaches e.dap_result['file'] / e.dap_result['line'], but the new trace-extraction logic stores these on e.file / e.lineno (and the actual server key is lineno, not line, nested under trace). The Attributes block also doesn't document the new file/lineno attributes.

📝 Proposed docstring fix
     Attributes:
         dap_result: Complete DAP result dictionary containing detailed error
                    information and context from the server
+        file: Server-side source file where the error originated, if available.
+        lineno: Server-side line number where the error originated, if available.
 
     Example:
         try:
             result = await client.some_operation()
             if client.did_fail(result):
                 raise DAPException(result)
         except DAPException as e:
             print(f"Error: {e}")
 
-            # Access detailed error context
-            if 'file' in e.dap_result and 'line' in e.dap_result:
-                print(f"Location: {e.dap_result['file']}:{e.dap_result['line']}")
+            # Access detailed error context
+            if e.file is not None:
+                print(f"Location: {e.file}:{e.lineno}")

Also applies to: 103-107

🤖 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 `@packages/client-python/src/rocketride/core/exceptions.py` around lines 67 -
85, The DAPException docstring is outdated after adding trace extraction, so
update the class documentation to describe the new file and lineno attributes
and stop teaching access via e.dap_result['file'] / e.dap_result['line']. In the
DAPException docstring and example, reference the new attributes exposed by the
exception itself and align the wording with the trace-based server payload
(using lineno under trace). Make sure the Attributes section and example in
DAPException reflect the current API so readers use e.file and e.lineno
consistently.
packages/ai/src/ai/modules/task/commands/cmd_monitor.py (1)

213-224: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Fail closed when delivering scoped server events.

If user_id or org_id is set and this connection has no _account_info, the current checks are skipped and a wildcard subscriber can receive a scoped dashboard/billing event.

Proposed fix
-        if org_id is not None and hasattr(self, '_account_info') and self._account_info:
+        info = getattr(self, '_account_info', None)
+        if org_id is not None:
+            if not info:
+                return
             conn_org = ''
-            if hasattr(self._account_info, 'organization') and self._account_info.organization:
-                org = self._account_info.organization
+            if hasattr(info, 'organization') and info.organization:
+                org = info.organization
                 conn_org = org.get('id', '') if isinstance(org, dict) else getattr(org, 'id', '')
             if conn_org != org_id:
                 return
         # Step 4: user scoping
-        if user_id is not None and hasattr(self, '_account_info') and self._account_info:
-            if self._account_info.userId != user_id:
+        if user_id is not None:
+            if not info or info.userId != user_id:
                 return
🤖 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 `@packages/ai/src/ai/modules/task/commands/cmd_monitor.py` around lines 213 -
224, The scoped event delivery checks in cmd_monitor.py are failing open when
_account_info is missing, which can let a wildcard subscriber receive
org/user-specific events. Update the Step 3 org scoping and Step 4 user scoping
logic in the relevant delivery method so that any non-null org_id or user_id
requires a valid _account_info; if it is absent or incomplete, return early
instead of skipping the check. Use the existing _account_info, organization, and
userId access paths to keep the behavior consistent while making the default
fail closed.
packages/ai/src/ai/modules/task/commands/cmd_debug.py (1)

309-359: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Possible UnboundLocalError masking the real failure in on_terminate/on_threads.

Same pattern already called out for on_attach in the test suite: token is only assigned inside the try block, but the except handler references it in the log message (Lines 358, 566). If get_task_token(...) itself raises before returning, the except clause raises UnboundLocalError instead of surfacing the original error, which will confuse debugging. Consider initializing token = request.get('token', self._debug_token) before the token-extraction call, or omitting token from the error message.

Also applies to: 528-567

🤖 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 `@packages/ai/src/ai/modules/task/commands/cmd_debug.py` around lines 309 -
359, The error handling in on_terminate (and the same pattern in
on_threads/on_attach) can raise UnboundLocalError because token is only assigned
after get_task_token(request, ctx) succeeds, but the except block always logs
token. Initialize token before the try block from request or self._debug_token,
or avoid referencing token in the failure log, so the original exception from
get_task_token or get_task is preserved and logged correctly.
packages/ai/tests/ai/modules/task/test_task_engine.py (1)

44-81: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Docstring not updated for new project_id seed.

Line 65 now seeds t.project_id, but the helper's docstring (lines 48-50) still enumerates only id, source, _task_name, _pipeline, _status, _threads, _pipelineTraceLevel, token as seeded attributes.

🤖 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 `@packages/ai/tests/ai/modules/task/test_task_engine.py` around lines 44 - 81,
Update the _task helper docstring in test_task_engine.py to include the newly
seeded project_id attribute in the list of consumed attributes. Keep the
docstring aligned with the actual initialization in _task, including id, source,
project_id, _task_name, _pipeline, _status, _threads, _pipelineTraceLevel, and
token.
docs/agents/ROCKETRIDE_OBSERVABILITY.md (1)

302-312: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Tokens schema example is stale relative to the new dynamic TASK_TOKENS model.

This example still documents the old fixed schema (cpu_utilization, cpu_memory, gpu_memory, total), but TASK_TOKENS is now a dynamic Record<string, number> keyed by metrics_conversions metric keys (cpu_compute, cpu_memory, gpu_compute, gpu_memory, plus arbitrary future keys), per packages/client-python/src/rocketride/types/task.py and packages/client-typescript/src/client/types/task.ts in this same PR. Leaving this doc unchanged will mislead integrators building against the apaevt_status_update wire protocol.

📝 Proposed doc fix
-  // Cumulative billing tokens (100 tokens = $1)
-  tokens: {
-    cpu_utilization: number, cpu_memory: number, gpu_memory: number, total: number
-  }
+  // Cumulative billing tokens (100 tokens = $1). Dynamic key/value map;
+  // keys match metric_key in the metrics_conversions DB table
+  // (e.g. cpu_compute, cpu_memory, gpu_compute, gpu_memory). Sum all
+  // values for the total. Additional keys may appear for new billable metrics.
+  tokens: { [metricKey: string]: number }
🤖 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 `@docs/agents/ROCKETRIDE_OBSERVABILITY.md` around lines 302 - 312, Update the
tokens example in the observability docs to match the new dynamic TASK_TOKENS
shape instead of the old fixed fields. In the apaevt_status_update schema
example, replace the hardcoded cpu_utilization/cpu_memory/gpu_memory/total
structure with a Record<string, number> keyed by metrics_conversions names, and
reference the TASK_TOKENS types from task.py and task.ts so the example reflects
cpu_compute, cpu_memory, gpu_compute, gpu_memory, and future keys.
packages/ai/src/ai/web/metrics/metrics.py (1)

98-124: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Stale class docstring after >MET*/>USG* split.

The class docstring/attributes still describe _timers as milliseconds tied to >MET* output, but report() below now explicitly targets the >USG* billing protocol and _timers also stores absolute values written via set_value. Worth updating the class-level docs to reflect the new dual-protocol architecture (live >MET* vs. periodic >USG*) so future readers aren't misled.

🤖 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 `@packages/ai/src/ai/web/metrics/metrics.py` around lines 98 - 124, Update the
MetricsManager class docstring and attribute descriptions to match the current
dual-protocol behavior: live `>MET*` metrics versus periodic `>USG*` billing
reports. In particular, revise the `_timers` description so it no longer says
only cumulative milliseconds for `>MET*`; it should also reflect absolute values
stored via `set_value` and consumed by `report()`. Keep the documentation
aligned with the `MetricsManager` class, `set_value`, and `report` symbols so
the current architecture is clear to readers.
packages/ai/src/ai/modules/task/task_engine.py (1)

1631-1647: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Replay current service state after creating TaskMetrics.

The stdio reader is connected before _task_metrics is constructed, so a fast subprocess can emit >SVC*1 first. In that case _status.serviceUp becomes true, but set_service_up(True) is never called and billing remains gated.

Proposed fix
                 )
                 self._task_metrics.start_monitoring()
+                if self._status.serviceUp:
+                    self._task_metrics.set_service_up(True)
                 self.debug_message(f'Started metrics monitoring for PID {self._engine_process.pid}')
🤖 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 `@packages/ai/src/ai/modules/task/task_engine.py` around lines 1631 - 1647, The
`TaskEngine` metrics initialization can miss a preexisting service-up state
because the stdio reader may set `_status.serviceUp` before `_task_metrics`
exists. After constructing `TaskMetrics` and before/after `start_monitoring()`
in the `TaskEngine` initialization flow, check the current `_status.serviceUp`
flag and immediately call `set_service_up(True)` (or the equivalent replay path)
so billing state is synchronized with the already-received service status.
🤖 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.

Inline comments:
In `@packages/ai/src/ai/common/dap/transport_stdio.py`:
- Around line 763-776: The new >USG* parsing branch in transport_stdio.py needs
direct test coverage to ensure it emits the apaevt_status_tokens event with the
parsed JSON body. Add a dedicated parser test around the same transport handling
path used for >MET*, but feed a >USG* message and assert both the event name and
payload shape produced by the message handler in transport_stdio.py.

In `@packages/ai/src/ai/modules/task/commands/cmd_cprofile.py`:
- Around line 109-125: The `_proxy_to_task` helper in `cmd_cprofile.py` still
uses a generic `Dict[str, Any]` request and an untyped `ctx=None`, which is
inconsistent with the updated typed handler contract. Update `_proxy_to_task` to
match the surrounding cProfile command methods by using the same DAP
request/response types and `ctx: RequestContext`, and make sure any call sites
continue to pass the typed request object through consistently.

In `@packages/ai/src/ai/modules/task/commands/cmd_monitor.py`:
- Around line 620-630: The permission check in _verify_monitor_permissions is
blocking unsubscribe for EVENT_TYPE.NONE when the caller has lost team access,
which leaves stale entries in _monitors. Update cmd_monitor.py so the monitor
removal path in _verify_monitor_permissions (or the caller that handles
EVENT_TYPE.NONE) allows unsubscribe to proceed without requiring current team
permissions, while keeping the existing task.monitor/task.data checks for
add/update events. Make sure the logic still uses resolve_task_permissions and
the event_type bitmask checks, but skips the deny path for EVENT_TYPE.NONE so
stale subscriptions can be removed.
- Around line 82-115: The sorting in _paginate still assumes requested sort_by
values are directly comparable, so fields like clientInfo, monitors, metrics, or
mixed scalar types can raise TypeError. Update the _sort_key/_paginate sorting
path to normalize any requested field into a total-order key that safely handles
dicts, lists, None, and mixed types, and keep using _paginate’s
sort_by/sort_order flow so arbitrary dashboard fields can be sorted without
exceptions.

In `@packages/ai/src/ai/modules/task/task_conn.py`:
- Around line 303-318: The RequestContext construction in task_conn.py is
trusting message-supplied arguments._ctx too broadly, which allows forged
identity on direct connections. Update the _ctx handling in the receive path
around raw_ctx / AccountInfo / RequestContext so it is only accepted when the
connection is explicitly marked as trusted orchestrator/pod traffic; otherwise
ignore or strip _ctx and fall back to connection-derived state. Use the existing
on_receive flow and RequestContext creation to gate this behavior without
changing authenticated direct-client semantics.

In `@packages/ai/src/ai/modules/task/task_engine.py`:
- Around line 742-753: The final status reset in task_engine’s stop-monitoring
flow is wiping out lifetime peak resource data by zeroing the peak_* fields.
Update the status cleanup logic so only the live point-in-time metrics are
cleared, and keep peak_cpu_percent, peak_cpu_memory_mb, and peak_gpu_memory_mb
intact in the terminal status. Use the existing self._status.metrics reset block
in task_engine to locate the change.

In `@packages/ai/src/ai/modules/task/task_metrics.py`:
- Around line 122-127: The baseline deferral in task metrics is too broad and
can misclassify the first post-ready `>USG*` object-boundary snapshot as
startup, dropping real work from billing. Update `task_metrics` around
`_baseline_pending` and the baseline capture path so `serviceUp` only defers
initialization until the first true startup usage record, and ensure the first
post-ready snapshot is still billed when it contains request work; use the
existing `serviceUp`, `_baseline_pending`, and `_baselines` logic to locate and
tighten this behavior.

In `@packages/ai/src/ai/modules/task/task_server.py`:
- Around line 746-749: The early return in the task auth check is too broad
because any pk_/tk_ credential is accepted for any control. Update the logic in
task_server.py around the auth shortcut so scoped credentials are only allowed
when they are bound to the same checked task/control, and ensure the permission
path still honors require for non-public access. Use the existing
auth/account_info lookup in the task authorization flow to validate the
credential against the resolved task before returning.

In `@packages/ai/src/ai/web/metrics/metrics.py`:
- Around line 173-207: The metrics accumulator currently uses `_timers` for both
additive updates in `add_value` and absolute replacements in `set_value`, which
makes the storage semantics misleading. Update the shared backing attribute in
the metrics class to a more generic name such as `_values`, and adjust both
`add_value` and `set_value` to use it consistently so the mixed
additive/absolute behavior is clear to future contributors.

In `@packages/ai/src/ai/web/metrics/process_reporter.py`:
- Around line 293-299: The stop path in ProcessReporter is clearing the thread
reference and calling _sample() even when the join times out, which can race
with the still-running reporter thread and allow a later start() to spawn
another thread. Update the shutdown logic in the method that sets self._stopped
and joins self._thread so that if the thread is still alive after the timed
wait, it does not null out self._thread or perform the final _sample() until the
thread has actually finished; keep the existing _sample() only on a clean stop
and preserve the active-thread state for start()/stop() coordination.

In `@packages/ai/tests/ai/account/test_account_helpers.py`:
- Around line 288-299: The new coverage only checks the team-scoped bypass in
resolve_team_permissions, but the production change also applies to
resolve_task_permissions. Add matching tests for resolve_task_permissions in
test_account_helpers.py that verify both sys.admin and internal callers receive
_FULL_TEAM_PERMISSIONS without team/task membership, using the same _acct helper
and the existing permission constants so both resolvers are covered.

In `@packages/ai/tests/ai/web/metrics/test_process_reporter.py`:
- Around line 63-69: The autouse fixture in fake_platform_reads only stubs
resource readers, but _sample() still mutates the shared metrics singleton and
can leak cpu_compute/cpu_memory state into other tests. Update this fixture to
also reset the global metrics instance exposed by pr_mod before and/or after
each test, using the metrics symbol in test_process_reporter.py so every test
starts from a clean slate.

In `@packages/client-python/src/rocketride/account.py`:
- Around line 108-121: Document the new public AccountApi.get_desktop() method
in the client-python docs and update its signature to use a parameterized return
type. Add the corresponding reference documentation under
packages/client-python/docs/ so the public API change is captured, and change
get_desktop() in account.py to return list[AppManifestEntry] instead of a bare
list for consistency with list_keys, list_members, and list_teams.

In `@packages/client-python/src/rocketride/mixins/data.py`:
- Around line 244-267: The chunked write logic in the data pipe send path skips
empty buffers because the while loop never runs for a zero-length `buffer`,
unlike the previous single-request behavior. Update the write flow in the
`send`/pipe-writing method in `DataMixin` so that `buffer == b''` still issues
exactly one `rrext_process` request with an empty `data` payload before closing
the pipe, while keeping the existing chunking behavior for non-empty buffers.

In `@packages/client-python/src/rocketride/mixins/events.py`:
- Around line 466-481: The public identify method in events.py dropped the
legacy client_name keyword, breaking existing callers before rrext_identify is
sent. Update Rocketride mixins Events.identify to accept client_name as an alias
alongside app_name, normalize whichever argument is provided into the stored
_app_name, and keep sending appName while preserving the legacy clientName field
for back-compat.

In `@packages/client-python/src/rocketride/mixins/store.py`:
- Around line 434-438: The drive-letter absolute path rejection is only in
_validate_relative_path, so callers that go through _validate_store_path
directly (like fs_open, fs_delete, fs_mkdir, and fs_stat) still allow Windows
paths through. Move the drive-letter check into _validate_store_path so all
store-path validation uses the same shared guard, and keep
_validate_relative_path as a thin wrapper if needed.

In `@packages/client-python/src/rocketride/types/dashboard.py`:
- Around line 120-133: The DASHBOARD_RESPONSE TypedDict currently makes overview
optional because total=False is applied to the whole type. Refactor the type in
dashboard.py so overview stays required while tasks, tasks_total, connections,
and connections_total remain optional, using separate TypedDict definitions or a
Required/NotRequired split within DASHBOARD_RESPONSE.

In `@packages/client-typescript/src/client/account.ts`:
- Line 34: The new public AccountApi.getDesktop() method is missing the required
SDK reference documentation update. Regenerate and commit the TypeScript API
docs under packages/client-typescript/docs/ using
client-typescript:docs-generate so the public signature change is reflected, and
ensure the generated reference entries for AccountApi and getDesktop() are
included alongside the existing client types.

In `@packages/client-typescript/src/client/client.ts`:
- Around line 210-230: The chunking logic in DataPipe.send no longer issues a
write for empty buffers because the while loop is skipped when buffer.length is
0. Update send in client.ts to preserve the previous behavior by explicitly
handling the zero-length case and sending a single write subcommand with an
empty Uint8Array before closing the pipe, while keeping the existing chunked
path for non-empty buffers.
- Around line 895-899: Preserve the authenticated intent in client connect flow:
`Client.connect()` sets `_desiredState` to `'authenticated'`, but `attach()` may
reset it to `'detached'` when changing URIs, so a failed persist-mode reconnect
can stop at `'attached'` without re-login. Update the reconnect/URI-switch path
in `connect()` and the `attach()`/`detach()` transition logic so a URI change
does not clear an authenticated target state, and ensure the state only
downgrades when explicitly requested. Use the `connect`, `attach`, `detach`,
`_desiredState`, and `login` symbols to keep the reconnect path re-attempting
authentication after a reattach.

In `@packages/client-typescript/src/client/types/dashboard.ts`:
- Around line 32-44: The DashboardPageParams.state_filter type is too broad and
should only allow the documented literals. Update the DashboardPageParams
interface in dashboard.ts so state_filter is constrained to 'running' |
'completed' | null, matching the existing comment and preventing invalid values
from compiling; use the DashboardPageParams symbol to locate the change.

In `@packages/shared-ui/src/modules/server/components/OverviewTab.tsx`:
- Line 455: The connection display fallback in OverviewTab’s rendering of
DashboardConnection should match ConnectionsTab so the same connection doesn’t
show different labels. Update the name chain in the task name render to include
clientId between clientInfo?.name and the Conn `#id` fallback, using the same
logic as the ConnectionsTab component.

---

Outside diff comments:
In `@docs/agents/ROCKETRIDE_OBSERVABILITY.md`:
- Around line 302-312: Update the tokens example in the observability docs to
match the new dynamic TASK_TOKENS shape instead of the old fixed fields. In the
apaevt_status_update schema example, replace the hardcoded
cpu_utilization/cpu_memory/gpu_memory/total structure with a Record<string,
number> keyed by metrics_conversions names, and reference the TASK_TOKENS types
from task.py and task.ts so the example reflects cpu_compute, cpu_memory,
gpu_compute, gpu_memory, and future keys.

In `@packages/ai/src/ai/modules/task/commands/cmd_debug.py`:
- Around line 309-359: The error handling in on_terminate (and the same pattern
in on_threads/on_attach) can raise UnboundLocalError because token is only
assigned after get_task_token(request, ctx) succeeds, but the except block
always logs token. Initialize token before the try block from request or
self._debug_token, or avoid referencing token in the failure log, so the
original exception from get_task_token or get_task is preserved and logged
correctly.

In `@packages/ai/src/ai/modules/task/commands/cmd_monitor.py`:
- Around line 213-224: The scoped event delivery checks in cmd_monitor.py are
failing open when _account_info is missing, which can let a wildcard subscriber
receive org/user-specific events. Update the Step 3 org scoping and Step 4 user
scoping logic in the relevant delivery method so that any non-null org_id or
user_id requires a valid _account_info; if it is absent or incomplete, return
early instead of skipping the check. Use the existing _account_info,
organization, and userId access paths to keep the behavior consistent while
making the default fail closed.

In `@packages/ai/src/ai/modules/task/task_engine.py`:
- Around line 1631-1647: The `TaskEngine` metrics initialization can miss a
preexisting service-up state because the stdio reader may set
`_status.serviceUp` before `_task_metrics` exists. After constructing
`TaskMetrics` and before/after `start_monitoring()` in the `TaskEngine`
initialization flow, check the current `_status.serviceUp` flag and immediately
call `set_service_up(True)` (or the equivalent replay path) so billing state is
synchronized with the already-received service status.

In `@packages/ai/src/ai/web/metrics/metrics.py`:
- Around line 98-124: Update the MetricsManager class docstring and attribute
descriptions to match the current dual-protocol behavior: live `>MET*` metrics
versus periodic `>USG*` billing reports. In particular, revise the `_timers`
description so it no longer says only cumulative milliseconds for `>MET*`; it
should also reflect absolute values stored via `set_value` and consumed by
`report()`. Keep the documentation aligned with the `MetricsManager` class,
`set_value`, and `report` symbols so the current architecture is clear to
readers.

In `@packages/ai/tests/ai/modules/task/test_task_engine.py`:
- Around line 44-81: Update the _task helper docstring in test_task_engine.py to
include the newly seeded project_id attribute in the list of consumed
attributes. Keep the docstring aligned with the actual initialization in _task,
including id, source, project_id, _task_name, _pipeline, _status, _threads,
_pipelineTraceLevel, and token.

In `@packages/client-python/src/rocketride/core/exceptions.py`:
- Around line 67-85: The DAPException docstring is outdated after adding trace
extraction, so update the class documentation to describe the new file and
lineno attributes and stop teaching access via e.dap_result['file'] /
e.dap_result['line']. In the DAPException docstring and example, reference the
new attributes exposed by the exception itself and align the wording with the
trace-based server payload (using lineno under trace). Make sure the Attributes
section and example in DAPException reflect the current API so readers use
e.file and e.lineno consistently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 822b5961-6063-42ef-b426-3df8291b19d0

📥 Commits

Reviewing files that changed from the base of the PR and between 89f6129 and 701de96.

📒 Files selected for processing (107)
  • apps/shell-ui/rsbuild.config.mts
  • apps/shell-ui/src/bootstrap.tsx
  • apps/shell-ui/src/components/layout/LoadingScreen.tsx
  • apps/shell-ui/src/components/layout/Shell.tsx
  • apps/shell-ui/src/components/layout/Sidebar.tsx
  • apps/shell-ui/src/connection/connection.ts
  • apps/shell-ui/src/hooks/AppRegistryContext.tsx
  • apps/shell-ui/src/hooks/useSubscriptions.ts
  • apps/shell-ui/src/index.ts
  • apps/shell-ui/src/lib/appLoader.ts
  • apps/shell-ui/src/views/settings/SettingsPage.tsx
  • apps/shell-ui/src/workspace/WorkspaceContext.tsx
  • apps/shell-ui/src/workspace/useWorkspaceState.ts
  • apps/vscode/src/connection/connection.ts
  • apps/vscode/src/connection/deploy-manager.ts
  • apps/vscode/src/providers/AccountProvider.ts
  • apps/vscode/src/providers/views/Account/AccountWebview.tsx
  • apps/vscode/src/providers/views/types.ts
  • apps/vscode/src/shared/types/taskStatus.ts
  • apps/vscode/src/shared/util/subscriptionGate.ts
  • docs/agents/ROCKETRIDE_OBSERVABILITY.md
  • packages/ai/src/ai/__init__.py
  • packages/ai/src/ai/account/__init__.py
  • packages/ai/src/ai/account/base.py
  • packages/ai/src/ai/account/models.py
  • packages/ai/src/ai/account/oss/__init__.py
  • packages/ai/src/ai/account/store_providers/__init__.py
  • packages/ai/src/ai/common/dap/dap_conn.py
  • packages/ai/src/ai/common/dap/transport_stdio.py
  • packages/ai/src/ai/common/models/audio/whisper.py
  • packages/ai/src/ai/common/models/base.py
  • packages/ai/src/ai/common/models/gliner/gliner.py
  • packages/ai/src/ai/common/models/ocr/doctr.py
  • packages/ai/src/ai/common/models/ocr/easyocr.py
  • packages/ai/src/ai/common/models/ocr/surya.py
  • packages/ai/src/ai/common/models/ocr/trocr.py
  • packages/ai/src/ai/common/models/transformers/sentence_transformers.py
  • packages/ai/src/ai/common/models/transformers/transformers.py
  • packages/ai/src/ai/common/models/vision/background.py
  • packages/ai/src/ai/common/models/vision/caption.py
  • packages/ai/src/ai/common/models/vision/depth.py
  • packages/ai/src/ai/common/models/vision/detection.py
  • packages/ai/src/ai/common/models/vision/segmentation.py
  • packages/ai/src/ai/common/models/vision/vision.py
  • packages/ai/src/ai/constants.py
  • packages/ai/src/ai/eaas.py
  • packages/ai/src/ai/modules/data/data_conn.py
  • packages/ai/src/ai/modules/task/commands/cmd_account.py
  • packages/ai/src/ai/modules/task/commands/cmd_app.py
  • packages/ai/src/ai/modules/task/commands/cmd_cprofile.py
  • packages/ai/src/ai/modules/task/commands/cmd_data.py
  • packages/ai/src/ai/modules/task/commands/cmd_debug.py
  • packages/ai/src/ai/modules/task/commands/cmd_deploy.py
  • packages/ai/src/ai/modules/task/commands/cmd_misc.py
  • packages/ai/src/ai/modules/task/commands/cmd_monitor.py
  • packages/ai/src/ai/modules/task/commands/cmd_public.py
  • packages/ai/src/ai/modules/task/commands/cmd_store.py
  • packages/ai/src/ai/modules/task/commands/cmd_task.py
  • packages/ai/src/ai/modules/task/task_conn.py
  • packages/ai/src/ai/modules/task/task_engine.py
  • packages/ai/src/ai/modules/task/task_metrics.py
  • packages/ai/src/ai/modules/task/task_server.py
  • packages/ai/src/ai/web/metrics/metrics.py
  • packages/ai/src/ai/web/metrics/process_reporter.py
  • packages/ai/src/ai/web/metrics/taskhook.py
  • packages/ai/src/ai/web/server.py
  • packages/ai/tests/ai/account/test_account_helpers.py
  • packages/ai/tests/ai/common/dap/test_dap_conn.py
  • packages/ai/tests/ai/common/dap/test_transport_stdio.py
  • packages/ai/tests/ai/modules/task/commands/test_cmd_account_app.py
  • packages/ai/tests/ai/modules/task/commands/test_cmd_debug.py
  • packages/ai/tests/ai/modules/task/commands/test_cmd_misc.py
  • packages/ai/tests/ai/modules/task/commands/test_cmd_monitor.py
  • packages/ai/tests/ai/modules/task/commands/test_cmd_public.py
  • packages/ai/tests/ai/modules/task/commands/test_cmd_task.py
  • packages/ai/tests/ai/modules/task/test_task_conn.py
  • packages/ai/tests/ai/modules/task/test_task_engine.py
  • packages/ai/tests/ai/modules/task/test_task_metrics.py
  • packages/ai/tests/ai/modules/task/test_task_server.py
  • packages/ai/tests/ai/web/metrics/test_metrics.py
  • packages/ai/tests/ai/web/metrics/test_process_reporter.py
  • packages/client-python/src/rocketride/__init__.py
  • packages/client-python/src/rocketride/account.py
  • packages/client-python/src/rocketride/client.py
  • packages/client-python/src/rocketride/core/dap_client.py
  • packages/client-python/src/rocketride/core/exceptions.py
  • packages/client-python/src/rocketride/mixins/connection.py
  • packages/client-python/src/rocketride/mixins/dashboard.py
  • packages/client-python/src/rocketride/mixins/data.py
  • packages/client-python/src/rocketride/mixins/events.py
  • packages/client-python/src/rocketride/mixins/store.py
  • packages/client-python/src/rocketride/types/__init__.py
  • packages/client-python/src/rocketride/types/client.py
  • packages/client-python/src/rocketride/types/dashboard.py
  • packages/client-python/src/rocketride/types/task.py
  • packages/client-typescript/src/client/account.ts
  • packages/client-typescript/src/client/client.ts
  • packages/client-typescript/src/client/exceptions/index.ts
  • packages/client-typescript/src/client/types/client.ts
  • packages/client-typescript/src/client/types/dashboard.ts
  • packages/client-typescript/src/client/types/task.ts
  • packages/shared-ui/src/components/tokens/Tokens.tsx
  • packages/shared-ui/src/modules/account/components/BillingPanel.tsx
  • packages/shared-ui/src/modules/server/MonitorView.tsx
  • packages/shared-ui/src/modules/server/components/ConnectionsTab.tsx
  • packages/shared-ui/src/modules/server/components/OverviewTab.tsx
  • packages/shared-ui/src/types/shell.ts

Comment thread packages/ai/src/ai/common/dap/transport_stdio.py
Comment thread packages/ai/src/ai/modules/task/commands/cmd_cprofile.py Outdated
Comment thread packages/ai/src/ai/modules/task/commands/cmd_monitor.py
Comment thread packages/ai/src/ai/modules/task/commands/cmd_monitor.py
Comment thread packages/ai/src/ai/modules/task/task_conn.py
Comment thread packages/client-typescript/src/client/account.ts
Comment thread packages/client-typescript/src/client/client.ts Outdated
Comment thread packages/client-typescript/src/client/client.ts Outdated
Comment thread packages/client-typescript/src/client/types/dashboard.ts
Comment thread packages/shared-ui/src/modules/server/components/OverviewTab.tsx Outdated
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor
🤖 Internal: Discord sync marker

Auto-managed by the Discord notification workflow. Stores the linked Discord message ID and forum thread ID. Do not edit or delete.

Security:
- task_conn._build_ctx: honor a forwarded _ctx only when the connection
  itself authenticated as an internal pod service (not any client)
- task_server.verify_task_access: bind pk_/tk_ scoped credentials to the
  checked task and restrict pk_ to task.data/task.monitor
- client-python store: move drive-letter path reject into the shared
  _validate_store_path so fs_open/delete/mkdir/stat are covered

Metrics/billing:
- process_reporter: emit first >USG* immediately so the billing baseline
  lands at serviceUp, not at the first object; guard stop() against a
  timed-out join
- task_engine: preserve peak_* watermarks in the final status
- metrics: rename MetricsManager._timers -> _values (+ docstrings)

SDKs:
- preserve zero-length pipe writes (python + typescript)
- events.identify(): keep legacy client_name keyword alias
- client.ts connect(): preserve authenticated intent across URI change
- dashboard types: DASHBOARD_RESPONSE.overview required; state_filter
  constrained to 'running'|'completed'|null
- account.get_desktop() -> list[AppManifestEntry]

Monitor/dashboard/UI:
- cmd_monitor: total-ordering sort key; allow EVENT_TYPE.NONE unsubscribe
  after access loss
- OverviewTab: add clientId to the connection name fallback chain

Tests/typing:
- transport_stdio: >USG* parser tests
- task-scoped sys/internal permission-bypass tests
- reset metrics singleton in process_reporter fixture
- cmd_cprofile: type _proxy_to_task

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
packages/ai/src/ai/modules/task/task_engine.py (1)

1598-1602: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Remove the sleep(0) yield points around subprocess creation. These widen a race where stop_task() can see _engine_process as None, skip termination, and leave a newly spawned subprocess running. If the interleaving is intentional, keep creation and assignment under the same termination guard instead.

🤖 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 `@packages/ai/src/ai/modules/task/task_engine.py` around lines 1598 - 1602, The
asyncio.sleep(0) yield points in task_engine.py around subprocess creation
should be removed because they let stop_task() interleave before _engine_process
is assigned. Update the task engine flow in the subprocess creation path so the
process is created and assigned under the same termination guard in
task_engine.py, using the existing _engine_process handling and stop_task()
logic to avoid the race.
packages/client-typescript/src/client/client.ts (1)

1859-1866: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Regenerate the client-typescript API docs for identify and getDashboard. These public signatures are present in packages/client-typescript/src/client/client.ts, but packages/client-typescript/docs/ doesn’t contain matching reference entries.

🤖 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 `@packages/client-typescript/src/client/client.ts` around lines 1859 - 1866,
Regenerate the client-typescript API docs so the public client methods are
documented consistently: `Client.identify` (and the related `getDashboard`
method mentioned in the review) are present in
`packages/client-typescript/src/client/client.ts` but missing matching reference
entries under `packages/client-typescript/docs/`. Update the generated docs for
the `Client` class so both signatures appear with their current behavior and
parameters, keeping the docs in sync with the source API.

Source: Coding guidelines

packages/ai/src/ai/modules/task/commands/cmd_monitor.py (1)

277-286: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Drop-oldest can still evict SSE chunks from the shared queue. At packages/ai/src/ai/modules/task/commands/cmd_monitor.py:277-286, this branch removes the queue head unconditionally for non-SSE overflow. Because _out_q mixes SSE and snapshot events, a slow consumer can have queued SSE chunks at the head; the next snapshot overflow would silently drop one instead of disconnecting, which violates the no-drop SSE path. Split the queues or only evict a non-SSE head.

🤖 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 `@packages/ai/src/ai/modules/task/commands/cmd_monitor.py` around lines 277 -
286, The overflow handling in cmd_monitor.py’s queue write path can wrongly
evict queued SSE chunks when a snapshot event arrives, because the current
drop-oldest branch removes the head of the shared _out_q unconditionally. Update
the logic around the non-SSE enqueue path so it never discards an SSE item from
the shared queue; either separate SSE and snapshot queues, or inspect the head
before dropping and only evict a non-SSE snapshot event while letting SSE
backpressure/disconnect behavior remain intact.
🤖 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.

Outside diff comments:
In `@packages/ai/src/ai/modules/task/commands/cmd_monitor.py`:
- Around line 277-286: The overflow handling in cmd_monitor.py’s queue write
path can wrongly evict queued SSE chunks when a snapshot event arrives, because
the current drop-oldest branch removes the head of the shared _out_q
unconditionally. Update the logic around the non-SSE enqueue path so it never
discards an SSE item from the shared queue; either separate SSE and snapshot
queues, or inspect the head before dropping and only evict a non-SSE snapshot
event while letting SSE backpressure/disconnect behavior remain intact.

In `@packages/ai/src/ai/modules/task/task_engine.py`:
- Around line 1598-1602: The asyncio.sleep(0) yield points in task_engine.py
around subprocess creation should be removed because they let stop_task()
interleave before _engine_process is assigned. Update the task engine flow in
the subprocess creation path so the process is created and assigned under the
same termination guard in task_engine.py, using the existing _engine_process
handling and stop_task() logic to avoid the race.

In `@packages/client-typescript/src/client/client.ts`:
- Around line 1859-1866: Regenerate the client-typescript API docs so the public
client methods are documented consistently: `Client.identify` (and the related
`getDashboard` method mentioned in the review) are present in
`packages/client-typescript/src/client/client.ts` but missing matching reference
entries under `packages/client-typescript/docs/`. Update the generated docs for
the `Client` class so both signatures appear with their current behavior and
parameters, keeping the docs in sync with the source API.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a0a9da2b-378e-4c26-9620-0e62c894b28c

📥 Commits

Reviewing files that changed from the base of the PR and between 701de96 and 4283f74.

📒 Files selected for processing (19)
  • packages/ai/src/ai/modules/task/commands/cmd_cprofile.py
  • packages/ai/src/ai/modules/task/commands/cmd_monitor.py
  • packages/ai/src/ai/modules/task/task_conn.py
  • packages/ai/src/ai/modules/task/task_engine.py
  • packages/ai/src/ai/modules/task/task_metrics.py
  • packages/ai/src/ai/modules/task/task_server.py
  • packages/ai/src/ai/web/metrics/metrics.py
  • packages/ai/src/ai/web/metrics/process_reporter.py
  • packages/ai/tests/ai/account/test_account_helpers.py
  • packages/ai/tests/ai/common/dap/test_transport_stdio.py
  • packages/ai/tests/ai/web/metrics/test_process_reporter.py
  • packages/client-python/src/rocketride/account.py
  • packages/client-python/src/rocketride/mixins/data.py
  • packages/client-python/src/rocketride/mixins/events.py
  • packages/client-python/src/rocketride/mixins/store.py
  • packages/client-python/src/rocketride/types/dashboard.py
  • packages/client-typescript/src/client/client.ts
  • packages/client-typescript/src/client/types/dashboard.ts
  • packages/shared-ui/src/modules/server/components/OverviewTab.tsx

@Rod-Christensen
Rod-Christensen merged commit 2925564 into develop Jul 10, 2026
23 checks passed
@Rod-Christensen
Rod-Christensen deleted the feat/alb branch July 10, 2026 04:27
@Rod-Christensen
Rod-Christensen restored the feat/alb branch July 10, 2026 07:05
@Rod-Christensen

Copy link
Copy Markdown
Collaborator Author

Backed out of develop (reset to 3ebc215) pending joint testing with the UI-consistency work. Re-landing via #1541.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation module:ai AI/ML modules module:client-python Python SDK and MCP client module:client-typescript module:ui Chat UI and Dropper UI module:vscode VS Code extension

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant