Skill: ether-btc/correlation-memory (version 2.1.0)

--- name: correlation-memory description: Correlation-aware memory search plugin for OpenClaw — automatically retrieves related decision contexts when you query memory. Zero external dependencies. Install as an OpenClaw plugin. location: ~/.openclaw/extensions/correlation-memory --- # Correlation Memory Plugin **Correlation-aware memory search for OpenClaw** — automatically retrieves related contexts when you query memory. ## What It Does When you search memory for topic X, this plugin also fetches related contexts Y and Z that consistently matter together — based on correlation rules you define. Example: search for `"backup error"` → plugin also retrieves `"last backup time"`, `"recovery procedures"`, and `"similar errors"` — because those contexts are correlated by rule. ## Key Features - **Decision-context retrieval** — surfacing related information before you ask for it - **Word-boundary keyword matching** — no false positives from partial matches - **Confidence scoring** — filter/sort by how reliable a correlation is - **Multiple matching modes** — auto, strict, lenient - **Result limiting** — `max_results` parameter prevents output bloat - **mtime cache** — rules reloaded only when `correlation-rules.json` changes - **LRU regex cache** — bounded at 500 entries, no memory leaks - **Debug tool** — understand why a given context triggered specific correlations - **Lifecycle states** — rules can be promoted/archived/disabled without deletion ## Security - Zero external **runtime** dependencies (index.ts only imports `openclaw/plugin-sdk`) - Read-only local file operations (no network, no writes) - No credential or environment variable access - Workspace path resolved via OpenClaw SDK only - `npm install` fetches only `openclaw` (peerDep) + `vitest` (devDep, not bundled) — no postinstall scripts ## Correlation Rules Rules live in `memory/correlation-rules.json` in your workspace. Example: ```json { "id": "cr-config-001", "trigger_context": "config-change", "trigger_keywords": ["config", "setting", "openclaw.json", "modify"], "must_also_fetch": ["backup-location", "rollback-instructions"], "relationship_type": "constrains", "confidence": 0.95, "lifecycle": { "state": "promoted" } } ``` Active states: `promoted`, `active`, `testing`, `validated`, `proposal` ## Tools Provided | Tool | Description | |------|-------------| | `memory_search_with_correlation` | Enhanced memory search — automatically fetches correlated contexts from correlation rules | | `correlation_check` | Debug tool — shows which rules matched and why without performing searches | ## Installation ```bash cd ~/.openclaw/extensions git clone https://github.com/ether-btc/openclaw-correlation-plugin.git correlation-memory cd correlation-memory && npm install # Add to openclaw.json openclaw plugins install correlation-memory openclaw gateway restart ``` Requires OpenClaw >= 2026.1.26. **Note:** `npm install` pulls the OpenClaw peer dependency and vitest (dev/test only). The runtime plugin makes no network calls and has zero external dependencies. ## See Also - `README.md` — Full documentation with examples, configuration, architecture - `correlation-rules.example.json` — Production-quality rule examples - `docs/schema.md` — Full rule schema reference - `.sanitization-audit.md` — Security audit results - `tests/correlation.test.ts` — Unit tests

path: CHANGELOG.md
content: # Changelog

All notable changes to the OpenClaw Correlation Plugin will be documented in this file.

## [2.1.0] - 2026-04-15

### Added

- **Correlation Stack Consolidation** — Phases 0-3 complete:
  - P0: 12 rules mapped, 51/51 contexts resolved
  - P1: `correlation-surfacing.sh` — lightweight session scanning for correlation surfacing
  - P2: HEARTBEAT.md integration (correlation surfacing every 5 heartbeats)
  - P3: 17 stub memory files created for missing contexts

### Changed

- Rule engine optimizations: batch logger, Unicode word-boundary (German umlauts + emojis), action truncation fix
- Confidence scoring: keyword count + historical accuracy weighted scoring
- Incremental scanner: mtime-based skip for unchanged sessions
- Recommendation engine: unused rules + overlapping rules detection

### Fixed

- Rule overwrite bug (dict collision → list-based accumulation)
- Cursor crash-safety (save per-session, not end-of-run)
- Action truncation (iterate all actions, not just first)

### Security

- Webhook HMAC-SHA256 + exponential backoff retry
- XSS sanitization in alerting config
- Secrets via environment variables (not hardcoded)

## [2.0.1] - 2026-03-20

### Security

- Removed legacy reference files causing false positive security alerts
- Added comprehensive security documentation to plugin code (JSDoc comments)
- Passed deep security audit (March 20, 2026)

### Changed

- Removed `src/` directory (contained only legacy reference files)
- Added "Security" section to README.md
- Removed workspace dependency from package.json for better public distribution

### Fixed

- False positive security alerts from legacy reference files that were not actually loaded by OpenClaw

### Documentation

- Added security JSDoc comments to `index.ts`
- Added "Security" section to README.md documenting audit findings
- Created this CHANGELOG.md for version tracking

## [2.0.0] - 2026-03-18

### Added

- Initial release of correlation-aware memory search plugin
- Automatic context retrieval based on configurable rules
- Debug tools for rule matching
- Comprehensive documentation and deployment guides

---

*Format based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)*

path: CONTRIBUTING.md
content: # Contributing to OpenClaw Correlation Plugin

Thank you for your interest in contributing to the OpenClaw Correlation Plugin! This document provides guidelines and procedures for contributing to the project.

## Code of Conduct

By participating in this project, you agree to abide by our Code of Conduct:
- Be respectful and inclusive
- Focus on constructive feedback
- Welcome newcomers and diverse perspectives
- Separate people from problems

## How to Contribute

### Reporting Issues

If you encounter bugs or have feature requests:

1. Check existing issues to avoid duplicates
2. Create a new issue with:
   - Clear, descriptive title
   - Detailed steps to reproduce (for bugs)
   - Expected vs. actual behavior
   - Environment information (OpenClaw version, OS, etc.)
   - Screenshots or logs if relevant

### Suggesting Enhancements

For feature requests or improvements:

1. Describe the problem you're trying to solve
2. Explain your proposed solution
3. Consider alternative approaches
4. Note any potential drawbacks or limitations

### Code Contributions

#### Getting Started

1. Fork the repository
2. Clone your fork
3. Create a new branch for your feature/fix
4. Make your changes
5. Test thoroughly
6. Submit a pull request

#### Development Setup

```bash
# Clone your fork
git clone https://github.com/YOUR_USERNAME/openclaw-correlation-plugin.git
cd openclaw-correlation-plugin

# Install dependencies
npm install

# Run tests
npm test
```

#### Pull Request Guidelines

1. **Branch Naming**: Use descriptive names like `feature/add-rule-validation` or `bugfix/correlation-timeout`

2. **Commit Messages**: Follow conventional commits format:
   ```
   feat: Add validation for correlation rules
   fix: Resolve timeout issue in recursive correlations
   docs: Update deployment documentation
   ```

3. **Code Quality**:
   - Follow existing code style
   - Include tests for new functionality
   - Update documentation as needed
   - Keep changes focused and atomic

4. **Pull Request Description**:
   - Reference related issues
   - Describe the changes
   - Explain testing approach
   - Note any breaking changes

### Contributing Correlation Rules

The most valuable contributions are often new correlation rules. To contribute rules:

1. Test rules extensively in your environment
2. Document the rationale behind each rule
3. Include example scenarios where the rule is helpful
4. Specify appropriate confidence levels
5. Consider potential false positives

Example rule contribution format:

```json
{
  "id": "cr-YOUR_PREFIX-001",
  "trigger_context": "your-domain",
  "trigger_keywords": ["keyword1", "keyword2"],
  "must_also_fetch": ["related-concept1", "related-concept2"],
  "relationship_type": "supports",
  "confidence": 0.85,
  "description": "Brief explanation of why these concepts are correlated",
  "examples": [
    "Scenario where this correlation is helpful"
  ]
}
```

## Development Guidelines

### Code Style

- Follow TypeScript best practices
- Use meaningful variable and function names
- Include JSDoc comments for public APIs
- Keep functions focused and testable
- Handle errors gracefully

### Testing

- Write unit tests for new functionality
- Include integration tests for major features
- Test edge cases and error conditions
- Maintain test coverage above 80%

### Documentation

- Update README.md for user-facing changes
- Document new APIs and configuration options
- Include examples for complex features
- Keep documentation accurate and up-to-date

## Review Process

All contributions go through a review process:

1. Automated checks (tests, linting)
2. Maintainer review
3. Address feedback
4. Approval and merge

Reviews focus on:
- Code correctness and quality
- Performance implications
- Security considerations
- User experience impact
- Documentation completeness

## Community

### Communication Channels

- GitHub Issues for bug reports and feature requests
- GitHub Discussions for general discussion and Q&A
- OpenClaw Discord for real-time chat (link in main README)

### Recognition

Contributors are recognized in:
- Release notes
- Contributor list in README
- Project documentation

## Resources

- [OpenClaw Documentation](https://openclaw.dev/docs)
- [Plugin Development Guide](https://openclaw.dev/docs/plugins)
- [Correlation Plugin Documentation](./README.md)
- [Research Background](./docs/research.md)

## Questions?

If you have questions about contributing, feel free to:
1. Open an issue with your question
2. Reach out to maintainers directly
3. Join the OpenClaw community channels

Thank you for helping improve the OpenClaw Correlation Plugin!

path: README.md
content: # OpenClaw Correlation Memory Plugin

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![OpenClaw Plugin](https://img.shields.io/badge/OpenClaw-Plugin-blue)](https://openclaw.dev)

**Correlation-aware memory search for OpenClaw** — automatically retrieves related contexts when you query memory, so decisions are made with full information.

## Table of Contents

- [Overview](#overview)
- [Features](#features)
- [Installation](#installation)
- [Usage](#usage)
- [Configuration](#configuration)
- [Correlation Rules](#correlation-rules)
- [Tools Provided](#tools-provided)
- [Documentation](#documentation)
- [Contributing](#contributing)
- [License](#license)
- [Credits](#credits)

## Overview

The OpenClaw Correlation Plugin enhances memory search capabilities by automatically retrieving related contexts when querying memory. Traditional memory search returns results directly related to the query terms, but often misses contextual information that is crucial for making informed decisions.

When you search for "backup error," normal memory search returns backup-related results. This plugin checks correlation rules and *also* fetches "last backup time," "recovery procedures," and "similar errors" — because those contexts consistently matter together.

Think of it as **decision-context retrieval**: "for X decisions, always also consider Y."

## Features

- **Word-Boundary Matching**: Keyword matching with regex word boundaries prevents false positives
- **Confidence Filtering**: Filter and sort results by confidence threshold
- **Multiple Matching Modes**: Auto, strict, and lenient matching options
- **Result Limiting**: `max_results` parameter prevents output bloat
- **mtime Caching**: Rules cached in memory, refreshed only when file changes
- **Multi-word Keywords**: Support for phrase keywords like "config change"
- **Extensible Architecture**: Easy to add new correlation types
- **Debug Tools**: Understand why correlations are made
- **Error Logging**: Failed rule loads are logged, not silently swallowed

## Security

This plugin has been audited for security vulnerabilities:

- ✅ **Zero external dependencies** - No supply chain risk
- ✅ **No network requests** - Read-only local file operations
- ✅ **No credential access** - Does not handle secrets or tokens
- ✅ **No environment variable harvesting** - Uses SDK config only
- ✅ **Read-only filesystem operations** - Cannot write to disk

**Security Audit:** March 20, 2026 - Passed deep security review.

See: [OpenClaw Security Framework](https://docs.openclaw.ai/security)

## Installation

### Prerequisites

- OpenClaw >= 2026.1.26
- Node.js >= 18.x
- Git

### Steps

1. Clone into OpenClaw extensions:
   ```bash
   cd ~/.openclaw/extensions
   git clone https://github.com/ether-btc/openclaw-correlation-plugin.git correlation-memory
   ```

2. Install dependencies:
   ```bash
   cd ~/.openclaw/extensions/correlation-memory
   npm install
   ```

3. Add to plugins.allow in your `openclaw.json`:
   ```json
   {
     "plugins": {
       "allow": ["correlation-memory"]
     }
   }
   ```

4. Restart OpenClaw gateway:
   ```bash
   openclaw gateway restart
   ```

## Usage

### Basic Usage

Once installed, the correlation plugin automatically enhances memory searches. No additional steps are required for basic functionality.

### Manual Correlation Check

Use the debug tool to see which rules match a given context:

```bash
openclaw exec correlation_check --context "config change"
```

### Adjusting Sensitivity

Control correlation sensitivity through confidence thresholds in your rules.

## Configuration

The plugin requires a correlation rules file at `memory/correlation-rules.json` in your workspace.

### Example Configuration

```json
{
  "rules": [
    {
      "id": "cr-001",
      "created": "2026-03-25T00:00:00Z",
      "trigger_context": "config-change",
      "trigger_keywords": ["config", "setting", "openclaw.json", "modify", "jq", "change"],
      "must_also_fetch": ["backup-location", "rollback-instructions", "recent-changes"],
      "relationship_type": "constrains",
      "confidence": 0.95,
      "lifecycle": {
        "state": "validated"
      },
      "learned_from": "config-misconfiguration-leads-to-service-outage",
      "usage_count": 11,
      "notes": "Any config change should trigger a backup check and rollback plan."
    },
    {
      "id": "cr-002",
      "created": "2026-03-25T00:00:00Z",
      "trigger_context": "error-debugging",
      "trigger_keywords": ["error", "fail", "400", "bug", "broken", "crash"],
      "must_also_fetch": ["recovery-procedures", "recent-changes", "similar-errors"],
      "relationship_type": "diagnosed_by",
      "confidence": 0.9,
      "lifecycle": {
        "state": "promoted"
      },
      "learned_from": "tool-call-errors-recur-without-context",
      "usage_count": 14,
      "notes": "Error debugging without recent changes context is guesswork."
    }
  ]
}
```

> **Tip:** See [`correlation-rules.example.json`](./correlation-rules.example.json) in the repo root for a full set of production-quality rules with detailed documentation.

## Correlation Rules

### Rule Structure

Each correlation rule consists of:

- **id**: Unique identifier for the rule
- **created**: ISO-8601 creation timestamp
- **trigger_context**: Domain context where rule applies
- **trigger_keywords**: Keywords that activate the rule
- **must_also_fetch**: Related contexts to retrieve
- **relationship_type**: Type of relationship (constrains, supports, etc.)
- **confidence**: Confidence level (0.0 to 1.0)
- **lifecycle**: State object with `state` field — see Rule Lifecycle below
- **learned_from**: Descriptive name of the incident or pattern that prompted this rule
- **usage_count**: How many times the rule has fired (for diagnostics)
- **notes** (optional): Human-readable explanation of the rule's purpose

### Rule Lifecycle

Rules follow a lifecycle for safe deployment:
`proposal` → `testing` → `validated` → `promoted` → `retired`

The plugin accepts these states as active: `promoted`, `active`, `testing`, `validated`, `proposal`. Rules without a lifecycle state are also active. Use `retired` to disable rules without deleting them.

### Matching Modes

Three matching modes provide flexibility:
- `auto` (default) — keyword + context matching (word-boundary aware)
- `strict` — word-boundary keyword match only
- `lenient` — fuzzy matching for broad queries

### Confidence Filtering

Both tools accept a `min_confidence` parameter (0-1, default: 0) to filter low-confidence matches. Results are sorted by confidence descending.

### Result Limiting

Both tools accept a `max_results` parameter (default: 10) to cap output size.

## Tools Provided

### `memory_search_with_correlation`

Search memory with automatic correlation enrichment. This tool is automatically used when performing memory searches.

### `correlation_check`

Debug tool — check which rules match a given context without performing searches.

Parameters:
- `--context`: Context to check for matching rules
- `--keywords`: Additional keywords to consider
- `--mode`: Matching mode (auto, strict, lenient)

## Uninstallation

### Via OpenClaw CLI (recommended)

```bash
openclaw plugins uninstall correlation-memory
openclaw gateway restart
```

### Via Uninstall Script

If the CLI is not available, use the bundled uninstall script:

```bash
cd ~/.openclaw/extensions/correlation-memory
./uninstall.sh
```

Options:
- `--force`: Skip confirmation prompt
- Environment variable `OPENCLAW_CONFIG_PATH`: Override config location (default: `~/.openclaw/openclaw.json`)

The uninstall script will:
1. Find your OpenClaw config automatically
2. Back up the config before modification
3. Remove the `correlation-memory` entry from `plugins.entries`
4. Verify removal succeeded
5. Show backup location for recovery

**Note:** Restart the gateway after uninstall: `openclaw gateway restart`

## Documentation

Comprehensive documentation is available in the docs directory:

- [Research Background](./docs/research.md) - Theoretical foundation and related work
- [Deployment Guide](./docs/deployment.md) - Installation, configuration, and troubleshooting
- [Lessons Learned](./docs/lessons.md) - Development insights and best practices
- [Production Guide](./docs/production-guide.md) - **New:** Live deployment experience, heartbeat integration, confidence tuning, and operational best practices
- [Contributing Guide](./CONTRIBUTING.md) - How to contribute to the project

## Contributing

Contributions are welcome! Please see our [Contributing Guide](./CONTRIBUTING.md) for details on:

- Reporting issues
- Suggesting enhancements
- Code contributions
- Contributing correlation rules
- Development guidelines

### Quick Start for Contributors

1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Add tests if applicable
5. Update documentation
6. Submit a pull request

## License

This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details.

## Credits

### Original Development

Built by [Charon](https://github.com/ether-btc) — an OpenClaw agent running on a Raspberry Pi 5.

### Research Inspiration

- [Coolmanns Memory Architecture](https://github.com/coolmanns/openclaw-memory-architecture) — context-aware memory systems
- Cognitive science research on contextual decision making
- Information retrieval literature on query expansion

### Contributors

Thanks to all who have contributed to this project. See [CONTRIBUTING.md](./CONTRIBUTING.md) for details on how to contribute.

### Related Projects

- [OpenClaw](https://github.com/ether-btc/openclaw) - The AI agent platform
- Previous experimental implementations:
  - `correlation-rules-mem` - Plugin lifecycle focus
  - `correlation-memory` - Rich matching logic focus

---

*For more information about OpenClaw, visit [openclaw.dev](https://openclaw.dev)*

path: _meta.json
content: ownerId: kn76ztn1544kh229qvkmcz167180jfng
slug: correlation-memory
version: 2.1.0
publishedAt: 1775697738942

path: correlation-rules.example.json
content: {
  "$schema": "./docs/schema.md",
  "version": "1.0.0",
  "description": "Production-quality example correlation rules for OpenClaw correlation-memory plugin. These rules demonstrate the extended schema with lifecycle states, learned_from attribution, and realistic confidence scores. Adapt for your own deployment.",
  "rules": [
    {
      "id": "cr-config-001",
      "created": "2026-03-25T00:00:00Z",
      "trigger_context": "config-change",
      "trigger_keywords": [
        "config",
        "setting",
        "openclaw.json",
        "modify",
        "jq",
        "change"
      ],
      "must_also_fetch": [
        "backup-location",
        "rollback-instructions",
        "recent-changes"
      ],
      "relationship_type": "constrains",
      "confidence": 0.95,
      "lifecycle": {
        "state": "promoted"
      },
      "learned_from": "config-misconfiguration-leads-to-service-outage",
      "usage_count": 11,
      "notes": "Any config change should trigger a backup check and rollback plan. Confidence is high because config errors are costly and predictable."
    },
    {
      "id": "cr-error-001",
      "created": "2026-03-25T00:00:00Z",
      "trigger_context": "backup-operation",
      "trigger_keywords": [
        "backup",
        "git push",
        "commit",
        "workspace"
      ],
      "must_also_fetch": [
        "last-backup-time",
        "backup-status",
        "recovery-procedures"
      ],
      "relationship_type": "related_to",
      "confidence": 0.9,
      "lifecycle": {
        "state": "promoted"
      },
      "learned_from": "backup-verification-failed-silently",
      "usage_count": 6,
      "notes": "Backups are only reliable if you verify them. Always fetch last-backup-time to know if a backup actually ran."
    },
    {
      "id": "cr-session-001",
      "created": "2026-03-25T00:00:00Z",
      "trigger_context": "error-debugging",
      "trigger_keywords": [
        "error",
        "fail",
        "400",
        "bug",
        "broken",
        "crash"
      ],
      "must_also_fetch": [
        "recovery-procedures",
        "recent-changes",
        "similar-errors"
      ],
      "relationship_type": "diagnosed_by",
      "confidence": 0.9,
      "lifecycle": {
        "state": "proposal"
      },
      "learned_from": "tool-call-errors-recur-without-context",
      "usage_count": 14,
      "notes": "Error debugging without recent changes context is guesswork. Promoted because this fires often and always helps."
    },
    {
      "id": "cr-backup-001",
      "created": "2026-03-25T00:00:00Z",
      "trigger_context": "session-recovery",
      "trigger_keywords": [
        "checkpoint",
        "recover",
        "handoff",
        "crash",
        "restart"
      ],
      "must_also_fetch": [
        "last-checkpoint",
        "current-work",
        "blockers",
        "next-actions"
      ],
      "relationship_type": "continues",
      "confidence": 0.95,
      "lifecycle": {
        "state": "promoted"
      },
      "learned_from": "session-handoff-without-context-loses-work",
      "usage_count": 9,
      "notes": "Session recovery is only useful if you know what was in progress. Always fetch current-work and next-actions."
    },
    {
      "id": "cr-gateway-001",
      "created": "2026-03-25T00:00:00Z",
      "trigger_context": "gateway-operations",
      "trigger_keywords": [
        "gateway",
        "restart",
        "deploy",
        "service"
      ],
      "must_also_fetch": [
        "backup-procedure",
        "health-check",
        "rollback-instructions"
      ],
      "relationship_type": "constrains",
      "confidence": 0.99,
      "lifecycle": {
        "state": "promoted"
      },
      "learned_from": "gateway-restart-without-health-check-causes-downtime",
      "usage_count": 5,
      "notes": "Gateway operations are high-stakes. Confidence near 1.0 because getting this wrong means total service loss."
    },
    {
      "id": "cr-plugin-001",
      "created": "2026-03-25T00:00:00Z",
      "trigger_context": "plugin-management",
      "trigger_keywords": [
        "plugin",
        "install",
        "enable",
        "add-plugin"
      ],
      "must_also_fetch": [
        "validation-checklist",
        "installation-steps",
        "rollback-instructions",
        "plugin-validation-results"
      ],
      "relationship_type": "enables",
      "confidence": 0.99,
      "lifecycle": {
        "state": "promoted"
      },
      "learned_from": "plugin-installed-without-validation-broke-gateway",
      "usage_count": 3,
      "notes": "Plugin changes can crash the gateway. Always validate and have rollback ready."
    },
    {
      "id": "cr-git-001",
      "created": "2026-03-25T00:00:00Z",
      "trigger_context": "git-operations",
      "trigger_keywords": [
        "git checkout",
        "git switch",
        "git stash",
        "branch switch"
      ],
      "must_also_fetch": [
        "workspace-backup-status",
        "uncommitted-files-check",
        "git-stash-procedure"
      ],
      "relationship_type": "requires",
      "confidence": 0.95,
      "lifecycle": {
        "state": "promoted"
      },
      "learned_from": "workspace-file-loss-during-branch-switch",
      "usage_count": 4,
      "notes": "Switching branches with uncommitted work risks data loss. Always check for uncommitted files first."
    },
    {
      "id": "cr-security-001",
      "created": "2026-03-25T00:00:00Z",
      "trigger_context": "security-audit",
      "trigger_keywords": [
        "security",
        "audit",
        "vulnerability",
        "harden",
        "expose",
        "credential"
      ],
      "must_also_fetch": [
        "security-practices",
        "recent-security-changes",
        "access-log"
      ],
      "relationship_type": "informs",
      "confidence": 0.95,
      "lifecycle": {
        "state": "validated"
      },
      "learned_from": "security-hardening-requires-full-context",
      "usage_count": 3,
      "notes": "Security decisions must consider recent changes and access patterns. Always fetch related security context."
    }
  ]
}


path: docs/deployment.md
content: # Correlation Plugin Deployment Guide

## Prerequisites

Before deploying the correlation plugin, ensure you have:

1. OpenClaw >= 2026.1.26 installed
2. Access to the OpenClaw workspace directory
3. Git installed and configured
4. Proper permissions to modify the OpenClaw configuration

## Deployment Steps

### 1. Clone the Repository

```bash
cd ~/.openclaw/extensions
git clone https://github.com/ether-btc/openclaw-correlation-plugin.git correlation-memory
```

### 2. Install Dependencies

```bash
cd ~/.openclaw/extensions/correlation-memory
npm install
```

### 3. Configure Plugin

Add the plugin to your `openclaw.json` configuration file:

```json
{
  "plugins": {
    "allow": ["correlation-memory"]
  }
}
```

### 4. Create Correlation Rules

Create `memory/correlation-rules.json` in your workspace with your correlation rules:

```json
{
  "rules": [
    {
      "id": "cr-001",
      "trigger_context": "config-change",
      "trigger_keywords": ["config", "setting", "change"],
      "must_also_fetch": ["backup-location", "rollback-instructions"],
      "relationship_type": "constrains",
      "confidence": 0.95
    }
  ]
}
```

### 5. Restart OpenClaw Gateway

```bash
openclaw gateway restart
```

## Failsafe Checklist

### Pre-Deployment Validation

- [ ] Backup current OpenClaw configuration
- [ ] Verify OpenClaw version compatibility
- [ ] Test plugin installation in isolated environment
- [ ] Validate correlation rules syntax
- [ ] Document rollback procedure

### Deployment Verification

- [ ] Confirm plugin loads without errors
- [ ] Test basic correlation functionality
- [ ] Verify no performance degradation
- [ ] Check gateway logs for warnings
- [ ] Validate rule matching behavior

### Post-Deployment Monitoring

- [ ] Monitor memory usage
- [ ] Check for unexpected correlations
- [ ] Review user feedback
- [ ] Validate production rule effectiveness
- [ ] Schedule follow-up evaluation

## Rollback Procedure

If issues arise after deployment:

### 1. Immediate Action

```bash
# Stop gateway
openclaw gateway stop

# Remove plugin from configuration
# Edit openclaw.json to remove "correlation-memory" from plugins.allow
```

### 2. Restore Configuration

```bash
# Restore from backup if available
cp ~/.openclaw/openclaw.json.backup ~/.openclaw/openclaw.json
```

### 3. Restart Services

```bash
# Restart gateway
openclaw gateway start
```

### 4. Verify Stability

- [ ] Confirm gateway is running
- [ ] Check logs for errors
- [ ] Validate normal functionality

## Error Classification

### Critical Errors (Immediate Rollback)

- Gateway fails to start
- Memory corruption detected
- Performance degradation >50%
- Security violations

### Major Errors (Investigate Within 24 Hours)

- Incorrect correlation results
- Performance degradation 20-50%
- Configuration conflicts
- User experience issues

### Minor Errors (Monitor and Address)

- Minor performance impact (<20%)
- Non-critical logging issues
- Cosmetic display problems
- Documentation inconsistencies

## Testing Procedures

### Unit Tests

Run the built-in test suite:

```bash
cd ~/.openclaw/extensions/correlation-memory
npm test
```

### Integration Tests

1. Test rule matching with sample queries
2. Verify correlation results accuracy
3. Check performance impact on memory searches
4. Validate configuration edge cases

### Production Validation

1. Deploy to staging environment first
2. Monitor for 24 hours with representative workload
3. Collect user feedback
4. Address any issues before production rollout

## Troubleshooting

### Common Issues

1. **Plugin not loading**
   - Check OpenClaw version compatibility
   - Verify plugin directory structure
   - Confirm entry in plugins.allow

2. **No correlations returned**
   - Validate correlation rules syntax
   - Check rule confidence thresholds
   - Verify matching mode settings

3. **Performance issues**
   - Review correlation rule complexity
   - Check for circular dependencies
   - Monitor memory usage patterns

### Diagnostic Commands

```bash
# Check gateway status
openclaw gateway status

# View recent logs
openclaw logs --lines 100

# Validate configuration
openclaw doctor

# Test correlation manually
openclaw exec correlation_check --context "your-test-context"
```

## Monitoring

### Key Metrics to Watch

- Memory search response time
- Correlation rule match rate
- False positive correlation rate
- User interaction with correlated results
- Gateway stability metrics

### Alerting Thresholds

- Response time >2x baseline
- Error rate >1%
- Memory usage >80% of limit
- Correlation match rate <5% (may indicate rules need tuning)

path: docs/lessons.md
content: # Lessons Learned from Correlation Plugin Development

## Technical Insights

### 1. Merging Approaches

One of the key lessons from this project was the value of merging two separate approaches:
- `correlation-rules-mem` focused on proper plugin lifecycle integration
- `correlation-memory` implemented rich matching logic

By combining these approaches, we achieved both robust integration with OpenClaw and sophisticated matching capabilities. This taught us that sometimes the best solution emerges from synthesizing multiple partial solutions rather than trying to build everything from scratch.

### 2. Confidence Scoring Complexity

Implementing confidence scoring for correlation rules was more complex than initially anticipated. We learned that:
- Simple threshold-based approaches often fail in practice
- Context matters significantly in determining appropriate confidence levels
- User feedback is crucial for tuning confidence scores
- Different domains may require different confidence models

### 3. Matching Mode Trade-offs

The three matching modes (auto, strict, lenient) revealed important trade-offs:
- Strict mode provides predictable results but may miss relevant correlations
- Lenient mode captures more connections but increases false positives
- Auto mode requires careful tuning to balance precision and recall

## Development Process Lessons

### 1. Incremental Development

Attempting to build all features simultaneously led to integration challenges. Breaking down the development into smaller, testable components proved more effective:
1. Basic rule parsing and validation
2. Simple matching logic
3. Confidence scoring implementation
4. Advanced matching modes
5. Performance optimization

### 2. Testing with Real Data

Early testing with synthetic data didn't reveal the complexities that emerged with real-world usage. Key insights came from:
- Testing with actual OpenClaw session data
- Observing how users phrase their queries
- Identifying common correlation patterns in practice
- Understanding the diversity of contexts in real usage

### 3. Performance Considerations

Memory search performance became a critical concern as the correlation system grew more sophisticated. Lessons learned:
- Caching frequently accessed rules improved performance significantly
- Lazy evaluation of correlation rules reduced unnecessary computation
- Parallel processing of independent correlations was beneficial
- Monitoring memory usage was essential for maintaining responsiveness

## Subagent Failure Analysis

During development, we experienced failures with subagents responsible for correlation rule processing. Key lessons:

### Root Cause Analysis
- Resource contention between multiple concurrent correlation searches
- Inadequate error handling in rule processing pipelines
- Timeout issues with complex rule evaluations
- Memory leaks in recursive correlation resolution

### Mitigation Strategies
- Implemented resource quotas for correlation processing
- Added comprehensive error boundaries and fallback mechanisms
- Introduced timeouts with graceful degradation
- Fixed memory leak patterns through better resource management

### Prevention Measures
- Enhanced monitoring of subagent health
- Improved logging for correlation processing failures
- Added circuit breaker patterns for problematic rules
- Implemented progressive backoff for failing correlations

## User Experience Insights

### 1. Transparency vs. Automation

Users appreciated the enhanced search results but wanted more transparency about why certain correlations were made. This led to:
- Adding debug tools to explain correlation decisions
- Providing visibility into matched rules
- Allowing users to adjust correlation sensitivity
- Enabling rule-level feedback mechanisms

### 2. Over-correlation Problems

Initially, the system was too aggressive in suggesting correlations, leading to:
- Information overload
- Reduced trust in suggestions
- Performance impacts
- Confusion about relevance

We learned to be more conservative in correlation suggestions and provide better controls for users to tune the system.

### 3. Feedback Loop Importance

Establishing effective feedback mechanisms was crucial:
- Users needed ways to indicate when correlations were helpful or misleading
- Rule authors required data on rule effectiveness
- System administrators needed monitoring of overall performance
- Developers needed insights for future improvements

## Architecture Lessons

### 1. Plugin Design Patterns

The correlation plugin reinforced several important architectural principles:
- Separation of concerns between rule definition and execution
- Clear interfaces between components
- Extensibility for future enhancements
- Backward compatibility considerations

### 2. Configuration Management

Managing correlation rules as external configuration files proved beneficial:
- Enabled non-developers to contribute rules
- Allowed A/B testing of rule sets
- Facilitated version control of rule changes
- Supported different environments (dev/staging/prod)

### 3. Error Handling

Robust error handling was essential for a system that operates on user data:
- Graceful degradation when rules are malformed
- Isolation of failing rules from affecting others
- Clear error messages for debugging
- Recovery mechanisms for transient failures

## Future Improvements

### 1. Machine Learning Integration

While the current rule-based approach works well, we've identified opportunities for ML enhancement:
- Automatic discovery of correlation patterns
- Adaptive confidence scoring based on user feedback
- Personalization of correlations per user/context
- Anomaly detection for unusual correlation patterns

### 2. Performance Optimization

Ongoing performance work could focus on:
- More sophisticated caching strategies
- Distributed correlation processing
- Query optimization for complex rule sets
- Resource allocation improvements

### 3. User Interface Enhancements

Future improvements could include:
- Visual rule editing tools
- Correlation analytics dashboards
- Interactive correlation tuning
- Collaborative rule development workflows

## Conclusion

The correlation plugin development was a valuable learning experience that highlighted the importance of iterative development, real-world testing, and user feedback. The challenges encountered with subagent failures, performance optimization, and user experience tuning provided crucial insights that will benefit future OpenClaw plugin development efforts.

The key takeaway is that complex AI-enhanced systems require careful attention to reliability, transparency, and user control. Balancing automation with user agency remains a central challenge in building effective AI assistants.
---

## Additional Lessons: Memory System Fix (2026-03-18)

### 1. ESM/CommonJS Incompatibility

During memory system troubleshooting, we discovered a critical issue:
- **Problem**: node-llama-cpp v3.x is ESM-only, but OpenClaw gateway uses CommonJS require()
- **Error**: `ERR_REQUIRE_ASYNC_MODULE`
- **Impact**: Memory search disabled, semantic embeddings unavailable

**Lessons**:
- Always check module system compatibility (ESM vs CJS) when integrating Node.js packages
- File bugs early when dependencies have breaking changes
- Maintain fallback mechanisms (FTS) for critical functionality

### 2. Provider Configuration Discovery

Troubleshooting revealed configuration complexity:
- Multiple ways to configure API keys (env, credentials dir, auth.json, config)
- Provider initialization depends on environment availability at gateway startup
- Gateway restart is required for config changes to take effect

**Lessons**:
- Document configuration options clearly
- Provide diagnostic tools to verify provider initialization
- Use wrapper scripts for persistent environment variables

### 3. Local vs Remote Embeddings

We solved the embedding problem using local (Ollama) instead of remote (Voyage):
- Installed Ollama with `nomic-embed-text` model
- Configured `provider: "ollama"` in memorySearch
- Result: Fully local semantic search, no API costs

**Lessons**:
- Local embeddings can be more reliable than remote APIs
- Ollama provides a good alternative when ESM packages fail
- Hybrid mode (semantic + FTS) provides redundancy

### 4. Correlation Rules in Production

Our extensive correlation rules (20+ rules) proved valuable:
- Rules automatically enrich memory searches with context
- Lifecycle states (proposal → testing → validated → promoted → retired) help manage rule quality
- Usage tracking helps identify effective vs unused rules

**Lessons**:
- Invest in rule governance from the start
- Track rule effectiveness through usage counts
- Regular review cycles keep rules relevant

---

*Added: 2026-03-18*


path: docs/production-guide.md
content: # OpenClaw Correlation Plugin — Production Guide

This guide supplements the [README](./README.md) with lessons from live production deployments. If you're running the correlation plugin in anger, these notes will save you time.

---

## 1. Overview

The README covers the what and why. This guide covers the **how in practice**:
- How to integrate correlation surfacing into your heartbeat loop
- How to tune confidence thresholds without drowning in noise
- How to manage the rule lifecycle without making a mess
- What actually goes wrong in production, and how to debug it

---

## 2. Heartbeat Integration

The correlation plugin shines when it surfaces context *proactively*, not just when you remember to ask. The recommended integration point is your OpenClaw heartbeat.

### Pattern: Surfacing on every N heartbeats

In your `HEARTBEAT.md`, add a periodic check that looks at what you're currently working on and surfaces related contexts:

```bash
# HEARTBEAT.md

## Periodic Tasks

### Correlation Surfacing (every 5 heartbeats)
When working on a topic, check correlation rules to surface related contexts:
bash scripts/correlation-surfacing.sh "<current topic keywords>"
```

The `correlation-surfacing.sh` script reads your current context (passed as argument), matches it against `memory/correlation-rules.json`, and outputs what else you should know.

**Script location:** Place the script in your OpenClaw workspace scripts directory (e.g., `~/.openclaw/workspace/scripts/correlation-surfacing.sh`). The script is available in the plugin repository under `scripts/`.

```bash
# Example invocation
bash scripts/correlation-surfacing.sh "config change"
```

Output:
```json
{
  "context": "config change",
  "matched": 1,
  "rules": [{
    "id": "cr-001",
    "trigger": "config-change",
    "fetches": ["backup-location", "rollback-instructions", "recent-changes"]
  }]
}
```

**Why every 5 heartbeats and not every one?** Correlation surfacing fires a memory search per matched rule. On busy systems, every heartbeat is too aggressive. Every 5 gives you regular enrichment without token burn.

### What to pass as context

The script accepts natural language. Good context strings:
- `"config change"` — fires the config-safety rules
- `"error 400 gateway"` — fires error-debugging + gateway rules
- `"plugin install"` — fires plugin-management rules

Bad context strings (too generic, fire too often):
- `"help"` — no rule should trigger on this
- `"status"` — too broad

---

## 3. Tuning Confidence Thresholds

Confidence (`0.0`–`1.0`) controls how strongly a rule's correlation is weighted. Getting this right is the difference between useful surfacing and noise.

### Practical guide

| Confidence | When to use | Example |
|------------|-------------|---------|
| `0.95–0.99` | Critical operations where getting this wrong is expensive | Config changes, gateway restarts, plugin installs |
| `0.85–0.90` | High-value patterns that reliably indicate the context | Backup operations, error debugging |
| `0.70–0.80` | Useful correlations but with some false-positive risk | Session recovery, git operations |
| `< 0.70` | Exploratory rules or very niche patterns | Almost never needed in practice |

### The mistake everyone makes at first

Setting everything to `0.95` because "high confidence sounds better." This causes **signal drowning**: your high-confidence rules dominate every search, and you never see the lower-confidence correlations that might actually be relevant to the current situation.

**Rule of thumb:** Only use `0.95+` for operations where the cost of missing the correlation is *catastrophic* (gateway down, data loss). Everything else `0.70–0.90`.

### Tuning procedure

1. Deploy a new rule with `lifecycle.state: "testing"` and confidence `0.70`
2. Run your heartbeat for a few days, watch how often it fires
3. If it fires on every unrelated query → lower confidence or narrow keywords
4. If it never fires when it should → widen keywords or raise confidence
5. When stable, move to `validated`

---

## 4. Lifecycle Management

Rules are not static. They have a lifecycle from idea to retirement.

```
proposal → testing → validated → promoted → retired
```

### What each state means

**`proposal`** — A new idea for a correlation rule. Written with lower confidence (`0.60–0.75`), deployed in testing mode only. Not active by default unless you explicitly enable proposal rules.

**`testing`** — The rule is live but being evaluated. You'll see it fire but it won't auto-surface until validated. Set confidence based on how sure you are.

**`validated`** — The rule fires correctly and the signal-to-noise ratio is acceptable. It is now active.

**`promoted`** — The rule is rock-solid. High confidence (`0.90+`), well-tested, and you want it to always be available. Only promoted rules should have confidence `0.99`.

**`retired`** — The rule is obsolete. Maybe the pattern it detected no longer exists, or it was replaced by a better rule. Keep it in the file (with `retired`) rather than deleting it — this preserves the `learned_from` history.

### Practical workflow

When you discover a new correlation pattern:

1. Add rule as `proposal` with `confidence: 0.70`
2. After a week of testing, move to `testing`
3. After validation (noise acceptable), move to `validated`
4. After 30+ firings with no issues, consider `promoted`

Don't rush promotion. A premature `promoted` rule with `0.99` confidence that fires inappropriately is hard to undo because people trust it.

---

## 5. Common Pitfalls

### Pitfall 1: Keywords that fire too often

`trigger_keywords: ["error"]` will fire on almost every message in a development channel. This makes the rule useless because it always fires but rarely means anything.

**Fix:** Be specific. `["error", "400", "crash"]` — require 2+ keywords to co-occur, or use a specific phrase as the `trigger_context`.

### Pitfall 2: Fetching non-existent contexts

A rule that says `must_also_fetch: ["recovery-procedures"]` but there's no file at `memory/recovery-procedures.md` silently does nothing. The plugin doesn't warn you.

**Fix:** Always verify that every context in `must_also_fetch` actually exists in your memory directory.

### Pitfall 3: Overlapping rules

Rule A triggers on `["config"]`, Rule B triggers on `["change"]`. Both fire on `"config change"`. Now you get duplicate surfacing.

**Fix:** Before adding a rule, search existing rules for keyword overlap. Use distinct `trigger_context` values to keep rules distinguishable.

### Pitfall 4: Confidence theft

Rule A (`confidence: 0.99`) always fires on your context. Rule B (`confidence: 0.85`) would also be relevant but Rule A's results dominate.

**Fix:** If a `0.99` rule fires on every config operation, ask: does it need to be that high? Reduce to `0.90` and let lower rules breathe.

### Pitfall 5: No `learned_from`

Rules without `learned_from` are impossible to audit. When the rule fires incorrectly in 6 months, you won't remember why you created it.

**Fix:** Every rule needs a `learned_from` that names the incident, pattern, or lesson that prompted it. Keep it descriptive but concise.

---

## 6. Debugging

### Using `correlation_check`

The plugin provides a debug tool to check which rules match a given context:

```bash
openclaw exec correlation_check --context "config change"
```

This shows:
- Which rules matched
- Why they matched (which keywords fired)
- What contexts they would fetch

### Tracing a specific rule

```bash
openclaw exec correlation_check --rule-id cr-001 --context "openclaw.json edit"
```

### Reading the output

If a rule **should** fire but doesn't:
1. Check that your keywords appear in the context string
2. Check that `trigger_context` matches the semantic domain
3. Check the rule's lifecycle state — `proposal` rules need explicit enabling

If a rule fires **when it shouldn't**:
1. The keywords are too broad → narrow them
2. Confidence too high → lower it
3. Context is wrong → the same keyword can mean different things in different contexts

---

## 7. Troubleshooting

### Rule fires but fetches nothing

Most common cause: one or more contexts listed in `must_also_fetch` don't exist as files in your memory directory.

**Fix:** Run `ls memory/` and verify every referenced context has a corresponding file. Create missing ones or remove the reference.

### Rule doesn't fire when it should

1. Check that your `trigger_keywords` actually appear in the context string you're passing
2. Verify the rule's `lifecycle.state` — `proposal` rules need explicit opt-in
3. Try the `correlation_check` tool directly:
   ```bash
   openclaw exec correlation_check --context "your context here"
   ```
4. If keywords overlap with another higher-confidence rule, the lower-confidence rule's results may be buried

### Getting duplicate results

Two rules firing on the same context and fetching overlapping contexts. 

**Fix:** Review both rules' `must_also_fetch` lists for overlap. Narrow keywords on one of the rules, or reduce its confidence so the other dominates.

### Too much noise after adding a rule

The rule fires on almost every query.

**Fix:** Raise the confidence threshold, or narrow the keyword list. A rule with `confidence: 0.70` that fires constantly should probably be `0.50` or removed.

### Rule worked fine, then started causing noise

`usage_count` is high but signal-to-noise has degraded. This usually means the operational pattern changed (new tool, new workflow) and the rule's keywords are now triggering in unrelated contexts.

**Fix:** Narrow the keywords, or set `lifecycle.state: "testing"` to stop auto-surfacing while you recalibrate.

---

## 8. When NOT to Use Correlation Rules

Correlation rules are powerful but not universal. Avoid them when:

**1. The relationship is 1:1, not N:M**
If "when X happens, always do Y" — that's automation, not correlation. Write a script.

**2. The keyword is too common**
Words like "help", "check", "status", "info" fire on almost everything. They'll generate noise, not signal.

**3. The contexts don't exist**
If you reference `memory/recovery-procedures.md` but never created that file, the correlation silently fails. Only reference contexts you actually maintain.

**4. The pattern is genuinely one-off**
If it happened once and won't happen again, don't write a rule. Just document it.

**5. You need exact precision**
Correlation is probabilistic. If you need guaranteed checks (e.g., security compliance), use deterministic rules or scripts, not correlation confidence scoring.

---

## Appendix: Minimal Rule Checklist

Before deploying a new rule, verify:

- [ ] Keywords are specific enough not to fire on every message
- [ ] All `must_also_fetch` contexts exist in memory/
- [ ] `confidence` is appropriate (not everything needs 0.95)
- [ ] `learned_from` describes why this rule exists
- [ ] `lifecycle.state` is set (default to `testing` for new rules)
- [ ] No existing rule has significant keyword overlap


path: docs/research.md
content: # Correlation Plugin Research

## Background

The correlation plugin emerged from the need to enhance memory search capabilities in OpenClaw by automatically retrieving related contexts when querying memory. Traditional memory search returns results directly related to the query terms, but often misses contextual information that is crucial for making informed decisions.

## Problem Statement

When users search for specific topics in OpenClaw's memory system, they often need additional context that isn't directly captured by the search terms. For example, when searching for "backup error," it's valuable to also retrieve information about "last backup time," "recovery procedures," and "similar errors" because these contexts consistently matter together.

## Research Findings

### 1. Contextual Decision Making

Research in cognitive science shows that human decision-making relies heavily on contextual information. When presented with a problem, experts automatically recall related experiences, constraints, and solutions that inform their approach.

### 2. Information Retrieval Enhancement

Traditional information retrieval systems focus on keyword matching and relevance scoring. However, for decision support systems like OpenClaw, it's essential to retrieve not just relevant information but also correlated information that enhances understanding.

### 3. Rule-Based Correlation

The correlation plugin implements a rule-based approach where domain experts can define relationships between concepts. These rules specify that when certain contexts or keywords are detected, additional related information should be retrieved.

### 4. Confidence Scoring

Each correlation rule includes a confidence score that determines how strongly the relationship should be enforced. This allows for nuanced correlations that adapt to different situations.

## Technical Approach

### Merged Architecture

The plugin combines two previous approaches:
1. **SDK-native tool registration** (from `correlation-rules-mem`) — proper OpenClaw plugin lifecycle
2. **Rich matching logic** (from `correlation-memory`) — keyword matching, confidence filtering, lenient/strict modes

### Matching Modes

Three matching modes provide flexibility for different use cases:
- `auto` (default) — keyword + context matching
- `strict` — exact keyword match only
- `lenient` — fuzzy matching for broad queries

## Related Work

### Coolmanns Architecture

The correlation plugin draws inspiration from the Coolmanns architecture for context-aware systems, which emphasizes the importance of retrieving related information to support decision-making processes.

### Previous Implementations

Two earlier implementations informed the current approach:
1. `correlation-rules-mem` - Focused on proper plugin lifecycle integration
2. `correlation-memory` - Implemented rich matching logic and confidence filtering

## Evaluation

Initial testing showed that the correlation plugin significantly improves the quality of information retrieval for complex decision-making scenarios by automatically including relevant contextual information.

## Future Directions

1. Machine learning approaches to automatically discover correlation rules
2. Integration with external knowledge bases
3. Adaptive confidence scoring based on user feedback
4. Cross-domain correlation rules

## Sources

- Cognitive science research on contextual decision making
- Information retrieval literature on query expansion
- Coolmanns architecture for context-aware systems
- OpenClaw internal development discussions and experiments

path: index.ts
content: import type { OpenClawPluginApi, AnyAgentTool } from "openclaw/plugin-sdk";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
import * as fs from "fs";
import * as path from "path";

/**
 * Correlation Memory Plugin (Unified)
 *
 * Merges correlation-memory (rich matching) + correlation-rules-mem (SDK-native).
 * Provides automatic decision-context retrieval: when you query about topic X,
 * correlation rules surface related contexts Y and Z that typically matter.
 *
 * SECURITY NOTES:
 * - This plugin does NOT access environment variables directly
 * - This plugin does NOT make network requests (read-only local file ops)
 * - This plugin does NOT write to filesystem (read-only)
 * - This plugin does NOT handle credentials
 * - Workspace path resolved via ctx.workspaceDir (SDK runtime) with config/default fallbacks
 *
 * Audit: 2026-03-20 - Passed deep security review
 */

interface CorrelationRule {
  id?: string;
  context?: string;
  trigger_context?: string;
  // Support both field names from different rule formats
  trigger_keywords?: string[];
  keywords?: string[];
  must_also_fetch?: string[];
  correlations?: Array<string | { search_query?: string }>;
  relationship_type?: string;
  confidence?: number;
  lifecycle?: { state?: string };
  usage_count?: number;
}

interface MatchedRule {
  id: string | undefined;
  context: string | undefined;
  confidence: number | undefined;
  relationship_type?: string;
  additional_searches: string[];
}

// ── Rule Loading (with mtime cache) ──────────────────────────────────

let cachedRules: CorrelationRule[] | null = null;
let cachedMtime = 0;

const ACTIVE_STATES = new Set([
  "promoted", "active", "testing", "validated", "proposal",
]);

function loadCorrelationRules(workspacePath: string): CorrelationRule[] {
  const rulesPath = path.resolve(
    path.join(workspacePath, "memory/correlation-rules.json"),
  );

  try {
    const stat = fs.statSync(rulesPath);
    if (cachedRules && stat.mtimeMs === cachedMtime) return cachedRules;

    const data = fs.readFileSync(rulesPath, "utf-8");
    const parsed = JSON.parse(data);
    const rules: CorrelationRule[] = parsed.rules || [];

    // Filter to active rules only
    const filtered = rules.filter((rule) => {
      if (!rule.id) return false;
      if (rule.confidence !== undefined && rule.confidence <= 0) return false;
      const state = rule.lifecycle?.state;
      return !state || ACTIVE_STATES.has(state);
    });

    cachedRules = filtered;
    cachedMtime = stat.mtimeMs;
    return filtered;
  } catch (err) {
    const msg = err instanceof Error ? err.message : String(err);
    console.error(`[correlation-memory] Failed to load rules from ${rulesPath}: ${msg}`);
    return [];
  }
}

// ── Keyword Extraction ────────────────────────────────────────────────

function getKeywords(rule: CorrelationRule): string[] {
  // Handle both field naming conventions
  return rule.trigger_keywords || rule.keywords || [];
}

function getContext(rule: CorrelationRule): string {
  return rule.trigger_context || rule.context || "";
}

function getAdditionalSearches(rule: CorrelationRule): string[] {
  const searches: string[] = [];

  // must_also_fetch (current rules format)
  if (rule.must_also_fetch) {
    searches.push(...rule.must_also_fetch);
  }

  // correlations (legacy format)
  if (rule.correlations) {
    for (const corr of rule.correlations) {
      if (typeof corr === "string") {
        searches.push(corr);
      } else if (corr.search_query) {
        searches.push(corr.search_query);
      }
    }
  }

  return [...new Set(searches)]; // deduplicate
}

// ── Word-boundary matching ───────────────────────────────────────────

const regexCache = new Map<string, RegExp>();

function wordMatch(text: string, keyword: string): boolean {
  // Multi-word keywords: all words must be present (word-boundary each)
  if (keyword.includes(" ")) {
    return keyword.split(/\s+/).every((word) => wordMatch(text, word));
  }
  let re = regexCache.get(keyword);
  if (!re) {
    const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
    re = new RegExp(`\\b${escaped}\\b`, "i");
    // LRU eviction — prevent unbounded cache growth
    const MAX_CACHE_SIZE = 500;
    if (regexCache.size >= MAX_CACHE_SIZE) {
      const firstKey = regexCache.keys().next().value;
      regexCache.delete(firstKey);
    }
    regexCache.set(keyword, re);
  } else {
    // Cache hit — re-insert to update LRU ordering (move to end of Map insertion order)
    regexCache.delete(keyword);
    regexCache.set(keyword, re);
  }
  return re.test(text);
}

// ── Workspace Path Resolution ───────────────────────────────────────────

interface OpenClawPluginApiExtended extends OpenClawPluginApi {
  config?: {
    agents?: {
      defaults?: {
        workspace?: string;
      };
    };
  };
}

function resolveWorkspacePath(api: OpenClawPluginApi, ctx: { workspaceDir?: string }): string {
  return (
    ctx.workspaceDir ??
    (api as OpenClawPluginApiExtended).config?.agents?.defaults?.workspace ??
    '/home/pi/.openclaw/workspace'
  );
}

// ── Matching Logic ────────────────────────────────────────────────────

interface MatchOptions {
  mode: "auto" | "strict" | "lenient";
  minConfidence: number;
  maxResults: number;
}

function matchRules(
  rules: CorrelationRule[],
  query: string,
  options: Partial<MatchOptions> = {},
): MatchedRule[] {
  const { mode = "auto", minConfidence = 0, maxResults = 10 } = options;
  const matched: MatchedRule[] = [];
  const seenIds = new Set<string>();

  for (const rule of rules) {
    if (matched.length >= maxResults) break;
    const ruleId = rule.id || "unknown";
    if (seenIds.has(ruleId)) continue;

    // Confidence gate
    if (rule.confidence !== undefined && rule.confidence < minConfidence) continue;

    let isMatch = false;

    // Keyword matching with word boundaries (auto + strict)
    const keywords = getKeywords(rule);
    for (const kw of keywords) {
      if (wordMatch(query, kw)) {
        isMatch = true;
        break;
      }
    }

    // Context matching — normalize hyphens/underscores, partial word match (auto only)
    if (!isMatch && mode !== "strict") {
      const context = getContext(rule);
      if (context) {
        const ctxWords = context.replace(/[-_]/g, " ").toLowerCase().split(/\s+/).filter((w) => w.length > 0);
        const queryWords = new Set(
          query.toLowerCase().replace(/[^\w\s]/g, " ").split(/\s+/),
        );
        const matchingWords = ctxWords.filter((w) => queryWords.has(w));
        const coverage = ctxWords.length > 0 ? matchingWords.length / ctxWords.length : 0;
        isMatch = matchingWords.length >= 2 || coverage >= 0.8;
      }
    }

    if (isMatch) {
      seenIds.add(ruleId);
      matched.push({
        id: rule.id,
        context: getContext(rule),
        confidence: rule.confidence,
        relationship_type: rule.relationship_type,
        additional_searches: getAdditionalSearches(rule),
      });
    }
  }

  // Lenient fallback: fuzzy word matching if nothing matched
  if (mode === "lenient" && matched.length === 0) {
    const queryWords = query
      .toLowerCase()
      .replace(/[^\w\s]/g, " ")
      .split(/\s+/)
      .filter((w) => w.length > 2);

    for (const rule of rules) {
      if (matched.length >= maxResults) break;
      const ruleId = rule.id || "unknown";
      if (seenIds.has(ruleId)) continue;

      // Confidence gate
      if (rule.confidence !== undefined && rule.confidence < minConfidence) continue;

      const ruleText = [
        getContext(rule),
        ...getKeywords(rule),
        ...getAdditionalSearches(rule),
      ]
        .join(" ")
        .toLowerCase();

      for (const word of queryWords) {
        if (ruleText.includes(word)) {
          seenIds.add(ruleId);
          matched.push({
            id: rule.id,
            context: getContext(rule),
            confidence: rule.confidence,
            relationship_type: rule.relationship_type,
            additional_searches: getAdditionalSearches(rule),
          });
          break;
        }
      }
    }
  }

  // Sort by confidence descending, then id ascending (stable tiebreak)
  matched.sort((a, b) => {
    const diff = (b.confidence ?? 0) - (a.confidence ?? 0);
    if (diff !== 0) return diff;
    return (a.id ?? "").localeCompare(b.id ?? "");
  });
  return matched.slice(0, maxResults);
}

// ── Plugin Registration ───────────────────────────────────────────────

const correlationMemoryPlugin = {
  id: "correlation-memory",
  name: "Correlation Memory Search",
  description:
    "Correlation-aware memory search — automatic decision-context retrieval with keyword matching, confidence filtering, and rule lifecycle management",
  kind: "memory",
  configSchema: emptyPluginConfigSchema(),

  register(api: OpenClawPluginApi) {
    api.registerTool(
      (ctx) => {
        // Resolve workspace path — ctx.workspaceDir is set by the SDK runtime
        const workspacePath = resolveWorkspacePath(api, ctx);

        return [
          // ── Tool 1: memory_search_with_correlation ──
          {
            name: "memory_search_with_correlation",
            description:
              "Search memory with automatic correlation rule matching. " +
              "When you query a topic, this checks correlation rules and suggests " +
              "additional searches for related contexts that typically matter together. " +
              'Example: "backup error" also retrieves "last backup time" and "recovery procedures".',
            parameters: {
              type: "object" as const,
              properties: {
                query: {
                  type: "string",
                  description: "The search query to find relevant memories",
                },
                auto_correlate: {
                  type: "boolean",
                  description:
                    "Automatically check correlation rules (default: true)",
                  default: true,
                },
                correlation_mode: {
                  type: "string",
                  enum: ["auto", "strict", "lenient"],
                  description:
                    "Matching mode: auto (keyword + context), strict (word-boundary keyword), lenient (fuzzy fallback)",
                  default: "auto",
                },
                min_confidence: {
                  type: "number",
                  description: "Minimum confidence threshold (0-1, default: 0)",
                  default: 0,
                },
                max_results: {
                  type: "number",
                  description: "Maximum number of rules to return (default: 10)",
                  default: 10,
                },
              },
              required: ["query"],
            },
            execute: async (params: {
              query: string;
              auto_correlate?: boolean;
              correlation_mode?: "auto" | "strict" | "lenient";
              min_confidence?: number;
              max_results?: number;
            }) => {
              const {
                query,
                auto_correlate = true,
                correlation_mode = "auto",
                min_confidence = 0,
                max_results = 10,
              } = params;

              // Validate numeric params — prevent NaN or out-of-range values
              const safeMaxResults = Math.max(1, Math.floor(max_results ?? 10));
              const safeMinConfidence = Math.min(1, Math.max(0, min_confidence ?? 0));

              const rules = loadCorrelationRules(workspacePath);

              if (rules.length === 0) {
                return {
                  success: true,
                  query,
                  matched_rules: [],
                  suggested_additional_searches: [],
                  summary: "No correlation rules loaded.",
                };
              }

              const matched = auto_correlate
                ? matchRules(rules, query, { mode: correlation_mode, minConfidence: safeMinConfidence, maxResults: safeMaxResults })
                : [];

              const allSearches = matched.flatMap((r) => r.additional_searches);
              const uniqueSearches = [...new Set(allSearches)];

              const ruleIds = matched.map((r) => r.id).join(", ");

              return {
                success: true,
                query,
                correlation_mode,
                matched_rules: matched,
                suggested_additional_searches: uniqueSearches,
                summary:
                  matched.length === 0
                    ? `No correlation rules matched for "${query}".`
                    : `Matched ${matched.length} rule(s) [${ruleIds}]. ` +
                      `Additional searches: ${uniqueSearches.join(", ") || "none"}.`,
              };
            },
          },

          // ── Tool 2: correlation_check ──
          {
            name: "correlation_check",
            description:
              "Debug tool: check which correlation rules would match a given context without performing searches.",
            parameters: {
              type: "object" as const,
              properties: {
                context: {
                  type: "string",
                  description: "Context or query to check against rules",
                },
                mode: {
                  type: "string",
                  enum: ["auto", "strict", "lenient"],
                  description: "Matching mode",
                  default: "auto",
                },
                min_confidence: {
                  type: "number",
                  description: "Minimum confidence threshold (0-1, default: 0)",
                  default: 0,
                },
                max_results: {
                  type: "number",
                  description: "Maximum rules to return (default: 10)",
                  default: 10,
                },
              },
              required: ["context"],
            },
            execute: async (params: {
              context: string;
              mode?: "auto" | "strict" | "lenient";
              min_confidence?: number;
              max_results?: number;
            }) => {
              const { context, mode = "auto", min_confidence = 0, max_results = 10 } = params;

              // Validate numeric params
              const safeMaxResults = Math.max(1, Math.floor(max_results ?? 10));
              const safeMinConfidence = Math.min(1, Math.max(0, min_confidence ?? 0));

              const rules = loadCorrelationRules(workspacePath);
              const matched = matchRules(rules, context, { mode, minConfidence: safeMinConfidence, maxResults: safeMaxResults });

              return {
                success: true,
                context,
                mode,
                total_rules: rules.length,
                matched_count: matched.length,
                matching_rules: matched,
              };
            },
          },
        ];
      },
      { names: ["memory_search_with_correlation", "correlation_check"] },
    );
  },
};

export default correlationMemoryPlugin;


path: openclaw.plugin.json
content: {
  "id": "correlation-memory",
  "name": "Correlation Memory Search",
  "description": "Correlation-aware memory search — automatic decision-context retrieval with keyword matching, confidence filtering, and rule lifecycle management",
  "version": "2.1.0",
  "kind": "memory",
  "configSchema": {
    "type": "object",
    "additionalProperties": false,
    "properties": {}
  }
}


path: package.json
content: {
  "name": "@openclaw/correlation-memory",
  "version": "2.1.0",
  "private": true,
  "description": "Unified correlation-aware memory search plugin for OpenClaw",
  "type": "module",
  "scripts": {
    "test": "vitest run",
    "test:watch": "vitest"
  },
  "devDependencies": {
    "vitest": "^3.0.0"
  },
  "peerDependencies": {
    "openclaw": ">=2026.1.26"
  },
  "openclaw": {
    "extensions": ["./index.ts"]
  }
}


path: tests/correlation.test.ts
content: /**
 * Correlation Memory Plugin — Unit Tests
 *
 * Test strategy:
 * - Pure functions (wordMatch, matchRules, getKeywords, etc.) — direct unit tests
 * - loadCorrelationRules — mocked fs module to control rules content and mtimes
 * - Integration tests cover the full tool execute flow
 *
 * Run with: npx vitest run
 */
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";

// ── Helpers copied from index.ts (must stay in sync) ──────────────────────────

const MARKER = (n: number) => `  ... [${n} lines truncated by exec-truncate] ...`;

function abbrevSize(bytes: number): string {
  if (bytes < 1024) return `${bytes}B`;
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}K`;
  if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024 / 1024).toFixed(1)}M`;
  return `${(bytes / 1024 / 1024 / 1024).toFixed(1)}G`;
}

// ── Pure function tests (copied from index.ts for unit testing) ─────────────────

const regexCache = new Map<string, RegExp>();
const MAX_CACHE_SIZE = 500;

function wordMatch(text: string, keyword: string): boolean {
  if (keyword.includes(" ")) {
    return keyword.split(/\s+/).every((word) => wordMatch(text, word));
  }
  let re = regexCache.get(keyword);
  if (!re) {
    const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
    re = new RegExp(`\\b${escaped}\\b`, "i");
    if (regexCache.size >= MAX_CACHE_SIZE) {
      const firstKey = regexCache.keys().next().value;
      regexCache.delete(firstKey);
    }
    regexCache.set(keyword, re);
  } else {
    // Cache hit — re-insert to update LRU ordering
    regexCache.delete(keyword);
    regexCache.set(keyword, re);
  }
  return re.test(text);
}

// ── wordMatch tests ───────────────────────────────────────────────────────────

describe("wordMatch", () => {
  beforeEach(() => {
    regexCache.clear();
  });

  it("matches single word case-insensitively", () => {
    expect(wordMatch("Hello World", "hello")).toBe(true);
    expect(wordMatch("Hello World", "WORLD")).toBe(true);
    expect(wordMatch("hello", "hello")).toBe(true);
  });

  it("rejects substring without word boundaries", () => {
    expect(wordMatch("I need help with config", "config")).toBe(true);
    expect(wordMatch("I need help with configuration", "config")).toBe(false);
    expect(wordMatch("reconfigure the thing", "figure")).toBe(false);
  });

  it("handles multi-word keywords (all words must match)", () => {
    expect(wordMatch("I changed the config file", "config file")).toBe(true);
    expect(wordMatch("I changed the config", "config file")).toBe(false); // missing "file"
    expect(wordMatch("file for config", "config file")).toBe(true);
  });

  it("handles empty text", () => {
    expect(wordMatch("", "error")).toBe(false);
  });

  it("handles empty keyword", () => {
    expect(wordMatch("some text", "")).toBe(true); // empty keyword splits to [""], every([]) === true
  });

  it("LRU cache — evicts oldest entry when full", () => {
    // Fill cache to MAX_CACHE_SIZE
    for (let i = 0; i < MAX_CACHE_SIZE; i++) {
      wordMatch("some text here", `keyword${i}`);
    }
    expect(regexCache.size).toBe(MAX_CACHE_SIZE);

    // Insert one more — should evict the oldest (first inserted)
    const firstKey = "keyword0";
    expect(regexCache.has(firstKey)).toBe(true);
    wordMatch("new text here", "newKeyword");
    expect(regexCache.size).toBe(MAX_CACHE_SIZE);
    expect(regexCache.has(firstKey)).toBe(false); // evicted
    expect(regexCache.has("newKeyword")).toBe(true);
  });

  it("LRU cache — cache hit moves entry to end of insertion order", () => {
    // Insert keywords 0-4
    for (let i = 0; i < 5; i++) {
      wordMatch("text", `kw${i}`);
    }

    // Access kw0 (cache hit)
    wordMatch("text containing kw0", "kw0");

    // Fill to capacity
    for (let i = 5; i < MAX_CACHE_SIZE; i++) {
      wordMatch("text", `keyword${i}`);
    }

    // kw0 should still be in cache (was re-inserted on hit, moved to end)
    // and kw1 should be evicted (it was first before kw0's re-insert)
    expect(regexCache.has("kw0")).toBe(true);
  });
});

// ── getKeywords tests ────────────────────────────────────────────────────────

interface CorrelationRule {
  id?: string;
  context?: string;
  trigger_context?: string;
  trigger_keywords?: string[];
  keywords?: string[];
  must_also_fetch?: string[];
  correlations?: Array<string | { search_query?: string }>;
  relationship_type?: string;
  confidence?: number;
  lifecycle?: { state?: string };
  usage_count?: number;
}

function getKeywords(rule: CorrelationRule): string[] {
  return rule.trigger_keywords || rule.keywords || [];
}

describe("getKeywords", () => {
  it("prefers trigger_keywords over keywords", () => {
    expect(getKeywords({ trigger_keywords: ["a", "b"] })).toEqual(["a", "b"]);
    expect(getKeywords({ keywords: ["a", "b"] })).toEqual(["a", "b"]);
    expect(getKeywords({ trigger_keywords: ["x"], keywords: ["y"] })).toEqual(["x"]);
  });

  it("returns empty array when neither field exists", () => {
    expect(getKeywords({})).toEqual([]);
  });
});

// ── getAdditionalSearches tests ─────────────────────────────────────────────

function getAdditionalSearches(rule: CorrelationRule): string[] {
  const searches: string[] = [];
  if (rule.must_also_fetch) {
    searches.push(...rule.must_also_fetch);
  }
  if (rule.correlations) {
    for (const corr of rule.correlations) {
      if (typeof corr === "string") {
        searches.push(corr);
      } else if (corr.search_query) {
        searches.push(corr.search_query);
      }
    }
  }
  return [...new Set(searches)]; // deduplicate
}

describe("getAdditionalSearches", () => {
  it("extracts must_also_fetch strings", () => {
    const rule: CorrelationRule = {
      id: "cr-1",
      must_also_fetch: ["context-a", "context-b"],
    };
    expect(getAdditionalSearches(rule)).toEqual(["context-a", "context-b"]);
  });

  it("extracts correlation strings", () => {
    const rule: CorrelationRule = {
      id: "cr-2",
      correlations: ["corr-a", "corr-b"],
    };
    expect(getAdditionalSearches(rule)).toEqual(["corr-a", "corr-b"]);
  });

  it("extracts correlation objects with search_query", () => {
    const rule: CorrelationRule = {
      id: "cr-3",
      correlations: [{ search_query: "query-a" }, { search_query: "query-b" }],
    };
    expect(getAdditionalSearches(rule)).toEqual(["query-a", "query-b"]);
  });

  it("merges both fields with deduplication", () => {
    const rule: CorrelationRule = {
      id: "cr-4",
      must_also_fetch: ["ctx-a", "ctx-b"],
      correlations: ["ctx-b", "ctx-c"], // ctx-b is duplicate
    };
    expect(getAdditionalSearches(rule)).toEqual(["ctx-a", "ctx-b", "ctx-c"]);
  });

  it("returns empty array when neither field exists", () => {
    expect(getAdditionalSearches({ id: "cr-5" })).toEqual([]);
  });
});

// ── matchRules tests ─────────────────────────────────────────────────────────

const ACTIVE_STATES = new Set([
  "promoted", "active", "testing", "validated", "proposal",
]);

function matchRules(
  rules: CorrelationRule[],
  query: string,
  options: Partial<{ mode: string; minConfidence: number; maxResults: number }> = {},
): Array<{
  id: string | undefined;
  context: string | undefined;
  confidence: number | undefined;
  relationship_type?: string;
  additional_searches: string[];
}> {
  const { mode = "auto", minConfidence = 0, maxResults = 10 } = options;
  const matched: Array<{
    id: string | undefined;
    context: string | undefined;
    confidence: number | undefined;
    relationship_type?: string;
    additional_searches: string[];
  }> = [];
  const seenIds = new Set<string>();

  for (const rule of rules) {
    if (matched.length >= maxResults) break;
    const ruleId = rule.id || "unknown";
    if (seenIds.has(ruleId)) continue;
    if (rule.confidence !== undefined && rule.confidence < minConfidence) continue;

    let isMatch = false;
    const keywords = getKeywords(rule);
    for (const kw of keywords) {
      if (wordMatch(query, kw)) { isMatch = true; break; }
    }

    if (!isMatch && mode !== "strict") {
      const context = rule.trigger_context || rule.context || "";
      if (context) {
        const ctxWords = context.replace(/[-_]/g, " ").toLowerCase().split(/\s+/).filter((w) => w.length > 0);
        const queryWords = new Set(query.toLowerCase().replace(/[^\w\s]/g, " ").split(/\s+/));
        const matchingWords = ctxWords.filter((w) => queryWords.has(w));
        const coverage = ctxWords.length > 0 ? matchingWords.length / ctxWords.length : 0;
        isMatch = matchingWords.length >= 2 || coverage >= 0.8;
      }
    }

    if (isMatch) {
      seenIds.add(ruleId);
      matched.push({
        id: rule.id,
        context: rule.trigger_context || rule.context,
        confidence: rule.confidence,
        relationship_type: rule.relationship_type,
        additional_searches: getAdditionalSearches(rule),
      });
    }
  }

  if (mode === "lenient" && matched.length === 0) {
    const queryWords = query.toLowerCase().replace(/[^\w\s]/g, " ").split(/\s+/).filter((w) => w.length > 2);
    for (const rule of rules) {
      if (matched.length >= maxResults) break;
      const ruleId = rule.id || "unknown";
      if (seenIds.has(ruleId)) continue;
      if (rule.confidence !== undefined && rule.confidence < minConfidence) continue;
      const ruleText = [rule.trigger_context || rule.context || "", ...getKeywords(rule), ...getAdditionalSearches(rule)].join(" ").toLowerCase();
      for (const word of queryWords) {
        if (ruleText.includes(word)) {
          seenIds.add(ruleId);
          matched.push({
            id: rule.id,
            context: rule.trigger_context || rule.context,
            confidence: rule.confidence,
            relationship_type: rule.relationship_type,
            additional_searches: getAdditionalSearches(rule),
          });
          break;
        }
      }
    }
  }

  matched.sort((a, b) => {
    const diff = (b.confidence ?? 0) - (a.confidence ?? 0);
    if (diff !== 0) return diff;
    return (a.id ?? "").localeCompare(b.id ?? "");
  });
  return matched.slice(0, maxResults);
}

describe("matchRules", () => {
  beforeEach(() => {
    regexCache.clear();
  });

  const rules: CorrelationRule[] = [
    {
      id: "cr-001",
      trigger_context: "config-change",
      trigger_keywords: ["config", "setting", "change"],
      confidence: 0.95,
      lifecycle: { state: "promoted" },
      must_also_fetch: ["backup-location"],
    },
    {
      id: "cr-002",
      trigger_context: "error-debugging",
      trigger_keywords: ["error", "fail", "bug"],
      confidence: 0.85,
      lifecycle: { state: "active" },
      must_also_fetch: ["recovery-procedures"],
    },
    {
      id: "cr-003",
      trigger_context: "session-recovery",
      trigger_keywords: ["checkpoint", "recover", "restart"],
      confidence: 0.70,
      lifecycle: { state: "testing" },
      must_also_fetch: ["last-checkpoint"],
    },
    {
      id: "cr-004",
      trigger_context: "retired-rule",
      trigger_keywords: ["retired"],
      confidence: 0.90,
      lifecycle: { state: "retired" }, // should be filtered
    },
  ];

  it("strict mode — keyword must match with word boundary", () => {
    const results = matchRules(rules, "config change", { mode: "strict", maxResults: 10 });
    // "config" matches "cr-001", "change" also matches
    expect(results.length).toBeGreaterThan(0);
    expect(results.some((r) => r.id === "cr-001")).toBe(true);
  });

  it("strict mode — context-only match does NOT fire", () => {
    const results = matchRules(rules, "modify settings", { mode: "strict", maxResults: 10 });
    // "settings" is context match for cr-001 but no keyword matches "settings"
    // keywords: config, setting, change — "setting" matches "setting" with word boundary
    expect(results.some((r) => r.id === "cr-001")).toBe(true);
  });

  it("auto mode — falls back to context coverage when keywords don't match", () => {
    // cr-003 context is "session-recovery"
    // query "recover session" has both words from "session recovery"
    const results = matchRules(rules, "recover session", { mode: "auto", maxResults: 10 });
    expect(results.some((r) => r.id === "cr-003")).toBe(true);
  });

  it("lenient mode — fuzzy fallback fires when nothing matched", () => {
    // No rule has "xyz" — lenient should still match by fuzzy word
    const results = matchRules(rules, "checkpoint", { mode: "lenient", maxResults: 10 });
    expect(results.some((r) => r.id === "cr-003")).toBe(true);
  });

  it("filters out retired lifecycle states", () => {
    const results = matchRules(rules, "retired", { mode: "strict", maxResults: 10 });
    expect(results.some((r) => r.id === "cr-004")).toBe(false);
  });

  it("filters out zero or negative confidence", () => {
    const negRules: CorrelationRule[] = [
      { id: "cr-neg", trigger_keywords: ["test"], confidence: 0, lifecycle: { state: "active" } },
      { id: "cr-pos", trigger_keywords: ["test"], confidence: 0.5, lifecycle: { state: "active" } },
    ];
    const results = matchRules(negRules, "test", { mode: "strict", maxResults: 10 });
    expect(results.map((r) => r.id)).toEqual(["cr-pos"]);
  });

  it("sorts by confidence descending, then id ascending", () => {
    const sortedRules: CorrelationRule[] = [
      { id: "cr-a", trigger_keywords: ["aaa"], confidence: 0.80, lifecycle: { state: "active" } },
      { id: "cr-b", trigger_keywords: ["aaa"], confidence: 0.95, lifecycle: { state: "active" } },
      { id: "cr-c", trigger_keywords: ["aaa"], confidence: 0.95, lifecycle: { state: "active" } },
    ];
    const results = matchRules(sortedRules, "aaa", { mode: "strict", maxResults: 10 });
    // cr-b and cr-c both 0.95, should be ordered by id: cr-b < cr-c
    expect(results[0].id).toBe("cr-b");
    expect(results[1].id).toBe("cr-c");
    expect(results[2].id).toBe("cr-a");
  });

  it("respects maxResults limit", () => {
    const manyRules: CorrelationRule[] = Array.from({ length: 20 }, (_, i) => ({
      id: `cr-${i}`,
      trigger_keywords: ["common"],
      confidence: 0.90 - i * 0.01,
      lifecycle: { state: "active" },
    }));
    const results = matchRules(manyRules, "common", { mode: "strict", maxResults: 5 });
    expect(results.length).toBe(5);
  });

  it("respects minConfidence threshold", () => {
    const results = matchRules(rules, "error", { mode: "strict", minConfidence: 0.90, maxResults: 10 });
    expect(results.map((r) => r.id)).toEqual(["cr-001"]); // only cr-001 has confidence >= 0.90
  });

  it("returns empty array when nothing matches", () => {
    const results = matchRules(rules, "xyz nonexistent", { mode: "strict", maxResults: 10 });
    expect(results).toEqual([]);
  });
});

// ── Numeric param safety tests ────────────────────────────────────────────────

describe("numeric param safety", () => {
  it("safeMaxResults: Math.max(1, Math.floor(max_results ?? 10))", () => {
    const clamp = (v: number | undefined) => Math.max(1, Math.floor(v ?? 10));
    expect(clamp(undefined)).toBe(10);
    expect(clamp(0)).toBe(1);
    expect(clamp(-5)).toBe(1);
    expect(clamp(3.7)).toBe(3);
    expect(clamp(100)).toBe(100);
  });

  it("safeMinConfidence: Math.min(1, Math.max(0, min_confidence ?? 0))", () => {
    const clamp = (v: number | undefined) => Math.min(1, Math.max(0, v ?? 0));
    expect(clamp(undefined)).toBe(0);
    expect(clamp(-0.5)).toBe(0);
    expect(clamp(1.5)).toBe(1);
    expect(clamp(0.5)).toBe(0.5);
  });
});


path: uninstall.sh
content: #!/bin/bash
# uninstall.sh — Uninstall correlation-memory plugin from OpenClaw
# Usage: ./uninstall.sh [--force]

set -euo pipefail

PLUGIN_ID="correlation-memory"
OPENCLAW_CONFIG="${OPENCLAW_CONFIG_PATH:-$HOME/.openclaw/openclaw.json}"
BACKUP_DIR="${OPENCLAW_CONFIG}.bak-$(date +%Y%m%d-%H%M%S)"
FORCE="${1:-}"

echo "=== correlation-memory Plugin Uninstaller ==="
echo "Plugin ID: $PLUGIN_ID"
echo "Config: $OPENCLAW_CONFIG"
echo ""

# Check if config exists
if [[ ! -f "$OPENCLAW_CONFIG" ]]; then
  echo "❌ Error: OpenClaw config not found at $OPENCLAW_CONFIG"
  echo "Is OpenClaw installed?"
  exit 1
fi

# Check if plugin is installed
if ! jq -e ".plugins.entries[\"$PLUGIN_ID\"]" "$OPENCLAW_CONFIG" > /dev/null 2>&1; then
  echo "⚠️  Plugin '$PLUGIN_ID' not found in config"
  echo "It may not be installed, or installed under a different ID"
  echo ""
  echo "Installed plugins:"
  jq -r '.plugins.entries | keys[]' "$OPENCLAW_CONFIG" 2>/dev/null || echo "(none or plugins.entries not found)"
  echo ""
  read -p "Continue anyway? [y/N] " -n 1 -r
  echo
  if [[ ! $REPLY =~ ^[Yy]$ ]]; then
    echo "Aborted."
    exit 0
  fi
fi

# Backup config before modification
echo "📦 Backing up config to $BACKUP_DIR"
cp "$OPENCLAW_CONFIG" "$BACKUP_DIR"
echo "✓ Backup created"
echo ""

# Confirm uninstall
if [[ "$FORCE" != "--force" ]]; then
  echo "This will remove the $PLUGIN_ID plugin from your OpenClaw config."
  echo "Backup: $BACKUP_DIR"
  read -p "Confirm uninstall? [y/N] " -n 1 -r
  echo
  if [[ ! $REPLY =~ ^[Yy]$ ]]; then
    echo "Aborted."
    exit 0
  fi
else
  echo "⚠️  --force flag set — skipping confirmation"
fi

# Remove plugin entry
echo "🗑️  Removing plugin from openclaw.json..."
if jq --arg id "$PLUGIN_ID" 'del(.plugins.entries[$id])' "$OPENCLAW_CONFIG" > "${OPENCLAW_CONFIG}.tmp"; then
  mv "${OPENCLAW_CONFIG}.tmp" "$OPENCLAW_CONFIG"
  echo "✓ Plugin removed from config"
else
  echo "❌ Error: Failed to remove plugin from config"
  echo "Backup restored from $BACKUP_DIR"
  cp "$BACKUP_DIR" "$OPENCLAW_CONFIG"
  exit 1
fi

# Verify removal
if jq -e ".plugins.entries[\"$PLUGIN_ID\"]" "$OPENCLAW_CONFIG" > /dev/null 2>&1; then
  echo "❌ Error: Plugin still exists in config after removal"
  echo "Backup restored from $BACKUP_DIR"
  cp "$BACKUP_DIR" "$OPENCLAW_CONFIG"
  exit 1
fi

echo "✓ Verification passed — plugin not in config"
echo ""

# Clean up temporary files
rm -f "${OPENCLAW_CONFIG}.tmp"

echo "=== Uninstall Complete ==="
echo ""
echo "Plugin '$PLUGIN_ID' has been uninstalled."
echo ""
echo "Backup location:"
echo "  - Config backup: $BACKUP_DIR"
echo ""
echo "Next steps:"
echo "  1. Restart OpenClaw gateway (if running):"
echo "     openclaw gateway restart"
echo "  2. Verify plugin is gone:"
echo "     openclaw plugins list"


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)