Compare commits

..
2 Commits
Author SHA1 Message Date
Nicolò Boschi 00d46c3a73 other fix 2026-01-28 14:43:35 +01:00
Nicolò Boschi 320712f998 fix(embed): daemon process XPC connection crash on macos 2026-01-28 14:34:42 +01:00
21 changed files with 43 additions and 290 deletions
+2 -2
View File
@@ -2,8 +2,8 @@ apiVersion: v2
name: hindsight
description: Hindsight helm chart
type: application
version: 0.4.0
appVersion: "0.4.0"
version: 0.3.0
appVersion: "0.3.0"
keywords:
- ai
- memory
+1 -1
View File
@@ -46,4 +46,4 @@ __all__ = [
"RemoteTEICrossEncoder",
"LLMConfig",
]
__version__ = "0.4.0"
__version__ = "0.1.0"
@@ -137,26 +137,15 @@ class LocalSTCrossEncoder(CrossEncoderModel):
# which can cause issues when accelerate is installed but no GPU is available.
# Note: We do NOT use device_map because CrossEncoder internally calls .to(device)
# after loading, which conflicts with accelerate's device_map handling.
import os
import torch
# Force CPU mode if HINDSIGHT_FORCE_CPU is set (used in daemon mode to avoid MPS/XPC issues)
force_cpu = os.getenv("HINDSIGHT_FORCE_CPU", "0") == "1"
# Check for GPU (CUDA) or Apple Silicon (MPS)
has_gpu = torch.cuda.is_available() or (hasattr(torch.backends, "mps") and torch.backends.mps.is_available())
if force_cpu:
device = "cpu"
logger.info("Reranker: forcing CPU mode (HINDSIGHT_FORCE_CPU=1)")
if has_gpu:
device = None # Let sentence-transformers auto-detect GPU/MPS
else:
# Check for GPU (CUDA) or Apple Silicon (MPS)
has_gpu = torch.cuda.is_available() or (
hasattr(torch.backends, "mps") and torch.backends.mps.is_available()
)
if has_gpu:
device = None # Let sentence-transformers auto-detect GPU/MPS
else:
device = "cpu"
device = "cpu"
self._model = CrossEncoder(
self.model_name,
@@ -222,21 +211,12 @@ class LocalSTCrossEncoder(CrossEncoderModel):
)
# Determine device based on hardware availability
import os
has_gpu = torch.cuda.is_available() or (hasattr(torch.backends, "mps") and torch.backends.mps.is_available())
force_cpu = os.getenv("HINDSIGHT_FORCE_CPU", "0") == "1"
if force_cpu:
device = "cpu"
if has_gpu:
device = None # Let sentence-transformers auto-detect GPU/MPS
else:
has_gpu = torch.cuda.is_available() or (
hasattr(torch.backends, "mps") and torch.backends.mps.is_available()
)
if has_gpu:
device = None # Let sentence-transformers auto-detect GPU/MPS
else:
device = "cpu"
device = "cpu"
self._model = CrossEncoder(
self.model_name,
@@ -132,26 +132,15 @@ class LocalSTEmbeddings(Embeddings):
# Determine device based on hardware availability.
# We always set low_cpu_mem_usage=False to prevent lazy loading (meta tensors)
# which can cause issues when accelerate is installed but no GPU is available.
import os
import torch
# Force CPU mode if HINDSIGHT_FORCE_CPU is set (used in daemon mode to avoid MPS/XPC issues)
force_cpu = os.getenv("HINDSIGHT_FORCE_CPU", "0") == "1"
# Check for GPU (CUDA) or Apple Silicon (MPS)
has_gpu = torch.cuda.is_available() or (hasattr(torch.backends, "mps") and torch.backends.mps.is_available())
if force_cpu:
device = "cpu"
logger.info("Embeddings: forcing CPU mode (HINDSIGHT_FORCE_CPU=1)")
if has_gpu:
device = None # Let sentence-transformers auto-detect GPU/MPS
else:
# Check for GPU (CUDA) or Apple Silicon (MPS)
has_gpu = torch.cuda.is_available() or (
hasattr(torch.backends, "mps") and torch.backends.mps.is_available()
)
if has_gpu:
device = None # Let sentence-transformers auto-detect GPU/MPS
else:
device = "cpu"
device = "cpu"
self._model = SentenceTransformer(
self.model_name,
@@ -210,21 +199,12 @@ class LocalSTEmbeddings(Embeddings):
)
# Determine device based on hardware availability
import os
has_gpu = torch.cuda.is_available() or (hasattr(torch.backends, "mps") and torch.backends.mps.is_available())
force_cpu = os.getenv("HINDSIGHT_FORCE_CPU", "0") == "1"
if force_cpu:
device = "cpu"
if has_gpu:
device = None # Let sentence-transformers auto-detect GPU/MPS
else:
has_gpu = torch.cuda.is_available() or (
hasattr(torch.backends, "mps") and torch.backends.mps.is_available()
)
if has_gpu:
device = None # Let sentence-transformers auto-detect GPU/MPS
else:
device = "cpu"
device = "cpu"
self._model = SentenceTransformer(
self.model_name,
@@ -2764,7 +2764,7 @@ class MemoryEngine(MemoryEngineInterface):
param_count += 1
units = await conn.fetch(
f"""
SELECT id, text, event_date, context, occurred_start, occurred_end, mentioned_at, document_id, chunk_id, fact_type, tags, created_at, proof_count, source_memory_ids
SELECT id, text, event_date, context, occurred_start, occurred_end, mentioned_at, document_id, chunk_id, fact_type, tags, created_at, proof_count
FROM {fq_table("memory_units")}
{where_clause}
ORDER BY mentioned_at DESC NULLS LAST, event_date DESC
@@ -2777,18 +2777,7 @@ class MemoryEngine(MemoryEngineInterface):
# Get links, filtering to only include links between units of the selected agent
# Use DISTINCT ON with LEAST/GREATEST to deduplicate bidirectional links
unit_ids = [row["id"] for row in units]
unit_id_set = set(unit_ids)
# Collect source memory IDs from observations
source_memory_ids = []
for unit in units:
if unit["source_memory_ids"]:
source_memory_ids.extend(unit["source_memory_ids"])
source_memory_ids = list(set(source_memory_ids)) # Deduplicate
# Fetch links involving both visible units AND source memories
all_relevant_ids = unit_ids + source_memory_ids
if all_relevant_ids:
if unit_ids:
links = await conn.fetch(
f"""
SELECT DISTINCT ON (LEAST(ml.from_unit_id, ml.to_unit_id), GREATEST(ml.from_unit_id, ml.to_unit_id), ml.link_type, COALESCE(ml.entity_id, '00000000-0000-0000-0000-000000000000'::uuid))
@@ -2799,69 +2788,14 @@ class MemoryEngine(MemoryEngineInterface):
e.canonical_name as entity_name
FROM {fq_table("memory_links")} ml
LEFT JOIN {fq_table("entities")} e ON ml.entity_id = e.id
WHERE ml.from_unit_id = ANY($1::uuid[]) OR ml.to_unit_id = ANY($1::uuid[])
WHERE ml.from_unit_id = ANY($1::uuid[]) AND ml.to_unit_id = ANY($1::uuid[])
ORDER BY LEAST(ml.from_unit_id, ml.to_unit_id), GREATEST(ml.from_unit_id, ml.to_unit_id), ml.link_type, COALESCE(ml.entity_id, '00000000-0000-0000-0000-000000000000'::uuid), ml.weight DESC
""",
all_relevant_ids,
unit_ids,
)
else:
links = []
# Copy links from source memories to observations
# Observations inherit links from their source memories via source_memory_ids
# Build a map from source_id to observation_ids
source_to_observations = {}
for unit in units:
if unit["source_memory_ids"]:
for source_id in unit["source_memory_ids"]:
if source_id not in source_to_observations:
source_to_observations[source_id] = []
source_to_observations[source_id].append(unit["id"])
copied_links = []
for link in links:
from_id = link["from_unit_id"]
to_id = link["to_unit_id"]
# Get observations that should inherit this link
from_observations = source_to_observations.get(from_id, [])
to_observations = source_to_observations.get(to_id, [])
# If from_id is a source memory, copy links to its observations
if from_observations:
for obs_id in from_observations:
# Only include if the target is visible
if to_id in unit_id_set or to_observations:
target = to_observations[0] if to_observations and to_id not in unit_id_set else to_id
if target in unit_id_set:
copied_links.append(
{
"from_unit_id": obs_id,
"to_unit_id": target,
"link_type": link["link_type"],
"weight": link["weight"],
"entity_name": link["entity_name"],
}
)
# If to_id is a source memory, copy links to its observations
if to_observations and from_id in unit_id_set:
for obs_id in to_observations:
copied_links.append(
{
"from_unit_id": from_id,
"to_unit_id": obs_id,
"link_type": link["link_type"],
"weight": link["weight"],
"entity_name": link["entity_name"],
}
)
# Keep only direct links between visible nodes
direct_links = [
link for link in links if link["from_unit_id"] in unit_id_set and link["to_unit_id"] in unit_id_set
]
# Get entity information
unit_entities = await conn.fetch(f"""
SELECT ue.unit_id, e.canonical_name
@@ -2879,18 +2813,6 @@ class MemoryEngine(MemoryEngineInterface):
entity_map[unit_id] = []
entity_map[unit_id].append(entity_name)
# For observations, inherit entities from source memories
for unit in units:
if unit["source_memory_ids"] and unit["id"] not in entity_map:
# Collect entities from all source memories
source_entities = []
for source_id in unit["source_memory_ids"]:
if source_id in entity_map:
source_entities.extend(entity_map[source_id])
if source_entities:
# Deduplicate while preserving order
entity_map[unit["id"]] = list(dict.fromkeys(source_entities))
# Build nodes
nodes = []
for row in units:
@@ -2924,15 +2846,14 @@ class MemoryEngine(MemoryEngineInterface):
}
)
# Build edges (combine direct links and copied links from sources)
# Build edges
edges = []
all_links = direct_links + copied_links
for row in all_links:
for row in links:
from_id = str(row["from_unit_id"])
to_id = str(row["to_unit_id"])
link_type = row["link_type"]
weight = row["weight"]
entity_name = row.get("entity_name")
entity_name = row["entity_name"]
# Color by link type
if link_type == "temporal":
@@ -58,7 +58,6 @@ def _normalize_tool_name(name: str) -> str:
- 'functions.done' (OpenAI-style prefix)
- 'call=functions.done' (some models)
- 'call=done' (some models)
- 'done<|channel|>commentary' (malformed special tokens appended)
Returns the normalized tool name (e.g., 'done', 'recall', etc.)
"""
@@ -70,11 +69,6 @@ def _normalize_tool_name(name: str) -> str:
if name.startswith("functions."):
name = name[len("functions.") :]
# Handle malformed special tokens appended to tool name
# e.g., 'done<|channel|>commentary' -> 'done'
if "<|" in name:
name = name.split("<|")[0]
return name
@@ -155,6 +155,7 @@ class LinkExpansionRetriever(GraphRetriever):
all_seeds.extend(temporal_seeds)
if not all_seeds:
logger.info("[LinkExpansion] No seeds found, returning empty results")
return [], timings
seed_ids = list({s.id for s in all_seeds})
-6
View File
@@ -140,12 +140,6 @@ def main():
args.port = DEFAULT_DAEMON_PORT
args.host = "127.0.0.1" # Only bind to localhost for security
# Force CPU mode for daemon to avoid macOS MPS/XPC issues
# MPS (Metal Performance Shaders) has unstable XPC connections in background processes
# that can cause assertion failures and process crashes at the C++ level
# (which Python exception handlers cannot catch)
os.environ["HINDSIGHT_FORCE_CPU"] = "1"
# Check if another daemon is already running
daemon_lock = DaemonLock()
if not daemon_lock.acquire():
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "hindsight-api"
version = "0.4.0"
version = "0.3.0"
description = "Hindsight: Agent Memory That Works Like Human Memory"
readme = "README.md"
requires-python = ">=3.11"
-90
View File
@@ -1897,93 +1897,3 @@ class TestMentalModelRefreshAfterConsolidation:
# Cleanup
await memory.delete_bank(bank_id, request_context=request_context)
@pytest.mark.asyncio
async def test_graph_endpoint_observations_inherit_links_and_entities(
self, memory: MemoryEngine, request_context
):
"""Test that graph endpoint shows links and entities for observations filtered by type.
When filtering graph by type=observation:
- Observations should inherit links from their source memories
- Observations should show entities inherited from source memories
- Even when source memories are not visible, their links should be copied to observations
"""
bank_id = f"test-graph-obs-{uuid.uuid4().hex[:8]}"
# Create the bank
await memory.get_bank_profile(bank_id=bank_id, request_context=request_context)
# Retain content that will create world facts with shared entities
# This should create facts that are linked by shared entities
await memory.retain_async(
bank_id=bank_id,
content="Alice works at Google as a software engineer.",
request_context=request_context,
)
await memory.retain_async(
bank_id=bank_id,
content="Bob also works at Google in the sales department.",
request_context=request_context,
)
# Wait for consolidation to create observations
import asyncio
await asyncio.sleep(2)
# Get graph data filtered by observation type only
graph_data = await memory.get_graph_data(
bank_id=bank_id,
fact_type="observation",
limit=1000,
request_context=request_context,
)
# Should have observations
assert graph_data["total_units"] > 0, "Should have observations"
assert len(graph_data["nodes"]) > 0, "Should have observation nodes"
# Verify all nodes are observations
for row in graph_data["table_rows"]:
assert row["fact_type"] == "observation", f"All nodes should be observations, got {row['fact_type']}"
# Should have edges (inherited from source memories)
# Even though we're only showing observations, they should inherit links from their sources
assert len(graph_data["edges"]) > 0, (
"Observations should have edges inherited from source memories. "
f"Found {len(graph_data['edges'])} edges"
)
# Should have entities (inherited from source memories)
observations_with_entities = [
row for row in graph_data["table_rows"] if row["entities"] and row["entities"] != "None"
]
assert len(observations_with_entities) > 0, (
"Observations should inherit entities from source memories. "
f"Found {len(observations_with_entities)} observations with entities"
)
# Verify entities contain expected values
all_entities = " ".join([row["entities"] for row in graph_data["table_rows"]])
assert "Alice" in all_entities or "Bob" in all_entities or "Google" in all_entities, (
f"Expected to find Alice, Bob, or Google in entities, got: {all_entities}"
)
# Verify edge types are valid
valid_link_types = {"semantic", "temporal", "entity"}
for edge in graph_data["edges"]:
link_type = edge["data"]["linkType"]
assert link_type in valid_link_types, f"Invalid link type: {link_type}"
# Verify all edges connect visible observation nodes
visible_node_ids = {row["id"] for row in graph_data["table_rows"]}
for edge in graph_data["edges"]:
source_id = edge["data"]["source"]
target_id = edge["data"]["target"]
assert source_id in visible_node_ids, f"Edge source {source_id[:8]} not in visible nodes"
assert target_id in visible_node_ids, f"Edge target {target_id[:8]} not in visible nodes"
# Cleanup
await memory.delete_bank(bank_id, request_context=request_context)
-11
View File
@@ -163,12 +163,6 @@ class TestToolNameNormalization:
assert _normalize_tool_name("call=functions.recall") == "recall"
assert _normalize_tool_name("call=functions.search_observations") == "search_observations"
def test_normalize_special_token_suffix(self):
"""Tool names with malformed special tokens should be normalized."""
assert _normalize_tool_name("done<|channel|>commentary") == "done"
assert _normalize_tool_name("recall<|endoftext|>") == "recall"
assert _normalize_tool_name("search_observations<|im_end|>extra") == "search_observations"
def test_is_done_tool(self):
"""Test _is_done_tool helper."""
# Standard
@@ -180,14 +174,9 @@ class TestToolNameNormalization:
assert _is_done_tool("call=done") is True
assert _is_done_tool("call=functions.done") is True
# With malformed special tokens
assert _is_done_tool("done<|channel|>commentary") is True
assert _is_done_tool("done<|endoftext|>") is True
# Not done
assert _is_done_tool("functions.recall") is False
assert _is_done_tool("call=functions.recall") is False
assert _is_done_tool("recall<|channel|>done") is False
class TestReflectAgentMocked:
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "hindsight-cli"
version = "0.4.0"
version = "0.3.0"
edition = "2021"
authors = ["Hindsight Team"]
description = "A beautiful CLI for Hindsight - semantic memory system"
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "hindsight-client"
version = "0.4.0"
version = "0.3.0"
description = "Python client for Hindsight - Semantic memory system with personality-driven thinking"
authors = [
{name = "Hindsight Team"}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@vectorize-io/hindsight-client",
"version": "0.4.0",
"version": "0.3.0",
"description": "TypeScript client for Hindsight - Semantic memory system with personality-driven thinking",
"main": "./dist/src/index.js",
"types": "./dist/src/index.d.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@vectorize-io/hindsight-control-plane",
"version": "0.4.0",
"version": "0.3.0",
"description": "Control plane for Hindsight - Semantic memory system",
"bin": {
"hindsight-control-plane": "./bin/cli.js"
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "hindsight-dev"
version = "0.4.0"
version = "0.3.0"
description = "Development utilities for Hindsight"
requires-python = ">=3.11"
dependencies = [
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "hindsight-embed"
version = "0.4.0"
version = "0.3.0"
description = "Hindsight embedded CLI - local memory operations without a server"
readme = "README.md"
requires-python = ">=3.11"
@@ -1,6 +1,6 @@
[project]
name = "hindsight-litellm"
version = "0.4.0"
version = "0.3.0"
description = "Universal LLM memory integration via LiteLLM - works with 100+ providers"
readme = "README.md"
requires-python = ">=3.10"
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "hindsight-all"
version = "0.4.0"
version = "0.3.0"
description = "Hindsight: Agent Memory That Works Like Human Memory - All-in-One Bundle"
readme = "README.md"
requires-python = ">=3.11"
-16
View File
@@ -77,22 +77,6 @@ for package in "${PYTHON_PACKAGES[@]}"; do
fi
done
# Update __version__ in Python __init__.py files
PYTHON_INIT_FILES=(
"hindsight-api/hindsight_api/__init__.py"
"hindsight-embed/hindsight_embed/__init__.py"
"hindsight-clients/python/hindsight_client_api/__init__.py"
)
for init_file in "${PYTHON_INIT_FILES[@]}"; do
if [ -f "$init_file" ]; then
print_info "Updating __version__ in $init_file"
sed -i.bak "s/^__version__ = \".*\"/__version__ = \"$VERSION\"/" "$init_file"
rm "${init_file}.bak"
else
print_warn "File $init_file not found, skipping"
fi
done
# Update Rust CLI
CARGO_FILE="hindsight-cli/Cargo.toml"
if [ -f "$CARGO_FILE" ]; then
Generated
+5 -5
View File
@@ -1295,7 +1295,7 @@ wheels = [
[[package]]
name = "hindsight-all"
version = "0.4.0"
version = "0.3.0"
source = { editable = "hindsight" }
dependencies = [
{ name = "hindsight-api" },
@@ -1319,7 +1319,7 @@ provides-extras = ["test"]
[[package]]
name = "hindsight-api"
version = "0.4.0"
version = "0.3.0"
source = { editable = "hindsight-api" }
dependencies = [
{ name = "aiohttp" },
@@ -1447,7 +1447,7 @@ dev = [
[[package]]
name = "hindsight-client"
version = "0.4.0"
version = "0.3.0"
source = { editable = "hindsight-clients/python" }
dependencies = [
{ name = "aiohttp" },
@@ -1481,7 +1481,7 @@ provides-extras = ["test"]
[[package]]
name = "hindsight-dev"
version = "0.4.0"
version = "0.3.0"
source = { editable = "hindsight-dev" }
dependencies = [
{ name = "hindsight-api" },
@@ -1527,7 +1527,7 @@ dev = [
[[package]]
name = "hindsight-embed"
version = "0.4.0"
version = "0.3.0"
source = { editable = "hindsight-embed" }
dependencies = [
{ name = "httpx" },