feat(hindsight-embed): external API support + OpenClaw fixes (#263, #264) (#265)

* feat(hindsight-embed): external API support + OpenClaw fixes

Adds comprehensive external API support and fixes critical OpenClaw plugin issues.

**External API Support:**
- Add HINDSIGHT_EMBED_API_URL to connect to external Hindsight API servers
- Add HINDSIGHT_EMBED_API_TOKEN for Bearer token authentication
- Add HINDSIGHT_EMBED_API_DATABASE_URL for custom PostgreSQL databases
- Skip daemon startup when external API URL is configured
- Add 10 comprehensive unit tests for external API scenarios

**OpenClaw Plugin Fixes:**
- Fix #263: Port mismatch (DEFAULT_PORT 8888 → 8889)
- Fix #264: Add daemon recovery after OpenClaw SIGUSR1 restarts
- Fix OpenRouter support: Pass HINDSIGHT_API_LLM_BASE_URL to daemon
- Fix macOS crashes: Auto-set FORCE_CPU flags for MPS/Metal issues

**LLM Configuration Refactor:**
- Auto-detect provider from standard env vars (OPENAI_API_KEY, etc.)
- Support explicit override via HINDSIGHT_API_LLM_* env vars
- Update model defaults (gemini-2.5-flash, openai/gpt-oss-20b)
- Remove provider-specific base URL support (only HINDSIGHT_API_LLM_BASE_URL)

**Documentation Updates:**
- Rewrite OpenClaw integration docs with crystal clear examples
- Add external API usage examples
- Add OpenRouter free model examples
- Update Quick Start with simplified provider setup

Closes #263, Closes #264

* docs(openclaw): streamline docs and add config inspection

- Remove duplicate/verbose sections (468 → 216 lines)
- Add section showing how to check ~/.hindsight/embed config file
- Add daemon status checking commands
- Keep only essential configuration examples
- Consolidate troubleshooting sections

* fix(test): update daemon health check port from 8889 to 8888

The test was checking port 8889 but we changed the daemon to use port 8888.
This commit is contained in:
Nicolò Boschi 2026-01-31 17:02:13 +01:00 committed by GitHub
parent 9c3fda74e2
commit 4b57b82301
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 634 additions and 293 deletions

View file

@ -15,7 +15,7 @@ from pathlib import Path
logger = logging.getLogger(__name__)
# Default daemon configuration
DEFAULT_DAEMON_PORT = 8889
DEFAULT_DAEMON_PORT = 8888
DEFAULT_IDLE_TIMEOUT = 0 # 0 = no auto-exit (hindsight-embed passes its own timeout)
LOCKFILE_PATH = Path.home() / ".hindsight" / "daemon.lock"
DAEMON_LOG_PATH = Path.home() / ".hindsight" / "daemon.log"

View file

@ -15,7 +15,7 @@ Zero-configuration local memory system with automatic daemon management. Perfect
1. **First command triggers startup**: When you run any `hindsight-embed` command, it checks if a local daemon is running
2. **Auto-daemon management**: If no daemon exists, it automatically spawns `hindsight-api --daemon` in the background
3. **Embedded database**: The daemon uses `pg0` (embedded PostgreSQL) — no separate database installation required
4. **Command forwarding**: Your command is forwarded to the local daemon via HTTP (localhost:8889)
4. **Command forwarding**: Your command is forwarded to the local daemon via HTTP (localhost:8888)
5. **Auto-shutdown**: After 5 minutes of inactivity (configurable), the daemon gracefully shuts down to free resources
**Key features:**
@ -23,7 +23,7 @@ Zero-configuration local memory system with automatic daemon management. Perfect
- **Zero setup** — One `configure` command and you're ready
- **Automatic lifecycle** — Daemon starts on-demand, stops when idle
- **Isolated storage** — Each bank gets its own embedded PostgreSQL database
- **Local-only** — Binds to `127.0.0.1:8889`, not accessible from network
- **Local-only** — Binds to `127.0.0.1:8888`, not accessible from network
- **Production-grade engine** — Uses the same memory engine as the full API service
Think of it as SQLite for long-term memory — all the power of Hindsight without managing servers.
@ -209,7 +209,7 @@ hindsight-embed daemon logs -f
Common issues:
- **Missing API key**: Set `HINDSIGHT_EMBED_LLM_API_KEY`
- **Port conflict**: Another service using port 8889
- **Port conflict**: Another service using port 8888
- **Permissions**: Check `~/.hindsight/` directory permissions
### Daemon Exits Immediately

View file

@ -6,141 +6,59 @@ sidebar_position: 4
Local, long term memory for [OpenClaw](https://openclaw.ai) agents using [Hindsight](https://vectorize.io/hindsight).
This plugin integrates [hindsight-embed](https://vectorize.io/hindsight/cli), a standalone daemon that bundles Hindsight's memory engine (API + PostgreSQL) into a single command. Everything runs locally on your machine, reuses the LLM you're already paying for, and costs nothing extra. The plugin automatically manages the daemon lifecycle and provides hooks for seamless memory capture and recall.
This plugin integrates [hindsight-embed](https://vectorize.io/hindsight/cli), a standalone daemon that bundles Hindsight's memory engine (API + PostgreSQL) into a single command. Everything runs locally on your machine, reuses the LLM you're already paying for, and costs nothing extra.
## Quick Start
**Step 1: Set up LLM for memory extraction**
Choose one provider and set its API key:
```bash
# 1. Configure your LLM provider
# Option A: OpenAI (uses gpt-4o-mini for memory extraction)
export OPENAI_API_KEY="sk-your-key"
openclaw config set 'agents.defaults.models."openai/gpt-4o-mini"' '{}'
# 2. Install and enable the plugin
# Option B: Anthropic (uses claude-3-5-haiku for memory extraction)
export ANTHROPIC_API_KEY="your-key"
# Option C: Gemini (uses gemini-2.5-flash for memory extraction)
export GEMINI_API_KEY="your-key"
# Option D: Groq (uses openai/gpt-oss-20b for memory extraction)
export GROQ_API_KEY="your-key"
```
**Step 2: Install the plugin**
```bash
openclaw plugins install @vectorize-io/hindsight-openclaw
```
# 3. Start OpenClaw
**Step 3: Start OpenClaw**
```bash
openclaw gateway
```
That's it! The plugin will automatically start capturing and recalling memories.
The plugin will automatically:
- Start a local Hindsight daemon (port 8888)
- Capture conversations after each turn
- Inject relevant memories before agent responses
**Important:** The LLM you configure above is **only for memory extraction** (background processing). Your main OpenClaw agent can use any model you configure separately.
## How It Works
### Auto-Capture (Hooks)
Every conversation is **automatically stored** after each turn:
- Extracts facts, entities, and relationships
- Processes in background (non-blocking)
- Stores in PostgreSQL via embedded `hindsight-api`
**Auto-Capture:** Every conversation is automatically stored after each turn. Facts, entities, and relationships are extracted in the background.
### Auto-Recall (Before Agent Start)
Before each agent response, relevant memories are **automatically injected**:
- Relevant memories retrieved (up to 1024 tokens)
- Injected into context with `<hindsight_memories>` tags (JSON format with metadata)
- Agent seamlessly uses past context
## Why Auto-Recall?
Traditional memory systems give agents a `search_memory` tool - the model must decide when to call it. In practice, models don't use memory tools consistently. They lack reliable self-awareness about what they should remember to check.
Auto-recall solves this by injecting relevant memories automatically before every agent turn. Memories are formatted as JSON with full metadata:
```json
<hindsight_memories>
[
{
{
"chunk_id": "openclawd_default-session_12",
"context": "",
"document_id": "default-session",
"id": "5f55f684-e6f5-46e3-9f5c-043bdf005511",
"mentioned_at": "2026-01-30T11:07:33.211396+00:00",
"occurred_end": "2025-01-29T23:14:30+00:00",
"occurred_start": "2025-01-29T23:14:30+00:00",
"tags": [],
"text": "Nicolò Boschi attended an OpenAI devday last year and found it cool. | When: 2025-01-30 | Involving: Nicolò Boschi",
"type": "world"
}
]
</hindsight_memories>
```
The agent sees past context automatically without needing to remember to remember. This approach trades token cost for reliability - but for conversational agents, spending 500 tokens on auto-injected context is better than ignoring 10,000 stored facts because the model didn't call a tool.
## Understanding OpenClaw Concepts
### Plugins
Extensions that add functionality to OpenClaw. This Hindsight plugin:
- Runs a background service (manages `hindsight-embed` daemon)
- Registers hooks (automatic event handlers)
### Hooks
Automatic event handlers that run without agent involvement:
- **`before_agent_start`**: Auto-recall - injects memories before agent processes message
- **`agent_end`**: Auto-capture - stores conversation after agent responds
Think of hooks as "forced automation" - they always run.
## Architecture
```
┌─────────────────────────────────────────┐
│ OpenClaw Gateway │
│ │
│ ┌───────────────────────────────────┐ │
│ │ Hindsight Plugin │ │
│ │ │ │
│ │ • Service: Manages daemon │ │
│ │ • Hook: before_agent_start │ │
│ │ → Auto-recall (1024 tokens) │ │
│ │ • Hook: agent_end │ │
│ │ → Auto-capture │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
uvx hindsight-embed
• Daemon on port 8889
• PostgreSQL (pg0://hindsight-embed)
• Bank: 'openclaw' (isolated within shared database)
• Fact extraction
```
**Database Architecture:** All banks share a single pg0 database instance (`pg0://hindsight-embed`). Bank isolation happens within the database via separate tables/schemas per bank ID. The 'openclaw' bank is automatically created when the plugin stores its first memory.
**Local-First Design:**
- **Your data stays local**: All conversations, facts, and relationships stored in PostgreSQL on your machine
- **No additional costs**: Reuses your configured LLM provider (OpenAI, Anthropic, Gemini, Groq, Ollama) - no separate memory API charges
- **No vendor lock-in**: Standard PostgreSQL storage, export anytime with `hindsight-embed memory export`
- **Works offline**: With Ollama, the entire stack (agent + memory + LLM) runs offline
- **Zero infrastructure setup**: No database deployment, connection strings, or credential management - everything handled automatically
## Installation
### Prerequisites
- **Node.js** 22+
- **OpenClaw** with plugin support
- **uv/uvx** for running `hindsight-embed`
- **LLM API key** (OpenAI, Anthropic, etc.)
### Setup
```bash
# 1. Configure your LLM provider
export OPENAI_API_KEY="sk-your-key"
openclaw config set 'agents.defaults.models."openai/gpt-4o-mini"' '{}'
# 2. Install and enable the plugin
openclaw plugins install @vectorize-io/hindsight-openclaw
# 3. Start OpenClaw
openclaw gateway
```
On first start, `uvx` will automatically download `hindsight-embed` (no manual installation needed).
**Auto-Recall:** Before each agent response, relevant memories are automatically injected into the context (up to 1024 tokens). The agent uses past context without needing to call tools.
Traditional memory systems give agents a `search_memory` tool - but models don't use it consistently. Auto-recall solves this by injecting memories automatically before every turn.
## Configuration
### Plugin Settings
Optional settings in `~/.openclaw/openclaw.json`:
```json
@ -150,7 +68,8 @@ Optional settings in `~/.openclaw/openclaw.json`:
"hindsight-openclaw": {
"enabled": true,
"config": {
"daemonIdleTimeout": 0
"daemonIdleTimeout": 0,
"embedVersion": "latest"
}
}
}
@ -159,143 +78,138 @@ Optional settings in `~/.openclaw/openclaw.json`:
```
**Options:**
- `daemonIdleTimeout` (number, default: `0`) - Seconds before daemon shuts down from inactivity (0 = never)
- `bankMission` (string, default: auto-generated) - Custom context for the memory bank. Defaults to: "You are an AI assistant helping users across multiple communication channels (Telegram, Slack, Discord, etc.). Remember user preferences, instructions, and important context from conversations to provide personalized assistance."
- `embedVersion` (string, default: `"latest"`) - hindsight-embed version to use (e.g., `"latest"`, `"0.4.2"`, or leave empty for latest). Use this to pin a specific version if latest is broken.
- `daemonIdleTimeout` - Seconds before daemon shuts down from inactivity (default: `0` = never)
- `embedVersion` - hindsight-embed version (default: `"latest"`)
- `bankMission` - Custom context for the memory bank (optional)
## Supported LLM Providers
### LLM Configuration
The plugin auto-detects your configured provider and API key:
The plugin auto-detects your LLM provider from these environment variables:
| Provider | Environment Variable | Model Example |
|----------|---------------------|---------------|
| OpenAI | `OPENAI_API_KEY` | `openai/gpt-4o-mini` |
| Anthropic | `ANTHROPIC_API_KEY` | `anthropic/claude-sonnet-4` |
| Gemini | `GEMINI_API_KEY` | `gemini/gemini-2.0-flash-exp` |
| Groq | `GROQ_API_KEY` | `groq/llama-3.3-70b` |
| Ollama | None needed | `ollama/llama3` |
| Provider | Env Var | Default Model |
|----------|---------|---------------|
| OpenAI | `OPENAI_API_KEY` | `gpt-4o-mini` |
| Anthropic | `ANTHROPIC_API_KEY` | `claude-3-5-haiku-20241022` |
| Gemini | `GEMINI_API_KEY` | `gemini-2.5-flash` |
| Groq | `GROQ_API_KEY` | `openai/gpt-oss-20b` |
**Override with explicit config:**
Configure with:
```bash
export OPENAI_API_KEY="sk-your-key"
openclaw config set 'agents.defaults.models."openai/gpt-4o-mini"' '{}'
export HINDSIGHT_API_LLM_PROVIDER=openai
export HINDSIGHT_API_LLM_MODEL=gpt-4o-mini
export HINDSIGHT_API_LLM_API_KEY=sk-your-key
# Optional: custom base URL (OpenRouter, Azure, vLLM, etc.)
export HINDSIGHT_API_LLM_BASE_URL=https://openrouter.ai/api/v1
```
## Verification
**Example: Free OpenRouter model**
**Check if plugin is loaded:**
```bash
openclaw plugins list | grep hindsight
# Should show: ✓ enabled │ Hindsight Memory │ ...
export HINDSIGHT_API_LLM_PROVIDER=openai
export HINDSIGHT_API_LLM_MODEL=xiaomi/mimo-v2-flash # FREE!
export HINDSIGHT_API_LLM_API_KEY=sk-or-v1-your-openrouter-key
export HINDSIGHT_API_LLM_BASE_URL=https://openrouter.ai/api/v1
```
**Test auto-recall:**
Send a message on any OpenClaw channel (Telegram, Slack, etc.):
```
User: My name is John and I love pizza
Bot: Got it! I'll remember that.
### External API (Advanced)
User: What do I like to eat?
Bot: You love pizza! # ← Used auto-recall
```
To use an existing Hindsight API server instead of the local daemon:
**View daemon logs:**
```bash
tail -f ~/.hindsight/daemon.log
export HINDSIGHT_EMBED_API_URL=http://your-server:8000
export HINDSIGHT_EMBED_API_TOKEN=your-api-token # Optional, if API requires auth
openclaw gateway
```
**Check memories in database:**
```bash
uvx hindsight-embed@latest memory recall openclaw "pizza" --output json
```
Useful for shared memory across multiple OpenClaw instances or production deployments.
## Inspecting Memories
The plugin uses `hindsight-embed` daemon which provides CLI commands for inspection:
### Check Configuration
View the daemon config that was written by the plugin:
**View daemon logs:**
```bash
uvx hindsight-embed@latest daemon logs
# Or follow logs in real-time:
cat ~/.hindsight/embed
```
This shows the LLM provider, model, and other settings the daemon is using.
### Check Daemon Status
```bash
# Check if daemon is running
uvx hindsight-embed@latest daemon status
# View daemon logs
tail -f ~/.hindsight/daemon.log
```
**Open web UI:**
```bash
uvx hindsight-embed@latest ui
# Opens browser to http://localhost:8890
# Browse memories, facts, entities, and relationships
```
### Query Memories
**List memory banks:**
```bash
uvx hindsight-embed@latest bank list
# Shows all banks including 'openclaw'
```
**Query memories:**
```bash
# Search memories
uvx hindsight-embed@latest memory recall openclaw "user preferences" --output json
uvx hindsight-embed@latest memory recall openclaw "user preferences"
# View recent memories
uvx hindsight-embed@latest memory list openclaw --limit 10
# Export all memories
uvx hindsight-embed@latest memory export openclaw --output memories.json
```
**Inspect facts and entities:**
```bash
# List extracted facts
uvx hindsight-embed@latest fact list openclaw
# List entities
uvx hindsight-embed@latest entity list openclaw
# Show entity relationships
uvx hindsight-embed@latest entity graph openclaw
# Open web UI
uvx hindsight-embed@latest ui
```
## Troubleshooting
**Plugin not loading?**
### Plugin not loading
```bash
# Check plugin installation
openclaw plugins list | grep -i hindsight
openclaw plugins list | grep hindsight
# Should show: ✓ enabled │ Hindsight Memory │ ...
# Reinstall if needed
openclaw plugins install @vectorize-io/hindsight-openclaw
```
**Daemon not starting?**
### Daemon not starting
```bash
# Check daemon status
uvx hindsight-embed@latest daemon status
# Manually start
uvx hindsight-embed@latest daemon start
# View logs
# View logs for errors
tail -f ~/.hindsight/daemon.log
# Check configuration
cat ~/.hindsight/embed
```
**No API key error?**
```bash
# Set in shell profile
echo 'export OPENAI_API_KEY="sk-your-key"' >> ~/.zshrc
source ~/.zshrc
### No API key error
# Verify
Make sure you've set one of the provider API keys:
```bash
export OPENAI_API_KEY="sk-your-key"
# or
export ANTHROPIC_API_KEY="your-key"
# Verify it's set
echo $OPENAI_API_KEY
```
**Memories not being stored?**
### Verify it's working
Check gateway logs for memory operations:
```bash
# Check gateway logs for auto-capture
tail -f /tmp/openclaw/openclaw-*.log | grep Hindsight
# Should see:
# [Hindsight Hook] agent_end triggered
# Should see on startup:
# [Hindsight] ✓ Using provider: openai, model: gpt-4o-mini
# Should see after conversations:
# [Hindsight] Retained X messages for session ...
# [Hindsight] Auto-recall: Injecting X memories
```

View file

@ -12,7 +12,7 @@ This package provides a simple CLI for storing and recalling memories using Hind
2. **Subsequent commands**: Near-instant responses (~1-2s) since daemon is already running
3. **Auto-shutdown**: Daemon automatically exits after 5 minutes of inactivity
The daemon runs on `localhost:8889` and uses an embedded PostgreSQL database (pg0) - everything stays local on your machine.
The daemon runs on `localhost:8888` and uses an embedded PostgreSQL database (pg0) - everything stays local on your machine.
## Installation
@ -121,8 +121,31 @@ Run `hindsight-embed configure` for a guided setup that saves to `~/.hindsight/e
| `HINDSIGHT_EMBED_LLM_PROVIDER` | LLM provider (`openai`, `groq`, `google`, `ollama`) | `openai` |
| `HINDSIGHT_EMBED_LLM_MODEL` | LLM model | `gpt-4o-mini` |
| `HINDSIGHT_EMBED_BANK_ID` | Default memory bank ID (optional, used when not specified in CLI) | `default` |
| `HINDSIGHT_EMBED_API_URL` | Use external API server instead of starting local daemon | None (starts local daemon) |
| `HINDSIGHT_EMBED_API_TOKEN` | Authentication token for external API (sent as Bearer token) | None |
| `HINDSIGHT_EMBED_API_DATABASE_URL` | Database URL for daemon | `pg0://hindsight-embed` |
| `HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT` | Seconds before daemon auto-exits when idle | `300` |
**Note:** All banks share a single pg0 database (`pg0://hindsight-embed`). Bank isolation happens within the database via the `bank_id` parameter passed to CLI commands.
**Using an External API Server:**
To connect to an existing Hindsight API server instead of starting the local daemon:
```bash
export HINDSIGHT_EMBED_API_URL=http://your-server:8000
export HINDSIGHT_EMBED_API_TOKEN=your-api-token # Optional, if API requires auth
hindsight-embed memory recall default "query"
```
**Custom Database:**
To use an external PostgreSQL database instead of the embedded pg0 database (useful when running as root or in containerized environments):
```bash
export HINDSIGHT_EMBED_API_DATABASE_URL=postgresql://user:password@localhost:5432/dbname
hindsight-embed daemon start
```
**Note:** All banks share a single database. Bank isolation happens within the database via the `bank_id` parameter passed to CLI commands.
### Files

View file

@ -15,6 +15,9 @@ Environment variables:
HINDSIGHT_EMBED_LLM_PROVIDER: Optional. LLM provider (default: "openai").
HINDSIGHT_EMBED_LLM_MODEL: Optional. LLM model (default: "gpt-4o-mini").
HINDSIGHT_EMBED_BANK_ID: Optional. Memory bank ID (default: "default").
HINDSIGHT_EMBED_API_URL: Optional. Use external API server instead of starting local daemon.
HINDSIGHT_EMBED_API_TOKEN: Optional. Authentication token for external API (sent as Bearer token).
HINDSIGHT_EMBED_API_DATABASE_URL: Optional. Database URL for daemon (default: "pg0://hindsight-embed").
HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT: Optional. Seconds before daemon auto-exits when idle (default: 300).
HINDSIGHT_EMBED_API_VERSION: Optional. hindsight-api version to use (default: matches embed version).
Note: Only applies when starting daemon. To change version, stop daemon first.

View file

@ -15,7 +15,7 @@ import httpx # Used only for health check
logger = logging.getLogger(__name__)
DAEMON_PORT = 8889
DAEMON_PORT = 8888
DAEMON_URL = f"http://127.0.0.1:{DAEMON_PORT}"
DAEMON_STARTUP_TIMEOUT = 180 # seconds - needs to be long for first run (downloads dependencies)
# Default idle timeout: 5 minutes - users can override with HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT env var
@ -76,12 +76,25 @@ def _start_daemon(config: dict) -> bool:
env["HINDSIGHT_API_LLM_MODEL"] = config["llm_model"]
# Use single shared pg0 database for all banks (banks are isolated within the database)
# Allow override via HINDSIGHT_API_DATABASE_URL for external PostgreSQL
# Allow override via HINDSIGHT_EMBED_API_DATABASE_URL for external PostgreSQL
# (e.g. when running as root where embedded pg0 cannot use initdb)
if "HINDSIGHT_API_DATABASE_URL" not in env:
if "HINDSIGHT_EMBED_API_DATABASE_URL" not in env:
env["HINDSIGHT_API_DATABASE_URL"] = "pg0://hindsight-embed"
else:
# Pass through the embed-specific env var to the daemon as the standard API env var
env["HINDSIGHT_API_DATABASE_URL"] = env["HINDSIGHT_EMBED_API_DATABASE_URL"]
env["HINDSIGHT_API_LOG_LEVEL"] = "info"
# On macOS, force CPU for embeddings/reranker to avoid MPS/Metal/XPC issues in daemon mode
# Only set if not already configured by user
import platform
if platform.system() == "Darwin":
if "HINDSIGHT_API_EMBEDDINGS_LOCAL_FORCE_CPU" not in env:
env["HINDSIGHT_API_EMBEDDINGS_LOCAL_FORCE_CPU"] = "1"
if "HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU" not in env:
env["HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU"] = "1"
# Get idle timeout from environment or use default
idle_timeout = int(os.getenv("HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT", str(DEFAULT_DAEMON_IDLE_TIMEOUT)))
@ -285,7 +298,7 @@ def run_cli(args: list[str], config: dict) -> int:
"""
Run the hindsight CLI with the given arguments.
Ensures daemon is running and passes the API URL.
Ensures daemon is running (unless HINDSIGHT_API_URL is already set) and passes the API URL.
Args:
args: CLI arguments (e.g., ["memory", "retain", "bank", "content"])
@ -306,14 +319,29 @@ def run_cli(args: list[str], config: dict) -> int:
print("Error: hindsight CLI not found", file=sys.stderr)
return 1
# Ensure daemon is running
if not ensure_daemon_running(config):
print("Error: Failed to start daemon", file=sys.stderr)
return 1
# Build environment with API URL pointing to daemon
# Build environment
env = os.environ.copy()
env["HINDSIGHT_API_URL"] = DAEMON_URL
# Check if user wants to use external API
api_url = env.get("HINDSIGHT_EMBED_API_URL")
if not api_url:
# No external API specified - ensure our daemon is running
if not ensure_daemon_running(config):
print("Error: Failed to start daemon", file=sys.stderr)
return 1
api_url = DAEMON_URL
else:
# Using external API - skip daemon startup
logger.debug(f"Using external API at {api_url}")
# Set the API URL for the CLI (using the standard HINDSIGHT_API_URL var that the CLI expects)
env["HINDSIGHT_API_URL"] = api_url
# Pass through API token if set (using the standard HINDSIGHT_API_KEY var that the CLI expects)
api_token = env.get("HINDSIGHT_EMBED_API_TOKEN")
if api_token:
env["HINDSIGHT_API_KEY"] = api_token
# Run CLI
try:

View file

@ -22,6 +22,7 @@ packages = ["hindsight_embed"]
dev = [
"ruff>=0.8.0",
"ty>=0.0.1",
"pytest>=8.0.0",
]
[tool.ruff]

View file

@ -209,7 +209,7 @@ echo "PASS: Memory recalled with JSON format successfully"
# Test 5: Check daemon is running
echo ""
echo "Test 5: Verifying daemon is running..."
if curl -s http://127.0.0.1:8889/health | grep -q "healthy"; then
if curl -s http://127.0.0.1:8888/health | grep -q "healthy"; then
echo "PASS: Daemon is running and healthy"
else
echo "FAIL: Daemon is not running"

View file

@ -0,0 +1 @@
"""Tests for hindsight-embed."""

View file

@ -0,0 +1,284 @@
"""Tests for daemon_client module."""
import os
import subprocess
from pathlib import Path
from unittest.mock import MagicMock, Mock, patch
import pytest
from hindsight_embed import daemon_client
@pytest.fixture
def config():
"""Default config for tests."""
return {
"llm_api_key": "test-key",
"llm_provider": "openai",
"llm_model": "gpt-4o-mini",
"bank_id": "test-bank",
}
@pytest.fixture
def mock_cli_binary(tmp_path):
"""Create a mock CLI binary."""
cli_path = tmp_path / "hindsight"
cli_path.write_text("#!/bin/bash\nexit 0")
cli_path.chmod(0o755)
return cli_path
class TestRunCli:
"""Tests for run_cli function."""
def test_run_cli_with_external_api_url(self, config, mock_cli_binary, monkeypatch):
"""Test that external HINDSIGHT_EMBED_API_URL skips daemon startup."""
# Set up environment with external API URL
external_api_url = "http://external-api:8000"
monkeypatch.setenv("HINDSIGHT_EMBED_API_URL", external_api_url)
# Mock functions
mock_ensure_cli = Mock(return_value=True)
mock_find_cli = Mock(return_value=mock_cli_binary)
mock_ensure_daemon = Mock(return_value=True)
mock_subprocess_run = Mock(return_value=Mock(returncode=0))
with (
patch.object(daemon_client, "ensure_cli_installed", mock_ensure_cli),
patch.object(daemon_client, "find_cli_binary", mock_find_cli),
patch.object(daemon_client, "ensure_daemon_running", mock_ensure_daemon),
patch("subprocess.run", mock_subprocess_run),
):
# Run CLI
exit_code = daemon_client.run_cli(["memory", "recall", "test", "query"], config)
# Verify daemon was NOT started (since external API URL is set)
assert mock_ensure_daemon.call_count == 0
# Verify CLI was called
assert mock_subprocess_run.call_count == 1
call_args = mock_subprocess_run.call_args
# Verify environment contains the external API URL
assert call_args.kwargs["env"]["HINDSIGHT_API_URL"] == external_api_url
# Verify exit code
assert exit_code == 0
def test_run_cli_without_external_api_url(self, config, mock_cli_binary, monkeypatch):
"""Test that without external API URL, daemon is started."""
# Ensure HINDSIGHT_EMBED_API_URL is not set
monkeypatch.delenv("HINDSIGHT_EMBED_API_URL", raising=False)
# Mock functions
mock_ensure_cli = Mock(return_value=True)
mock_find_cli = Mock(return_value=mock_cli_binary)
mock_ensure_daemon = Mock(return_value=True)
mock_subprocess_run = Mock(return_value=Mock(returncode=0))
with (
patch.object(daemon_client, "ensure_cli_installed", mock_ensure_cli),
patch.object(daemon_client, "find_cli_binary", mock_find_cli),
patch.object(daemon_client, "ensure_daemon_running", mock_ensure_daemon),
patch("subprocess.run", mock_subprocess_run),
):
# Run CLI
exit_code = daemon_client.run_cli(["memory", "recall", "test", "query"], config)
# Verify daemon WAS started (since no external API URL)
assert mock_ensure_daemon.call_count == 1
assert mock_ensure_daemon.call_args[0][0] == config
# Verify CLI was called
assert mock_subprocess_run.call_count == 1
call_args = mock_subprocess_run.call_args
# Verify environment contains the local daemon URL
assert call_args.kwargs["env"]["HINDSIGHT_API_URL"] == daemon_client.DAEMON_URL
# Verify exit code
assert exit_code == 0
def test_run_cli_daemon_startup_failure(self, config, mock_cli_binary, monkeypatch):
"""Test that daemon startup failure is handled properly."""
# Ensure HINDSIGHT_EMBED_API_URL is not set
monkeypatch.delenv("HINDSIGHT_EMBED_API_URL", raising=False)
# Mock functions - daemon startup fails
mock_ensure_cli = Mock(return_value=True)
mock_find_cli = Mock(return_value=mock_cli_binary)
mock_ensure_daemon = Mock(return_value=False) # Daemon fails to start
with (
patch.object(daemon_client, "ensure_cli_installed", mock_ensure_cli),
patch.object(daemon_client, "find_cli_binary", mock_find_cli),
patch.object(daemon_client, "ensure_daemon_running", mock_ensure_daemon),
):
# Run CLI
exit_code = daemon_client.run_cli(["memory", "recall", "test", "query"], config)
# Verify daemon startup was attempted
assert mock_ensure_daemon.call_count == 1
# Verify exit code indicates failure
assert exit_code == 1
def test_run_cli_without_cli_binary(self, config, monkeypatch):
"""Test that missing CLI binary is handled properly."""
# Ensure HINDSIGHT_EMBED_API_URL is not set
monkeypatch.delenv("HINDSIGHT_EMBED_API_URL", raising=False)
# Mock functions - CLI not installed
mock_ensure_cli = Mock(return_value=True)
mock_find_cli = Mock(return_value=None) # CLI not found
with (
patch.object(daemon_client, "ensure_cli_installed", mock_ensure_cli),
patch.object(daemon_client, "find_cli_binary", mock_find_cli),
):
# Run CLI
exit_code = daemon_client.run_cli(["memory", "recall", "test", "query"], config)
# Verify exit code indicates failure
assert exit_code == 1
def test_run_cli_with_api_token(self, config, mock_cli_binary, monkeypatch):
"""Test that HINDSIGHT_EMBED_API_TOKEN is passed through to the CLI."""
# Set up environment with external API URL and token
external_api_url = "http://external-api:8000"
api_token = "test-bearer-token-12345"
monkeypatch.setenv("HINDSIGHT_EMBED_API_URL", external_api_url)
monkeypatch.setenv("HINDSIGHT_EMBED_API_TOKEN", api_token)
# Mock functions
mock_ensure_cli = Mock(return_value=True)
mock_find_cli = Mock(return_value=mock_cli_binary)
mock_ensure_daemon = Mock(return_value=True)
mock_subprocess_run = Mock(return_value=Mock(returncode=0))
with (
patch.object(daemon_client, "ensure_cli_installed", mock_ensure_cli),
patch.object(daemon_client, "find_cli_binary", mock_find_cli),
patch.object(daemon_client, "ensure_daemon_running", mock_ensure_daemon),
patch("subprocess.run", mock_subprocess_run),
):
# Run CLI
exit_code = daemon_client.run_cli(["memory", "recall", "test", "query"], config)
# Verify daemon was NOT started (since external API URL is set)
assert mock_ensure_daemon.call_count == 0
# Verify CLI was called
assert mock_subprocess_run.call_count == 1
call_args = mock_subprocess_run.call_args
# Verify environment contains both the API URL and the API key
assert call_args.kwargs["env"]["HINDSIGHT_API_URL"] == external_api_url
assert call_args.kwargs["env"]["HINDSIGHT_API_KEY"] == api_token
# Verify exit code
assert exit_code == 0
class TestStartDaemon:
"""Tests for _start_daemon function."""
def test_start_daemon_respects_database_url_env(self, config, monkeypatch):
"""Test that HINDSIGHT_EMBED_API_DATABASE_URL is respected if already set."""
custom_db_url = "postgresql://custom:password@localhost:5432/custom_db"
monkeypatch.setenv("HINDSIGHT_EMBED_API_DATABASE_URL", custom_db_url)
# Mock subprocess.Popen to capture the env
captured_env = {}
def mock_popen(*args, **kwargs):
captured_env.update(kwargs.get("env", {}))
# Return a mock process
mock_proc = MagicMock()
mock_proc.poll.return_value = None
return mock_proc
# Reduce timeout to 0.1s to avoid waiting
monkeypatch.setattr(daemon_client, "DAEMON_STARTUP_TIMEOUT", 0.1)
# Mock daemon health check to fail immediately (so we don't wait for startup)
mock_is_running = Mock(return_value=False)
with (
patch("subprocess.Popen", side_effect=mock_popen),
patch.object(daemon_client, "_is_daemon_running", mock_is_running),
patch.object(daemon_client, "_find_hindsight_api_command", return_value=["fake-cmd"]),
):
# Start daemon (will fail health check, but we just want to verify env)
daemon_client._start_daemon(config)
# Verify the custom database URL was NOT overwritten
assert captured_env.get("HINDSIGHT_API_DATABASE_URL") == custom_db_url
def test_start_daemon_sets_default_database_url(self, config, monkeypatch):
"""Test that default database URL is set if not already in env."""
# Ensure HINDSIGHT_EMBED_API_DATABASE_URL is not set
monkeypatch.delenv("HINDSIGHT_EMBED_API_DATABASE_URL", raising=False)
# Mock subprocess.Popen to capture the env
captured_env = {}
def mock_popen(*args, **kwargs):
captured_env.update(kwargs.get("env", {}))
# Return a mock process
mock_proc = MagicMock()
mock_proc.poll.return_value = None
return mock_proc
# Reduce timeout to 0.1s to avoid waiting
monkeypatch.setattr(daemon_client, "DAEMON_STARTUP_TIMEOUT", 0.1)
# Mock daemon health check to fail immediately (so we don't wait for startup)
mock_is_running = Mock(return_value=False)
with (
patch("subprocess.Popen", side_effect=mock_popen),
patch.object(daemon_client, "_is_daemon_running", mock_is_running),
patch.object(daemon_client, "_find_hindsight_api_command", return_value=["fake-cmd"]),
):
# Start daemon (will fail health check, but we just want to verify env)
daemon_client._start_daemon(config)
# Verify the default database URL was set
assert captured_env.get("HINDSIGHT_API_DATABASE_URL") == "pg0://hindsight-embed"
class TestIsDaemonRunning:
"""Tests for _is_daemon_running function."""
def test_daemon_running_returns_true_on_200(self):
"""Test that daemon is considered running when health check returns 200."""
mock_response = Mock()
mock_response.status_code = 200
mock_client = MagicMock()
mock_client.__enter__.return_value.get.return_value = mock_response
with patch("httpx.Client", return_value=mock_client):
assert daemon_client._is_daemon_running() is True
def test_daemon_not_running_returns_false_on_error(self):
"""Test that daemon is considered not running when health check fails."""
mock_client = MagicMock()
mock_client.__enter__.return_value.get.side_effect = Exception("Connection refused")
with patch("httpx.Client", return_value=mock_client):
assert daemon_client._is_daemon_running() is False
def test_daemon_not_running_returns_false_on_non_200(self):
"""Test that daemon is considered not running when health check returns non-200."""
mock_response = Mock()
mock_response.status_code = 500
mock_client = MagicMock()
mock_client.__enter__.return_value.get.return_value = mock_response
with patch("httpx.Client", return_value=mock_client):
assert daemon_client._is_daemon_running() is False

View file

@ -12,6 +12,7 @@ export class HindsightEmbedManager {
private llmProvider: string;
private llmApiKey: string;
private llmModel?: string;
private llmBaseUrl?: string;
private daemonIdleTimeout: number;
private embedVersion: string;
@ -20,15 +21,17 @@ export class HindsightEmbedManager {
llmProvider: string,
llmApiKey: string,
llmModel?: string,
llmBaseUrl?: string,
daemonIdleTimeout: number = 0, // Default: never timeout
embedVersion: string = 'latest' // Default: latest
) {
this.port = 8889; // hindsight-embed uses fixed port 8889
this.baseUrl = `http://127.0.0.1:8889`;
this.port = 8888; // hindsight-embed daemon uses same port as API
this.baseUrl = `http://127.0.0.1:8888`;
this.embedDir = join(homedir(), '.openclaw', 'hindsight-embed');
this.llmProvider = llmProvider;
this.llmApiKey = llmApiKey;
this.llmModel = llmModel;
this.llmBaseUrl = llmBaseUrl;
this.daemonIdleTimeout = daemonIdleTimeout;
this.embedVersion = embedVersion || 'latest';
}
@ -48,6 +51,17 @@ export class HindsightEmbedManager {
env['HINDSIGHT_EMBED_LLM_MODEL'] = this.llmModel;
}
// Pass through base URL for OpenAI-compatible providers (OpenRouter, etc.)
if (this.llmBaseUrl) {
env['HINDSIGHT_API_LLM_BASE_URL'] = this.llmBaseUrl;
}
// On macOS, force CPU for embeddings/reranker to avoid MPS/Metal issues in daemon mode
if (process.platform === 'darwin') {
env['HINDSIGHT_API_EMBEDDINGS_LOCAL_FORCE_CPU'] = '1';
env['HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU'] = '1';
}
// Write env vars to ~/.hindsight/config.env for daemon persistence
await this.writeConfigEnv(env);
@ -149,6 +163,15 @@ export class HindsightEmbedManager {
return this.process !== null;
}
async checkHealth(): Promise<boolean> {
try {
const response = await fetch(`${this.baseUrl}/health`, { signal: AbortSignal.timeout(2000) });
return response.ok;
} catch {
return false;
}
}
private async writeConfigEnv(env: NodeJS.ProcessEnv): Promise<void> {
const hindsightDir = join(homedir(), '.hindsight');
const embedConfigPath = join(hindsightDir, 'embed');
@ -167,6 +190,7 @@ export class HindsightEmbedManager {
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith('#') &&
!trimmed.startsWith('HINDSIGHT_EMBED_LLM_') &&
!trimmed.startsWith('HINDSIGHT_API_LLM_') &&
!trimmed.startsWith('HINDSIGHT_EMBED_BANK_ID') &&
!trimmed.startsWith('HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT')) {
extraSettings.push(line);
@ -193,10 +217,21 @@ export class HindsightEmbedManager {
if (env.HINDSIGHT_EMBED_LLM_API_KEY) {
configLines.push(`HINDSIGHT_EMBED_LLM_API_KEY=${env.HINDSIGHT_EMBED_LLM_API_KEY}`);
}
if (env.HINDSIGHT_API_LLM_BASE_URL) {
configLines.push(`HINDSIGHT_API_LLM_BASE_URL=${env.HINDSIGHT_API_LLM_BASE_URL}`);
}
if (env.HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT) {
configLines.push(`HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT=${env.HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT}`);
}
// Add platform-specific config (macOS FORCE_CPU flags)
if (env.HINDSIGHT_API_EMBEDDINGS_LOCAL_FORCE_CPU) {
configLines.push(`HINDSIGHT_API_EMBEDDINGS_LOCAL_FORCE_CPU=${env.HINDSIGHT_API_EMBEDDINGS_LOCAL_FORCE_CPU}`);
}
if (env.HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU) {
configLines.push(`HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU=${env.HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU}`);
}
// Add extra settings if they exist
if (extraSettings.length > 0) {
configLines.push('');

View file

@ -28,88 +28,81 @@ const __dirname = dirname(__filename);
// Default bank name
const BANK_NAME = 'openclaw';
// Provider mapping: moltbot provider name -> hindsight provider name
const PROVIDER_MAP: Record<string, string> = {
anthropic: 'anthropic',
openai: 'openai',
'openai-codex': 'openai',
gemini: 'gemini',
groq: 'groq',
ollama: 'ollama',
};
// Provider detection from standard env vars
const PROVIDER_DETECTION = [
{ name: 'openai', keyEnv: 'OPENAI_API_KEY', defaultModel: 'gpt-4o-mini' },
{ name: 'anthropic', keyEnv: 'ANTHROPIC_API_KEY', defaultModel: 'claude-3-5-haiku-20241022' },
{ name: 'gemini', keyEnv: 'GEMINI_API_KEY', defaultModel: 'gemini-2.5-flash' },
{ name: 'groq', keyEnv: 'GROQ_API_KEY', defaultModel: 'openai/gpt-oss-20b' },
{ name: 'ollama', keyEnv: '', defaultModel: 'llama3.2' },
];
// Environment variable mapping
const ENV_KEY_MAP: Record<string, string> = {
anthropic: 'ANTHROPIC_API_KEY',
openai: 'OPENAI_API_KEY',
'openai-codex': 'OPENAI_API_KEY',
gemini: 'GEMINI_API_KEY',
groq: 'GROQ_API_KEY',
ollama: '', // No key needed for local ollama
};
function detectLLMConfig(api: MoltbotPluginAPI): {
function detectLLMConfig(): {
provider: string;
apiKey: string;
model?: string;
envKey?: string;
baseUrl?: string;
source: string;
} {
// Get models from config (agents.defaults.models is a dictionary of models)
const models = api.config.agents?.defaults?.models;
if (!models || Object.keys(models).length === 0) {
throw new Error(
'No models configured in Moltbot. Please configure at least one model in agents.defaults.models'
);
// Override values from HINDSIGHT_API_LLM_* env vars (highest priority)
const overrideProvider = process.env.HINDSIGHT_API_LLM_PROVIDER;
const overrideModel = process.env.HINDSIGHT_API_LLM_MODEL;
const overrideKey = process.env.HINDSIGHT_API_LLM_API_KEY;
const overrideBaseUrl = process.env.HINDSIGHT_API_LLM_BASE_URL;
// If provider is explicitly set, use that (with overrides)
if (overrideProvider) {
if (!overrideKey && overrideProvider !== 'ollama') {
throw new Error(
`HINDSIGHT_API_LLM_PROVIDER is set to "${overrideProvider}" but HINDSIGHT_API_LLM_API_KEY is not set.\n` +
`Please set: export HINDSIGHT_API_LLM_API_KEY=your-api-key`
);
}
const providerInfo = PROVIDER_DETECTION.find(p => p.name === overrideProvider);
return {
provider: overrideProvider,
apiKey: overrideKey || '',
model: overrideModel || (providerInfo?.defaultModel),
baseUrl: overrideBaseUrl,
source: 'HINDSIGHT_API_LLM_PROVIDER override',
};
}
// Try all configured models to find one with an available API key
const configuredModels = Object.keys(models);
// Auto-detect from standard provider env vars
for (const providerInfo of PROVIDER_DETECTION) {
const apiKey = providerInfo.keyEnv ? process.env[providerInfo.keyEnv] : '';
for (const modelKey of configuredModels) {
const [moltbotProvider, ...modelParts] = modelKey.split('/');
const model = modelParts.join('/');
const hindsightProvider = PROVIDER_MAP[moltbotProvider];
if (!hindsightProvider) {
continue; // Skip unsupported providers
// Skip ollama in auto-detection (must be explicitly requested)
if (providerInfo.name === 'ollama') {
continue;
}
const envKey = ENV_KEY_MAP[moltbotProvider];
const apiKey = envKey ? process.env[envKey] || '' : '';
// For ollama, no key is needed
if (hindsightProvider === 'ollama') {
return { provider: hindsightProvider, apiKey: '', model, envKey: '' };
}
// If we found a key, use this provider
if (apiKey) {
return { provider: hindsightProvider, apiKey, model, envKey };
return {
provider: providerInfo.name,
apiKey,
model: overrideModel || providerInfo.defaultModel,
baseUrl: overrideBaseUrl, // Only use explicit HINDSIGHT_API_LLM_BASE_URL
source: `auto-detected from ${providerInfo.keyEnv}`,
};
}
}
// No API keys found for any provider - show helpful error
const configuredProviders = configuredModels
.map(m => m.split('/')[0])
.filter(p => PROVIDER_MAP[p]);
const keyInstructions = configuredProviders
.map(p => {
const envVar = ENV_KEY_MAP[p];
return envVar ? `${envVar} (for ${p})` : null;
})
.filter(Boolean)
.join('\n');
// No configuration found - show helpful error
throw new Error(
`No API keys found for Hindsight memory plugin.\n\n` +
`Configured providers in Moltbot: ${configuredProviders.join(', ')}\n\n` +
`Please set one of these environment variables:\n${keyInstructions}\n\n` +
`You can set them in your shell profile (~/.zshrc or ~/.bashrc):\n` +
` export ANTHROPIC_API_KEY="your-key-here"\n\n` +
`Or run OpenClaw with the environment variable:\n` +
` ANTHROPIC_API_KEY="your-key" openclaw gateway\n\n` +
`Alternatively, configure ollama provider which doesn't require an API key.`
`No LLM configuration found for Hindsight memory plugin.\n\n` +
`Option 1: Set a standard provider API key (auto-detect):\n` +
` export OPENAI_API_KEY=sk-your-key # Uses gpt-4o-mini\n` +
` export ANTHROPIC_API_KEY=your-key # Uses claude-3-5-haiku\n` +
` export GEMINI_API_KEY=your-key # Uses gemini-2.0-flash-exp\n` +
` export GROQ_API_KEY=your-key # Uses llama-3.3-70b-versatile\n\n` +
`Option 2: Override with Hindsight-specific config:\n` +
` export HINDSIGHT_API_LLM_PROVIDER=openai\n` +
` export HINDSIGHT_API_LLM_MODEL=gpt-4o-mini\n` +
` export HINDSIGHT_API_LLM_API_KEY=sk-your-key\n` +
` export HINDSIGHT_API_LLM_BASE_URL=https://openrouter.ai/api/v1 # Optional\n\n` +
`Tip: Use a cheap/fast model for memory extraction (e.g., gpt-4o-mini, claude-3-5-haiku, or free models on OpenRouter)`
);
}
@ -129,14 +122,17 @@ export default function (api: MoltbotPluginAPI) {
try {
console.log('[Hindsight] Plugin loading...');
// Detect LLM configuration from Moltbot
// Detect LLM configuration from environment
console.log('[Hindsight] Detecting LLM config...');
const llmConfig = detectLLMConfig(api);
const llmConfig = detectLLMConfig();
const baseUrlInfo = llmConfig.baseUrl ? `, base URL: ${llmConfig.baseUrl}` : '';
const modelInfo = llmConfig.model || 'default';
if (llmConfig.provider === 'ollama') {
console.log(`[Hindsight] ✓ Using provider: ${llmConfig.provider}, model: ${llmConfig.model || 'default'} (no API key required)`);
console.log(`[Hindsight] ✓ Using provider: ${llmConfig.provider}, model: ${modelInfo} (${llmConfig.source})`);
} else {
console.log(`[Hindsight] ✓ Using provider: ${llmConfig.provider}, model: ${llmConfig.model || 'default'} (API key: ${llmConfig.envKey})`);
console.log(`[Hindsight] ✓ Using provider: ${llmConfig.provider}, model: ${modelInfo} (${llmConfig.source}${baseUrlInfo})`);
}
console.log('[Hindsight] Getting plugin config...');
@ -161,6 +157,7 @@ export default function (api: MoltbotPluginAPI) {
llmConfig.provider,
llmConfig.apiKey,
llmConfig.model,
llmConfig.baseUrl,
pluginConfig.daemonIdleTimeout,
pluginConfig.embedVersion
);
@ -198,9 +195,62 @@ export default function (api: MoltbotPluginAPI) {
api.registerService({
id: 'hindsight-memory',
async start() {
console.log('[Hindsight] Service start called - checking daemon health...');
// Wait for background init if still pending
console.log('[Hindsight] Service start called - ensuring initialization complete...');
if (initPromise) await initPromise;
if (initPromise) {
try {
await initPromise;
} catch (error) {
console.error('[Hindsight] Initial initialization failed:', error);
// Continue to health check below
}
}
// Check if daemon is actually healthy (handles SIGUSR1 restart case)
if (embedManager && isInitialized) {
const healthy = await embedManager.checkHealth();
if (healthy) {
console.log('[Hindsight] Daemon is healthy');
return;
}
console.log('[Hindsight] Daemon is not responding - reinitializing...');
// Reset state for reinitialization
embedManager = null;
client = null;
isInitialized = false;
}
// Reinitialize if needed (fresh start or recovery from dead daemon)
if (!isInitialized) {
console.log('[Hindsight] Reinitializing daemon...');
const llmConfig = detectLLMConfig();
const pluginConfig = getPluginConfig(api);
const port = pluginConfig.embedPort || Math.floor(Math.random() * 10000) + 10000;
embedManager = new HindsightEmbedManager(
port,
llmConfig.provider,
llmConfig.apiKey,
llmConfig.model,
llmConfig.baseUrl,
pluginConfig.daemonIdleTimeout,
pluginConfig.embedVersion
);
await embedManager.start();
client = new HindsightClient(llmConfig.provider, llmConfig.apiKey, llmConfig.model, pluginConfig.embedVersion);
client.setBankId(BANK_NAME);
if (pluginConfig.bankMission) {
await client.setBankMission(pluginConfig.bankMission);
}
isInitialized = true;
console.log('[Hindsight] Reinitialization complete');
}
},
async stop() {

View file

@ -1537,6 +1537,7 @@ dependencies = [
[package.dev-dependencies]
dev = [
{ name = "pytest" },
{ name = "ruff" },
{ name = "ty" },
]
@ -1546,6 +1547,7 @@ requires-dist = [{ name = "httpx", specifier = ">=0.27.0" }]
[package.metadata.requires-dev]
dev = [
{ name = "pytest", specifier = ">=8.0.0" },
{ name = "ruff", specifier = ">=0.8.0" },
{ name = "ty", specifier = ">=0.0.1" },
]