* feat: Add Claude Code integration plugin Complete port of hindsight-openclaw (v0.4.19) adapted to Claude Code's hook-based plugin architecture. Pure Python stdlib, no external dependencies. - Auto-recall via UserPromptSubmit hook (additionalContext injection) - Auto-retain via async Stop hook (chunked retention with sliding window) - Daemon management (auto-start/stop hindsight-embed via uvx) - Dynamic bank IDs with per-agent/project/channel/user granularity - All 34 configuration options with env var overrides - File-based state persistence with fcntl locking - Graceful degradation on all error paths Works with Claude Code Channels (Telegram, Discord, Slack) and interactive sessions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: Set correct chunked retention defaults (10/2, not 1/0) retainEveryNTurns=10 and retainOverlapTurns=2 are the production-tested values — every 10 turns, retain a 12-turn sliding window. The previous defaults (1/0) would retain every single turn with no overlap, defeating the chunked retention design that prevents API bombardment. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: Align recallBudget and daemonIdleTimeout with Openclaw defaults recallBudget: "low" → "mid" (Openclaw default) daemonIdleTimeout: 300 → 0 (Openclaw default, never auto-stop) As an official Hindsight integration, defaults should match Openclaw. Users can optimize locally via settings.json or env vars. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
122 lines
4.5 KiB
Python
122 lines
4.5 KiB
Python
"""Bank ID derivation and mission management.
|
|
|
|
Port of Openclaw's deriveBankId() and banksWithMissionSet logic, adapted
|
|
for Claude Code's context model.
|
|
|
|
Openclaw derives bank IDs from: agent, channel, user, provider.
|
|
Claude Code equivalent dimensions:
|
|
- agent → configured name or "claude-code" (HINDSIGHT_AGENT_NAME)
|
|
- project → derived from cwd (working directory basename)
|
|
- session → session_id from hook input
|
|
- channel → from env var HINDSIGHT_CHANNEL_ID (for Telegram/Discord agents)
|
|
- user → from env var HINDSIGHT_USER_ID (for multi-user agents)
|
|
|
|
The channel/user dimensions enable the same per-user/per-channel isolation
|
|
that Openclaw provides via its messageProvider/channelId/senderId context.
|
|
Telegram/Discord agents set HINDSIGHT_CHANNEL_ID and HINDSIGHT_USER_ID in
|
|
their environment to achieve equivalent behavior.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import urllib.parse
|
|
|
|
from .state import read_state, write_state
|
|
|
|
DEFAULT_BANK_NAME = "claude-code"
|
|
|
|
# Valid granularity fields for Claude Code
|
|
VALID_FIELDS = {"agent", "project", "session", "channel", "user"}
|
|
|
|
|
|
def derive_bank_id(hook_input: dict, config: dict) -> str:
|
|
"""Derive a bank ID from hook context and config.
|
|
|
|
Port of: deriveBankId() in index.js
|
|
|
|
When dynamicBankId is false, returns the static bank.
|
|
When true, composes from granularity fields joined by '::'.
|
|
|
|
Args:
|
|
hook_input: The hook's stdin JSON (has session_id, cwd).
|
|
config: Plugin configuration dict.
|
|
"""
|
|
prefix = config.get("bankIdPrefix", "")
|
|
|
|
if not config.get("dynamicBankId", False):
|
|
# Static mode — single bank for everything
|
|
base = config.get("bankId") or DEFAULT_BANK_NAME
|
|
return f"{prefix}-{base}" if prefix else base
|
|
|
|
# Dynamic mode — compose from granularity fields
|
|
fields = config.get("dynamicBankGranularity")
|
|
if not fields or not isinstance(fields, list):
|
|
fields = ["agent", "project"]
|
|
|
|
# Warn on unknown fields (mirrors Openclaw's runtime check)
|
|
for f in fields:
|
|
if f not in VALID_FIELDS:
|
|
print(
|
|
f'[Hindsight] Unknown dynamicBankGranularity field "{f}" — '
|
|
f"valid for Claude Code: {', '.join(sorted(VALID_FIELDS))}",
|
|
file=sys.stderr,
|
|
)
|
|
|
|
# Build field values from hook context + env vars
|
|
cwd = hook_input.get("cwd", "")
|
|
session_id = hook_input.get("session_id", "")
|
|
agent_name = config.get("agentName", "claude-code")
|
|
|
|
# Channel and user come from environment variables, set by the host agent
|
|
# (e.g. Telegram bot sets HINDSIGHT_CHANNEL_ID=telegram-group-12345)
|
|
channel_id = os.environ.get("HINDSIGHT_CHANNEL_ID", "")
|
|
user_id = os.environ.get("HINDSIGHT_USER_ID", "")
|
|
|
|
field_map = {
|
|
"agent": agent_name,
|
|
"project": os.path.basename(cwd) if cwd else "unknown",
|
|
"session": session_id or "unknown",
|
|
"channel": channel_id or "default",
|
|
"user": user_id or "anonymous",
|
|
}
|
|
|
|
segments = [urllib.parse.quote(field_map.get(f, "unknown"), safe="") for f in fields]
|
|
base_bank_id = "::".join(segments)
|
|
|
|
return f"{prefix}-{base_bank_id}" if prefix else base_bank_id
|
|
|
|
|
|
def ensure_bank_mission(client, bank_id: str, config: dict, debug_fn=None):
|
|
"""Set bank mission on first use, skip if already set.
|
|
|
|
Port of: banksWithMissionSet Set tracking in index.js
|
|
|
|
Uses a state file to persist which banks have had their mission set
|
|
across ephemeral hook invocations.
|
|
"""
|
|
mission = config.get("bankMission", "")
|
|
if not mission or not mission.strip():
|
|
return
|
|
|
|
# Check if we've already set mission for this bank
|
|
missions_set = read_state("bank_missions.json", {})
|
|
if bank_id in missions_set:
|
|
return
|
|
|
|
try:
|
|
retain_mission = config.get("retainMission")
|
|
client.set_bank_mission(bank_id, mission, retain_mission=retain_mission, timeout=10)
|
|
missions_set[bank_id] = True
|
|
# Cap tracked banks (mirrors Openclaw's MAX_TRACKED_BANK_CLIENTS)
|
|
if len(missions_set) > 10000:
|
|
keys = sorted(missions_set.keys())
|
|
for k in keys[: len(keys) // 2]:
|
|
del missions_set[k]
|
|
write_state("bank_missions.json", missions_set)
|
|
if debug_fn:
|
|
debug_fn(f"Set mission for bank: {bank_id}")
|
|
except Exception as e:
|
|
# Don't fail if mission set fails — bank might not exist yet,
|
|
# will be created on first retain (mirrors Openclaw behavior)
|
|
if debug_fn:
|
|
debug_fn(f"Could not set bank mission for {bank_id}: {e}")
|