Skip to content

feat(nodes): add anomaly detection node for pipeline output monitoring#569

Open
charliegillet wants to merge 7 commits intorocketride-org:developfrom
charliegillet:feature/anomaly-detector-node-v2
Open

feat(nodes): add anomaly detection node for pipeline output monitoring#569
charliegillet wants to merge 7 commits intorocketride-org:developfrom
charliegillet:feature/anomaly-detector-node-v2

Conversation

@charliegillet
Copy link
Copy Markdown
Contributor

@charliegillet charliegillet commented Mar 31, 2026

Summary

  • Add new anomaly_detector pipeline node that monitors numeric output values for statistical anomalies
  • Supports three detection methods: Z-Score, IQR (Interquartile Range), and Rolling Average deviation
  • Thread-safe sliding window implementation for streaming pipeline data with configurable window size
  • Severity classification (normal/warning/critical) with configurable thresholds

Detection Methods

Method Best For
Z-Score Normally distributed data, general-purpose outlier detection
IQR Skewed distributions, robust to extreme outliers
Rolling Average Detecting drift/shift in streaming time-series data

Node Structure

nodes/src/nodes/anomaly_detector/
├── __init__.py          # Module exports
├── IGlobal.py           # Global context - initializes detector per pipeline run
├── IInstance.py          # Instance handler - processes text and documents
├── detector.py          # Core detection engine (stdlib only: math, threading, collections)
├── requirements.txt     # No external dependencies
└── services.json        # Node configuration and UI shape

Configuration

  • method: Detection algorithm (z_score, iqr, rolling_avg)
  • sensitivity: Detection sensitivity threshold
  • windowSize: Sliding window size for statistical calculations
  • metric: Metadata field name containing the numeric value to monitor
  • warningThreshold / criticalThreshold: Severity classification boundaries

Type

Feature — new pipeline node

Test Plan

  • Verify node loads in pipeline builder UI with correct configuration fields
  • Test Z-Score detection with known normal + outlier values
  • Test IQR detection with skewed distributions
  • Test Rolling Average detection with drift scenarios
  • Verify thread safety under concurrent pipeline execution
  • Confirm document metadata is correctly enriched with anomaly results
  • Validate text pass-through with anomaly annotations

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added Anomaly Detector node for detecting anomalies in pipeline output using statistical methods (Z-Score, IQR, rolling average deviation).
    • Added protocol capabilities enum to client library.
  • Bug Fixes

    • Improved connection error handling with better exception wrapping.
    • Enhanced port handling in URI normalization.
    • Added visual feedback for pipeline stopping state.
  • Tests

    • Improved test reliability with timeout guards and retry logic.

charliegillet and others added 2 commits March 30, 2026 17:11
…screen

Handle TASK_STATE.STOPPING in the control button to show "Stopping..."
with a disabled state and distinct orange styling, preventing duplicate
clicks and giving immediate visual feedback during pipeline shutdown.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions github-actions bot added module:nodes Python pipeline nodes module:vscode VS Code extension labels Mar 31, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 31, 2026

Warning

Rate limit exceeded

@charliegillet has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 7 minutes and 5 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 7 minutes and 5 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: dc4f6e81-b892-47aa-82b8-0967a194a587

📥 Commits

Reviewing files that changed from the base of the PR and between b87062e and c426edb.

📒 Files selected for processing (14)
  • apps/vscode/src/providers/views/PageStatus/styles.css
  • nodes/src/nodes/anomaly_detector/IGlobal.py
  • nodes/src/nodes/anomaly_detector/IInstance.py
  • nodes/src/nodes/anomaly_detector/detector.py
  • packages/client-typescript/scripts/tasks.js
  • packages/client-typescript/src/cli/rocketride.ts
  • packages/client-typescript/src/client/client.ts
  • packages/client-typescript/src/client/constants.ts
  • packages/client-typescript/src/client/core/DAPClient.ts
  • packages/client-typescript/src/client/types/client.ts
  • packages/client-typescript/src/client/types/dashboard.ts
  • packages/client-typescript/src/client/types/events.ts
  • packages/client-typescript/src/client/types/index.ts
  • packages/client-typescript/tests/RocketRideClient.test.ts
📝 Walkthrough

Walkthrough

This PR introduces a new Anomaly Detector pipeline node for statistical anomaly detection (Z-Score, IQR, rolling average methods), updates the VSCode UI to display "Stopping" feedback, and enhances the TypeScript client library with improved error handling via ConnectionException and a new PROTOCOL_CAPS capability enum.

Changes

Cohort / File(s) Summary
Anomaly Detector Node
nodes/src/nodes/anomaly_detector/detector.py, nodes/src/nodes/anomaly_detector/IGlobal.py, nodes/src/nodes/anomaly_detector/IInstance.py, nodes/src/nodes/anomaly_detector/__init__.py, nodes/src/nodes/anomaly_detector/requirements.txt
Introduces complete anomaly detection pipeline node with thread-safe detector, lifecycle management, and text/document enrichment with anomaly scores, severity classification, and optional anomaly annotations.
Anomaly Detector Configuration
nodes/src/nodes/anomaly_detector/services.json
Defines node service metadata including input/output lanes, preconfigured detection profiles (z_score, iqr, rolling_avg), and configuration fields for method selection, sensitivity, window size, thresholds, and method-specific parameters.
VSCode UI - PageStatus
apps/vscode/src/providers/views/PageStatus/PageStatus.tsx, apps/vscode/src/providers/views/PageStatus/styles.css
Adds handling for STOPPING task state with disabled control button showing "Stopping..." label and corresponding CSS styling (orange background, white text, disabled cursor).
Client TypeScript - Connection Error Handling
packages/client-typescript/src/client/client.ts, packages/client-typescript/src/client/types/client.ts
Wraps non-ConnectionException errors into ConnectionException in onConnectError; updates ConnectErrorCallback type from (message: string) => Promise<void> to (error: ConnectionException) => void | Promise<void>. Enhances normalizeUri port-defaulting logic to detect explicit port in authority string.
Client TypeScript - Type System & Exports
packages/client-typescript/src/client/types/service.ts, packages/client-typescript/src/client/types/index.ts
Adds PROTOCOL_CAPS enum (bitmask flags for service-driver capabilities: SECURITY, FILESYSTEM, NETWORK, etc.) and re-exports service types via barrel file.
Client TypeScript - Test Infrastructure
packages/client-typescript/jest.config.js, packages/client-typescript/tests/RocketRideClient.test.ts
Adds Jest forceExit configuration; wraps disconnect/terminate calls in Promise.race with timeout guards (10s/15s); adds per-send retry logic with 250ms delay and adjusts test timeouts to use TEST_CONFIG.

Sequence Diagram(s)

sequenceDiagram
    participant Pipeline
    participant IGlobal
    participant IInstance
    participant AnomalyDetector

    Pipeline->>IGlobal: beginGlobal()
    activate IGlobal
    IGlobal->>IGlobal: Load configuration
    IGlobal->>AnomalyDetector: instantiate(config)
    deactivate IGlobal

    Pipeline->>IInstance: writeText(text)
    activate IInstance
    IInstance->>IInstance: Check detector available
    IInstance->>AnomalyDetector: evaluate_text(text)
    AnomalyDetector->>AnomalyDetector: detect(value)
    AnomalyDetector-->>IInstance: annotated text
    IInstance-->>Pipeline: enriched output
    deactivate IInstance

    Pipeline->>IInstance: writeDocuments(documents)
    activate IInstance
    IInstance->>IInstance: Clone document
    IInstance->>AnomalyDetector: evaluate_document(metadata)
    AnomalyDetector-->>IInstance: anomaly result
    IInstance->>IInstance: Add anomaly fields to metadata
    IInstance-->>Pipeline: enriched documents
    deactivate IInstance

    Pipeline->>IGlobal: endGlobal()
    activate IGlobal
    IGlobal->>IGlobal: Reset detector to None
    deactivate IGlobal
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

  • Fix: SDK connect error callback type #576: Both PRs modify SDK connect-error callback handling in packages/client-typescript by wrapping errors in ConnectionException and updating ConnectErrorCallback type signature.

Suggested labels

module:ui

Suggested reviewers

  • jmaionchi
  • stepmikhaylov
  • Rod-Christensen

Poem

🐰 A detector awakens with keen statistical sight,
Finding anomalies hidden in data's flight!
The client now catches exceptions with care,
While stopping states show with UI flair—
Together they dance through the pipeline's delight! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and accurately describes the primary change: adding a new anomaly detection node for monitoring pipeline output.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Collaborator

@asclearuc asclearuc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for this contribution — the node structure is clean, the services.json follows the required format, and using only stdlib is a nice touch. A few things to address before we merge:

Unrelated VSCode changes

PageStatus.tsx and styles.css are not related to this node. Please remove them from this PR.

Z-Score and Rolling Average are identical

_detect_z_score and _detect_rolling_avg compute exactly the same value — abs(value - mean) / std_dev — and differ only in the details label. This is misleading: a user choosing "Rolling Average" would expect behaviour distinct from Z-Score (e.g., deviation from a moving average rather than from the global window mean). Please either implement genuinely different logic for rolling average, or remove it as a separate option and document that the two are equivalent.

evaluate_text silently skips multi-value text

float(text.strip()) requires the entire string to be a single bare number. For any text that is not — including common formats like "1 1 2 3 5 8", "score: 0.95", or "3.14 2.71"float() raises ValueError, the except branch returns the text unchanged, and no detection happens at all. This is likely not the intended behaviour. Please clarify what input format this method is designed for and either document the constraint clearly or implement extraction of numeric values from the text.

Crash in CONFIG mode

beginGlobal exits early with pass in CONFIG mode, leaving self.detector = None. If the engine calls writeText or writeDocuments during configuration, both methods will raise AttributeError on self.IGlobal.detector. Please add a guard:

if self.IGlobal.detector is None:
    self.instance.writeText(text)
    return

- Remove unrelated PageStatus.tsx and styles.css changes
- Fix rolling average to use a sliding sub-window for local mean instead
  of duplicating the z-score full-window approach
- Fix evaluate_text to extract numeric values via regex when full text
  parse fails, and log a debug message when no number is found
- Add detector None guards in writeText/writeDocuments to prevent crash
  in CONFIG mode

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions github-actions bot added module:client-typescript and removed module:vscode VS Code extension labels Mar 31, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@nodes/src/nodes/anomaly_detector/IGlobal.py`:
- Around line 47-49: Change all regular string literals to use single quotes in
this block: update the f-string argument to debug(f'    Loading anomaly
detector: method={config.get(\'method\', \'z_score\')}') so the inner config.get
uses single quotes instead of double quotes, and ensure any other literal
strings passed to debug or used when instantiating AnomalyDetector (e.g., the
debug call '    Anomaly detector initialized' and the AnomalyDetector(config)
instantiation) follow the single-quote style consistently.

In `@nodes/src/nodes/anomaly_detector/IInstance.py`:
- Around line 70-83: The code is mutating a Pydantic DocMetadata via dict-style
assignment and using a shallow copy; change the copy and assignment to use
Pydantic-friendly patterns: call doc.model_copy(deep=True) when creating
enriched_doc to avoid mutating the original, ensure enriched_doc.metadata is a
DocMetadata instance (not a raw dict) before setting fields, and replace
bracket-style assignments (enriched_doc.metadata['anomaly_score'] etc.) with
attribute assignments (enriched_doc.metadata.anomaly_score = result['score'],
enriched_doc.metadata.anomaly_severity = result['severity'],
enriched_doc.metadata.anomaly_is_anomalous = result['is_anomalous'], and set
enriched_doc.metadata.anomaly_details if result.get('details')). Use the same
symbols shown (documents, enriched_doc, doc.model_copy,
self.IGlobal.detector.evaluate_document) to locate and update the code.
🪄 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: 7aa040a5-2db1-419e-9a56-fc237019ac67

📥 Commits

Reviewing files that changed from the base of the PR and between b6b2dc1 and c3eccc4.

📒 Files selected for processing (7)
  • nodes/src/nodes/anomaly_detector/IGlobal.py
  • nodes/src/nodes/anomaly_detector/IInstance.py
  • nodes/src/nodes/anomaly_detector/__init__.py
  • nodes/src/nodes/anomaly_detector/detector.py
  • nodes/src/nodes/anomaly_detector/requirements.txt
  • nodes/src/nodes/anomaly_detector/services.json
  • packages/client-typescript/src/cli/rocketride.ts

Comment on lines +70 to +83
for doc in documents:
enriched_doc = doc.model_copy()
if enriched_doc.metadata is None:
enriched_doc.metadata = {}

result = self.IGlobal.detector.evaluate_document(enriched_doc.metadata)

enriched_doc.metadata['anomaly_score'] = result['score']
enriched_doc.metadata['anomaly_severity'] = result['severity']
enriched_doc.metadata['anomaly_is_anomalous'] = result['is_anomalous']
if result.get('details'):
enriched_doc.metadata['anomaly_details'] = result['details']

enriched_docs.append(enriched_doc)
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Does Pydantic v2 BaseModel with extra='allow' support dict-style item assignment like model['key'] = value?

💡 Result:

No, Pydantic v2 BaseModel with extra='allow' does not support dict-style item assignment like model['key'] = value. BaseModel instances are not subscriptable by default; they do not implement setitem or getitem. They support attribute access via dot notation (model.key) for defined fields and extra fields (stored in pydantic_extra and accessible via model_extra property or getattr), but not dictionary-style bracket notation model['key']. Users must subclass and implement getitem and setitem manually for dict-like access, as shown in discussions where this is not native behavior. Official docs confirm access via attributes or model_extra dict, not direct subscripting on the model instance.

Citations:


Dict-style assignment on Pydantic DocMetadata will fail at runtime.

Lines 77-81 use bracket notation (metadata['anomaly_score'] = value) on a DocMetadata Pydantic model, but Pydantic v2 BaseModel does not support __setitem__. This will raise TypeError.

Use attribute access instead: enriched_doc.metadata.anomaly_score = result['score'].

Additionally, model_copy() without deep=True performs a shallow copy, so modifications to enriched_doc.metadata will also mutate the original doc.metadata.

Proposed fix
         for doc in documents:
-            enriched_doc = doc.model_copy()
+            enriched_doc = doc.model_copy(deep=True)
             if enriched_doc.metadata is None:
                 enriched_doc.metadata = {}

             result = self.IGlobal.detector.evaluate_document(enriched_doc.metadata)

-            enriched_doc.metadata['anomaly_score'] = result['score']
-            enriched_doc.metadata['anomaly_severity'] = result['severity']
-            enriched_doc.metadata['anomaly_is_anomalous'] = result['is_anomalous']
+            enriched_doc.metadata.anomaly_score = result['score']
+            enriched_doc.metadata.anomaly_severity = result['severity']
+            enriched_doc.metadata.anomaly_is_anomalous = result['is_anomalous']
             if result.get('details'):
-                enriched_doc.metadata['anomaly_details'] = result['details']
+                enriched_doc.metadata.anomaly_details = result['details']
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nodes/src/nodes/anomaly_detector/IInstance.py` around lines 70 - 83, The code
is mutating a Pydantic DocMetadata via dict-style assignment and using a shallow
copy; change the copy and assignment to use Pydantic-friendly patterns: call
doc.model_copy(deep=True) when creating enriched_doc to avoid mutating the
original, ensure enriched_doc.metadata is a DocMetadata instance (not a raw
dict) before setting fields, and replace bracket-style assignments
(enriched_doc.metadata['anomaly_score'] etc.) with attribute assignments
(enriched_doc.metadata.anomaly_score = result['score'],
enriched_doc.metadata.anomaly_severity = result['severity'],
enriched_doc.metadata.anomaly_is_anomalous = result['is_anomalous'], and set
enriched_doc.metadata.anomaly_details if result.get('details')). Use the same
symbols shown (documents, enriched_doc, doc.model_copy,
self.IGlobal.detector.evaluate_document) to locate and update the code.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: replaced all dict-style item assignment (model["key"] = value) on DocMetadata with attribute access (model.key = value). Since DocMetadata uses extra="allow", Pydantic v2 allows setting arbitrary attributes. Also removed the metadata = {} fallback — if metadata is None, we now skip the document and pass it through unchanged, since creating a bare DocMetadata would fail validation (required fields objectId, chunkId). The evaluate_document call now receives metadata.model_dump() to ensure we pass a plain dict to the detector.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rate Limit Exceeded

@charliegillet have exceeded the limit for the number of chat messages per hour. Please wait 1 minutes and 36 seconds before sending another message.

The previous removal commit left these files behind develop, which
would revert the stopping-button feature on merge.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions github-actions bot added the module:vscode VS Code extension label Apr 3, 2026
Unrelated formatting and refactoring changes were accidentally included
in the anomaly detector feedback commit.

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

Addressed all review feedback:

  • Removed unrelated PageStatus changes and restored files to match develop
  • Removed unrelated client-typescript formatting changes introduced in previous feedback commit
  • Rolling average now uses a sliding sub-window for local mean (genuinely different from z-score)
  • Added detector None guards in writeText/writeDocuments for CONFIG mode
  • Fixed evaluate_text to extract numeric values via regex fallback

Thanks for the review!

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/client-typescript/jest.config.js`:
- Around line 53-56: The Jest config currently sets forceExit: true which masks
cleanup failures; remove or set forceExit to false in jest.config.js and rely on
the existing disconnect() cleanup plus its Promise.race() timeout (lines
referencing disconnect() and Promise.race()) so tests will surface lingering
WebSocket handles/timers instead of silently exiting. Ensure CI/local runs still
complete by keeping or adjusting the disconnect() timeout as needed, but do not
re-enable forceExit.

In `@packages/client-typescript/src/client/client.ts`:
- Around line 398-409: The host-check currently uses
url.hostname.includes('rocketride.ai') which false-positively matches things
like staging-rocketride.ai.internal; change it to test for exact domain or its
subdomains (e.g. url.hostname === 'rocketride.ai' ||
url.hostname.endsWith('.rocketride.ai')) so the block that sets url.port (using
CONST_DEFAULT_WEB_PORT) only skips when the hostname is the exact cloud domain
or a direct subdomain; apply this replacement where hasExplicitPort and url.port
are evaluated.

In `@packages/client-typescript/src/client/types/client.ts`:
- Line 32: Restore the public ConnectErrorCallback type to accept the wider
union (error: Error | string) => void | Promise<void> instead of narrowing it to
(error: ConnectionException) => void; keep ConnectionException only for internal
runtime values but export the callback type as the broader union so existing
consumers (e.g., ConnectErrorCallback usages in apps/vscode, apps/chat-ui,
apps/dropper-ui) remain compatible and typings compile against the previous
contract.

In `@packages/client-typescript/src/client/types/service.ts`:
- Around line 90-94: The client-side PROTOCOL_CAPS enum includes DEPRECATED and
EXPERIMENTAL flags that are not present in the engine's authoritative enum
(stops at FOCUS = BIT(14)); remove the DEPRECATED and EXPERIMENTAL entries from
the PROTOCOL_CAPS enum (i.e., delete the DEPRECATED and EXPERIMENTAL symbols) so
the client only exposes flags that the server actually emits, or alternatively
update the enum to exactly match the engine's bit assignments up through FOCUS
(BIT(14)) to keep the client aligned with the engine source of truth.

In `@packages/client-typescript/tests/RocketRideClient.test.ts`:
- Around line 1825-1840: The current retry inside runPipeline around client.send
can cause duplicate writes because send() is not idempotent; remove the blind
retry and either (a) let the send failure surface so the test fails, or (b)
implement a safe retry by recreating the pipeline/connection or supplying an
idempotency key before retrying. Locate the retry block in runPipeline (the
try/catch around client.send called with token and text) and replace the catch
behavior: do not call client.send(token, ...) a second time without a new
idempotency boundary—either re-establish the pipeline/connection (get a fresh
token) before retrying or simply rethrow the error so the test records the
failure.
- Around line 79-83: The Promise.race teardown uses an un-cleared setTimeout so
the timer handle stays alive and teardown can silently pass; change the pattern
around client.disconnect() (and the pipeline-termination race) to create the
timeout with a handle, race it against the real teardown promise, and on
whichever promise resolves first clear the timeout handle (clearTimeout) and if
the timeout resolved, cause the test to fail (reject or throw) so teardown
cannot silently pass while resources remain; apply this same fix to the pipeline
termination race to clear its timeout handle and fail when the timeout wins.
🪄 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: 905c927f-0893-4bf8-97f3-0de5d50c333d

📥 Commits

Reviewing files that changed from the base of the PR and between c3eccc4 and b87062e.

📒 Files selected for processing (8)
  • apps/vscode/src/providers/views/PageStatus/PageStatus.tsx
  • apps/vscode/src/providers/views/PageStatus/styles.css
  • packages/client-typescript/jest.config.js
  • packages/client-typescript/src/client/client.ts
  • packages/client-typescript/src/client/types/client.ts
  • packages/client-typescript/src/client/types/index.ts
  • packages/client-typescript/src/client/types/service.ts
  • packages/client-typescript/tests/RocketRideClient.test.ts

Comment on lines +398 to 409
// The URL API silently strips ports that are default-for-scheme
// (e.g. :443 on https, :80 on http), so url.port alone cannot
// distinguish "no port given" from "scheme-default port given".
// Check the raw input for an explicit `:digits` after the scheme.
const withoutScheme = normalized.replace(/^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//, '');
const authority = withoutScheme.split(/[/?#]/, 1)[0] ?? '';
const hasExplicitPort = authority.startsWith('[')
? /\]:\d+$/.test(authority) // IPv6 literal with explicit port
: /:\d+$/.test(authority); // hostname/IPv4 with explicit port

if (!url.port && !hasExplicitPort && !url.hostname.includes('rocketride.ai')) {
url.port = CONST_DEFAULT_WEB_PORT;
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use exact cloud-host matching here.

The explicit-port detection looks good, but url.hostname.includes('rocketride.ai') will also match unrelated hosts like staging-rocketride.ai.internal, so those URIs will skip the default :5565 port and normalize incorrectly. Compare against the exact domain or its subdomains instead.

🛠️ Suggested fix
-			if (!url.port && !hasExplicitPort && !url.hostname.includes('rocketride.ai')) {
+			const isRocketRideCloudHost =
+				url.hostname === 'rocketride.ai' || url.hostname.endsWith('.rocketride.ai');
+			if (!url.port && !hasExplicitPort && !isRocketRideCloudHost) {
 				url.port = CONST_DEFAULT_WEB_PORT;
 			}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// The URL API silently strips ports that are default-for-scheme
// (e.g. :443 on https, :80 on http), so url.port alone cannot
// distinguish "no port given" from "scheme-default port given".
// Check the raw input for an explicit `:digits` after the scheme.
const withoutScheme = normalized.replace(/^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//, '');
const authority = withoutScheme.split(/[/?#]/, 1)[0] ?? '';
const hasExplicitPort = authority.startsWith('[')
? /\]:\d+$/.test(authority) // IPv6 literal with explicit port
: /:\d+$/.test(authority); // hostname/IPv4 with explicit port
if (!url.port && !hasExplicitPort && !url.hostname.includes('rocketride.ai')) {
url.port = CONST_DEFAULT_WEB_PORT;
// The URL API silently strips ports that are default-for-scheme
// (e.g. :443 on https, :80 on http), so url.port alone cannot
// distinguish "no port given" from "scheme-default port given".
// Check the raw input for an explicit `:digits` after the scheme.
const withoutScheme = normalized.replace(/^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//, '');
const authority = withoutScheme.split(/[/?#]/, 1)[0] ?? '';
const hasExplicitPort = authority.startsWith('[')
? /\]:\d+$/.test(authority) // IPv6 literal with explicit port
: /:\d+$/.test(authority); // hostname/IPv4 with explicit port
const isRocketRideCloudHost =
url.hostname === 'rocketride.ai' || url.hostname.endsWith('.rocketride.ai');
if (!url.port && !hasExplicitPort && !isRocketRideCloudHost) {
url.port = CONST_DEFAULT_WEB_PORT;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/client-typescript/src/client/client.ts` around lines 398 - 409, The
host-check currently uses url.hostname.includes('rocketride.ai') which
false-positively matches things like staging-rocketride.ai.internal; change it
to test for exact domain or its subdomains (e.g. url.hostname ===
'rocketride.ai' || url.hostname.endsWith('.rocketride.ai')) so the block that
sets url.port (using CONST_DEFAULT_WEB_PORT) only skips when the hostname is the
exact cloud domain or a direct subdomain; apply this replacement where
hasExplicitPort and url.port are evaluated.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted: all packages/client-typescript/ changes have been restored to match develop. These were unrelated to the anomaly detector node.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rate Limit Exceeded

@charliegillet have exceeded the limit for the number of chat messages per hour. Please wait 1 minutes and 31 seconds before sending another message.

* including DAP messages, callbacks, configuration options, and transport interfaces.
*/

import { ConnectionException } from '../exceptions/index.js';
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n packages/client-typescript/src/client/types/client.ts | sed -n '30,45p'

Repository: rocketride-org/rocketride-server

Length of output: 485


🏁 Script executed:

cat -n packages/client-typescript/src/client/types/client.ts | sed -n '135,145p'

Repository: rocketride-org/rocketride-server

Length of output: 614


🏁 Script executed:

rg "ConnectErrorCallback|connectErrorCallback" --type ts --type tsx -A 3 -B 1

Repository: rocketride-org/rocketride-server

Length of output: 103


🏁 Script executed:

rg "connectErrorCallback\s*=" --type ts --type tsx -A 5

Repository: rocketride-org/rocketride-server

Length of output: 103


🏁 Script executed:

rg "ConnectErrorCallback|connectErrorCallback" -A 3 -B 1

Repository: rocketride-org/rocketride-server

Length of output: 6927


🏁 Script executed:

rg "onConnectError\s*=" -A 5

Repository: rocketride-org/rocketride-server

Length of output: 58


🏁 Script executed:

rg "_callerOnConnectError" -A 2

Repository: rocketride-org/rocketride-server

Length of output: 1204


🏁 Script executed:

rg "this\._callerOnConnectError\(" -B 2 -A 2

Repository: rocketride-org/rocketride-server

Length of output: 593


🏁 Script executed:

rg "onConnectError|connectErrorCallback" apps/ -A 3

Repository: rocketride-org/rocketride-server

Length of output: 2203


Keep ConnectErrorCallback on the wider public contract.

This narrows a public callback from the previously widened (error: Error | string) => void | Promise<void> shape down to (error: ConnectionException) => void, which is a breaking change for existing consumers. Downstream handlers in apps/vscode/src/debugger/session.ts, apps/chat-ui/src/hooks/clientSingleton.ts, and apps/dropper-ui/src/hooks/clientSingleton.ts are all typed to accept string parameters, not ConnectionException objects. The runtime can pass a ConnectionException instance without requiring that specific type in the exported API.

♻️ Suggested fix
-import { ConnectionException } from '../exceptions/index.js';
-
 ...
-export type ConnectErrorCallback = (error: ConnectionException) => void | Promise<void>;
+export type ConnectErrorCallback = (error: Error | string) => void | Promise<void>;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/client-typescript/src/client/types/client.ts` at line 32, Restore
the public ConnectErrorCallback type to accept the wider union (error: Error |
string) => void | Promise<void> instead of narrowing it to (error:
ConnectionException) => void; keep ConnectionException only for internal runtime
values but export the callback type as the broader union so existing consumers
(e.g., ConnectErrorCallback usages in apps/vscode, apps/chat-ui,
apps/dropper-ui) remain compatible and typings compile against the previous
contract.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted: packages/client-typescript/ files restored to develop. Unrelated changes removed from this PR.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rate Limit Exceeded

@charliegillet have exceeded the limit for the number of chat messages per hour. Please wait 1 minutes and 30 seconds before sending another message.

Comment on lines +90 to +94
/** Driver is deprecated */
DEPRECATED = 1 << 15,

/** Driver is experimental */
EXPERIMENTAL = 1 << 16,
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep PROTOCOL_CAPS aligned with the engine source of truth.

The authoritative enum in packages/server/engine-core/apLib/url/Url.hpp:41-67 stops at FOCUS = BIT(14). Adding DEPRECATED and EXPERIMENTAL here invents client-visible flags the server does not emit today and risks colliding with future engine assignments.

🧭 Suggested fix
-	/** Driver is deprecated */
-	DEPRECATED = 1 << 15,
-
-	/** Driver is experimental */
-	EXPERIMENTAL = 1 << 16,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/** Driver is deprecated */
DEPRECATED = 1 << 15,
/** Driver is experimental */
EXPERIMENTAL = 1 << 16,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/client-typescript/src/client/types/service.ts` around lines 90 - 94,
The client-side PROTOCOL_CAPS enum includes DEPRECATED and EXPERIMENTAL flags
that are not present in the engine's authoritative enum (stops at FOCUS =
BIT(14)); remove the DEPRECATED and EXPERIMENTAL entries from the PROTOCOL_CAPS
enum (i.e., delete the DEPRECATED and EXPERIMENTAL symbols) so the client only
exposes flags that the server actually emits, or alternatively update the enum
to exactly match the engine's bit assignments up through FOCUS (BIT(14)) to keep
the client aligned with the engine source of truth.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted: packages/client-typescript/ files restored to develop. Unrelated changes removed from this PR.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rate Limit Exceeded

@charliegillet have exceeded the limit for the number of chat messages per hour. Please wait 1 minutes and 29 seconds before sending another message.

Comment on lines +79 to +83
// Use a bounded timeout so teardown never hangs the suite
await Promise.race([
client.disconnect(),
new Promise<void>(resolve => setTimeout(resolve, 10000)),
]);
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Clear these timeout handles and fail if teardown stalls.

Both Promise.race blocks leave their setTimeout handle alive when cleanup finishes first, so even successful teardowns keep an open timer around for 10-15 seconds. If the timeout wins, teardown also passes silently and the next test can start with live clients/pipelines.

🧹 Suggested pattern
-			await Promise.race([
-				client.disconnect(),
-				new Promise<void>(resolve => setTimeout(resolve, 10000)),
-			]);
+			let timeoutId: ReturnType<typeof setTimeout> | undefined;
+			try {
+				await Promise.race([
+					client.disconnect(),
+					new Promise<never>((_, reject) => {
+						timeoutId = setTimeout(
+							() => reject(new Error('client.disconnect() timed out during test teardown')),
+							10000
+						);
+					}),
+				]);
+			} finally {
+				if (timeoutId !== undefined) clearTimeout(timeoutId);
+			}

Apply the same helper/pattern to the pipeline-termination race below.

Also applies to: 1569-1581

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/client-typescript/tests/RocketRideClient.test.ts` around lines 79 -
83, The Promise.race teardown uses an un-cleared setTimeout so the timer handle
stays alive and teardown can silently pass; change the pattern around
client.disconnect() (and the pipeline-termination race) to create the timeout
with a handle, race it against the real teardown promise, and on whichever
promise resolves first clear the timeout handle (clearTimeout) and if the
timeout resolved, cause the test to fail (reject or throw) so teardown cannot
silently pass while resources remain; apply this same fix to the pipeline
termination race to clear its timeout handle and fail when the timeout wins.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted: packages/client-typescript/ files restored to develop. Unrelated changes removed from this PR.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rate Limit Exceeded

@charliegillet have exceeded the limit for the number of chat messages per hour. Please wait 1 minutes and 26 seconds before sending another message.

These changes were accidentally included in the anomaly detector PR
and are not related to the feature. Restoring them to match develop.

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

@asclearuc Addressed all review feedback:

Removed unrelated files:

  • Removed PageStatus.tsx and styles.css changes (VSCode stop button UI)
  • Removed all client-typescript changes (jest config, client.ts, types, tests) that were accidentally included

Previously addressed (in earlier commits):

  • Z-Score vs Rolling Average: _detect_rolling_avg now uses a sliding sub-window (window[-rolling_n:]) for the local mean, making it genuinely different from Z-Score which uses the full window mean
  • evaluate_text: now extracts the first numeric value via regex when float() fails on the full string, with a debug log when no number is found
  • CONFIG mode crash: IInstance.writeText and writeDocuments both guard against self.IGlobal.detector is None

The PR now only contains the anomaly detector node files.

@nihalnihalani
Copy link
Copy Markdown
Contributor

Senior Review — PR #569: feat(nodes): add anomaly detection node

@charliegillet, the Z-Score and IQR implementations are well done — clean structure and good test coverage for those methods.

However, there is a blocking issue that needs to be resolved before this can move forward:

Rolling Average computes the same values as Z-Score

The Rolling Average method currently computes anomalies using the global window mean and standard deviation, which produces identical results to the Z-Score method. This defeats the purpose of having it as a separate detection strategy.

Suggestion: Implement genuinely different logic for Rolling Average — for example, flag anomalies based on deviation from an exponential moving average (EMA) or a sliding-window mean with a different sensitivity characteristic. The key is that a user choosing "Rolling Average" should get meaningfully different detection behavior than "Z-Score."

Additional items

  1. CONFIG mode guard — Add a guard so that when the node is in CONFIG mode, it returns the configuration schema without attempting to run detection logic. Follow the same pattern used in other nodes (early return when self.mode == "CONFIG").

  2. Unrelated VSCode files — There appear to be VSCode configuration files included in this PR. Please remove them from the changeset (add to .gitignore if not already present).

The foundation here is strong. Fixing the Rolling Average logic is the main thing standing between this and approval.

@nihalnihalani
Copy link
Copy Markdown
Contributor

🚀 Merge Request

Solid Z-Score and IQR implementation.

Before merge:

  • Rolling Average must implement genuinely different logic from Z-Score (currently identical)
  • Add CONFIG mode guard (detector is None when beginGlobal exits early)
  • Document or fix evaluate_text input format expectations
  • Remove unrelated VSCode file changes

The algorithmic correctness issue is the main blocker.

- Revert unrelated PageStatus and client-typescript changes to match develop
- Use single quotes per coding guidelines in IGlobal.py debug logging
- Fix Pydantic v2 dict-style assignment on DocMetadata BaseModel by using
  attribute access (model.key = value) instead of model['key'] = value
- Differentiate rolling average from z-score: use percentage deviation from
  rolling mean instead of std_dev-normalized deviation
- Skip documents with no metadata instead of assigning empty dict

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions github-actions bot added builder and removed module:nodes Python pipeline nodes labels Apr 8, 2026
@charliegillet
Copy link
Copy Markdown
Contributor Author

Thanks for the review @asclearuc! All feedback has been addressed in commit c426edb:

Unrelated changes reverted:

  • apps/vscode/src/providers/views/PageStatus/ files restored to match develop
  • All packages/client-typescript/ files restored to match develop

Anomaly detector fixes:

  1. Single quotes in IGlobal.py — Extracted the config.get() call into a local variable to keep all string literals as single quotes and avoid f-string escaping issues on Python < 3.12.
  2. Pydantic v2 dict-style assignment in IInstance.py — Replaced model['key'] = value with model.key = value (attribute access), which is the correct approach for Pydantic v2 BaseModel with extra='allow'. Also removed the problematic metadata = {} fallback (would fail DocMetadata validation) and instead skip documents with no metadata.
  3. Rolling Average vs Z-Score — Completely reworked _detect_rolling_avg to use percentage deviation from the rolling mean (abs(value - local_mean) / abs(local_mean) * 100) instead of std_dev normalization. This gives a meaningfully different algorithm that is more intuitive for business metrics.

All files pass ruff check and ruff format --check.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 8, 2026

No description provided.

@charliegillet
Copy link
Copy Markdown
Contributor Author

Latest fixes

  • Reverted all unrelated files (14 client-typescript + 2 PageStatus files restored to develop)
  • Fixed critical Pydantic v2 bug — switched from dict-style metadata['key'] to attribute access metadata.key
  • Rewrote _detect_rolling_avg to use percentage deviation from rolling mean (genuinely different from z-score now)
  • Fixed single-quote f-string escaping in IGlobal.py

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants