Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions hindsight-integrations/claude-code/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,10 @@ Auto-recall runs on every user prompt. It queries Hindsight for relevant memorie
| `recallContextTurns` | `HINDSIGHT_RECALL_CONTEXT_TURNS` | `1` | How many prior conversation turns to include when composing the recall query. `1` = only the latest user message; higher values give more context but may dilute the query. |
| `recallMaxQueryChars` | `HINDSIGHT_RECALL_MAX_QUERY_CHARS` | `800` | Maximum character length of the query sent to Hindsight. Longer queries are truncated. |
| `recallRoles` | — | `["user", "assistant"]` | Which message roles to include when building the recall query from prior turns. |
| `recallTags` | `HINDSIGHT_RECALL_TAGS` | `[]` | Optional tags to pass to the recall API, such as `["memory_type:rule"]`. The env var accepts JSON or a comma-separated list. |
| `recallTagsMatch` | `HINDSIGHT_RECALL_TAGS_MATCH` | `"any"` | Tag matching mode used with `recallTags` or `recallTagGroups`: `"any"`, `"all"`, `"any_strict"`, or `"all_strict"`. |
| `recallTagGroups` | `HINDSIGHT_RECALL_TAG_GROUPS` | `null` | Optional compound tag filter passed through to the recall API. The env var must be JSON. |
| `recallAdditionalBankFilters` | `HINDSIGHT_RECALL_ADDITIONAL_BANK_FILTERS` | `{}` | Optional per-bank tag filter overrides for banks listed in `recallAdditionalBanks`, keyed by bank ID. Each value may set `recallTags`, `recallTagsMatch`, and `recallTagGroups`. The env var must be JSON. |
| `recallPromptPreamble` | — | built-in string | Text placed above the recalled memories in the injected context block. Customize this to change how Claude interprets the memories. |

---
Expand Down
9 changes: 9 additions & 0 deletions hindsight-integrations/claude-code/scripts/lib/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ def recall(
max_tokens: int = 1024,
budget: str = "mid",
types: Optional[list] = None,
tags: Optional[list] = None,
tags_match: Optional[str] = None,
tag_groups: Optional[object] = None,
timeout: int = 10,
) -> dict:
"""Recall memories from a bank.
Expand All @@ -126,6 +129,12 @@ def recall(
body["budget"] = budget
if types:
body["types"] = types
if tags:
body["tags"] = tags
if tags_match:
body["tags_match"] = tags_match
if tag_groups:
body["tag_groups"] = tag_groups
return self.request("POST", path, body, timeout=timeout)

def retain(
Expand Down
20 changes: 20 additions & 0 deletions hindsight-integrations/claude-code/scripts/lib/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
"recallContextTurns": 1,
"recallMaxQueryChars": 800,
"recallRoles": ["user", "assistant"],
"recallTags": [],
"recallTagsMatch": "any",
"recallTagGroups": None,
"recallAdditionalBankFilters": {},
"recallPromptPreamble": (
"Relevant memories from past conversations (prioritize recent when "
"conflicting). Only use memories that are directly useful to continue "
Expand Down Expand Up @@ -73,6 +77,10 @@
"HINDSIGHT_RECALL_MAX_TOKENS": ("recallMaxTokens", int),
"HINDSIGHT_RECALL_MAX_QUERY_CHARS": ("recallMaxQueryChars", int),
"HINDSIGHT_RECALL_CONTEXT_TURNS": ("recallContextTurns", int),
"HINDSIGHT_RECALL_TAGS": ("recallTags", list),
"HINDSIGHT_RECALL_TAGS_MATCH": ("recallTagsMatch", str),
"HINDSIGHT_RECALL_TAG_GROUPS": ("recallTagGroups", dict),
"HINDSIGHT_RECALL_ADDITIONAL_BANK_FILTERS": ("recallAdditionalBankFilters", dict),
"HINDSIGHT_API_PORT": ("apiPort", int),
"HINDSIGHT_DAEMON_IDLE_TIMEOUT": ("daemonIdleTimeout", int),
"HINDSIGHT_REQUEST_TIMEOUT_SECONDS": ("requestTimeoutSeconds", int),
Expand All @@ -93,8 +101,20 @@ def _cast_env(value: str, typ):
return value.lower() in ("true", "1", "yes")
if typ is int:
return int(value)
if typ is list:
parsed = json.loads(value)
if isinstance(parsed, list):
return parsed
return None
if typ is dict:
parsed = json.loads(value)
if isinstance(parsed, (dict, list)):
return parsed
return None
return value
except (ValueError, AttributeError):
if typ is list:
return [part.strip() for part in value.split(",") if part.strip()]
return None


Expand Down
18 changes: 18 additions & 0 deletions hindsight-integrations/claude-code/scripts/recall.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,11 @@ def _dbg(*a):

debug_log(config, f"Recalling from bank '{bank_id}', query length: {len(query)}")

recall_tags = config.get("recallTags") or None
tag_groups = config.get("recallTagGroups") or None
tags_match = config.get("recallTagsMatch") if recall_tags or tag_groups else None
additional_bank_filters = config.get("recallAdditionalBankFilters") or {}

# Call Hindsight recall API
try:
response = client.recall(
Expand All @@ -154,6 +159,9 @@ def _dbg(*a):
max_tokens=config.get("recallMaxTokens", 1024),
budget=config.get("recallBudget", "mid"),
types=config.get("recallTypes"),
tags=recall_tags,
tags_match=tags_match,
tag_groups=tag_groups,
timeout=10,
)
except Exception as e:
Expand All @@ -165,13 +173,23 @@ def _dbg(*a):
# Also recall from any additional banks (e.g. shared user profile bank)
additional_banks = config.get("recallAdditionalBanks", [])
for extra_bank_id in additional_banks:
extra_filter = additional_bank_filters.get(extra_bank_id, {})
extra_tags = extra_filter.get("recallTags", recall_tags) or None
extra_tag_groups = extra_filter.get("recallTagGroups", tag_groups) or None
extra_tags_match = extra_filter.get(
"recallTagsMatch",
tags_match if extra_tags or extra_tag_groups else None,
)
try:
extra_response = client.recall(
bank_id=extra_bank_id,
query=query,
max_tokens=config.get("recallMaxTokens", 1024),
budget=config.get("recallBudget", "mid"),
types=config.get("recallTypes"),
tags=extra_tags,
tags_match=extra_tags_match,
tag_groups=extra_tag_groups,
timeout=10,
)
extra_results = extra_response.get("results", [])
Expand Down
4 changes: 4 additions & 0 deletions hindsight-integrations/claude-code/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
"recallContextTurns": 1,
"recallMaxQueryChars": 800,
"recallRoles": ["user", "assistant"],
"recallTags": [],
"recallTagsMatch": "any",
"recallTagGroups": null,
"recallAdditionalBankFilters": {},
"recallPromptPreamble": "Relevant memories from past conversations (prioritize recent when conflicting). Only use memories that are directly useful to continue this conversation; ignore the rest:",
"retainRoles": ["user", "assistant"],
"retainEveryNTurns": 10,
Expand Down
24 changes: 23 additions & 1 deletion hindsight-integrations/claude-code/tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from unittest.mock import MagicMock, patch

import pytest

from lib.client import USER_AGENT, HindsightClient, _validate_api_url


Expand Down Expand Up @@ -149,6 +148,29 @@ def fake_open(req, timeout=None):
assert captured["body"]["budget"] == "high"
assert captured["body"]["types"] == ["world", "experience"]

def test_sends_tag_filters(self):
c = HindsightClient("http://localhost:9077")
captured = {}

def fake_open(req, timeout=None):
captured["body"] = json.loads(req.data.decode())
return FakeResp({"results": []})

with patch("urllib.request.urlopen", side_effect=fake_open):
c.recall(
"bank",
"query",
tags=["memory_type:rule"],
tags_match="any_strict",
tag_groups=[{"op": "all", "tags": ["memory_type:rule", "tech_stack:supabase"]}],
)

assert captured["body"]["tags"] == ["memory_type:rule"]
assert captured["body"]["tags_match"] == "any_strict"
assert captured["body"]["tag_groups"] == [
{"op": "all", "tags": ["memory_type:rule", "tech_stack:supabase"]}
]


class TestHindsightClientRetain:
def test_posts_with_async_true(self):
Expand Down
27 changes: 26 additions & 1 deletion hindsight-integrations/claude-code/tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import os

import pytest

from lib.config import _cast_env, load_config


Expand Down Expand Up @@ -82,6 +81,32 @@ def test_request_timeout_env_override(self, tmp_path, monkeypatch):
cfg = load_config()
assert cfg["requestTimeoutSeconds"] == 60

def test_recall_tags_env_override_accepts_comma_list(self, tmp_path, monkeypatch):
monkeypatch.setenv("CLAUDE_PLUGIN_ROOT", str(tmp_path))
monkeypatch.setenv("HINDSIGHT_RECALL_TAGS", "memory_type:rule, tech_stack:supabase")
cfg = load_config()
assert cfg["recallTags"] == ["memory_type:rule", "tech_stack:supabase"]

def test_recall_tag_groups_env_override_accepts_json(self, tmp_path, monkeypatch):
monkeypatch.setenv("CLAUDE_PLUGIN_ROOT", str(tmp_path))
monkeypatch.setenv(
"HINDSIGHT_RECALL_TAG_GROUPS",
'[{"op":"all","tags":["memory_type:rule","tech_stack:supabase"]}]',
)
cfg = load_config()
assert cfg["recallTagGroups"] == [{"op": "all", "tags": ["memory_type:rule", "tech_stack:supabase"]}]

def test_recall_additional_bank_filters_env_override_accepts_json(self, tmp_path, monkeypatch):
monkeypatch.setenv("CLAUDE_PLUGIN_ROOT", str(tmp_path))
monkeypatch.setenv(
"HINDSIGHT_RECALL_ADDITIONAL_BANK_FILTERS",
'{"normative":{"recallTags":["memory_type:rule"],"recallTagsMatch":"all"}}',
)
cfg = load_config()
assert cfg["recallAdditionalBankFilters"] == {
"normative": {"recallTags": ["memory_type:rule"], "recallTagsMatch": "all"}
}

def test_invalid_settings_json_falls_back_to_defaults(self, tmp_path, monkeypatch):
monkeypatch.setenv("CLAUDE_PLUGIN_ROOT", str(tmp_path))
(tmp_path / "settings.json").write_text("not valid json{{")
Expand Down
Loading