Commit graph

598 commits

Author SHA1 Message Date
Nicolò Boschi
3e967add78
docs: add FAQ entry for conversation retain format (#529)
Addresses common questions from community discussions on the recommended
format and flow for retaining conversations (JSON array vs plain text,
upsert pattern, avoiding pre-summarization).
2026-03-09 15:47:25 +01:00
Nicolò Boschi
00ccf0b218
fix(consolidation): respect bank mission over ephemeral-state heuristic (#525)
* Add Hindsight as git subtree + BCGU noise filtering tests

Adds hindsight server source as a subtree under hindsight-api/ so we
can iterate on server-side fixes directly.

test_bcgu_noise_filtering.py proves that a well-crafted
retain_custom_instructions (BCGU_RETAIN_MISSION) can suppress
talking-head noise at fact extraction time — eliminating the need for
client-side --filter-vision-noise preprocessing.

Tests cover:
- Default mode extracts 3 noise facts from talking-head frame (problem documented)
- BCGU mission produces 0 noise facts from same talking-head frame
- BCGU mission still extracts 2 high-value ChatGPT screen facts correctly
- Mixed doc (2 talking-head + 2 screen): 0% noise ratio with BCGU mission
- Pure talking-head doc: 0 facts extracted

All 5 tests pass in ~32s using gpt-4o-mini.

* fix(consolidation): respect mission context over ephemeral-state heuristic

Two related fixes for the consolidation engine when a bank mission is
configured:

1. **Mission override for ephemeral-state filter** (`prompts.py`):
   The system prompt previously instructed the LLM to discard any fact
   that looked like "ephemeral state" (e.g. current position, transient
   actions).  When a mission is active the mission itself defines what is
   valuable — timestamped screen actions, session events, tool interactions
   may all be mission-critical even though they look ephemeral.  Added a
   MISSION OVERRIDE block that explicitly tells the LLM the mission takes
   priority over the generic ephemeral-state guidance.

2. **Remove contradictory durable-knowledge nudge** (`consolidator.py`):
   The user-prompt builder was injecting "Focus on DURABLE knowledge that
   serves this mission, not ephemeral state" alongside the mission text.
   This phrasing contradicted missions that intentionally capture
   timestamped events.  Replaced with a neutral directive that simply
   signals the mission overrides general rules.

3. **JSON control-character sanitisation** (`consolidator.py`):
   LLMs occasionally embed literal ASCII control characters (0x00–0x1f)
   inside JSON string values, causing `json.loads` to raise a
   JSONDecodeError.  Added a try/except that strips control characters
   and retries the parse before re-raising, preventing spurious failures.

* refactor(consolidation): move sanitize_llm_output to llm_wrapper, reuse in consolidator

- Add `sanitize_llm_output()` to `llm_wrapper.py` as the single canonical
  function for stripping characters that break downstream systems
  (ASCII control chars 0x00-0x08/0x0B-0x0C/0x0E-0x1F/0x7F and Unicode
  surrogates). Tab, newline, and carriage-return are preserved.
- Reduce `_sanitize_text()` in `fact_extraction.py` to a thin wrapper
  that delegates to `sanitize_llm_output()`.
- Update `consolidator.py` to import and call `sanitize_llm_output()`
  directly instead of reimplementing the logic inline.
- Remove test_bcgu_noise_filtering.py (should not have been committed).

* fix(consolidation): apply sanitize_llm_output to observation text fields

sanitize_llm_output was imported but unused after the old _call_llm_once
path was removed. The batch flow uses structured Pydantic output so
there's no raw json.loads call — instead, apply sanitization via
field_validator on _CreateAction.text and _UpdateAction.text so control
characters are stripped before observation text reaches the database.

* fix(entity-resolver): correct mention_count for new entities in batch retain

When the same entity (e.g. "Bob") appears across N items in a single batch
retain, _resolve_entities_batch_impl deduplicates them into one name group
before inserting, then queued only ONE _EntityStat regardless of N. The
flush therefore always incremented mention_count by 1 beyond the INSERT
value — giving 2 for any number of mentions.

Two-part fix:
- INSERT with mention_count=0 so the post-transaction flush is the single
  source of truth for the count (avoids an off-by-one for N=1 as well).
- Append one _EntityStat per original mention (len(g.indices)) instead of
  one per unique name, so flush_pending_stats() adds the correct total N.

This makes the batch path consistent with the single-entity path, which
already accumulates one stat per mention via entities_to_update.
2026-03-09 15:04:36 +01:00
Nicolò Boschi
f7a60f898d
feat: filter operations by type + fix stale auto-refresh closure (#522) (#527)
* feat: filter operations by type + fix stale closure in auto-refresh

- Add `type` query param to GET /operations endpoint and engine layer
- Add operation type dropdown filter in Background Operations UI
- Fix auto-refresh interval using stale statusFilter/offset closure by
  adding filter state to useEffect deps and wrapping loadOperations in
  useCallback (fixes #522)
- Regenerate OpenAPI spec and all SDK clients

* fix: update Rust CLI list_operations call with new type parameter
2026-03-09 13:17:00 +01:00
Nicolò Boschi
7accac94b2
fix: migrate mental_models.embedding dimension alongside memory_units (#526)
ensure_embedding_dimension() now also checks and migrates mental_models.embedding,
fixing silent failures when changing embedding model dimensions. Extracted shared
per-table logic into _migrate_table_embedding_dimension() to avoid duplication.
Adds test coverage for the mental_models dimension migration path.

Fixes #523
2026-03-09 12:23:50 +01:00
Chris Bartholomew
fa3501d448
Fix Iris parser httpx read timeout for file uploads (#524)
The httpx.AsyncClient was created without a timeout parameter,
defaulting to 5 seconds for reads. This is too short for uploading
PDFs to presigned URLs and waiting for Iris API responses. Set
explicit timeouts: 30s default, 120s for reads.
2026-03-09 11:22:44 +01:00
Chris Bartholomew
f88b50a45e
fix: serialize alembic upgrades in-process (#521) 2026-03-09 11:22:24 +01:00
Nicolò Boschi
1b4ad7f435
feat: change tags for a document (#517)
* feat: add update document tags endpoint with observation invalidation

Adds PATCH /v1/default/banks/{bank_id}/documents/{document_id} to change
tags on a document without re-processing content.

- Updates tags on the document and all associated memory units atomically
- Invalidates observations derived from the document's memory units
- Resets consolidated_at on the document's own units for re-consolidation
- Also resets consolidated_at on co-source memories from other documents
  that shared those observations (matching delete_document behavior)
- Triggers async consolidation when observations are invalidated
- 9 new tests covering all invalidation scenarios

UI: adds inline tag editor to the document detail panel in the control plane
Docs: new "Update Document Tags" section in documents.mdx with Python/JS examples

* refactor: simplify UpdateDocumentTagsResponse to {success: true}

* refactor: make PATCH /documents generic update_document endpoint

Renames update_document_tags → update_document (engine + HTTP + clients + UI).
Currently only tags are supported; the structure is open for future fields.
Tags are the only field with side effects (observation invalidation + re-consolidation).
2026-03-07 09:00:13 +01:00
Chris Bartholomew
d2504ac5ed
Fix GCS auth for Workload Identity Federation credentials (#518)
* Fix GCS auth for external_account credentials (Workload Identity)

obstore's built-in credential parsing only supports service_account and
authorized_user JSON types. Use google.auth as a credential_provider
callback to support all credential types including external_account
(Workload Identity Federation), impersonated credentials, and metadata
server credentials.

* Hide GOOGLE_APPLICATION_CREDENTIALS during GCSStore construction

GCSStore eagerly parses the credential file from env vars even when a
custom credential_provider is passed. Temporarily unset the env var
during construction so obstore doesn't choke on external_account
credential files (Workload Identity Federation).

* Support HINDSIGHT_GOOGLE_CREDENTIALS_FILE for GCS auth

When GOOGLE_APPLICATION_CREDENTIALS must be unset to prevent obstore
from parsing unsupported credential types (e.g. external_account),
google.auth can load credentials from HINDSIGHT_GOOGLE_CREDENTIALS_FILE
instead. This avoids mutating env vars at runtime.

* Simplify GCS credential workaround: hide env var during construction

Remove HINDSIGHT_GOOGLE_CREDENTIALS_FILE indirection. Instead, let
google.auth.default() load credentials normally via GOOGLE_APPLICATION_CREDENTIALS,
then temporarily hide the env var during GCSStore() construction so obstore
doesn't try to parse credential types it doesn't support.

* Work around obstore bug: hide env var during GCSStore construction

obstore always parses credential files from GOOGLE_APPLICATION_CREDENTIALS
and the well-known ADC path, even when credential_provider is supplied
(contrary to docs). This crashes on external_account credentials from
Workload Identity Federation.

Temporarily hide the env var during GCSStore() construction. google.auth
has already loaded credentials by this point via credential_provider.
2026-03-07 08:59:51 +01:00
Ben
d9d7021a49
doc: Upgrading OpenClaw's Memory with Hindsight (#515)
* doc: add adding-memory-to-openclaw-with-hindsight blog post

* doc: update OpenClaw blog cover image

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* doc: update OpenClaw blog title

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* doc: add Hindsight Cloud note to external API section

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 12:35:12 -05:00
Nicolò Boschi
e2baca8bfe
feat: mental model history tracking and UI diff view (#516)
* feat: mental model refresh history tracking and UI diff view

- DB migration: add history JSONB column to mental_models table
- Track previous content on each refresh in update_mental_model
- Add get_mental_model_history() engine method
- New GET /mental-models/{id}/history endpoint
- Control plane proxy route and getMentalModelHistory() in api.ts
- MentalModelDetailModal: add History tab with lazy loading, carousel
  navigation (left=older, right=newer), word-level content diff view

* fix: resolve alembic migration head conflict for mental model history

* feat: mental model history tracking, side-by-side diff UI, and config flag

- Track content changes on every mental model update/refresh (persisted in JSONB history column)
- New GET /mental-models/{id}/history endpoint returning changes most-recent-first
- Side-by-side diff view in History tab (Before/After columns, line-level highlights)
- Actions dropdown in detail panel (Edit, Refresh, View History, Delete)
- HINDSIGHT_API_ENABLE_MENTAL_MODEL_HISTORY config flag (default: true)
- Also adds missing HINDSIGHT_API_ENABLE_OBSERVATION_HISTORY to configuration docs
- Python client wrapper method get_mental_model_history()
- Tests for history persistence (recorded, ordered, name-only skipped, missing returns None)
- Fix NameError: timezone not imported in update_mental_model

* fix: call get_mental_model_history before delete in doc example
2026-03-06 17:50:48 +01:00
Nicolò Boschi
576473b6aa
feat: observation history tracking and diff UI (#513)
* feat: add source facts token limits to consolidation and recall

- Add two new configurable (per-bank) parameters:
  - consolidation_source_facts_max_tokens: total token budget for source
    facts across all observations in the consolidation prompt (-1 = unlimited)
  - consolidation_source_facts_max_tokens_per_observation: per-observation
    cap so each observation gets a fair share of source facts (-1 = unlimited,
    default 256)
- Both are also exposed as recall API parameters via SourceFactsIncludeOptions
  (max_tokens and max_tokens_per_observation)
- Consolidation now uses resolve_full_config to respect bank-level overrides
- Improve consolidation prompt: temporal metadata (occurred_start=, | Involving:)
  is now clearly separated from observation text, with a concrete example showing
  the expected synthesis style and explicit rules not to copy raw fact lines
- Add tests for recall source facts capping and consolidation config forwarding
- Expose all three new fields in the control plane bank config UI
- Document new env vars in configuration.md
- Regenerate OpenAPI spec and all SDK clients

* fix: reorder observations UI fields and rename Label Groups to Entity Labels

* fix: revert Entities section title (only rename inner label)

* doc: add consolidation source facts and batch size fields to memory-banks docs

* feat: add observation history tracking and UI diff view

- Track observation changes over time in a JSONB history column,
  appending each update's previous state (text, tags, dates, sources)
  instead of overwriting
- Add HINDSIGHT_API_ENABLE_OBSERVATION_HISTORY config flag (default: true)
  to toggle history recording
- Expose history field in get_memory_unit for observations
- Fix observations/[modelId] route that was proxying to wrong endpoint
- Add History tab in observation modal and History section in panel,
  showing word-level and tag diffs between each change (newest first)
- Extract shared ObservationHistoryView component used by both modal and panel
- Add --random-port flag to start.sh to run multiple dev instances
- Scope Next.js distDir by port to prevent lock file collisions between instances
- Restyle consolidation pending badge (rounded-md with border) and add
  inline refresh button; fix loading flicker on data refresh

* feat: dedicated observation history endpoint with source facts diff

- Add GET /memories/{id}/history endpoint returning enriched history with
  resolved source fact texts and is_new flags per change
- Deprecate history field in GET /memories/{id} (always returns empty list)
- Reconstruct cumulative source facts per history entry by working backwards
  from current state, marking newly added facts with is_new
- Replace inline history panel with "View History" button opening modal
- History modal fetches from dedicated endpoint lazily on tab switch
- Timeline view now opens MemoryDetailModal instead of side panel
- History view uses prev/next navigation (left = older, right = newer)
- Fix --random-port: pass dynamic API_PORT as HINDSIGHT_CP_DATAPLANE_API_URL
  to control plane, preserving caller values over .env
2026-03-06 16:16:05 +01:00
Nicolò Boschi
99220d0527
feat: per-request file parser selection with fallback chains (#514)
* feat: allow per-request file parser selection with fallback chains

Clients can now specify which parser(s) to use when calling the file
retain endpoint, instead of being locked to the server-side default.

Changes:
- `parser` field added to `FileRetainRequest` (request-level default)
  and `FileRetainMetadata` (per-file override); accepts a single name
  or an ordered fallback chain (list)
- Resolution priority: per-file > request-level > server default
- `HINDSIGHT_API_FILE_PARSER` now accepts a comma-separated fallback
  chain (e.g. `iris,markitdown`); fully backward-compatible
- New `HINDSIGHT_API_FILE_PARSER_ALLOWLIST` env var restricts which
  parsers clients may request (defaults to all registered parsers)
- Invalid/disallowed parser names are rejected with HTTP 400
- `FileParserRegistry.convert_with_fallback()` tries each parser in
  order, falling back on UnsupportedFileTypeError, empty content, or
  any other error
- Worker updated to use the fallback chain stored per-task
- OpenAPI spec and all generated clients regenerated

* fix: handle on_file_convert_complete hook and rebase onto main

- Return ConvertResult dataclass from convert_with_fallback() instead
  of a plain str, carrying both the content and the winning parser name
- Use winning_parser_name in the on_file_convert_complete hook so
  parser_name reflects the parser that actually succeeded, not the chain
- Update all test calls to submit_async_file_retain() to use the new
  per-item parser field instead of the removed top-level parser= kwarg

* docs: document HINDSIGHT_API_FILE_PARSER fallback chain and ALLOWLIST
2026-03-06 16:15:43 +01:00
Nicolò Boschi
8540c33236
refactor: remove dead code and clarify observations vs mental models (#512)
* refactor: remove dead code and clarify observations vs mental models

- Delete engine/mental_models/ module (stale Pydantic models with wrong
  schema, describing an old design where mental models were directives;
  had no importers outside itself)
- Remove unused imports in api/http.py (acquire_with_retry, Observation)
- Remove unused Pydantic models in api/http.py (BanksResponse,
  ObservationEvidenceResponse)
- Add clarifying NOTE to consolidation/consolidator.py distinguishing
  observations (auto-generated bottom-up) from mental models (user-defined
  pinned reflections refreshed via reflect)

* chore: run generate scripts after dead code removal
2026-03-06 14:39:33 +01:00
Nicolò Boschi
5d05962db0
feat: add source facts token limits to consolidation and recall (#509)
* feat: add source facts token limits to consolidation and recall

- Add two new configurable (per-bank) parameters:
  - consolidation_source_facts_max_tokens: total token budget for source
    facts across all observations in the consolidation prompt (-1 = unlimited)
  - consolidation_source_facts_max_tokens_per_observation: per-observation
    cap so each observation gets a fair share of source facts (-1 = unlimited,
    default 256)
- Both are also exposed as recall API parameters via SourceFactsIncludeOptions
  (max_tokens and max_tokens_per_observation)
- Consolidation now uses resolve_full_config to respect bank-level overrides
- Improve consolidation prompt: temporal metadata (occurred_start=, | Involving:)
  is now clearly separated from observation text, with a concrete example showing
  the expected synthesis style and explicit rules not to copy raw fact lines
- Add tests for recall source facts capping and consolidation config forwarding
- Expose all three new fields in the control plane bank config UI
- Document new env vars in configuration.md
- Regenerate OpenAPI spec and all SDK clients

* fix: reorder observations UI fields and rename Label Groups to Entity Labels

* fix: revert Entities section title (only rename inner label)

* doc: add consolidation source facts and batch size fields to memory-banks docs
2026-03-06 13:01:33 +01:00
Chris Bartholomew
1d17dea2f1
Add on_file_convert_complete extension hook after file-to-markdown conversion (#507)
* Add file upload API with parser selection and conversion hooks

- Add FileRetainRequest.parser field for per-request parser selection
- Add FileConvertResult dataclass and on_file_convert_complete extension hook
- Fire hook after file-to-markdown conversion with output text for metering
- Fix obstore.Bytes incompatibility with httpx in Iris parser (GCS returns
  obstore.Bytes instead of plain bytes)
- Export new types from extensions __init__

* remove parser field from FileRetainRequest API

Parser selection remains server-side only via HINDSIGHT_API_FILE_PARSER config.

* test: add tests for on_file_convert_complete extension hook

Verifies that the hook is called with correct parameters on success,
called once per file for multi-file uploads, and not called when
file conversion fails.

* test: verify tenant_id propagation to on_file_convert_complete hook

---------

Co-authored-by: Nicolò Boschi <boschi1997@gmail.com>
2026-03-06 09:56:53 +01:00
Nicolò Boschi
928dc696e8
doc: document all missing bank config fields in memory-banks.mdx (#508)
- Add retain_chunk_size (max chars per chunk for fact extraction)
- Rename mission → reflect_mission to match actual API field name
- Add mcp_enabled_tools (per-bank MCP tool allowlist)
- Add llm_gemini_safety_settings (Gemini/VertexAI content filtering)
2026-03-06 09:55:45 +01:00
Nicolò Boschi
e4dd654ec5
fix: openclaw tests + split doc-examples CI per language (#503)
* fix: update openclaw tests to use before_prompt_build hook and split doc-examples CI per language

- Update hooks.integration.test.ts: rename describe block and all
  triggerHook calls from 'before_agent_start' to 'before_prompt_build'
  to match the hook registered in index.ts (changed in PR #480)
- Fix 'includes the user message' test: prependContext contains memories
  (bullet list), not the raw user query; update assertion accordingly
- Split test-doc-examples CI job into a matrix over [python, node, cli, go]
  so each language runs in parallel; language-specific setup steps
  (Rust/CLI build, Node.js, Python client, TypeScript client) are
  conditional on matrix.language to avoid unnecessary work

* fix: spy on HindsightClient prototype to intercept all per-bank client instances

getClientForContext creates new HindsightClient instances per bank when
dynamicBankId is true, so vi.spyOn(c, 'recall') on the default client
never captured calls. Spy on HindsightClient.prototype instead so all
dynamically created bank clients are intercepted.
2026-03-06 09:01:56 +01:00
Derek Bouius
0ad8c2d09c
fix: refresh bank list when dropdown is opened (#504)
Previously, the bank selector dropdown only loaded banks on initial page
load, requiring a full page refresh to see newly created banks. Now calls
loadBanks() each time the popover opens.
2026-03-05 23:14:54 +01:00
Derek Bouius
1e40cd22a6
fix: truncate long bank names in selector dropdown (#505)
Long bank names overflowed the fixed-width selector button. Wraps the
label text in a truncate span so it ellipsizes gracefully.
2026-03-05 23:14:36 +01:00
Nicolò Boschi
1e5aa7de4d
doc: add 0.4.16 release blog post and changelog (#502) 2026-03-05 18:29:55 +01:00
Nicolò Boschi
58fdac44f7 Release v0.4.16
- Update version to 0.4.16 in all components
- Regenerate OpenAPI spec and client SDKs
- Python packages: hindsight-api, hindsight-dev, hindsight-all, hindsight-litellm, hindsight-crewai, hindsight-pydantic-ai, hindsight-embed
- Python client: hindsight-clients/python
- TypeScript client: hindsight-clients/typescript
- Rust CLI: hindsight-cli
- Control Plane: hindsight-control-plane
- OpenClaw integration: hindsight-integrations/openclaw
- AI SDK integration: hindsight-integrations/ai-sdk
- Chat SDK integration: hindsight-integrations/chat
- Helm chart
- Sync documentation to version-0.4
2026-03-05 17:54:59 +01:00
Nicolò Boschi
891c33b1d7
fix: preserve None temporal fields for observations without source dates (#501) 2026-03-05 17:42:23 +01:00
Ben
7ed57fdd85
doc: Give Your OpenAI App a Memory in 5 Minutes (#498)
* doc: add add-memory-to-openai-application blog post


---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 11:34:04 -05:00
Nicolò Boschi
e0a2ac63e7
fix: run schema migrations in thread to prevent event loop deadlock (#500)
When a new tenant schema is provisioned while retain/recall operations
are in-flight, run_migration() was calling synchronous migration
functions directly on the asyncio event loop. These functions execute
CREATE INDEX CONCURRENTLY, which waits for all active transactions to
commit. But in-flight asyncpg transactions cannot flush their COMMIT
because the event loop is blocked — deadlock.

Fix: wrap all four sync migration calls in asyncio.to_thread() so they
run in the thread pool, keeping the event loop free.

Reproduced with the unfixed code: test_retain_memory timed out with
httpx.ReadTimeout when run concurrently with test_create_tenant.
All 75 integration tests pass after the fix.
2026-03-05 17:19:51 +01:00
Chris Bartholomew
75b95106ba
fix: use correct schema name in webhook outbox callback to prevent silent transaction rollback (#499)
The retain outbox callback was passing context.tenant_id (raw UUID like
0f3ad4ec-8b88-...) instead of the PostgreSQL schema name (tenant_0f3ad4ec_...).
This caused the webhook manager to query a non-existent schema, triggering a
PostgreSQL error that silently aborted the entire retain transaction — rolling
back all inserted memory data with no clear indication of data loss.

Fixed both the async worker path (memory_engine.py) and sync HTTP path (http.py)
to use _current_schema.get() which holds the correct tenant-prefixed schema name.

Also changed fire_event_with_conn to re-raise exceptions instead of swallowing
them, since errors inside a caller's transaction poison it irreversibly.
2026-03-05 17:07:48 +01:00
Tian Z
d425e93cb4
feat(openclaw): v2 recall/retention controls, scalability fixes, and Gemini safety settings (#480)
* feat(openclaw): squash branch updates for fork PR

* revert(api): drop memory_engine query normalization from this PR

* fix(openclaw): harden hook isolation and sanitize recall logging

* chore(openclaw): gate missing-senderId notice behind debug logger

* fix(openclaw): address remaining PR review follow-ups

* fix(openclaw): address upstream review comments on isolation and tests

* feat(openclaw): prepend current timestamp to recalled memory context

* chore(openclaw): sync package-lock version to 0.4.14

* chore(openclaw): format recall timestamp as yyyy-mm-dd HH:MM

* feat(openclaw): add configurable recall context composition

- Add recallRoles config to filter which message roles are included in recall query context
- Add recallContextTurns to control how many user turns of prior context to include
- Add recallMaxQueryChars to cap composed query length
- Reduce default max_tokens from 2048 to 1024 for recall responses
- Update documentation and plugin schema with new configuration options

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix(openclaw): put latest user message at end of recall query, add debug to schema

- Reorder composed recall query so latest user message is at the bottom,
  giving embedding models the most weight where it matters most
- Update truncateRecallQuery to trim oldest context lines first,
  always preserving the suffix (priority instruction + latest message)
- Add debug flag to openclaw.plugin.json schema
- Update tests to reflect new query order

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix(openclaw): add verbose debug logging for recall/retain

- Log full recall query (not just first 50 chars)
- Log all raw recall results with scores and content before topK trimming
- Log retain transcript preview and document ID

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix(openclaw): strip sender metadata envelope from prior context in recall query

Prior context messages passed to composeRecallQuery contained raw OpenClaw
envelope blocks (Sender/untrusted metadata JSON) which were diluting the
semantic signal of the recall query. Strip them the same way extractRecallQuery
already does for the latest message.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix(openclaw): add debug log for event.messages at recall time

Helps diagnose why recallContextTurns > 1 may not show extra context
by logging message count and roles available in event.messages.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix(openclaw): strip sender metadata envelope from rawMessage before recall query extraction

The rawMessage from Telegram group chats arrives wrapped in a:
  ---
  Sender (untrusted metadata):
  ```json {...}```

  <actual message>
  ---

envelope. This wasn't being stripped before extractRecallQuery used it,
so the full envelope including JSON metadata was being sent as the recall
query, severely diluting semantic relevance.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix(openclaw): warn when recallContextTurns > 1 but event.messages is empty

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix(openclaw): read messages from event.context.sessionEntry.messages for recall and retain

event.messages was always empty — the actual conversation history is at
event.context.sessionEntry.messages. Fall back to event.messages for
backwards compatibility. This fixes recallContextTurns and retain both
being unable to see the conversation history.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix(openclaw): extract stripMetadataEnvelopes helper and apply to retain path

- Add shared stripMetadataEnvelopes() to strip OpenClaw sender/conversation
  metadata blocks from message content in all paths (recall query extraction,
  prior context composition, and retain transcript)
- This prevents metadata-polluted memories (name/sender ID facts) from being
  stored and ensures recall queries contain clean user text only

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix(openclaw): strip metadata envelopes after channel envelope extraction too

The prompt format is: [ChannelName ...]\n<metadata envelope>\n<message>
After extracting content after [ChannelName], the metadata envelope was
still present. Now stripMetadataEnvelopes runs again after the channel
envelope extraction step.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix(openclaw): switch recall hook from before_agent_start to before_prompt_build

before_prompt_build runs after session load and has messages available,
enabling recallContextTurns to work correctly. before_agent_start runs
pre-session with no messages.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix(openclaw): move current time inside memory tag, simplify recall query format

- Move "Current time" line inside <hindsight_memories> so it's not exposed
  to the recall search as part of the query context
- Remove RECALL_QUERY_PRIORITY_INSTRUCTION and "Latest user message:" label
  from composed recall query — the raw message is more effective for
  semantic search without the extra prompt noise

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix(openclaw): address PR review comments on bank ID fallback and memory leaks

- Add early return in deriveBankId when ctx is undefined, falling back
  to static default bank instead of generating a placeholder-filled ID
- Remove unused RECALL_QUERY_PRIORITY_INSTRUCTION dead constant
- Evict from banksWithMissionSet when evicting from clientsByBankId
  to prevent unbounded memory growth in long-running instances
- Fix integration test hook name: before_agent_start → before_prompt_build
- Fix integration test assertions to match actual composeRecallQuery output

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix(openclaw): extract sender ID from inbound metadata blocks for bank ID derivation

Agent-phase hooks (before_prompt_build, agent_end) don't carry senderId in ctx
by design. Parse it from the "Conversation info / Sender (untrusted metadata)"
JSON blocks that OpenClaw injects into the prompt/messages instead.

- Add extractSenderIdFromText() helper that scans all metadata blocks and
  returns the first sender_id / id field found
- before_prompt_build: extract from event.prompt/rawMessage, spread into ctx
  before calling deriveBankId and getClientForContext
- agent_end: scan user messages for the metadata block, spread into effectiveCtx
  before calling deriveBankId and getClientForContext
- Gracefully skipped when senderId is already present in ctx

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix(openclaw): scan messages from end for sender ID to handle group chats

When multiple users have spoken in a session, scanning from the front
returns the first sender in history rather than the one who triggered
the current agent run. Reverse the slice before finding so we always
pick the most recent user message's sender ID.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix(openclaw): use event.messages for sender ID in agent_end, not sessionEntry

sessionEntry.messages is the cleaned-up history without OpenClaw's injected
metadata prefix blocks. event.messages is the raw payload that still contains
the "Conversation info (untrusted metadata)" JSON — so parse sender_id from
there instead.

Also removes the unnecessary senderIdBySession cache added in the previous
attempt, since event.messages has everything needed directly.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix(openclaw): cache sender ID from before_prompt_build for use in agent_end

event.prompt in before_prompt_build contains OpenClaw's injected metadata
blocks with sender_id. event.messages in agent_end is clean history without
them — so parsing messages in agent_end never finds a sender ID.

Fix: cache the resolved sender ID (keyed by sessionKey) when it's extracted
in before_prompt_build, then look it up by sessionKey in agent_end.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* docs(openclaw): revert Auto-Recall token count to 1024 as unchanged from main

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix(openclaw): revert recallMaxTokens default from 2048 to 1024 to match main

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-03-05 16:55:16 +01:00
Nicolò Boschi
cb6d1c469c
fix: resolve chunks for observation results via source_memory_ids (#496)
* fix: resolve chunks for observation results via source_memory_ids

Observations have no direct chunk_id (they are synthesized from source
memories). When include_chunks=True and fact_type includes 'observation',
chunks were silently returned as None.

Fix collects source chunk_ids via a single JOIN on source_memory_ids,
using array_position to preserve observation rank order so observation
source chunks are interleaved at the correct position rather than
appended after all direct-fact chunks.

* fix: use correct run_consolidation method name in test
2026-03-05 14:12:21 +01:00
Nicolò Boschi
4c058b4b98
fix: cap uvicorn graceful shutdown at 5s and enable force-kill on double Ctrl+C (#495)
Add timeout_graceful_shutdown=5 to uvicorn config to prevent the 30-second
shutdown delay and enable force-kill behavior on a second Ctrl+C signal.
2026-03-05 11:35:23 +01:00
Nicolò Boschi
aa8e5475c4
fix: replace additive combined scoring with multiplicative CE boosts (#494) 2026-03-05 11:12:28 +01:00
Nicolò Boschi
ad2cf72aab
perf: add GIN index on source_memory_ids for observation lookup (#485)
* perf: add GIN index on source_memory_ids for observation lookup

Addresses a 927x performance regression (45ms → 0.049ms) reported by a
user with ~77k observations. The array overlap operator (&&) on
source_memory_ids was doing a full sequential scan over all observations,
causing recall timeouts (57-64s) and slow user recall (18-27s avg).

The partial GIN index reduces consolidation recall from timeout to ~15s
and user recall to ~6s.

* fix: use pre-bounded memory_links for observation graph expansion

Replace raw unit_entities join in _expand_observations() with the same
memory_links entity graph used by non-observation fact types. The previous
approach joined unit_entities twice (seeds→entities→connected_sources),
which explodes at scale (30-70s at 100k observations). The LIMIT 500
workaround was non-deterministic and dropped valid results.

Using memory_links (pre-bounded to MAX_LINKS_PER_ENTITY=50 at retain time)
is algorithmically identical to the non-observation entity expansion and
keeps graph retrieval at ~2s p50 even at 100k observations.

Also fix migration down_revision (z1u2v3w4x5y6 → d2e3f4a5b6c7) and add
observation generation + fact-type filtering to the recall perf benchmark.
2026-03-05 10:15:40 +01:00
Ben
3f2a6ec9ce
doc: add MCP blog post (#492)
* doc: add MCP agent memory blog post
2026-03-04 15:02:58 -05:00
Chris Bartholomew
f17406fdf0
Fix bank-level MCP tool filtering for FastMCP 3.x (#491)
FastMCP 3.x replaced _tool_manager.get_tools() with a provider pattern
(LocalProvider._list_tools via _components). The existing wrapper on
_tool_manager.get_tools() silently failed (caught AttributeError) since
_tool_manager no longer exists in v3.

Now wraps FastMCP.list_tools() and FastMCP.get_tool() for v3, while
preserving the _tool_manager approach for v2 compatibility.
2026-03-04 10:29:29 -05:00
Nicolò Boschi
66423b85f5
fix: resolve TypeError when LLM returns invalid JSON across all retries (#488) (#490)
- Rename shadowed `max_retries` variable to `llm_max_retries` and move
  config resolution outside the loop; the old code captured `range(2)`
  then overwrote `max_retries` inside the loop, so comparisons used a
  different value than the loop bound — causing `continue` on the final
  iteration, exhausting the loop, and reaching `raise last_error` where
  `last_error` was still None → TypeError
- Add fallback `raise RuntimeError(...)` after the retry loop so that if
  `last_error` is None a descriptive error is raised instead of None
- Add unit tests covering non-dict JSON responses with various retry counts
2026-03-04 14:17:55 +01:00
Nicolò Boschi
abbf874d84
feat: webhook system with retain.completed event, UI, and docs (#487)
* doc: update cookbook

* fix(cookbook): preserve tag keys during sync, strip local .md links

- Fix extract_tags_from_readme/notebook to return dict[str,str] preserving
  sdk/topic keys instead of bare values, preventing topics like
  "Customer Service" from being misclassified as SDK
- Add strip_local_md_links() to remove relative .md references that
  would cause broken link errors in Docusaurus build

* ci: run test-doc-examples independently without waiting for test-rust-cli

Build the CLI directly in the job instead of downloading the artifact,
so test-doc-examples can start at the beginning in parallel with all other jobs.

* feat: webhook system with task-owned retry, retain.completed event, and UI

- New webhook system: register per-bank webhooks with HMAC signing, configurable
  HTTP method/timeout/headers/params (http_config JSONB), and PATCH support
- Webhook deliveries run as async_operations (webhook_delivery type) with
  task-owned retry via RetryTaskAt exception and exponential backoff
  (60s / 5m / 30m / 2h / 8h, max 6 attempts)
- New retain.completed event fires per-document for both sync and async retain
- Delivery debug info (status code, response body) stored in result_metadata
- Control plane UI: webhooks tab per bank with create/edit/delete and a
  deliveries table with cursor pagination and expandable response details
- 28 webhook tests covering HMAC signing, delivery retries, CRUD endpoints,
  PATCH update, and retain.completed queuing
- Docs page at developer/api/webhooks documenting event payloads and delivery
- OpenAPI spec and all client SDKs (Python, TypeScript, Rust, Go) regenerated

* fix: update tests for task-owned retry model and guard _webhook_manager attribute

- test_worker.py: test_executor_exception_triggers_retry now raises RetryTaskAt
  (plain exceptions are immediate failures in the new system); rename
  test_executor_exception_marks_failed_after_max_retries to
  test_executor_exception_marks_failed_immediately to reflect new semantics
- test_batch_api.py: remove max_retries kwarg from WorkerPoller constructor
- memory_engine.py: use getattr for _webhook_manager in _fire_retain_webhook
  to avoid AttributeError when engine is created without __init__ (tests)

* fix: remove max_retries from benchmark WorkerPoller call

* fix(webhooks): transactional outbox, observations_deleted tracking, sidebar

- Queue webhook delivery rows atomically with the primary operation using the
  transactional outbox pattern — prevents lost events on process crash:
  - Retain (sync + async): outbox_callback passed into orchestrator.retain_batch
    and called inside the DB transaction, replacing the post-commit fire call
  - Consolidation: new _mark_operation_completed_and_fire_webhook combines the
    status UPDATE and webhook INSERT in one transaction
  - Added fire_event_with_conn() to WebhookManager for in-connection delivery

- Track observations_deleted count in consolidation stats and expose it in the
  consolidation.completed webhook payload (was always None)

- Add Webhooks page to docs sidebar

- Document at-least-once delivery guarantee with operation_id dedup guidance

* fix(ui): add retain.completed to available webhook event types

* feat(ui): add delete confirmation dialog for webhooks

* fix(webhooks): include operation_id in task_payload so delivery is marked completed

The task_payload JSON was missing the operation_id field, causing execute_task
to see operation_id=None and skip _mark_operation_completed — leaving every
delivery row stuck in 'pending' forever.

Added a test that inserts a real async_operations row and verifies the status
transitions to 'completed' after a successful execute_task call.

* style: fix prettier formatting in webhooks-view
2026-03-04 14:17:01 +01:00
Nicolò Boschi
51d2fc5309
doc: add ZeroEntropy reranker to models.md (#489) 2026-03-04 13:28:25 +01:00
Nicolò Boschi
ea27ef95ec
fix: resolve all Dependabot security vulnerabilities (#486)
* fix: resolve all Dependabot security vulnerabilities

npm (package-lock.json):
- fast-xml-parser: 4.5.3 → 4.5.4 (critical entity encoding bypass + DoS)
- serialize-javascript: 6.0.2 → 7.0.4 (high RCE via RegExp/Date)
- minimatch: 3.1.2 → 3.1.5, 5.1.6 → 5.1.9, 9.0.5 → 9.0.9 (high ReDoS)
- ajv: 6.12.6 → 6.14.0, 8.17.1 → 8.18.0 (medium ReDoS with $data option)
- qs: 6.14.1 → 6.15.0 (low arrayLimit bypass DoS)
- rollup: 4.57.x → 4.59.0 in ai-sdk and openclaw integrations (high path traversal)

Python (uv.lock / pyproject.toml):
- cryptography: 46.0.3 → 46.0.5 (high subgroup attack on SECT curves)
- pillow: 12.0.0 → 12.1.1 (high out-of-bounds write in PSD loading)
- langchain-core: 1.2.7 → 1.2.17 (low SSRF in ChatOpenAI token counting)
- langsmith: 0.4.42 → 0.7.11 (medium SSRF via tracing header injection)
- protobuf: 6.33.1 → 6.33.5 (high JSON recursion depth bypass)

Rust (Cargo.lock):
- bytes: 1.11.0 → 1.11.1 in hindsight-clients/rust (medium integer overflow)

Remaining unfixable: diskcache <= 5.6.3 (no patched version available)

* fix: remove over-broad schema-utils ajv override that broke docs build

The 'schema-utils': {'ajv': '^8.18.0'} override was forcing schema-utils@3.x
(used by url-loader/file-loader with ajv-keywords@3.x) to use ajv@8.18.0.
In 8.18.0, internal property _formats was renamed to formats, breaking
ajv-keywords@3.x's _formatLimit.js which accesses ajv._formats.date.

Removing the broad override: schema-utils@4.3.3 (root level) already has
ajv@8.18.0 in its nested install from the prior npm update, while
schema-utils@3.x correctly falls back to the hoisted root ajv@6.14.0.
2026-03-04 13:14:50 +01:00
Nicolò Boschi
edf60e0f3c
ci: add linux-arm64 binary to release and CI (#484)
Add aarch64-unknown-linux-gnu build using native ubuntu-24.04-arm
GitHub-hosted runner, avoiding cross-compilation entirely.

- release.yml: add hindsight-linux-arm64 matrix entry
- test.yml: add build-rust-cli-arm64 job to verify compilation on PRs

Closes #483
2026-03-04 09:54:54 +01:00
Ben
719e79a4d9
doc: fix LiteLLM blog — OPINION → OBSERVATION, update title (#482)
* doc: fix OPINION → OBSERVATION and update blog title

* doc: update title to final version
2026-03-03 14:47:44 -05:00
Ben
3857a30491
blog: add LiteLLM persistent memory post (#481)
* Add LiteLLM persistent memory blog post

* doc: add blog image for LiteLLM post

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 14:22:42 -05:00
Nicolò Boschi
3d87ef5cee
doc: update cookbook (#479)
* doc: update cookbook

* fix(cookbook): preserve tag keys during sync, strip local .md links

- Fix extract_tags_from_readme/notebook to return dict[str,str] preserving
  sdk/topic keys instead of bare values, preventing topics like
  "Customer Service" from being misclassified as SDK
- Add strip_local_md_links() to remove relative .md references that
  would cause broken link errors in Docusaurus build

* ci: run test-doc-examples independently without waiting for test-rust-cli

Build the CLI directly in the job instead of downloading the artifact,
so test-doc-examples can start at the beginning in parallel with all other jobs.
2026-03-03 18:46:59 +01:00
Nicolò Boschi
5c3d3274d7
docs: 0.4.15 release blog post and changelog (#477)
* docs: add 0.4.15 release blog post and changelog

* docs: update 0.4.15 blog cover image
2026-03-03 15:52:56 +01:00
Nicolò Boschi
144e4c49d1 Release v0.4.15
- Update version to 0.4.15 in all components
- Regenerate OpenAPI spec and client SDKs
- Python packages: hindsight-api, hindsight-dev, hindsight-all, hindsight-litellm, hindsight-crewai, hindsight-pydantic-ai, hindsight-embed
- Python client: hindsight-clients/python
- TypeScript client: hindsight-clients/typescript
- Rust CLI: hindsight-cli
- Control Plane: hindsight-control-plane
- OpenClaw integration: hindsight-integrations/openclaw
- AI SDK integration: hindsight-integrations/ai-sdk
- Chat SDK integration: hindsight-integrations/chat
- Helm chart
- Sync documentation to version-0.4
2026-03-03 15:03:42 +01:00
Nicolò Boschi
861295dd7c
refactor: replace set_gemini_safety_settings() with LLMProvider.with_config() (#474)
* refactor: replace set_gemini_safety_settings() with LLMProvider.with_config()

Removes the fragile ContextVar-setter pattern where callers had to remember
to call set_gemini_safety_settings() at every operation entry point.

Instead, LLMProvider.with_config(resolved_config) returns a
ConfiguredLLMProvider wrapper that:
- injects per-bank settings (Gemini safety settings) on every call via
  token-based ContextVar set/reset — properly scoped, no leakage
- proxies all attribute access to the underlying provider via __getattr__
- requires zero changes to LLMInterface or any provider implementations

Call sites (retain, reflect, consolidation) now pass
llm_config.with_config(resolved_config) to sub-components instead of
setting a global context var and hoping nothing else runs in between.
This pattern also composes naturally with a future per-bank provider
factory: callers always receive something with a .call() method.

* fix: pass messages/tools as kwargs in ConfiguredLLMProvider to preserve class-level patch compatibility
2026-03-03 15:00:32 +01:00
Nicolò Boschi
15f4b8769b
fix(ts-sdk): send null instead of undefined when includeEntities is false (#476)
* fix(ts-sdk): send null instead of undefined when includeEntities is false

When `includeEntities: false` was passed, the client serialized `entities`
as `undefined`, which is stripped from JSON. The API then applied its
default (`EntityIncludeOptions()` — enabled), silently ignoring the flag.

Fix: send `null` explicitly when `includeEntities === false` so the API
correctly interprets it as "disable entities".

chunks and source_facts are unaffected since their API defaults are null
(disabled), so omitting them from JSON produces the correct behaviour.

Also adds integration tests covering all three states of includeEntities.

* fix(ts-sdk): use toBeFalsy for null entity check in test
2026-03-03 14:54:48 +01:00
Nicolò Boschi
61bf428ba9
perf: fetch all recall chunks in a single query instead of batched while-loop (#475)
Replace the multi-round-trip while-loop in step 5.5 of recall_async with a
single WHERE chunk_id = ANY($1) query covering all candidate chunk IDs.
Token-budget accounting happens in Python after the single fetch.

Measured on a 97K-unit / 98M-link bank (budget=HIGH, include_chunks,
include_entities):
  p50:  1.209s → 0.611s  (−49%)
  mean: 1.534s → 0.772s  (−50%)
  p95:  3.366s → 2.316s  (−31%)

Also update recall_perf.py benchmark to use Budget.HIGH, include_chunks,
include_entities, and a realistic mixed fact_type distribution.
2026-03-03 14:52:48 +01:00
Nicolò Boschi
73ef99e7b1
feat: add configurable Gemini/Vertex AI safety settings (#473)
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
2026-03-03 13:48:29 +01:00
Nicolò Boschi
7942f181c2
fix(performance): improve recall and retain performance on large banks (#469) 2026-03-03 13:35:22 +01:00
Anton Evseev
5aff8e0c70
refactor(openclaw): replace console.log with debug() helper gated by plugin config (#456)
Replace ~73 console.log calls with a debug() helper that is silent by default.
Debug output is now controlled via plugin config param (debug: true) instead of
environment variables, making it easier for users to configure.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 11:05:04 +01:00
DK09876
e407f4bc55
feat: add extension hooks for root routing and error headers (#470)
* feat: add OAuth extension hooks for MCP authentication

Add extension points in core that allow cloud extensions to support
OAuth 2.1 (RFC 9728 / RFC 7591) for MCP server authentication:

- HttpExtension.get_root_router() for well-known endpoint mounting
- AuthenticationError.headers for WWW-Authenticate propagation
- MCP middleware forwards auth error headers to clients

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: document get_root_router and AuthenticationError.headers

Add documentation for the new extension points introduced in the
OAuth extension hooks commit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove OAuth-specific wording from extension docs

Make the AuthenticationError headers example generic instead of
OAuth-specific, since these are general-purpose extension hooks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 10:50:03 +01:00
Ben
8138fa9002
blog: add CrewAI persistent memory post (#471)
* Add CrewAI persistent memory blog post

* Update blog: add image, remove full example and alternatives sections

* Add CrewAI blog hero image
2026-03-02 16:00:27 -05:00