Adds per-bank configurable safety settings for Gemini/Vertex AI:
- New `HINDSIGHT_API_LLM_GEMINI_SAFETY_SETTINGS` env var (JSON array)
- Hierarchical config field so banks can override via Config API
- ContextVar pattern for zero-signature-change per-request override
- All 6 thresholds supported: UNSPECIFIED, OFF, BLOCK_NONE, BLOCK_LOW_AND_ABOVE, BLOCK_MEDIUM_AND_ABOVE, BLOCK_ONLY_HIGH
- UI: Models > Gemini/Vertex AI section with per-category threshold selectors and link to Google docs
- Graceful handling when bank_config_api feature is disabled
- 12 new tests covering config parsing, GeminiLLM behaviour, and context var override
* feat: entity labels
* feat: entity labels — optional, free_values, multi_value, UI polish
Completes the entity labels system:
**Schema & extraction**
- Dynamic Pydantic Labels model per fact: each group becomes a typed
field (Literal | None, list[Literal], str | None, or list[str])
- `optional: bool` flag per group — non-optional enum fields appear in
JSON schema required array so structured-output providers enforce them
- `free_values: bool` flag per group — accepts any LLM-generated string
instead of a predefined enum; example values shown as hints in prompt
- New `is_label_entity()` helper for labels-only mode filtering that
handles both enum lookup and free_values key-prefix matching
- Sentinel rejection: "None"/"null"/"n/a" strings dropped in post-processing
**BM25 / dense retrieval**
- `text_signals` column on memory_units: entity names + date tokens for
enriched BM25 indexing without polluting stored fact text
- Dense embedding includes occurred_end when it differs from occurred_start
- Alembic migration z1u2v3w4x5y6 (merge revision fixing two heads)
**UI (bank-config-view)**
- Shadcn Switch replaces custom Toggle for both entity-labels and observations
- Shadcn Checkbox for multi/optional/free_values per group
- Input heights bumped to h-8 throughout the editor
- "Label Groups" → "Entity Labels", "Free-form entities" → "Entities"
- Free-text groups show "Example hints" banner in values section
**Tests (45 unit + 3 LLM integration)**
- build_labels_model: single, multi, mixed, free_values optional/required/multi
- is_label_entity: enum match, free_values prefix match, no false positives
- Post-processing: null/absent/string-None/free_values/sentinels/multi-value
- Schema: labels in required, structured object, no labels when unconfigured
- LLM integration: single-value enum, multi-value enum, free_values retain
**Docs**
- retain.md: new Entity Labels section covering groups, flags, examples
- configuration.md: retain_free_form_entities env var + entity_labels note
* fix(tests): update hierarchical fields count for entity_labels additions
entity_labels and retain_free_form_entities are hierarchical fields,
bumping the expected count from 11 to 13.
* fix(migration): rename text_signals revision to avoid collision with main
Main branch claimed z1u2v3w4x5y6 for observation_scopes. Rename our
text_signals migration to a2b3c4d5e6f7, chaining after z1u2v3w4x5y6.
* refactor(entity-labels): simplify free_values — always str|None, no multi
- free_values groups always produce str | None (multi_value and optional
flags are ignored for free text groups — always optional, never multi)
- Prompt section for free_values groups shows only key + description,
no values list (users put examples in the description instead)
- UI: section title "Entities", toggle "Free Form Entities", replace
per-group checkboxes with a type dropdown (Enum / Free text); only
show multi checkbox and values list when type is Enum
- Update tests to reflect new behaviour
* refactor(entity-labels): replace free_values/multi_value booleans with type field
- LabelGroup now uses type: "value" | "multi-values" | "text" instead of
free_values/multi_value boolean pair
- Backward-compat migration converts legacy dicts automatically
- Rename retain_free_form_entities → entities_allow_free_form throughout
- Update UI dropdown to show Single value / Multi-values / Free text
- Remove separate multi checkbox (captured by type selection)
- Update docs examples and configuration.md
- Update all tests to use new field names
* fix(migration): backfill observation_scopes column for DBs with swapped z1u2v3w4x5y6
Local DBs that had z1u2v3w4x5y6 applied when it referred to the old
text_signals migration (before it was renamed to a2b3c4d5e6f7) won't have
observation_scopes in their memory_units table. This migration adds the
column with IF NOT EXISTS so it's a no-op on clean installs.
* feat(entity-labels): add tag field to auto-populate memory unit tags from labels
When a LabelGroup has tag=True, extracted key:value entities for that group
are automatically written to the memory unit's tags array. This lets entity
labels double as tags, enabling immediate filtering via the existing
tags/tags_match API params with no extra infrastructure.
- Add tag: bool = False to LabelGroup
- _inject_label_tags() helper called in both sync and batch extraction paths
- UI: add Tag checkbox per label group row
- Docs: document the new tag field
- Tests: 4 new unit tests covering all tag injection paths
* style: ruff format migration file
* fix(migration): fix multiple alembic heads after rebase — point text_signals after nullable_event_date
* fix(clients): update timestamp field to use Timestamp wrapper type after timestamp=unset feature
* style: ruff format agent.py
* fix(docs): update Go quickstart example to use NullableTimestamp for timestamp field
Change DEFAULT_ENABLE_BANK_CONFIG_API from false to true, update all docs,
error messages, and client docstrings to reflect the new default. Remove
explicit env var overrides in CI and tests that are no longer needed.
* feat: implement hierarchical configuration (system, tenant, bank)
* feat: implement hierarchical configuration (system, tenant, bank)
* docs: add instructions for hierarchical config in CLAUDE.md
* feat: add ENABLE_BANK_CONFIG_API flag (disabled by default)
- Add HINDSIGHT_API_ENABLE_BANK_CONFIG_API env var (default: false)
- Return 403 Forbidden from bank config endpoints when disabled
- Update tests to enable the flag
- Update CLAUDE.md documentation
This provides security control over the bank configuration API,
ensuring it's only accessible when explicitly enabled.
* docs: add hierarchical configuration section
* feat(cli): add bank config commands (config, set-config, reset-config)
- Add 'hindsight bank config' to view bank configuration
- Add 'hindsight bank set-config' to update LLM settings per bank
- Add 'hindsight bank reset-config' to reset to defaults
- Implements client API calls to new bank config endpoints
* fix(cli): fix compilation errors in bank config commands
- Fix type signature: use ApiClient instead of api::Client
- Fix confirmation: use ui::prompt_confirmation instead of ui::confirm
- Fix error handling: use anyhow! macro instead of errors::Error
- Fix type conversion: convert HashMap to serde_json::Map for API call
* feat: implement type-safe hierarchical config with bank overrides
Implements a production-ready hierarchical configuration system that prevents
accidentally using global defaults when bank-specific overrides exist.
- Created StaticConfigProxy that wraps HindsightConfig
- get_config() now returns proxy that blocks access to bank-configurable fields
- Raises ConfigFieldAccessError with clear message when accessing configurable fields
- Added _get_raw_config() for internal use only
- Forces developers to use resolve_full_config(bank_id, context) for bank settings
- Added resolve_full_config() method that returns complete HindsightConfig
- Resolves hierarchy: Global (env) → Tenant → Bank
- No caching to support multi-server deployments (always fresh from DB)
- LLM provider pooling handles expensive operations separately
- Updated entire retain pipeline to pass resolved config through call chain
- memory_engine.py: Resolves config at top level where bank_id/context available
- orchestrator.py: Accepts and passes config to fact_extraction
- fact_extraction.py: Uses passed config instead of get_config()
- utils.py: Added optional config param for backward compatibility
- consolidator.py: Uses resolve_full_config() for enable_observations check
- memory_engine.py: Resolves config before triggering consolidation
- Renamed "Memory Bank" to "Bank Configuration" with tabs
- Combined Stats and Operations into "General" tab
- Consolidated Profile and Configuration into "Configuration" tab
- Moved Actions dropdown to page level (outside tabs)
- Created new component for managing bank-specific config
- Displays configurable fields: retain_chunk_size, retain_extraction_mode, etc.
- Edit via dialog with form validation
- Reset to defaults via AlertDialog confirmation
- Shows field IDs in monospace for clarity
- Visual separation with borders and hover effects
- Removed inline edit mode, switched to dialog-based editing
- Separate dialogs for Disposition and Mission editing
- Read-only display with clear edit buttons
- Removed duplicate stats cards and operations
- bank-stats-view.tsx: Overview statistics (memories, links, documents, pending ops)
- bank-operations-view.tsx: Background operations table with filtering
**Problem**: Consolidation always used global enable_observations, ignoring bank overrides
**Root Cause**: consolidator.py called get_config() instead of resolving bank-specific config
**Solution**: Pass resolved config through the entire pipeline
**Problem**: asyncpg returning JSONB as JSON string instead of parsed dict
**Solution**: Explicit JSON parsing in config_resolver.py with type checking
- All 19 API integration tests pass
- All 10 hierarchical config tests pass
- Retain operations work correctly with bank-specific config
- Consolidation respects bank-specific enable_observations setting
- Updated developer/configuration.md with type-safe config access pattern
- Added examples showing correct usage patterns
- Documented ConfigFieldAccessError and resolution methods
- get_config() now returns StaticConfigProxy (blocks configurable field access)
- Code accessing bank-configurable fields must use resolve_full_config()
- Clear migration path with helpful error messages
Fixes hierarchical configuration to be production-ready with proper type safety.
* refactor: remove LLM client pool and simplify config resolver
Since LLM config (provider, model, api_key) is now static and not
bank-configurable, the LLMClientPool is no longer needed.
Changes:
- Remove hindsight_api/llm_client_pool.py (no longer needed)
- Remove memory_engine._get_bank_llm_config() (dead code, never called)
- Simplify config_resolver.py by eliminating duplication between
resolve_full_config() and get_bank_config()
- get_bank_config() now calls resolve_full_config() and filters results
- Remove outdated "LLM provider pooling" comments from docstrings
All tests pass (10 hierarchical config tests, 19 API integration tests)
* fix: update tests to use _get_raw_config() for configurable fields
Fixed test fixtures that were accessing configurable fields (like
enable_observations) from get_config(), which now raises
ConfigFieldAccessError due to type-safe config access.
Changes:
- test_consolidation.py: Changed enable_observations fixture to use
_get_raw_config() instead of get_config()
- test_consolidation.py: Updated test_consolidation_returns_disabled_status
to set bank config instead of mocking get_config()
- test_link_expansion_retrieval.py: Changed fixture to use _get_raw_config()
- test_observations.py: Changed disable_observations fixture to use
_get_raw_config()
- Regenerated OpenAPI spec and clients
All 39 previously failing tests now pass.
* fix: add missing config parameter to test calls of extract_facts_from_text()
Fixed 45 test failures where tests were calling extract_facts_from_text()
without the new required config parameter.
Changes:
- Added config=_get_raw_config() to all extract_facts_from_text() calls
- Fixed test_main_module.py to patch _get_raw_config instead of get_config
- Updated 6 test files with 37 function call sites
All tests should now pass.
* fix: add missing config parameter to test_skip_podcast_meta_commentary
One more test was missing the config parameter for extract_facts_from_text().