feat(alb): saas compatible connection multiplexing and metrics#1399
Conversation
📝 WalkthroughWalkthroughThis 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. ChangesShell-UI app registry and deep-link loading
Server DAP RequestContext and permission refactor
Task billing and metrics pipeline rework
Client SDK updates
Shared-UI dashboard and token components
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
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
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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
packages/client-typescript/src/client/client.tsOops! Something went wrong! :( ESLint: 9.39.4 TypeError: expand is not a function packages/client-typescript/src/client/types/dashboard.tsOops! Something went wrong! :( ESLint: 9.39.4 TypeError: expand is not a function packages/shared-ui/src/modules/server/components/OverviewTab.tsxOops! Something went wrong! :( ESLint: 9.39.4 TypeError: expand is not a function 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. Comment |
… 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>
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>
There was a problem hiding this comment.
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 winDocstring 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 one.file/e.lineno(and the actual server key islineno, notline, nested undertrace). The Attributes block also doesn't document the newfile/linenoattributes.📝 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 winFail closed when delivering scoped server events.
If
user_idororg_idis 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 winPossible
UnboundLocalErrormasking the real failure inon_terminate/on_threads.Same pattern already called out for
on_attachin the test suite:tokenis only assigned inside thetryblock, but theexcepthandler references it in the log message (Lines 358, 566). Ifget_task_token(...)itself raises before returning, theexceptclause raisesUnboundLocalErrorinstead of surfacing the original error, which will confuse debugging. Consider initializingtoken = request.get('token', self._debug_token)before the token-extraction call, or omittingtokenfrom 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 valueDocstring not updated for new
project_idseed.Line 65 now seeds
t.project_id, but the helper's docstring (lines 48-50) still enumerates onlyid, source, _task_name, _pipeline, _status, _threads, _pipelineTraceLevel, tokenas 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 winTokens schema example is stale relative to the new dynamic
TASK_TOKENSmodel.This example still documents the old fixed schema (
cpu_utilization, cpu_memory, gpu_memory, total), butTASK_TOKENSis now a dynamicRecord<string, number>keyed bymetrics_conversionsmetric keys (cpu_compute,cpu_memory,gpu_compute,gpu_memory, plus arbitrary future keys), perpackages/client-python/src/rocketride/types/task.pyandpackages/client-typescript/src/client/types/task.tsin this same PR. Leaving this doc unchanged will mislead integrators building against theapaevt_status_updatewire 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 winStale class docstring after
>MET*/>USG*split.The class docstring/attributes still describe
_timersas milliseconds tied to>MET*output, butreport()below now explicitly targets the>USG*billing protocol and_timersalso stores absolute values written viaset_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 winReplay current service state after creating
TaskMetrics.The stdio reader is connected before
_task_metricsis constructed, so a fast subprocess can emit>SVC*1first. In that case_status.serviceUpbecomes true, butset_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
📒 Files selected for processing (107)
apps/shell-ui/rsbuild.config.mtsapps/shell-ui/src/bootstrap.tsxapps/shell-ui/src/components/layout/LoadingScreen.tsxapps/shell-ui/src/components/layout/Shell.tsxapps/shell-ui/src/components/layout/Sidebar.tsxapps/shell-ui/src/connection/connection.tsapps/shell-ui/src/hooks/AppRegistryContext.tsxapps/shell-ui/src/hooks/useSubscriptions.tsapps/shell-ui/src/index.tsapps/shell-ui/src/lib/appLoader.tsapps/shell-ui/src/views/settings/SettingsPage.tsxapps/shell-ui/src/workspace/WorkspaceContext.tsxapps/shell-ui/src/workspace/useWorkspaceState.tsapps/vscode/src/connection/connection.tsapps/vscode/src/connection/deploy-manager.tsapps/vscode/src/providers/AccountProvider.tsapps/vscode/src/providers/views/Account/AccountWebview.tsxapps/vscode/src/providers/views/types.tsapps/vscode/src/shared/types/taskStatus.tsapps/vscode/src/shared/util/subscriptionGate.tsdocs/agents/ROCKETRIDE_OBSERVABILITY.mdpackages/ai/src/ai/__init__.pypackages/ai/src/ai/account/__init__.pypackages/ai/src/ai/account/base.pypackages/ai/src/ai/account/models.pypackages/ai/src/ai/account/oss/__init__.pypackages/ai/src/ai/account/store_providers/__init__.pypackages/ai/src/ai/common/dap/dap_conn.pypackages/ai/src/ai/common/dap/transport_stdio.pypackages/ai/src/ai/common/models/audio/whisper.pypackages/ai/src/ai/common/models/base.pypackages/ai/src/ai/common/models/gliner/gliner.pypackages/ai/src/ai/common/models/ocr/doctr.pypackages/ai/src/ai/common/models/ocr/easyocr.pypackages/ai/src/ai/common/models/ocr/surya.pypackages/ai/src/ai/common/models/ocr/trocr.pypackages/ai/src/ai/common/models/transformers/sentence_transformers.pypackages/ai/src/ai/common/models/transformers/transformers.pypackages/ai/src/ai/common/models/vision/background.pypackages/ai/src/ai/common/models/vision/caption.pypackages/ai/src/ai/common/models/vision/depth.pypackages/ai/src/ai/common/models/vision/detection.pypackages/ai/src/ai/common/models/vision/segmentation.pypackages/ai/src/ai/common/models/vision/vision.pypackages/ai/src/ai/constants.pypackages/ai/src/ai/eaas.pypackages/ai/src/ai/modules/data/data_conn.pypackages/ai/src/ai/modules/task/commands/cmd_account.pypackages/ai/src/ai/modules/task/commands/cmd_app.pypackages/ai/src/ai/modules/task/commands/cmd_cprofile.pypackages/ai/src/ai/modules/task/commands/cmd_data.pypackages/ai/src/ai/modules/task/commands/cmd_debug.pypackages/ai/src/ai/modules/task/commands/cmd_deploy.pypackages/ai/src/ai/modules/task/commands/cmd_misc.pypackages/ai/src/ai/modules/task/commands/cmd_monitor.pypackages/ai/src/ai/modules/task/commands/cmd_public.pypackages/ai/src/ai/modules/task/commands/cmd_store.pypackages/ai/src/ai/modules/task/commands/cmd_task.pypackages/ai/src/ai/modules/task/task_conn.pypackages/ai/src/ai/modules/task/task_engine.pypackages/ai/src/ai/modules/task/task_metrics.pypackages/ai/src/ai/modules/task/task_server.pypackages/ai/src/ai/web/metrics/metrics.pypackages/ai/src/ai/web/metrics/process_reporter.pypackages/ai/src/ai/web/metrics/taskhook.pypackages/ai/src/ai/web/server.pypackages/ai/tests/ai/account/test_account_helpers.pypackages/ai/tests/ai/common/dap/test_dap_conn.pypackages/ai/tests/ai/common/dap/test_transport_stdio.pypackages/ai/tests/ai/modules/task/commands/test_cmd_account_app.pypackages/ai/tests/ai/modules/task/commands/test_cmd_debug.pypackages/ai/tests/ai/modules/task/commands/test_cmd_misc.pypackages/ai/tests/ai/modules/task/commands/test_cmd_monitor.pypackages/ai/tests/ai/modules/task/commands/test_cmd_public.pypackages/ai/tests/ai/modules/task/commands/test_cmd_task.pypackages/ai/tests/ai/modules/task/test_task_conn.pypackages/ai/tests/ai/modules/task/test_task_engine.pypackages/ai/tests/ai/modules/task/test_task_metrics.pypackages/ai/tests/ai/modules/task/test_task_server.pypackages/ai/tests/ai/web/metrics/test_metrics.pypackages/ai/tests/ai/web/metrics/test_process_reporter.pypackages/client-python/src/rocketride/__init__.pypackages/client-python/src/rocketride/account.pypackages/client-python/src/rocketride/client.pypackages/client-python/src/rocketride/core/dap_client.pypackages/client-python/src/rocketride/core/exceptions.pypackages/client-python/src/rocketride/mixins/connection.pypackages/client-python/src/rocketride/mixins/dashboard.pypackages/client-python/src/rocketride/mixins/data.pypackages/client-python/src/rocketride/mixins/events.pypackages/client-python/src/rocketride/mixins/store.pypackages/client-python/src/rocketride/types/__init__.pypackages/client-python/src/rocketride/types/client.pypackages/client-python/src/rocketride/types/dashboard.pypackages/client-python/src/rocketride/types/task.pypackages/client-typescript/src/client/account.tspackages/client-typescript/src/client/client.tspackages/client-typescript/src/client/exceptions/index.tspackages/client-typescript/src/client/types/client.tspackages/client-typescript/src/client/types/dashboard.tspackages/client-typescript/src/client/types/task.tspackages/shared-ui/src/components/tokens/Tokens.tsxpackages/shared-ui/src/modules/account/components/BillingPanel.tsxpackages/shared-ui/src/modules/server/MonitorView.tsxpackages/shared-ui/src/modules/server/components/ConnectionsTab.tsxpackages/shared-ui/src/modules/server/components/OverviewTab.tsxpackages/shared-ui/src/types/shell.ts
🤖 Internal: Discord sync markerAuto-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>
There was a problem hiding this comment.
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 winRemove the
sleep(0)yield points around subprocess creation. These widen a race wherestop_task()can see_engine_processasNone, 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 winRegenerate the client-typescript API docs for
identifyandgetDashboard. These public signatures are present inpackages/client-typescript/src/client/client.ts, butpackages/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 liftDrop-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_qmixes 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
📒 Files selected for processing (19)
packages/ai/src/ai/modules/task/commands/cmd_cprofile.pypackages/ai/src/ai/modules/task/commands/cmd_monitor.pypackages/ai/src/ai/modules/task/task_conn.pypackages/ai/src/ai/modules/task/task_engine.pypackages/ai/src/ai/modules/task/task_metrics.pypackages/ai/src/ai/modules/task/task_server.pypackages/ai/src/ai/web/metrics/metrics.pypackages/ai/src/ai/web/metrics/process_reporter.pypackages/ai/tests/ai/account/test_account_helpers.pypackages/ai/tests/ai/common/dap/test_transport_stdio.pypackages/ai/tests/ai/web/metrics/test_process_reporter.pypackages/client-python/src/rocketride/account.pypackages/client-python/src/rocketride/mixins/data.pypackages/client-python/src/rocketride/mixins/events.pypackages/client-python/src/rocketride/mixins/store.pypackages/client-python/src/rocketride/types/dashboard.pypackages/client-typescript/src/client/client.tspackages/client-typescript/src/client/types/dashboard.tspackages/shared-ui/src/modules/server/components/OverviewTab.tsx
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-requestRequestContextthreading. The subsystems below are interdependent and reviewed together.The
aiunit-test suite is migrated to the refactors below and is green:./builder ai:test→ 1329 passed, 0 failed.1. DECOUPLING APPS FROM AUTH (the
AccountInfodiet)What changed
AccountInfono longer carriesapps: list[AppManifestEntry]orcredits: dictAccountInfodoes carry a slimsubscriptions: 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 toConnectResultviato_connect_result()and is re-sent on everyapaext_accountpush, so cachedgetAccountInfo()stays fresh.ConnectResult(wire format) dropsapps/creditsand gainssubscriptionsin Python and TypeScript SDKsServerInfoResultdropsapps— the probe now returns a singlehomeapp entryAccountInfo.to_pod_context()returns a slim dict for inter-pod forwarding (now includeswaitlisted)rrext_account_me?subcommand=desktopreturns the user's desktop apps on demandaccount.get_desktop()/account.getDesktop()AppStatusenum +isActiveStatus()/ACTIVE_APP_STATUSES(subscribed/trialing/free) promoted into the TS SDK (mirrored in Python), reused by the gate, SettingsPage, anduseSubscriptionsWhy 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 everybroadcast_account_updatere-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
AccountInfois the lightweightsubscriptionsstatus 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 existingapaext_accountpush instead of a separate fetch.OSS impact
OSS
authenticate()no longer buildsapps=[...]; it now populatessubscriptionsas all apps'free'(an active status), so every subscription check passes on OSS without per-componentsaascapability branches. The probe returnshome: self._home_app. The previously-NotImplementedErrorOSSdesktopsubcommand is now implemented, returning all apps withonDesktop=trueso 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.ts2. APP REGISTRY (shell-ui architecture)
What changed
apps/shell-ui/src/hooks/AppRegistryContext.tsx— single source of truth for registered appsWorkspaceProviderno longer accepts anappsprop; reads fromuseAppRegistry()useSubscriptionsreads subscription status fromAccountInfo.subscriptions(reactive onapaext_account), not from the registry. The registry keeps only desktop membership (onDesktop) + the MFentry— 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.mergeAppssplits full vs lean incoming entries: full entries (with an MFentryURL) register as remotes; a leanapaext_desktopmembership push (id +onDesktoponly) now patchesonDesktoponto the existing registry row by id instead of being dropped by theentryfilterAppRegistryProvideranduseAppRegistryfor downstream consumersWhy it changed
Previously the app list was assembled from three sources (probe,
ConnectResult.apps, runtime merges) via a fragileuseMemoin Shell.tsx, with races between probe results and post-auth merges. The registry centralizes membership; entitlement moves to the authoritativeAccountInfo.subscriptionsmap so the two concerns don't fight.OSS impact
OSS shell behavior is unchanged functionally —
rocketride.hellois registered from probe, and after auth the desktop fetch returns all OSS apps. Custom shell consumers that importedWorkspaceProviderwith anappsprop must wrap withAppRegistryProviderinstead.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.ts3. DEEP LINK GATE + BRANDED LOADING
What changed
LoadingScreengains app-branded (icon + name + spinner) and deep-link-pending (spinner only) modesDeepLinkGatewrapsShellLayout— 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.)rrext_public_catalogbefore starting authhideAppSwitcheris true (vanity domain mode)Why it changed
Deep-linking via
?appId=someApppreviously 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.tsx4. DESKTOP UPDATE EVENT CHANNEL
What changed
apaext_desktopDAP events (separate fromapaext_account) for "app added/removed from my desktop" — carries desktop membership only, not subscription statusConnectionManagerroutes it toshell:desktopUpdate; VSCode does the same andDeployManagerforwards itShellConnectionEventMapgains theshell:desktopUpdateentryWhy it changed
Previously any desktop change triggered a full
broadcast_account_update(re-auth, rebuildAccountInfo, push to all connections). The dedicated event sends only the desktop delta, and only to the affected user. Subscription changes stay onapaext_account; desktop membership ridesapaext_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.ts5. REQUEST CONTEXT (
ctx) THREADINGWhat changed
RequestContextdataclass:account_info,conn_id,sourceDAPConn.on_receive()is factored into three overridable hooks —_track_message(per-message bookkeeping),_pre_dispatch_gate(returnsFalseto reject), and_build_ctx(per-requestRequestContext) — with behavior-preserving base implementations.TaskConnoverrides only those three (auth gate, inbound-metric counters, pod-_ctxextraction) instead of copy-pasting the whole dispatch method.DAPConngainshas_permission(),verify_permission(),verify_auth()(moved fromTaskConn)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_inforeferences in handlers replaced withctx.account_infoget_task_control_by_token()+TaskServer.verify_task_access(control, ctx, ...)Permission-model consistency (post-review fixes)
resolve_team_permissionsnow honors the samesys.admin/internalbypass asresolve_task_permissionsbefore 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 forwardswaitlisted, so a forwarded ctx can't deserializewaitlisted=Falseand slip a waitlisted user pastverify_authin pod mode._build_ctxguards the forwarded-_ctxaccount lookup (raw_ctx.get('account_info')), so a malformed/pre-auth forward yieldsaccount_info=Noneinstead of aKeyErrorthat escapeson_receiveand silently drops the message.Why it changed
In the pod architecture the caller's identity arrives via
_ctxinjected by the orchestrator; reaching forself._account_info(the orchestrator's service credential in pod mode) was wrong. Threadingctxmakes the caller identity explicit regardless of topology. For OSS (single-process)ctxis 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:
dap_conn.pyhas the only_call_methodoverride inai; it callsmethod(message, ctx)and every resolvable handler accepts(request, ctx)(cprofile/process handlers defaultctx=None).dap_base.pybase still callsmethod(message); the message-onlyon_*methods all live onDAPClientsubclasses 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 customDAPConnsubclasses / 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 allcmd_*.pyfiles6. EVENT BROADCAST — ordered per-connection delivery (post-review fix)
What changed
broadcast_task_eventno longer spawns an unretainedasyncio.create_task(_send())per connection. Each connection now owns a bounded outboundasyncio.Queue+ a lifelong drain task; broadcast doesput_nowaitonto each subscribed connection's queue and returns O(1).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.py7. METRICS OVERHAUL (subprocess self-reporting)
What changed
task_metrics.py: removed allpsutil/pynvmlparent-side sampling; the class is now billing-onlyProcessReporter(packages/ai/src/ai/web/metrics/process_reporter.py) — daemon thread inside the subprocess sampling CPU (getrusage/psutil), RSS, GPU VRAM (pynvml)>MET*for live dashboard metrics,>USG*for billing>USG*is now emitted both at object boundaries (taskhook) AND periodically (CONST_BILLING_REPORT_INTERVAL = 15s) fromProcessReporter, 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 0avg_*task-metric fields (never written, consumed nowhere) from the SDK types, vscode types, and the engine's zeroingtransport_stdio.py:>USG*handler parses billing payload →apaevt_status_tokensmetrics.py:add_time()→add_value(); newset_value(); report format{timers, counters, events}→{values, events}TASK_TOKENSmodel: fixed fields →ConfigDict(extra='allow')(dynamic keys)task_server.pyCONST_METRICS_SAMPLE_INTERVAL/CONST_METRICS_STOP_TIMEOUTremoved;CONST_PROCESS_REPORT_INTERVAL = 1.0addedWhy 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→ flatvalues) is a wire-level breaking change; code consuming raw>MET*orTASK_TOKENSfields 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.ts8. DASHBOARD PAGINATION + RELOCATION
What changed
on_rrext_dashboardremoved fromcmd_misc.pyand rebuilt incmd_monitor.pywith real pagination, sorting, and state filtering (the previous version ignoredrequest['arguments']entirely and returned an unpaginated dump withtasks_total = len(returned)){ tasks: { offset, limit, sort_by, sort_order, state_filter }, connections: { ... } };state_filtermapsrunning/completedto task states; totals are computed on the filtered list before slicing;limit:0omits the sectiontasks/connectionsoptional;tasks_total/connections_totaladded;DashboardOverviewgainscompletedTasks,totalTasks,eaasNodesgetDashboard()and Pythonget_dashboard()accept the pagination params; PythonDASHBOARD_*types mirror the TS shape?? []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 assumedtasks/connectionsare 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.tsx9. SDK HARDENING (DAPException, transport guards, auto-chunking, path validation)
What changed
DAPExceptionreplacesRuntimeError/Errorfor failed DAP calls; carriesfile/linenofrom the server tracesend(),request(),disconnect())attach()fix: desired-state upgrade-only (prevents downgrade fromauthenticatedtoattached)DataPipe.write()auto-chunks at 512KB in both SDKsvalidateStorePath()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
RuntimeErrorlost the server-side trace;DAPExceptionpreserves it. Transport guards prevent crashes during reconnection races. Theattach()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 RuntimeErrorstill catchesDAPException(subclass) in Python. Auto-chunking and theattach()/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.ts10. STORE PROVIDER LAZY IMPORTS
What changed
store_providers/__init__.pyno longer eagerly importsFilesystemStore/MemoryStore/S3Store/AzureBlobStore— just a docstring explaining the lazy-import rationaleStoreCommands(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 S3Storebreak; usefrom 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.py11. MISCELLANEOUS
globalThisviaSymbol.for()to survive MF module duplicationshell:loginRequest/shell:subscribebuffered if no listener and replayed on registrationStore.create()called in__init__/start()instead of lazy first-accessmake_device_lock()replacesthreading.Lock()in 6 vision models —nullcontext()in proxy mode, since the lock is only needed for local GPU access[TIMING]perf-counter blocks intask_engine/task_server, and[on_rrext_monitor]/[set_monitor]argument/permission dumps incmd_monitorTaskMetrics.pidparam (stored, never read)PYTHONASYNCIODEBUG=1block (ai/__init__.py) andloop.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 shipBREAKING CHANGES
All rows below are changes introduced by this PR (against
origin/develop).ConnectResult.appsremovedconnectResult.appsafter authclient.account.getDesktop()/get_desktop()ConnectResult.creditsremovedconnectResult.creditsServerInfoResult.appsremovedappsfor pre-auth catalogrrext_public_catalog; probe returns singlehomefieldConnectResult.subscriptionsaddedsubscriptions[appId](∈subscribed/trialing/free= active)on_*handler signatures →(request, ctx)DAPConnsubclasses / command mixins overridingon_*ctxto the overriding handler; fails loud (TypeError)metrics.add_time()→add_value()metrics.add_time()add_value(){timers, counters}→{values}>MET*/>USG*output orTASK_STATUS.tokensfields by namevalues;TASK_TOKENSfields are dynamicTASK_TOKENSfixed fields → dynamicextra='allow'.cpu_utilization/.cpu_memory/.gpu_memoryby nametasks/connectionsnow optional{ tasks, connections }without null checks?? []/.get('tasks', [])DashboardOverviewnew fieldsDashboardOverview(unlikely for consumers)completedTasks,totalTasks,eaasNodesstore_providers/__init__.pyno longer re-exports backendsfrom store_providers import S3Storefrom store_providers.s3 import S3StoreWorkspaceProviderno longer acceptsappspropappsAppRegistryProviderget_task_control()→get_task_control_by_token()verify_task_access()for permission checksRISK ASSESSMENT BY AREA
apps/creditsreturnundefined/KeyErrorrather than erroring at the call site. The newsubscriptionsmap is additive.ctxthreading>USG*change what gets billed (verify billing totals).AccountInfo.subscriptions).appsRefsynchronous update mitigates ensureApp/desktop-fetch/switchApp races; the desktop-fetch dedupe + deep-link gate timeout close the remaining ones.DAPException extends RuntimeError; transport guards + path-validation reject are strictly defensive; auto-chunking transparent.NOTES FOR THE REVIEWER
>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.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