{
  "id": "hindsight-openclaw",
  "name": "Hindsight Memory",
  "description": "Hindsight memory plugin for OpenClaw — persistent conversational memory backed by the Hindsight retrieval engine. Auto-recall injects relevant memories into the prompt, auto-retain stores conversation turns into per-user/per-channel banks. Memory plugin (kind=memory): does not register agent tools.",
  "version": "0.9.1",
  "kind": "memory",
  "activation": {
    "onStartup": true
  },
  "configContracts": {
    "secretInputs": {
      "paths": [
        {
          "path": "llmApiKey",
          "expected": "string"
        },
        {
          "path": "hindsightApiToken",
          "expected": "string"
        },
        {
          "path": "recallRouter.jinaApiKey",
          "expected": "string"
        }
      ]
    }
  },
  "configSchema": {
    "type": "object",
    "properties": {
      "daemonIdleTimeout": {
        "type": "number",
        "description": "Seconds before daemon shuts down from inactivity (0 = never)",
        "default": 0
      },
      "embedPort": {
        "type": "number",
        "description": "Port for hindsight-embed server (auto-assigned if not specified)",
        "default": 0
      },
      "bankMission": {
        "type": "string",
        "description": "Agent identity/purpose stored on the memory bank. Helps the memory engine understand context for better fact extraction during retain. Set once per bank on first use — this is not a recall prompt.",
        "default": "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": {
        "type": "string",
        "description": "hindsight-embed version to use (e.g. 'latest', '0.4.2', or empty for latest)",
        "default": "latest"
      },
      "llmProvider": {
        "type": "string",
        "description": "LLM provider for Hindsight memory (e.g. 'openai', 'anthropic', 'gemini', 'groq', 'ollama', 'openai-codex', 'claude-code').",
        "enum": [
          "openai",
          "anthropic",
          "gemini",
          "groq",
          "ollama",
          "openai-codex",
          "claude-code"
        ]
      },
      "llmModel": {
        "type": "string",
        "description": "LLM model to use (e.g. 'gpt-4o-mini', 'claude-3-5-haiku-20241022'). Used with llmProvider."
      },
      "llmApiKey": {
        "type": [
          "string",
          "object"
        ],
        "description": "API key for the LLM provider used by the Hindsight memory daemon. Set via 'openclaw config set ... --ref-source env --ref-id OPENAI_API_KEY' to reference an env var without storing plaintext."
      },
      "llmBaseUrl": {
        "type": "string",
        "description": "Optional base URL override for OpenAI-compatible providers (e.g. 'https://openrouter.ai/api/v1')."
      },
      "embedPackagePath": {
        "type": "string",
        "description": "Local path to hindsight package for development (e.g. '/path/to/hindsight'). When set, uses 'uv run --directory <path>' instead of 'uvx hindsight-embed@latest'."
      },
      "apiPort": {
        "type": "number",
        "description": "Port for the openclaw profile daemon (default: 9077)",
        "default": 9077
      },
      "hindsightApiUrl": {
        "type": "string",
        "description": "External Hindsight API URL (e.g. 'https://mcp.hindsight.devcraft.team'). When set, skips local daemon and connects directly to this API."
      },
      "hindsightApiToken": {
        "type": [
          "string",
          "object"
        ],
        "description": "API token for external Hindsight API authentication. Required if the external API has authentication enabled."
      },
      "dynamicBankId": {
        "type": "boolean",
        "description": "Enable per-user memory banks. When true, memories are isolated by user per channel (e.g., slack-U123, telegram-456789). When false, all users share a single 'openclaw' bank.",
        "default": true
      },
      "bankId": {
        "type": "string",
        "description": "Static bank ID used when dynamicBankId is false."
      },
      "bankIdPrefix": {
        "type": "string",
        "description": "Optional prefix for bank IDs (e.g., 'prod' results in 'prod-slack-U123'). Useful for separating environments."
      },
      "retainTags": {
        "type": "array",
        "items": {
          "type": "string"
        },
        "description": "Tags applied to every retained document (e.g. ['source_system:openclaw', 'agent:agentname'])."
      },
      "retainSource": {
        "type": "string",
        "description": "Source value written into retained document metadata. Defaults to 'openclaw'.",
        "default": "openclaw"
      },
      "autoRecall": {
        "type": "boolean",
        "description": "Automatically recall memories on every prompt and inject them as context. Set to false when agent has its own recall tool.",
        "default": true
      },
      "excludeProviders": {
        "type": "array",
        "items": {
          "type": "string"
        },
        "description": "Message providers to exclude from recall and retain (e.g. ['heartbeat', 'telegram', 'discord'])",
        "default": [
          "heartbeat"
        ]
      },
      "excludeSenders": {
        "type": "array",
        "items": {
          "type": "string"
        },
        "description": "Sender IDs to exclude from recall and retain. Defends against phantom memory banks created when the OpenClaw gateway falls back to a channel name as senderId for unauthenticated sessions (e.g. 'openclaw-control-ui' when an internal browser session connects without a token). Pass [] to disable the protection. Default: ['anonymous', 'unknown', 'openclaw-control-ui']",
        "default": [
          "anonymous",
          "unknown",
          "openclaw-control-ui"
        ]
      },
      "dynamicBankGranularity": {
        "type": "array",
        "items": {
          "type": "string",
          "enum": [
            "agent",
            "channel",
            "user",
            "provider"
          ]
        },
        "description": "Fields used to derive bank ID. Controls memory isolation granularity. Default: ['agent', 'channel', 'user'].",
        "default": [
          "agent",
          "channel",
          "user"
        ]
      },
      "autoRetain": {
        "type": "boolean",
        "description": "Automatically retain conversation as memories after each interaction. Set to false to disable.",
        "default": true
      },
      "retainRoles": {
        "type": "array",
        "items": {
          "type": "string",
          "enum": [
            "user",
            "assistant",
            "system",
            "tool"
          ]
        },
        "description": "Message roles to include in retained transcript. Default: ['user', 'assistant'].",
        "default": [
          "user",
          "assistant"
        ]
      },
      "retainFormat": {
        "type": "string",
        "description": "Serialization format for retained conversation content. 'json' (default) emits a structured array of {role, content} messages, matching the Claude Code integration. 'text' emits the legacy '[role: x] ... [x:end]' markers.",
        "enum": [
          "json",
          "text"
        ],
        "default": "json"
      },
      "retainToolCalls": {
        "type": "boolean",
        "description": "When true (default) and retainFormat is 'json', each message's content is an Anthropic-shaped array of typed blocks including tool_use (agent tool calls) and tool_result (truncated at 2000 chars). Operational MCP tools — Hindsight's own recall/retain/search — are filtered out to avoid feedback loops. Set to false to retain text-only content, one string per message.",
        "default": true
      },
      "retainEveryNTurns": {
        "type": "integer",
        "description": "Retain every Nth turn instead of every turn. 1 = every turn (default). Values > 1 enable chunked retention with a sliding window.",
        "minimum": 1,
        "default": 1
      },
      "retainOverlapTurns": {
        "type": "integer",
        "description": "Extra prior turns to include when chunked retention fires. Window = retainEveryNTurns + retainOverlapTurns. Only applies when retainEveryNTurns > 1.",
        "minimum": 0,
        "default": 0
      },
      "recallBudget": {
        "type": "string",
        "description": "Recall effort level. Higher budgets use more retrieval strategies for better results but take longer.",
        "enum": [
          "low",
          "mid",
          "high"
        ],
        "default": "mid"
      },
      "recallMaxTokens": {
        "type": "integer",
        "description": "Maximum tokens for recall response. Controls how much memory context is injected per turn.",
        "minimum": 1,
        "default": 1024
      },
      "recallTypes": {
        "type": "array",
        "items": {
          "type": "string",
          "enum": [
            "world",
            "experience",
            "observation"
          ]
        },
        "description": "Memory types to recall. Defaults to ['world', 'experience'] — excludes verbose observation entries.",
        "default": [
          "world",
          "experience"
        ]
      },
      "recallRoles": {
        "type": "array",
        "items": {
          "type": "string",
          "enum": [
            "user",
            "assistant",
            "system",
            "tool"
          ]
        },
        "description": "Roles to include when composing contextual recall query. Default: ['user', 'assistant'].",
        "default": [
          "user",
          "assistant"
        ]
      },
      "recallContextTurns": {
        "type": "integer",
        "minimum": 1,
        "description": "Number of user turns to include in recall query context. 1 keeps latest-message-only behavior.",
        "default": 1
      },
      "recallMaxQueryChars": {
        "type": "integer",
        "minimum": 1,
        "description": "Maximum character length for composed recall query before calling recall.",
        "default": 800
      },
      "recallTopK": {
        "type": "integer",
        "minimum": 1,
        "description": "Maximum number of memories to inject per turn. Applied after API response as a hard cap."
      },
      "recallPromptPreamble": {
        "type": "string",
        "description": "Text shown above recalled memories in the injected context block.",
        "default": "Relevant memories from past conversations (prioritize recent when conflicting). Only use memories that are directly useful to continue this conversation; ignore the rest:"
      },
      "recallTimeoutMs": {
        "type": "integer",
        "minimum": 1000,
        "description": "Timeout for auto-recall in milliseconds. Increase if recall times out with high budget.",
        "default": 10000
      },
      "recallInjectionPosition": {
        "type": "string",
        "enum": [
          "prepend",
          "append",
          "user"
        ],
        "description": "Where to inject recalled memories. 'prepend' = start of system prompt (default), 'append' = end of system prompt (preserves prompt cache), 'user' = before user message.",
        "default": "prepend"
      },
      "recallMinRelevance": {
        "type": [
          "number",
          "null"
        ],
        "minimum": 0,
        "maximum": 1,
        "description": "Minimum cross-encoder score [0..1] required for a recalled fact to be injected. Activates the recall trace and filters out every fact below the threshold. When all results fall below, no <hindsight_memories> block is emitted (silence beats noise). Default 0.3 is a no-op on the `rrf` passthrough reranker (constant 0.5 ≥ 0.3) and an effective filter once a real neural reranker (Jina/Cohere/BGE) is configured server-side. Set to 0 to keep the trace observable without filtering. Set to null to disable both trace and filtering.",
        "default": 0.3
      },
      "recallRouter": {
        "type": "object",
        "description": "Recall router — decides whether (and what kinds of) memory recall should be performed for a given user turn. Heuristics skip recall on heartbeats, cron, agent meta-questions, and CLI test pings (zero cost). When mode='jina-classifier' and jinaApiKey is set, ambiguous cases consult Jina /v1/classify to refine the decision.",
        "properties": {
          "enabled": {
            "type": "boolean",
            "description": "Master switch. When false, recall is never gated (legacy behavior).",
            "default": true
          },
          "mode": {
            "type": "string",
            "enum": [
              "heuristic",
              "jina-classifier"
            ],
            "description": "Engine for ambiguous cases. 'heuristic' = deterministic regex rules only (default, zero cost). 'jina-classifier' = call Jina /v1/classify when heuristics are inconclusive.",
            "default": "heuristic"
          },
          "jinaApiKey": {
            "type": [
              "string",
              "object"
            ],
            "description": "Jina API key. Required when mode='jina-classifier'. Configure as SecretRef: 'openclaw config set ... recallRouter.jinaApiKey --ref-source env --ref-id JINA_API_KEY'."
          },
          "classifierId": {
            "type": "string",
            "description": "Optional Jina few-shot classifier ID. When provided, the router uses POST /v1/classify with this ID instead of zero-shot. Train classifiers out-of-band via the Jina Playground."
          },
          "routes": {
            "type": "number",
            "enum": [
              2,
              4
            ],
            "description": "Number of zero-shot classes. 2 = NONE/ALL (robust on short FR prompts). 4 = NONE/WORLD_ONLY/EXPERIENCE_ONLY/ALL (finer routing, requires longer or unambiguous prompts).",
            "default": 2
          },
          "labels": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Custom label list for zero-shot classification. Overrides 'routes'. Each label must start with one of NONE / WORLD_ONLY / EXPERIENCE_ONLY / ALL followed by ': description'. At least 2 labels."
          },
          "minConfidence": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Minimum classifier confidence required to trust a prediction. Below this, the router fails open to ALL with reason 'classifier_low_confidence'. Calibrated against Jina v3 zero-shot scores (cluster around 0.25 when no label matches).",
            "default": 0.35
          },
          "observationFollowsNarrow": {
            "type": "boolean",
            "description": "When the router narrows to WORLD_ONLY or EXPERIENCE_ONLY, controls whether 'observation' (consolidated facts) is preserved alongside the chosen type. Default true — consolidated observations are derived from world/experience and are often the highest-signal recall targets, so dropping them on narrow routes would silently lose the most relevant fact. Set false for strict route exclusivity. Only effective when 'observation' is already in recallTypes.",
            "default": true
          }
        },
        "additionalProperties": false
      },
      "debug": {
        "type": "boolean",
        "description": "Enable debug logging for Hindsight plugin operations. Equivalent to logLevel: 'debug'.",
        "default": false
      },
      "logLevel": {
        "type": "string",
        "description": "Console log verbosity. 'off' = no output, 'error' = errors only, 'warning' = errors + warnings, 'info' = key events + periodic summaries, 'debug' = all details.",
        "enum": [
          "off",
          "error",
          "warning",
          "info",
          "debug"
        ],
        "default": "info"
      },
      "logSummaryIntervalMs": {
        "type": "number",
        "description": "Interval in ms to batch retain/recall log summaries. 0 = log every event individually. Default: 300000 (5 min).",
        "default": 300000
      },
      "retainQueuePath": {
        "type": "string",
        "description": "Path to JSONL file for buffering failed retains (external API mode only). Default: ~/.openclaw/data/hindsight-retain-queue.jsonl"
      },
      "retainQueueMaxAgeMs": {
        "type": "number",
        "description": "Max age in ms for queued retain items. -1 = keep forever.",
        "default": -1
      },
      "retainQueueFlushIntervalMs": {
        "type": "number",
        "description": "How often to attempt flushing queued retains in ms. Default: 60000 (1 min).",
        "default": 60000
      },
      "ignoreSessionPatterns": {
        "type": "array",
        "items": {
          "type": "string"
        },
        "description": "Session key glob patterns to skip entirely (no recall, no retain). E.g. [\"agent:main:**\", \"agent:*:cron:**\"]. * matches non-colon chars, ** matches anything."
      },
      "statelessSessionPatterns": {
        "type": "array",
        "items": {
          "type": "string"
        },
        "description": "Session key glob patterns for read-only sessions: retain is always skipped, recall is skipped when skipStatelessSessions is true. E.g. [\"agent:*:subagent:**\", \"agent:*:heartbeat:**\"]."
      },
      "skipStatelessSessions": {
        "type": "boolean",
        "description": "When true (default), sessions matching statelessSessionPatterns also skip recall. When false, they can recall but never retain.",
        "default": true
      },
      "provenanceReport": {
        "type": "string",
        "enum": [
          "off",
          "metadata",
          "full"
        ],
        "default": "off",
        "description": "Provenance reporting toward chat frontends (provenance/v1): off = no emission; metadata = fact ids/types/dates/scores; full = plus the exact injected fact text. Reports ride the gateway agent-event bus under the chat's own ACL — never logs."
      }
    },
    "additionalProperties": false
  },
  "uiHints": {
    "daemonIdleTimeout": {
      "label": "Daemon Idle Timeout",
      "placeholder": "0 (never timeout)"
    },
    "embedPort": {
      "label": "Embed Server Port",
      "placeholder": "0 (auto-assign)"
    },
    "bankMission": {
      "label": "Bank Mission",
      "placeholder": "Custom context for what this agent does..."
    },
    "embedVersion": {
      "label": "Hindsight Embed Version",
      "placeholder": "latest (or pin to specific version like 0.4.2)"
    },
    "llmProvider": {
      "label": "LLM Provider",
      "placeholder": "e.g. openai, anthropic, gemini, groq"
    },
    "llmModel": {
      "label": "LLM Model",
      "placeholder": "e.g. gpt-4o-mini, claude-3-5-haiku-20241022"
    },
    "llmApiKey": {
      "label": "LLM API Key",
      "placeholder": "API key for the chosen LLM provider",
      "sensitive": true
    },
    "llmBaseUrl": {
      "label": "LLM Base URL",
      "placeholder": "e.g. https://openrouter.ai/api/v1 (optional)"
    },
    "embedPackagePath": {
      "label": "Local Package Path (Dev)",
      "placeholder": "/path/to/hindsight (for local development)"
    },
    "apiPort": {
      "label": "API Port",
      "placeholder": "9077 (default)"
    },
    "hindsightApiUrl": {
      "label": "External Hindsight API URL",
      "placeholder": "e.g. https://mcp.hindsight.devcraft.team (leave empty for local daemon)"
    },
    "hindsightApiToken": {
      "label": "External API Token",
      "placeholder": "API token if external API requires authentication",
      "sensitive": true
    },
    "dynamicBankId": {
      "label": "Dynamic Bank IDs",
      "placeholder": "true (isolate memories per channel)"
    },
    "bankId": {
      "label": "Static Bank ID",
      "placeholder": "e.g. openclaw, shared-bank (used when dynamicBankId is false)"
    },
    "bankIdPrefix": {
      "label": "Bank ID Prefix",
      "placeholder": "e.g., prod, staging (optional)"
    },
    "retainTags": {
      "label": "Retain Tags",
      "placeholder": "e.g. ['source_system:openclaw', 'agent:agentname']"
    },
    "retainSource": {
      "label": "Retain Source",
      "placeholder": "openclaw"
    },
    "autoRecall": {
      "label": "Auto-Recall",
      "placeholder": "true (inject memories on every prompt)"
    },
    "excludeProviders": {
      "label": "Excluded Providers",
      "placeholder": "e.g. heartbeat, telegram, discord"
    },
    "excludeSenders": {
      "label": "Excluded Senders",
      "placeholder": "e.g. openclaw-control-ui, anonymous"
    },
    "dynamicBankGranularity": {
      "label": "Bank Granularity",
      "placeholder": "e.g. ['agent', 'channel', 'user']"
    },
    "autoRetain": {
      "label": "Auto-Retain",
      "placeholder": "true (enable auto-retention)"
    },
    "retainRoles": {
      "label": "Retain Roles",
      "placeholder": "e.g. ['user', 'assistant']"
    },
    "retainFormat": {
      "label": "Retain Format",
      "placeholder": "json (default) or text (legacy markers)"
    },
    "retainToolCalls": {
      "label": "Retain Tool Calls",
      "placeholder": "true (default) — include tool_use / tool_result blocks in retained JSON"
    },
    "retainEveryNTurns": {
      "label": "Retain Every N Turns",
      "placeholder": "1 (every turn, default)"
    },
    "retainOverlapTurns": {
      "label": "Retain Overlap Turns",
      "placeholder": "0 (no overlap, default)"
    },
    "recallBudget": {
      "label": "Recall Budget",
      "placeholder": "low, mid, or high"
    },
    "recallMaxTokens": {
      "label": "Recall Max Tokens",
      "placeholder": "1024 (default)"
    },
    "recallTypes": {
      "label": "Recall Types",
      "placeholder": "e.g. ['world', 'experience']"
    },
    "recallRoles": {
      "label": "Recall Roles",
      "placeholder": "e.g. ['user', 'assistant']"
    },
    "recallContextTurns": {
      "label": "Recall Context Turns",
      "placeholder": "1 (latest only, default)"
    },
    "recallMaxQueryChars": {
      "label": "Recall Max Query Chars",
      "placeholder": "800 (default)"
    },
    "recallTopK": {
      "label": "Recall Top K",
      "placeholder": "e.g. 5 (no limit by default)"
    },
    "recallPromptPreamble": {
      "label": "Recall Prompt Preamble",
      "placeholder": "Instruction shown above recalled memories in injected context"
    },
    "recallTimeoutMs": {
      "label": "Recall Timeout (ms)",
      "placeholder": "10000 (default)"
    },
    "recallInjectionPosition": {
      "label": "Recall Injection Position",
      "placeholder": "prepend, append, or user"
    },
    "recallMinRelevance": {
      "label": "Recall Min Relevance",
      "placeholder": "0.3 (default) — set to 0 to disable filtering but keep trace, null to disable both"
    },
    "recallRouter": {
      "label": "Recall Router",
      "placeholder": "{ enabled: true, mode: 'heuristic' } — set mode='jina-classifier' and jinaApiKey to enable semantic routing"
    },
    "debug": {
      "label": "Debug"
    },
    "logLevel": {
      "label": "Log Level"
    },
    "logSummaryIntervalMs": {
      "label": "Log Summary Interval (ms)",
      "placeholder": "300000"
    },
    "retainQueuePath": {
      "label": "Retain Queue File Path",
      "placeholder": "~/.openclaw/data/hindsight-retain-queue.jsonl"
    },
    "retainQueueMaxAgeMs": {
      "label": "Retain Queue Max Age (ms)",
      "placeholder": "-1 (forever)"
    },
    "retainQueueFlushIntervalMs": {
      "label": "Retain Queue Flush Interval (ms)",
      "placeholder": "60000"
    },
    "ignoreSessionPatterns": {
      "label": "Ignore Session Patterns",
      "placeholder": "e.g. [\"agent:main:**\", \"agent:*:cron:**\"]"
    },
    "statelessSessionPatterns": {
      "label": "Stateless Session Patterns",
      "placeholder": "e.g. [\"agent:*:subagent:**\", \"agent:*:heartbeat:**\"]"
    },
    "skipStatelessSessions": {
      "label": "Skip Stateless Sessions",
      "placeholder": "true (default)"
    }
  }
}