Commit graph

14 commits

Author SHA1 Message Date
DK09876
8364b9c5d5
fix: MCP tool calls fail when MCP_AUTH_TOKEN and TENANT_API_KEY differ (#635)
* 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>
2026-03-20 18:36:05 +01:00
Christian Navolskyi
20e17f28ad
Enhance OpenAI client initialization with query params (#623)
Extract query parameters from base_url when creating the OpenAI client.
2026-03-19 20:46:51 +01:00
Nicolò Boschi
ea662d062e
feat: fact_types and mental model exclusion filters for reflect (#615)
* 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
2026-03-19 17:03:41 +01:00
Chris Bartholomew
94cf89b570
Fix non-atomic async operation creation (#619)
* 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
2026-03-19 16:38:04 +01:00
Chris Bartholomew
438ce98b40
Fix entity_id null constraint for non-ASCII entity names (#612)
* 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
2026-03-19 10:32:47 +01:00
Nicolò Boschi
446c75f3e2
fix: correctly map LLM fact_type \"assistant\" to \"experience\" for DB storage (#609)
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\"
2026-03-19 10:32:08 +01:00
Octopus
1f1462a5f6
feat: upgrade MiniMax default model from M2.5 to M2.7 (#606)
* 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>
2026-03-18 17:15:20 +01:00
Nicolò Boschi
28dac7c7f8
fix: prevent silent memory loss on consolidation LLM failure (#601)
* 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
2026-03-17 20:15:33 +01:00
Nicolò Boschi
e4f8a157c2
feat(retain): verbatim, chunks modes and named retain strategies (#593)
* feat(retain): add verbatim extraction mode

Adds retain_extraction_mode="verbatim" that stores each chunk as-is
without LLM summarization. The LLM still runs to extract entities,
temporal info, and location for full indexability — only the fact text
is replaced with the original chunk content (one memory per chunk).

Useful for RAG-style indexing and benchmarks where original text
must be preserved in memory.

- Add "verbatim" to RETAIN_EXTRACTION_MODES in config.py
- Add VERBATIM_FACT_EXTRACTION_PROMPT with instructions to preserve text
- Add _collapse_to_verbatim() post-processing to enforce 1 fact/chunk
- Expose in bank config UI dropdown with updated description
- Update configuration.md docs with verbatim mode description
- Add unit test for _collapse_to_verbatim and integration test via LLM
- Fix pre-existing main.py CLI override missing new reranker fields
- Fix pre-existing cross_encoder.py ty type error via setattr

* refactor(retain): verbatim mode skips 'what' field entirely

Instead of asking the LLM to echo the chunk text back into 'what' and
then discarding it, verbatim mode now uses a dedicated schema
(VerbatimExtractedFact) that omits the 'what' field altogether.
The LLM only returns metadata (entities, temporal info, location, who),
saving output tokens and avoiding any risk of paraphrasing before the
backfill.

- Add VerbatimExtractedFact / VerbatimFactExtractionResponse models
- Verbatim mode skips causal-relations section (nothing to relate causally)
- _extract_facts_from_chunk: allow missing 'what' in verbatim mode,
  set combined_text="" (backfilled by _collapse_to_verbatim)
- Update verbatim prompt to say DO NOT include 'what'

* feat(retain): add index_only extraction mode

Zero-LLM retain mode: chunks are stored as-is with no LLM call, no
entity extraction, and no temporal indexing. Embeddings still run for
semantic search. User-provided entities via RetainContent.entities
are the sole source of entity data.

Early return placed before the batch-API check so no LLM queue or
concurrency locks are acquired.

- Add "index_only" to RETAIN_EXTRACTION_MODES
- Add _extract_facts_index_only() with pure Python chunking path
- Add to UI dropdown and update description
- Update configuration.md with index_only docs and table entry
- Add unit test asserting zero token usage and exact text preservation

* feat(retain): add named retain strategies

Allows mixing extraction modes in a single bank via named strategies.
Each strategy is a set of hierarchical config overrides (extraction_mode,
chunk_size, entity_labels, entities_allow_free_form, etc.) applied on
top of the resolved bank config at retain time.

- retain_strategies: dict of strategy_name → config overrides (bank config)
- retain_default_strategy: default strategy when none specified (bank config)
- strategy field on /retain request: per-call override
- apply_strategy() in config_resolver applies overrides via dataclasses.replace()
- strategy propagates through retain_batch_async → _retain_batch_async_internal
  and through the async worker task payload
- Any hierarchical field is overridable per strategy, including entity_labels
  and entities_allow_free_form
- Docs updated with strategy configuration example and RRF fairness note
- Unit test for apply_strategy covering overrides, unknown strategy, and
  non-hierarchical field filtering

* feat(retain): add per-item strategy and strategy tests

- Add `strategy` field to `MemoryItem` so individual items in a retain
  request can override the request-level strategy
- Add `strategy` field to `FileRetainMetadata` for per-file strategy
  override in file retain requests
- Group memory items by effective strategy in `api_retain`; each group
  is processed as a separate batch, results are aggregated
- Thread strategy through `submit_async_file_retain` →
  `_handle_file_convert_retain` → retain task payload
- Add `operation_ids` to `RetainResponse` for async requests with
  mixed per-item strategies
- Add `test_strategy_overrides_extraction_mode_for_index_only`: unit
  test verifying a named strategy with index_only bypasses the LLM
- Add `test_retain_request_per_item_strategy_field`: unit test for
  per-item strategy grouping logic

* feat(ui): add retain strategies and default strategy to bank config UI

- Add StrategiesEditor component: per-strategy cards with name input and
  JSON overrides textarea; supports add/remove; validates JSON inline
- Add Default Strategy text input (retain_default_strategy)
- Update RetainEdits type and retainSlice() to include both new fields
- Regenerate OpenAPI spec (retain_strategies, retain_default_strategy,
  per-item strategy on MemoryItem/FileRetainMetadata, operation_ids on
  RetainResponse)

* refactor(ui): move retain strategies into its own dedicated config section

* feat(ui): improve retain strategies UX and add strategy to document dialog

- Strategy form now includes entity section (free form toggle + entity labels editor)
- Default strategy selector moved outside tab panel, above strategy chips
- Strategy tabs redesigned with underline indicator style for clarity
- Remove strategy confirms with AlertDialog
- Fix tab re-render bug when typing strategy name (skipSyncRef)
- Add strategy field to Add New Document dialog (text + per-file for uploads)
- File upload collapsible uses same Document/Tags/Source tabbed layout
- API: validate empty strategy names in config_resolver
- api.ts: add strategy field to retain and uploadFiles types

* fix: forward strategy through HTTP layer and SDK; add integration test

- route.ts: extract and forward `strategy` from request body to retainBatch
- TypeScript SDK: accept and forward `strategy` in retainBatch options and per-item
- config_resolver.py: validate empty strategy name keys on update
- bank-config-view.tsx: merge entity fields into RetainStrategyForm, redesign strategy tabs with underline style, add confirmation dialog for removal, fix tab-reset-on-typing with skipSyncRef, move default strategy selector outside panel
- bank-selector.tsx: add strategy field to Add Document dialog (per-file in tabbed collapsible)
- test_retain.py: add end-to-end integration test verifying named strategy application (index_only = 0 LLM tokens)

* fix: regenerate TypeScript client with strategy field in RetainRequest/MemoryItem

- Regenerate OpenAPI spec to include strategy field in RetainRequest and MemoryItem
- Regenerate TypeScript client from updated spec
- Add strategy to MemoryItemInput interface
- Remove (item as any) cast now that strategy is properly typed

* rename: index_only extraction mode → chunks

* remove top-level strategy from RetainRequest; strategy is per-item only

* fix(clients): update Go and Python generated clients with strategy/operation_ids fields

* fix(ci): update hierarchical field count, add strategy to Rust MemoryItem initializers

* fix(go-client): minimal targeted YAML updates for strategy/operation_ids fields
2026-03-17 18:08:25 +01:00
Nicolò Boschi
d2bfa84bca
docs: add config vars for local reranker FP16 and bucket batching (#589)
* docs: add config vars for local reranker FP16 and bucket batching (#588)

* fix: add missing reranker local fields to CLI config override and fix ty type error

- Add reranker_local_fp16, reranker_local_bucket_batching, reranker_local_batch_size
  to the manual HindsightConfig() constructor call in main.py (CLI override block)
- Replace direct module attribute assignment with setattr() in the transformers 5.x
  monkey-patch so ty can resolve it without raising unresolved-attribute
2026-03-16 17:35:09 +01:00
Fabio Scarsi
e7da7d0e4f
feat: local reranker FP16, bucket batching, and transformers 5.x compatibility (#588)
Three independent, cumulative improvements to LocalSTCrossEncoder:

1. transformers 5.x compatibility patch for XLM-RoBERTa models (Jina v2)
2. FP16 inference (opt-in via HINDSIGHT_API_RERANKER_LOCAL_FP16)
3. Length-sorted bucket batching (opt-in via HINDSIGHT_API_RERANKER_LOCAL_BUCKET_BATCHING)

All behind .env switches with conservative defaults preserving current behavior.

Fixes #586, Closes #587
2026-03-16 15:38:33 +01:00
Nicolò Boschi
21f9f46ca3
fix: support gemini-3.1-flash-lite-preview by preserving thought_signature in tool calls (#568)
Gemini 3.1+ thinking models include a thought_signature field in functionCall
parts. When reconstructing conversation history for subsequent turns, this
signature must be preserved or the API returns 400 INVALID_ARGUMENT.

- Add optional thought_signature field to LLMToolCall
- Capture thought_signature from Gemini response parts
- Pass thought_signature back when reconstructing multi-turn history
- Add gemini-3.1-flash-lite-preview to the LLM provider test matrix
2026-03-13 16:43:01 +01:00
Nicolò Boschi
5de793eec7
feat: compound tag filtering via tag_groups (#562)
* feat: add compound tag filtering via tag_groups

Adds tag_groups to RecallRequest and ReflectRequest to express arbitrary
boolean tag predicates: leaf {tags, match}, and/or/not compounds.
Top-level groups are AND-ed. Existing tags/tags_match unchanged.

Examples:
  Step filter AND user scope:
    tag_groups: [{tags: ["step:5","step:8"], match: "any_strict"},
                 {tags: ["user:alice"], match: "all_strict"}]
  Exclusion:
    tag_groups: [{tags: ["user:alice"], match: "all_strict"},
                 {not: {tags: ["archived"], match: "any_strict"}}]

- Recursive SQL builder (build_tag_groups_where_clause) threads through
  all 4 retrieval strategies (semantic/BM25, temporal, graph, MPFP)
- Python-side filter (filter_results_by_tag_groups) for post-traversal
- 22 new unit tests
- OpenAPI spec + all clients regenerated (Rust, Python, TypeScript, Go)

* fix: add tag_groups: None to Rust CLI struct initializers

* fix: add tag_groups: None to Rust client test RecallRequest initializer

* feat: reject tags+tag_groups together, add tag_groups integration tests

- Add model_validator to RecallRequest and ReflectRequest that returns 422
  when both `tags` and `tag_groups` are set (mutually exclusive)
- Add 5 integration tests for tag_groups compound filtering:
  * validation: 422 when both fields are set
  * AND filter: two leaf groups (step scope AND user scope)
  * OR compound: user:alice OR user:bob
  * NOT compound: user:alice AND NOT archived
  * Nested: user:alice AND (step:5 OR step:8)

* ci: trigger CI run
2026-03-13 14:30:11 +01:00
Nicolò Boschi
15ea23d5d6
feat: introduce hindsight-api-slim and hindsight-all-slim packages (#560)
* feat: introduce hindsight-api-slim and hindsight-all-slim packages

Closes #552

- Move all source code from hindsight-api/ to new hindsight-api-slim/
- hindsight-api-slim has heavy ML deps (torch, sentence-transformers,
  transformers, einops, flashrank, mlx, mlx-lm, safetensors) and
  pg0-embedded as optional extras: [local-ml], [embedded-db], [all]
- hindsight-api becomes a zero-code meta-package depending on
  hindsight-api-slim[all] for full backward compatibility
- Add hindsight-all-slim meta-package: hindsight-api-slim + client + embed
- hindsight-all updated to depend on hindsight-api-slim[all]
- pg0.py: lazy-import pg0 with clear ImportError pointing to [embedded-db]
- Dockerfile: replace sed hack with proper uv sync --extra flags
- Update release.yml, test.yml, lint.sh, release.sh, CLAUDE.md and
  all path references throughout the repo

* refactor: rename hindsight/ directory to hindsight-all/

* docs: document hindsight-api-slim and hindsight-all-slim package variants

Add package variants table and extras explanation to installation.md

* docs: remove emojis from installation.md, use professional tone

* docs: link Docker slim variant to pip package variants section

* docs: consolidate Docker image variants into single table

* ci: fix working-directory paths after package restructure

- Replace all hindsight-api → hindsight-api-slim in test.yml
- Replace hindsight → hindsight-all in test.yml
- Add --extra embedded-db to test-embed API install step

* ci: add local-ml and embedded-db extras to API sync steps

These extras were previously implicit in the old hindsight-api package
(which bundled everything). Now that hindsight-api-slim uses optional
extras, we must explicitly request local-ml and embedded-db in CI.

* ci: add API install step with embedded-db to test-embed smoke test

The smoke test starts hindsight-api as a daemon, which requires pg0-embedded.
Add a dedicated install step for hindsight-api-slim with embedded-db extra
so the daemon can start successfully.

* ci: remove --no-install-project when using optional extras

When --no-install-project is combined with --extra, the optional deps
are not installed because extras require the project to be active.
Remove --no-install-project from steps that need local-ml or embedded-db.

* ci: fix ordering of uv sync steps to preserve optional extras

When uv sync runs for a different workspace member, it removes optional
extras installed for other members. Fix by always running extra-requiring
API sync last, after other workspace member syncs.

Also remove --no-install-project from embedded-db sync in test-embed,
as --no-install-project prevents optional extras from being active.

* ci: add local-ml extra to test-embed API install for smoke test

The smoke test starts the full API server which needs sentence-transformers
for local embeddings (default provider). Add local-ml extra to the install.

* ci: simplify extras with --all-extras and add slim pip smoke test

- Replace explicit --extra local-ml --extra embedded-db with --all-extras
  for cleaner, more maintainable sync steps
- Add test-pip-slim job: tests hindsight-api-slim[embedded-db] without
  local ML models, using Cohere for embeddings/reranking (mirrors Docker
  slim smoke test approach)

* ci: simplify slim smoke test to health check only (mirrors Docker test)
2026-03-13 13:50:03 +01:00