Skip to content

Rl analytics#5538

Open
ihh wants to merge 52 commits intoGMOD:mainfrom
ihh:rl-analytics
Open

Rl analytics#5538
ihh wants to merge 52 commits intoGMOD:mainfrom
ihh:rl-analytics

Conversation

@ihh
Copy link
Copy Markdown
Member

@ihh ihh commented Apr 8, 2026

Code Review Reference: jbrowse-plugin-rl-analytics

Purpose

Captures user interactions with a JBrowse 2 session as RL-compatible (state, action, reward, next_state) tuples and exports them as JSONL or streams them to a webhook. (reward is a dummy zero field included for compatibility with RL training environments.) Designed to enable training and evaluation of agents that operate JBrowse on behalf of users.

The commit history includes some abandoned attempts at a gamified tutorial for Mechanical Turks
(turns out to be much harder than I anticipated to make this polished and smooth).

Architecture

session ──onAction──▶ ActionListener ──▶ ActionBuffer (debounce)
                                               │
                                               ▼
                                       EpisodeManager
                                       (StateEncoder)
                                               │
                       ┌───────────────────────┴───────────────┐
                       ▼                                       ▼
                 JSONLExporter                          WebhookExporter
                 (delta-encoded)                        (batch + interval)

Source layout (src/)

File Role
index.ts Plugin entry. Installs middleware on the session, wires components, exposes getExportManager() / getEpisodeManager() / getActionListener() for tests via a module-level WeakMap<Plugin, refs>.
config.ts Six config slots, all consumed via readConfObject: enabled, webhookUrl, webhookBatchSize, webhookFlushIntervalMs, inactivityTimeoutMs, maxEpisodes.
ActionLogger/ActionListener.ts MST addMiddleware listener. Filters sub-actions via parentActionEvent. Maps MST action names → semantic ActionType via ACTION_MAP. Extracts per-action metadata in extractMetadata().
ActionLogger/ActionBuffer.ts Debounces same-type actions in a sliding window. Calls onDebouncedAction with the merged event.
ActionLogger/ActionTypes.ts ActionType enum + ClassifiedAction shape.
RLPipeline/StateEncoder.ts Reads MST view getters → BrowserState. Memoizes within a frame keyed on (bpPerPx, offsetPx, numTracks, firstRegion, lastActionTimestamp). Tracks action counts and unique refNames visited. encode() returns a fixed 21-dim numeric vector.
RLPipeline/EpisodeManager.ts Builds Step records from prev/next BrowserState, manages episode lifecycle, inactivity timeout (dynamic check interval = clamp(1s, 30s, timeout/2)), bounded ring buffer of completed episodes via maxEpisodes.
RLPipeline/types.ts BrowserState, Step, Episode types.
Export/ExportManager.ts Holds the JSONLExporter and (optional) WebhookExporter; exposes getJSONL() for tests/UI.
Export/JSONLExporter.ts Serializes episodes to JSONL. Delta-encodes next_observation against observation to reduce file size.
Export/WebhookExporter.ts Buffered POSTer with batch-size and interval triggers. Restores buffer on fetch failure. Uses navigator.sendBeacon on dispose if available.
ObserverView/viewModel.ts The "Action Monitor" view model. Reactive log via observable.array<LogEntry> for O(1) push and stable React keys.
ObserverView/ObserverView.tsx The view UI. Uses MUI useTheme() for dark/light. requestAnimationFrame-coalesced auto-scroll.

Tests (src/__tests__/)

File Tests Coverage
ActionBuffer.test.ts Debounce window, merge semantics, flush on type change.
EpisodeManager.test.ts Episode lifecycle, inactivity timeout, maxEpisodes eviction, prevState caching.
StateEncoder.test.ts Shape, zoomLevel buckets, label visibility, track type flags, encode() vector dims, action count tracking, unique refName tracking, throwing-getter resilience, frame memoization (hit + invalidation).
WebhookExporter.test.ts Batch flush, interval flush, empty buffer, fetch-failure restore, sendBeacon on dispose, empty-URL disabled — all via mocked global.fetch.

Plus products/jbrowse-web/src/tests/RLAnalytics.test.tsx:
end-to-end integration through the real LinearGenomeView. Covers
MST onAction propagation, full pipeline → JSONL with enriched
state, prevState caching across steps, SHOW_TRACK capture, and
delta-encoded JSONL output.

Total: 35 tests across 5 files.

Key design decisions

  1. Middleware on session, not rootModel. Action capture is scoped
    to a single session; this avoids capturing assembly-loading and
    bootstrap actions that occur before the user takes control.

  2. Plugin state in a WeakMap. No mutable state on the plugin
    instance itself; refs (actionListener, episodeManager,
    exportManager) live in a module-level WeakMap keyed by the
    plugin object. Accessor methods (getExportManager() etc.) exist
    for the integration test.

  3. parentActionEvent filter. Suppresses sub-actions like
    scrollTo inside zoomTo, so only top-level user-initiated
    actions become Steps.

  4. Frame-level state memoization. StateEncoder.extractState
    returns the same object on repeat calls within a frame to avoid
    re-walking MST getters. Invalidated on the next animation frame
    or when any view input changes.

  5. Delta-encoded JSONL. next_observation is stored as the diff
    from observation, computed by shallow-equality on each
    BrowserState field. The Python converter
    (scripts/convert_to_gymnasium.py) reconstructs full
    observations.

  6. isAlive() checks on the observer view in
    getObserverView() to handle MST destruction races.

  7. Webhook export is fully optional and gated by config. If
    webhookUrl is empty the exporter short-circuits and never
    touches fetch.

Generated docs

docs/ACTIONS.md is auto-generated from ActionListener.ts by
scripts/generate_action_doc.mjs. It lists all 13 semantic action
types, the MST action names mapped to each, and the metadata fields
extracted per action. Regenerate with:

node scripts/generate_action_doc.mjs

Type safety

  • Zero as any in index.ts — uses AbstractSessionModel and
    AbstractViewModel from @jbrowse/core/util, plus a local
    RootModelWithSession interface.
  • Zero eslint-disable in index.ts.
  • The few as any casts in tests are for constructing minimal mock
    views/steps and are isolated to fixtures.

Metrics

  • Source: ~1100 lines of TypeScript across 12 source files
  • Tests: 5 files, 35 tests
  • UMD bundle: ~36 KB
  • Published tarball: "files": ["esm"] — excludes scripts/,
    docs/, dist/, tests
  • Dependencies: aligned with the monorepo (@jbrowse/core
    workspace, MUI ^7.3.8, mobx 6.15, mobx-react 9.2)

Known follow-ups (non-blocking)

  1. extractMetadata falls back to storing raw args for unknown
    action names (logOtherActions opt-in). If an unknown action
    passes MST node references, JSON serialization could fail. Safer
    to drop unknown args entirely.
  2. A generated Python dataclass for the JSONL BrowserState schema
    would let downstream RL training stay in sync with the plugin.
  3. crypto.randomUUID() requires Node 18+ / jsdom 20+. Fine on
    current CI; flag if the matrix widens.

ihh and others added 30 commits March 22, 2026 12:55
New plugin with three subsystems:
- Action Logger: MST onPatch() interception, action classification, debounced ring buffer
- RL Data Pipeline: state encoding, potential-based reward shaping, episode management
- Scavenger Hunt Widget: guided task system with validation, MTurk completion codes

Includes JSONL/webhook export, Python Gymnasium converter, and tests for
ActionClassifier, RewardCalculator, EpisodeManager, and TaskValidator.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Register plugin in jbrowse-web corePlugins and package.json
- Attach onPatch listener to session (not rootModel) to avoid
  interfering with view init processing
- Guard StateEncoder property access with try/catch for views
  that haven't been rendered yet (e.g. width not initialized)
- Wrap PatchListener callbacks in try/catch for resilience
- Use queueMicrotask for episodeManager.recordAction to avoid
  disrupting synchronous MST patch processing
- Only extract state from LinearGenomeView (skip Dotplot, etc.)

All 78 tests pass (36 plugin + 42 jbrowse-web integration).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add public accessors (getExportManager, getEpisodeManager, getPatchListener)
  to RLAnalyticsPlugin for testing and external integration
- New RLAnalytics.test.tsx validates full pipeline: MST patches → action
  classification → episode recording → JSONL export with volvox data
- Add volvox-specific scavenger hunt tasks (ctgA/ctgB coordinates)
- Remove debug logging from PatchListener

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Experiment 2: UMD bundle build (47KB)
- build_umd.sh uses esbuild + post-processing to create IIFE bundle
- All JBrowse deps resolved via window.JBrowseExports at runtime
- Icons gracefully degraded (null) since @mui/icons-material not in re-exports

Experiment 3: Local parasitic test infrastructure
- config_with_rl_analytics.json: volvox config with UMD plugin entry
- UMD bundle in test_data/volvox/ for dev server testing

Experiment 4: Config generator script
- generate_parasitic_config.mjs: fetches any JBrowse config, resolves
  relative URIs to absolute, injects RL analytics plugin entry

Experiment 6: Webhook receiver
- webhook_receiver.mjs: lightweight Node.js HTTP server for real-time
  analytics collection (POST /ingest, GET /status, GET /episodes)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Collapse multi-line imports before processing
- Convert 'import {X as Y}' to 'var {X: Y}' (destructuring syntax)
- Extract default export name from 'export { X as default }' pattern
- Add globalThis fallback for Node.js compatibility
- Validated: parses as valid JS, instantiates with mock JBrowseExports

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
umdUrl resolves relative to page root; umdLoc with {uri: "..."} resolves
relative to the config file's directory, which is what we need.

Validated in browser: 46 steps collected, 5 action types, full pipeline
through to Gymnasium .npz conversion confirmed working.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Route episode recording through debounced buffer (onDebouncedAction)
  instead of raw onAction — trackpad swipe bursts now merge into single
  actions (37 PAN_RIGHT -> ~5 debounced pans)
- Increase debounce window from 100ms to 500ms for trackpad momentum
- Fix track ID capture in classifier (check configuration string ref)
- Fix ScavengerHunt view validation: use getSession(model) to access
  the LinearGenomeView for position checking
- Fix generateCompletionCode MST action error: call setCompletionCode
  action from async .then() instead of setting volatile directly
- Remove RLAnalytics from corePlugins (load only via UMD for parasitic)
- Use umdLoc (relative to config dir) instead of umdUrl

Validated in browser: 6 tasks completed, 21 debounced steps captured,
completion code 21F5C2FB2EB5.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comprehensive GDD for "JBrowse Navigator" — a structured game-like
funnel from MTurk/Prolific entrants to qualified genome browser users.

Key design principles:
- Tutorial through play (no explicit instructions, learn by doing)
- Anti-speedrun by design (tasks require browser controls, not search bar)
- Award system gates tier progression (Explorer, Eagle Eye, Curator, etc.)
- Soft failure handling (retry, auto-advance, never stuck)
- Every game mechanic maps to RL data quality signal

5-tier funnel: Hook (30s) → Discovery (2-3min) → Competence (3-5min)
→ Expertise (5-10min) → Mastery (3-5min). ~4% graduation rate.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comprehensive expansion of the game design covering:

Awards (40+): Navigation, track/display, view types, feature interaction,
menu exploration, and mastery — each mapping to an RL-instrumented behavior.
Every discoverable JBrowse feature gets an award.

Level packs:
- "The Toolkit": menu exploration (right-click, rubberband, bookmarks, undo)
- "New Perspectives": dotplot, circular, synteny, split-screen views
- "The Lab": color-by-strand, compact mode, mismatch highlighting, sequence export
- "Variant Analysis": SNP/indel classification, impact triage, deletions

New action types (20+): RUBBERBAND_ZOOM, CONTEXT_MENU_OPEN, DISPLAY_CONFIG_CHANGE,
COLOR_BY_CHANGE, SORT_ALIGNMENTS, GET_SEQUENCE, EXPORT_SVG, ADD_BOOKMARK, UNDO/REDO, etc.

Visual feedback "dream" features:
- Pulsing highlight rings on quest-relevant UI elements (achievable now via CSS)
- Floating arrows with gentle bob animation (achievable now via DOM portal)
- Award burst particle effect (CSS-only)
- Quest beacon after 30s inactivity
- Viewport target overlay on overview bar
- Semantic zoom level HUD ("Genome"/"Region"/"Gene"/"Exon"/"Sequence")
- Progress trail on overview bar (fog-of-war clearing)
- Achievement gallery widget (aspirational)

Extended instrumentation architecture: MST patches + DOM events + extension
point hooks, unified through ActionClassifier into debounced ActionBuffer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Award system:
- AwardManager tracks earned awards from action + state triggers
- AwardChips display earned awards in widget with pop-in animation
- AwardSnackbar shows transient notification when award earned
- TierGate blocks task progression until required awards earned
- Awards defined in task set JSON (6 awards: first_steps, eagle_eye,
  curator, collector, cartographer, biologist)

Task system extensions:
- navigate_constrained: requires specific action types, rejects search-only
- action_required: validates required tracks are open
- Navigation constraints: requiredActionTypes, minActions, zoomRange
- Answer validation modes: exact, fuzzy, keyword_set, any_nonempty
- searchPenalty, awardOnComplete, coaching messages
- Auto-advance after max retries (default 3)
- Tier 0-4 support (was 1-3)
- Combined submit button (no more separate "Submit Answer" + "Submit & Continue")

Full volvox game task set (game_tasks_v2.json):
- Tier 0: Zoom In, Look Around (action-gated, no search possible)
- Tier 1: Read the Code, Reveal Hidden Data, Name That Feature, The Other Contig
- Tier 2: Gene Anatomy, The Neighbor, Variant Spotting, attention check
- Tier 3: Layers of Evidence, Free Exploration
- Tier 4: Teach a Robot, Graduation

Action log fed to widget for navigation constraint validation.
38 tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Narrator system:
- NARRATOR.md: design brief defining personality (enthusiastic, patient,
  philosophical scientific coach), idle behavior, fog of war, drift notifications
- Centralized locale file (src/ScavengerHunt/locale/en.ts) with all
  player-facing text keyed by ID, ready for localization
- Narrator intro/success lines for every task, idle escalation,
  validation feedback, encouragement, drift notifications

Auto-validation:
- No more "Check" button for navigation/action tasks — game watches
  continuously (500ms polling) and auto-advances when goal is met
- Text-answer tasks keep a "Submit" button but also auto-validate
  once an answer is entered
- Narrator success line shown briefly before advancing to next task

Task flow improvements:
- Task intro line (narrator voice) shown when each task begins
- Narrator message area replaces validation error alerts
- Success state shown with green background, info with grey

Re-added RLAnalytics to corePlugins (needed for dev server + tests;
UMD still available for parasitic deployment separately).

38 tests pass. 74KB UMD bundle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fixes from playtesting:
- Fix track name: use 'gff3tabix_genes' not 'volvox-sorted-vcf' (doesn't exist)
- Fix zoom task: accept ZOOM_IN OR ZOOM_OUT (not both required), mention
  zoom buttons and trackpad pinch as alternatives to scroll wheel
- Fix "Name That Gene": better framing ("zoom in to comfortable level where
  you can read labels") instead of vague "pan until you find a feature"
- requiredActionTypes now uses ANY-of logic (not ALL-of) — matching the
  design intent ("you need to zoom" = either direction works)
- Add explicit VCF track opening task (t2-open-vcf) before variant questions

New: NARRATOR.md design brief
- Defines narrator personality: enthusiastic, patient, philosophical coach
- Establishes auto-validation as core principle (no "Check" button)
- Documents fog-of-war, idle nudges, drift notifications
- Mandates centralized text assets for localization

New: src/ScavengerHunt/locale/en.ts
- All narrator text centralized by key
- Intro/success lines for every task
- Idle escalation (30s/60s/90s/120s/180s)
- Validation feedback, encouragement, drift notifications
- Ready for localization (create fr.ts, es.ts, etc.)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Major UX changes from playtesting feedback:

Floating panel:
- Quest UI now renders as a fixed-position floating panel (React portal
  to document.body) instead of in JBrowse's widget drawer
- Stays visible when track selector or other widgets open
- Minimizable to a small "Q" tab, click header to toggle
- 320px wide, right-aligned, with scroll

Narrator log (chat-like history):
- Messages accumulate in a scrollable list, never replaced
- Auto-scrolls to bottom on new messages
- Color-coded by type: intro (grey), success (green), hint (blue),
  award (gold), system (dim grey)
- Fade-in animation on new entries

Task card redesign:
- Simplified: left border accent, no CardHeader overhead
- Title + description inline
- Hints shown below task card, expandable

Removed "Check" button entirely for nav/action tasks.
Text tasks keep a "Submit" button but also auto-validate.

38 tests pass. 79KB UMD.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Architecture rewrite based on code review findings:

GameEngine (new, pure logic, no React):
- Owns task lifecycle, award checking, narrator events, auto-validation
- Driven by actions from PatchListener (onAction) and text answers
- Widget is now a thin renderer of GameEngine state
- No more setInterval polling — validation runs on each action

Bugs fixed:
- AwardManager now actually wired: actions feed into checkAction() with
  real BrowserState, awards fire reactively from state thresholds
- prevState/nextState in EpisodeManager: cache previous state so reward
  shaping actually computes non-zero spatial rewards
- completeCurrentTask dedup: check completedTaskIds.includes before push
- zoomRange added to MST NavigationConstraintModel (was silently dropped)
- No more render-during-render: graduation message handled by GameEngine
- Module-level actionLogRef removed: action log lives in GameEngine

Widget simplification (ScavengerHuntWidget.tsx):
- Pure renderer of GameEngine + MST model state
- No game logic, no validation, no polling
- Floating panel with scrollable narrator history preserved
- ~160 lines (was 318)

38 tests pass. 78KB UMD.

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

1. Remove all scroll wheel / trackpad references from task descriptions,
   hints, coaching, and narrator lines. Only reference + / - buttons.
2. Revert to regular JBrowse widget drawer (remove FloatingPanel).
   The floating panel looked bad; widget drawer is the standard UX.
3. Basepairs task now requires volvox_refseq track to be visible
   (requiredTracks). The Eagle Eye award was firing when bases weren't
   visible because the reference track wasn't open.
4. Task description now tells user to open the reference sequence track
   first, then zoom in.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The scavenger hunt is now a JBrowse view (ScavengerHuntView) instead
of a sidebar widget. Views live in the main content area and persist
when other widgets (track selector, feature details) open.

Architecture:
- viewModel.ts: MST model composing BaseViewModel, with task state,
  awards, answers, hints. Has height property for resizing.
- ScavengerHuntView.tsx: horizontal layout with narrator log on left,
  task panel on right. Scrollable within its height.
- GameEngine wired via setGameEngine() module-level ref (same pattern)
- Auto-loaded from ?scavengerTasks= URL param as a view

The view appears alongside the LinearGenomeView, so both are visible
simultaneously. Default height 250px.

Old widget code preserved in model.ts/ScavengerHuntWidget.tsx but
no longer registered — can be removed later.

38 tests pass. 74KB UMD.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Submit button:
- submitValidation() now adds narrator feedback on failure (shows
  the validation reason in the narrator log as a hint)
- tryAutoValidate() remains silent on failure (for action-triggered checks)
- "Enter an answer first" shown if submit clicked without typing

Gene name recognition:
- Expanded keyword list to include ALL named features from volvox GFF3:
  EDEN, Apple1-4, seg01-15, b101, agt830, agt221, agt767, FakeSNP
- Case variants included (Eden, eden, apple1, etc.)
- Validation already case-insensitive (lowercases both sides)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Removed: entire ScavengerHunt game layer (tutorial, awards, narrator,
GameEngine, TaskValidator, locale, all game components). Preserved on
branch rl-analytics-tutorial-v1.

Kept: ActionLogger, RLPipeline, Export — the core instrumentation.

Enhanced BrowserState observation (for RL agent consumption):
- zoomLevel: semantic classification (genome/region/gene/sequence/basepair)
- Track type booleans: hasReferenceSequence, hasGeneTrack, hasAlignmentTrack,
  hasVariantTrack, hasQuantitativeTrack
- activeTracks: full TrackInfo[] with trackId, trackType, displayType
- visibleContentBlocks: count of rendered content blocks
- Session-level features: sessionDurationMs, totalActionsThisSession,
  actionCountsByType, uniqueRefNamesVisited
- Numeric encoding expanded from 11 to 17 dimensions

New: RLObserverView — a logging console view showing what the RL
system sees in real time. Opens via Add → RL Observer or ?rlObserver.

Fixed: EpisodeManager caches prevState between steps.

30KB UMD. 26 tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
MobX volatile plain arrays don't trigger re-renders on push().
Fix: reassign array on every addLogEntry() call.

Removed debug console.log statements.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When zoomTo() internally calls scrollTo(), onPatch sees ZOOM + PAN as
separate events. Now using MST onAction() to annotate each patch with
its sourceAction name. The ActionBuffer absorbs PAN patches that share
the same sourceAction as a preceding ZOOM (e.g., both from 'zoomTo').

This means rubberband zoom on the navbar now produces a single ZOOM_OUT
entry instead of ZOOM_OUT + PAN_LEFT + PAN_RIGHT.

The sourceAction metadata is included in the observer log (shown in
parentheses) and in the exported JSONL data.

Fallback: <10ms gap detection still works for cases where sourceAction
isn't available (e.g., UMD deployment without onAction support).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The old architecture classified actions by regex-matching on patch paths
(e.g., /views/\d+/bpPerPx → ZOOM_IN). This was fragile, missed compound
actions, and required heuristic absorption of sub-action patches.

New architecture: MST onAction fires once per action call with the action
name (zoomTo, horizontalScroll, navTo, showTrack, etc.) directly. No regex,
no compound-action ambiguity. Sub-actions (scrollTo inside zoomTo) are
filtered by simply not including them in the ACTION_MAP.

Changes:
- New ActionListener replaces PatchListener + ActionClassifier
- ACTION_MAP maps MST action names → semantic ActionType enum
- Only user-meaningful actions are captured (internal actions ignored)
- Action args extracted as metadata (bpPerPx, distance, trackId, etc.)
- Observer log now shows MST action name (zoomTo, horizontalScroll, etc.)
  instead of patch-derived classifications
- Simplified ActionType enum: ZOOM (not IN/OUT), PAN (not LEFT/RIGHT)
  — direction is in metadata, the action type is the user intent
- Removed PatchListener.ts, ActionClassifier.ts, ActionClassifier.test.ts
- Updated all tests for new types

30KB UMD. 14 tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
onAction only fires for actions on the target node itself, not child
nodes. View-level actions (zoomTo, horizontalScroll) were missed when
listening on the session.

addMiddleware intercepts ALL actions in the entire MST tree. Sub-actions
(scrollTo inside zoomTo) are filtered via call.parentActionEvent —
only top-level user-initiated actions are captured.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
addMiddleware on session still only catches session-level actions.
Attaching to rootModel (the true MST root) intercepts everything
in the entire tree: views, tracks, widgets, session.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
From console debugging, rubberband zoom fires as 'moveTo' (not zoomTo)
at the top level. Panning via click-drag fires 'horizontalScroll' which
calls 'scrollTo'. Both now in the map.

Sub-action filtering via parentActionEvent correctly handles:
- moveTo → zoomTo → scrollTo (only moveTo captured)
- horizontalScroll → scrollTo (only horizontalScroll captured)

Removed debug console.log statements.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
moveTo now shows target coordinates, scrollTo shows offset.
Temporary debug logging to identify the track-add MST action name.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Track selector uses 'toggleTrack' (not 'showTrack') as the MST action.
Added to ACTION_MAP with metadata extraction for trackId.
Removed debug logging.

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

New instrumented actions:
- moveTrackUp / moveTrackDown → REORDER_TRACK (with direction metadata)
- addToHighlights / removeHighlight → BOOKMARK
- undo / redo → UNDO

View now displays as "Action Monitor" instead of "Untitled view".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Bookmarks: add 'addBookmark' (the actual widget action name)
- Track reorder: add moveTrack, moveTrackToTop, moveTrackToBottom
- View name: pass displayName in initial state (not setDisplayName after)
- Metadata for moveTrack (movingId, targetId)

To bookmark a region: rubberband-select on the genome view (click-drag
while holding shift), then choose "Bookmark region" from the popup menu.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- moveTo args are BpOffset objects, not numbers — fixed metadata extraction
- moveTrack now shows movingId → targetId in observer
- Added addBookmark to ACTION_MAP (actual bookmark widget action name)
- Bookmark: use rubberband select (click-drag without shift on overview
  bar, or right-click after selecting) → "Bookmark region"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ihh and others added 22 commits March 30, 2026 22:35
- View name: setDisplayName('Action Monitor') called on both new and
  session-restored observer views. Also finds existing observer from
  session restore instead of creating duplicates.
- moveTrack: show "reorder" instead of cryptic auto-generated instance IDs
- moveTo: show refName instead of NaN (args are BpOffset objects)
- Menu label changed from "RL Observer" to "Action Monitor"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
moveTrack args are MST auto-generated instance IDs (e.g. B_cm5usGvr).
Now resolved to config trackIds (e.g. volvox_gc) by looking up the
instance ID in the view's track list at action time.

Observer now shows: moveTrack volvox_gc → before volvox_refseq

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The track configuration can be a string reference (the trackId itself)
or an object with a trackId property. Now handles both.
Also searches through session.views if call.tree doesn't have tracks
directly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The call.tree in middleware doesn't reliably expose the track list.
Instead, resolve instance IDs to config trackIds using the live
LinearGenomeView accessed via getView() at log-display time.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The ActionListener was trying to resolve IDs via call.tree which doesn't
work reliably. Now stores raw instance IDs in metadata, and resolves
them to config trackIds in logToObserver using the live view.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
JBrowse's drag-and-drop calls moveTrack with the same ID for both
moving and target on drag initiation. These are no-ops that don't
actually reorder anything. Now filtered in the middleware.

The actual track order is captured in the BrowserState observation
(activeTracks array), which is what matters for RL training.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Revert the no-op filter — moveTrack(id, id) IS the reorder event from
drag-and-drop. Show resolved track name without the cryptic raw IDs.

Feature clicks are already captured as OPEN_WIDGET with widgetType
'BaseFeatureWidget' — no additional instrumentation needed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Search bar:
- navToLocString captures raw location string (e.g. "ctgA:1000-2000")
- navToSearchString captures raw search input text (e.g. "EDEN")
- Shown in observer as: navToLocString "ctgA:1000-2000"

Display config (track-level):
- setColorScheme: color-by changes (strand, insertSize, etc.)
- setSortedBy / setSortedByAtPosition: read sorting
- setFeatureHeight: track height changes
- setDrawSingletons, setDrawProperPairs, setDrawInter, setDrawLongRange
- setLineWidth: arc display line width

SVG export:
- exportSvg captured as CONFIG_CHANGE

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- navToLocString: capture both raw string (if string) and parsed target
- setColorScheme: stringify the full colorBy arg when type field is absent
  (fixes empty display when track initially sets its color scheme)
- JSONL verified: 9 steps, search queries, config changes all captured

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
addBookmark fires as a sub-action of menu click handlers, so it was
filtered by the parentActionEvent check. Now whitelisted alongside
addToHighlights.

Confirmed working: bookmark creation, bookmark navigation (shows as
navToLocString with JSON target).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New state fields:
- viewportCenterBp: center of viewport in genomic coordinates
- displayedRegions: all displayed regions (for multi-region views)
- labelsVisible: heuristic — feature labels readable at current zoom
- openWidgets: list of open widget types (feature details, bookmarks, etc.)

Enriched TrackInfo:
- height, colorScheme, sortedBy per track (display config state)

Observation vector: 21 dimensions (was 17):
  +viewportCenterBp, +labelsVisible, +openWidgets count, +displayedRegions count

Updated Python Gymnasium converter:
- New ACTION_MAP matching current ActionType enum
- 21-dim encode_state matching JS StateEncoder
- Verified: (9, 21) observations from latest JSONL

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
For unsupervised exploration data collection, there is no meaningful
in-situ reward. The placeholder (-0.01/step, -0.5 oscillation) was
dishonest — it encoded assumptions about what "good" navigation is.

Reward field is now 0 for all steps. Reward shaping is a post-processing
step on the exported JSONL, defined by the RL researcher's objective.

Removed: RewardCalculator.ts, RewardCalculator.test.ts
Removed reward display from Action Monitor log lines.

11 tests pass. 36KB UMD.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Git hygiene:
- Remove node_modules/ symlinks from git (9 files)
- Remove dist/ build artifact from git
- Add .gitignore for dist/
- Move design docs (GAME_DESIGN.md, etc.) to docs/ subdirectory

Config schema:
- Strip to 5 slots that are actually read at runtime
- Wire all config values via getConf() with safe fallbacks
- Remove 6 dead slots (scavengerTasksUrl, rewardShaping, etc.)

Plugin lifecycle:
- No more mutable state on plugin singleton (except _testRefs)
- All subsystems created as local variables in configure(), wired via closures
- Observer view found dynamically via getObserverView() with isAlive() check
- No stale reference to destroyed view model

Memory:
- Episode eviction: maxEpisodes config (default 100), oldest evicted on overflow
- Prevents unbounded memory growth from long sessions

Dead code removed:
- RewardCalculator (already removed) reference in EpisodeManager
- resolveTrackId() in ActionListener (never called)
- onAction() callback API in ActionListener (never used)
- ActionCallback type export (unused)
- episode.taskId in JSONLExporter (field doesn't exist on Episode)

Session access:
- Centralized getSessionSafe() helper with proper typing
- setTimeout(0) instead of queueMicrotask (actually yields to renderer)

View model:
- Accepts pluginManager parameter (JBrowse convention)
- JSDoc #property / #action annotations for API docs
- menuItems() view (empty but present, standard for views)

11 tests pass. 35KB UMD.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Middleware scoped to session (not rootModel) — no longer intercepts
   root-level config/assembly actions. Only session children (views,
   tracks, widgets) are monitored.

2. _testRefs moved to module-level WeakMap — no mutable class fields.

3. Tests moved to src/__tests__/ (JBrowse convention).
   EpisodeManager test updated with eviction test and reward=0 check.

4. viewModel: postProcessSnapshot strips volatile log entries.
   menuItems() returns "Clear log" action. Accepts pluginManager param.

5. ObserverView uses MUI theme (useTheme) instead of hardcoded colors.
   Adapts to dark/light mode.

6. scrollIntoView throttled via requestAnimationFrame — no more
   continuous smooth-scroll during rapid actions.

7. Log entries use MobX observable.array for O(1) push reactivity
   instead of O(n) spread-copy on every entry.

8. WebhookExporter wired to config: webhookUrl config slot, read at
   configure() time, configureWebhook() called if non-empty.

11 unit tests + 3 integration tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- README.md: usage docs including parasitic deployment workflow
- CodeReview.md: second-pass JBrowse-maintainer-perspective review
- package.json: add 'pnpm build:umd' script
- viewModel: remove dead no-op postProcessSnapshot
- Remove stale test/EpisodeManager.test.ts (was untracked on disk)

The parasitic deployment workflow can now be run as:
  pnpm build:umd                                        # produces dist/*.umd.js
  node scripts/generate_parasitic_config.mjs <args>     # generates config
  node scripts/webhook_receiver.mjs                     # receives data

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The webhook pipeline was cosmetic — configureWebhook() created the
exporter and started its flush timer, but nothing called
webhookExporter.push(). Webhook POSTs always sent empty batches.

Fix: onDebouncedAction now calls exportManager.webhook?.push(step,
episodeId) after recordAction succeeds. The episode ID is returned
from EpisodeManager.recordAction() alongside the step.

Also:
- EpisodeManager exposes currentEpisodeId getter
- WebhookExporter.push signature: removed unused taskId parameter
- test_data/volvox/config_with_webhook.json: sample config for testing

Tested end-to-end: webhook receiver collects steps with correct
episode_id, timestamps, and full observation/nextState payloads.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
getConf(model, slot) expects model.configuration as the navigation root,
but we were passing the config node itself. Switched to readConfObject
which takes the config node directly. Also moved config reading BEFORE
subsystem creation so config values actually affect construction.

Tested end-to-end: live browser session → webhook receiver collects
PAN/ZOOM/ADD_VIEW/OPEN_WIDGET steps with correct episode IDs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Type safety:
- Replace all `(rootModel as any)` casts with typed `RootModelWithSession`
  interface
- Use AbstractSessionModel, AbstractViewModel from @jbrowse/core/util
- No more `as any` in index.ts

Tests (11 → 25):
- Add ActionBuffer.test.ts: debounce, merge, type separation, eviction,
  drain, dispose (6 tests)
- Add StateEncoder.test.ts: shape, zoomLevel, labelsVisible, track flags,
  encode(), recordAction, unique refNames, throwing getters (9 tests)
- Remove unused ClassifiedAction.path field

UI code quality:
- LogEntry type: {id, text} with stable monotonic IDs
- ObserverView uses entry.id as key (no more re-association on eviction)

MUI version: verified already matches monorepo (7.3.8)

25 tests pass. Clean typecheck.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All pass-2 blockers now addressed. Plugin is in shape for a draft PR
to GMOD/jbrowse-components. Remaining items are lower-priority polish
(state memoization, delta encoding, broader integration tests).

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

- StateEncoder: within-frame memoization keyed on view inputs
- JSONLExporter: delta-encode next_observation against observation
- EpisodeManager: dynamic inactivity check interval
- New WebhookExporter test suite (mocked fetch, sendBeacon, restore-on-failure)
- New StateEncoder memoization tests
- New integration tests for SHOW_TRACK and delta-encoded JSONL
- scripts/generate_action_doc.mjs + docs/ACTIONS.md
- Python converter handles delta-encoded JSONL
- CodeReview.md rewritten as a current-state PR reference

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ihh
Copy link
Copy Markdown
Member Author

ihh commented Apr 8, 2026

I don't know if this belongs on main or not tbh. Posting as a PR for your review @cmdcolin

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant