Skip to content

feat(observations): enumerate + filter + visualize observation scopes - #2149

Merged
nicoloboschi merged 9 commits into
mainfrom
feat/observation-scopes-ui
Jun 11, 2026
Merged

feat(observations): enumerate + filter + visualize observation scopes#2149
nicoloboschi merged 9 commits into
mainfrom
feat/observation-scopes-ui

Conversation

@nicoloboschi

Copy link
Copy Markdown
Collaborator

Summary

Observations live under a scope — the exact tag set they were consolidated with — but until now there was no way to see which scopes exist or filter by them. This adds scope enumeration, filtering, document-level surfacing, a live consolidation indicator, and a constellation visualization.

Backend (hindsight-api-slim)

  • exact tag-match mode (engine/search/tags.py) — order-independent set equality (@> AND <@), distinct from all_strict's contains semantics. Needed so scope [a] doesn't match [a, b].
  • GET /observations/scopeslist_observation_scopes enumerates distinct observation scopes (normalized tag sets) with counts; [] = global/untagged scope.
  • Global-scope filtertags_match=exact with no tags filters to untagged observations.
  • observation_scopes captured into documents.retain_params at retain time and surfaced on get_document (new-documents-only by design).

Control plane (hindsight-control-plane)

  • Scope filter — searchable Popover+Command combobox (substring search like the tag filter), exact-scope selection, compact height-capped trigger. Wired into list + graph views.
  • Document detail dialog — shows the configured observation_scopes ("Observation scopes" row).
  • Live consolidation refresh — while pending_consolidation > 0, the observations view polls every 4s and silently refreshes observations/scopes/badge in place (the "In Sync" badge previously read a one-shot, up-to-60s-cached stat).
  • Constellation "Group by scope" — lays observations out around per-scope centroids, colors each scope distinctly, and wraps each in a translucent labeled convex-hull blob.

Tests

  • Unit: exact SQL clause + Python filter.
  • Integration: scope enumeration, exact-scope filter, global-scope filter, observation_scopes round-trip through retain_params.

Notes

  • Also fixes a pre-existing bug: memoryDetailPanel.curation* i18n keys were referenced in code but missing from all locales.
  • OpenAPI + all SDK clients + docs-skill regenerated.

🤖 Generated with Claude Code

Add an exact (set-equality) tag match mode, a list_observation_scopes
engine method + GET /observations/scopes endpoint, and a scope filter in
the control-plane Observations tab (list + graph views). A scope is the
exact tag set an observation was consolidated under; the empty set is the
global/untagged scope. Regenerated OpenAPI + clients + docs skill.
invalidate-memory-dialog.tsx references memoryDetailPanel.curationInvalidateTitle/
Explain/ReasonPlaceholder/Cancel/Invalidate, but these keys were never added to any
locale (the parity test passed because all 10 locales lacked them equally), so the
invalidate dialog logged IntlError: MISSING_MESSAGE and rendered raw key names.
Add all five strings across the 10 locales. Pre-existing gap, unrelated to scopes.
…ti tags

The scope dropdown trigger relied on SelectValue, which clones the selected
item's wrapping pill layout; a multi-tag or long-tag scope (e.g. [session:2,
user:nicolo]) wrapped to two lines and overflowed the fixed-height control.
Render a compact, single-line, truncating summary in the trigger instead,
keeping the full pills only in the open dropdown list.
… detail dialog

observation_scopes passed at retain time was only persisted per source fact
(memory_units), never on the document, so the document detail dialog couldn't
show which scoping was requested. Capture it into documents.retain_params in
_build_retain_params (alongside context/event_date/metadata) and surface it as
a top-level field on the get_document response. The control-plane document
detail dialog now shows an 'Observation scopes' row (mode badge or scope chips).

New-documents-only by design: existing docs have no captured value and show
nothing. Note: this also clarifies that all_combinations on 2 tags correctly
creates 3 scopes — the transient '2' is async consolidation still in flight.
…e constellation

Two UX improvements to the observations view:

1. Live refresh while consolidating. The 'In Sync' badge previously read a
   one-shot, up-to-60s-cached stat, so it could show green while observations
   were still materializing (each scope is a separate consolidation pass). The
   view now polls every 4s while pending_consolidation > 0, silently refreshing
   the observations, scope list, and badge in place until consolidation settles.

2. Group-by-scope clustering on the Constellation. A new 'Group by scope' toggle
   lays observations out around per-scope centroids (instead of the id-hash ring),
   colors each scope distinctly, and wraps each scope's nodes in a translucent,
   labeled convex-hull blob — so overlapping tag scopes read as visual clusters.
   Adds clusterKeyFn/clusterColorFn/clusterLabelFn props to Constellation and an
   inline monotone-chain convex hull; suppresses the heat legend while clustering.
With many scopes the scope filter dropdown grew past the bottom of the screen.
Cap its height at min(60vh, --radix-select-content-available-height) so it fits
the space below the trigger and scrolls for the rest, instead of overflowing.
Replace the plain Select with a Popover + Command (cmdk) combobox so scopes can
be searched by typing — matching the tag filter's search UX — which matters once
a bank has many scopes. Uses a substring filter over each scope's tags (not
cmdk's fuzzy default, which over-matches scattered letters). Keeps the compact,
single-line, height-capped trigger; selection still applies exact-scope filtering.
@nicoloboschi
nicoloboschi force-pushed the feat/observation-scopes-ui branch from 9358baa to d62e10a Compare June 11, 2026 16:16
The new scope-enumeration endpoint powers the control-plane scope filter/clusters
and isn't a useful end-user CLI command, so add it to the [skip] list (matches
the other UI-only endpoints) to satisfy check-cli-coverage.
#2135 removed the TokenUsage re-export from llm_wrapper, but test_load_large_batch
and test_retain still imported it from there, breaking test collection across the
API test jobs. Import it from response_models (where it's defined), matching every
other test.
@nicoloboschi
nicoloboschi force-pushed the feat/observation-scopes-ui branch from 7317b92 to 7833863 Compare June 11, 2026 17:00
@nicoloboschi
nicoloboschi merged commit b08f434 into main Jun 11, 2026
178 of 180 checks passed
@nicoloboschi
nicoloboschi deleted the feat/observation-scopes-ui branch June 11, 2026 17:34
nicoloboschi added a commit that referenced this pull request Jun 16, 2026
…nts (#2230)

The `exact` set-equality match mode landed in the API + generated clients
in #2149 but was never exposed in the control plane, documented, or added
to the hand-maintained SDK wrappers. This completes the feature.

Control plane: add `exact` to the TagsMatch type/unions and to the
tags_match dropdowns in think-view, search-debug-view, and both
mental-model trigger forms; add translated labels to all 10 locales.

Docs: document `exact` in the recall tags_match table + tag_groups,
the reflect tags value list, and the observations scope-listing guide;
regenerate the docs skill mirror.

Clients: add `exact` to the hand-maintained Python and TypeScript wrapper
Literals/unions and docstrings (generated clients already had it; Rust is
generated from openapi.json at build time).

Supersedes #2159.
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