Remove the BFS spreading activation and MPFP (Multi-Path Fact Propagation)
graph retrieval strategies, leaving link_expansion as the sole graph
retrieval algorithm. Rename MPFPTimings to GraphRetrievalTimings and
mpfp_timings field to graph_timings since the timing struct is used by
LinkExpansionRetriever.
Deleted:
- hindsight-api-slim/hindsight_api/engine/search/mpfp_retrieval.py
- hindsight-api-slim/tests/test_mpfp_retrieval.py
Removed config: HINDSIGHT_API_MPFP_TOP_K_NEIGHBORS
* fix(db): respect vector extension config in per-bank index migration
Migration d5e6f7a8b9c0 hardcoded HNSW when creating per-bank partial
vector indexes, ignoring HINDSIGHT_API_VECTOR_EXTENSION. This caused
banks migrated from pre-v0.4.18 to get HNSW indexes even when
pgvectorscale (DiskANN) or vchord was configured.
- Fix the original migration to read the vector extension config
- Add migration a4b5c6d7e8f9 to detect and recreate mismatched indexes
(skipped entirely when extension is pgvector, since those are correct)
* chore: regenerate openapi.json for v0.4.22 version bump
Add a new "Constellation" memory visualization as the default view in the
control plane, powered by @chenglou/pretext for DOM-free text layout on canvas.
- Canvas-rendered zoomable/pannable memory map with spatial label deconfliction
- Nodes colored by link-count heat gradient (Hindsight brand teal→cyan→blue)
- Star-like rendering with varied size/opacity based on connectivity
- Hover shows rich tooltip with full memory metadata (text, entities, tags, dates)
- Hover highlights connected nodes and their links, dims the rest
- Click to select and view memory details in the side panel
- Fullscreen mode toggle
- Link type legend and heat gradient legend on the HUD
Also optimizes the graph API endpoint:
- Entity query now filters by visible unit IDs (was doing full table scan)
- Links query caps at 10k edges sorted by weight (was returning 500k+ uncapped)
- Replaced expensive DISTINCT ON with LEAST/GREATEST sort with simple ORDER BY
When a mental model has tags, refresh_mental_model hardcoded
tags_match="all_strict", causing empty results when most memories
are untagged. Add configurable tags_match and tag_groups fields
to MentalModelTrigger so users can control refresh filtering.
- Add tags_match (any/all/any_strict/all_strict) to override default
- Add tag_groups for compound boolean tag expressions during refresh
- Default behavior unchanged (all_strict when tags present)
- Update both refresh paths (task-based and direct)
- Add UI controls in Create/Update mental model dialogs
- Regenerate OpenAPI spec and client SDKs
CI failures are unrelated to this PR:
- test_mental_models_dimension_change_empty_table: database OID error (infrastructure flake)
- test_reflect_searches_mental_models_when_available: LLM-dependent assertion (flaky)
When using Azure AI Foundry Cohere rerank endpoints, the Cohere SDK
incorrectly appends /v1/rerank to the base_url, but Azure endpoints
already include the full path (e.g., /models/.../invoke). This causes
double-pathing and 404 errors.
This commit modifies CohereCrossEncoder to detect when base_url is
provided and use httpx directly for custom endpoints, while keeping
the native Cohere SDK for standard API usage. The Azure Cohere API
response format is compatible with the native format.
Fixes#783
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Enable passing arbitrary extra_body parameters to OpenAI-compatible API
calls via a JSON-encoded env var. This supports custom model servers
(e.g. vLLM) that need parameters like chat_template_kwargs to control
thinking mode.
Co-authored-by: EMIRHAN GAZI <emirhan.gazi+tcell@turkcell.com.tr>
* feat: add optional LiteLLM SDK embedding output dimensions
Allow configuring an optional output dimension for litellm-sdk embeddings and pass it through only when set, while preserving default behavior.
Made-with: Cursor
* test: assert wrapped init error for invalid dimensions
Add a LiteLLM SDK embeddings test that verifies invalid OpenAI dimensions fail during initialize() and preserve provider error details in the wrapped RuntimeError.
Made-with: Cursor
* feat: expose document_metadata in API and control plane
Add document_metadata (sourced from retain_params.metadata) to both
list and get document endpoints. Display it in the control plane
documents table and detail panel. Drop the unused metadata column
from the documents table (was always stored as empty {}).
* fix: code review fixes for document_metadata feature
- Remove unnecessary `import json as _json` (json already imported at module level)
- Simplify redundant truthiness checks in retain_params parsing
- Regenerate OpenAPI spec and client SDKs (Python, TypeScript, Go)
- Add tests for document_metadata in get_document and list_documents
* feat(ui): improve documents table and detail panel
- Relative timestamps with full date on hover
- Remove context column from table
- Metadata shown as k=v badges (blue, like tags)
- Size in bytes instead of chars
- Document IDs wrap instead of truncating
- Detail panel wider (560px)
- Retain params: context, event_date, metadata badges
* fix(engine): classify first-person agent experiences as 'experience' fact type
The extraction prompt defined "assistant" too narrowly as only "interactions
with assistant (requests, recommendations)", causing the LLM to classify
first-person agent actions (code changes, debugging, discoveries) as "world".
Broadened the fact_type definition in the prompt and Pydantic model descriptions
to cover all first-person actions, experiences, and observations by the speaker.
* style: fix line length in fact_extraction.py
Document the new configurable base URL for the ZeroEntropy reranker
provider added in #766. Also fix a type error where RERANK_URL was
renamed to rerank_url but one usage was missed.
create_bank_hnsw_indexes() hardcoded USING hnsw regardless of the configured
vector extension, causing "column cannot have more than 2000 dimensions for
hnsw index" when using pgvectorscale or vchord with high-dimensional embeddings.
Now reads get_config().vector_extension and uses the appropriate index type:
- pgvector → USING hnsw
- pgvectorscale → USING diskann
- vchord → USING vchordrq
Closes#738
Verbose mode was the only extraction mode that skipped injecting the
retain_mission FOCUS section into its prompt template. Users who set a
retain_mission got no filtering when using verbose mode.
A 429 usage_limit_reached response during verify_connection() caused the
server to refuse to start entirely. Quota exhaustion is not a configuration
error — the server should start and serve retain/recall requests normally,
it just can't make LLM calls until the quota resets.
Co-authored-by: Marco Rutsch <marco@rutimka.de>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: parse query params from base_url in OpenAI embeddings client
The OpenAI-compatible LLM provider already parses query parameters
(e.g. ?api-version=xxx for Azure OpenAI) from the base_url and passes
them as default_query to the OpenAI client. However, the OpenAI
embeddings provider did not do this, causing Azure OpenAI embeddings
to fail with 404 errors at runtime.
This applies the same URL parsing logic from the LLM provider to the
embeddings provider, enabling Azure OpenAI embeddings to work correctly.
* ci: add workflow to build fork Docker image
* ci: add slim image build (no local models)
* ci: remove fork build workflow per review request
---------
Co-authored-by: Antoine Khater <ak@ptgroup.eu>
* fix(claude-code): implement tool_choice support for forced tool calls
The call_with_tools() method now properly handles the tool_choice parameter
to force specific tool calls. Previously, the parameter was accepted but ignored,
causing the reflect agent to fail when trying to force specific tools on each
iteration.
Fixes#732
Changes:
- When tool_choice forces a specific function: filter allowed_tools to only
that tool (with mcp prefix) and add a strong system prompt instruction
- When tool_choice is 'required': add instruction that model must call at
least one tool
- When tool_choice is 'none': clear allowed_tools and mcp_servers to disable
all tools
- When tool_choice is 'auto' (default): no change (existing behavior)
This matches the approach used in the OpenAI provider while adapting to the
Claude Agent SDK's lack of native tool_choice parameter by using allowed_tools
filtering and system prompt instructions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* style: fix ruff formatting in alembic migration
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add max_observations_per_scope bank config
Adds a configurable limit on the number of observations per tag scope.
When the limit is reached, consolidation only updates/deletes existing
observations — no new ones are created. Enforcement is done via a
constrained Pydantic response model (max_length on creates list) so the
LLM structurally cannot exceed the limit, plus prompt guidance.
- Config: HINDSIGHT_API_MAX_OBSERVATIONS_PER_SCOPE (-1 = unlimited)
- Reorder action execution: deletes → updates → creates
- Dynamic _ConsolidationBatchResponse with max_length constraint
- Prompt CAPACITY CONSTRAINT section when near/at limit
- Observations with no tags skip the limit entirely
- Control plane UI field + docs
* fix: strengthen max_observations tests with mock LLM + defensive truncation
- Rewrite integration tests to use MockLLM with deterministic responses
(one observation per fact) instead of relying on real LLM behavior
- Add defensive truncation in _consolidate_batch_with_llm as belt-and-
suspenders — catches LLM providers that ignore JSON schema max_length
- Tests now assert exact counts, not just upper bounds
The stats endpoint JOINs memory_links to memory_units just to filter
by bank_id. With 8.2M+ links per bank this takes 18+ seconds, and
the control plane polls every 10s — perpetually blocking the server.
Add bank_id column directly to memory_links so the query can filter
on ml.bank_id instead of mu.bank_id, letting Postgres push the filter
down before the JOIN.
- Migration: add bank_id TEXT NOT NULL, backfill from memory_units
- All 4 INSERT paths (temporal, semantic, entity, causal) now write bank_id
- Stats query filters on ml.bank_id instead of mu.bank_id
* fix(migrations): use HINDSIGHT_API_MIGRATION_DATABASE_URL when set
Session-level advisory locks are broken when the database URL goes
through PgBouncer in transaction mode: the backend connection is
returned to the pool on COMMIT, orphaning the lock, so multiple pods
can simultaneously run migrations for the same schema.
When HINDSIGHT_API_MIGRATION_DATABASE_URL is set, use it for both
the advisory lock connection and the Alembic run. Callers should
point this at the direct PostgreSQL endpoint (bypassing the pooler)
so the session-level lock is held for the full migration duration.
* refactor(migrations): move MIGRATION_DATABASE_URL to standard config
Wire HINDSIGHT_API_MIGRATION_DATABASE_URL through HindsightConfig
instead of reading os.getenv() directly in migrations.py. Add the
field to the dataclass, from_env(), log_config(), all call sites,
.env.example, and the configuration docs page.
* fix: update test mocks for migration_database_url kwarg and regenerate docs skill
---------
Co-authored-by: Nicolò Boschi <boschi1997@gmail.com>
Port fixes from #461 (claude_code_llm) to codex_llm:
- Replace json.dumps(result) with result.model_dump_json() for Pydantic models to fix TypeError during consolidation
- Wrap record_llm_call tracing block in try/except so logging failures never propagate to retry handler
Co-authored-by: Marco Rutsch <marco@rutimka.de>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: add audit log for feature usage tracking
Add full auditability for all mutating and core API operations across
HTTP, MCP, and system (worker) transports. Audit entries record raw
request/response as JSONB, timing (started_at/ended_at), action, and
transport type.
Backend:
- New audit_log table with JSONB columns for expandability without
future migrations (merge migration of 3 existing heads)
- AuditLogger with fire-and-forget writes via asyncio.create_task
- @audited decorator on 28 HTTP route handlers
- MCP tool audit wrapping for 16 auditable tools
- Worker task execution wrapped with audit_context
- List endpoint with action, transport, date range filters + pagination
- Stats endpoint with per-day counts for charting
- Configurable retention sweep (concurrent-safe DELETE)
Config (env-only, static):
- HINDSIGHT_API_AUDIT_LOG_ENABLED (default: false)
- HINDSIGHT_API_AUDIT_LOG_ACTIONS (comma-separated allowlist, empty=all)
- HINDSIGHT_API_AUDIT_LOG_RETENTION_DAYS (default: -1, keep forever)
Control Plane:
- New "Audit Logs" tab on bank configuration page
- Line chart showing request volume (today/7d/30d) with action filter
- Filterable table with action, transport, date range filters
- Paginated list with detail dialog showing raw request/response JSON
Tests:
- 13 tests covering list, filters, pagination, stats, disabled mode,
action allowlist, and ordering
* fix: split 3-way merge migration into two 2-way merges
Alembic doesn't support 3-parent merge migrations. Split into a no-op
merge of 2 heads (b1c2d3e4f5g6) followed by the audit_log table
migration merging the third head.
* fix: correct merge migration to merge actual 2 heads
The original analysis incorrectly identified 3 heads. There were only 2
(a3b4c5d6e7f8 and c8e5f2a3b4d1). Remove the unnecessary intermediate
merge migration and fix the audit_log migration to merge these 2 heads.
* fix: use 'heads' instead of 'head' in migration runner
Alembic's upgrade('head') fails when multiple heads exist (e.g. from
namespace package overlaps between hindsight-api and hindsight-api-slim).
Using 'heads' (plural) handles this gracefully by upgrading all branches.
* chore: regenerate OpenAPI spec with audit log endpoints
* chore: regenerate TypeScript client and docs skill OpenAPI spec
Python and Go clients still need regeneration (requires Docker).
* chore: regenerate all client SDKs (Python, Go, TypeScript)
Adds generated audit log API clients for Python (audit_api.py),
Go (api_audit.go), and TypeScript client type updates.
- Add 'ark' and 'volcano' as valid LLM providers (both are aliases for Volcano Engine)
- Set default model to 'doubao-pro-32k' for both providers
- Add them to OpenAICompatibleLLM provider list
- Exclude from json_object response format support
Co-authored-by: yishun.eason <yishun.eason@bytedance.com>
* docs(python-client): improve pydoc strings for async-first usage and low-level API access
- Class docstring now clearly documents async-first pattern: a* methods
preferred, sync wrappers for scripts/REPLs only
- Every sync method docstring points to its async counterpart
- Every async method docstring says "preferred"
- Expose 10 low-level API properties (documents, entities, operations,
webhooks, monitoring, etc.) so agents/users can discover the full API
surface without guessing at _-prefixed internals
- Add missing API parameters: tag_groups (recall/reflect), fact_types,
exclude_mental_models, exclude_mental_model_ids (reflect),
observation_scopes/strategy (retain items), background (create_bank)
- Fix areflect missing include_facts param that sync reflect already had
- Sync recall/reflect now delegate to async counterparts (no logic duplication)
* style(retain): format long function call arguments one-per-line
* feat(retain): delta retain — skip LLM re-extraction for unchanged chunks on upsert
When upserting a document (same document_id), instead of deleting all
facts and re-extracting from scratch, compare chunk content hashes
and only process changed/new chunks. Unchanged chunks keep their
existing facts, entities, and links.
- Add content_hash column to chunks table (migration b3c4d5e6f7a8)
- Add chunk delta comparison functions in chunk_storage.py
- Add delta_mode to fact_storage.handle_document_tracking (skip full delete)
- Add update_memory_units_tags for propagating tag changes to existing facts
- Refactor orchestrator into _try_delta_retain and _full_retain paths
- Automatic fallback to full retain for pre-migration data or all-changed scenarios
- Fix ty type error in metrics.py (resource module import on Windows)
- 16 new tests covering entities, links, tags, metadata, edge cases
* refactor(retain): deduplicate delta and full retain paths
Extract shared _insert_facts_and_links() and _extract_and_embed()
functions used by both the full retain and delta retain paths.
Remove delta_mode flag from handle_document_tracking — delta path
uses dedicated upsert_document_metadata() instead.
* chore: regenerate clients, openapi spec, and lockfile
* chore: regenerate docs skill
* feat: Windows native support — run Hindsight without Docker on Windows
Four compatibility fixes that allow Hindsight to run natively on Windows
with an external PostgreSQL + pgvector installation:
1. **pyproject.toml**: Conditional event loop dependency
- `winloop` on Windows (sys_platform == 'win32')
- `uvloop` on Linux/macOS (sys_platform != 'win32')
2. **main.py**: winloop integration via `winloop.install()`
- Patches asyncio event loop policy globally before uvicorn starts
- uvicorn sees "asyncio" but runs winloop underneath (same perf as uvloop)
- Falls back to default asyncio if winloop unavailable
3. **metrics.py**: Guard `resource` module import
- `resource` is Unix-only (getrusage, getrlimit)
- Conditional import with None fallback
- Skip process metrics collection on Windows
4. **fact_storage.py**: Cross-platform strftime
- `%-d` (no-padding day) is glibc-only, fails on Windows
- Replaced with `%d` + `.replace(" 0", " ")` for same output
## Windows Setup Guide
### Prerequisites
- Python 3.11+
- PostgreSQL 17 with pgvector extension
- Ollama (for local embeddings) or external embedding provider
### Install PostgreSQL + pgvector on Windows
```bash
winget install PostgreSQL.PostgreSQL.17
# Build pgvector from source (requires Visual Studio Build Tools)
git clone https://github.com/pgvector/pgvector.git
# In x64 Native Tools Command Prompt:
set PGROOT=C:\Program Files\PostgreSQL\17
nmake /F Makefile.win
nmake /F Makefile.win install
# Enable extension
psql -U postgres -d hindsight -c "CREATE EXTENSION IF NOT EXISTS vector;"
```
### Install and Run Hindsight
```bash
pip install -e ".[embedded-db]"
# Set environment variables
set HINDSIGHT_API_LLM_PROVIDER=openai
set HINDSIGHT_API_LLM_API_KEY=your-api-key
set HINDSIGHT_API_LLM_BASE_URL=https://your-llm-endpoint/v1
set HINDSIGHT_API_LLM_MODEL=your-model
set HINDSIGHT_API_DATABASE_URL=postgresql://postgres@localhost:5432/hindsight
set HINDSIGHT_API_EMBEDDING_PROVIDER=ollama
set HINDSIGHT_API_PORT=8889
hindsight-api
```
Data persists in PostgreSQL on your local disk — survives reboots,
updates, and anything that would wipe a Docker volume.
Tested on Windows 11 with PostgreSQL 17.9, pgvector 0.8.2,
Python 3.11, RTX 5080 (CUDA embeddings + reranking).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: handle strftime ValueError on Windows in fact_storage
The strftime call on occurred_start/occurred_end can raise ValueError
on Windows when the datetime object has unexpected format properties.
Wrap in try/except to gracefully skip date signal rather than crash
the entire retain batch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: control plane UI fixes for recall and data view
- Sanitize NaN cross-encoder scores to 0.0 in reranking pipeline
(Pydantic serializes NaN as JSON null, breaking UI score display)
- Add null-coalesce for score in search debug view to prevent crash
- Switch data view text filter from debounced onChange to Enter key
(avoids slow ILIKE queries on every keystroke for large banks)
- Show loading spinner in search icon during filter requests
- Preserve search/tag filters when clicking "Load more"
* chore: sync generated files after rebase
Adds a proper 'none' provider option so users can run Hindsight as a
chunk store with semantic search but without any LLM dependency, replacing
the hacky workaround of setting provider to 'mock'.
When HINDSIGHT_API_LLM_PROVIDER=none:
- Retain automatically uses chunks mode (no fact extraction)
- Recall works normally (semantic search, BM25, graph retrieval)
- Reflect returns HTTP 400 with clear error message
- Consolidation/observations are disabled
- Mental model refresh returns HTTP 400
- No API key required
* feat(reflect): make source facts in search_observations configurable
The recent fix (#669) hardcoded include_source_facts=False in
search_observations to prevent context overflow. This makes it
configurable via HINDSIGHT_API_REFLECT_SOURCE_FACTS_MAX_TOKENS
(env/tenant/bank), defaulting to -1 (disabled).
- -1: source facts disabled (current behavior, default)
- 0: source facts enabled with no token limit
- >0: source facts enabled with a token budget
* docs: add reflect_source_facts_max_tokens to configuration reference
* fix: update configurable fields count in tests and regenerate docs skill
* feat: add LiteLLM LLM provider for Bedrock and 100+ providers
Add a new `litellm` LLM provider that uses the LiteLLM SDK for chat
completions and tool calling, enabling AWS Bedrock and 100+ other
providers for Hindsight's core engine (retain, recall, reflect).
- New LiteLLMLLM provider in engine/providers/litellm_llm.py
- Registered in factory, valid providers list, and no-api-key set
- Refactored API key validation to use requires_api_key() helper
- Added boto3 dependency for Bedrock auth
- Updated docs: configuration, models, monitoring, providers grid
* feat: add bedrock as first-class LLM provider alias
Add `bedrock` as a dedicated provider name that auto-prepends the
`bedrock/` prefix to model names and delegates to LiteLLMLLM under
the hood. This makes Bedrock support more discoverable — users set
`HINDSIGHT_API_LLM_PROVIDER=bedrock` with plain Bedrock model IDs.
* test: add Bedrock to CI provider tests
- Add bedrock/us.amazon.nova-lite-v1:0 to MODEL_MATRIX in test_llm_provider.py
- Add AWS credential check in should_skip_provider()
- Pass AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION_NAME secrets to test-api job
- Update default bedrock model to amazon.nova-2-lite-v1:0
* fix: regenerate docs skill files and bump memory test timeout
- Regenerate skills/hindsight-docs references after docs changes
- Bump test_llm_provider_memory_operations timeout to 600s for slower
providers like Bedrock via LiteLLM
* test: skip bedrock lite models in memory operations test
Nova Lite has a 10K output token limit which is too low for fact
extraction (requires 64K). The api_methods test (completion, tools,
structured output) already validates the provider works correctly.
* test: use Nova Pro for bedrock CI tests to cover full memory pipeline
Nova Lite only supports 10K output tokens, too low for fact extraction.
Switch to Nova Pro which supports the full 64K output needed for
retain/reflect operations. This ensures bedrock is tested on all
Hindsight functionalities, not just basic API methods.
* test: switch bedrock CI to Nova 2 Lite (supports 64K output tokens)
Nova v1 models (Pro, Lite) have a 10K output token limit which is
too low for fact extraction. Nova 2 Lite supports 64K+ output tokens,
enabling full memory pipeline testing (retain + reflect).
* fix: return metadata in recall responses (#674)
Metadata stored during retain was never retrieved during recall.
Add metadata to all SQL SELECT queries, the RetrievalResult dataclass,
ScoredResult.to_dict(), and MemoryFact construction in the recall pipeline.
* test: add metadata round-trip test for retain→recall
Replace placeholder metadata test with one that actually passes
metadata via retain_batch_async and asserts it is returned on recall.
* fix: parse metadata JSON string from database in MemoryFact
asyncpg may return JSONB columns as strings. Add a field_validator
to MemoryFact.metadata to handle JSON string deserialization.
search_observations in the reflect agent hardcoded include_source_facts=True
with max_source_facts_tokens=-1 (unlimited). For banks with many observations
backed by thousands of facts, a single tool call could produce 300K+ tokens,
exceeding the default 100K context budget and causing forced synthesis with
an empty 'Retrieved Data' section.
The reflect agent synthesizes from observations, not raw backing facts.
Disable source facts to keep payloads proportional to observation count
(~6K vs ~310K in the reporter's case).
The consolidation path already has configurable source fact limits (PR #509,
v0.4.17). The reflect path was not updated.
Fixes#668
Co-authored-by: Kagura Chen <daniyuu19@sjtu.edu.cn>
* fix(entity_resolver): prevent _pending_stats/_pending_cooccurrences memory leak
Add discard_pending_stats() to EntityResolver to clean up both pending dicts
for the current task key. Call it at the start of each _run_db_work attempt so
that exceptions between accumulation and flush_pending_stats() — including
deadlock retries — never leave stale entries keyed by recycled task IDs.
Fixes#660
* test(entity_resolver): add unit tests for discard_pending_stats()
Covers: clears both dicts for current task, is idempotent when empty,
and does not touch entries belonging to other task keys.
No database required — purely in-memory logic.
* test: add unit tests for pg_trgm auto-detection and ValidationResult.accept_with() enrichment
Two recent PRs landed without dedicated tests:
- #626/#649 (pg_trgm fallback in EntityResolver): add 5 mocked unit tests
covering the trigram→full fallback, single-check guarantee, and sticky
downgrade behaviour.
- #639 (accept_with() enrichment): add 7 pure unit tests for the factory
method plus 5 integration tests verifying the engine applies enriched
contents (retain) and tags/tag_groups (recall) returned by validators.
Also verifies RecallContext carries tag filter state.
* fix: remove 504 from reflect OpenAPI spec to fix progenitor Rust client build
progenitor-impl-0.11.2 panics with `assertion failed: response_types.len() <= 1`
when an endpoint declares more than one response type. PR #643 added
`responses={504: ...}` to the reflect decorator, which injected a second
response type into the generated OpenAPI spec and broke the Rust client build.
Remove the `responses=` kwarg — the 504 is still raised at runtime via
JSONResponse(status_code=504), it just won't appear in the OpenAPI schema.
Regenerate openapi.json accordingly.
* chore: sync generated files and ruff formatting (lint + docs skill)
On managed PostgreSQL services (e.g. Azure Flexible Server), the pg_trgm
extension may not be available, causing two failures:
1. Migration c1a2b3d4e5f6 crashes on CREATE EXTENSION
2. Even if migration is bypassed, the default 'trigram' entity lookup
strategy uses the % operator which requires pg_trgm, causing retain
background tasks to fail silently
Changes:
- Migration now gracefully skips pg_trgm and index creation if the
extension cannot be loaded
- EntityResolver auto-detects pg_trgm availability on first use and
falls back to 'full' lookup strategy with a warning log
Co-authored-by: coder999999999 <coder999999999@users.noreply.github.com>
Validators can now return enriched data via ValidationResult.accept_with()
instead of only accepting or rejecting operations. The engine applies
returned fields (contents, tags, tag_groups) to the operation parameters.
- Add accept_with() factory to ValidationResult with optional enrichment
fields: contents, tags, tags_match, tag_groups
- Add tags, tags_match, tag_groups to RecallContext so validators can
see current filter state
- Update _validate_operation to return ValidationResult
- Apply enrichment from result at all retain (2 sites) and recall call
sites in MemoryEngine
- Existing validators using accept()/reject() work unchanged
LLM providers like MiniMax wrap JSON responses in markdown code fences
(```json ... ```), causing JSON parse failures and 5-11 retries per
extraction. The existing fence stripping logic was gated to only
"lmstudio" and "ollama" providers (and for Ollama, unreachable due to
the _call_ollama_native redirect).
Changes:
- Extract _strip_code_fences() helper function
- Apply fence stripping to all providers in call() (not just local)
- Add fence stripping safety net to _call_ollama_native()
- Add 10 tests covering bare JSON, fenced JSON, malformed fences,
and real-world MiniMax response format
Fixesvectorize-io/hindsight#645
Co-authored-by: feniix <feniix@desktop>
* fix(recall): reject empty queries with 400 and fix SQL parameter gap causing IndeterminateDatatypeError
When query text contains only punctuation/symbols (no word characters after
normalization), the BM25 arms are skipped but the old code still placed `limit`
at \$3 in the params list. If tags or tag_groups were also set, their params
(\$4+) were referenced in the SQL while \$3 was a gap, causing PostgreSQL to
raise IndeterminateDatatypeError.
Fix the parameter layout so `limit` is only appended to params when tokens are
present (i.e. when BM25 arms actually use LIMIT \$3), and shift tags_param_idx
from 4 to 3 in the no-tokens path.
Also add a field_validator on RecallRequest.query that rejects empty-after-
normalization queries at the API layer with a 400 before they reach the DB.
* refactor: extract tokenize_query helper and reuse in RecallRequest validator
* fix: MCP tool calls fail when MCP_AUTH_TOKEN and TENANT_API_KEY differ
When both HINDSIGHT_API_MCP_AUTH_TOKEN and ApiKeyTenantExtension are
configured with different values, MCP transport auth passes but tool
execution fails because the MCP token gets re-validated against the
tenant API key in the engine layer.
Add mcp_authenticated flag to RequestContext so the engine skips tenant
re-validation when MCP transport auth already succeeded.
Fixes#627
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: strengthen assertion to verify no auth error in tool response
The original test only checked that "banks" key existed in the response,
which was true even for error responses like {"error": "...", "banks": []}.
Now asserts "error" not in parsed to properly catch auth failures.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add fact_types and mental model exclusion filters to reflect and mental models
Adds three new filtering options to both the reflect endpoint and mental model creation/refresh:
- `fact_types`: restrict which fact types (world, experience, observation) are retrieved.
Disables irrelevant agent tools entirely (no wasted tokens).
- `exclude_mental_models`: skip the search_mental_models tool altogether.
- `exclude_mental_model_ids`: exclude specific mental models by ID (merged with the
existing self-exclusion logic during mental model refresh).
For mental models, options are persisted in the existing `trigger` JSONB column so they
are automatically applied on every refresh. The `UpdateMentalModelRequest` already
proxies `trigger`, so no extra endpoint changes are needed.
Also fixes the test fixture (`pg0_db_url` in conftest.py) to correctly resolve pg0://
URLs and run migrations before tests, which was causing all DB-dependent tests to fail
with "relation public.banks does not exist" when HINDSIGHT_API_DATABASE_URL=pg0://uuuu.
* fix: guard against disabled-tool hallucination and regenerate clients
- Add enabled_tools guard in reflect agent: if an LLM calls a tool that
was excluded (e.g. recall when fact_types=["observation"]), return an
error result instead of executing it
- Regenerate OpenAPI spec and all SDK clients (Go, Python, TypeScript)
to include new fact_types / exclude_mental_models fields
* fix: add missing ReflectRequest fields in Rust CLI struct initializers
* fix: filter hallucinated tool calls before trace to prevent disabled tools appearing in results
* chore: merge main, fix lint formatting and update skills openapi.json
* feat: expose fact_types, exclude_mental_models, exclude_mental_model_ids in control plane UI
* fix: add missing trigger fields to MentalModel type in control plane api.ts
* fix: add missing trigger fields to local MentalModel interface in mental-models-view
* feat: tabbed mental model dialogs (Basic / Options tabs)
* refactor: shared FactTypeFilter component, tabbed mental model dialogs use General tab, clean up labels
* feat: pill-style toggle buttons for fact type filter (blue/emerald/amber per type)
* fix: add spacing between Fact Types label and pills, rename to Exclude all mental models
* Fix non-atomic async operation creation in _submit_async_operation
Previously the method performed two separate database round-trips:
1. INSERT into async_operations with no task_payload (null)
2. submit_task → UPDATE to set task_payload
A process crash or network error between steps 1 and 2 left a row with
task_payload IS NULL permanently. The worker's claim query requires
task_payload IS NOT NULL, so these orphaned rows could never be picked up
and the queue appeared degraded indefinitely.
Fix: build full_payload before the INSERT and include task_payload in the
same INSERT statement, making operation creation atomic. submit_task is
still called afterwards — for SyncTaskBackend it executes the task
immediately (unchanged behaviour); for BrokerTaskBackend it becomes an
idempotent UPDATE (payload already set) kept for symmetry.
* Preserve datetime payloads in atomic async insert
* Fix entity_id null constraint for non-ASCII entity names (Turkish İ etc.)
Python's str.lower() and PostgreSQL's LOWER() produce different results for
some Unicode characters. The most common case is Turkish İ (U+0130):
Python: 'İstanbul'.lower() == 'i\u0307stanbul' (i + combining dot, 2 chars)
PostgreSQL: LOWER('İstanbul') == 'istanbul' (plain i, 1 char)
In _resolve_from_candidates, the fallback SELECT for conflicted entity names
passed Python-lowercased strings to LOWER(canonical_name) = ANY($names), so
PostgreSQL couldn't match them. entity_ids[idx] stayed None, which then
caused a NOT NULL violation on unit_entities.entity_id, failing the entire
retain.
Fix: pass original mixed-case names to the fallback SELECT and use
LOWER(canonical_name) = ANY(SELECT LOWER(n) FROM unnest($2) AS n) so
PostgreSQL lowercases both sides identically. The query also returns the
original input_name so we can add a Python-lowercased key to id_by_name
for the assignment loop that uses Python-lowercased keys.
* Add regression test for Unicode entity conflict
The Pydantic model extraction paths (batch API and parallel extraction) used
fact_from_llm.fact_type directly, bypassing the \"assistant\" → \"experience\"
conversion and causing DB CHECK constraint violations.
Unified the conversion logic across all paths:
- \"assistant\" → \"experience\"
- \"world\" → \"world\"
- anything else: fall back to fact_kind (\"assistant\" → \"experience\"), else \"world\"
* feat: upgrade MiniMax default model from M2.5 to M2.7
MiniMax has released MiniMax-M2.7, their latest model with a 1M context
window (up from 204K). This updates the default model across config,
docs, and examples. M2.5 remains fully compatible for users who prefer it.
- Update PROVIDER_DEFAULT_MODELS to MiniMax-M2.7
- Update .env.example and documentation references
- Add test_minimax_provider.py with M2.7 and backward compat tests
* chore: remove test file per review feedback
---------
Co-authored-by: PR Bot <pr-bot@minimaxi.com>
* fix: prevent silent memory loss on consolidation LLM failure
When all LLM retries are exhausted during consolidation, memories were
being marked consolidated_at unconditionally, permanently excluding them
from future consolidation runs without producing any observations.
Fix with two complementary mechanisms:
- Adaptive batch splitting: on LLM failure, the batch is halved and
retried recursively down to batch_size=1, recovering most transient
failures (rate limits, Pydantic validation on long prompts) without
operator intervention
- consolidation_failed_at column: only single-memory batches that still
fail after all retries are marked here instead of consolidated_at, so
they remain visible and retryable
- New API endpoint POST /v1/default/banks/{bank_id}/consolidation/retry-failed
resets these memories for the next consolidation run
* chore: regenerate OpenAPI spec
* fix: rename consolidation endpoint from /retry-failed to /recover
* fix: add consolidation_failed_at column, adaptive batch splitting, and recovery API
- Migration a3b4c5d6e7f8: add consolidation_failed_at TIMESTAMPTZ column to
memory_units with an index for efficient failure queries; properly chains off
g7h8i9j0k1l2 (backsweep_orphan_observations)
- Consolidator: filter pending memories with consolidation_failed_at IS NULL
so failed memories are not re-fetched in an infinite loop
- Consolidator: adaptive batch splitting — when a batch exhausts all 3 LLM
retries, halve it and retry sub-batches recursively; only single-memory
batches that also exhaust all retries get consolidation_failed_at set
- New tests (9 total) covering: adaptive splitting recovers all memories,
larger batch splitting, single-memory permanent failure, exclusion from
next run, partial batch failure, recover resets columns, recover returns
0 when none failed, recover-then-consolidate succeeds, HTTP endpoint
* chore: regenerate Go, Python, TypeScript clients with recover consolidation endpoint
* feat: add Recover Consolidation action to bank Actions dropdown
* style: apply ruff formatting to http.py and config.py
* fix: handle consolidation scope in large batch test mock LLM
The mock LLM was returning {"facts": ...} for ALL calls including consolidation.
Consolidation doesn't use skip_validation=True so it expects a _ConsolidationBatchResponse
instance, not a raw dict. Before this PR consolidation silently swallowed the AttributeError
(failed=False was returned); now failed=True triggers adaptive splitting and timeouts.
Fix: return _ConsolidationBatchResponse() when scope=="consolidation".
* fix: restrict claude-agent-sdk to macOS platform only (no Linux wheel available)
Also fix pre-existing type errors: use setattr for XLM-RoBERTa monkey-patch
and add missing reranker_local_fp16/bucket_batching/batch_size fields to main.py config constructor.
* fix: add UV_INDEX_STRATEGY=unsafe-best-match to fix markupsafe cp314 wheel conflict
PyTorch CPU index serves markupsafe==3.0.3 with only cp314 wheels.
uv's default first-index strategy stops at the first index with any version
even if no compatible wheel exists. unsafe-best-match searches all indices
for the best compatible wheel, falling back to PyPI for markupsafe.
* fix: use explicit pytorch index to prevent markupsafe wheel conflict
Configure the pytorch CPU index as explicit=true in pyproject.toml so it is
ONLY used for torch (via [tool.uv.sources]). All other packages (including
markupsafe) are resolved exclusively from PyPI, preventing the pytorch index
from serving incompatible cp314-only wheels for non-pytorch packages.
Remove UV_INDEX and UV_INDEX_STRATEGY from CI workflow (no longer needed
since the index is now configured in pyproject.toml).
* ci: trigger CI run
* ci: retry trigger
* ci: trigger after remote URL fix
* ci: add workflow_dispatch to unblock manual trigger
* fix: remove empty env blocks left after UV_INDEX removal
* fix: add type: ignore for optional claude_agent_sdk imports (macOS-only)
* fix: correct type: ignore rules for claude_agent_sdk and fix utcnow deprecation