Skip to content

About

GenClass Runtime: a 10 MB in-browser model that detects race conditions, stale responses and duplicate requests in web apps; opt-in guard mode can prevent some of them. npm i @genclass/runtime

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

7 stars

Watchers

0 watching

Forks

 
 

Latest commit

 

History

642 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

GenClass Runtime

npm CI license website

Website and docs: genclass.dev · Discord · Docs · Benchmarks · Pricing

A runtime for web apps. It watches the app from the inside and uses a small local model to flag, and optionally prevent, stale responses, races, duplicate requests, inconsistent state and failure storms.

One line, first in <head> or first in your entry file:

<script src="https://cdn.jsdelivr.net/npm/@genclass/runtime@0.3.0-beta.1"></script>
import "@genclass/runtime/auto"; // after npm install @genclass/runtime@beta

Use the newest line, 0.3.0-beta.1 (npm dist-tag beta): every npm and npx command here names @beta, and the script tag pins the version (the unversioned URL serves npm's latest, possibly an older release from the CDN cache). A bare npm install @genclass/runtime or npx @genclass/runtime init gets npm's latest, the older 0.2.x line, which has no token, dashboard, interaction analytics, visual checks or agent link, whose CLI has no login, connect, dashboard or --sink, and whose runtime ignores token and analytics: true without a word. Keep 0.2.x only for an app that cannot take a prerelease.

That covers the whole app: network, user actions, errors, timing, and the app's state (React component state, Redux / Redux Toolkit and Zustand devtools stores are discovered automatically; discovered React and Zustand state is observed only). Observe mode by default: it reports only. npx @genclass/runtime@beta init writes the line for you; GenClass.init() is the explicit setup (package README).

GenClass Runtime records what the app does: user actions, async operations and their causal chains, network traffic (fetch, XHR, WebSocket, EventSource), store writes with per-field versions, errors and timing. It computes generic facts about each write, request and response, and asks a small GenClass model only about salient situations. The model runs in the browser (WebGPU or WASM, in a Web Worker). It answers two questions: what is happening, and which available action is best. There is no list of known bugs in the code.

Status: @genclass/runtime@0.3.0-beta.1, the newest line (npm beta dist-tag: npm install @genclass/runtime@beta; npm's latest is still the older 0.2.x, the exception for apps that cannot take a prerelease) with model @genclass/runtime-model@0.2.0. Observe by default; guard is opt-in; heal is experimental. Measured false-intervention and recall rates are under Measured behaviour. AI coding agents: start at AGENTS.md.

Install

Pick one; all of them start the same runtime in observe mode unless you choose otherwise. Full install guide, with init flags, page configuration and the script-tag attributes: package README.

npx @genclass/runtime@beta init     # finds the framework, adds the import, shows the diff first
npm install @genclass/runtime@beta  # then: import "@genclass/runtime/auto" (or GenClass.init())

To audit any page for visual bugs without adding anything to it: npx @genclass/runtime@beta scan <url> (alias audit; a score and each problem with its element and a suggested CSS fix; Audit a page). Coding agents get the same as genclass_scan in @genclass/mcp.

The package is ESM only (no require). Jest users need transformIgnorePatterns that lets @genclass/runtime be transformed.

What you will see in the first two minutes

  • One console line saying anonymous diagnostics are on ([GenClass] Sends anonymous diagnostics (decisions and counts, no page text) to improve the model. Opt out: …); since 0.2.2 it does not appear on localhost or other local hosts (0.2.1 sends and shows it there too, by mistake).
  • [GenClass] Model ready (genclass-runtime-r17, wasm, q8, 0.4s). Mode: observe. once the model has loaded at idle.
  • Then possibly nothing for a while. Observe mode reports only salient situations (a response landing over newer data, a repeat, a failure streak, a broken relation, an error), and the first response of each kind after a page load is never a delivery decision: the runtime first has to see what that request writes.
  • The devtools overlay: Alt+Shift+G on localhost (the init command loads it in development only).
  • The first-visit cost: about 13 MB downloaded from jsDelivr on WASM (model 10 MB plus ONNX Runtime's wasm) or 18 MB on WebGPU, cached afterwards, and 190–280 MB of memory in a Web Worker while the model is loaded. Devices that report 2 GB of memory or less, and Save-Data connections, do not load the model by default (they observe without it and say so in one console line; new in 0.3.0: 0.2.x loads everywhere and only lazily on Save-Data). For phones and strict Content-Security-Policies see self-hosting and loadIf in the package README.

New: your coding agent can see the race, replay it, and prove the fix

@genclass/mcp (unreleased) is an MCP server for Claude Code and other coding agents. Your app's development build connects to it with one line, and the agent gets every click, request, the order the responses arrived and what they wrote, uncaught errors with the click and request behind them, and the on-device model's detections. It can run your steps again with the network answering in different orders (and slower) to find what depends on timing, replay the exact order that broke, and write a Playwright test that fails until the fix is in. Everything stays on your machine.

claude mcp add genclass -- npx -y @genclass/mcp@0.1.0
import { GenClass } from "@genclass/runtime";
import { connectAgent } from "@genclass/runtime/agent";

const rt = GenClass.init();
if (import.meta.env.DEV) connectAgent(rt); // development builds only

On a real page in headless Chromium, it found a stale-results race in 5 of 6 reordered runs of a page that never showed it on the developer's fast backend, with no false report on the fixed page (AGENT.md, RESULTS.md section 16).

New in 0.3.0: app dashboards and protect()

In 0.3.0-beta.0 (@beta on npm): 0.2.x ignores token and has no protect() export (an import of it fails), and its init creates no token. Details: CHANGELOG, 0.3.0-beta.0.

import { GenClass, protect } from "@genclass/runtime"; // 0.3.0

GenClass.init({ token: "gc_your_app_token", scope: "functions" }); // scope: decide only inside protect()ed functions
// what GenClass finds here (a double submit, overlapping calls, a failure) is reported under "checkout submit"
export const submitOrder = protect("checkout submit", (cart) => fetch("/api/orders", { method: "POST", body: JSON.stringify(cart) }));
  • Token and dashboard. A token (gc_ plus 22 letters and digits, public) groups your app's diagnostics under a private dashboard link (https://genclass.dev/dashboard/...). The dashboard is free for every app, with or without an account. Get one at genclass.dev/start, or let init create it after you approve its diff. Or keep the dashboards in an optional genclass.dev account: npx @genclass/runtime@beta login signs in from the terminal (device flow) and connects the app in the current folder; connect, whoami and logout do the rest. The dashboard fills from deployed pages with telemetry on. Local and private hosts (localhost, *.local, *.test, private networks) do not send by default; telemetry: true or any telemetry options object (telemetry: { ... }) is an explicit opt-in that sends from them too, with the token. Data is kept for 90 days.
  • protect(name, fn) returns a function with the same signature (same this, arguments, return value and errors); decisions about what it causes carry fn: name. Without a runtime it just calls fn. Package README.
  • More sensitive observe mode. Observe also reports possible detections (below the report gate, at the eager profile's value; one console.debug line each, ending in (possible); your rt.on("detect") and report function get them only with detect: "possible"; detect: "likely" turns them off), and decides what happens while the model loads or the tab is hidden once it can, instead of skipping it, reporting those at the possible tier at most until their false-report rate is measured. Their telemetry detect events carry tier: "possible"; nothing new about the page is sent. Package README.

How it works

  1. Observe. Fetch, XHR, WebSocket, EventSource, DOM events, errors, navigation, storage, long tasks and timers are instrumented. Async work is linked into causal chains. Stores created through GenClass, or wrapped with its Redux, Zustand, React or generic adapters, get per-field versions and write histories.

  2. Learn. The runtime learns per-operation baselines (latency, failure rate, request rate) and which fields each operation usually writes. It also learns cadences (polling, debounced saves) and relations that keep holding (total == sum(items[*].price × qty), selectedId ∈ items[*].id).

  3. Triage. Cheap facts are computed for every write and request. Only salient situations reach the model: a response landing over newer data, a repeated request, a failure streak, a broken relation, an error.

  4. Decide at the network boundary. Responses and push messages are judged before the app sees them:

    • deliver (passive);
    • discard (deliver, but drop the writes it makes over newer data);
    • defer (wait for related in-flight work).

    Store writes are never held or reordered by default. Salient writes that no delivery decision covered are decided in the background, and can be reverted within 2 s under strict rules.

  5. Act by mode.

    mode behaviour
    observe (default) reports only; never holds, delays or changes anything
    guard (opt-in) discard, defer, coalesce, delay, only when the model's gate for that trigger says acting beats doing nothing by the margin of the chosen aggressiveness profile
    heal (experimental) also retry, serve_cached, block, hedge, rollback, resync and custom actions, at the heal margin of the profile

    Detections are reported when the top diagnosis is not expected with probability at or above the profile's report threshold (0.95 / 0.90 / 0.70 for cautious / balanced / eager). rt.gates() shows what is in force.

  6. Explain.

    • Every detection and intervention is logged in plain English, with the exact situation text the model read.
    • Reversible actions can be undone.
    • ?genclass=off installs nothing.

Measured behaviour

@genclass/runtime@0.3.0-beta.1 with the model @genclass/runtime-model@0.2.0. The model and the situation text it reads (situation-v2.3) are the same as in 0.2.x; the numbers below were measured with them.

  • Runtime: works and is unit-tested (situation format tag situation-v2.3). Install with npx @genclass/runtime@beta init, one import (@genclass/runtime/auto) or one script tag.

  • Default mode is observe: the model diagnoses salient situations and the runtime reports likely problems, without changing what the app does (it never holds or delays a response). guard is opt-in; heal is experimental.

  • How eager it acts once you opt in: aggressiveness: "cautious" | "balanced" (default) | "eager".

  • Model: genclass-runtime-r17 2.0.0-rc4t (r17-v2dT, gain gate, 10 MB (10.2 MB q8), about 180 ms per decision on one WASM thread; WASM or WebGPU), loaded from jsDelivr at idle and cached. On held-out data (RESULTS.md §1):

    profile guard FIR guard recall (clear / real) heal FIR heal recall (clear / real) report threshold
    cautious 0.005% 2.4% / 1.4% 0.26% 3.7% / 3.8% 0.95
    balanced (default) 0.13% 7.8% / 7.5% 0.59% 7.1% / 10.4% 0.90
    eager 0.54% 14.2% / 21.0% 1.84% 17.1% / 24.2% 0.70

    Our targets are guard FIR ≤ 0.1% and heal FIR ≤ 0.5%. cautious stays under both; balanced is slightly over both (0.13% and 0.59% on simulated apps); eager is well over. FIR on held-out real apps was 0.00% for every profile. Recall is still low: at balanced, guard acts on under 8% of the cases where acting would help. Installed from the registry into a fresh app, guard mode fixed an out-of-order typeahead in 6 of 6 trials, and clean typing made 0 model calls. See the model card.

  • Options (activation, route scopes, protected endpoints, breaker, shadow, veto, action limits, sinks): OPTIONS-SPEC.md.

  • Privacy notice: anonymous diagnostics are on by default, without page text, and off on localhost (since 0.2.2; 0.2.1 sends from localhost too, by mistake); opt out with GenClass.init({ telemetry: false }) or ?genclass=no-telemetry. See Privacy and telemetry.

Privacy and telemetry

  • Decisions are local. Situations are built and decided in the browser by the local model; nothing is sent anywhere to make a decision.
  • Anonymous diagnostics are on by default with GenClass.init() in a browser, except on local and private hosts (localhost, *.localhost, *.local, *.test, 127.0.0.1, ::1, private IP ranges) (0.2.1 sends from local hosts too, by mistake; fixed in 0.2.2, see the CHANGELOG), in Node/SSR, and in createRuntime(). They go to the GenClass maintainers' collector (telemetry-worker/, a Cloudflare Worker writing to a private R2 bucket) and are used to improve the model.
  • By default no text from your page is sent. Sent: a random per-page session id, versions, mode, device class, the app's hostname and id-normalised path (from 0.3.0 a segment with an @, such as an e-mail address, is :id too), and per decision (every detection and every decision whose diagnosis is not expected, every action and veto; of the routine ones, one in four and at most 200 per page load; counts are exact; 0.2.2 and earlier send every decision) the trigger, the diagnosis and its confidence, the action and gate, the outcome and latency. The collector stores no IP address or user agent. One console notice per page says it is on.
  • The measured visual checks send nothing (new in 0.3.0): their findings stay in the page (the console, rt.checks(), your own sinks).
  • The situation text the model read is sent only if you opt in with GenClass.init({ telemetry: { include: { situation: true } } }). It is redacted by field meaning, but can still contain route names with query strings, short error messages and short field values; read TELEMETRY.md before enabling it.
  • Opt out: GenClass.init({ telemetry: false }), ?genclass=no-telemetry, localStorage.setItem("genclass.telemetry", "off"); browsers sending Global Privacy Control are never collected.
  • Interaction analytics are off unless the app turns them on (analytics: true with the app's token, new in 0.3.0-beta.1): per page view the route, engaged time, scroll depth, and clicks, rage clicks and dead clicks per element, the element named from markup, never its text. No visitor id, nothing stored on the device, every opt-out above applies, and they never feed a decision. Where analytics need consent (EU, EEA, UK), start them after consent with runtime.analytics.start(). See TELEMETRY.md.
  • With a token (0.3.0: token option, data-token, token= in the meta tag), each batch also carries your app's public token, so the same diagnostics also show on your app's private dashboard (whoever holds its link can open it), and decisions about protect()ed functions carry the function's name (fn, the label you chose). Without a token neither is sent; without telemetry, a token does nothing.
  • Possible detections (0.3.0, observe): their detect events carry tier: "possible" (a likely detection has no tier, as before), and the page-load summary counts them apart from detections (possibleDetections). They say which tier a detection was in; nothing new about the page is sent.
  • The CLI contacts genclass.dev only for the command you run: init creates a dashboard only after you approve its diff (it sends the name from package.json); login asks for a sign-in code with client_name: "genclass-runtime <version>" (shown on the approval page and in your account's session list) and polls until you approve it; whoami and connect read your account's apps; connect sends a new app's name, or an existing dashboard's link from .genclass.local to add it to your account; logout ends the CLI session. The account requests carry user-agent: genclass-runtime-cli/<version>. What an account stores: PRIVACY.md.
  • Apps that ship GenClass may need to disclose this to their users (GDPR/CCPA). Full schema, storage and guidance: TELEMETRY.md.

Security

  • SECURITY.md: how to report a vulnerability, supported versions.
  • packages/runtime/INTERCEPTION.md: every browser API GenClass wraps or listens to, what each mode may change, what it never does, every action's preconditions and undo, and how disable() and ?genclass=off restore the page (checked against the code by a unit test).
  • docs/runtime/THREAT-MODEL.md: page scripts, the model supply chain, crafted responses, denial of service, telemetry and state discovery, with mitigations and residual risks.
  • rt.audit() exports a structured record of every decision and action; payment, checkout and sign-in endpoints should stay observe-only (requests: { protect: protectPreset("payments", "auth") }).

Full guide (modes, triggers, adapters, observability, performance, privacy, known limitations): packages/runtime/README.md. API: docs/runtime/API.md.

Results so far

All numbers are on held-out data, with each recall reported next to its false-intervention rate (FIR). Source: docs/runtime/RESULTS.md.

what result
Round 1 model, R17 (9.6 MB int8; previous format situation-v1), simulated apps diagnosis 90.5%, action 81.9%, guard FIR 0.05%, heal FIR 0.24%, calibration error 0.009. Recall on clear stale/duplicate cases is only 7.7% (precise but timid).
Why round 1 was timid (sim/SEPARABILITY.md) Many clear cases had benign twins with identical visible facts; 24% of clear rows were mislabelled expected; labels assumed knowledge a runtime cannot have. v2 adds measured facts and fixes the labels.
r17-v2dT (situation-v2; shipped as @genclass/runtime-model@0.2.0) Gain gate with three profiles (table above). balanced: guard FIR 0.13%, heal FIR 0.59% on simulated apps (slightly over the 0.1% / 0.5% targets); cautious: 0.005% / 0.26% (under). 0.00% on held-out real apps for every profile. Calibration error not yet measured for this model (r17-v2b: 0.021–0.023).
r17-v2b (shipped as @genclass/runtime-model@0.1.0) Simulated apps: diagnosis 84.2%, action 77.8%. Real apps: diagnosis 83.6%, action 80.0%. Guard FIR 0.01%, heal FIR 0.07%; heal acts on 5.9% of clear simulated cases and 5.7% of actionable real-app cases; guard on 0.6%.
Training continues teacher model, distillation, DAgger rounds; ~10M simulated gold rows, ~50M unlabeled rows, ~0.6M real-app rows
Never make a correct app worse (always-passive model, heal vs observe, 66 real apps × 6 seeds) 0/396 clean runs changed: final page text (inputs and alerts excluded) and server state. Request timing and store contents are not compared, nor is observe mode against no runtime. With chaos: 3/198 changed.
Same check on the v1 runtime (store-write holds) 4/198 clean runs changed; the React/Redux RealWorld app never rendered its home feed
Demos (model 0.2.0, balanced, 10 chaos + 5 clean seeds per demo and mode) The chaos bug rate over the six demos was 72% off, 70% observe, 72% guard and 73% heal (differences under 5 points are noise): nothing is healed at the shipped gates, because candidate gains rarely clear the margins; 0 false interventions on clean runs.

What's in this repo

path what
packages/runtime @genclass/runtime, the library. Observers, causality, stores and adapters, learned baselines/relations/profiles, facts, triage, policy gate, actions, model host (Web Worker, ONNX Runtime Web on WebGPU/WASM), devtools overlay. README · CHANGELOG
packages/mcp @genclass/mcp (unreleased), the MCP server for coding agents: pages connect through @genclass/runtime/agent; the agent gets findings in causal order, replays, explore verdicts and generated regression tests (AGENT.md). README
packages/playwright @genclass/playwright (unreleased), GenClass for Playwright: a fixture that puts the runtime on every page under test (observe mode, seeded network chaos with page.route, the model skipped by default) and writes one race file per test, and a reporter that turns those files into one markdown report for the pull request, with the evidence line and what reproduced across seeds. README
packages/nuxt, packages/astro @genclass/nuxt and @genclass/astro (unreleased): the runtime as a Nuxt module and as an Astro integration, one line in the framework's config, with options for the mode, the token, telemetry and the devtools overlay; browser only. Nuxt README · Astro README
packages/runtime-model @genclass/runtime-model, the default model package (data only; files/ is gitignored and filled at release time) and its model card
sim Training-data simulator: random apps run on the real runtime in a deterministic virtual world, labelled by counterfactual outcomes
realapps 128 real apps (written for the corpus, plus open-source ones) run with the real runtime in headless Chromium, labelled the same way; the never-worse sweep covered the first 66. README
training Vocabulary pruning, curriculum (curriculum/rt.py mirrors the runtime's renderer), multi-node CPU training on Azure, int8 ONNX export, evaluation (EVAL.md)
demos Six demo apps (typeahead, autosave, checkout, flaky dashboard, live kanban, real-time decisions) with a Service Worker chaos backend and Playwright trials; bench/heal runs them per mode, bench/visual measures the visual checks on them
compat, templates Framework compatibility matrix (7 apps, 15 data layers, every mode; RESULTS.md) and eight starter templates with the one line (React + Vite, Next.js, Vue + Vite, SvelteKit, Astro, Nuxt, React Router, Angular; templates/README.md)
docs/runtime Results · Coding agents · Architecture · Build contract · API · Threat model
telemetry-worker, site The diagnostics collector (Cloudflare Worker) and the genclass.dev site
research Earlier GenClass/Jev research (voice-control Chrome extension, the jev_local encoder and server, classifier benchmarks); the training scripts still use jev_local (README)
AGENTS.md, CONTRIBUTING.md How to work in this repository

Development

Node ≥ 20. From the repo root:

ONNXRUNTIME_NODE_INSTALL=skip npm ci   # the root lockfile is committed; skip onnxruntime-node's CUDA download
npm run typecheck -w @genclass/runtime
npm run build -w @genclass/runtime
cd packages/runtime
NODE_OPTIONS=--expose-gc npx vitest run --exclude "test/browser/**" --exclude test/review-perf.test.ts
NODE_OPTIONS=--expose-gc npx vitest run test/review-perf.test.ts --retry=2   # timing budgets, run alone

The same steps run in CI (.github/workflows/ci.yml, Node 22) on pushes to main, runtime, mvp and mvp-v2, on pull requests, and on manual dispatch. 667 unit tests pass in CI (14 skipped: the model-parity tests, which need GENCLASS_MODEL_DIR), plus the 4 perf tests, which time a 5,000-item store and run in their own step because they can fail under parallel load.

Releases are published by .github/workflows/release.yml from a v* tag, with npm provenance.

Not covered by CI:

  • Playwright browser tests: npm run test:browser in packages/runtime.
  • @genclass/playwright: its typecheck, build, unit tests and the fixture's end-to-end check in Chromium run in their own workflow, .github/workflows/playwright-reporter.yml, on pushes and pull requests that touch the package; it never blocks ci.yml.
  • The npm tarball smoke test: test/smoke/smoke.sh.
  • The sim, the real-app corpus, the demos' evaluation, and training.

These are heavier or need Azure; see AGENTS.md before running them.

The training format is tagged. The current tag is situation-v2.3; the shipped model was trained on data from situation-v2. Any change to packages/runtime/src/situation/* (or other text the model reads) changes the model's input and means a new tag and regenerated data (training/curriculum/rt.py mirrors the renderer and must change with it).

License

Apache-2.0. See NOTICE.

About

GenClass Runtime: a 10 MB in-browser model that detects race conditions, stale responses and duplicate requests in web apps; opt-in guard mode can prevent some of them. npm i @genclass/runtime

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

7 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages