GDS Monitor
A GDS monitor detects behavioral changes over time — drift at the entity level, regime shifts at the population level, emerging anomalies not yet crossed the threshold, and calibration degradation. The approach is top-down: check population health first, drill to entities only when something is flagged.
Prerequisite: This skill requires patterns with temporal data (has_temporal: true
in sphere_overview). If no patterns have temporal data, report that temporal monitoring
is unavailable and hand off to gds-explorer for static analysis instead.
For concrete tool output examples, see references/examples.md.
Quick health check
Two calls tell you whether anything needs attention:
sphere_overview(detail="summary") -> calibration, anomaly rates, profiling_alerts, has_temporal
check_alerts() -> alerts sorted by severity
edge_stats(pattern_id) -> if event patterns exist, graph monitoring available
If alerts are present, respond to them (see table below). If check_alerts
returns nothing, proceed to find_regime_changes and find_drifting_entities
anyway — absence of alerts does NOT mean absence of temporal patterns.
Alerts check geometric health (calibration staleness, threshold violations);
regime changes and drift detect behavioral shifts that are structurally valid
but operationally significant. Skipping temporal tools when alerts are clean
is the most common monitoring gap.
Responding to alerts
| Alert | What it means and what to do |
|---|---|
anomaly_rate_spike |
anomaly_summary(pattern_id) then contrast_populations({anomaly: true}) — what spiked? If edge table available: degree_velocity on top spikers to check connection rate acceleration. |
population_size_shock |
get_sphere_info() — compare entity counts — data ingestion issue? |
theta_miscalibration |
recalibrate(pattern_id) then re-check sphere_overview() |
regime_changepoint |
find_regime_changes(pattern_id) then compare_time_windows() for flagged period |
calibration_drift |
Note for next rebuild. Current results still usable. |
stale_vector_index |
Incrementally-appended geometry rows sit outside the ANN (IVF) index, so trajectory / similarity tools (detect_trajectory_anomaly, find_similar_entities, find_drifting_similar) silently miss them. MEDIUM severity. Drill down with the vector_index_health block (see below) to read the unindexed fraction, then trigger a reindex on the builder side. |
stale_vector_index drilldown — reading vector_index_health
The alert is metadata-only and fires when the unindexed fraction of a pattern's geometry exceeds the stale threshold (default 0.1). There is no standalone health tool — the per-pattern detail is surfaced as a vector_index_health block on every audit_pattern_dims response:
audit_pattern_dims(pattern_id) -> vector_index_health block:
{ index_present, index_type, num_indexed_rows, num_unindexed_rows,
total_rows, indexed_fraction, num_partitions, is_stale,
stale_threshold, recommendation }
Triage:
is_stale == false— index covers the population; ANN-backed tools are complete. No action.is_stale == true— therecommendationstring names the fix; an unindexed run accumulated from incremental ingest. ANN tools under-report on the unindexed tail until a reindex runs. Note it as a monitoring finding and hand the reindex to the sphere owner (it is a build-side operation — see the incremental-ingest path in gds-sphere-designer, which reindexes viaincremental_update(reindex=True)orfinalize_incremental()).index_present == falsewith a small population — expected. The builder skips the IVF index below the build-time row floor; ANN tools fall back to a full scan and lose nothing. Not an actionable alert.
Interpret is_stale as "ANN results are currently incomplete", NOT "the geometry is wrong" — the rows exist and exact-scan tools (find_anomalies, explain_anomaly) see them; only the approximate-neighbour tools are affected.
Event rate divergence
Compare event anomaly rate per entity vs static anchor anomaly status:
aggregate(event_pattern, group_by_line=anchor_line,
geometry_filters={"is_anomaly": true})
Entities with >15% anomalous events but NOT flagged as anchor anomalies have concentrated temporal anomalies — investigate with drift detection.
Shortcut: sphere_overview(detail="full") returns event_rate_divergence_alerts
pre-computed for all anchor patterns (top 20 by rate).
Caution: detail="full" can take minutes on large spheres (>100K entities) —
use only for targeted diagnostics, never in interactive loops.
Windowed volume comparison
Compare event counts per entity across two time periods to detect bursts or drops.
Use time_from/time_to on aggregate — ISO-8601 half-open interval [from, to).
Pick periods from sphere context (recent batch window, pre/post incident, quarter).
aggregate(event_pattern, group_by_line=anchor_line, metric="count",
time_from=period_a_start, time_to=period_a_end, limit=20)
aggregate(event_pattern, group_by_line=anchor_line, metric="count",
time_from=period_b_start, time_to=period_b_end, limit=20)
Entities with 2x+ count increase in period B are burst candidates (order splitting,
seasonal spikes, data ingestion anomalies). Requires timestamp_col on the event
pattern (set via temporal config in sphere.yaml).
Drift detection
find_drifting_entities(pattern_id, top_n=10, forecast_horizon=3)
Interpreting drift results
- Single high-displacement drifter = entity-level behavioral change
- Cluster of drifters in same direction = systemic population shift
forecast_anomaly=true+reliability=high= early warning
Drift direction triage. find_drifting_entities now returns drift_direction per entity. On a monitoring sweep, triage thus: deteriorating (highest priority, entities moving away from the population centre — trending toward anomaly), neutral (lateral motion, monitor), normalizing (lowest priority, already self-correcting). Filter the top-N list by drift_direction == "deteriorating" before escalating, otherwise alerts include entities that are actively recovering.
For top drifters, dive_solid(key, pattern_id) reveals whether it is
gradual drift or a sudden jump. If edge table available: degree_velocity(key, pattern_id) — accelerating degree alongside geometric displacement = strong behavioral change confirmation.
Dimension-specific drift
Displacement is a single number — it hides WHICH dimension changed. For top drifters:
dive_solid(key, pattern_id) -> compare first vs last slice deltas
-> Which dimension changed most? (e.g., counterparty count spike = new relationships,
burst metric spike = activity change, value metric spike = monetary shift)
If top drifters ALL show the same dominant dimension (e.g. burst_monthly), other dimensions' real changes are invisible in the ranking. In that case:
- For each NON-dominant dimension: check if ANY drifter has significant change on THAT specific dimension
dive_solidon 3 random high-displacement entities, compare first vs last slice PER DIMENSION separately — not just total displacement- A drifter with small total displacement but large change on ONE dim is more interesting than a drifter with large displacement on a high-variance dimension
Tool shortcut — rank_by_dimension:
find_drifting_entities(pattern_id, rank_by_dimension="suppliers")
-> entities ranked by drift on the "suppliers" dimension specifically,
not total displacement
Use this when you suspect a specific dimension is the signal but total displacement is dominated by a different high-variance dimension.
Compare calibration epochs to inspect drift
After a builder rebuild, compare_calibrations(pattern_id) (default args)
returns the drift between the two most recent epochs. Use top_n=N to
limit the ranked dimension list, verbose=True to also get the full
per-dimension breakdown, or pass explicit v_from / v_to to compare any
two epochs the sphere has on disk (list_calibration_versions(pid) returns
what's available).
Example query: "Has the population shifted since the last rebuild?" →
compare_calibrations("account_pattern") → check overall_drift_rms
(RMS in σ units, comparable across patterns) and top_drifted (which
dimensions moved the most).
When the anchor pattern declares edge_dim_aggregations:, the report also
carries edge_dim_threshold_drift — a per-source-dim {from, to, delta}
map of the _count_above_threshold cutoff. A large delta means the
population p95 of that source dim shifted between epochs, so the same
agent narrative ("X anomalous on _count_above_threshold = +Nσ") does
NOT mean the same edge regime as in the previous epoch. Worth flagging
when it shows up in monitoring alongside a non-zero overall_drift_rms.
Verdict composer — calibration_drift_report
compare_calibrations returns the raw per-dimension μ/σ/θ drift table but leaves
the "is this drift large enough to distrust cross-epoch reasoning?" call to the
agent. For a monitoring sweep, prefer calibration_drift_report(pattern_id) — it
wraps compare_calibrations and adds a drift_verdict over overall_drift_rms
plus a routing recommendation:
calibration_drift_report(pattern_id) # default args = latest vs previous epoch
-> drift_verdict: "stable" | "moderate" | "significant"
-> top_drifted, interpretation, recommended_next_steps
Monitoring routing by verdict:
stable— the coordinate system held; a "still anomalous" / "no longer anomalous" comparison across these epochs reflects the entity, not recalibration. Cross-epoch entity verdicts are safe to report.moderate— some dimensions moved materially; treat cross-epoch verdicts on thetop_drifteddimensions with caution before escalating an entity.significant— the coordinate system shifted substantially; a cross-epoch "no longer anomalous" claim is dominated by recalibration. Do NOT compare entity verdicts as if θ were fixed. Usedecompose_drift(entity, pattern)to split per-entity change into intrinsic (entity moved) vs extrinsic (population recalibrated around it), and pair withfind_calibration_influencers(see end of this skill) to find which entities drove the shift.
Drop to raw compare_calibrations only when you need the full per-dimension
table without the interpreted verdict, or to compare two specific non-adjacent
epochs (calibration_a / calibration_b).
Threshold sensitivity at a glance
sphere_overview() carries a theta_sensitivity_summary block per
pattern (when populated by a recent builder rebuild). The block tells
the agent — without a per-pattern drilldown — whether the chosen
anomaly_percentile is in a smooth region of the population's
delta_norm distribution or perched near a heavy-tail jump.
Fields per pattern:
stable_band_from/stable_band_to/stable_band_length— the longest contiguous run of percentiles where adjacent-pairtheta_meanratio stays below 1.30. Within this band, recalibrating to a different percentile shifts the threshold by less than 30 % per step.n_cliffs— number of percentile boundaries (inp90 .. p99) where thetheta_meanratio is at or above 1.50. A single move across a cliff jumps the threshold by 50 % or more — heavy-tail region of the distribution.theta_at_p95— the production-default p95 threshold value for quick visual reference.
Quick triage rules:
stable_band_length >= 8ANDn_cliffs == 0: pattern is light-tail and the chosen percentile sits comfortably. Recalibration moves anywhere in the band are safe.stable_band_length <= 4ORn_cliffs >= 2: heavy-tail pattern. Production p95 may already sit on a cliff edge. Calltheta_sensitivity(pattern_id)for the full per-percentile sweep and inspect thecliffs[]list before any recalibration proposal.- p95 inside a cliff pair (e.g.
cliffs[0]['from'] == 'p95'): do not recalibrate up — moving to p96 jumps theta materially. Consider tightening the percentile down (p93or below) instead. - Empty
theta_sensitivity_summary: the sphere predates the diagnostic. Trigger a rebuild before relying on this surface.
When the theta_sensitivity_summary block flags a non-trivial structure
(cliffs > 0 or stable_band_length < 6) and you are about to propose a
recalibration move, prefer the verdict composer over the raw sweep:
theta_sensitivity_report(pattern_id)
-> recalibration_safety: "safe" | "caution" | "unsafe"
-> n_cliffs, stable_band, cliffs, theta_sensitivity (per-percentile sweep)
-> interpretation, recommended_next_steps
theta_sensitivity_report wraps theta_sensitivity and reads the band / cliff
structure into a single recalibration_safety verdict, so you do not have to
interpret the raw band/cliff lists yourself:
safe— a contiguous stable band exists and no cliffs; movinganomaly_percentileshifts the threshold smoothly. Recalibration anywhere in the swept range is safe.caution— a stable band exists but cliffs sit outside it; keepanomaly_percentileinsidestable_bandand avoid the boundaries incliffs[].unsafe— no stable band; every percentile step shifts θ materially (heavy-tail region). Do not recalibrate by percentile without re-measuring the flagged population.
Use raw theta_sensitivity(pattern_id) directly only when you want the
per-percentile ratios without the interpreted verdict.
Dim-quality warnings
sphere_overview() carries an optional dim_quality_warnings[] block per pattern surfacing silent build-time failure modes that break z-score / delta_norm semantics. These were previously invisible at agent runtime — the dim sat in the delta vector contributing nothing or contributing wrong signal, and the investigator had no way to know without scrolling the calibration log.
type |
Trigger | Why it breaks delta_norm |
|---|---|---|
dead_dim |
sigma_diag[i] < 1e-10 (zero variance across population) |
z-score (x - mu) / sigma is undefined / explodes; the dim contributes nothing meaningful and silently dilutes other dims' signal |
sparse_dim |
dim_percentiles[d]['p50'] == 0 AND p99 > 0 (mostly-zero with rare nonzero) |
gaussian z-score assumption is wrong — empirical distribution is point-mass-at-zero plus a tail; Bregman divergence with poisson / bernoulli kind tag is the correct distance |
dominant_dim_mass |
pattern-level: one dim's share of ((p99 − μ_d) / σ_d)² across dims ≥ 0.70 |
pattern's delta_norm ranking is effectively single-dim; the sphere is a one-dim detector on this pattern, multi-dim composition adds little |
negative_space |
per-dim: dim_percentiles[d]['p50'] == 0 AND dimension_kinds[i] == "gaussian" |
gaussian z-score is wrong — empirical distribution is point-mass-at-zero, not centered on the mode; gaussian semantics treat zero as the typical value when it is actually the absence value |
heteroscedasticity |
pattern-level: Brown-Forsythe (median-centred Levene) p < 0.01 on delta_norm partitioned by group_by_property |
global θ assumption violated for this pattern; delta_norm variance differs sharply across the levels of the grouping variable, so a single global threshold produces unequal false-positive rates per group |
non_normal_dim |
per-dim: Shapiro-Wilk (N ≤ 5000) or Kolmogorov-Smirnov (larger N) p < 0.01 on a kind='gaussian' dim |
gaussian z-score is wrong — empirical distribution is heavy-tailed, so (x − μ) / σ produces unequal false-positive rates per quantile of the dim; mu/sigma are stable but the percentile semantics are not |
Each warning carries dim_label, reason (the offending value), and advice (concrete remediation); dominant_dim_mass and heteroscedasticity additionally carry evidence_value (the firing metric — share for dominant_dim_mass, p-value for heteroscedasticity) + threshold (the cutoff) so the agent can rank patterns by severity. Note: the heteroscedasticity warning's dim_label is the GROUPING VARIABLE name (a categorical line property), not a δ-dim — unlike the per-dim auditors. Triage rules:
Pattern-level calibration auditors — dominant_dim_mass, negative_space, and heteroscedasticity are sphere-design-quality signals; if they fire, raise a calibration ticket rather than chasing the entities flagged by find_anomalies on the affected pattern — those entities are likely noise.
- Any
dead_dimwarning: drop the dim from the pattern, OR investigate the data source for missing values / constant column. Builder-time issue — won't fix itself on rebuild without a YAML edit. sparse_dimwarning on a binary-like signal (e.g. structuring flag, fraud label): switch to Bregman withkind: bernoulli. The warning is not a bug — it's the gaussian assumption being wrong for the data shape.sparse_dimwarning on a sparse counter (e.g. tx-count where most accounts have zero): split intois_active(bernoulli) +tx_count_when_active(poisson) dims. The single-dim representation conflates two regimes.dominant_dim_masswarning: cross-check per-polygonreliability_flags.single_dim_drivenincidence onfind_anomaliestop-N for the same pattern — pattern-level dominance ⟹ high per-polygon incidence agreement. Raise a calibration ticket; consider splitting the dominant dim into its own pattern or re-weighting viadimension_weightsso the other dims actually contribute.negative_spacewarning: re-declare the dim withkind='bernoulli'(presence/absence) orkind='poisson'(count), or split into a binaryis_activedim plus a continuousvalue_when_activedim. Same remediation family assparse_dimbut the trigger is gaussian-declared, not empirical-distribution-shaped.heteroscedasticitywarning: per-group θ calibration is statistically warranted on the named grouping — the pattern already carries it viagroup_by_property, so the warning is confirmation, not a prescription for new work. If per-group θ is undesirable downstream (e.g. cross-group comparison of anomaly scores), apply a variance-stabilizing transform —log1pondelta_normbefore thresholding — to make scores comparable across groups. Re-check after rebuild: variance-stabilizing should push the Levene p-value back above 0.01.non_normal_dimwarning: the empirical distribution of a gaussian-declared dim is heavy-tailed — z-score percentiles are unstable. Remediation in priority order: (a) re-declare kind as'poisson'(counts) or'bernoulli'(binary) if the data shape fits; (b) apply variance-stabilizing transform in the column source script (log1pfor skewed positive,sqrtfor right-skewed) and rebuild; (c) accept the warning if the dim is rarely top-of-rank — non-normal dims rarely dominatedelta_normrankings on their own. Composes withnegative_space: ifnegative_spacealready fires on the same dim,non_normal_dimis suppressed (the kind itself is the bug, not the empirical departure).- No
dim_quality_warningsblock: pattern passed all five checks. Don't infer "perfectly healthy" — the warnings only catch the five specific silent-failure modes; other quality issues (correlated dims, stale data, drifted calibration) need separate diagnostics.
Regime changes
find_regime_changes(pattern_id, n_regimes=5)
Interpreting regime change results
- 1 unidirectional shift = regime change (external event, policy change)
- 2 shifts in opposite directions = oscillation (seasonal, cyclical)
- ALL entities dropping simultaneously = data boundary artifact, not real change
- Multiple small shifts = gradual evolution
Use compare_time_windows(pattern_id, pre_start, pre_end, post_start, post_end)
to identify which dimensions shifted and by how much.
Emerging anomalies
find_anomalies(pattern_id, top_n=10, include_emerging=True)
Emerging entities are trending toward the anomaly boundary but haven't
crossed yet. Only act on reliability=high forecasts.
If edge table available: contagion_score_batch(emerging_keys, pattern_id) — emerging entities with high neighborhood contamination are higher-priority early warnings than those with clean neighborhoods. Pass timestamp_cutoff (Unix seconds) to compare contagion at the previous monitor tick vs now — a jump between the two is a hard alert signal.
Handing off to other skills
Monitor flags, investigator digs. When you find:
- Specific entity with
source_count >= 2— hand to gds-investigator - Sudden trajectory change on one entity — hand to gds-investigator
- Population-level shift — report as finding, no entity drill needed
Anti-patterns
| Anti-pattern | Fix |
|---|---|
Using detail="full" by default |
Start with detail="summary" — "full" adds I/O per pattern and can take minutes on >100K entities |
| Treating drift as anomaly | Drifters may still be within normal range — check is_anomaly |
| Binary geometry drift analysis | Binary geometry produces degenerate drift results — needs continuous geometry |
| Investigating low-magnitude drifters | Focus on top-N by displacement — low-magnitude drift is noise |
When things don't work
- Tool returns empty results — try different parameters, wider sample, or a different pattern. Check that the pattern has temporal data.
- Tool errors — check pattern_id and version, verify sphere is open. If the error persists, report it rather than retrying in a loop.
- No anomalies found — not every sphere has every anomaly type. Report "no signal detected" as a valid finding.
- find_drifting_entities returns no drifters — the population may be stable, or the pattern lacks sufficient temporal slices. Both are valid findings worth reporting.
- find_regime_changes returns no changepoints — the population geometry has been stable over the observed period. Report stability as a finding.
- check_alerts returns no alerts — geometric health is clean, but still
run
find_regime_changesandfind_drifting_entitiesto check for behavioral shifts. No alerts + no drift + no regime changes = truly healthy.
Skill delegation
| Need | Skill |
|---|---|
| Root cause tracing, entity 360, hypothesis testing | gds-investigator |
| Event rates, Simpson's, temporal bursts, drift recipes | gds-detective |
| Cross-pattern, neighbor, trajectory, segment scans | gds-scanner |
| Orientation, profiling, clustering | gds-explorer |
Full monitoring examples: references/examples.md
When alerts say population shifted unexpectedly
If compare_calibrations between two epochs shows large RMS drift on mu or sigma and you don't have an obvious data-source change to explain it, run find_calibration_influencers to see WHICH entities had highest impact on the new calibration. The hidden-influencer cell often catches data-quality regressions (duplicated records, miscategorised entities) that warped μ/σ across the population.
mcp__hypertopos__find_calibration_influencers(
pattern_id="<pattern>",
classify="all",
top_n=10,
)
Inspect the cell_counts distribution — a healthy population is dominated by "normal". A spike in "hidden" or "distorter" per epoch is a signal that recalibration was driven by a small subset, not population-wide drift.