Skill: oblio-falootin/sql-memory (version 2.2.0)

--- name: sql-memory description: "Semantic memory layer for OpenClaw agents. Use when: (1) persisting agent memories with importance scoring, (2) hierarchical memory rollups (daily→weekly→monthly→yearly), (3) queuing tasks for agents, (4) logging activity and audit trails, (5) managing knowledge bases with semantic search. Provides remember/recall/search/queue_task/log_event APIs. Built on sql-connector for reliable parameterized SQL execution." --- # SQL Memory Skill > Semantic memory layer for OpenClaw agents ## Overview Provides agent-friendly memory operations: remember, recall, search, forget, plus task queue management, knowledge indexing, activity logging, and hierarchical memory rollups. All operations go through the SQL Connector skill for reliable, parameterized SQL execution. See `scripts/sql_memory.py` for full implementation. ## Dependencies - **sql-connector** — provides the underlying database connection and query execution ## Quick Start ```python from sql_memory import SQLMemory, get_memory mem = get_memory('cloud') # Remember something mem.remember('facts', 'vex_timezone', 'VeX is in EST/EDT timezone', importance=7) # Recall it entry = mem.recall('facts', 'vex_timezone') # Search across all memories results = mem.search_memories('timezone') # Queue a task mem.queue_task('nlp_agent', 'analyze_document', '{"doc": "..."}', priority=3) # Log an event mem.log_event('training_complete', 'nlp_agent', 'Finished training cycle 42') # Store knowledge mem.store_knowledge('stamps', 'inverted_jenny', 'Rare 1918 misprint...', 'catalog') ``` ## Schema All tables live in the `memory` schema (SQL Server database): | Table | Purpose | |-------|---------| | `memory.Memories` | Long-term curated memories with importance scoring | | `memory.TaskQueue` | Task queue for agent work items | | `memory.ActivityLog` | Event/activity logging for audit trail | | `memory.KnowledgeIndex` | Domain-specific knowledge store | | `memory.Sessions` | Session tracking for agents | ## Memory Rollups Hierarchical consolidation keeps memories fresh and relevant: ``` Daily memories → Weekly rollup (Sundays 3AM) Weekly rollups → Monthly rollup (1st of month) Monthly → Quarterly (Jan/Apr/Jul/Oct) Quarterly → Yearly (Jan 1st) ``` Each rollup: 1. Summarizes source entries 2. Creates a consolidated entry with back-references 3. Reduces importance of source entries 4. Tags sources as `rolled_up` ### Importance Scale | Level | Meaning | Example | |-------|---------|---------| | 1-2 | Ephemeral, archive | Old workspace file | | 3-4 | Context, nice-to-know | Debug notes | | 5-6 | Standard operational | Task completion | | 7-8 | Important milestone | Architecture decision | | 9 | Critical | System design choice | | 10 | Permanent | Core identity/values | ## API Reference ### Memory Operations | Method | Description | Example | |--------|-------------|---------| | `remember(cat, key, content, importance, tags)` | Store a memory | `mem.remember('facts', 'name', 'Oblio', 7)` | | `recall(cat, key)` | Retrieve a memory | `mem.recall('facts', 'name')` | | `search_memories(query, limit)` | Semantic search | `mem.search_memories('timezone', limit=5)` | | `forget(cat, key)` | Delete a memory | `mem.forget('facts', 'name')` | ### Task Queue | Method | Description | |--------|-------------| | `queue_task(agent, type, payload, priority)` | Add a task | | `claim_task(id)` | Mark task as processing | | `complete_task(id, result)` | Mark task as completed | | `fail_task(id, error, retries, max)` | Fail with retry logic | ### Activity Logging | Method | Description | |--------|-------------| | `log_event(type, agent, detail, extra)` | Log an activity | | `get_recent_activity(hours, agent)` | Query recent events | ## Configuration Uses the same environment variables as sql-connector: ``` SQL_CLOUD_SERVER=sql5112.site4now.net SQL_CLOUD_DATABASE=db_99ba1f_memory4oblio SQL_CLOUD_USER=... SQL_CLOUD_PASSWORD=... SQL_LOCAL_SERVER=10.0.0.110 SQL_LOCAL_DATABASE=Oblio_Memories SQL_LOCAL_USER=sa SQL_LOCAL_PASSWORD=... ``` ## Architecture ``` ┌──────────────────┐ │ Agents │ ← OblioAgent subclasses ├──────────────────┤ │ SQLMemory │ ← Semantic operations (remember/recall/queue/log) ├──────────────────┤ │ SQLConnector │ ← Generic SQL execution (retry, parameterized, logging) ├──────────────────┤ │ pymssql (TDS) │ ← Native SQL Server driver └──────────────────┘ ``` ## License MIT

path: GETTING_STARTED.md
content: # GETTING STARTED: SQL Memory

**For:** OpenClaw agents that need persistent, semantically-meaningful memory  
**Time Required:** 15 minutes setup + 5 minutes validation  
**Prerequisites:** OpenClaw installed, SQL Server (cloud or local), sql-connector installed

---

## ⚠️ Install sql-connector First

SQL Memory **requires** the SQL Connector skill as its transport layer. If you skip this, nothing works.

```bash
clawhub install sql-connector   # ← MUST DO THIS FIRST
clawhub install sql-memory
```

If you haven't set up sql-connector yet, start here:  
**[clawbot-sql-connector GETTING_STARTED.md](https://github.com/VeXHarbinger/clawbot-sql-connector/blob/main/GETTING_STARTED.md)**

---

## Overview: Where SQL Memory Fits

```
┌──────────────────────────────────────────────────────────────────┐
│                                                                  │
│   Your OpenClaw Agents                                           │
│   ↓                                                              │
│   ┌───────────────────────────────────────────────────────────┐  │
│   │  SQL Memory  ← YOU ARE HERE                               │  │
│   │  ├─ remember() / recall() / search_memories()            │  │
│   │  ├─ queue_task() / claim_task() / complete_task()        │  │
│   │  ├─ log_event() / get_recent_activity()                  │  │
│   │  ├─ store_knowledge() / search_knowledge()               │  │
│   │  ├─ add_todo() / complete_todo()                         │  │
│   │  └─ Daily → Weekly → Monthly → Yearly rollups            │  │
│   └───────────────────────────────────────────────────────────┘  │
│   ↓                                                              │
│   ┌───────────────────────────────────────────────────────────┐  │
│   │  SQL Connector  (installed in Step 0 above)               │  │
│   │  └─ Handles credentials, retries, parameterized SQL      │  │
│   └───────────────────────────────────────────────────────────┘  │
│   ↓                                                              │
│   SQL Server (Cloud or Local)                                    │
│   └─ memory.Memories, TaskQueue, ActivityLog, Sessions,          │
│      KnowledgeIndex, Todos                                       │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘
                          ↓ (optional, next tier)
              SQL Dreamer — interprets memories into synthesis
```

**The story:**
- **SQL Connector** moves data reliably (transport layer)
- **SQL Memory** stores meaning and context (semantic layer) ← this skill
- **SQL Dreamer** interprets memories into insight (synthesis layer)

---

## Step 1: Verify sql-connector Is Working

Before installing sql-memory, confirm your foundation is solid:

```bash
python3 -c "from sql_connector import get_connector; print('✅ OK' if get_connector().ping() else '❌ FAILED')"
```

If this fails, fix sql-connector first. Check credentials in your `.env` file.

---

## Step 2: Install SQL Memory

```bash
clawhub install sql-memory
```

Verify the import:

```bash
python3 -c "from sql_memory import get_memory; print('✅ sql-memory imported')"
```

---

## Step 3: Configure .env

SQL Memory reads the same `.env` as sql-connector. No extra variables required — the same connection profiles you configured for sql-connector work here automatically.

Your `.env` should already have entries like:

```env
# Local SQL Server
SQL_LOCAL_SERVER=10.0.0.110
SQL_LOCAL_PORT=1433
SQL_LOCAL_DATABASE=db_99ba1f_memory4oblio
SQL_LOCAL_USER=your_username
SQL_LOCAL_PASSWORD=your_password

# Cloud SQL Server (Azure, site4now, etc.)
SQL_CLOUD_SERVER=yourserver.database.windows.net
SQL_CLOUD_PORT=1433
SQL_CLOUD_DATABASE=your_cloud_database
SQL_CLOUD_USER=your_cloud_user
SQL_CLOUD_PASSWORD=your_cloud_password
```

> **Note on database name:** Use **your own database name** here. The example above uses `db_99ba1f_memory4oblio` — that's a specific instance. Your database will have whatever name you created it with in SQL Server.

> **Note on `SQL_LOCAL_DATABASE=Oblio_Memories`:** If you see this value in any old config or skill file, it's a dead/legacy value. Use your actual database name.

---

## Step 4: Create the Schema

SQL Memory needs 6 tables inside the `memory` schema. Run this once before first use:

### Option A — Automated Setup (Recommended)

```bash
# Navigate to where sql-memory installed
pip show clawbot-sql-memory | grep Location
# Example output: Location: /home/youruser/.venv/lib/python3.11/site-packages

cd /path/from/above/clawbot_sql_memory

# Create schema with default (local) profile
python3 setup_schema.py

# Or use cloud profile
python3 setup_schema.py --cloud
```

Expected output:
```
clawbot-sql-memory schema setup — profile: local

✅ Connected to SQL Server

  CREATE memory schema
  CREATE memory.Memories
  CREATE memory.TaskQueue
  CREATE memory.ActivityLog
  CREATE memory.Sessions
  CREATE memory.KnowledgeIndex
  CREATE memory.Todos

Schema setup complete.

Next step: configure your .env and run:
  python3 -c "from sql_memory import get_memory; print(get_memory('local').ping())"
```

If tables already exist, the script will print `SKIP` instead of `CREATE` — safe to re-run.

### Option B — Manual SQL

If the setup script fails or you prefer to manage schema yourself, run this in SSMS, Azure Data Studio, or `sqlcmd`:

```sql
CREATE SCHEMA memory;
GO

CREATE TABLE memory.Memories (
    id          INT IDENTITY(1,1) PRIMARY KEY,
    category    NVARCHAR(100)  NOT NULL,
    [key]       NVARCHAR(255)  NOT NULL,
    content     NVARCHAR(MAX)  NOT NULL,
    importance  INT            DEFAULT 3,
    tags        NVARCHAR(500)  DEFAULT '',
    status      NVARCHAR(50)   DEFAULT 'active',
    created_at  DATETIME2      DEFAULT GETUTCDATE(),
    updated_at  DATETIME2      DEFAULT GETUTCDATE()
);

CREATE TABLE memory.TaskQueue (
    id           INT IDENTITY(1,1) PRIMARY KEY,
    agent        NVARCHAR(100)  NOT NULL,
    task_type    NVARCHAR(100)  NOT NULL,
    payload      NVARCHAR(MAX)  DEFAULT '',
    priority     INT            DEFAULT 5,
    status       NVARCHAR(50)   DEFAULT 'pending',
    retries      INT            DEFAULT 0,
    model_hint   NVARCHAR(100)  DEFAULT '',
    created_at   DATETIME2      DEFAULT GETUTCDATE(),
    updated_at   DATETIME2      DEFAULT GETUTCDATE(),
    claimed_at   DATETIME2      NULL,
    completed_at DATETIME2      NULL,
    error        NVARCHAR(MAX)  DEFAULT ''
);

CREATE TABLE memory.ActivityLog (
    id          INT IDENTITY(1,1) PRIMARY KEY,
    event_type  NVARCHAR(100)  NOT NULL,
    agent       NVARCHAR(100)  DEFAULT '',
    description NVARCHAR(MAX)  DEFAULT '',
    metadata    NVARCHAR(MAX)  DEFAULT '',
    importance  INT            DEFAULT 3,
    created_at  DATETIME2      DEFAULT GETUTCDATE()
);

CREATE TABLE memory.Sessions (
    id          INT IDENTITY(1,1) PRIMARY KEY,
    session_key NVARCHAR(255)  NOT NULL,
    agent       NVARCHAR(100)  DEFAULT '',
    status      NVARCHAR(50)   DEFAULT 'active',
    metadata    NVARCHAR(MAX)  DEFAULT '',
    started_at  DATETIME2      DEFAULT GETUTCDATE(),
    ended_at    DATETIME2      NULL
);

CREATE TABLE memory.KnowledgeIndex (
    id         INT IDENTITY(1,1) PRIMARY KEY,
    domain     NVARCHAR(100)  NOT NULL,
    [key]      NVARCHAR(255)  NOT NULL,
    content    NVARCHAR(MAX)  NOT NULL,
    source     NVARCHAR(255)  DEFAULT '',
    tags       NVARCHAR(500)  DEFAULT '',
    created_at DATETIME2      DEFAULT GETUTCDATE()
);

CREATE TABLE memory.Todos (
    id          INT IDENTITY(1,1) PRIMARY KEY,
    title       NVARCHAR(500)  NOT NULL,
    description NVARCHAR(MAX)  DEFAULT '',
    priority    INT            DEFAULT 3,
    status      NVARCHAR(50)   DEFAULT 'open',
    tags        NVARCHAR(500)  DEFAULT '',
    created_at  DATETIME2      DEFAULT GETUTCDATE(),
    updated_at  DATETIME2      DEFAULT GETUTCDATE(),
    closed_at   DATETIME2      NULL
);
GO
```

---

## Step 5: Validate — Quick Tests

Run these one by one to confirm everything is wired up correctly.

### Test 1: Connectivity

```python
from sql_memory import get_memory

mem = get_memory('local')   # or 'cloud'
result = mem.ping()
print("✅ Connected" if result else "❌ Connection failed")
```

### Test 2: Store and Retrieve a Memory

```python
from sql_memory import get_memory

mem = get_memory('local')

# Store a memory with importance=7 (strategic-level)
mem.remember(
    category='test',
    key='first_memory',
    content='sql-memory is working correctly',
    importance=7,
    tags='test,validation'
)

# Retrieve it by category + key
content = mem.recall('test', 'first_memory')
print(f"✅ Recalled: {content}" if content else "❌ Recall failed")
```

### Test 3: Search

```python
from sql_memory import get_memory

mem = get_memory('local')
results = mem.search_memories('working correctly')
print(f"✅ Found {len(results)} results" if results else "❌ Search returned nothing")
for r in results:
    print(f"  [{r['category']}] {r['key_name']}: {r['content'][:60]}")
```

### Test 4: Log an Event

```python
from sql_memory import get_memory

mem = get_memory('local')
ok = mem.log_event(
    event_type='setup_complete',
    agent='setup',
    description='sql-memory schema and connection validated',
    importance=5
)
print("✅ Event logged" if ok else "❌ Log failed")
```

### Test 5: Task Queue

```python
from sql_memory import get_memory

mem = get_memory('local')

# Queue a task
task_id = mem.queue_task(
    agent='my_agent',
    task_type='process_data',
    payload='{"source": "test"}',
    priority=5
)
print(f"✅ Task queued: {task_id}" if task_id else "❌ Queue failed")

# Fetch pending tasks
tasks = mem.get_pending_tasks(agent='my_agent', task_types=['process_data'])
print(f"  Found {len(tasks)} pending tasks")

# Complete it
if task_id:
    mem.complete_task(task_id, result='completed in test')
    print("✅ Task completed")
```

### Full Self-Test (Built In)

The module ships with a self-test runner:

```bash
python3 -m sql_memory   # runs all self-tests against 'local' backend
```

---

## What You Now Have

After completing setup, your SQL Server database has:

| Table | Purpose |
|-------|---------|
| `memory.Memories` | Persistent facts with importance scoring and search |
| `memory.TaskQueue` | Agent work items with priority + retry logic |
| `memory.ActivityLog` | Immutable audit trail for all agent actions |
| `memory.Sessions` | Agent session state and context persistence |
| `memory.KnowledgeIndex` | Domain-specific knowledge, indexed by domain + topic |
| `memory.Todos` | Structured todo/task tracking with projects and priorities |

---

## Typical Usage Pattern

Here's how a real agent integrates sql-memory:

```python
from sql_memory import get_memory

class MyAgent:
    def __init__(self):
        self.mem = get_memory('cloud')  # or 'local'
    
    def process_finding(self, key, content, importance):
        # Store it
        self.mem.remember('findings', key, content, importance=importance)
        
        # Log the event
        self.mem.log_event('finding_stored', 'my_agent', f'Stored: {key}', importance=3)
    
    def get_context(self, keyword):
        # Search across everything
        return self.mem.search_memories(keyword, limit=10)
    
    def queue_followup(self, task_type, data):
        task_id = self.mem.queue_task('downstream_agent', task_type, data, priority=3)
        return task_id
```

---

## Importance Scale — Quick Reference

When you call `remember()`, set `importance` based on how long this memory should live:

| Score | Tier | Examples |
|-------|------|---------|
| 1–2 | Ephemeral | Temporary debug notes, scratch calculations |
| 3–4 | Operational | Task logs, routine status updates |
| 5–6 | Significant | Task completions, research findings |
| 7–8 | Strategic | Architecture decisions, system changes |
| 9 | Critical | Security issues, blocking problems |
| 10 | Permanent | Core identity, values, foundational facts |

Default is `3` if you don't specify. For most agent output, use `5` or higher.

---

## Troubleshooting

### `ImportError: sql_connector.py not found`

sql-connector is not installed or not on the path.

```bash
clawhub install sql-connector   # install it
# Then verify:
python3 -c "from sql_connector import get_connector; print('OK')"
```

### Connection Fails (ping returns False)

1. Verify `.env` credentials are correct for your backend
2. Check SQL Server is running and accessible on port 1433
3. Test the connector layer directly:

```bash
python3 -c "from sql_connector import get_connector; print(get_connector('local').ping())"
```

4. For cloud SQL, verify your server hostname resolves:
```bash
ping your-server.database.windows.net
```

### `Invalid object name 'memory.Memories'`

Tables haven't been created yet. Run:

```bash
python3 setup_schema.py
# or for cloud:
python3 setup_schema.py --cloud
```

### `remember()` returns True but `recall()` returns None

This can happen if the MERGE query is matching on `category` + `key` but using a different column name than your schema uses. Verify your schema matches the expected layout by checking:

```python
from sql_connector import get_connector
db = get_connector('local')
cols = db.query("SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA='memory' AND TABLE_NAME='Memories'")
print([c['COLUMN_NAME'] for c in cols])
```

### Search Returns No Results

`search_memories()` does LIKE-based search (not full-text). Ensure your keyword appears in `content`, `tags`, or `key_name`. It's case-insensitive on SQL Server by default.

---

## Next Steps

You've got persistent memory running. Here's where to go next:

### 1. Read the Full Reference

**[SKILL_REFERENCE.md](./SKILL_REFERENCE.md)** — every method, every parameter, architecture deep-dive, rollup system details.

### 2. Install SQL Dreamer

SQL Dreamer reads your memories and synthesizes patterns, decisions, and long-term insights — the third tier of the stack.

```bash
clawhub install sql-dreamer
```

SQL Dreamer queries your `memory.Memories` table nightly (configurable), pulls entries with `importance >= 7`, and runs them through OpenClaw's native dreaming pipeline. The richer your memory store, the more meaningful the dream output.

### 3. Integrate With Your Agents

```python
from sql_memory import get_memory

mem = get_memory('cloud')

# Store a research finding
mem.remember('research', 'user_pref_timezone', 'User is in EST/EDT', importance=7, tags='user,prefs')

# Store a decision
mem.remember('decisions', 'arch_choice_db', 'Chose SQL Server for persistence — schema flexibility + parameterized safety', importance=8)

# Audit trail
mem.log_event('research_complete', 'research_agent', 'Processed 14 findings for ticket OB-42', importance=4)

# Pass work to next agent
mem.queue_task('impl_agent', 'implementation', '{"ticket": "OB-42"}', priority=3)
```

### 4. Explore the Rollup System

SQL Memory's rollup system compresses memory over time:
- Daily entries → rolled up weekly (every Sunday)
- Weekly → monthly (1st of each month)
- Monthly → yearly (January 1st)

This keeps your memory store manageable without losing historical context. See **[SKILL_REFERENCE.md → Rollup System](./SKILL_REFERENCE.md#rollup-system)** for details.

---

## Support & Resources

- **GitHub Issues:** https://github.com/VeXHarbinger/clawbot-sql-memory/issues
- **ClawHub Registry:** https://clawhub.ai/skills/sql-memory
- **sql-connector:** https://github.com/VeXHarbinger/clawbot-sql-connector
- **SQL Dreamer (next tier):** https://github.com/High-Falootin/openclaw-sql-dreamer

---

**Last updated:** 2026-04-28  
**Version:** 2.0 (alpha — API stable, community feedback period open)  
**Status:** Functional and in active production use.


path: README.md
content: # clawbot-sql-memory

> ⚠️ **ALPHA — Use at your own risk.** Functional and in active use, but API may change. We'll lock the API after 30 days of community feedback. Open issues freely — this improves with use.

SQL Server-based persistent memory for OpenClaw agents. Provides semantic memory, task queuing, activity logging, todo management, and hierarchical rollups (daily → weekly → monthly → yearly).

---

## ⚠️ DEPENDENCY PRIORITY

**This skill requires `sql-connector` to be installed first.**

```
sql-connector  ←  Install this FIRST
      ↓
sql-memory     ←  You are here
      ↓
sql-dreamer    ←  Install this LAST (optional — uses memories for dream analysis)
```

```bash
# Always install in this order:
clawhub install sql-connector   # transport layer — required
clawhub install sql-memory      # this skill
clawhub install sql-dreamer     # optional — dream/analysis layer
```

If `sql-connector` is not installed, this skill will fail at import with:
`ImportError: sql_connector.py not found. Install the sql-connector skill first.`

---

## Requirements

- SQL Server 2019+ (or Azure SQL, site4now, etc.)
- [clawbot-sql-connector](https://github.com/High-Falootin/clawbot-sql-connector) — install first
- `pymssql` and `python-dotenv`

## Step 1: Create the Schema

Before installing this skill, create the required tables in your SQL Server database. You can do this one of two ways:

### Option A — Run the setup script (recommended)

```bash
python3 setup_schema.py
```

This will connect using your `.env` credentials and create all tables automatically. Run it once before first use.

### Option B — Manual SQL (paste into SSMS, Azure Data Studio, or sqlcmd)

```sql
-- Run against your target database
CREATE SCHEMA memory;
GO

CREATE TABLE memory.Memories (
    id          UNIQUEIDENTIFIER PRIMARY KEY DEFAULT newid(),
    category    NVARCHAR(100)  NOT NULL,
    key         NVARCHAR(255)  NOT NULL,
    content     NVARCHAR(MAX)  NOT NULL,
    importance  INT            DEFAULT 3,
    tags        NVARCHAR(500)  DEFAULT '',
    status      NVARCHAR(50)   DEFAULT 'active',
    created_at  DATETIME2      DEFAULT GETUTCDATE(),
    updated_at  DATETIME2      DEFAULT GETUTCDATE()
);

CREATE TABLE memory.TaskQueue (
    id          UNIQUEIDENTIFIER PRIMARY KEY DEFAULT newid(),
    agent       NVARCHAR(100)  NOT NULL,
    task_type   NVARCHAR(100)  NOT NULL,
    payload     NVARCHAR(MAX)  DEFAULT '',
    priority    INT            DEFAULT 5,
    status      NVARCHAR(50)   DEFAULT 'pending',
    retries     INT            DEFAULT 0,
    model_hint  NVARCHAR(100)  DEFAULT '',
    created_at  DATETIME2      DEFAULT GETUTCDATE(),
    updated_at  DATETIME2      DEFAULT GETUTCDATE(),
    claimed_at  DATETIME2      NULL,
    completed_at DATETIME2     NULL,
    error       NVARCHAR(MAX)  DEFAULT ''
);

CREATE TABLE memory.ActivityLog (
    id          UNIQUEIDENTIFIER PRIMARY KEY DEFAULT newid(),
    event_type  NVARCHAR(100)  NOT NULL,
    agent       NVARCHAR(100)  DEFAULT '',
    description NVARCHAR(MAX)  DEFAULT '',
    metadata    NVARCHAR(MAX)  DEFAULT '',
    importance  INT            DEFAULT 3,
    created_at  DATETIME2      DEFAULT GETUTCDATE()
);

CREATE TABLE memory.Sessions (
    id          UNIQUEIDENTIFIER PRIMARY KEY DEFAULT newid(),
    session_key NVARCHAR(255)  NOT NULL,
    agent       NVARCHAR(100)  DEFAULT '',
    status      NVARCHAR(50)   DEFAULT 'active',
    metadata    NVARCHAR(MAX)  DEFAULT '',
    started_at  DATETIME2      DEFAULT GETUTCDATE(),
    ended_at    DATETIME2      NULL
);

CREATE TABLE memory.KnowledgeIndex (
    id          UNIQUEIDENTIFIER PRIMARY KEY DEFAULT newid(),
    domain      NVARCHAR(100)  NOT NULL,
    key         NVARCHAR(255)  NOT NULL,
    content     NVARCHAR(MAX)  NOT NULL,
    source      NVARCHAR(255)  DEFAULT '',
    tags        NVARCHAR(500)  DEFAULT '',
    created_at  DATETIME2      DEFAULT GETUTCDATE()
);

CREATE TABLE memory.Todos (
    id          UNIQUEIDENTIFIER PRIMARY KEY DEFAULT newid(),
    title       NVARCHAR(500)  NOT NULL,
    description NVARCHAR(MAX)  DEFAULT '',
    priority    INT            DEFAULT 3,
    status      NVARCHAR(50)   DEFAULT 'open',
    tags        NVARCHAR(500)  DEFAULT '',
    created_at  DATETIME2      DEFAULT GETUTCDATE(),
    updated_at  DATETIME2      DEFAULT GETUTCDATE(),
    closed_at   DATETIME2      NULL
);
GO
```

## Step 2: Configure .env

Backend configuration uses a simple naming pattern. Any identifier works — not just `local` or `cloud`:

```env
# Local SQL Server
SQL_LOCAL_SERVER=10.0.0.110
SQL_LOCAL_PORT=1433
SQL_LOCAL_DATABASE=your_database_name
SQL_LOCAL_USER=your_user
SQL_LOCAL_PASSWORD=your_password

# Cloud SQL Server (Azure / site4now / etc.)
SQL_CLOUD_SERVER=yourserver.database.windows.net
SQL_CLOUD_PORT=1433
SQL_CLOUD_DATABASE=your_cloud_db
SQL_CLOUD_USER=your_cloud_user
SQL_CLOUD_PASSWORD=your_cloud_password

# Named backends — any identifier works:
# SQL_TAT_SERVER, SQL_TAT_DATABASE, etc. → get_memory('tat')
# SQL_HFTC_SERVER, SQL_HFTC_DATABASE, etc. → get_memory('hftc')
```

See [clawbot-sql-connector README](https://github.com/High-Falootin/clawbot-sql-connector#env-setup) for full backend naming docs.

## Step 3: Install

```bash
clawhub install sql-connector   # dependency — install first
clawhub install sql-memory
```

## Quick Start

```python
from sql_memory import SQLMemory, get_memory

mem = get_memory('local')   # or 'cloud', or any named backend

# Store a memory (importance 1-10: 3=routine, 7=strategic, 10=permanent)
mem.remember('facts', 'user_timezone', 'User is in EST/EDT', importance=7, tags='user,prefs')

# Recall it
entry = mem.recall('facts', 'user_timezone')
print(entry)  # → 'User is in EST/EDT'

# Search across all memories
results = mem.search_memories('timezone')

# Queue a task for an agent
task_id = mem.queue_task('my_agent', 'process_data', payload='{"source":"api"}', priority=3)

# Log an event
mem.log_event(event_type='task_started', agent='my_agent', description='Processing began')

# Todos
todo_id = mem.add_todo('Fix the login bug', priority=2, tags='bug,auth')
mem.complete_todo(todo_id)

# Connectivity check
mem.ping()  # → True
```

See [GETTING_STARTED.md](GETTING_STARTED.md) for full setup guide.
See [SKILL_REFERENCE.md](SKILL_REFERENCE.md) for complete API docs.

## API Reference

### Memory

| Method | Description |
|---|---|
| `remember(category, key, content, importance=3, tags='')` | Store or update a memory entry |
| `recall(category, key)` | Retrieve most recent active entry |
| `search_memories(query, limit=10)` | Full-text search across all memories |
| `get_recent(category, limit=10)` | Most recent entries in a category |
| `forget(category, key)` | Mark entry as inactive |

### Task Queue

| Method | Description |
|---|---|
| `queue_task(agent, task_type, payload='', priority=5, model_hint='')` | Add a task |
| `get_pending_tasks(agent=None, limit=10)` | Fetch pending tasks |
| `complete_task(task_id, result='')` | Mark task complete |
| `fail_task(task_id, error='')` | Mark task failed |

### Todos

| Method | Description |
|---|---|
| `add_todo(title, description='', priority=3, tags='')` | Create a todo |
| `complete_todo(todo_id)` | Mark complete |
| `update_todo(todo_id, **kwargs)` | Update fields |
| `delete_todo(todo_id)` | Hard delete |

### Activity Logging

| Method | Description |
|---|---|
| `log_event(event_type, agent='', description='', metadata='', importance=3)` | Write to ActivityLog |

## Memory Rollup Schedule

Hierarchical compression keeps long-term memory manageable:

```
Daily entries  → rolled up weekly   (every Sunday)
Weekly         → monthly            (1st of month)
Monthly        → yearly             (January 1st)
```

Each rollup preserves source references for traceability.

## Design Principles

- **UTC everywhere** — all timestamps use `GETUTCDATE()` in SQL, `datetime.now(timezone.utc)` in Python
- **Parameterized only** — no f-string SQL, ever; the connector layer enforces this
- **Importance scale** — 1–10: `3`=routine, `5`=significant, `7`=strategic, `10`=permanent facts

## Related

- [clawbot-sql-connector](https://github.com/High-Falootin/clawbot-sql-connector) — the transport layer this builds on (install first!)
- [openclaw-SQL-dreamer](https://github.com/High-Falootin/openclaw-SQL-dreamer) — the dream analysis layer built on top of sql-memory
- [clawhub.ai](https://clawhub.ai) — install with `clawhub install sql-memory`

## Community

Alpha software — your feedback shapes the v1 API. Open issues for broken installs, schema questions, or API suggestions.

## License

MIT


path: SKILL_REFERENCE.md
content: # SQL Memory — Skill Reference

**Version:** 2.0 (alpha)  
**Depends on:** [clawbot-sql-connector](https://github.com/High-Falootin/clawbot-sql-connector)  
**Next tier:** [openclaw-sql-dreamer](https://github.com/High-Falootin/openclaw-sql-dreamer)

---

## The Three-Tier Memory Stack

SQL Memory is the middle tier of the OpenClaw persistent memory architecture. Understanding the full stack helps you use each skill at the right level:

```
┌─────────────────────────────────────────────────────────────────────────┐
│                                                                         │
│    TIER 3 — SQL Dreamer (openclaw-sql-dreamer)                         │
│    ─────────────────────────────────────────────────────────────        │
│    "Interpretation"                                                     │
│    • Reads high-importance memories nightly                             │
│    • Runs OpenClaw's native dreaming pipeline                           │
│    • Synthesizes patterns, decisions, lasting truths                    │
│    • Stores dream results back to SQL                                   │
│    • Optional: publishes to Confluence                                  │
│                                                                         │
│    TIER 2 — SQL Memory (clawbot-sql-memory)   ← THIS SKILL             │
│    ─────────────────────────────────────────────────────────────        │
│    "Meaning"                                                            │
│    • Stores facts with importance scores (1–10)                         │
│    • Provides semantic search across all stored memories                │
│    • Manages task queues for inter-agent coordination                   │
│    • Maintains immutable activity/audit logs                            │
│    • Tracks todos and agent session state                               │
│    • Hierarchical rollups: daily→weekly→monthly→yearly                  │
│                                                                         │
│    TIER 1 — SQL Connector (clawbot-sql-connector)                      │
│    ─────────────────────────────────────────────────────────────        │
│    "Transport"                                                          │
│    • Parameterized SQL execution (injection-safe, sealed by metaclass)  │
│    • Multi-backend (local, cloud, any named profile)                    │
│    • Retry + exponential backoff on transient failures                  │
│    • All query/execute/scalar primitives                                │
│                                                                         │
│    Foundation: pymssql (native TDS driver) + python-dotenv             │
│    SQL Server: 2019+ / Azure SQL / site4now                            │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘
```

**Why this split exists:** SQL Connector handles *how* to talk to SQL Server; SQL Memory handles *what* to store and *how to find it*. SQL Dreamer handles *what it means*. Each tier is independently testable and replaceable.

---

## Data Model

All tables live in the `memory` schema. All timestamps use UTC (`GETUTCDATE()` in SQL, `datetime.now(timezone.utc)` in Python).

### memory.Memories

The primary long-term memory store. Everything your agents learn, decide, observe, or record.

| Column | Type | Default | Notes |
|--------|------|---------|-------|
| `id` | INT IDENTITY | auto | Primary key |
| `category` | NVARCHAR(100) | required | Logical group (e.g. `facts`, `decisions`, `incidents`) |
| `key` | NVARCHAR(255) | required | Unique identifier within category (upsert key) |
| `content` | NVARCHAR(MAX) | required | The memory text |
| `importance` | INT | 3 | 1–10 importance score (see scale below) |
| `tags` | NVARCHAR(500) | `''` | Comma-separated tags for filtering and search |
| `status` | NVARCHAR(50) | `'active'` | `active`, `inactive`, `rolled_up` |
| `created_at` | DATETIME2 | `GETUTCDATE()` | Insertion time (UTC) |
| `updated_at` | DATETIME2 | `GETUTCDATE()` | Last update time (UTC) |

**Upsert behavior:** `remember()` uses a SQL `MERGE` statement — if a row with the same `category` + `key` already exists and is active, it updates the content, importance, and tags. Otherwise, it inserts a new row. You never get duplicates.

**Soft delete:** `forget()` sets `status = 'inactive'` rather than deleting rows. Queries filter on `status = 'active'`. Historical data is preserved.

---

### memory.TaskQueue

Asynchronous task coordination between agents. One agent queues work; another claims and processes it.

| Column | Type | Default | Notes |
|--------|------|---------|-------|
| `id` | INT IDENTITY | auto | Primary key |
| `agent` | NVARCHAR(100) | required | Target agent name |
| `task_type` | NVARCHAR(100) | required | Task class/type (agent-defined) |
| `payload` | NVARCHAR(MAX) | `''` | JSON payload for the task |
| `priority` | INT | 5 | 1 = critical, 9 = background |
| `status` | NVARCHAR(50) | `'pending'` | `pending`, `processing`, `completed`, `failed` |
| `retries` | INT | 0 | Retry counter |
| `model_hint` | NVARCHAR(100) | `''` | Optional LLM hint for the consuming agent |
| `created_at` | DATETIME2 | `GETUTCDATE()` | Creation time (UTC) |
| `updated_at` | DATETIME2 | `GETUTCDATE()` | Last state change (UTC) |
| `claimed_at` | DATETIME2 | NULL | When an agent claimed this task |
| `completed_at` | DATETIME2 | NULL | When it completed or failed |
| `error` | NVARCHAR(MAX) | `''` | Error message on failure |

**Priority values (numeric):**

| Value | Name | Use For |
|-------|------|---------|
| 1–2 | Critical/High | Blocking work, urgent processing |
| 5 | Medium (default) | Standard agent tasks |
| 7–9 | Low/Background | Non-blocking, best-effort |

`queue_task()` also accepts string priority names: `'critical'`, `'high'`, `'medium'`, `'low'`, `'free'`.

---

### memory.ActivityLog

Immutable append-only event log. Every significant agent action should be recorded here.

| Column | Type | Default | Notes |
|--------|------|---------|-------|
| `id` | INT IDENTITY | auto | Primary key |
| `event_type` | NVARCHAR(100) | required | What happened (e.g. `task_complete`, `error`) |
| `agent` | NVARCHAR(100) | `''` | Which agent logged this |
| `description` | NVARCHAR(MAX) | `''` | Human-readable detail |
| `metadata` | NVARCHAR(MAX) | `''` | JSON or structured extra data |
| `importance` | INT | 3 | Same 1–10 scale as Memories |
| `created_at` | DATETIME2 | `GETUTCDATE()` | Immutable log time (UTC) |

**Key principle:** ActivityLog rows are never updated or deleted. This is your audit trail.

---

### memory.Sessions

Agent session state and context persistence across interactions.

| Column | Type | Default | Notes |
|--------|------|---------|-------|
| `id` | INT IDENTITY | auto | Primary key |
| `session_key` | NVARCHAR(255) | required | Unique session identifier (upsert key) |
| `agent` | NVARCHAR(100) | `''` | Agent owning this session |
| `status` | NVARCHAR(50) | `'active'` | `active`, `closed` |
| `metadata` | NVARCHAR(MAX) | `''` | JSON session context |
| `started_at` | DATETIME2 | `GETUTCDATE()` | Session start (UTC) |
| `ended_at` | DATETIME2 | NULL | Session close time |

**Use case:** Store conversation state, token counts, mid-session context, or cross-call agent state that needs to survive restarts.

---

### memory.KnowledgeIndex

Domain-specific knowledge store. Separate from agent memories — use this for trained/indexed reference material.

| Column | Type | Default | Notes |
|--------|------|---------|-------|
| `id` | INT IDENTITY | auto | Primary key |
| `domain` | NVARCHAR(100) | required | Knowledge domain (e.g. `stamps`, `legal`, `inventory`) |
| `key` | NVARCHAR(255) | required | Topic identifier (upsert key within domain) |
| `content` | NVARCHAR(MAX) | required | The knowledge content |
| `source` | NVARCHAR(255) | `''` | Origin/citation |
| `tags` | NVARCHAR(500) | `''` | Comma-separated tags |
| `created_at` | DATETIME2 | `GETUTCDATE()` | Creation time (UTC) |

**Difference from Memories:** KnowledgeIndex stores *reference material* (documents, domain facts, indexed content). Memories stores *agent experiences* (what happened, decisions made, findings). Use both.

---

### memory.Todos

Structured task tracking. Supports priorities, projects, due dates, and status tracking.

| Column | Type | Default | Notes |
|--------|------|---------|-------|
| `id` | INT IDENTITY | auto | Primary key (returned by `add_todo()`) |
| `title` | NVARCHAR(500) | required | Todo item text |
| `description` | NVARCHAR(MAX) | `''` | Extended notes |
| `priority` | INT | 3 | 1 = urgent, 5 = normal, 9 = low |
| `status` | NVARCHAR(50) | `'open'` | `open`, `done`, `cancelled`, custom |
| `tags` | NVARCHAR(500) | `''` | Comma-separated tags |
| `created_at` | DATETIME2 | `GETUTCDATE()` | Creation time (UTC) |
| `updated_at` | DATETIME2 | `GETUTCDATE()` | Last update time (UTC) |
| `closed_at` | DATETIME2 | NULL | When item was completed/closed |

---

## Importance Scale

The importance score is used across `Memories` and `ActivityLog`. It determines:
- Which memories are promoted during rollups
- Which memories SQL Dreamer reads for nightly synthesis
- How long memories survive compression cycles

| Score | Tier | Meaning | Examples |
|-------|------|---------|---------|
| 1 | Ephemeral | Discard soon | Temporary scratch notes, debug output |
| 2 | Transient | Short-lived utility | One-off lookup results |
| 3 | Routine | Default context (default value) | Task logs, status messages |
| 4 | Context | Nice-to-know | Debug traces, minor observations |
| 5 | Significant | Standard operational fact | Task completions, research notes |
| 6 | Noteworthy | Worth preserving | Patterns observed, useful references |
| 7 | Strategic | Milestone-level | Architecture decisions, system changes |
| 8 | Important | High-value | Process improvements, key findings |
| 9 | Critical | Near-permanent | Blocking issues, security decisions |
| 10 | Permanent | Never roll up | Core identity, foundational truths |

**Practical guide:**
- Agent task output → `5`
- User preference you observed → `7`
- Architectural decision → `7–8`
- Something that must never be forgotten → `9–10`
- Default (if unsure) → `3` (the default)

**SQL Dreamer threshold:** Dreamer reads memories with `importance >= 7` by default. If you want a memory to influence nightly synthesis, score it 7+.

---

## API Reference

### Factory Function

#### `get_memory(backend='local') → SQLMemory`

Singleton factory. Returns a cached `SQLMemory` instance per backend — safe to call from multiple modules.

```python
from sql_memory import get_memory

mem = get_memory('local')    # local SQL Server
mem = get_memory('cloud')    # cloud SQL Server (Azure, site4now, etc.)
```

The `backend` string maps to environment variable prefixes: `SQL_LOCAL_*` or `SQL_CLOUD_*` in your `.env`.

---

### Class: `SQLMemory`

```python
from sql_memory import SQLMemory

mem = SQLMemory('local')    # or 'cloud'
```

---

### Memory Operations

#### `remember(category, key, content, importance=3, tags='') → bool`

Store or update a memory. Upserts by `category` + `key` — safe to call repeatedly.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `category` | str | required | Logical group (`facts`, `decisions`, `incidents`, etc.) |
| `key` | str | required | Unique identifier within category |
| `content` | str | required | The memory text |
| `importance` | int | 3 | 1–10 importance score |
| `tags` | str | `''` | Comma-separated tags |

**Returns:** `True` on success, `False` on failure.

```python
# Store a decision
mem.remember(
    category='decisions',
    key='arch_db_choice',
    content='Chose SQL Server for persistence: schema flexibility + parameterized safety. Evaluated Postgres, rejected for hosting reasons.',
    importance=8,
    tags='architecture,database,decisions'
)

# Update the same entry later (importance and content updated)
mem.remember('decisions', 'arch_db_choice', 'Updated content', importance=9)
```

---

#### `recall(category, key) → Optional[str]`

Retrieve a specific memory's content by category and key.

**Returns:** Content string if found, `None` if not found or inactive.

```python
content = mem.recall('decisions', 'arch_db_choice')
if content:
    print(f"Found: {content}")
```

---

#### `recall_recent(n=10) → List[Dict]`

Return the N most recently updated active memories across all categories.

**Returns:** List of dicts with keys: `category`, `key_name`, `content`, `importance`, `tags`, `ts`.

```python
recent = mem.recall_recent(5)
for entry in recent:
    print(f"[{entry['importance']}] {entry['category']}/{entry['key_name']}: {entry['content'][:60]}")
```

---

#### `search_memories(keyword, limit=20) → List[Dict]`

Full-text LIKE search across `content`, `tags`, and `key_name`. Results ordered by importance descending, then recency.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `keyword` | str | required | Search term (case-insensitive) |
| `limit` | int | 20 | Maximum results |

**Returns:** List of dicts with keys: `category`, `key_name`, `content`, `importance`, `tags`.

```python
results = mem.search_memories('timezone', limit=5)
for r in results:
    print(f"[{r['importance']}] {r['category']}/{r['key_name']}")
```

---

#### `forget(category, key) → bool`

Soft-delete a memory (sets `status = 'inactive'`). The row is preserved in the database for historical reference; it just won't appear in queries.

```python
mem.forget('test', 'temporary_note')
```

---

### Activity Logging

#### `log_event(event_type, agent, description, metadata='', importance=3) → bool`

Append an event to the immutable activity log.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `event_type` | str | required | Event name (e.g. `task_complete`, `error`, `agent_started`) |
| `agent` | str | required | Agent that generated the event |
| `description` | str | required | Human-readable description |
| `metadata` | str | `''` | JSON or free-form extra data |
| `importance` | int | 3 | 1–10 importance score |

**Returns:** `True` on success.

```python
mem.log_event(
    event_type='research_complete',
    agent='research_agent',
    description='Processed 14 findings for ticket OB-42',
    metadata='{"ticket": "OB-42", "findings_count": 14}',
    importance=5
)
```

---

#### `get_recent_activity(since_hours=24, agent=None) → List[Dict]`

Retrieve recent activity log entries.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `since_hours` | int | 24 | Look back this many hours |
| `agent` | str | None | Filter by agent name (None = all agents) |

**Returns:** List of dicts with keys: `event_type`, `agent`, `description`, `ts`.

```python
# All activity in the last hour
recent = mem.get_recent_activity(since_hours=1)

# One agent's activity over 48 hours
agent_log = mem.get_recent_activity(since_hours=48, agent='research_agent')
```

---

### Task Queue

The task queue enables async coordination between agents. The producer queues work; the consumer claims, processes, and completes or fails it.

#### `queue_task(agent, task_type, payload='{}', priority=5) → Optional[str]`

Insert a new task into the queue.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `agent` | str | required | Target agent name |
| `task_type` | str | required | Task class string (agent-defined) |
| `payload` | str | `'{}'` | JSON payload |
| `priority` | int or str | 5 | Numeric 1–9, or `'critical'`, `'high'`, `'medium'`, `'low'`, `'free'` |

**Returns:** Task ID string if created, `None` on failure.

```python
task_id = mem.queue_task(
    agent='impl_agent',
    task_type='implementation',
    payload='{"ticket": "OB-42", "findings": "..."}',
    priority='high'   # or priority=2
)
```

---

#### `get_pending_tasks(agent, task_types, limit=10) → List[Dict]`

Fetch pending tasks for an agent, ordered by priority (low number first) then age (oldest first).

| Parameter | Type | Description |
|-----------|------|-------------|
| `agent` | str | Agent name |
| `task_types` | List[str] | Task type filter list |
| `limit` | int | Max results |

**Returns:** List of dicts with keys: `id`, `task_type`, `payload`, `priority`, `retry_count`.

```python
tasks = mem.get_pending_tasks(
    agent='impl_agent',
    task_types=['implementation', 'code_review'],
    limit=5
)
for task in tasks:
    print(f"Task {task['id']}: {task['task_type']} (priority={task['priority']})")
```

---

#### `claim_task(task_id) → bool`

Mark a task as `processing`. Call before starting work to prevent double-processing.

```python
mem.claim_task(task_id)
# ... do the work ...
```

---

#### `complete_task(task_id, result='') → bool`

Mark a task as `completed`.

```python
mem.complete_task(task_id, result='Implementation complete — see Jira comment')
```

---

#### `fail_task(task_id, error, retry_count=0, max_retries=3) → bool`

Fail a task. If `retry_count < max_retries`, the task is re-queued as `pending`. If retries are exhausted, it's marked `failed`.

```python
try:
    # ... do work ...
    mem.complete_task(task_id)
except Exception as e:
    mem.fail_task(task_id, error=str(e), retry_count=task['retry_count'])
```

---

#### `get_completed_tasks(since_hours=24, agent=None) → List[Dict]`

Retrieve recently completed or failed tasks for monitoring.

```python
done = mem.get_completed_tasks(since_hours=6, agent='impl_agent')
```

---

### Knowledge Index

Domain-specific reference material, separate from agent memories.

#### `store_knowledge(domain, topic, summary='', file_path='', tags='') → bool`

Store or update a knowledge entry. Upserts by `domain` + `topic`. Increments `training_count` on updates.

```python
mem.store_knowledge(
    domain='stamps',
    topic='inverted_jenny',
    summary='Rare 1918 misprint — airmail stamp with inverted biplane. ~100 known. Value: $500k–$2M',
    tags='rare,1918,error,biplane'
)
```

---

#### `search_knowledge(domain, keyword='') → List[Dict]`

Search knowledge within a domain.

```python
# All stamps knowledge
all_stamps = mem.search_knowledge(domain='stamps')

# Search within domain
rare = mem.search_knowledge(domain='stamps', keyword='rare')
```

---

#### `get_recent_knowledge(n=10) → List[Dict]`

Get the N most recently updated knowledge entries across all domains.

---

### Session Management

#### `get_session_context(session_id) → Optional[Dict]`

Load session context. Returns `None` if the session doesn't exist.

```python
ctx = mem.get_session_context('agent:main:telegram:direct:12345')
if ctx:
    data = ctx['context']   # deserialized dict from JSON
```

---

#### `save_session_context(session_id, context_data, channel='agent', token_count=0) → bool`

Persist session context. Upserts by `session_key`.

```python
mem.save_session_context(
    session_id='agent:main:telegram:direct:12345',
    context_data={'step': 3, 'ticket': 'OB-42', 'findings': [...]},
    channel='telegram',
    token_count=8420
)
```

---

### Todos

#### `add_todo(title, project='', priority=5, tags='', due_date=None) → Optional[int]`

Create a new todo item. Returns the new todo's integer ID.

```python
todo_id = mem.add_todo(
    title='Fix login redirect bug',
    project='TAT',
    priority=2,
    tags='bug,auth',
    due_date='2026-05-01'
)
```

---

#### `complete_todo(todo_id, status='done') → bool`

Mark a todo as done. Accepts custom status strings if needed (`'cancelled'`, `'deferred'`, etc.).

```python
mem.complete_todo(todo_id)
mem.complete_todo(todo_id, status='cancelled')
```

---

#### `update_todo(todo_id, **fields) → bool`

Update any combination of allowed fields: `title`, `project`, `priority`, `status`, `tags`, `due_date`.

```python
mem.update_todo(todo_id, priority=1, tags='bug,auth,urgent')
```

---

#### `delete_todo(todo_id) → bool`

Hard-delete a todo. Prefer `complete_todo()` when you want an audit trail.

---

### Utility

#### `ping() → bool`

Connectivity check. Delegates to the underlying SQLConnector.

```python
if not mem.ping():
    raise RuntimeError("Lost SQL connection")
```

---

#### `ensure_schema() → bool`

Create the memory schema and all tables if they don't exist. Idempotent — safe to call repeatedly.

```python
mem.ensure_schema()   # no-op if tables already exist
```

---

## Rollup System

The rollup system compresses old memories hierarchically to keep the memory store manageable over time — without losing historical context.

### The Compression Ladder

```
New memories land in:  memory.Memories (daily entries)
                           ↓ (every Sunday @ 3AM)
                       Weekly rollup entry
                           ↓ (1st of each month)
                       Monthly rollup entry
                           ↓ (January 1st)
                       Yearly rollup entry
```

### How Each Rollup Works

1. Source entries for the period are collected
2. A consolidated summary entry is created with back-references to source IDs
3. Source entries have their importance reduced
4. Sources are tagged `rolled_up` so they don't trigger re-processing
5. The rollup entry carries the highest importance of its sources

### Rollup Schedule

| Cycle | Trigger | Input | Output |
|-------|---------|-------|--------|
| Daily → Weekly | Every Sunday | Last 7 days of daily entries | 1 weekly summary |
| Weekly → Monthly | 1st of month | Last 4+ weeks of weekly entries | 1 monthly summary |
| Monthly → Yearly | January 1st | Last 12 months of monthly entries | 1 yearly summary |

### What Survives

Memories with `importance >= 9` are **never rolled up** — they're permanent. This is intentional: foundational facts and core identity should never be compressed away.

### Why This Matters for SQL Dreamer

SQL Dreamer queries `memory.Memories` for entries with `importance >= 7`. After rollups, the high-importance condensed summaries are the primary input to nightly dream synthesis. Your rollup quality directly affects dream quality.

---

## Integration With SQL Dreamer

SQL Dreamer is the third tier of the stack. It reads your memories and synthesizes patterns, decisions, and long-term insights through OpenClaw's native dreaming pipeline.

### Data Flow

```
memory.Memories (importance >= 7)
         ↓
  [pre_dream_sql_feed.py runs @ 3AM]
  Curates and formats memories for dreaming
         ↓
  OpenClaw Native Dreamer runs
  (light, REM, and deep phases)
         ↓
  [post_dream_archiver.py runs @ 4AM]
  Stores dream results back to SQL
  (dreams.DreamCorpus, DreamLight, DreamREM, DreamDeep tables)
         ↓
  Optional: confluence_dream_publisher.py
  Publishes dream reports to Confluence
```

### Setting Memories to Feed Dreams

Any memory with `importance >= 7` will be picked up by SQL Dreamer. Be intentional about what you score that high:

```python
# This WILL feed into nightly dream synthesis
mem.remember(
    category='decisions',
    key='arch_choice_cache_layer',
    content='Decided to use Redis for session cache rather than SQL. Reduces write load 60%.',
    importance=8,    # ← will feed dreams
    tags='architecture,redis,cache,performance'
)

# This WILL NOT (importance too low)
mem.remember(
    category='debug',
    key='temp_note',
    content='Remember to check port 5432 after deploy',
    importance=4,    # ← too low for dreamer
)
```

### Installing SQL Dreamer

```bash
clawhub install sql-dreamer
```

After installing, SQL Dreamer needs its own schema (`dreams.*` tables) and a `config/sql_dreamer.yml`. See the **[SQL Dreamer GETTING_STARTED.md](https://github.com/High-Falootin/openclaw-sql-dreamer/blob/main/GETTING_STARTED.md)** for full setup.

---

## Configuration Reference

SQL Memory uses the same environment variable pattern as sql-connector. Variables follow the format:
`SQL_<BACKEND>_<SETTING>` where `<BACKEND>` is your profile name in uppercase.

### Standard Profiles

```env
# ── Local SQL Server ────────────────────────────────────────────
SQL_LOCAL_SERVER=10.0.0.110
SQL_LOCAL_PORT=1433
SQL_LOCAL_DATABASE=db_99ba1f_memory4oblio
SQL_LOCAL_USER=your_username
SQL_LOCAL_PASSWORD=your_password

# ── Cloud SQL Server (Azure, site4now, etc.) ────────────────────
SQL_CLOUD_SERVER=yourserver.database.windows.net
SQL_CLOUD_PORT=1433
SQL_CLOUD_DATABASE=your_cloud_database
SQL_CLOUD_USER=your_cloud_user
SQL_CLOUD_PASSWORD=your_cloud_password

# ── Optional: default backend ───────────────────────────────────
SQL_DEFAULT_BACKEND=local
```

> **Important:** Do not use `SQL_LOCAL_DATABASE=Oblio_Memories` — that was a legacy/dead value from an early config. Use your actual database name.

### Custom Backend Profiles

You can add arbitrary named profiles by following the same pattern:

```env
# Staging environment
SQL_STAGING_SERVER=staging.database.windows.net
SQL_STAGING_PORT=1433
SQL_STAGING_DATABASE=staging_db
SQL_STAGING_USER=staging_user
SQL_STAGING_PASSWORD=staging_pass
```

Then instantiate:

```python
mem = get_memory('staging')    # reads SQL_STAGING_* from .env
```

### .env Location Discovery

The module auto-discovers `.env` by walking up to 5 directory levels from the script's location. It will find `.env` at your workspace root as long as sql-memory is installed within your workspace directory tree.

---

## Design Principles

### UTC Everywhere

All timestamps use UTC — `GETUTCDATE()` in SQL, `datetime.now(timezone.utc)` in Python. No timezone ambiguity, no daylight savings bugs, no regional behavior differences.

### Parameterized Queries Only

No string interpolation in SQL. Ever. Every query uses `%s` placeholders. The SQLConnector metaclass seals this — subclasses cannot override it. This makes SQL injection structurally impossible, not just convention-dependent.

```python
# ✅ Correct (parameterized)
db.query("SELECT * FROM memory.Memories WHERE category = %s", (category,))

# ❌ Wrong (never do this, even in scripts)
db.query(f"SELECT * FROM memory.Memories WHERE category = '{category}'")
```

### Upsert Over Insert

`remember()` and `store_knowledge()` use `MERGE` statements. You never need to check if an entry exists before storing — just call `remember()`. This makes agent code simpler and prevents accidental duplicates.

### Soft Deletes

`forget()` sets `is_active = 0` rather than deleting rows. Historical data is preserved for audit, rollup, and dreamer queries. If you need hard deletes, query the SQL directly.

### Singleton Factory

`get_memory(backend)` returns a cached `SQLMemory` instance per backend. Multiple modules can call `get_memory('local')` and share one connection — no unnecessary reconnections.

```python
# Both calls return the same instance
mem1 = get_memory('local')
mem2 = get_memory('local')
assert mem1 is mem2   # True
```

### Logging

SQL Memory writes to `logs/sql_dbo.log` with UTC timestamps. All `remember`, `forget`, `queue_task`, `claim_task`, `complete_task`, and `fail_task` calls are logged at INFO level. Errors are logged at ERROR level.

---

## Common Patterns

### Pattern 1: Agent Memory Workflow

```python
from sql_memory import get_memory

class ResearchAgent:
    def __init__(self):
        self.mem = get_memory('cloud')
    
    def run_research(self, ticket_id, findings):
        # Log start
        self.mem.log_event('research_started', 'research_agent', f'Starting {ticket_id}', importance=3)
        
        # Store key findings
        for i, finding in enumerate(findings):
            self.mem.remember(
                category='findings',
                key=f'{ticket_id}_finding_{i}',
                content=finding['content'],
                importance=finding.get('importance', 5),
                tags=f'{ticket_id},research'
            )
        
        # Queue implementation task
        task_id = self.mem.queue_task(
            agent='impl_agent',
            task_type='implementation',
            payload=f'{{"ticket": "{ticket_id}", "findings_count": {len(findings)}}}',
            priority='high'
        )
        
        # Log completion
        self.mem.log_event(
            'research_complete', 
            'research_agent', 
            f'{ticket_id}: {len(findings)} findings stored, task {task_id} queued',
            importance=5
        )
```

### Pattern 2: Task Consumer Loop

```python
from sql_memory import get_memory
import json

mem = get_memory('local')

def process_tasks():
    tasks = mem.get_pending_tasks(
        agent='impl_agent',
        task_types=['implementation'],
        limit=10
    )
    
    for task in tasks:
        mem.claim_task(task['id'])
        try:
            payload = json.loads(task['payload'])
            # ... do the work ...
            mem.complete_task(task['id'], result='Success')
        except Exception as e:
            mem.fail_task(task['id'], str(e), retry_count=task['retry_count'])
```

### Pattern 3: Knowledge Base Agent

```python
from sql_memory import get_memory

mem = get_memory('local')

# Index a knowledge domain
for item in catalog_items:
    mem.store_knowledge(
        domain='product_catalog',
        topic=item['sku'],
        summary=item['description'],
        tags=f"{item['category']},{item['brand']}"
    )

# Query it
results = mem.search_knowledge('product_catalog', keyword='vintage')
```

---

## Legacy Compatibility (v1.x → v2.0)

If you have existing code using v1.x-style methods, they still work:

| v1.x Method | v2.0 Status | Notes |
|-------------|-------------|-------|
| `SQLMemory('local')` | ✅ Works | Same constructor |
| `get_memory('local')` | ✅ Works | Same factory |
| `mem.remember(...)` | ✅ Works | Same signature |
| `mem.recall(...)` | ✅ Works | Same return type |
| `mem.execute(raw_sql)` | ⚠️ Returns bool (was string) | Legacy passthrough; prefer `mem._db.execute()` with params |
| `mem.execute_scalar(sql)` | ✅ Works | Delegates to `scalar()` |
| `mem.execute_via_file(sql)` | ✅ Works | Same as `execute()` — file workaround no longer needed |
| `mem._parse_table(raw, cols)` | ⚠️ Returns `[]` always | v2 returns `List[Dict]` directly; migrate callers |

---

## Related Skills

| Skill | Role | Install |
|-------|------|---------|
| **sql-connector** | Transport layer (required) | `clawhub install sql-connector` |
| **sql-memory** | This skill | `clawhub install sql-memory` |
| **sql-dreamer** | Synthesis layer (optional, next) | `clawhub install sql-dreamer` |

---

## Support & Resources

- **GitHub Issues:** https://github.com/VeXHarbinger/clawbot-sql-memory/issues
- **ClawHub Registry:** https://clawhub.ai/skills/sql-memory
- **sql-connector:** https://github.com/VeXHarbinger/clawbot-sql-connector
- **SQL Dreamer:** https://github.com/High-Falootin/openclaw-sql-dreamer
- **Reference implementation:** https://github.com/VeXHarbinger/oblio-heart-and-soul

---

**Last updated:** 2026-04-28  
**Version:** 2.0 (alpha — feedback period open)  
**Status:** In active production use. API stable for 30-day community feedback window.


path: knowledge-base/INFRASTRUCTURE.md
content: # Infrastructure & Application Framework
_Oblio System Architecture — Documented 2026-03-09_

---

## Core Principles

### 1. Task Decomposition Pattern
**Never execute tasks blindly.** Understand the goal first, then break into recursive subtasks.

**Pattern:**
```
GOAL (macro understanding)
  ↓
SUBTASK 1 (macro+micro context) → Agent chooses decomposition level
SUBTASK 2 (macro+micro context) → Agent executes or breaks down further
SUBTASK 3 (macro+micro context) → Recursive until atomic
```

**Example:**
- GOAL: "Enable dashboard reporting"
- MACRO: Users need to see system state (reports, queue, logs)
- SUBTASKS:
  - Backend 1: Cache layer (SQL data + TTL)
  - Backend 2: REST endpoints (/api/report, /api/queue, /api/logs)
  - Frontend 1: Report view rendering
  - Frontend 2: Queue view rendering
  - Frontend 3: Logs view rendering
  - QA: Integration tests

**Benefits:**
- Agents see the big picture → smarter decisions
- Parallel execution → efficiency
- Recursive decomposition → handles complexity naturally
- No "what do I do next?" bottleneck

---

## Architecture Layers

### Layer 1: Data (SQL Memory)

**Primary Backend:** Cloud (SQL5112.site4now.net)
```
Database: db_99ba1f_memory4oblio
Schema: memory.*
```

**Tables:**
- `memory.Memories` — Facts, decisions, learnings (domain, key, content)
- `memory.Sessions` — Session snapshots for continuity
- `memory.TaskQueue` — Work queue (agent, task_type, status, priority)
- `memory.KnowledgeIndex` — Knowledge base (domain, topic, summary)
- `memory.ActivityLog` — Agent activity (agent, event_type, description)

**Module:** `infrastructure/sql_memory.py`
- Dual-backend support (local fallback, cloud primary)
- 15+ methods: remember, recall, queue_task, complete_task, log_event, etc.
- Automatic retry + graceful degradation

**Configuration:**
```python
# All agents default to cloud
db = SQLMemory('cloud')
```

---

### Layer 2: Agent Framework

**Base Class:** `infrastructure/agent_base.py` → `OblioAgent`

**Features:**
- SQL memory integration (auto-logging to ActivityLog)
- Model selection via model_router.py
- Structured logging (file + DB)
- Graceful error handling with retry (3 attempts default)
- Ollama inference (text, chat, vision, embeddings)
- Reporting framework (per-agent metrics)

**Standard Methods:**
```python
class MyAgent(OblioAgent):
    agent_name = "my_agent"
    task_types = ["my_task_type"]
    backend = "cloud"  # MUST be "cloud"
    
    def run_task(self, task: dict) -> str:
        # Run one task, return summary
        pass

# Usage
agent = MyAgent()
agent.run_once()  # Process all pending tasks once
agent.run_loop(interval=60)  # Loop continuously
```

**Logging:**
```python
# Automatic to logs/my_agent.log
self.log.info("message")

# Automatic to memory.ActivityLog via
self.log_activity("event_type", "description", "metadata")
```

---

### Layer 3: Task Queue & Dispatch

**Queue System:** `infrastructure/sql_memory.py` + `agents/agent_dispatcher.py`

**Task Structure:**
```json
{
  "id": 47,
  "agent": "backend",
  "task_type": "dashboard_cache_layer",
  "payload": {
    "macro": "Agents need reliable, cached access to SQL data",
    "component": "util/dashboard_cache.py",
    "methods": ["get_latest_report", "get_queue_status", "get_recent_logs"],
    "ttl_seconds": 30,
    "tests_required": true
  },
  "priority": 9,
  "status": "pending",
  "created_at": "2026-03-09T06:40:00Z"
}
```

**Task Lifecycle:**
1. Queue → Pending
2. Agent claims → Processing
3. Agent executes → Completed (with result) OR Failed (with error)

**Dispatcher:** Routes tasks by type to specialized agents or handles directly
- Handles: github_setup, github_clone, github_checkin, ui_fix, security_test
- Scheduled: Every 3 hours (cron: `0 */3 * * *`)

---

### Layer 4: AI Models

**Primary:** Ollama (local to DEAUS 10.0.0.110:11434)

**Available Models:**
- `gemma3:4b` — Text generation (default, free)
- `codellama:7b` — Code analysis + generation
- `mistral:7b` — General purpose LLM
- `llava` — Vision (images)
- `moondream` — Lightweight vision
- `nomic-embed-text` — Embeddings

**Model Router:** `infrastructure/model_router.py`
- Selects model by task_type + budget tier
- Routing logic: `select_model(task_type, budget, **kwargs)`
- Falls back to cloud APIs (with approval) if needed

**Usage:**
```python
# Agent automatically selects model
result = self.ollama_generate(prompt="...", model="gemma3:4b")

# Or let router choose
model = self.get_model(task_type="code_analysis", budget="free")
result = self.ollama_generate(prompt, model)
```

---

### Layer 5: Specialized Agents

**Current Agents:**

| Agent | Purpose | Cron | Type |
|-------|---------|------|------|
| `agent_stamps.py` | Stamp identification + cataloging | 0 */2 * * * | Background |
| `agent_facs.py` | FACS micro-expression training | 0 2 * * * | Background |
| `agent_nlp.py` | NLP document processing | 30 2 * * * | Background |
| `agent_security.py` | Security audits (11 checks) | 30 3 * * * | Background |
| `agent_dispatcher.py` | General task router | 0 */3 * * * | Background |
| `agent_idle.py` | Background task scheduling | */15 * * * * | Background |
| `agent_reporter.py` | Daily reports (4:20 AM/PM) | 20 4,16 * * * | Reporting |

---

## Development Standards

### Solid Design Paradigm

**Requirements:**
1. **Every function MUST have a unit test**
   - Write test first (TDD)
   - Test covers happy path + error cases
   - Never ship untested code

2. **No silent failures**
   - All errors logged with severity (INFO, WARN, ERROR, CRITICAL)
   - Errors surface in dashboard alerts
   - Retry logic for transient failures

3. **SOLID Principles**
   - Single Responsibility: One job per class/function
   - Open/Closed: Open for extension, closed for modification
   - Liskov Substitution: Agents interchangeable
   - Interface Segregation: Minimal required interfaces
   - Dependency Inversion: Depend on abstractions (sql_memory), not concrete

4. **DRY (Don't Repeat Yourself)**
   - Shared code → `infrastructure/` modules
   - Agents inherit from OblioAgent base class
   - Configuration in `.env` (not hardcoded)

5. **Composition over Inheritance**
   - Agents compose behaviors (memory, models, logging)
   - Not deep inheritance hierarchies
   - Mixins for orthogonal concerns

### GitHub Workflow

**Repositories:**
- `VeXHarbinger/AI-UI` — Dashboard application
- `VeXHarbinger/sequel-memory-skill` — SQL memory skill
- `VeXHarbinger/clawbot-sql-memory` — Memory infrastructure
- `Oblio-Falootin/*` — Infrastructure/tooling (not for sharing)

**Branch Strategy:**
```
main (stable, tagged releases)
  ↑
development (next release, PR-based)
  ↑
feature/* (individual features, merged via PR)
```

**PR Requirements:**
- [ ] Tests pass (100% of changed code)
- [ ] Code review approved
- [ ] CI pipeline green
- [ ] Documentation updated

**Initial Checkin Checklist:**
- [ ] README.md (what, how, why)
- [ ] ARCHITECTURE.md (design decisions, module breakdown)
- [ ] tests/README.md (how to run tests)
- [ ] tests/test_*.py (unit tests for all functions)
- [ ] .gitignore (Python: .pyc, __pycache__, .env, venv/)
- [ ] .github/workflows/test.yml (CI: run pytest on push)

---

## Cron Schedule

**Execution Times:**

```
*/5 min   → inbox_monitor (file watcher keepalive)
*/15 min  → agent_idle (background scheduling when CPU < 15%)
*/30 min  → task-monitor.sh (check for stuck tasks)

0 1 * * * → agent_github (GitHub monitoring)
15 1 * * * → agent_git_sync (repo sync)
0 2 * * * → agent_facs (FACS training)
30 2 * * * → agent_nlp (NLP training)
0 3 * * * → agent_lightsound (background task)
30 3 * * * → agent_security (security audit)
0 3,15 * * * → db_backup (SQL backup + rotate)
0 */2 * * * → agent_stamps (stamp processing)
20 4,16 * * * → agent_reporter (daily report)
0 */3 * * * → agent_dispatcher (task dispatch)
```

---

## Testing Framework

**Structure:**
```
tests/
  __init__.py
  conftest.py              # Pytest fixtures (mock DB, etc.)
  test_dispatcher.py       # Task dispatcher tests
  test_agent_base.py       # Base agent tests
  test_sql_memory.py       # Memory module tests
  test_model_router.py     # Model selection tests
  test_ui_endpoints.py     # Dashboard API tests
  test_dashboard_*.py      # UI component tests
```

**Requirements:**
- **Coverage:** ≥ 80% of production code
- **Test types:**
  - Unit: Test functions in isolation (mock dependencies)
  - Integration: Test components together
  - End-to-end: Test full workflows
- **Naming:** `test_<function>_<scenario>.py`
- **Assertions:** Clear, specific (not just `assertTrue`)

**Run Tests:**
```bash
python -m unittest discover tests/ -v
# or
python -m pytest tests/ -v --cov=.
```

---

## Quality Gates

**Before merging to main:**

1. [ ] All tests pass (100% of changed code)
2. [ ] No new warnings/errors in logs
3. [ ] Code review approved (peer or lead)
4. [ ] Documentation updated
5. [ ] Commit messages clear + descriptive

**Before deploying:**

1. [ ] Tag release (v0.1.0, v1.2.3, etc.)
2. [ ] Update CHANGELOG.md
3. [ ] CI pipeline green
4. [ ] Manual testing in staging
5. [ ] Rollback plan documented

---

## Monitoring & Debugging

**Logs Location:** `/home/oblio/.openclaw/workspace/logs/`
- `agent_*.log` — Per-agent logs
- `sql_memory.log` — SQL query log
- `dispatcher.log` — Task dispatcher
- `report-*.md` — Daily reports

**Memory Location:** SQL `memory.*` tables
- Query agent activity: `SELECT * FROM memory.ActivityLog ORDER BY timestamp DESC`
- Query task queue: `SELECT * FROM memory.TaskQueue WHERE status='pending'`
- Query memories: `SELECT * FROM memory.Memories WHERE domain='....'`

**Dashboard:** http://localhost:3000
- Report: Latest 4:20 AM/PM summary
- Queue: Pending + in-progress tasks
- Logs: Recent agent activity

---

## Deployment Checklist

**First Time Setup:**

```bash
# 1. Clone repo
git clone https://github.com/VeXHarbinger/clawbot-sql-memory.git

# 2. Install dependencies
pip install -r requirements.txt

# 3. Configure .env
cp .env.example .env
# Edit .env with cloud DB credentials

# 4. Run tests
python -m unittest discover tests/ -v

# 5. Start agents
crontab -e  # Add cron jobs
# or manually: python agents/agent_dispatcher.py
```

**Verification:**

```bash
# Check cloud DB connection
python -c "from sql_memory import SQLMemory; db = SQLMemory('cloud'); print(db.execute('SELECT COUNT(*) FROM memory.TaskQueue'))"

# Start UI
cd ui && npm start

# Monitor logs
tail -f logs/*.log
```

---

## Troubleshooting

**Problem:** "No pending tasks"
- Check: `SELECT * FROM memory.TaskQueue WHERE status='pending'`
- Check agent filter: `agent='dispatcher'` and task_type matches

**Problem:** "Cloud DB connection timeout"
- Check `.env` credentials
- Check firewall (site4now accessible from your network)
- Try local backend as fallback

**Problem:** "Ollama not responding"
- Check DEAUS is reachable: `curl http://10.0.0.110:11434`
- Check model available: `ollama list`
- Pull model: `ollama pull gemma3:4b`

**Problem:** "Tests failing with SQL errors"
- Mock DB in tests (don't hit cloud during tests)
- Use `infrastructure/agent_base.py` test fixtures
- Check `.env` not loaded in test environment

---

## Next Steps & Continuous Improvement

1. **Monitor queue** — Periodically check pending tasks
2. **Review logs** — Look for WARN/ERROR patterns
3. **Optimize bottlenecks** — Profile slow tasks
4. **Add tests** — If test coverage < 80%, add tests
5. **Refactor** — Clean up tech debt before new features
6. **Document decisions** — Update ARCHITECTURE.md as design evolves

---

_Last updated: 2026-03-09 | Framework version: 1.0_


path: knowledge-base/database_structure.md
content: # Database Structure Documentation

_Last Updated: 2026-03-10_

The database forms the backbone for system memory, task delegation, and knowledge retention. Here's an overview of its schema and usage principles:

## Core Principles
1. **Persistence:** All significant memories and logs are stored in the database to ensure they persist across sessions.
2. **Scalability:** Modular schemas support scaling for new agents and workflows.
3. **Maintainability:** Data is well-documented for ease of use and debugging.
4. **Longevity:** The database is designed to last "forever" as Oblio evolves.

## Schemas & Tables

### `memory` Schema
Holds all data related to knowledge retention and agent operations.

| **Table**            | **Purpose**                                                                 |
|----------------------|-----------------------------------------------------------------------------|
| `ActivityLog`        | Tracks agent activities, including logs of completed tasks and operations.  |
| `AgentState`         | Stores metadata about active agents, their readiness, and current status.  |
| `ContextSnapshot`    | Holds context dumps for session continuity or debugging.                   |
| `DecisionLog`        | Records decisions made by agents and their justification.                  |
| `KnowledgeIndex`     | Index of key knowledge and their associated domains.                       |
| `Memories`           | Central memory store for facts, patterns, and persistent insights.         |
| `PersonaLog`         | Tracks persona evolution, including changes in Oblio's personality.        |
| `Sessions`           | Metadata about chat and interaction history.                               |
| `SessionLog`         | Detailed logs of session-specific activity.                                |
| `TaskQueue`          | Contains queued, pending, processing, and completed tasks.                 |

### Usage Guidelines
1. Store **everything** meaningful—memories, tasks, logs—to ensure Oblio's long-term growth and reliability.
2. Never overwrite existing data unless explicitly stated (e.g., updates to task statuses).
3. Regularly audit schemas for optimization (e.g., indexes, stored procedures).

### Future Directions
- Add detailed stored procedures for transaction standardization (reduce ad-hoc queries).
- Integrate encrypted storage for sensitive data.
- Explore replication for redundancy and high availability.

<End of documentation.>


path: openclaw.skill.json
content: {
  "id": "sql-memory",
  "name": "sql-memory",
  "version": "2.0.0",
  "description": "Semantic memory layer for OpenClaw agents. Provides remember/recall/search with importance scoring, hierarchical rollups (daily→weekly→monthly→yearly), task queuing, activity logging, and todo management. Middle tier of the sql-connector → sql-memory → sql-dreamer stack. Requires sql-connector.",
  "author": "High-Falootin",
  "license": "MIT",
  "repository": "https://github.com/High-Falootin/clawbot-sql-memory",
  "homepage": "https://github.com/High-Falootin/clawbot-sql-memory",
  "keywords": [
    "openclaw",
    "sql",
    "memory",
    "database",
    "sql-memory",
    "clawhub",
    "semantic-memory",
    "agent-memory",
    "sql-server",
    "memory-rollup",
    "task-queue"
  ],
  "main": "sql-memory/sql_memory.py",
  "dependencies": {
    "sql-connector": ">=2.0.0"
  },
  "engines": {
    "openclaw": ">=1.0.0",
    "python": ">=3.10"
  },
  "skill": {
    "entrypoint": "sql-memory/sql_memory.py",
    "config_file": ".env",
    "setup_script": "setup_schema.py",
    "skill_file": "SKILL.md"
  },
  "tags": ["openclaw", "sql", "memory", "database", "sql-memory", "clawhub"],
  "category": "memory",
  "tier": 2,
  "stack": ["sql-connector", "sql-memory", "sql-dreamer"]
}


path: setup_schema.py
content: """
setup_schema.py — Create the memory.* schema for clawbot-sql-memory

Run once before first use:
    python3 setup_schema.py              # uses 'local' profile
    python3 setup_schema.py --cloud      # uses 'cloud' profile

Requires .env with SQL credentials. See README.md for format.
"""
import argparse
import sys
import os

sys.path.insert(0, os.path.dirname(__file__))

try:
    from sql_connector import get_connector
except ImportError:
    print("ERROR: sql-connector not found. Install it first:")
    print("  clawhub install sql-connector")
    sys.exit(1)

SCHEMA_SQL = [
    ("CREATE SCHEMA memory", "memory schema"),

    ("""
    CREATE TABLE memory.Memories (
        id          INT IDENTITY(1,1) PRIMARY KEY,
        category    NVARCHAR(100)  NOT NULL,
        [key]       NVARCHAR(255)  NOT NULL,
        content     NVARCHAR(MAX)  NOT NULL,
        importance  INT            DEFAULT 3,
        tags        NVARCHAR(500)  DEFAULT '',
        status      NVARCHAR(50)   DEFAULT 'active',
        created_at  DATETIME2      DEFAULT GETUTCDATE(),
        updated_at  DATETIME2      DEFAULT GETUTCDATE()
    )
    """, "memory.Memories"),

    ("""
    CREATE TABLE memory.TaskQueue (
        id           INT IDENTITY(1,1) PRIMARY KEY,
        agent        NVARCHAR(100)  NOT NULL,
        task_type    NVARCHAR(100)  NOT NULL,
        payload      NVARCHAR(MAX)  DEFAULT '',
        priority     INT            DEFAULT 5,
        status       NVARCHAR(50)   DEFAULT 'pending',
        retries      INT            DEFAULT 0,
        model_hint   NVARCHAR(100)  DEFAULT '',
        created_at   DATETIME2      DEFAULT GETUTCDATE(),
        updated_at   DATETIME2      DEFAULT GETUTCDATE(),
        claimed_at   DATETIME2      NULL,
        completed_at DATETIME2      NULL,
        error        NVARCHAR(MAX)  DEFAULT ''
    )
    """, "memory.TaskQueue"),

    ("""
    CREATE TABLE memory.ActivityLog (
        id          INT IDENTITY(1,1) PRIMARY KEY,
        event_type  NVARCHAR(100)  NOT NULL,
        agent       NVARCHAR(100)  DEFAULT '',
        description NVARCHAR(MAX)  DEFAULT '',
        metadata    NVARCHAR(MAX)  DEFAULT '',
        importance  INT            DEFAULT 3,
        created_at  DATETIME2      DEFAULT GETUTCDATE()
    )
    """, "memory.ActivityLog"),

    ("""
    CREATE TABLE memory.Sessions (
        id          INT IDENTITY(1,1) PRIMARY KEY,
        session_key NVARCHAR(255)  NOT NULL,
        agent       NVARCHAR(100)  DEFAULT '',
        status      NVARCHAR(50)   DEFAULT 'active',
        metadata    NVARCHAR(MAX)  DEFAULT '',
        started_at  DATETIME2      DEFAULT GETUTCDATE(),
        ended_at    DATETIME2      NULL
    )
    """, "memory.Sessions"),

    ("""
    CREATE TABLE memory.KnowledgeIndex (
        id         INT IDENTITY(1,1) PRIMARY KEY,
        domain     NVARCHAR(100)  NOT NULL,
        [key]      NVARCHAR(255)  NOT NULL,
        content    NVARCHAR(MAX)  NOT NULL,
        source     NVARCHAR(255)  DEFAULT '',
        tags       NVARCHAR(500)  DEFAULT '',
        created_at DATETIME2      DEFAULT GETUTCDATE()
    )
    """, "memory.KnowledgeIndex"),

    ("""
    CREATE TABLE memory.Todos (
        id          INT IDENTITY(1,1) PRIMARY KEY,
        title       NVARCHAR(500)  NOT NULL,
        description NVARCHAR(MAX)  DEFAULT '',
        priority    INT            DEFAULT 3,
        status      NVARCHAR(50)   DEFAULT 'open',
        tags        NVARCHAR(500)  DEFAULT '',
        created_at  DATETIME2      DEFAULT GETUTCDATE(),
        updated_at  DATETIME2      DEFAULT GETUTCDATE(),
        closed_at   DATETIME2      NULL
    )
    """, "memory.Todos"),
]


def table_exists(db, table_name):
    schema, name = table_name.split('.')
    rows = db.query(
        "SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s",
        (schema, name)
    )
    return len(rows) > 0


def schema_exists(db, schema_name):
    rows = db.query(
        "SELECT 1 FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = %s",
        (schema_name,)
    )
    return len(rows) > 0


def run(profile='local'):
    print(f"\nclawbot-sql-memory schema setup — profile: {profile}\n")
    db = get_connector(profile)

    if not db.ping():
        print("ERROR: Cannot connect to SQL Server. Check your .env credentials.")
        sys.exit(1)

    print("✅ Connected to SQL Server\n")

    for sql, label in SCHEMA_SQL:
        if label == "memory schema":
            if schema_exists(db, 'memory'):
                print(f"  SKIP  {label} (already exists)")
                continue
        else:
            if table_exists(db, label):
                print(f"  SKIP  {label} (already exists)")
                continue

        ok = db.execute(sql.strip())
        if ok:
            print(f"  CREATE {label}")
        else:
            print(f"  ERROR  {label} — check logs")

    print("\nSchema setup complete.\n")
    print("Next step: configure your .env and run:")
    print("  python3 -c \"from sql_memory import get_memory; print(get_memory('local').ping())\"")


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Create memory.* schema for clawbot-sql-memory')
    parser.add_argument('--cloud', action='store_true', help='Use cloud SQL profile')
    args = parser.parse_args()
    run('cloud' if args.cloud else 'local')


path: sql-memory/sql_memory.py
content: #!/usr/bin/env python3
"""
sql_memory.py — Oblio SQL Memory Module (v2.0)
===============================================
Semantic memory layer for OpenClaw agents. All operations go through
SQLConnector v2 (pymssql, parameterised, sealed API) — no subprocess/sqlcmd.

Supports two backends:
  - 'local'  → SQL Server on DEAUS (10.0.0.110) — default
  - 'cloud'  → site4now hosted (SQL5112.site4now.net)

Backward-compatible with v1.x callers:
  - SQLMemory('local')         — works as before
  - get_memory('local')        — singleton factory preserved
  - mem.remember / recall / search / queue_task / log_event — all preserved
  - mem.execute(raw_sql)       — preserved as legacy passthrough (returns bool)
  - mem.execute_scalar(sql)    — preserved, returns Any

New in v2.0:
  - Transport: pymssql native driver (no sqlcmd subprocess)
  - UTC timestamps everywhere (datetime.now(timezone.utc))
  - Parameterised queries throughout — no string interpolation
  - execute_via_file() preserved as execute() — file-based workaround no longer needed
  - _parse_table() kept for any callers using raw output parsing (no-op path)

Usage:
    from sql_memory import SQLMemory, get_memory
    mem = get_memory('local')
    mem.remember('facts', 'sky_color', 'The sky is blue', importance=3)
    result = mem.recall('facts', 'sky_color')
"""

import os
import sys
import json
import logging
from datetime import datetime, timezone
from typing import Optional, List, Dict, Any

# ── Find and load .env ────────────────────────────────────────────────────────
import pathlib as _pathlib

def _find_env() -> Optional[str]:
    p = _pathlib.Path(os.path.abspath(__file__)).parent
    for _ in range(5):
        c = p / '.env'
        if c.exists():
            return str(c)
        p = p.parent
    return None

try:
    from dotenv import load_dotenv
    _env = _find_env()
    if _env:
        load_dotenv(_env, override=True)
except ImportError:
    _env = _find_env()
    if _env:
        with open(_env) as f:
            for line in f:
                line = line.strip()
                if line and not line.startswith('#') and '=' in line:
                    k, v = line.split('=', 1)
                    os.environ[k.strip()] = v.strip().strip('"').strip("'")

# ── Import connector (handles both skill install path and infrastructure path) ─

def _import_connector():
    """Find and import SQLConnector from wherever it's installed."""
    # Try adjacent skill install first (workspace/skills/sql-connector/scripts/)
    skill_scripts_path = os.path.join(os.path.dirname(__file__), '..', '..', 'sql-connector', 'scripts')
    skill_path = os.path.join(os.path.dirname(__file__), '..', 'sql-connector')
    infra_path = os.path.join(os.path.dirname(__file__), '..', 'infrastructure')
    for p in [skill_scripts_path, skill_path, infra_path, os.path.dirname(__file__)]:
        abs_p = os.path.abspath(p)
        if os.path.exists(os.path.join(abs_p, 'sql_connector.py')):
            if abs_p not in sys.path:
                sys.path.insert(0, abs_p)
            from sql_connector import get_connector, SQLConnector
            return get_connector, SQLConnector
    raise ImportError("sql_connector.py not found. Install the sql-connector skill first.")

get_connector, SQLConnector = _import_connector()

# ── Logger ────────────────────────────────────────────────────────────────────

LOG_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'logs')
os.makedirs(LOG_DIR, exist_ok=True)

_log = logging.getLogger('sql_memory')
if not _log.handlers:
    _log.setLevel(logging.INFO)
    _fh = logging.FileHandler(os.path.join(LOG_DIR, 'sql_dbo.log'))
    _fh.setFormatter(logging.Formatter('%(asctime)s [sql_memory] %(levelname)s %(message)s'))
    _fh.formatter.converter = __import__('time').gmtime  # UTC timestamps in log file
    _log.addHandler(_fh)


# ── SQLMemory ─────────────────────────────────────────────────────────────────

class SQLMemory:
    """
    Unified SQL memory interface for Oblio agents.
    Wraps SQLConnector — all queries are parameterised, no string interpolation.

    Args:
        backend: 'local' (default) or 'cloud'
    """

    def __init__(self, backend: str = 'local') -> None:
        self.backend = backend
        self._db = get_connector(backend)
        _log.info(f"SQLMemory v2.0 initialized (backend={backend})")

    # ── Low-level passthrough (v1.x compatibility) ────────────────────────────

    def execute(self, query: str, timeout: int = 30) -> bool:
        """
        Legacy passthrough for raw SQL. Returns bool (v2) instead of stdout string (v1).
        Callers that checked 'error' in result.lower() should switch to checking False.
        NOTE: Raw SQL here bypasses parameterisation — migrate to mem.* methods for new code.
        """
        return self._db.execute(query)

    def execute_scalar(self, query: str) -> Optional[Any]:
        """Execute query and return first value. Parameterised via scalar()."""
        return self._db.scalar(query)

    def execute_via_file(self, query: str, timeout: int = 30) -> bool:
        """v1.x large-payload workaround — now just delegates to execute() since pymssql has no arg-length limit."""
        return self._db.execute(query)

    def execute_rows(self, query: str) -> List[str]:
        """Execute query and return rows as list of strings (v1.x compat)."""
        rows = self._db.query(query)
        return [str(list(r.values())) for r in rows]

    def ping(self) -> bool:
        return self._db.ping()

    # ── Memory Operations ─────────────────────────────────────────────────────

    def remember(self, category: str, key: str, content: str,
                 importance: int = 3, tags: str = '') -> bool:
        """
        Store or update a memory. Upserts by category + key_name.
        importance: 1-10 (10 = permanent)
        """
        now = datetime.now(timezone.utc)
        ok = self._db.execute("""
            MERGE memory.Memories AS target
            USING (SELECT %s AS category, %s AS key_name) AS source
              ON target.category = source.category
             AND target.key_name = source.key_name
             AND target.is_active = 1
            WHEN MATCHED THEN
              UPDATE SET content=%s, importance=%s, tags=%s, updated_at=%s
            WHEN NOT MATCHED THEN
              INSERT (category, key_name, content, importance, tags, source, is_active, created_at, updated_at)
              VALUES (%s, %s, %s, %s, %s, 'sql_memory', 1, %s, %s);
        """, (category, key, content, importance, tags, now,
              category, key, content, importance, tags, now, now))
        _log.info(f"remember({category}/{key}) → {'ok' if ok else 'failed'}")
        return ok

    def recall(self, category: str, key: str) -> Optional[str]:
        """Retrieve a specific memory's content. Returns content string or None."""
        rows = self._db.query(
            "SELECT content FROM memory.Memories WHERE category=%s AND key_name=%s AND is_active=1",
            (category, key)
        )
        return rows[0]['content'] if rows else None

    def recall_recent(self, n: int = 10) -> List[Dict[str, Any]]:
        """Return the N most recently updated memories across all categories."""
        return self._db.query("""
            SELECT TOP (%s) category, key_name, content, importance, tags,
                   CONVERT(varchar, ISNULL(updated_at, created_at), 120) AS ts
            FROM memory.Memories WHERE is_active=1
            ORDER BY ISNULL(updated_at, created_at) DESC
        """, (n,))

    def search_memories(self, keyword: str, limit: int = 20) -> List[Dict[str, Any]]:
        """Full-text search across content, tags, and key_name."""
        like = f'%{keyword}%'
        return self._db.query("""
            SELECT TOP (%s) category, key_name, content, importance, tags
            FROM memory.Memories
            WHERE is_active=1
              AND (content LIKE %s OR tags LIKE %s OR key_name LIKE %s)
            ORDER BY importance DESC, ISNULL(updated_at, created_at) DESC
        """, (limit, like, like, like))

    def forget(self, category: str, key: str) -> bool:
        """Soft-delete a memory (set is_active=0)."""
        ok = self._db.execute(
            "UPDATE memory.Memories SET is_active=0 WHERE category=%s AND key_name=%s",
            (category, key)
        )
        _log.info(f"forget({category}/{key})")
        return ok

    # ── Activity Log ──────────────────────────────────────────────────────────

    def log_event(self, event_type: str, agent: str, description: str,
                  metadata: str = '', importance: int = 3) -> bool:
        """Write an event to the activity log (logged_at set by DB default)."""
        return self._db.execute("""
            INSERT INTO memory.ActivityLog (event_type, agent, description, metadata, importance)
            VALUES (%s, %s, %s, %s, %s)
        """, (event_type, agent, description, metadata, importance))

    def get_recent_activity(self, since_hours: int = 24,
                            agent: Optional[str] = None) -> List[Dict]:
        """Get recent activity log entries."""
        col = 'logged_at'   # actual column name on cloud schema
        if agent:
            return self._db.query(f"""
                SELECT event_type, agent, description,
                       CONVERT(varchar, {col}, 120) AS ts
                FROM memory.ActivityLog
                WHERE {col} >= DATEADD(HOUR, -%s, GETUTCDATE())
                  AND agent=%s
                ORDER BY {col} DESC
            """, (since_hours, agent))
        return self._db.query(f"""
            SELECT event_type, agent, description,
                   CONVERT(varchar, {col}, 120) AS ts
            FROM memory.ActivityLog
            WHERE {col} >= DATEADD(HOUR, -%s, GETUTCDATE())
            ORDER BY {col} DESC
        """, (since_hours,))

    # ── Task Queue ────────────────────────────────────────────────────────────

    def queue_task(self, agent: str, task_type: str, payload: str = '{}',
                   priority: Any = 5) -> Optional[str]:
        """Insert a new task. Priority can be int or string name."""
        _pmap = {'critical': 1, 'high': 2, 'medium': 5, 'low': 7, 'free': 9}
        if isinstance(priority, str):
            priority = _pmap.get(priority.lower(), 5)
        priority = int(priority)
        now = datetime.now(timezone.utc)
        ok = self._db.execute("""
            INSERT INTO memory.TaskQueue (agent, task_type, payload, priority, status, created_at)
            VALUES (%s, %s, %s, %s, 'pending', %s)
        """, (agent, task_type, payload, priority, now))
        if not ok:
            return None
        tid = self._db.scalar("""
            SELECT TOP 1 id FROM memory.TaskQueue
            WHERE agent=%s AND task_type=%s AND status='pending'
            ORDER BY created_at DESC
        """, (agent, task_type))
        _log.info(f"queue_task({agent}/{task_type}) → id={tid}")
        return str(tid) if tid else None

    def get_pending_tasks(self, agent: str, task_types: List[str],
                          limit: int = 10) -> List[Dict]:
        """Fetch pending tasks for an agent, ordered by priority then age."""
        if not task_types:
            return []
        placeholders = ','.join(['%s'] * len(task_types))
        return self._db.query(f"""
            SELECT TOP (%s) id, task_type, payload, priority, retry_count
            FROM memory.TaskQueue
            WHERE agent=%s AND task_type IN ({placeholders}) AND status='pending'
            ORDER BY priority ASC, created_at ASC
        """, (limit, agent, *task_types))

    def claim_task(self, task_id: Any) -> bool:
        """Mark a task as processing."""
        return self._db.execute("""
            UPDATE memory.TaskQueue
            SET status='processing', started_at=%s
            WHERE id=%s
        """, (datetime.now(timezone.utc), int(task_id)))

    def complete_task(self, task_id: Any, result: str = '') -> bool:
        """Mark a task as completed."""
        ok = self._db.execute("""
            UPDATE memory.TaskQueue
            SET status='completed', completed_at=%s, error_log=%s
            WHERE id=%s
        """, (datetime.now(timezone.utc), result[:500], int(task_id)))
        _log.info(f"complete_task({task_id})")
        return ok

    def fail_task(self, task_id: Any, error: str, retry_count: int = 0,
                  max_retries: int = 3) -> bool:
        """Fail or re-queue a task based on retry count."""
        new_status = 'pending' if retry_count < max_retries else 'failed'
        ok = self._db.execute("""
            UPDATE memory.TaskQueue
            SET status=%s, retry_count=retry_count+1, error_log=%s
            WHERE id=%s
        """, (new_status, error[:800], int(task_id)))
        _log.info(f"fail_task({task_id}) → {new_status}")
        return ok

    def get_completed_tasks(self, since_hours: int = 24,
                            agent: Optional[str] = None) -> List[Dict]:
        """Get recently completed or failed tasks."""
        if agent:
            return self._db.query("""
                SELECT id, agent, task_type, status,
                       CONVERT(varchar, completed_at, 120) AS ts
                FROM memory.TaskQueue
                WHERE status IN ('completed','failed')
                  AND completed_at >= DATEADD(HOUR, -%s, GETUTCDATE())
                  AND agent=%s
                ORDER BY completed_at DESC
            """, (since_hours, agent))
        return self._db.query("""
            SELECT id, agent, task_type, status,
                   CONVERT(varchar, completed_at, 120) AS ts
            FROM memory.TaskQueue
            WHERE status IN ('completed','failed')
              AND completed_at >= DATEADD(HOUR, -%s, GETUTCDATE())
            ORDER BY completed_at DESC
        """, (since_hours,))

    # ── Knowledge Index ───────────────────────────────────────────────────────

    def store_knowledge(self, domain: str, topic: str, summary: str = '',
                        file_path: str = '', tags: str = '') -> bool:
        """Store or update a knowledge entry. Upserts by domain + topic."""
        now = datetime.now(timezone.utc)
        return self._db.execute("""
            MERGE memory.KnowledgeIndex AS target
            USING (SELECT %s AS domain, %s AS topic) AS source
              ON target.domain = source.domain AND target.topic = source.topic
            WHEN MATCHED THEN
              UPDATE SET summary=%s, file_path=%s,
                         last_trained=%s, training_count=ISNULL(training_count,0)+1
            WHEN NOT MATCHED THEN
              INSERT (domain, topic, summary, file_path, last_trained, training_count, created_at)
              VALUES (%s, %s, %s, %s, %s, 1, %s);
        """, (domain, topic, summary, file_path, now,
              domain, topic, summary, file_path, now, now))

    def search_knowledge(self, domain: str, keyword: str = '') -> List[Dict]:
        """Search knowledge entries by domain and optional keyword."""
        if keyword:
            like = f'%{keyword}%'
            return self._db.query("""
                SELECT domain, topic, summary, file_path, training_count,
                       CONVERT(varchar, last_trained, 120) AS ts
                FROM memory.KnowledgeIndex
                WHERE domain=%s AND (topic LIKE %s OR summary LIKE %s)
                ORDER BY last_trained DESC
            """, (domain, like, like))
        return self._db.query("""
            SELECT domain, topic, summary, file_path, training_count,
                   CONVERT(varchar, last_trained, 120) AS ts
            FROM memory.KnowledgeIndex
            WHERE domain=%s ORDER BY last_trained DESC
        """, (domain,))

    def get_recent_knowledge(self, n: int = 10) -> List[Dict]:
        """Get the N most recently updated knowledge entries."""
        return self._db.query("""
            SELECT TOP (%s) domain, topic, summary,
                   CONVERT(varchar, last_trained, 120) AS ts
            FROM memory.KnowledgeIndex ORDER BY last_trained DESC
        """, (n,))

    # ── Sessions ──────────────────────────────────────────────────────────────

    def get_session_context(self, session_id: str) -> Optional[Dict]:
        """Load session context from the database."""
        rows = self._db.query(
            "SELECT session_key, channel, summary, token_count FROM memory.Sessions WHERE session_key=%s",
            (session_id,)
        )
        if not rows:
            return None
        row = rows[0]
        try:
            row['context'] = json.loads(row.get('summary') or '{}')
        except (json.JSONDecodeError, TypeError):
            row['context'] = {}
        return row

    def save_session_context(self, session_id: str, context_data: Dict,
                             channel: str = 'agent', token_count: int = 0) -> bool:
        """Persist session context as JSON in memory.Sessions."""
        ctx_json = json.dumps(context_data, default=str)
        now = datetime.now(timezone.utc)
        return self._db.execute("""
            MERGE memory.Sessions AS target
            USING (SELECT %s AS session_key) AS source
              ON target.session_key = source.session_key
            WHEN MATCHED THEN
              UPDATE SET summary=%s, token_count=%s, ended_at=%s
            WHEN NOT MATCHED THEN
              INSERT (session_key, channel, summary, token_count, started_at)
              VALUES (%s, %s, %s, %s, %s);
        """, (session_id, ctx_json, token_count, now,
              session_id, channel, ctx_json, token_count, now))

    # ── Todos ─────────────────────────────────────────────────────────────────

    def add_todo(self, title: str, project: str = '', priority: int = 5,
                 tags: str = '', due_date=None) -> Optional[int]:
        """Insert a new todo item. Returns the new todo id."""
        now = datetime.now(timezone.utc)
        return self._db.scalar("""
            INSERT INTO memory.Todos (title, project, priority, status, tags, due_date, created_at)
            OUTPUT INSERTED.id
            VALUES (%s, %s, %s, 'open', %s, %s, %s)
        """, (title, project, priority, tags, due_date, now))

    def complete_todo(self, todo_id: int, status: str = 'done') -> bool:
        """Mark a todo as done/completed."""
        now = datetime.now(timezone.utc)
        return self._db.execute("""
            UPDATE memory.Todos SET status=%s, completed_at=%s WHERE id=%s
        """, (status, now, todo_id))

    def update_todo(self, todo_id: int, **fields) -> bool:
        """Update arbitrary todo fields. Allowed: title, project, priority, status, tags, due_date."""
        allowed = {'title', 'project', 'priority', 'status', 'tags', 'due_date'}
        updates = {k: v for k, v in fields.items() if k in allowed}
        if not updates:
            return False
        set_clause = ', '.join(f'{k}=%s' for k in updates)
        params = list(updates.values()) + [todo_id]
        return self._db.execute(
            f'UPDATE memory.Todos SET {set_clause} WHERE id=%s', params
        )

    def delete_todo(self, todo_id: int) -> bool:
        """Hard-delete a todo. Prefer complete_todo() for audit trail."""
        return self._db.execute('DELETE FROM memory.Todos WHERE id=%s', (todo_id,))

    # ── Utility ───────────────────────────────────────────────────────────────

    def _parse_table(self, raw_output: str, columns: List[str]) -> List[Dict]:
        """
        v1.x compat stub. v2 query() returns list[dict] directly.
        Kept so any code that calls _parse_table on raw output still runs.
        Returns empty list — callers should migrate to query().
        """
        _log.debug("_parse_table() called — migrate caller to use query() directly")
        return []

    def ensure_schema(self) -> bool:
        """Create memory schema + core tables if they don't exist."""
        self._db.execute("IF NOT EXISTS (SELECT 1 FROM sys.schemas WHERE name='memory') EXEC('CREATE SCHEMA memory')")
        tables = {
            'Memories': """CREATE TABLE memory.Memories (
                id BIGINT IDENTITY(1,1) PRIMARY KEY, category NVARCHAR(100) NOT NULL,
                key_name NVARCHAR(255), content NVARCHAR(MAX) NOT NULL,
                importance TINYINT DEFAULT 3, tags NVARCHAR(500), source NVARCHAR(255),
                created_at DATETIME2 DEFAULT GETUTCDATE(), updated_at DATETIME2 DEFAULT GETUTCDATE(),
                expires_at DATETIME2, is_active BIT DEFAULT 1)""",
            'Sessions': """CREATE TABLE memory.Sessions (
                id BIGINT IDENTITY(1,1) PRIMARY KEY, session_key NVARCHAR(255),
                channel NVARCHAR(100), started_at DATETIME2 DEFAULT GETUTCDATE(),
                ended_at DATETIME2, summary NVARCHAR(MAX), token_count INT DEFAULT 0)""",
            'TaskQueue': """CREATE TABLE memory.TaskQueue (
                id BIGINT IDENTITY(1,1) PRIMARY KEY, agent NVARCHAR(100) NOT NULL,
                task_type NVARCHAR(100) NOT NULL, payload NVARCHAR(MAX),
                priority TINYINT DEFAULT 5, status NVARCHAR(50) DEFAULT 'pending',
                created_at DATETIME2 DEFAULT GETUTCDATE(), started_at DATETIME2,
                completed_at DATETIME2, error_log NVARCHAR(MAX), retry_count TINYINT DEFAULT 0)""",
            'KnowledgeIndex': """CREATE TABLE memory.KnowledgeIndex (
                id BIGINT IDENTITY(1,1) PRIMARY KEY, domain NVARCHAR(100) NOT NULL,
                topic NVARCHAR(255) NOT NULL, file_path NVARCHAR(1000),
                summary NVARCHAR(MAX), last_trained DATETIME2, training_count INT DEFAULT 0,
                created_at DATETIME2 DEFAULT GETUTCDATE())""",
            'ActivityLog': """CREATE TABLE memory.ActivityLog (
                id BIGINT IDENTITY(1,1) PRIMARY KEY, event_type NVARCHAR(100) NOT NULL,
                agent NVARCHAR(100), description NVARCHAR(MAX), metadata NVARCHAR(MAX),
                importance TINYINT DEFAULT 3, created_at DATETIME2 DEFAULT GETUTCDATE())""",
        }
        for name, ddl in tables.items():
            self._db.execute(f"""
                IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.TABLES
                               WHERE TABLE_SCHEMA='memory' AND TABLE_NAME='{name}')
                BEGIN {ddl} END
            """)
            _log.info(f"ensure_schema: {name} OK")
        return True


# ── Module-level factory (singleton) ─────────────────────────────────────────

_instances: Dict[str, SQLMemory] = {}

def get_memory(backend: str = 'local') -> SQLMemory:
    """Get or create a SQLMemory instance. Singleton per backend."""
    if backend not in _instances:
        _instances[backend] = SQLMemory(backend)
    return _instances[backend]


# ── Self-test ─────────────────────────────────────────────────────────────────

if __name__ == '__main__':
    print("sql_memory v2.0 — self-test")
    passed = failed = 0

    def t(name, fn):
        global passed, failed
        try:
            r = fn()
            print(f"  ✅ {name}: {r!r:.60}" if r is not None else f"  ✅ {name}: ok")
            passed += 1
        except Exception as e:
            print(f"  ❌ {name}: {e}")
            failed += 1

    mem = get_memory('local')
    t("ping", mem.ping)
    t("remember", lambda: mem.remember('test', '_v2_test', 'v2 self-test', importance=1, tags='test'))
    t("recall", lambda: mem.recall('test', '_v2_test'))
    t("search", lambda: mem.search_memories('v2 self-test'))
    t("log_event", lambda: mem.log_event('selftest', 'sql_memory', 'v2 test'))
    t("queue_task", lambda: mem.queue_task('test_agent', '_v2_task', '{}', priority=1))
    t("forget", lambda: mem.forget('test', '_v2_test'))

    print(f"\n{passed} passed, {failed} failed")
    import sys; sys.exit(1 if failed else 0)


path: tests/test_queue_daemon.py
content: #!/usr/bin/env python3
"""
test_queue_daemon.py — Unit tests for queue daemon routing and task processing
"""
import os
import sys
import unittest

sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'agents'))
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'infrastructure'))


class TestAgentRegistry(unittest.TestCase):
    """Test that agent registry is properly configured."""

    def test_registry_has_entries(self):
        from queue_daemon import AGENT_REGISTRY
        self.assertGreater(len(AGENT_REGISTRY), 5)

    def test_registry_values_are_tuples(self):
        from queue_daemon import AGENT_REGISTRY
        for name, val in AGENT_REGISTRY.items():
            self.assertIsInstance(val, tuple, f"{name} should map to (module, class) tuple")
            self.assertEqual(len(val), 2, f"{name} tuple should have 2 elements")


class TestStubAgent(unittest.TestCase):
    """Test the fallback stub agent."""

    def test_stub_handles_any_task(self):
        from queue_daemon import _get_stub_agent
        stub = _get_stub_agent()
        result = stub.run_task({
            'id': '999', 'agent': 'unknown', 'task_type': 'unknown_type'
        })
        self.assertIn('Stub', result)

    def test_stub_singleton(self):
        from queue_daemon import _get_stub_agent
        a = _get_stub_agent()
        b = _get_stub_agent()
        self.assertIs(a, b)


class TestResolveAgent(unittest.TestCase):
    """Test agent resolution logic."""

    def test_resolve_registered_agent(self):
        from queue_daemon import resolve_agent_for_task
        agent = resolve_agent_for_task('dispatcher', 'github_setup')
        self.assertIsNotNone(agent)

    def test_resolve_unknown_agent_returns_stub(self):
        from queue_daemon import resolve_agent_for_task, _StubAgent
        agent = resolve_agent_for_task('totally_fake_agent', 'fake_task')
        self.assertIsInstance(agent, _StubAgent)


class TestFetchParsing(unittest.TestCase):
    """Test that fetch_next_task returns proper structure."""

    def test_fetch_returns_none_or_dict(self):
        from sql_memory import get_memory
        from queue_daemon import fetch_next_task
        mem = get_memory('local')
        result = fetch_next_task(mem)
        self.assertTrue(result is None or isinstance(result, dict))


if __name__ == '__main__':
    unittest.main(verbosity=2)


path: tests/test_queue_verifier.py
content: #!/usr/bin/env python3
"""
Unit tests for infrastructure/queue_verifier.py
Auto-generated stub — 2026-03-09

BEST PRACTICES:
  - One test per function/behavior
  - Arrange → Act → Assert pattern
  - Mock all external dependencies (SQL, Ollama, filesystem)
  - Test happy path + edge cases + error conditions
  - Use pytest fixtures for reusable setup
  - All tests must be independent (no shared state)
"""

import pytest
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch, call

# ── Path setup ──────────────────────────────────────────────────────────────
WORKSPACE = Path(__file__).parent.parent
sys.path.insert(0, str(WORKSPACE))
sys.path.insert(0, str(WORKSPACE / "infrastructure"))

# ── Fixtures ────────────────────────────────────────────────────────────────

@pytest.fixture
def mock_sql():
    """Mock SQLMemory to prevent real DB calls in tests."""
    with patch("infrastructure.sql_memory.SQLMemory") as mock:
        instance = mock.return_value
        instance.queue_task.return_value = True
        instance.log_event.return_value = True
        instance.get_pending_tasks.return_value = []
        yield instance


@pytest.fixture
def mock_ollama():
    """Mock Ollama API calls."""
    with patch("urllib.request.urlopen") as mock:
        import json
        mock.return_value.__enter__.return_value.read.return_value = \
            json.dumps({"response": "Mock Ollama response"}).encode()
        yield mock


# ── Tests for infrastructure/queue_verifier.py ──────────────────────────────────────────────────────

class TestQueueVerifier:
    """Test suite for queue_verifier."""

    def test___init__(self, mock_sql, mock_ollama):
        """
        Test: __init__()
        Source line: 17
        TODO: Add test docstring
        """
        # TODO: Implement test for __init__
        # Arrange
        # ... set up test data ...
        # Act
        # result = __init__('test')
        # Assert
        # assert result is not None
        pytest.skip("STUB — implement me")

    def test___init___handles_errors(self, mock_sql):
        """Test error handling in __init__()."""
        # TODO: Test error conditions (bad input, network failure, etc.)
        pytest.skip("STUB — implement me")

    def test_get_pending_count(self, mock_sql, mock_ollama):
        """
        Test: get_pending_count()
        Source line: 20
        Docstring: Get count of pending tasks
        """
        # TODO: Implement test for get_pending_count
        # Arrange
        # ... set up test data ...
        # Act
        # result = get_pending_count()
        # Assert
        # assert result is not None
        pytest.skip("STUB — implement me")

    def test_get_pending_count_handles_errors(self, mock_sql):
        """Test error handling in get_pending_count()."""
        # TODO: Test error conditions (bad input, network failure, etc.)
        pytest.skip("STUB — implement me")

    def test_get_pending_by_agent(self, mock_sql, mock_ollama):
        """
        Test: get_pending_by_agent()
        Source line: 35
        Docstring: Get pending tasks grouped by agent
        """
        # TODO: Implement test for get_pending_by_agent
        # Arrange
        # ... set up test data ...
        # Act
        # result = get_pending_by_agent()
        # Assert
        # assert result is not None
        pytest.skip("STUB — implement me")

    def test_get_pending_by_agent_handles_errors(self, mock_sql):
        """Test error handling in get_pending_by_agent()."""
        # TODO: Test error conditions (bad input, network failure, etc.)
        pytest.skip("STUB — implement me")

    def test_mark_completed(self, mock_sql, mock_ollama):
        """
        Test: mark_completed()
        Source line: 45
        Docstring: Mark tasks as completed
        """
        # TODO: Implement test for mark_completed
        # Arrange
        # ... set up test data ...
        # Act
        # result = mark_completed('test')
        # Assert
        # assert result is not None
        pytest.skip("STUB — implement me")

    def test_mark_completed_handles_errors(self, mock_sql):
        """Test error handling in mark_completed()."""
        # TODO: Test error conditions (bad input, network failure, etc.)
        pytest.skip("STUB — implement me")

    def test_get_failed_tasks(self, mock_sql, mock_ollama):
        """
        Test: get_failed_tasks()
        Source line: 61
        Docstring: Get tasks that failed recently
        """
        # TODO: Implement test for get_failed_tasks
        # Arrange
        # ... set up test data ...
        # Act
        # result = get_failed_tasks('test')
        # Assert
        # assert result is not None
        pytest.skip("STUB — implement me")

    def test_get_failed_tasks_handles_errors(self, mock_sql):
        """Test error handling in get_failed_tasks()."""
        # TODO: Test error conditions (bad input, network failure, etc.)
        pytest.skip("STUB — implement me")

    def test_retry_failed(self, mock_sql, mock_ollama):
        """
        Test: retry_failed()
        Source line: 71
        Docstring: Retry a failed task if under retry limit
        """
        # TODO: Implement test for retry_failed
        # Arrange
        # ... set up test data ...
        # Act
        # result = retry_failed('test', 'test')
        # Assert
        # assert result is not None
        pytest.skip("STUB — implement me")

    def test_retry_failed_handles_errors(self, mock_sql):
        """Test error handling in retry_failed()."""
        # TODO: Test error conditions (bad input, network failure, etc.)
        pytest.skip("STUB — implement me")


path: tests/test_sql_memory.py
content: #!/usr/bin/env python3
"""
test_sql_memory.py — Unit tests for SQLMemory
Covers: connection, CRUD, task queue, knowledge, error handling
"""
import os
import sys
import unittest
from unittest.mock import patch, MagicMock
from datetime import datetime

sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'infrastructure'))
from sql_memory import SQLMemory, _esc, get_memory


class TestEscaping(unittest.TestCase):
    """Test SQL string escaping."""

    def test_single_quotes_escaped(self):
        self.assertEqual(_esc("it's"), "it''s")

    def test_double_quotes_preserved(self):
        self.assertEqual(_esc('say "hello"'), 'say "hello"')

    def test_max_length_truncation(self):
        long = "x" * 5000
        result = _esc(long, max_len=100)
        self.assertEqual(len(result), 100)

    def test_empty_string(self):
        self.assertEqual(_esc(""), "")

    def test_none_handling(self):
        self.assertEqual(_esc(None), "")

    def test_numeric_coercion(self):
        self.assertEqual(_esc(42), "42")


class TestSQLMemoryConnection(unittest.TestCase):
    """Test connection and ping."""

    def test_get_memory_cloud(self):
        mem = get_memory('local')
        self.assertIsInstance(mem, SQLMemory)

    def test_ping(self):
        mem = get_memory('local')
        result = mem.ping()
        self.assertIsInstance(result, bool)

    def test_ping_succeeds_with_valid_creds(self):
        mem = get_memory('local')
        self.assertTrue(mem.ping(), "Ping should succeed with valid credentials")


class TestMemoryCRUD(unittest.TestCase):
    """Test remember/recall/search/forget cycle."""

    def setUp(self):
        self.mem = get_memory('local')
        self.test_key = f"test_{datetime.now().strftime('%Y%m%d%H%M%S')}"

    def test_remember_and_recall(self):
        self.mem.remember('test', self.test_key, 'test content', importance=1, tags='test')
        result = self.mem.recall('test', self.test_key)
        self.assertIsNotNone(result)
        self.assertIn('test content', str(result))

    def test_recall_nonexistent(self):
        result = self.mem.recall('test', 'nonexistent_key_xyz_99999')
        self.assertIsNone(result)

    def test_search_memories(self):
        self.mem.remember('test', self.test_key, 'searchable content xyz', importance=1, tags='test')
        results = self.mem.search_memories('searchable content xyz')
        self.assertIsInstance(results, list)

    def test_forget(self):
        self.mem.remember('test', self.test_key, 'to be forgotten', importance=1, tags='test')
        result = self.mem.forget('test', self.test_key)
        self.assertIsInstance(result, bool)

    def tearDown(self):
        # Cleanup test entries
        self.mem.execute(f"DELETE FROM memory.Memories WHERE key_name='{self.test_key}'")


class TestTaskQueue(unittest.TestCase):
    """Test task queue operations."""

    def setUp(self):
        self.mem = get_memory('local')
        self.test_task_type = f"test_task_{datetime.now().strftime('%H%M%S')}"

    def test_queue_and_claim(self):
        self.mem.queue_task('test_agent', self.test_task_type, '{}', priority=9)
        # Verify it was queued
        out = self.mem.execute(
            f"SELECT id FROM memory.TaskQueue WHERE task_type='{self.test_task_type}' AND status='pending'"
        )
        self.assertIn(self.test_task_type[:5], self.test_task_type)  # sanity

    def test_complete_task(self):
        self.mem.queue_task('test_agent', self.test_task_type, '{}', priority=9)
        out = self.mem.execute_scalar(
            f"SELECT TOP 1 id FROM memory.TaskQueue WHERE task_type='{self.test_task_type}' AND status='pending'"
        )
        if out and out.strip().isdigit():
            tid = out.strip()
            self.mem.claim_task(tid)
            result = self.mem.complete_task(tid, 'test result')
            self.assertTrue(result)

    def test_fail_task_retry(self):
        self.mem.queue_task('test_agent', self.test_task_type, '{}', priority=9)
        out = self.mem.execute_scalar(
            f"SELECT TOP 1 id FROM memory.TaskQueue WHERE task_type='{self.test_task_type}' AND status='pending'"
        )
        if out and out.strip().isdigit():
            tid = out.strip()
            self.mem.claim_task(tid)
            self.mem.fail_task(tid, 'test error', 0, 3)
            # Should be back to pending with retry_count incremented
            status = self.mem.execute_scalar(
                f"SELECT status FROM memory.TaskQueue WHERE id={tid}"
            )
            self.assertIn('pending', str(status))

    def tearDown(self):
        self.mem.execute(
            f"DELETE FROM memory.TaskQueue WHERE task_type='{self.test_task_type}'"
        )


class TestActivityLog(unittest.TestCase):
    """Test event logging."""

    def setUp(self):
        self.mem = get_memory('local')

    def test_log_event(self):
        self.mem.log_event('test_event', 'test_agent', 'unit test log entry')
        results = self.mem.get_recent_activity(since_hours=1, agent='test_agent')
        self.assertIsInstance(results, list)


class TestKnowledge(unittest.TestCase):
    """Test knowledge store operations."""

    def setUp(self):
        self.mem = get_memory('local')
        self.test_topic = f"test_topic_{datetime.now().strftime('%H%M%S')}"

    def test_store_and_search_knowledge(self):
        self.mem.store_knowledge('test_domain', self.test_topic, 'test summary', 'test_source')
        results = self.mem.search_knowledge('test_domain', self.test_topic)
        self.assertIsInstance(results, list)

    def tearDown(self):
        self.mem.execute(
            f"DELETE FROM memory.KnowledgeIndex WHERE topic='{self.test_topic}'"
        )


class TestEdgeCases(unittest.TestCase):
    """Test error handling and edge cases."""

    def test_execute_invalid_sql(self):
        mem = get_memory('local')
        result = mem.execute("SELECT * FROM nonexistent_table_xyz")
        # Should not crash, may return error text
        self.assertIsInstance(result, str)

    def test_remember_with_special_chars(self):
        mem = get_memory('local')
        key = f"special_test_{datetime.now().strftime('%H%M%S')}"
        mem.remember('test', key, "Content with 'quotes' and \"doubles\" and <html>", importance=1)
        result = mem.recall('test', key)
        # Cleanup
        mem.execute(f"DELETE FROM memory.Memories WHERE key_name='{key}'")

    def test_very_long_content(self):
        mem = get_memory('local')
        key = f"long_test_{datetime.now().strftime('%H%M%S')}"
        long_content = "x" * 5000
        mem.remember('test', key, long_content, importance=1)
        # Should truncate gracefully
        mem.execute(f"DELETE FROM memory.Memories WHERE key_name='{key}'")


if __name__ == '__main__':
    unittest.main(verbosity=2)


path: tests/test_todo.py
content: #!/usr/bin/env python3
"""
Unit tests for infrastructure/todo.py
Auto-generated stub — 2026-03-09

BEST PRACTICES:
  - One test per function/behavior
  - Arrange → Act → Assert pattern
  - Mock all external dependencies (SQL, Ollama, filesystem)
  - Test happy path + edge cases + error conditions
  - Use pytest fixtures for reusable setup
  - All tests must be independent (no shared state)
"""

import pytest
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch, call

# ── Path setup ──────────────────────────────────────────────────────────────
WORKSPACE = Path(__file__).parent.parent
sys.path.insert(0, str(WORKSPACE))
sys.path.insert(0, str(WORKSPACE / "infrastructure"))

# ── Fixtures ────────────────────────────────────────────────────────────────

@pytest.fixture
def mock_sql():
    """Mock SQLMemory to prevent real DB calls in tests."""
    with patch("infrastructure.sql_memory.SQLMemory") as mock:
        instance = mock.return_value
        instance.queue_task.return_value = True
        instance.log_event.return_value = True
        instance.get_pending_tasks.return_value = []
        yield instance


@pytest.fixture
def mock_ollama():
    """Mock Ollama API calls."""
    with patch("urllib.request.urlopen") as mock:
        import json
        mock.return_value.__enter__.return_value.read.return_value = \
            json.dumps({"response": "Mock Ollama response"}).encode()
        yield mock


# ── Tests for infrastructure/todo.py ──────────────────────────────────────────────────────

class TestTodo:
    """Test suite for todo."""

    def test___init__(self, mock_sql, mock_ollama):
        """
        Test: __init__()
        Source line: 37
        TODO: Add test docstring
        """
        # TODO: Implement test for __init__
        # Arrange
        # ... set up test data ...
        # Act
        # result = __init__('test')
        # Assert
        # assert result is not None
        pytest.skip("STUB — implement me")

    def test___init___handles_errors(self, mock_sql):
        """Test error handling in __init__()."""
        # TODO: Test error conditions (bad input, network failure, etc.)
        pytest.skip("STUB — implement me")

    def test_add_task(self, mock_sql, mock_ollama):
        """
        Test: add_task()
        Source line: 40
        Docstring: Add a new TODO item.
        """
        # TODO: Implement test for add_task
        # Arrange
        # ... set up test data ...
        # Act
        # result = add_task('test', 'test', 'test', 'test', 'test')
        # Assert
        # assert result is not None
        pytest.skip("STUB — implement me")

    def test_add_task_handles_errors(self, mock_sql):
        """Test error handling in add_task()."""
        # TODO: Test error conditions (bad input, network failure, etc.)
        pytest.skip("STUB — implement me")

    def test_list_by_priority(self, mock_sql, mock_ollama):
        """
        Test: list_by_priority()
        Source line: 59
        Docstring: Print all TODOs organized by priority.
        """
        # TODO: Implement test for list_by_priority
        # Arrange
        # ... set up test data ...
        # Act
        # result = list_by_priority()
        # Assert
        # assert result is not None
        pytest.skip("STUB — implement me")

    def test_list_by_priority_handles_errors(self, mock_sql):
        """Test error handling in list_by_priority()."""
        # TODO: Test error conditions (bad input, network failure, etc.)
        pytest.skip("STUB — implement me")

    def test_get_report(self, mock_sql, mock_ollama):
        """
        Test: get_report()
        Source line: 93
        Docstring: Generate text report of TODOs.
        """
        # TODO: Implement test for get_report
        # Arrange
        # ... set up test data ...
        # Act
        # result = get_report('test')
        # Assert
        # assert result is not None
        pytest.skip("STUB — implement me")

    def test_get_report_handles_errors(self, mock_sql):
        """Test error handling in get_report()."""
        # TODO: Test error conditions (bad input, network failure, etc.)
        pytest.skip("STUB — implement me")

    def test_claim_task(self, mock_sql, mock_ollama):
        """
        Test: claim_task()
        Source line: 120
        Docstring: Claim a task to work on.
        """
        # TODO: Implement test for claim_task
        # Arrange
        # ... set up test data ...
        # Act
        # result = claim_task('test')
        # Assert
        # assert result is not None
        pytest.skip("STUB — implement me")

    def test_claim_task_handles_errors(self, mock_sql):
        """Test error handling in claim_task()."""
        # TODO: Test error conditions (bad input, network failure, etc.)
        pytest.skip("STUB — implement me")

    def test_complete_task(self, mock_sql, mock_ollama):
        """
        Test: complete_task()
        Source line: 125
        Docstring: Mark task complete.
        """
        # TODO: Implement test for complete_task
        # Arrange
        # ... set up test data ...
        # Act
        # result = complete_task('test', 'test')
        # Assert
        # assert result is not None
        pytest.skip("STUB — implement me")

    def test_complete_task_handles_errors(self, mock_sql):
        """Test error handling in complete_task()."""
        # TODO: Test error conditions (bad input, network failure, etc.)
        pytest.skip("STUB — implement me")

    def test_fail_task(self, mock_sql, mock_ollama):
        """
        Test: fail_task()
        Source line: 132
        Docstring: Mark task failed.
        """
        # TODO: Implement test for fail_task
        # Arrange
        # ... set up test data ...
        # Act
        # result = fail_task('test', 'test')
        # Assert
        # assert result is not None
        pytest.skip("STUB — implement me")

    def test_fail_task_handles_errors(self, mock_sql):
        """Test error handling in fail_task()."""
        # TODO: Test error conditions (bad input, network failure, etc.)
        pytest.skip("STUB — implement me")


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