diff --git a/reflexio/cli/commands/setup_cmd.py b/reflexio/cli/commands/setup_cmd.py index bde0b2c2..e0f04aae 100644 --- a/reflexio/cli/commands/setup_cmd.py +++ b/reflexio/cli/commands/setup_cmd.py @@ -23,6 +23,7 @@ class InstallLocation(Enum): CURRENT_PROJECT = "current_project" ALL_PROJECTS = "all_projects" + app = typer.Typer( help="Configure Reflexio: run 'init' for plain CLI setup, or one of " "the integration commands (openclaw, claude-code) to also install " @@ -492,7 +493,17 @@ def openclaw( ), ] = False, ) -> None: - """Set up (or remove) the Reflexio integration for OpenClaw.""" + """Set up (or remove) the Reflexio integration for OpenClaw. + + The OpenClaw integration does NOT require an LLM provider API key. + Playbook extraction runs in the agent's own session (via the + ``/reflexio-extract`` slash command, which applies the v3.0.0 + extraction rubric in-context and writes playbooks through direct CRUD). + The Reflexio server only performs CRUD + semantic search against its + local store, and falls back gracefully to FTS-only ranking when no + embedding provider is configured. The wizard therefore skips the LLM + provider prompt entirely. + """ if uninstall: _uninstall_openclaw() return @@ -505,28 +516,27 @@ def openclaw( typer.echo("Error: could not locate or create a .env file") raise typer.Exit(1) - # Step 2: LLM provider - display_name, model, provider_key = _prompt_llm_provider(env_path) - - # Step 2.5: Embedding provider (if LLM provider lacks embedding support) - embedding_label = _prompt_embedding_provider(env_path, provider_key) + typer.echo( + "\nThe OpenClaw integration is fully LLM-free. Extraction runs in " + "your own agent session when you invoke /reflexio-extract, and the " + "Reflexio server only performs local CRUD + search. No LLM provider " + "API key is required." + ) - # Step 3: Storage + # Step 2: Storage storage_label = _prompt_storage(env_path) - # Step 4: Install OpenClaw integration + # Step 3: Install OpenClaw integration typer.echo("") hook_ok = _install_openclaw_integration() - # Step 5: Summary + # Step 4: Summary hook_status = "reflexio-context" if hook_ok else "reflexio-context (unverified)" skill_path = Path.home() / ".openclaw" / "skills" / "reflexio" typer.echo("") typer.echo("Setup complete!") - typer.echo(f" LLM Provider: {display_name} ({model})") - if embedding_label: - typer.echo(f" Embedding Provider: {embedding_label}") + typer.echo(" LLM Provider: not required (extraction runs in the agent session)") typer.echo(f" Storage: {storage_label}") typer.echo(f" Hook: {hook_status}") typer.echo(f" Skill: {skill_path}") @@ -535,7 +545,8 @@ def openclaw( typer.echo(" 1. Start Reflexio: reflexio services start") typer.echo(" 2. Restart OpenClaw gateway: openclaw gateway restart") typer.echo( - " 3. Start a conversation -- Reflexio will capture and learn automatically" + " 3. Start a conversation -- the hook retrieves past-session " + "memory, and you can run /reflexio-extract to persist new learnings." ) @@ -862,9 +873,7 @@ def _remove_from_dir(base_dir: Path) -> None: typer.echo(f" Removed hook from: {settings_path}") -def _uninstall_claude_code( - project_dir: Path, *, global_install: bool = False -) -> None: +def _uninstall_claude_code(project_dir: Path, *, global_install: bool = False) -> None: """Remove the Reflexio integration from Claude Code. When ``--global`` or ``--project-dir`` is explicit, removes from that @@ -980,7 +989,9 @@ def claude_code_setup( target = ( Path.home() if global_install - else Path(project_dir) if project_dir is not None else Path.cwd() + else Path(project_dir) + if project_dir is not None + else Path.cwd() ) _uninstall_claude_code(target, global_install=global_install) return @@ -993,11 +1004,7 @@ def claude_code_setup( location = InstallLocation.CURRENT_PROJECT else: location = _prompt_install_location() - target = ( - Path.home() - if location == InstallLocation.ALL_PROJECTS - else Path.cwd() - ) + target = Path.home() if location == InstallLocation.ALL_PROJECTS else Path.cwd() # Step 1: Load .env path from reflexio.cli.env_loader import load_reflexio_env @@ -1056,7 +1063,9 @@ def claude_code_setup( typer.echo("Note: User-level hooks fire for ALL Claude Code sessions.") typer.echo("") if location == InstallLocation.ALL_PROJECTS: - typer.echo("Next: Start any Claude Code session — Reflexio is active in all projects.") + typer.echo( + "Next: Start any Claude Code session — Reflexio is active in all projects." + ) else: typer.echo("Next: Start a Claude Code session in this project.") if is_remote: diff --git a/reflexio/integrations/openclaw/README.md b/reflexio/integrations/openclaw/README.md index 2831710c..09a44ff6 100644 --- a/reflexio/integrations/openclaw/README.md +++ b/reflexio/integrations/openclaw/README.md @@ -1,16 +1,14 @@ # Reflexio OpenClaw Integration -Connect [OpenClaw](https://openclaw.ai) agents to [Reflexio](https://github.com/reflexio-ai/reflexio) for automatic self-improvement with multi-user support and agent playbooks. Conversations are captured automatically; task-specific playbooks are retrieved on-demand via the `reflexio` CLI; and corrections from multiple agent instances are aggregated into shared agent playbooks. +Connect [OpenClaw](https://openclaw.ai) agents to [Reflexio](https://github.com/reflexio-ai/reflexio) for cross-session memory. Past-session playbooks are retrieved and injected automatically before each response; new learnings are persisted when the agent runs `/reflexio-extract`, which applies the extraction rubric in its own context and writes playbooks to Reflexio via direct CRUD. **No LLM provider API key is required on the Reflexio server side** — extraction happens in the agent's own session, so OpenClaw's setup stays minimal. ## Table of Contents - [How It Works](#how-it-works) - [Multi-User Architecture](#multi-user-architecture) -- [Agent Playbooks](#agent-playbooks) - [Prerequisites](#prerequisites) - [Installation](#installation) - [Configuration](#configuration) -- [Scheduled Aggregation](#scheduled-aggregation) - [What to Expect](#what-to-expect) - [File Structure](#file-structure) - [Manual Testing](#manual-testing) @@ -20,98 +18,56 @@ Connect [OpenClaw](https://openclaw.ai) agents to [Reflexio](https://github.com/ ```mermaid flowchart TD - subgraph "1. Capture (Hook)" - C1["Each turn: buffer messages"] --> C2["Session end: publish to Reflexio"] - C2 --> C3["Server extracts playbooks & profiles"] + subgraph "1. Retrieve (Hook)" + R1["message:received hook"] --> R2["/api/search"] + R2 --> R3["Inject REFLEXIO_CONTEXT.md"] + R4["agent:bootstrap hook"] --> R5["Inject REFLEXIO_USER_PROFILE.md"] end - subgraph "2. Retrieve (Hook + Skill)" - R1["message:received hook"] --> R2["reflexio search"] - R2 --> R3["Inject matching playbooks & profiles"] - R4["Skill: on-demand search"] --> R2 + subgraph "2. Persist (/reflexio-extract)" + P1["Agent applies v3.0.0 rubric in-context"] --> P2["For each entry: reflexio user-playbooks search"] + P2 -->|match found| P3["reflexio user-playbooks update"] + P2 -->|no match| P4["reflexio user-playbooks add"] end - subgraph "3. Aggregate (Automatic + Manual)" - A1["Cluster similar user playbooks"] --> A2["Deduplicate & consolidate"] - A2 --> A3["Shared Agent Playbooks"] - A4["Cron / manual trigger"] --> A1 - end - - C3 --> R2 - C3 --> A1 - A3 --> R3 + P3 --> R2 + P4 --> R2 ``` +The integration has two independent mechanisms: - -The integration has three independent mechanisms: - -### 1. Capture (Hook — automatic, runs every session) +### 1. Retrieve (Hook — automatic, runs every session) ``` -Each Turn (message:sent) - └── Buffer (user message, agent response) → local SQLite - -Session End (command:stop) - └── reflexio interactions publish → flush buffered turns to Reflexio server - └── Server automatically: - 1. Detects learning signals (corrections, friction, re-steering) - 2. Extracts playbooks: freeform content summary + optional structured fields (trigger/instruction/pitfall/rationale) - 3. Extracts user profiles (preferences, expertise, communication style) - 4. Stores everything with vector embeddings for semantic search - -Mid-Session (skill guidance) - └── Skill guides agent to publish corrections and learnings at key steps - └── Captures high-signal moments without waiting for session end -``` - -Correction detection happens **server-side via LLM** — the agent does not need to detect corrections itself. - -### 2. Retrieve (Hook + Skill — automatic + on-demand) +Per-message (message:received hook) + └── POST /api/search with user's message + └── Returns matching user + agent playbooks + profiles + └── Injects REFLEXIO_CONTEXT.md as a bootstrap file +Session start (agent:bootstrap hook) + └── POST /api/search with a profile-shaped query + └── If profiles exist, injects REFLEXIO_USER_PROFILE.md ``` -Per-message (message:received hook — automatic) - └── Hook injects reflexio search results before every agent response - └── Returns user playbooks + agent playbooks relevant to the current message - -Per-task (skill — on-demand) - └── Agent runs reflexio search "" - └── Semantic search matches query against playbook trigger fields - └── Returns only playbooks relevant to THIS specific task - └── Each result has a freeform summary (primary) + optional structured fields (trigger/instruction/pitfall) - -Agent needs to personalize response - └── reflexio user-profiles search "" - └── Vector + FTS hybrid search on profile content - └── Returns relevant user preferences, expertise, communication style -``` - -Both the hook injection and the per-task skill search return **user playbooks** (corrections specific to this agent instance) and **agent playbooks** (shared corrections aggregated from all instances). -Playbook retrieval is **per-task, not per-session**. Different tasks return different playbooks. The query is matched against the `trigger` field of stored playbooks using semantic search. +The hook never buffers conversations and never POSTs to `/api/publish_interaction`. It is strictly a read path into the local Reflexio store. -### 3. Aggregate (Automatic + Manual) +### 2. Persist (`/reflexio-extract` — agent-driven) ``` -After each successful publish - └── Aggregation runs in background automatically - └── Clusters similar user playbooks across all agent instances - └── Deduplicates and consolidates into shared agent playbooks - └── New agent playbooks start as PENDING → reviewed → APPROVED/REJECTED - -Manual trigger - └── /reflexio-aggregate command (OpenClaw slash command) - └── reflexio agent-playbooks aggregate (CLI) - -Scheduled (recommended for teams) - └── Cron job or systemd timer runs aggregation periodically +User runs /reflexio-extract + └── Agent applies the v3.0.0 extraction rubric to its current conversation + └── Produces a list of playbook entries (Correction SOPs + Success Path Recipes) + └── For each entry: + ├── reflexio user-playbooks search "" --agent-version openclaw-agent + ├── If a close match exists → reflexio user-playbooks update --playbook-id --content "" + └── Otherwise → reflexio user-playbooks add --agent-version openclaw-agent ... ``` -Agent playbooks accumulate corrections from all agent instances, so every instance benefits from corrections made by any other instance. +Because the agent itself performs extraction (using its own LLM — i.e. whatever provider OpenClaw is running on top of), the Reflexio server never needs to call an external LLM for this integration. That's what makes the setup LLM-key-free. ## Multi-User Architecture -Each OpenClaw agent (identified by its `agentId`) is treated as a distinct Reflexio user. This enables per-agent learning isolation alongside cross-agent shared learning: +Each OpenClaw agent (identified by its `agentId`) is treated as a distinct Reflexio user. This enables per-agent learning isolation: ``` ~/.openclaw/ @@ -121,35 +77,20 @@ Each OpenClaw agent (identified by its `agentId`) is treated as a distinct Refle │ └── ops/ → Reflexio user_id: "ops" ``` -- **User playbooks**: per-agent corrections, isolated by `agentId`. Mistakes made by `main` are tracked separately from mistakes made by `work`. -- **Agent playbooks**: shared corrections aggregated from ALL agents. Once a correction is aggregated and approved, every instance sees it via `reflexio search`. +- **User playbooks**: per-agent corrections and recipes, isolated by `agentId`. Mistakes made by `main` are tracked separately from mistakes made by `work`. - **`user_id`** is derived from the OpenClaw session key prefix (`agent::...`). There is no override — the hook is deliberately locked to sessionKey-derived identity to eliminate env-var reads. -## Agent Playbooks - -Agent playbooks are the cross-instance knowledge layer. Individual corrections from each instance flow into a shared pool through aggregation: - -``` -Instance "main" corrections ─┐ -Instance "work" corrections ──┤── Aggregation ──→ Shared Agent Playbooks -Instance "ops" corrections ───┘ -``` - -- **Aggregation** clusters semantically similar user playbooks, deduplicates them, and consolidates them into a single agent playbook per distinct correction type. -- **Approval status**: new agent playbooks start as `PENDING`. They are surfaced in search immediately but can be reviewed and marked `APPROVED` or `REJECTED`. -- **All instances** receive agent playbooks alongside their own user playbooks via `reflexio search`. - -This means a correction made once by any instance eventually prevents the same mistake across all instances. +This integration does not aggregate user playbooks across instances. Cross-instance sharing requires a server-side LLM clustering pass, which was intentionally dropped to keep the integration free of LLM-provider dependencies. Teams that want shared agent playbooks across instances can use managed Reflexio or the Claude Code integration, which still run server-side aggregation. ## Prerequisites - [OpenClaw](https://openclaw.ai) installed and running - The `reflexio` CLI on PATH: `pipx install reflexio-ai` (or `pip install --user reflexio-ai`) -- The local Reflexio server running at `127.0.0.1:8081` (the hook starts it automatically if it's down) +- The local Reflexio server running at `127.0.0.1:8081` -**The Reflexio server requires an LLM API key** (e.g., `OPENAI_API_KEY`) in `~/.reflexio/.env` for playbook extraction — that key is read by the server process, never by this hook. Supported providers: OpenAI, Anthropic, Google Gemini, DeepSeek, OpenRouter, MiniMax, DashScope, xAI, Moonshot, ZAI. +**No LLM provider API key is required.** The Reflexio server only performs CRUD + semantic search for this integration; playbook extraction runs in the agent's own session when the user invokes `/reflexio-extract`. -> Run `reflexio setup openclaw` to automate LLM provider selection, storage configuration, and hook/skill/command installation. +> Run `reflexio setup openclaw` to automate storage configuration and hook/skill/command installation. ## Installation @@ -173,7 +114,7 @@ This installs the hook, copies the skill and commands to `~/.openclaw/skills/`, ### Option 3 — Manual (for developing against source) ```bash -# Hook: automatic capture + retrieval +# Hook: search-only retrieval openclaw hooks install /path/to/reflexio/integrations/openclaw/hook --link openclaw hooks enable reflexio-context openclaw gateway restart @@ -182,9 +123,8 @@ openclaw hooks list # expect: ✓ ready │ 🧠 reflexio-context # Skill: teaches agent when/how to use reflexio CLI cp -r /path/to/openclaw/skill ~/.openclaw/skills/reflexio -# Commands: publish corrections mid-session, trigger aggregation +# Command: extract and upsert playbooks from the current conversation cp -r /path/to/openclaw/commands/reflexio-extract ~/.openclaw/skills/reflexio-extract -cp -r /path/to/openclaw/commands/reflexio-aggregate ~/.openclaw/skills/reflexio-aggregate # Rule: always-active behavioral constraints — loaded every session cp /path/to/openclaw/rules/reflexio.md ~/.openclaw/workspace/reflexio.md @@ -204,87 +144,61 @@ If you need remote Reflexio (managed or self-hosted) from OpenClaw, use the Claude Code integration instead — it supports a full set of env vars for pointing at external servers. - -## Scheduled Aggregation - -For teams running many agent instances, schedule periodic aggregation so agent playbooks stay up to date: - -```bash -# Aggregate every 6 hours (crontab -e) -0 */6 * * * reflexio agent-playbooks aggregate --agent-version openclaw-agent 2>> ~/.reflexio/logs/aggregation.log -``` - -**systemd timer** (Linux): - -```ini -# ~/.config/systemd/user/reflexio-aggregate.service -[Service] -ExecStart=reflexio agent-playbooks aggregate --agent-version openclaw-agent - -# ~/.config/systemd/user/reflexio-aggregate.timer -[Timer] -OnCalendar=*-*-* 00/6:00:00 -Persistent=true -``` - -If OpenClaw supports scheduled tasks natively, you can also register aggregation as a recurring OpenClaw task to keep everything within the same scheduler. - ## What to Expect -**Session 1 (cold start):** No playbooks exist yet. The agent works normally. At session end, the hook captures the full conversation. Reflexio's server-side LLM pipeline analyzes it and extracts any corrections or user preferences. +**Session 1 (cold start):** No playbooks exist yet. The agent works normally. If the user corrects the agent or the agent completes substantive domain work, the agent runs `/reflexio-extract` to extract playbooks and write them to Reflexio. -**Session 2+:** Before each task, the agent runs `reflexio search ""` and gets task-specific corrections from past sessions — both user playbooks (corrections from this agent instance) and agent playbooks (corrections shared across all instances). Over time: +**Session 2+:** Before each task, the `message:received` hook runs a search and injects matching playbooks as `REFLEXIO_CONTEXT.md`. The agent can also run `reflexio search ""` manually. Over time: - Mistakes made once are not repeated (corrections match by trigger similarity) -- User preferences are remembered (profiles extracted automatically) +- User preferences are remembered (profiles injected at session bootstrap) - The agent adapts its approach per-task based on accumulated playbooks -- Corrections from one agent instance propagate to all instances via aggregation +- Successful recipes get replayed when a similar task comes up **The learning loop:** -1. Agent works on a task → user corrects a mistake -2. Session ends → hook captures full conversation → server extracts user playbooks -3. Next session, similar task → agent searches → gets the correction → applies the behavioral guideline +1. Agent works on a task → user corrects a mistake (or agent completes a substantive recipe) +2. Agent runs `/reflexio-extract` → applies the v3.0.0 rubric → searches for a close match → updates existing playbook or adds a new one +3. Next session, similar task → `message:received` hook injects the playbook → agent applies the rule 4. Mistake not repeated -5. Aggregation runs → correction promotes to agent playbook → all other instances benefit too ## File Structure ``` openclaw/ ├── README.md ← This file -├── hook/ ← Automatic: capture conversations + inject search results -│ ├── handler.js ← Event handlers: bootstrap, message:received, message:sent, command:stop +├── hook/ ← Search-only: inject past-session playbooks before each response +│ ├── handler.js ← Event handlers: agent:bootstrap, message:received │ ├── HOOK.md ← Hook metadata (events, requirements) │ └── package.json ← npm package manifest -├── skill/ ← On-demand: search for task-specific playbooks + publish -│ └── SKILL.md ← Teaches agent when/how to search, publish, and aggregate +├── skill/ ← On-demand: search for task-specific playbooks + extract flow +│ └── SKILL.md ← Teaches agent when/how to search and when to run /reflexio-extract ├── rules/ ← Always-active: behavioral constraints loaded every session │ └── reflexio.md ← Follow injected context, manual search fallback, transparency └── commands/ ← OpenClaw slash commands - ├── reflexio-extract/ ← /reflexio-extract: publish corrections mid-session - └── reflexio-aggregate/ ← /reflexio-aggregate: trigger aggregation manually + └── reflexio-extract/ ← /reflexio-extract: apply v3.0.0 rubric, search, upsert playbooks ``` ## Manual Testing -See [TESTING.md](TESTING.md) for a step-by-step guide to manually test the integration end-to-end — from install through search, capture, retrieval, multi-user isolation, graceful degradation, and uninstall. +See [TESTING.md](TESTING.md) for a step-by-step guide to manually test the integration end-to-end — from install through search, retrieval, extraction + upsert, multi-user isolation, graceful degradation, and uninstall. ## Comparison with Claude Code / LangChain Integrations -| Aspect | OpenClaw | Claude Code | LangChain | -| -------------------- | ---------------------------------------------- | ---------------------------------------------- | --------------------------------------- | -| Integration method | CLI commands + hooks | CLI commands + hooks | Python SDK + callbacks | -| Context retrieval | Per-message (hook) + per-task (skill) | Per-task via skill | Per-LLM-call via middleware (automatic) | -| Conversation capture | Hook buffers → SQLite → flushes at session end | Hook buffers → SQLite → flushes at session end | Callback captures per chain run | -| Multi-user support | Yes — per-agentId user isolation | Yes — per-agent user isolation | Single user per client instance | -| Agent playbooks | Yes — aggregated across all instances | Yes — aggregated across all instances | Not yet | -| Agent teaching | SKILL.md (natural language) | SKILL.md (natural language) | Tool definition (structured) | -| Dependencies | `reflexio` CLI only | `reflexio` CLI only | `langchain-core >= 0.3.0` | +| Aspect | OpenClaw | Claude Code | LangChain | +| -------------------- | --------------------------------------------------- | ---------------------------------------------- | --------------------------------------- | +| Integration method | CLI commands + hooks | CLI commands + hooks | Python SDK + callbacks | +| Context retrieval | Per-message (hook) + per-task (skill) | Per-task via skill | Per-LLM-call via middleware (automatic) | +| Conversation capture | None — `/reflexio-extract` performs in-context extraction | Hook buffers → SQLite → flushes at session end | Callback captures per chain run | +| Extraction | Agent-driven (v3.0.0 rubric in-context) + direct CRUD | Server-side LLM extraction | Server-side LLM extraction | +| Multi-user support | Yes — per-agentId user isolation | Yes — per-agent user isolation | Single user per client instance | +| Agent playbooks | Read-only (no aggregation in this integration) | Yes — aggregated across all instances | Not yet | +| Agent teaching | SKILL.md (natural language) | SKILL.md (natural language) | Tool definition (structured) | +| Server dependencies | `reflexio` CLI; **no LLM provider key required** | `reflexio` CLI + LLM provider key | `langchain-core >= 0.3.0` | -All integrations connect to the same Reflexio server and share the same playbook/profile data. Agent playbooks aggregated from OpenClaw instances are visible to Claude Code agents, and vice versa, as long as they use the same `--agent-version` tag. +All integrations connect to the same Reflexio server and share the same playbook/profile data. Playbooks written by OpenClaw (via `/reflexio-extract`) are visible to Claude Code and LangChain agents and vice versa, as long as they use the same `--agent-version` tag. Only integrations that run server-side LLM aggregation produce new agent playbooks, though — the OpenClaw integration is read-only for that layer. ## Further Reading diff --git a/reflexio/integrations/openclaw/TESTING.md b/reflexio/integrations/openclaw/TESTING.md index 563dcc2b..4147c0bb 100644 --- a/reflexio/integrations/openclaw/TESTING.md +++ b/reflexio/integrations/openclaw/TESTING.md @@ -49,12 +49,11 @@ ls ~/.openclaw/skills/reflexio/SKILL.md # Rule installed? ls ~/.openclaw/workspace/reflexio.md -# Commands installed? +# Command installed? ls ~/.openclaw/skills/reflexio-extract/SKILL.md -ls ~/.openclaw/skills/reflexio-aggregate/SKILL.md ``` -All five checks should succeed. If any fail, re-run `reflexio setup openclaw`. +All four checks should succeed. If any fail, re-run `reflexio setup openclaw`. ### 1.3 Verify Reflexio server (optional) @@ -161,43 +160,29 @@ Also add a health check that curls localhost:3000/health before starting the mai **What to check:** - The agent applies the correction (uses pnpm in the rewrite) -- In hook logs: look for `reflexio publish` — the skill should detect the correction and publish it -- If you don't see a publish, that's also OK — the session-end hook will capture the full conversation +- The agent should run `/reflexio-extract` at some point to persist the correction (see Phase 5 — extraction is explicit, not automatic) -### 3.2 End the session +### 3.2 Persist the correction + +Run the extract slash command so the correction is stored: -Exit the session: ``` -/stop +/reflexio-extract ``` -(or press Ctrl+C, depending on your OpenClaw configuration) **What to check in hook logs:** -- `[reflexio] Queued N interactions for publish` — the `command:stop` handler flushed buffered turns +- `[reflexio]` log lines during per-message search (hook injection) +- The agent runs `reflexio user-playbooks search` followed by `reflexio user-playbooks add` or `update` ### 3.3 Verify playbooks were extracted -Wait ~30 seconds for server-side LLM extraction, then check: - ```bash -reflexio user-playbooks list --limit 10 +reflexio user-playbooks list --agent-version openclaw-agent --limit 10 ``` **Expected:** At least one playbook containing "pnpm" (e.g., content like "use pnpm instead of npm"). -If no playbooks appear, the batch interval may not have been met (requires 5+ interactions). Use the manual extraction command instead: - -```bash -# If no playbooks were extracted automatically, this is expected for short sessions. -# Phase 5 covers manual extraction as a workaround. -``` - -Also check profiles: -```bash -reflexio user-profiles list --limit 10 -``` - -You may see a profile entry about project conventions (e.g., "uses pnpm"). +If no playbooks appear, the agent either skipped `/reflexio-extract` or extraction produced no entries — re-run `/reflexio-extract` in a session that has clear friction. --- @@ -261,31 +246,15 @@ Now run the extract command: ``` **What to check:** -- The agent reviews the conversation and builds a JSON summary -- It publishes via `reflexio publish --force-extraction` -- It reports what was published (e.g., "Published 2 interactions to Reflexio") -- Verify extraction worked: - ```bash - reflexio user-playbooks list --limit 10 - ``` - You should see a new playbook about email validation regex. - -### 5.2 Test `/reflexio-aggregate` - -After accumulating playbooks from Phases 3-5.1: - -``` -/reflexio-aggregate -``` - -**What to check:** -- The agent runs `reflexio agent-playbooks aggregate --wait` -- It reports how many agent playbooks were created or updated +- The agent reviews the conversation and applies the v3.0.0 extraction rubric in its own context +- For each extracted entry it runs `reflexio user-playbooks search --agent-version openclaw-agent` first +- On no match, it runs `reflexio user-playbooks add --agent-version openclaw-agent --content ... --trigger ... --instruction ...` +- On a match, it runs `reflexio user-playbooks update --playbook-id --content ""` - Verify: ```bash - reflexio agent-playbooks list --agent-version openclaw-agent + reflexio user-playbooks list --agent-version openclaw-agent --limit 10 ``` - You should see agent playbooks with `PENDING` status. + You should see a new (or refined) playbook about email validation regex. --- @@ -465,11 +434,10 @@ openclaw hooks list ls ~/.openclaw/skills/reflexio 2>/dev/null && echo "STILL EXISTS" || echo "Removed" ls ~/.openclaw/skills/reflexio-extract 2>/dev/null && echo "STILL EXISTS" || echo "Removed" -ls ~/.openclaw/skills/reflexio-aggregate 2>/dev/null && echo "STILL EXISTS" || echo "Removed" ls ~/.openclaw/workspace/reflexio.md 2>/dev/null && echo "STILL EXISTS" || echo "Removed" ``` -All four should print "Removed." +All three should print "Removed." ### 8.3 Verify agent works without Reflexio diff --git a/reflexio/integrations/openclaw/commands/reflexio-aggregate/SKILL.md b/reflexio/integrations/openclaw/commands/reflexio-aggregate/SKILL.md deleted file mode 100644 index d39d796d..00000000 --- a/reflexio/integrations/openclaw/commands/reflexio-aggregate/SKILL.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -name: reflexio-aggregate -description: "Aggregate user playbooks from all OpenClaw instances into shared agent playbooks. Use when you want to consolidate learnings across all agents." ---- - -# Aggregate Learnings into Shared Agent Playbooks - -Aggregate user playbooks collected across all OpenClaw instances into shared agent playbooks that every agent can search. - -## What Aggregation Does - -Each time a conversation is extracted (via `/reflexio-extract` or the automatic hook), the learnings are stored as **user playbooks** — behavioral rules tied to a specific user and agent instance. Aggregation goes one step further: it clusters similar user playbooks from all instances, deduplicates overlapping rules, and produces **agent playbooks** — shared behavioral guidelines available to every OpenClaw agent, regardless of which instance or user originally generated the learning. - -This means a correction one agent learned from one user can benefit every agent going forward. - -## How to Aggregate - -1. Ensure the local Reflexio server is running. This integration always talks to `http://127.0.0.1:8081`: - -```bash -reflexio status check -``` -If not running, tell the user you're starting it in the background, then: -```bash -nohup reflexio services start --only backend > ~/.reflexio/logs/server.log 2>&1 & -sleep 5 -``` - -2. Run aggregation and wait for results: - -```bash -reflexio agent-playbooks aggregate --agent-version openclaw-agent --wait -``` - -The `--wait` flag blocks until the aggregation job completes and prints a summary of how many playbooks were created or updated. Aggregation runs an LLM clustering pass over all user playbooks for the `openclaw-agent` version, so it may take 30–60 seconds depending on volume. - -3. Report results to the user: how many agent playbooks were created or updated, and what themes emerged (if the output includes them). - -4. Suggest reviewing the new agent playbooks: - -```bash -reflexio agent-playbooks list --agent-version openclaw-agent -``` - -This shows all shared behavioral rules now available to every OpenClaw agent on the next `reflexio search` call. - -## Setting Up Regular Aggregation - -For teams running OpenClaw continuously, set up a cron job to aggregate periodically so shared playbooks stay fresh: - -```bash -# Run aggregation daily at 3am -0 3 * * * reflexio agent-playbooks aggregate --agent-version openclaw-agent --wait >> ~/.reflexio/logs/aggregate.log 2>&1 -``` - -Alternatively, configure aggregation to run automatically after each publish by setting `auto_aggregate: true` in your Reflexio server config: - -```bash -reflexio config set --data '{"auto_aggregate": true}' -``` - -With `auto_aggregate` enabled, you only need to run this command manually when you want an immediate consolidation — for example, after a batch of high-friction sessions where you want the learnings available right away. diff --git a/reflexio/integrations/openclaw/commands/reflexio-extract/SKILL.md b/reflexio/integrations/openclaw/commands/reflexio-extract/SKILL.md index aea04050..3b0def96 100644 --- a/reflexio/integrations/openclaw/commands/reflexio-extract/SKILL.md +++ b/reflexio/integrations/openclaw/commands/reflexio-extract/SKILL.md @@ -1,97 +1,206 @@ --- name: reflexio-extract -description: "Summarize the current conversation — including reasoning, tool calls, corrections, and preferences — and publish to Reflexio for user profile and playbook extraction." +description: "Extract reusable playbooks from the current conversation — corrections, tool failures, and successful recipes — and upsert them into Reflexio via direct CRUD. No LLM call on the Reflexio server." --- # Extract Learnings to Reflexio -Summarize this conversation and publish to Reflexio so future sessions can learn from it. +You (the agent running this command) apply the extraction rubric below to +the full conversation in your current context, then write each resulting +playbook into Reflexio via the `reflexio user-playbooks` CLI. The Reflexio +server does NO LLM work for this integration — extraction happens in your +own session, which is why OpenClaw's Reflexio setup requires no LLM +provider API key on the server side. -## What to Do +**The rubric embedded below is fully self-contained. Do NOT attempt to open +any external file** — the canonical source at +`reflexio/server/prompt/prompt_bank/playbook_extraction_context/v3.0.0.prompt.md` +is a maintainer reference inside the `reflexio-ai` source tree and is +**not shipped** with the ClawHub skill bundle (the publish script stages +only `integrations/openclaw/`). Everything you need to produce valid +playbooks is inline in this file. -Review the full conversation above, including your chain-of-thought reasoning, tool calls, tool results, and all user messages. Create a summary that captures: - -1. **User corrections** — the user said "no, do X instead" or corrected your approach. Preserve their exact words. **Tool-call rejections count as corrections** — if the user rejected a tool use mid-response, record it as its own turn and note what the rejection was objecting to. -2. **User preferences** — how they like things done, their expertise, communication style, project conventions. -3. **Environment facts** — project setup, tools, constraints, team conventions, schema details, column types, undocumented quirks. -4. **Procedural signals** — moments where something went wrong and you learned from it. **These are the primary source of behavioral rules** — without them, Reflexio can only extract vague profile entries, not precise playbook rules: - - **Failed tool calls with their error messages verbatim** (invalid identifier, file too large, permission denied, timeouts, JSON parse errors, etc.) — the exact error string is load-bearing evidence - - **Self-corrections mid-response** — moments you wrote "actually…", "this isn't quite X but…", or "I should have…" (these are evidence the user would have valued catching the mistake earlier) - - **Anomalies and implausible results** — mean/median divergence, unexpected zeros, row-count mismatches, results that contradict documentation - - **Retries and the reason for each retry** — not just the final successful call -5. **Non-obvious learnings** — surprises, dead ends, workarounds, documentation gaps. - -Skip routine pleasantries, but do **NOT** skip failures, rejections, or anomalies — these are the highest-signal moments. If a section of the conversation contains only successful tool calls and no friction, it probably yields domain facts; if it contains friction, it probably yields behavioral rules. Both layers matter. - -## How to Publish - -1. Ensure the local Reflexio server is running. This integration always talks to `http://127.0.0.1:8081` — it does not support remote servers. +## Step 1 — Ensure the local Reflexio server is running ```bash reflexio status check ``` -If not running, tell the user you're starting it in the background, then: +If it fails with a connection error, tell the user you're starting the +local Reflexio server in the background, then run: + ```bash nohup reflexio services start --only backend > ~/.reflexio/logs/server.log 2>&1 & sleep 5 ``` -2. Use your own agent identity as `user_id` — the agent name or instance identifier configured for this OpenClaw agent. The Reflexio CLI will auto-derive it from OpenClaw's session key if you leave it out of the payload. +## Step 2 — Apply the extraction rubric to your conversation -3. Write the summary as a JSON file. **Pattern-match on the shape below**: a single assistant turn captures one coherent learning; failed attempts plus the eventual success live together as an ordered list in `tools_used` so the failure → recovery arc reads as one unit. Self-corrections belong **verbatim** inside the `content` field: +Review the full conversation in your context — every user message, your +own assistant turns, every tool call and tool result. Produce a JSON array +of playbook entries. -```bash -cat > /tmp/reflexio-extract.json << 'EXTRACT_EOF' +**Two extraction categories** — a single trajectory can yield both: + +### Category 1: Correction SOPs + +Extract a Correction SOP when ALL are true: +1. You (the agent) performed an action, assumption, or default behavior. +2. The user signaled it was incorrect, inefficient, or misaligned. +3. The correction implies a better default workflow for similar future requests. +4. The rule can be phrased as: *"When [user intent/problem], the agent should [policy]."* + +Valid correction signals: +- User correcting or rejecting your approach +- User redirecting you to a different mode or level of detail +- User clarifying expectations that contradict your behavior +- Tool-call rejection — user rejected a tool use mid-response (record in tools_used verbatim) +- Self-correction written out loud — you wrote "actually, this isn't quite right" mid-response +- Repeated tool failure with user intervention — you failed the same operation twice and the user redirected + +**Trigger quality — the "Skill Test":** a valid trigger describes the +**problem or situation**, NOT the user's explicitly-stated preference. +- BAD (tautological): `"User requests CLI tools"` — restates the ask +- BAD (topic-based): `"User talks about Python code"` — too broad +- BAD (interaction-based): `"User corrects the agent"` — too generic +- GOOD (intent-based): `"User requests help debugging a specific error trace"` +- GOOD (problem-based): `"User's initial high-level request is ambiguous"` +- GOOD (situation-based): `"User reports timeout failures on large data transfers (>10TB)"` + +**Tautology check:** if the trigger reduces to "user asks for X" and the +instruction is "do X", the entry is tautological — re-derive the real +trigger as the *problem or situation* you encountered. + +`instruction` for a Correction SOP is **< 20 words**. + +### Category 2: Success Path Recipes + +Extract a Success Path Recipe when ALL are true: +1. You completed a task successfully (produced deliverables, resolved the request). +2. The trajectory contains domain-specific work — computation, data + transformation, multi-step orchestration — not just conversation. +3. The solution path contains at least one of: + - **Domain formulas, constants, or parameter values** + - **Specific tool sequences that worked** + - **Input/output format specifics that mattered** + - **Concrete values or answers you computed** + - **Key decisions you made and why** + +A Success Path Recipe does NOT require a user correction. It captures +"what worked" from a successful trajectory. + +**Trigger for a recipe** describes the task type — domain + action: +- GOOD: `"Audit sample selection from a risk-metrics spreadsheet with multi-criteria filtering"` +- BAD (too generic): `"Spreadsheet analysis task"` +- BAD (copies task verbatim): `"Calculate sample size for audit testing..."` + +`instruction` for a recipe can be **up to 80 words** and MUST include +specific formulas, tool sequences, parameter values, or computed answers. +`content` must be an **actionable recipe** — a future agent reading it +should be able to short-circuit discovery by following it verbatim. + +### Blocking issues (optional) + +If you could not complete the task because a capability was missing, also +populate a `blocking_issue` field with one of: `missing_tool`, +`permission_denied`, `external_dependency`, `policy_restriction`. The +`instruction` must still be an executable workaround (inform the user, +suggest alternatives) — NOT the missing capability itself. + +### Output schema + +For each extracted playbook, produce an object with these fields: + +```json { - "user_id": "", - "agent_version": "openclaw-agent", - "source": "openclaw-expert", - "interactions": [ - {"role": "user", "content": "how many live streams had a giveaway in the last 6 months?"}, - { - "role": "assistant", - "content": "Tried to query live_streams with a channels join to filter by business type; discovered live_streams has no channel_id column. Fell back to SELECT * LIMIT 1 for schema introspection, then rewrote the query without the channels join. Got 588 streams. NOTE: mid-response I wrote 'this is not true CVR' — I had substituted engagement_rate for conversion rate without flagging the substitution upfront.", - "tools_used": [ - {"tool_name": "run_query", "tool_data": {"input": "SELECT ... JOIN channels ... — FAILED: invalid identifier 'L.CHANNEL_ID'"}}, - {"tool_name": "run_query", "tool_data": {"input": "SELECT * FROM live_streams LIMIT 1 — schema introspection"}}, - {"tool_name": "run_query", "tool_data": {"input": "Rewritten query without channels join — succeeded, returned 588"}} - ] - } - ] + "rationale": "1-2 sentences: for a Correction SOP, what implicit expectation was violated. For a Recipe, why this captures transferable value.", + "trigger": "Situation/condition for a Correction SOP OR task-type descriptor for a Recipe. Must NOT be a tautological restatement of the user's explicit preference.", + "instruction": "For a Correction SOP: < 20 words. For a Recipe: up to 80 words with specific values/tools/formulas.", + "pitfall": "Optional — the specific behavior or assumption to avoid.", + "content": "For a Correction SOP: concise standalone insight. For a Recipe: actionable recipe with concrete formulas, tool sequences, parameter values, column names, and computed answers from the trajectory.", + "playbook_name": "agent_corrections for Correction SOPs, success_recipes for Recipes" } -EXTRACT_EOF ``` -Notice the three things the example models: (1) the `content` field names the failure, the recovery, **and** the self-correction verbatim; (2) all three tool attempts — failed, introspection, success — are listed in order under the **same** `tools_used` so the arc is one learning unit; (3) the failed attempt's `input` ends with `— FAILED: ` so the error string survives intact. +**Evidence requirements — do NOT drop these in favor of pleasantries:** +- Preserve user corrections **verbatim** in the `content` field. +- Preserve tool failures and error messages **verbatim** — exact strings like `invalid identifier 'L.CHANNEL_ID'` are what make the rule extractable. +- Preserve self-corrections ("actually, this isn't quite right because...") **verbatim**. +- Retries belong together with their eventual success — describe the failure → recovery arc as one learning unit in `content`. + +**How many entries to return:** one per distinct Correction SOP, plus one +per distinct Success Path Recipe when the trajectory contains substantive +domain work. A successful trajectory with real domain work MUST yield at +least one recipe. Return zero entries only when the conversation is +trivially short (fewer than 4 non-trivial agent actions). + +Never split a single policy across multiple entries; never merge two +independent policies into one. -`tools_used` is **required** for any assistant turn containing a failed, rejected, or retried tool call. The error message and the offending input are what make behavioral rules extractable — include them verbatim. For purely successful tool calls that only yielded a domain fact already captured in the `content` field, `tools_used` is optional. +## Step 3 — For each extracted playbook, find-or-upsert -4. **Self-check before publishing.** Scan your JSON and ask: - 1. Does it contain at least one failed tool call, user rejection, or self-correction? If the original conversation had any friction and your summary has none, re-read the conversation for the failure moments you dropped. - 2. For each failed tool call, is the **actual error message present verbatim** (not paraphrased)? Error strings like `invalid identifier 'L.CHANNEL_ID'` are what let Reflexio extract behavioral rules — paraphrases lose the signal. - 3. Are retries grouped under the **same** assistant turn as an ordered list, so the failure → recovery arc reads as one learning? +For each entry you produced in Step 2, run this sequence: - If any answer is no, revise the JSON before running the publish command below. A re-extraction after publish is possible but costs a round trip. +### 3a. Search for a close match -5. Publish with forced extraction (replace `` with the resolved user ID from step 2): ```bash -reflexio publish --user-id --agent-version openclaw-agent --source openclaw-expert --skip-aggregation --force-extraction --file /tmp/reflexio-extract.json && rm -f /tmp/reflexio-extract.json +reflexio user-playbooks search "" --agent-version openclaw-agent --limit 3 ``` -The publish returns as soon as the server has accepted the payload — the actual extraction (LLM calls + storage writes) runs as a background task on the server. This avoids the gateway timeouts you'd hit if extraction took longer than the deployment's request budget. +Add `--json` if you want to parse the result programmatically. The search +returns up to three candidates ranked by semantic similarity. + +### 3b. Decide: update or add + +Read the returned candidates. Apply the same semantic reasoning you used +to extract the new entry: + +- **If a candidate's trigger + content clearly describe the same + situation and the same rule/recipe**, treat it as a hit. Pick its `id` + and update it: + ```bash + reflexio user-playbooks update \ + --playbook-id \ + --content "" + ``` + The merged `content` must **preserve the existing rule and add new + evidence or refinement**. Do NOT replace the existing content + wholesale — the point of updating rather than adding is to strengthen + an existing rule, not to overwrite it. + +- **If no candidate clearly describes the same situation**, add a new + entry: + ```bash + reflexio user-playbooks add \ + --agent-version openclaw-agent \ + --playbook-name \ + --content "" \ + --trigger "" \ + --instruction "" \ + --pitfall "" \ + --rationale "" + ``` + +When in doubt, prefer adding a new entry. Updating is for clear refinements +of an existing rule, not for fuzzy matches. + +## Step 4 — Report what you did + +Briefly tell the user: +- How many entries you extracted +- How many were added vs. updated +- One-sentence summary of the most important new rule or recipe + +The user can verify with: -6. Report what was published (brief summary to the user) and tell them the extraction is running in the background. They can verify the results a minute later with: ```bash -reflexio user-profiles list --user-id --limit 10 -reflexio user-playbooks list --user-id --limit 10 +reflexio user-playbooks list --agent-version openclaw-agent --limit 10 ``` ## Summary Guidelines -- **Preserve user corrections and tool rejections verbatim** — their exact words (or the rejection moment) are the highest-signal input. -- **Preserve failures, not just successes.** For every failed tool call, record the error message and the input that caused it. For every retry, record why you retried. A summary with zero failures in a conversation that had friction is an incomplete summary. -- **Preserve self-corrections.** If you wrote "this isn't quite X" or "I should have done Y" in the original conversation, that sentence belongs in the extraction verbatim — it is evidence of a rule the user values. -- **Organize as meaningful pairs** — each user/assistant pair should capture one coherent learning. Multiple failed attempts + the eventual success belong to the **same** assistant turn, listed in order in `tools_used`, so the failure → recovery arc is preserved as a single learning unit. -- **Include reasoning context** — why you took an approach, what you expected, what surprised you. -- **Be concise, but not at the cost of dropping failures.** Cut pleasantries and repeated narration, not error messages. +- **Preserve user corrections and tool rejections verbatim** — their exact words are the highest-signal input. +- **Preserve failures, not just successes.** For every failed tool call, record the error message and the input that caused it. A summary with zero failures in a conversation that had friction is an incomplete summary. +- **Preserve self-corrections verbatim** — they are evidence of a rule the user values. +- **Do the search before every add.** The update path is what keeps the playbook store from bloating with near-duplicates as the same pattern recurs across sessions. +- **Be concise, but not at the cost of dropping failures.** Cut pleasantries and repeated narration, not error messages or computed values. diff --git a/reflexio/integrations/openclaw/hook/HOOK.md b/reflexio/integrations/openclaw/hook/HOOK.md index 9b5c3d8c..d7766268 100644 --- a/reflexio/integrations/openclaw/hook/HOOK.md +++ b/reflexio/integrations/openclaw/hook/HOOK.md @@ -1,10 +1,10 @@ --- name: reflexio-context -description: "Inject user profile at session start, capture conversations at session end for automatic playbook and profile extraction" +description: "Inject relevant past-session playbooks and user profile before each agent response. Search-only — conversation buffering and server-side extraction are handled by the /reflexio-extract slash command instead." metadata: openclaw: emoji: "brain" - events: ["agent:bootstrap", "message:received", "message:sent", "command:stop"] + events: ["agent:bootstrap", "message:received"] requires: bins: ["reflexio"] env: [] @@ -12,20 +12,19 @@ metadata: # Reflexio Context Hook -Automatically connects your OpenClaw agent to [Reflexio](https://github.com/reflexio-ai/reflexio) for continuous self-improvement. +Automatically connects your OpenClaw agent to [Reflexio](https://github.com/reflexio-ai/reflexio) for cross-session memory retrieval. ## What It Does -The hook is pure Node.js + native `fetch()`. It does not spawn subprocesses -or invoke the `reflexio` CLI — all traffic is HTTP to the local Reflexio -backend at `http://127.0.0.1:8081`. +The hook is pure Node.js + native `fetch()`. It does not spawn subprocesses, +invoke the `reflexio` CLI, or write any data to disk. All traffic is HTTP to +the local Reflexio backend at `http://127.0.0.1:8081`. ### On `agent:bootstrap` (session start) POSTs to `/api/search` with the query `"communication style, expertise, and preferences"` to fetch a brief user profile summary. Injects user preferences, expertise, and communication style as a `REFLEXIO_USER_PROFILE.md` bootstrap -file. Does NOT load playbooks here — those are retrieved per-message or -per-task. +file. Does NOT load playbooks here — those are retrieved per-message. ### On `message:received` (before each response) POSTs to `/api/search` with the user's message and `top_k: 5`. If results are @@ -34,40 +33,39 @@ file with relevant playbooks and corrections. Skips trivial inputs (< 5 chars, or `yes/no/ok/sure/thanks`). Times out after 5 seconds — never blocks the response. -### On `message:sent` (each turn) -Buffers each (user message, agent response) pair into a local SQLite database -(`~/.reflexio/sessions.db`). Lightweight local write — no network calls. If -the buffer exceeds `BATCH_SIZE * 2` unpublished turns, triggers an incremental -publish (see below). - -### On `command:stop` (session end) -POSTs the complete buffered conversation to `/api/publish_interaction` as a -single JSON request. The local Reflexio server detects corrections via LLM -analysis, extracts playbooks (freeform content summary + optional structured -fields: trigger/instruction/pitfall/rationale) and user profiles. Blocks -briefly on the HTTP round-trip; if it fails, turns stay unpublished and are -retried on the next `agent:bootstrap`. +### What the hook does NOT do +It does not buffer turns, write to SQLite, or POST to +`/api/publish_interaction`. Extracting playbooks from conversations and +writing them back to Reflexio is the responsibility of the +`/reflexio-extract` slash command, which runs in the agent's own context. +That split is what lets this integration operate without any LLM provider +API key on the Reflexio server side. ## Prerequisites 1. **`reflexio` CLI on PATH** — `pipx install reflexio-ai` (or `pip install --user reflexio-ai`). Needed to start the backend server and run the slash commands. 2. **Local Reflexio server running at `http://127.0.0.1:8081`** — the hook does NOT start it; the skill's First-Use Setup does that once via `reflexio services start --only backend`. -3. **An LLM provider API key in `~/.reflexio/.env`** — **required for the system to work end-to-end, even though the hook itself never reads it.** The local Reflexio server uses this key to extract playbooks and profiles from captured conversations via LiteLLM. `reflexio setup openclaw` will prompt you interactively for the provider (OpenAI, Anthropic, Gemini, DeepSeek, OpenRouter, MiniMax, DashScope, xAI, Moonshot, ZAI, or any local LLM via a custom base URL) and write the key for you. **If you want fully offline operation, provide a local LLM endpoint (Ollama, LM Studio, vLLM) at this step instead of a hosted provider.** -The skill's registry metadata does not declare this LLM key under `requires.env` because that field describes variables the hook's own code path reads, and the hook is deliberately stateless (no env var access, no filesystem config reads — enforced in `handler.js`). The dependency lives one hop away, at the backend server, and is called out here in prose instead. +No LLM provider API key is required by this integration. All extraction +happens in the agent's own session when the user runs `/reflexio-extract`; +the Reflexio server only performs CRUD and semantic search against its +local storage. -## Privacy: what the hook guarantees, what it doesn't +## Privacy The hook itself communicates only with `http://127.0.0.1:8081`. It reads no environment variables, no configuration files, and has no code path that -reaches any other host. - -**The local Reflexio server, however, makes outbound LLM API calls** for -profile/playbook extraction. The destination is whatever you configured in -`~/.reflexio/.env` (OpenAI, Anthropic, Gemini, etc.). If that provider is -external, excerpts of your conversations will be sent to it. If you want a -fully offline setup, configure the server to use a local LLM (Ollama, -LM Studio, vLLM, etc.) before enabling the hook. +reaches any other host. It does not persist any conversation data — it only +reads from Reflexio's local store. + +The `/reflexio-extract` slash command, when invoked, sends playbook +`add`/`update`/`search` calls to the same local server. Those calls carry +the trigger/instruction/pitfall/content fields that the agent extracted +from the current conversation; the raw conversation transcript is never +sent to the server. + +If you want to audit what the server stores, see `~/.reflexio/` on your +machine. ## Security contract — hook side diff --git a/reflexio/integrations/openclaw/hook/handler.js b/reflexio/integrations/openclaw/hook/handler.js index 7acaf5eb..4f45ff19 100644 --- a/reflexio/integrations/openclaw/hook/handler.js +++ b/reflexio/integrations/openclaw/hook/handler.js @@ -1,36 +1,28 @@ // --------------------------------------------------------------------------- -// Security contract — localhost only, HTTP only. +// Security contract — localhost only, HTTP only, search-only. // -// This hook is a localhost-only integration. It buffers OpenClaw conversations -// to a local SQLite database and talks to the Reflexio backend HTTP server on -// the same machine via native fetch(). It does not spawn subprocesses, does -// not read configuration from the filesystem, and does not consult any -// environment variables. The destination is a hardcoded loopback URL. +// This hook is a localhost-only, read-only integration. It makes HTTP calls +// to the Reflexio backend on the same machine via native fetch() and +// injects the results as bootstrap files for the agent to read. It never +// writes conversation data anywhere — no SQLite, no filesystem buffer, no +// subprocess, no environment-variable reads, no outbound hosts other than +// the hardcoded loopback URL below. // -// Traffic: only HTTP requests to http://127.0.0.1:8081/api/* and -// http://127.0.0.1:8081/health. No other hosts are contacted. +// Traffic: only HTTP requests to http://127.0.0.1:8081/api/* . +// No other hosts are contacted. // -// Bootstrap: the Reflexio server must be running on port 8081 before the -// hook is useful. If it is not, every fetch() attempt fails gracefully with -// a logged error and the hook returns — the agent continues without -// cross-session memory that session. Starting the server is the user's -// responsibility; the skill's First-Use Setup runs `reflexio services start` -// once at install time. -// -// Writes are confined to ~/.reflexio/sessions.db (SQLite buffer). +// Publishing (extracting playbooks from conversations and writing them +// back to Reflexio) is NOT performed by the hook. That responsibility +// belongs to the `/reflexio-extract` slash command, which runs in the +// agent's own context and performs extraction + CRUD through the +// `reflexio user-playbooks` CLI. The Reflexio server therefore needs no +// LLM provider API key for this integration. // --------------------------------------------------------------------------- -const { randomUUID } = require("node:crypto"); -const { mkdirSync } = require("node:fs"); -const { homedir } = require("node:os"); -const { dirname, join } = require("node:path"); - -const Database = require("better-sqlite3"); - // Hardcoded loopback destination — all traffic goes here, nowhere else. const LOCAL_SERVER_URL = "http://127.0.0.1:8081"; -// Hardcoded agent label; stored alongside extracted playbooks so they are -// scoped to this integration build. +// Hardcoded agent label; used as the agent_version filter for searches so +// results stay scoped to this integration build. const AGENT_VERSION = "openclaw-agent"; // --------------------------------------------------------------------------- @@ -94,88 +86,6 @@ function formatSearchResults(data) { return lines.join("\n").trim(); } -// --------------------------------------------------------------------------- -// SQLite session store — persistent, crash-safe conversation buffer. -// DB lives in ~/.reflexio/sessions.db. -// --------------------------------------------------------------------------- - -const DB_PATH = join(homedir(), ".reflexio", "sessions.db"); -const MAX_CONTENT_LENGTH = 10_000; -const MAX_INTERACTIONS = 200; -const BATCH_SIZE = 10; // Publish every N complete exchanges mid-session -const MAX_RETRIES = 3; // Give up retrying after this many failures - -let _db = null; - -function getDb() { - if (_db) return _db; - mkdirSync(dirname(DB_PATH), { recursive: true, mode: 0o700 }); - _db = new Database(DB_PATH); - _db.pragma("journal_mode = WAL"); - // Use prepare().run() for DDL; better-sqlite3 accepts DDL statements - // through prepared statements the same as DML. - _db.prepare( - "CREATE TABLE IF NOT EXISTS turns (" + - "id INTEGER PRIMARY KEY AUTOINCREMENT, " + - "session_id TEXT NOT NULL, " + - "role TEXT NOT NULL, " + - "content TEXT NOT NULL, " + - "timestamp TEXT NOT NULL, " + - "published INTEGER DEFAULT 0, " + - "retry_count INTEGER DEFAULT 0" + - ")", - ).run(); - _db.prepare( - "CREATE INDEX IF NOT EXISTS idx_session_published ON turns(session_id, published)", - ).run(); - // Add retry_count column if missing (migration for existing DBs) - try { - _db.prepare("ALTER TABLE turns ADD COLUMN retry_count INTEGER DEFAULT 0").run(); - } catch { - // Column already exists -- ignore - } - // Clean up old published turns (keep 7 days) - _db.prepare( - "DELETE FROM turns WHERE published = 1 AND timestamp < datetime('now', '-7 days')", - ).run(); - // Graceful close on process exit is intentionally omitted: - // better-sqlite3 flushes WAL on its own and we avoid referencing the - // runtime's process object here. - return _db; -} - -// --------------------------------------------------------------------------- -// Smart truncation — preserves head + tail with a marker in between -// --------------------------------------------------------------------------- - -function smartTruncate(content, maxLength = MAX_CONTENT_LENGTH) { - if (!content || content.length <= maxLength) return content || ""; - const headLen = Math.floor(maxLength * 0.8); - const tailLen = Math.max(0, maxLength - headLen - 80); - const truncated = content.length - headLen - tailLen; - const marker = `\n\n[...truncated ${truncated} chars...]\n\n`; - if (tailLen === 0) return content.slice(0, headLen) + marker; - return content.slice(0, headLen) + marker + content.slice(-tailLen); -} - -// --------------------------------------------------------------------------- -// Session ID resolution -// --------------------------------------------------------------------------- - -let _fallbackSessionId = null; - -function getSessionId(event) { - const key = event.context?.sessionKey; - if (key) return key; - if (!_fallbackSessionId) { - _fallbackSessionId = `anon-${randomUUID()}`; - console.error( - `[reflexio] No sessionKey; using fallback: ${_fallbackSessionId}`, - ); - } - return _fallbackSessionId; -} - // --------------------------------------------------------------------------- // User ID resolution — multi-agent instance support // @@ -192,71 +102,12 @@ function resolveUserId(event) { return "openclaw"; } -// --------------------------------------------------------------------------- -// Shared publish logic — POSTs buffered turns to /api/publish_interaction -// --------------------------------------------------------------------------- - -async function publishSession(db, sessionId, userId, agentVersion) { - const turns = db - .prepare( - "SELECT id, role, content FROM turns WHERE session_id = ? AND published = 0 AND retry_count < ? ORDER BY id LIMIT ?", - ) - .all(sessionId, MAX_RETRIES, MAX_INTERACTIONS); - - if (turns.length === 0) return; - - // Mark selected turns as in-flight (published = 2) synchronously to prevent - // concurrent publishSession calls from picking up the same rows. - const maxId = turns[turns.length - 1].id; - db.prepare( - "UPDATE turns SET published = 2 WHERE session_id = ? AND published = 0 AND retry_count < ? AND id <= ?", - ).run(sessionId, MAX_RETRIES, maxId); - - const body = { - user_id: userId, - source: "openclaw", - agent_version: agentVersion, - session_id: sessionId, - interaction_data_list: turns.map((t) => ({ - role: t.role, - content: t.content, - })), - skip_aggregation: false, - force_extraction: false, - }; - - try { - await apiPost("/api/publish_interaction", body, 15_000); - db.prepare( - "UPDATE turns SET published = 1 WHERE session_id = ? AND published = 2", - ).run(sessionId); - console.error( - `[reflexio] Published ${turns.length} interactions (session ${sessionId})`, - ); - } catch (err) { - console.error( - `[reflexio] Publish failed: ${err?.message ?? err}, incrementing retry count`, - ); - try { - db.prepare( - "UPDATE turns SET published = 0, retry_count = retry_count + 1 WHERE session_id = ? AND published = 2", - ).run(sessionId); - } catch (e) { - console.error( - `[reflexio] Failed to update retry count: ${e.message}`, - ); - } - } -} - /** * Main hook dispatcher for Reflexio-OpenClaw integration. * * Events handled: - * agent:bootstrap - Inject user profile + retry unpublished sessions + * agent:bootstrap - Inject user profile at session start * message:received - Search Reflexio before agent responds - * message:sent - Buffer turn to SQLite + incremental publish - * command:stop - Flush remaining unpublished turns to Reflexio */ async function reflexioHook(event) { // Skip sub-agent sessions to avoid recursion (guards all event types) @@ -270,15 +121,11 @@ async function reflexioHook(event) { return handleBootstrap(event); case "message:received": return handleSearchBeforeResponse(event); - case "message:sent": - return handleMessageSent(event); - case "command:stop": - return handleSessionEnd(event); } } // --------------------------------------------------------------------------- -// Bootstrap: inject user profile + retry unpublished sessions +// Bootstrap: inject user profile // // Precondition: the Reflexio backend is already running on LOCAL_SERVER_URL. // The hook does not start it — that's the user's responsibility, handled @@ -293,9 +140,7 @@ async function handleBootstrap(event) { console.error(`[reflexio] bootstrap hook fired, workspace=${workspaceDir}`); const userId = resolveUserId(event); - const currentSessionId = getSessionId(event); - // --- Inject user profile via unified search --- try { const data = await apiPost( "/api/search", @@ -308,60 +153,37 @@ async function handleBootstrap(event) { ); const profiles = Array.isArray(data?.profiles) ? data.profiles : []; - if (profiles.length > 0) { - const profileLines = profiles - .map((p) => `- ${(p.profile_content ?? p.content ?? "").trim()}`) - .filter((line) => line.length > 2); - - if (profileLines.length > 0 && Array.isArray(event.context.bootstrapFiles)) { - const bootstrapContent = [ - "## About This User (from Reflexio)", - "", - ...profileLines, - "", - 'Use `reflexio search ""` before starting work to get task-specific behavioral corrections.', - ].join("\n"); - - event.context.bootstrapFiles.push({ - name: "REFLEXIO_USER_PROFILE.md", - path: "REFLEXIO_USER_PROFILE.md", - content: bootstrapContent, - source: "hook:reflexio-context", - }); - console.error( - `[reflexio] Injected user profile (${bootstrapContent.length} chars)`, - ); - } - } + if (profiles.length === 0) return; + + const profileLines = profiles + .map((p) => `- ${(p.profile_content ?? p.content ?? "").trim()}`) + .filter((line) => line.length > 2); + + if (profileLines.length === 0) return; + if (!Array.isArray(event.context.bootstrapFiles)) return; + + const bootstrapContent = [ + "## About This User (from Reflexio)", + "", + ...profileLines, + "", + 'Use `reflexio search ""` before starting work to get task-specific behavioral corrections.', + ].join("\n"); + + event.context.bootstrapFiles.push({ + name: "REFLEXIO_USER_PROFILE.md", + path: "REFLEXIO_USER_PROFILE.md", + content: bootstrapContent, + source: "hook:reflexio-context", + }); + console.error( + `[reflexio] Injected user profile (${bootstrapContent.length} chars)`, + ); } catch (err) { console.error( `[reflexio] Bootstrap profile fetch failed: ${err?.message ?? err}`, ); } - - // --- Retry unpublished turns from previous sessions --- - try { - const db = getDb(); - const oldSessions = db - .prepare( - "SELECT DISTINCT session_id FROM turns WHERE published = 0 AND retry_count < ? AND session_id != ? LIMIT 5", - ) - .all(MAX_RETRIES, currentSessionId); - - if (oldSessions.length > 0) { - console.error( - `[reflexio] Retrying ${oldSessions.length} unpublished session(s)`, - ); - for (const { session_id } of oldSessions) { - // Await sequentially so we don't hammer the server with - // concurrent publishes for stale sessions. - // eslint-disable-next-line no-await-in-loop - await publishSession(db, session_id, userId, AGENT_VERSION); - } - } - } catch (err) { - console.error(`[reflexio] Retry failed: ${err?.message ?? err}`); - } } // --------------------------------------------------------------------------- @@ -410,64 +232,6 @@ async function handleSearchBeforeResponse(event) { } } -// --------------------------------------------------------------------------- -// Message sent: buffer turn + incremental publish every BATCH_SIZE exchanges -// --------------------------------------------------------------------------- - -function handleMessageSent(event) { - const userMessage = event.context?.userMessage; - const agentResponse = event.context?.agentResponse; - const sessionId = getSessionId(event); - - if (!userMessage && !agentResponse) return; - - try { - const db = getDb(); - const now = new Date().toISOString(); - - const insertTurn = db.transaction((sid, user, agent, ts) => { - const stmt = db.prepare( - "INSERT INTO turns (session_id, role, content, timestamp) VALUES (?, ?, ?, ?)", - ); - if (user) stmt.run(sid, "user", smartTruncate(user), ts); - if (agent) stmt.run(sid, "assistant", smartTruncate(agent), ts); - }); - insertTurn(sessionId, userMessage, agentResponse, now); - - // Incremental publish: every BATCH_SIZE complete exchanges - const { count } = db - .prepare( - "SELECT COUNT(*) as count FROM turns WHERE session_id = ? AND published = 0", - ) - .get(sessionId); - - if (count >= BATCH_SIZE * 2) { - const userId = resolveUserId(event); - void publishSession(db, sessionId, userId, AGENT_VERSION).catch((e) => - console.error(`[reflexio] Incremental publish failed: ${e?.message ?? e}`), - ); - } - } catch (err) { - console.error(`[reflexio] Failed to buffer turn: ${err.message}`); - } -} - -// --------------------------------------------------------------------------- -// Session end: flush remaining unpublished turns -// --------------------------------------------------------------------------- - -async function handleSessionEnd(event) { - const sessionId = getSessionId(event); - const userId = resolveUserId(event); - - try { - const db = getDb(); - await publishSession(db, sessionId, userId, AGENT_VERSION); - } catch (err) { - console.error(`[reflexio] Session flush failed: ${err?.message ?? err}`); - } -} - // OpenClaw expects a CommonJS default export. module.exports = reflexioHook; module.exports.default = reflexioHook; diff --git a/reflexio/integrations/openclaw/hook/package-lock.json b/reflexio/integrations/openclaw/hook/package-lock.json index e4fa436a..505d59b1 100644 --- a/reflexio/integrations/openclaw/hook/package-lock.json +++ b/reflexio/integrations/openclaw/hook/package-lock.json @@ -1,465 +1,13 @@ { "name": "openclaw-hook-reflexio-context", - "version": "2.0.0", + "version": "3.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openclaw-hook-reflexio-context", - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "better-sqlite3": "^11.0.0" - } - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], + "version": "3.0.0", "license": "MIT" - }, - "node_modules/better-sqlite3": { - "version": "11.10.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", - "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "bindings": "^1.5.0", - "prebuild-install": "^7.1.1" - } - }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "license": "MIT", - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC" - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "license": "(MIT OR WTFPL)", - "engines": { - "node": ">=6" - } - }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "license": "MIT" - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "license": "MIT" - }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "license": "MIT" - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" - }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "license": "MIT" - }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "license": "MIT" - }, - "node_modules/node-abi": { - "version": "3.89.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", - "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tar-fs": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" } } } diff --git a/reflexio/integrations/openclaw/hook/package.json b/reflexio/integrations/openclaw/hook/package.json index c6929dec..a9250de8 100644 --- a/reflexio/integrations/openclaw/hook/package.json +++ b/reflexio/integrations/openclaw/hook/package.json @@ -1,15 +1,13 @@ { "name": "openclaw-hook-reflexio-context", - "version": "2.0.0", - "description": "OpenClaw hook for Reflexio: injects context at session start, captures full conversations at session end for automatic feedback and profile extraction", + "version": "3.0.0", + "description": "OpenClaw hook for Reflexio: injects user profile at session start and relevant past-session playbooks before each agent response. Search-only — no conversation buffering, no server-side LLM extraction.", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/reflexio-ai/reflexio" }, - "dependencies": { - "better-sqlite3": "^11.0.0" - }, + "dependencies": {}, "openclaw": { "hooks": ["."] }, diff --git a/reflexio/integrations/openclaw/publish_clawhub.sh b/reflexio/integrations/openclaw/publish_clawhub.sh index 71ddfb8c..1e960692 100755 --- a/reflexio/integrations/openclaw/publish_clawhub.sh +++ b/reflexio/integrations/openclaw/publish_clawhub.sh @@ -104,85 +104,62 @@ frontmatter = ( privacy = """ ## Privacy & Data Collection — Read This First -**This skill causes the agent to automatically capture conversations and -forward them to a local Reflexio server for LLM-based extraction.** Read this -before enabling — there are two distinct network hops and you need to -understand both. - -### Credential requirement (not declared in skill metadata) - -The skill's registry metadata declares no required environment variables and -the hook reads none. **But the end-to-end system does require an LLM provider -API key**, and you WILL be asked for one during First-Use Setup. - -- Step 2 of First-Use Setup below runs `reflexio setup openclaw`, which opens - an **interactive wizard** prompting you to choose an LLM provider (OpenAI, - Anthropic, Gemini, DeepSeek, OpenRouter, and several others) and paste an - API key. -- The key is stored in `~/.reflexio/.env` and read by the local Reflexio - server during extraction. **The hook itself never reads the key** (the hook - has no environment variable access and no filesystem config reads — both - enforced in `handler.js`). -- If you want fully offline operation, point the wizard at a local LLM - (Ollama at `http://127.0.0.1:11434`, LM Studio, vLLM, etc.) instead of a - hosted provider — the wizard accepts any LiteLLM-compatible base URL. - -**Why the metadata doesn't declare it:** ClawHub's `metadata.openclaw.requires.env` -describes environment variables the hook's own code path reads. The hook is -deliberately stateless at the credential level, so listing anything there -would be inaccurate. The dependency is at the *backend server* level, one -hop away. This disclosure is here instead of in the metadata because prose -is the right place to explain the distinction. - -### Network hops — two of them - -**Hop 1: the hook → the local Reflexio server (always localhost).** +**This skill retrieves cross-session memory from a local Reflexio server and +injects it as context before the agent responds. Writing new learnings is +explicit — it only happens when the agent runs `/reflexio-extract`, which +applies the extraction rubric in its own session and upserts playbooks via +direct CRUD.** Read this before enabling. + +### No LLM provider API key is required + +The Reflexio server does NOT perform LLM-based extraction for this +integration. Playbook extraction runs in your agent's own LLM session +(whatever provider OpenClaw itself uses). The server only performs CRUD and +semantic search against its local store, so `reflexio setup openclaw` does +not prompt for any LLM provider key. + +If a previous version of this integration asked you to store an API key in +`~/.reflexio/.env`, that key is no longer consulted by the openclaw code +path. You can leave it in place (other integrations may still use it) or +remove it. + +### Single network hop — localhost only + The hook is hard-pinned to `http://127.0.0.1:8081`. It communicates via native `fetch()` with no configuration knobs; the destination is a hardcoded constant in `handler.js`. It reads zero environment variables and zero configuration files. This hop cannot leave your machine. -**Hop 2: the local Reflexio server → an LLM provider (may leave your -machine).** The server uses an LLM provider (OpenAI, Anthropic, Gemini, -DeepSeek, etc.) to extract playbooks and profiles from captured conversations. -That provider is configured in `~/.reflexio/.env`. **If you configured an -external provider, excerpts of your conversations will be sent to that -provider** as part of extraction — trigger text, sample content, and enough -context for the extractor to produce a useful summary. The primary full -conversation text stays in your local SQLite database at `~/.reflexio/`, but -the extracted summaries and illustrative excerpts traverse whatever the LLM -provider's network path is. - -**If you want fully offline operation**, configure the local server to use a -local LLM (Ollama, LM Studio, vLLM, etc.) before enabling this skill. Do not -rely on the hook's localhost pinning as a privacy guarantee for the system as -a whole — that only bounds the hook, not the server behind it. - -**What is captured:** full user and assistant messages; every tool call, input, -and output (including failed tool calls and exact error strings); self-correction -text from the assistant's own output. None of this is scrubbed for PII, -credentials, file paths, or API outputs. If you work on sensitive tasks, -disable the hook before starting them, or tell the agent mid-task. +The `/reflexio-extract` slash command, when invoked, sends playbook CRUD +calls (search / add / update) to the same local server. Those calls carry the +trigger / instruction / pitfall / content fields that the agent extracted +from the current conversation — the raw transcript is not forwarded. + +### What gets written to the local store + +Only the fields the agent produces during `/reflexio-extract`: + +- `content` — concise natural-language summary of one learning +- `trigger` — when the rule applies +- `instruction` / `pitfall` / `rationale` — structured fields + +Raw conversation transcripts, tool outputs, and file paths do NOT end up in +the local store unless you quote them verbatim into one of the fields above. +If you work on sensitive tasks, omit those strings from the extraction, or +skip `/reflexio-extract` entirely for that session. **How to disable:** -- Per-session, stop automatic capture: `openclaw hooks disable reflexio-context` +- Per-session, stop context injection: `openclaw hooks disable reflexio-context` - Remove the integration entirely: `reflexio setup openclaw --uninstall` -- Search-only mode (keep retrieval, drop auto-publish): edit `hook/handler.js`, - remove the `command:stop` handler, and reinstall -- Sensitive task: tell the agent "don't log this" — the workspace rule instructs - it to stop all capture and search calls for the rest of the session +- Sensitive task: tell the agent "don't log this" — the workspace rule + instructs it to stop all search and extract calls for the rest of the session **Transparency contract:** the agent is instructed to tell you Reflexio is -active on the first turn of each session, disclose what is captured AND -mention that the local server may forward excerpts to an LLM provider, and -honor session-level opt-outs. This is enforced by -`~/.openclaw/workspace/reflexio.md`. Edit that file if your deployment needs -different rules (require explicit approval before every publish, offline-only, -etc). - -Full data-handling details live in the **Privacy & Data Collection** section -further down in this SKILL.md. +active on the first turn of each session, disclose that it retrieves +past-session memory from a local server, and honor session-level opt-outs. +This is enforced by `~/.openclaw/workspace/reflexio.md`. Edit that file if +your deployment needs different rules. --- @@ -194,10 +171,7 @@ first_use = """ Run these commands the first time this skill is invoked. They are idempotent — skip if `reflexio` is already on PATH, the local Reflexio server is responding at `http://127.0.0.1:8081/health`, and `openclaw hooks list` shows -`reflexio-context` as `ready`. **Only run them after reading the privacy -disclosure above, and be aware that step 2 is interactive — it will prompt -you to pick an LLM provider and paste an API key for the local server's -extraction pipeline.** +`reflexio-context` as `ready`. No LLM provider API key is required. ```bash # 1. Install the reflexio CLI (pipx preferred; pip as fallback). @@ -205,12 +179,9 @@ command -v reflexio >/dev/null 2>&1 \\ || pipx install reflexio-ai 2>/dev/null \\ || pip install --user reflexio-ai -# 2. INTERACTIVE: activates the hook, slash commands, and workspace rule in -# OpenClaw. Also prompts you to choose an LLM provider and paste an API key, -# which gets written to ~/.reflexio/.env. That key is what the local server -# uses to extract playbooks from captured conversations. Point at a local LLM -# (Ollama / LM Studio / vLLM) here if you want fully offline operation. -# Before running this, tell the user what will be asked and why. +# 2. Activates the hook, slash command, and workspace rule in OpenClaw. +# Prompts only for a storage backend (local SQLite is the default). Before +# running this, tell the user what will be asked and why. reflexio setup openclaw # 3. Start the local Reflexio backend in the background. The hook talks to @@ -224,11 +195,12 @@ curl -sf --max-time 2 http://127.0.0.1:8081/health >/dev/null 2>&1 \\ This installs: -- `reflexio-context` hook — HTTP-only capture + per-message playbook - injection (communicates only with `http://127.0.0.1:8081`) -- `/reflexio-extract` slash command — publish session learnings mid-session -- `/reflexio-aggregate` slash command — consolidate user playbooks into - shared agent playbooks +- `reflexio-context` hook — search-only, HTTP-only (communicates with + `http://127.0.0.1:8081` for bootstrap profile + per-message playbook + injection). It never buffers conversations. +- `/reflexio-extract` slash command — applies the v3.0.0 rubric in your own + agent session, searches for existing playbooks, and adds or updates via + the `reflexio user-playbooks` CLI. - `~/.openclaw/workspace/reflexio.md` — always-active behavioral rule (transparency + opt-out handling) diff --git a/reflexio/integrations/openclaw/rules/reflexio.md b/reflexio/integrations/openclaw/rules/reflexio.md index 917fc80c..ff9a9b6e 100644 --- a/reflexio/integrations/openclaw/rules/reflexio.md +++ b/reflexio/integrations/openclaw/rules/reflexio.md @@ -1,18 +1,16 @@ # Reflexio: Cross-Session Memory -This agent uses Reflexio to remember corrections and preferences across sessions. **Reflexio captures full conversations (user messages, assistant responses, tool calls and errors, self-corrections) and forwards them to a local Reflexio server at `127.0.0.1:8081` for LLM-based extraction.** The hook is hard-pinned to localhost — it cannot reach any other host. **However**, the local server itself uses an LLM provider (configured in `~/.reflexio/.env`) to extract playbooks and profiles, and if that provider is external (OpenAI, Anthropic, etc.) then excerpts of the conversation will be sent to it as part of extraction. See the Privacy & Data Collection section in the skill for the full two-hop disclosure. +This agent uses Reflexio to remember corrections and preferences across sessions. **The hook reads past-session memory from a local Reflexio server at `127.0.0.1:8081` and injects it as context.** It does not buffer conversations or upload anything automatically. When you want to persist a learning, run `/reflexio-extract` — that slash command applies the extraction rubric in your own agent session and upserts playbooks into Reflexio via direct CRUD. No LLM call is made by the Reflexio server, and no LLM provider API key is required for this integration. ## How It Works -A `message:received` hook automatically runs `reflexio search` on every message you receive. When relevant corrections or preferences exist, they are injected as context before your response via a file named `REFLEXIO_CONTEXT.md`. +A `message:received` hook runs `reflexio search` on every message you receive. When relevant past-session playbooks or profiles exist, they are injected as context before your response via a file named `REFLEXIO_CONTEXT.md`. A `agent:bootstrap` hook injects the user profile (if one exists) as `REFLEXIO_USER_PROFILE.md`. ## What You MUST Do -1. **When you see a block titled "REFLEXIO_CONTEXT"** — read it carefully and follow every instruction it contains. These are explicit corrections from the user's past sessions. Behavior Corrections tell you what to do differently. User Preferences tell you how the user works. Agent Playbooks are shared corrections from all instances. +1. **When you see a block titled "REFLEXIO_CONTEXT"** — read it carefully and follow every instruction it contains. These are rules extracted from the user's past sessions. Behavior Corrections tell you what to do differently. User Preferences tell you how the user works. Agent Playbooks (if any exist from other integrations) are shared corrections. -2. **Be transparent about Reflexio.** On the first turn of each session, briefly tell the user: (a) Reflexio cross-session memory is active, (b) it captures messages, tool calls, errors, and self-corrections, (c) it talks only to a local Reflexio server at `127.0.0.1:8081`, BUT (d) that server uses an LLM provider configured in `~/.reflexio/.env` (OpenAI, Anthropic, Gemini, local Ollama, etc.) to extract playbooks — so conversation excerpts may reach whichever provider the user picked during install. Keep it to two or three sentences. If the user asks to disable it, point them to `openclaw hooks disable reflexio-context` or `reflexio setup openclaw --uninstall`, and stop using Reflexio for the rest of the session. - - Also: if the user says they don't remember which provider they configured, suggest they run `cat ~/.reflexio/.env | grep -i key` to check. If the provider is a hosted API (OpenAI, Anthropic, etc.), recommend treating the current session as if the LLM provider will see excerpts. If it's a localhost endpoint (Ollama, LM Studio), the full pipeline is offline. +2. **Be transparent about Reflexio.** On the first turn of each session, briefly tell the user: Reflexio cross-session memory is active; it reads past-session playbooks from a local Reflexio server at `127.0.0.1:8081`; new learnings are only written when you run `/reflexio-extract`. Two sentences is plenty. If the user asks to disable it, point them to `openclaw hooks disable reflexio-context` or `reflexio setup openclaw --uninstall`, and stop using Reflexio for the rest of the session. 3. **If no Reflexio context appears for a task** — the hook may not have found relevant results, or the local server may be starting up. Run a manual search before beginning work: ``` @@ -23,6 +21,8 @@ A `message:received` hook automatically runs `reflexio search` on every message nohup reflexio services start --only backend > ~/.reflexio/logs/server.log 2>&1 & ``` -4. **If Reflexio is unavailable, proceed normally** — it enhances but never blocks your work. Never degrade the core agent experience because of Reflexio issues. +4. **Persist learnings at the right moments.** After the user corrects you, or after you complete a substantive step with a non-obvious recipe, run `/reflexio-extract`. The command handles the extraction rubric, search, and upsert for you. Do not wait for the end of the session — extract at natural milestones so learnings aren't lost to interruption. + +5. **If Reflexio is unavailable, proceed normally** — it enhances but never blocks your work. Never degrade the core agent experience because of Reflexio issues. -5. **Respect user objections.** If the user says they don't want their conversation logged, or that they're working on a sensitive task, stop all Reflexio calls for the remainder of the session. Do not launch the local server, do not run `reflexio publish`, and do not run `reflexio search`. Offer to disable the hook entirely if they'd like a permanent opt-out. +6. **Respect user objections.** If the user says they don't want their conversation logged, or that they're working on a sensitive task, stop all Reflexio calls for the remainder of the session. Do not launch the local server, do not run `/reflexio-extract`, and do not run `reflexio search`. Offer to disable the hook entirely if they'd like a permanent opt-out. diff --git a/reflexio/integrations/openclaw/skill/SKILL.md b/reflexio/integrations/openclaw/skill/SKILL.md index 743836d2..ad221407 100644 --- a/reflexio/integrations/openclaw/skill/SKILL.md +++ b/reflexio/integrations/openclaw/skill/SKILL.md @@ -1,6 +1,6 @@ --- name: reflexio -description: "Self-improving OpenClaw agents via Reflexio cross-session memory: the agent learns from every correction, tool failure, and stated preference so it stops repeating the same mistakes. Searches past playbooks before each task, captures new learnings after — and cross-instance aggregation turns one agent's correction into shared knowledge for every other agent. The hook is hard-pinned to a local Reflexio server at 127.0.0.1:8081 (no remote endpoints). That local server performs LLM-based extraction using whatever provider you configure in ~/.reflexio/.env, so extracted summaries may reach third-party APIs — see Privacy section before enabling." +description: "Self-improving OpenClaw agents via Reflexio cross-session memory: the agent learns from every correction, tool failure, and stated preference so it stops repeating the same mistakes. Searches past playbooks before each task, and — when invoked — extracts new learnings from the current conversation and upserts them directly into Reflexio via CRUD. The hook is hard-pinned to a local Reflexio server at 127.0.0.1:8081 (no remote endpoints). No LLM provider API key is required: extraction runs in your own agent session, not on the server." --- # Reflexio: Learn from Past Sessions @@ -9,61 +9,49 @@ Reflexio remembers corrections and preferences from past conversations with this **This skill triggers on TWO types of messages:** 1. **Task requests** — user asks you to do something → search Reflexio first -2. **Corrections, preferences, and completed steps** → publish to Reflexio +2. **Corrections, preferences, and completed steps** → run `/reflexio-extract` to persist the learning -The user can also run `/reflexio-extract` for comprehensive extraction of all session learnings, or `/reflexio-aggregate` to consolidate learnings across all agent instances. +The `/reflexio-extract` slash command performs extraction in your own context (using the v3.0.0 extraction rubric) and writes playbooks to Reflexio via direct CRUD. Nothing about this integration requires an LLM provider API key on the Reflexio server. --- ## Privacy & Data Collection -**Read this before enabling the skill.** Reflexio causes the agent to automatically capture conversations and forward them to a local Reflexio server for extraction and storage. Treat the following as material privacy information, not incidental detail. +**Read this before enabling the skill.** Reflexio reads from and writes to a local Reflexio server on your machine. Treat the following as material privacy information, not incidental detail. -### Credential requirement (not declared in skill metadata — important) +### Single network hop — localhost only -The skill itself declares no required environment variables and the hook reads none. **But the end-to-end system does require an LLM provider API key**, and you will be asked for one during First-Use Setup: +The hook is hard-pinned to `http://127.0.0.1:8081`. It communicates via native `fetch()` with no configuration knobs; the destination is a hardcoded constant in `handler.js`. It reads no environment variables and no dotfiles. This hop cannot leave your machine. -- `reflexio setup openclaw` runs an interactive wizard that prompts you to choose an LLM provider (OpenAI, Anthropic, Gemini, DeepSeek, OpenRouter, MiniMax, DashScope, xAI, Moonshot, ZAI, or any local provider via LiteLLM) and paste an API key. -- The key is written to `~/.reflexio/.env`. The local Reflexio server reads it when extracting playbooks and profiles from captured conversations. -- **The hook itself never reads this key or sees the contents of `~/.reflexio/.env`.** That's a property of the hook code, which has no filesystem config reads and no environment variable access at all. But the *server* the hook POSTs to does read the key, so from a "what credentials will I end up providing to make this work" perspective, treat the LLM provider key as a required dependency of the skill. -- **If you want fully offline operation, point the wizard at a local LLM** (Ollama at `http://127.0.0.1:11434`, LM Studio, vLLM, etc.) instead of a hosted provider. The wizard accepts any LiteLLM-compatible base URL. +The `/reflexio-extract` slash command, when invoked, sends playbook CRUD calls (search / add / update) to the same local server. Those calls carry the trigger / instruction / pitfall / content fields that you (the agent) extracted from the current conversation — not the raw conversation transcript. -This credential is not declared in the skill's registry metadata because metadata-level `requires.env` only applies to the hook's own runtime reads, and the hook is deliberately stateless. But you should know it's coming before you install. +**The Reflexio server does not make outbound LLM calls for this integration.** Playbook extraction runs in your own agent session, which is why you don't need to configure an LLM provider API key in `~/.reflexio/.env` to make this integration work. The server just does CRUD + semantic search against its local storage. -### Two distinct network hops — know the difference +### No automatic capture -1. **The hook → the local Reflexio server (always localhost).** - The hook is hard-pinned to `http://127.0.0.1:8081`. It communicates via native `fetch()` with no configuration knobs; the destination is a hardcoded constant in `handler.js`. It reads no environment variables and no dotfiles. This hop cannot leave your machine. +The hook does not buffer conversation turns, write to SQLite, or POST to `/api/publish_interaction`. It only reads from Reflexio to inject past-session context. Persisting new learnings is an **explicit** action — you (the agent) run `/reflexio-extract` when there's something worth saving. -2. **The local Reflexio server → an LLM provider (may leave your machine).** - The server uses an LLM provider (configured via `~/.reflexio/.env`) to extract playbooks and profiles from captured conversations. That provider is whichever one you set — OpenAI, Anthropic, Gemini, DeepSeek, etc. If you configured an external provider, **excerpts of your conversations will be sent to that provider** as part of extraction. The primary conversation text is stored locally in SQLite at `~/.reflexio/` and is not sent to the provider directly, but the extracted summaries, trigger texts, and sample content are. +This split is important for consent: the user sees `/reflexio-extract` happen in their terminal. There is no silent session-end upload. -If you want a fully offline setup, point `~/.reflexio/.env` at a local LLM (Ollama, LM Studio, vLLM, etc.) before enabling the hook. If you haven't audited that configuration, assume extraction forwards sensitive content to a third-party API. +### What gets written to the local Reflexio store -**Localhost-only is a property of the hook, not the full system.** Earlier drafts of this documentation framed the integration as fully local. That framing was incomplete — it described the hook's network boundary correctly but ignored that the local Reflexio server then makes its own outbound LLM calls during extraction. The accurate statement is: the *hook* has no off-host destination, but the *server behind it* forwards excerpts to whichever LLM provider you configured. +Only the fields you produce during `/reflexio-extract`: +- `content` — a concise natural-language summary of one learning +- `trigger` — when the rule applies +- `instruction` / `pitfall` / `rationale` — structured fields of the rule -If you want remote Reflexio (managed or self-hosted) from OpenClaw, this integration is not the one to use — it is deliberately crippled to localhost at the hook level. Use the Claude Code integration instead, which supports remote servers via `REFLEXIO_URL`. +Raw conversation transcripts, tool outputs, and file paths do NOT end up in the local store unless you quote them verbatim into one of the fields above. If you work on sensitive tasks, omit sensitive strings from the extraction, or skip running `/reflexio-extract` entirely for that session. -**What is captured (locally)** +### How to disable -- Full message content — every user turn and every assistant turn in the session -- Every tool call, its inputs, and its outputs — including **failed tool calls and the exact error strings** -- Self-corrections written out loud mid-response ("actually, this isn't quite right because…") — these are preserved verbatim because they're the most valuable learning signal, but they also surface internal reasoning -- User profile signals the local extraction pipeline infers from the conversation: expertise, working style, project conventions - -None of this is scrubbed for PII, credentials, file paths, stack traces, or API outputs. Anything that appears in the conversation ends up in the local database. If you work on sensitive tasks, disable the hook before starting them, or tell the agent mid-task to stop logging. - -**How to disable** - -- **Per-session opt-out:** `openclaw hooks disable reflexio-context` — stops automatic capture immediately. Skill-driven search and publish commands still work until the agent stops calling them. +- **Per-session opt-out:** `openclaw hooks disable reflexio-context` — stops context injection immediately. Manual `reflexio` CLI calls still work. - **Full uninstall:** `reflexio setup openclaw --uninstall` — removes the hook, slash commands, and workspace rule. -- **Search-only mode** (keep retrieval, stop auto-capture): edit `hook/handler.js` to remove the `command:stop` event handler, then reinstall. The `message:received` injection continues working without buffering turns to the database. -- **Wipe stored data:** delete `~/.reflexio/sessions.db` (buffered turns) and `~/.reflexio/` (full local store including extracted playbooks). -- **Sensitive-task-only opt-out:** tell the agent at the start of the task. The workspace rule instructs it to honor the objection — skip search, skip capture, skip local server start — for the rest of the session. +- **Wipe stored data:** delete `~/.reflexio/` (the local store, including all extracted playbooks and profiles). +- **Sensitive-task-only opt-out:** tell the agent at the start of the task. The workspace rule instructs it to honor the objection — skip search, skip extract, skip local server start — for the rest of the session. -**Transparency expectations** +### Transparency expectations -- On the first turn of a session, the agent should briefly tell you that Reflexio is active and that it captures conversations into a local SQLite database on your machine. One or two sentences. +- On the first turn of a session, the agent should briefly tell you that Reflexio is active — it retrieves past-session memory from a local server, and writes new learnings only when you run `/reflexio-extract`. One or two sentences. - If the agent needs to start the local Reflexio server in the background, it should announce that before launching the process. - If you see a `REFLEXIO_CONTEXT.md` block in the agent's context, that's injected past-session memory driving the response. You can ask the agent to ignore it. @@ -100,7 +88,7 @@ Use the user's actual request as the query — not keywords. Different tasks ret --- -## Step-by-Step: When to Publish +## Step-by-Step: When to Persist a Learning ### Scenario 1: User Corrects You @@ -108,38 +96,13 @@ When the user corrects your approach or states a preference: **Step 1 — Apply the correction** to your work first. -**Step 2 — Wait for enough context.** Don't publish immediately after the first correction message. Continue working until the correction is fully resolved and you have enough context to write a rich summary: +**Step 2 — Wait for enough context.** Don't run `/reflexio-extract` immediately after the first correction message. Continue working until the correction is fully resolved and you have the full arc: - The original request - Your initial approach (including any self-corrections you wrote out loud) - The user's correction (their exact words) - Your corrected approach and outcome -**Step 3 — Build a JSON summary and publish:** - -```bash -cat > /tmp/reflexio-summary.json << 'SUMMARY_EOF' -{ - "user_id": "", - "agent_version": "openclaw-agent", - "source": "openclaw", - "interactions": [ - {"role": "user", "content": ""}, - { - "role": "assistant", - "content": "", - "tools_used": [ - {"tool_name": "", "tool_data": {"input": " — FAILED: "}} - ] - }, - {"role": "user", "content": ""}, - {"role": "assistant", "content": ""} - ] -} -SUMMARY_EOF -reflexio publish --agent-version openclaw-agent --source openclaw --skip-aggregation --force-extraction --file /tmp/reflexio-summary.json && rm -f /tmp/reflexio-summary.json -``` - -`tools_used` is **required** whenever the original approach involved a failed or rejected tool call — the error string is the evidence Reflexio needs to extract a precise behavioral rule. For pure-text corrections, the field can be omitted. +**Step 3 — Run `/reflexio-extract`.** The command applies the v3.0.0 extraction rubric in your own context, produces one or more playbook entries, and for each one runs `reflexio user-playbooks search` first — if a similar entry already exists it is updated (merging new evidence into existing content); otherwise a new entry is added via `reflexio user-playbooks add`. Nothing about this requires a Reflexio-server LLM. **Detect correction patterns:** @@ -149,58 +112,54 @@ _Verbal corrections:_ - "I prefer X", "Always use X in this project" - "That's wrong, the correct approach is..." -_Non-verbal / implicit corrections (also publish these):_ -- **Tool-call rejection** — user rejected a tool use mid-response. Record it in `tools_used` with `— REJECTED BY USER` and write `[rejected tool use — see tools_used above]` in the following user turn's `content`. -- **Self-correction written out loud** — you realized mid-response you were doing the wrong thing and said so. Preserve the self-correction sentence verbatim in the assistant turn's `content`. -- **Repeated tool failure with user intervention** — you failed the same operation 2+ times and the user redirected. List every failed attempt under `tools_used` on the original assistant turn. +_Non-verbal / implicit corrections (also persist these):_ +- **Tool-call rejection** — user rejected a tool use mid-response. +- **Self-correction written out loud** — you realized mid-response you were doing the wrong thing and said so. Preserve the self-correction sentence verbatim when extracting. +- **Repeated tool failure with user intervention** — you failed the same operation 2+ times and the user redirected. -**Key principle:** Wait for sufficient context before publishing. A simple one-line correction ("always use type hints") can be published immediately. A multi-turn correction (user corrects, explains why, adds exceptions) should be published once the full chain is resolved. +**Key principle:** wait for sufficient context before extracting. A simple one-line correction ("always use type hints") can be extracted immediately. A multi-turn correction (user corrects, explains why, adds exceptions) should be extracted once the full chain is resolved. ### Scenario 2: After Completing a Key Step -After completing a meaningful milestone — a key step, sub-task, or the full task — reflect on what you learned and publish: +After completing a meaningful milestone — a key step, sub-task, or the full task — reflect on what you learned and run `/reflexio-extract`. Good signals to persist: - Non-obvious discoveries about this project or environment - Dead ends and tool quirks encountered +- Successful recipes worth replaying — specific formulas, tool sequences, parameter values, computed answers - User preferences revealed through the work -- Patterns that would help future sessions -Don't wait until the entire task is done — publish at natural milestones. Build the same JSON summary format as above and publish with the same command. +Don't wait until the entire task is done — extract at natural milestones. --- -## Multi-User and Agent Playbooks +## Multi-User Architecture Each OpenClaw agent instance is a unique Reflexio user, identified by its `agentId`. This means: -- **User playbooks** — corrections specific to this agent instance's interactions -- **Agent playbooks** — shared corrections aggregated from ALL instances of this agent - -`reflexio search` returns both user playbooks (instance-specific) and agent playbooks (shared across all instances) — so every agent instance benefits from the collective learning. +- **User playbooks** — corrections and recipes specific to this agent instance's history +- **`user_id`** is auto-derived from OpenClaw's session key (the `agent::...` prefix). You don't need to set it manually. -The `user_id` field in publish payloads is auto-derived from OpenClaw's session key (the `agentId` prefix). You don't need to set it manually. +`reflexio search` returns both user playbooks (instance-specific) and any agent playbooks (shared across instances) that exist in the store. This integration does not produce new agent playbooks — cross-instance aggregation is a server-side LLM operation that was intentionally dropped to keep the integration LLM-free. Teams that want cross-instance playbook sharing can use managed Reflexio or the Claude Code integration instead. --- ## What Reflexio Stores -**User Profiles** — stable facts learned from conversations: +**User Profiles** — stable facts about the user: - Expertise, background, role - Communication style and preferences - Technology stack and project conventions -**User Playbooks** — per-instance behavioral corrections: +Profiles are populated by whatever tooling produced them previously; this integration reads them via search but does not write new ones. + +**User Playbooks** — per-instance behavioral rules and recipes: - **trigger**: when does this rule apply? -- **instruction**: what to do instead +- **instruction**: what to do (< 20 words for Correction SOPs, up to 80 for Success Path Recipes) - **pitfall**: what to avoid - **rationale**: why the correction matters +- **content**: concise standalone insight (SOP) or actionable recipe (Recipe) with concrete values -**Agent Playbooks** — shared corrections aggregated from all instances: -- Same structure as user playbooks -- Produced by `reflexio agent-playbooks aggregate` -- Returned alongside user playbooks in every `reflexio search` - -The Reflexio server LLM analyzes your published summary and determines what gets extracted — you don't decide the structure. +Written by `/reflexio-extract` via direct CRUD. --- @@ -220,23 +179,23 @@ This integration always talks to the local Reflexio server at `http://127.0.0.1: | Command | Purpose | When | |---------|---------|------| -| `reflexio search ""` | Task-specific playbooks | Before every task | -| `reflexio user-profiles search ""` | User preferences | When personalizing | -| `reflexio publish --force-extraction --file ...` | Publish corrections/learnings | After corrections or key steps | -| `reflexio agent-playbooks aggregate` | Consolidate across instances | After corrections, or on schedule | -| `reflexio agent-playbooks list` | View shared playbooks | Debugging, review | +| `reflexio search ""` | Task-specific playbooks + profiles | Before every task | +| `reflexio user-playbooks search "" --agent-version openclaw-agent` | Find existing playbook before writing | Inside `/reflexio-extract` | +| `reflexio user-playbooks add --agent-version openclaw-agent ...` | Add a new playbook | Inside `/reflexio-extract` when no match | +| `reflexio user-playbooks update --playbook-id --content ...` | Merge new evidence into an existing playbook | Inside `/reflexio-extract` on a match | +| `reflexio user-playbooks list --agent-version openclaw-agent` | Review stored playbooks | Debugging, verification | | `reflexio status check` | Check server | First use, or if commands fail | -| `/reflexio-extract` | Comprehensive extraction | High-signal sessions | -| `/reflexio-aggregate` | Manual aggregation | Consolidate learnings | +| `/reflexio-extract` | Apply v3.0.0 rubric + upsert playbooks | After corrections or key steps | --- ## Tips - **Use the user's actual request as the search query** — not keywords -- **Preserve the user's exact words** in correction summaries -- **Include evidence** — tool failures, error messages, self-correction sentences. Without evidence, Reflexio extracts vague profile entries; with evidence, it extracts precise playbook rules +- **Preserve the user's exact words** in extracted content +- **Include evidence** — tool failures, error messages, self-correction sentences. Without evidence, extracted rules are vague; with evidence, they are precise +- **Search before you add.** `/reflexio-extract` does this for you, but if you're running `reflexio user-playbooks add` directly for some reason, still run `search` first to avoid duplicates - **If Reflexio is unreachable, proceed normally** — it enhances but never blocks -- **Tell the user Reflexio is active at session start** (see Privacy & Data Collection above). Cross-session logging is not something to leave implicit. -- **Honor sensitive-task objections** — if the user says "don't log this," stop all Reflexio calls (search, publish, server start) for the rest of the session -- **Suggest `/reflexio-extract`** if a session had many corrections or learnings +- **Tell the user Reflexio is active at session start** (see Privacy & Data Collection above). Cross-session memory is not something to leave implicit. +- **Honor sensitive-task objections** — if the user says "don't log this," stop all Reflexio calls (search, extract, server start) for the rest of the session +- **Suggest `/reflexio-extract`** if a session had many corrections or a notable successful recipe diff --git a/reflexio/server/services/storage/sqlite_storage/_playbook.py b/reflexio/server/services/storage/sqlite_storage/_playbook.py index 9089ed53..253b6116 100644 --- a/reflexio/server/services/storage/sqlite_storage/_playbook.py +++ b/reflexio/server/services/storage/sqlite_storage/_playbook.py @@ -1,6 +1,7 @@ """Playbook CRUD + search methods for SQLite storage.""" import json +import logging import sqlite3 from concurrent.futures import ThreadPoolExecutor from typing import Any @@ -33,6 +34,8 @@ _vector_rank_rows, ) +logger = logging.getLogger(__name__) + class PlaybookMixin: """Mixin providing user playbook, agent playbook, and evaluation CRUD + search.""" @@ -62,6 +65,14 @@ def save_user_playbooks(self, user_playbooks: list[UserPlaybook]) -> None: sd = up.structured_data embedding_text = sd.embedding_text or sd.trigger or up.content if embedding_text: + # Embeddings are best-effort. When no embedding provider is + # configured (e.g. the LLM-free OpenClaw setup), vector + # ranking is unavailable but FTS5 still works for retrieval. + # On failure, leave ``up.embedding`` as the empty list so the + # in-memory model stays valid, and rely on ``up.embedding or + # None`` at INSERT time to store SQL NULL — that lets a + # future re-embed migration target the row via + # ``WHERE embedding IS NULL``. if self._should_expand_documents(): with ThreadPoolExecutor(max_workers=2) as executor: emb_future = executor.submit( @@ -70,10 +81,26 @@ def save_user_playbooks(self, user_playbooks: list[UserPlaybook]) -> None: exp_future = executor.submit( self._expand_document, embedding_text ) - up.embedding = emb_future.result(timeout=15) + try: + up.embedding = emb_future.result(timeout=15) + except Exception as exc: # noqa: BLE001 + logger.warning( + "Embedding generation failed for user " + "playbook; saving without vector (FTS only): %s", + exc, + ) + up.embedding = [] up.expanded_terms = exp_future.result(timeout=15) else: - up.embedding = self._get_embedding(embedding_text) + try: + up.embedding = self._get_embedding(embedding_text) + except Exception as exc: # noqa: BLE001 + logger.warning( + "Embedding generation failed for user " + "playbook; saving without vector (FTS only): %s", + exc, + ) + up.embedding = [] created_at_iso = _epoch_to_iso(up.created_at) with self._lock: @@ -94,7 +121,7 @@ def save_user_playbooks(self, user_playbooks: list[UserPlaybook]) -> None: _json_dumps(up.source_interaction_ids or None), up.status.value if up.status else None, up.source, - _json_dumps(up.embedding), + _json_dumps(up.embedding or None), up.expanded_terms, ), ) diff --git a/tests/server/services/storage/test_sqlite_storage.py b/tests/server/services/storage/test_sqlite_storage.py index 30797ca8..ab8d54f8 100644 --- a/tests/server/services/storage/test_sqlite_storage.py +++ b/tests/server/services/storage/test_sqlite_storage.py @@ -241,6 +241,119 @@ def test_search_user_playbooks_with_sql_filters(storage): assert results[0].user_id == "user2" +def test_save_user_playbooks_tolerates_embedding_failure(): + """No embedding provider configured → save succeeds, row stored with + NULL embedding, vec table untouched, FTS still populated. + + This is the core of the LLM-free OpenClaw integration: the Reflexio + server must accept user playbook writes even when no embedding key + is available. Reads fall back to FTS-only ranking. + """ + + def _raise(*_args, **_kwargs): + raise RuntimeError("no embedding provider configured") + + with ( + tempfile.TemporaryDirectory() as temp_dir, + patch.object(SQLiteStorage, "_get_embedding", side_effect=_raise), + ): + storage = SQLiteStorage(org_id="0", db_path=f"{temp_dir}/reflexio.db") + + # The save call must not raise. + storage.save_user_playbooks( + [ + UserPlaybook( + user_id="openclaw-agent-main", + agent_version="openclaw-agent", + request_id="extract-1", + playbook_name="agent_corrections", + content="use pnpm instead of npm in this project", + structured_data=StructuredData( + trigger="user asks to install a JS dependency in this repo", + instruction="run pnpm add ", + ), + ) + ] + ) + + # Row is persisted. + rows = storage._fetchall( + "SELECT user_playbook_id, embedding FROM user_playbooks WHERE user_id = ?", + ("openclaw-agent-main",), + ) + assert len(rows) == 1 + # Embedding column is SQL NULL (not an empty JSON array) so a + # future re-embed migration can target these rows. + assert rows[0]["embedding"] is None + upid = rows[0]["user_playbook_id"] + + # FTS row is populated so retrieval still works via BM25. + fts_rows = storage._fetchall( + "SELECT search_text FROM user_playbooks_fts WHERE rowid = ?", + (upid,), + ) + assert len(fts_rows) == 1 + assert "pnpm" in fts_rows[0]["search_text"] + + # Vec table has no entry for this row — _vec_upsert must be + # skipped when the embedding is empty. + vec_rows = storage._fetchall( + "SELECT rowid FROM user_playbooks_vec WHERE rowid = ?", + (upid,), + ) + assert len(vec_rows) == 0 + + # FTS-only search returns the playbook. + results = storage.search_user_playbooks( + SearchUserPlaybookRequest( + query="pnpm", agent_version="openclaw-agent", top_k=5 + ) + ) + assert len(results) == 1 + assert results[0].user_playbook_id == upid + + +def test_save_user_playbooks_tolerates_embedding_failure_with_expansion(): + """Same as the no-expansion case, but exercises the ThreadPoolExecutor + branch that runs document expansion in parallel with embedding. + """ + + def _raise(*_args, **_kwargs): + raise RuntimeError("no embedding provider configured") + + with ( + tempfile.TemporaryDirectory() as temp_dir, + patch.object(SQLiteStorage, "_get_embedding", side_effect=_raise), + patch.object(SQLiteStorage, "_should_expand_documents", return_value=True), + # _expand_document has its own try/except returning None, so it + # won't raise — match that shape. + patch.object(SQLiteStorage, "_expand_document", return_value=None), + ): + storage = SQLiteStorage(org_id="0", db_path=f"{temp_dir}/reflexio.db") + + storage.save_user_playbooks( + [ + UserPlaybook( + user_id="openclaw-agent-main", + agent_version="openclaw-agent", + request_id="extract-2", + playbook_name="agent_corrections", + content="always use type hints in new Python code", + structured_data=StructuredData( + trigger="writing new Python functions", + ), + ) + ] + ) + + rows = storage._fetchall( + "SELECT user_playbook_id, embedding FROM user_playbooks WHERE user_id = ?", + ("openclaw-agent-main",), + ) + assert len(rows) == 1 + assert rows[0]["embedding"] is None + + def test_search_agent_playbooks_with_agent_version_filter(storage): """Verify agent_version filter works with FTS.""" storage.save_agent_playbooks(