# Agent 长期记忆系统 Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** 为 NetaClaw Agent 添加跨会话长期记忆，支持 MySQL FULLTEXT 和 SQLite FTS5 双后端，Agent 粒度可配。

**Architecture:** Provider 抽象层 + 双后端实现。MemoryProvider 接口统一 save/search/delete 操作，MysqlMemoryProvider 用 FULLTEXT ngram，SqliteMemoryProvider 用 FTS5 trigram。工厂函数根据 Agent 配置选择后端。memory_save / memory_recall 两个工具注入 Agent 工具列表，prefetch 在每轮对话前检索相关记忆注入 system prompt。

**Tech Stack:** TypeScript, TypeORM (MySQL), better-sqlite3 (SQLite FTS5), @sinclair/typebox (tool schema)

**Spec:** `docs/superpowers/specs/2026-04-12-agent-memory-system-design.md`

---

## File Structure

```
src/modules/netaclaw/
├── memory/
│   ├── provider.ts           # MemoryProvider 接口 + MemoryEntry 类型 + AgentMemoryConfig
│   ├── factory.ts            # createMemoryProvider 工厂函数
│   ├── mysql_provider.ts     # MysqlMemoryProvider
│   ├── sqlite_provider.ts    # SqliteMemoryProvider
│   └── prefetch.ts           # prefetchMemory() + formatMemoryContext()
├── entity/
│   └── memory.ts             # NetaClawMemoryEntity (新增)
├── tools/builtin/
│   └── memory.ts             # memory_save + memory_recall 工具
```

Modified files:
- `src/modules/netaclaw/runtime/agent.ts` — AgentRunParams 新增 memoryContext，消息注入
- `src/modules/netaclaw/controller/chat.ts` — prefetch 调用 + memory 工具注入
- `src/entities.ts` — 注册 NetaClawMemoryEntity

---

### Task 1: Install dependencies + Create MemoryEntry types and MemoryProvider interface

**Files:**
- Create: `src/modules/netaclaw/memory/provider.ts`

- [ ] **Step 1: Install better-sqlite3**

```bash
cd packages/backend && pnpm add better-sqlite3 && pnpm add -D @types/better-sqlite3
```

- [ ] **Step 2: Create provider.ts with types and interface**

Create `src/modules/netaclaw/memory/provider.ts`:

```typescript
/**
 * 长期记忆 Provider 抽象层
 */

export type MemoryType = 'user' | 'project' | 'feedback' | 'reference';

export interface MemoryEntry {
  id: number;
  agentName: string;
  userId: string;
  type: MemoryType;
  name: string;
  content: string;
  description: string;
  metadata?: Record<string, unknown>;
  createdAt: Date;
  updatedAt: Date;
}

export interface MemorySearchOpts {
  agentName: string;
  userId: string;
  type?: MemoryType;
  limit?: number;
}

export interface AgentMemoryConfig {
  enabled: boolean;
  backend: 'mysql' | 'sqlite';
  sqlitePath?: string;
  prefetchLimit?: number;
}

export interface MemoryProvider {
  save(entry: Omit<MemoryEntry, 'id' | 'createdAt' | 'updatedAt'>): Promise<MemoryEntry>;
  update(id: number, partial: Partial<Pick<MemoryEntry, 'name' | 'content' | 'description' | 'type' | 'metadata'>>): Promise<MemoryEntry>;
  delete(id: number): Promise<void>;
  search(query: string, opts: MemorySearchOpts): Promise<MemoryEntry[]>;
  list(opts: MemorySearchOpts): Promise<MemoryEntry[]>;
  getById(id: number): Promise<MemoryEntry | null>;
  close?(): Promise<void>;
}
```

- [ ] **Step 3: Verify no TypeScript errors**

```bash
cd packages/backend && npx tsc --noEmit src/modules/netaclaw/memory/provider.ts
```

- [ ] **Step 4: Commit**

```bash
git add src/modules/netaclaw/memory/provider.ts package.json pnpm-lock.yaml
git commit -m "feat(memory): add MemoryProvider interface and types + install better-sqlite3"
```

---

### Task 2: Create NetaClawMemoryEntity for MySQL

**Files:**
- Create: `src/modules/netaclaw/entity/memory.ts`
- Modify: `src/entities.ts`

- [ ] **Step 1: Create the entity file**

Create `src/modules/netaclaw/entity/memory.ts`:

```typescript
import { BaseEntity } from '../../base/entity/base.js';
import { Column, Entity, Index } from 'typeorm';

/**
 * NetaClaw 长期记忆
 */
@Entity('netaclaw_memory')
export class NetaClawMemoryEntity extends BaseEntity {
  @Index()
  @Column({ comment: 'Agent名称', length: 100 })
  agentName: string;

  @Index()
  @Column({ comment: '用户ID', length: 100 })
  userId: string;

  @Index()
  @Column({ comment: '记忆类型: user/project/feedback/reference', length: 20 })
  type: string;

  @Column({ comment: '记忆标题', length: 255 })
  name: string;

  @Column({ type: 'text', comment: '记忆正文' })
  content: string;

  @Column({ comment: '一行描述', length: 500, default: '' })
  description: string;

  @Column({ type: 'json', comment: '元数据', nullable: true })
  metadata: Record<string, unknown>;
}
```

Note: FULLTEXT 索引需要通过 migration 或手动 SQL 添加，TypeORM 不直接支持 `WITH PARSER ngram`。在 Step 3 中处理。

- [ ] **Step 2: Register entity in src/entities.ts**

在 `src/entities.ts` 中，在 `import * as entity29 from './modules/netaclaw/entity/model_channel';` 之后添加：

```typescript
import * as entity30 from './modules/netaclaw/entity/memory';
```

在 `entities` 数组末尾（`]` 之前）添加：

```typescript
  ...Object.values(entity30),
```

- [ ] **Step 3: Create SQL migration for FULLTEXT index**

Create `src/modules/netaclaw/memory/migration.sql` (reference file, run manually or via init):

```sql
-- 在 TypeORM 自动建表后执行，添加 ngram FULLTEXT 索引
ALTER TABLE netaclaw_memory
  ADD FULLTEXT INDEX ft_content (name, content, description) WITH PARSER ngram;
```

- [ ] **Step 4: Commit**

```bash
git add src/modules/netaclaw/entity/memory.ts src/entities.ts src/modules/netaclaw/memory/migration.sql
git commit -m "feat(memory): add NetaClawMemoryEntity with FULLTEXT index migration"
```

---

### Task 3: Implement MysqlMemoryProvider

**Files:**
- Create: `src/modules/netaclaw/memory/mysql_provider.ts`

- [ ] **Step 1: Implement MysqlMemoryProvider**

Create `src/modules/netaclaw/memory/mysql_provider.ts`:

```typescript
import { Repository } from 'typeorm';
import { NetaClawMemoryEntity } from '../entity/memory.js';
import { MemoryProvider, MemoryEntry, MemorySearchOpts } from './provider.js';

function toEntry(e: NetaClawMemoryEntity): MemoryEntry {
  return {
    id: e.id,
    agentName: e.agentName,
    userId: e.userId,
    type: e.type as MemoryEntry['type'],
    name: e.name,
    content: e.content,
    description: e.description,
    metadata: e.metadata,
    createdAt: e.createTime,
    updatedAt: e.updateTime,
  };
}

export class MysqlMemoryProvider implements MemoryProvider {
  constructor(private repo: Repository<NetaClawMemoryEntity>) {}

  async save(entry: Omit<MemoryEntry, 'id' | 'createdAt' | 'updatedAt'>): Promise<MemoryEntry> {
    const saved = await this.repo.save({
      agentName: entry.agentName,
      userId: entry.userId,
      type: entry.type,
      name: entry.name,
      content: entry.content,
      description: entry.description ?? '',
      metadata: entry.metadata,
    });
    return toEntry(saved);
  }

  async update(id: number, partial: Partial<Pick<MemoryEntry, 'name' | 'content' | 'description' | 'type' | 'metadata'>>): Promise<MemoryEntry> {
    await this.repo.update(id, partial);
    const updated = await this.repo.findOneByOrFail({ id });
    return toEntry(updated);
  }

  async delete(id: number): Promise<void> {
    await this.repo.delete(id);
  }

  async search(query: string, opts: MemorySearchOpts): Promise<MemoryEntry[]> {
    const limit = opts.limit ?? 10;
    let qb = this.repo.createQueryBuilder('m')
      .where('m.agentName = :agentName', { agentName: opts.agentName })
      .andWhere('m.userId = :userId', { userId: opts.userId })
      .andWhere(`MATCH(m.name, m.content, m.description) AGAINST(:query IN BOOLEAN MODE)`, { query })
      .orderBy(`MATCH(m.name, m.content, m.description) AGAINST(:query IN BOOLEAN MODE)`, 'DESC')
      .limit(limit);

    if (opts.type) {
      qb = qb.andWhere('m.type = :type', { type: opts.type });
    }

    const results = await qb.getMany();
    return results.map(toEntry);
  }

  async list(opts: MemorySearchOpts): Promise<MemoryEntry[]> {
    const where: any = { agentName: opts.agentName, userId: opts.userId };
    if (opts.type) where.type = opts.type;
    const results = await this.repo.find({
      where,
      order: { updateTime: 'DESC' },
      take: opts.limit ?? 10,
    });
    return results.map(toEntry);
  }

  async getById(id: number): Promise<MemoryEntry | null> {
    const e = await this.repo.findOneBy({ id });
    return e ? toEntry(e) : null;
  }
}
```

- [ ] **Step 2: Verify no TypeScript errors**

```bash
cd packages/backend && npx tsc --noEmit
```

- [ ] **Step 3: Commit**

```bash
git add src/modules/netaclaw/memory/mysql_provider.ts
git commit -m "feat(memory): implement MysqlMemoryProvider with FULLTEXT search"
```

---

### Task 4: Implement SqliteMemoryProvider

**Files:**
- Create: `src/modules/netaclaw/memory/sqlite_provider.ts`

- [ ] **Step 1: Implement SqliteMemoryProvider**

Create `src/modules/netaclaw/memory/sqlite_provider.ts`:

```typescript
import Database from 'better-sqlite3';
import * as fs from 'fs';
import * as path from 'path';
import { MemoryProvider, MemoryEntry, MemorySearchOpts } from './provider.js';

const INIT_SQL = `
CREATE TABLE IF NOT EXISTS memory (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  agent_name TEXT NOT NULL,
  user_id TEXT NOT NULL,
  type TEXT NOT NULL CHECK(type IN ('user', 'project', 'feedback', 'reference')),
  name TEXT NOT NULL,
  content TEXT NOT NULL,
  description TEXT NOT NULL DEFAULT '',
  metadata TEXT,
  created_at TEXT DEFAULT (datetime('now')),
  updated_at TEXT DEFAULT (datetime('now'))
);

CREATE INDEX IF NOT EXISTS idx_agent_user ON memory(agent_name, user_id);
`;

const FTS_SQL = `
CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(
  name, content, description,
  content='memory', content_rowid='id',
  tokenize='trigram'
);
`;

const TRIGGER_SQL = `
CREATE TRIGGER IF NOT EXISTS memory_ai AFTER INSERT ON memory BEGIN
  INSERT INTO memory_fts(rowid, name, content, description)
  VALUES (new.id, new.name, new.content, new.description);
END;

CREATE TRIGGER IF NOT EXISTS memory_ad AFTER DELETE ON memory BEGIN
  INSERT INTO memory_fts(memory_fts, rowid, name, content, description)
  VALUES ('delete', old.id, old.name, old.content, old.description);
END;

CREATE TRIGGER IF NOT EXISTS memory_au AFTER UPDATE ON memory BEGIN
  INSERT INTO memory_fts(memory_fts, rowid, name, content, description)
  VALUES ('delete', old.id, old.name, old.content, old.description);
  INSERT INTO memory_fts(rowid, name, content, description)
  VALUES (new.id, new.name, new.content, new.description);
END;
`;

function toEntry(row: any): MemoryEntry {
  return {
    id: row.id,
    agentName: row.agent_name,
    userId: row.user_id,
    type: row.type,
    name: row.name,
    content: row.content,
    description: row.description,
    metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
    createdAt: new Date(row.created_at),
    updatedAt: new Date(row.updated_at),
  };
}

export class SqliteMemoryProvider implements MemoryProvider {
  private db: Database.Database;

  constructor(dbPath?: string) {
    const resolvedPath = dbPath ?? path.resolve(process.cwd(), 'data/memory/memory.db');
    fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
    this.db = new Database(resolvedPath);
    this.db.pragma('journal_mode = WAL');
    this.db.exec(INIT_SQL);
    this.db.exec(FTS_SQL);
    this.db.exec(TRIGGER_SQL);
  }

  async save(entry: Omit<MemoryEntry, 'id' | 'createdAt' | 'updatedAt'>): Promise<MemoryEntry> {
    const stmt = this.db.prepare(`
      INSERT INTO memory (agent_name, user_id, type, name, content, description, metadata)
      VALUES (?, ?, ?, ?, ?, ?, ?)
    `);
    const info = stmt.run(
      entry.agentName, entry.userId, entry.type,
      entry.name, entry.content, entry.description ?? '',
      entry.metadata ? JSON.stringify(entry.metadata) : null,
    );
    return (await this.getById(info.lastInsertRowid as number))!;
  }

  async update(id: number, partial: Partial<Pick<MemoryEntry, 'name' | 'content' | 'description' | 'type' | 'metadata'>>): Promise<MemoryEntry> {
    const sets: string[] = [];
    const values: any[] = [];
    if (partial.name !== undefined) { sets.push('name = ?'); values.push(partial.name); }
    if (partial.content !== undefined) { sets.push('content = ?'); values.push(partial.content); }
    if (partial.description !== undefined) { sets.push('description = ?'); values.push(partial.description); }
    if (partial.type !== undefined) { sets.push('type = ?'); values.push(partial.type); }
    if (partial.metadata !== undefined) { sets.push('metadata = ?'); values.push(JSON.stringify(partial.metadata)); }
    sets.push("updated_at = datetime('now')");
    values.push(id);
    this.db.prepare(`UPDATE memory SET ${sets.join(', ')} WHERE id = ?`).run(...values);
    return (await this.getById(id))!;
  }

  async delete(id: number): Promise<void> {
    this.db.prepare('DELETE FROM memory WHERE id = ?').run(id);
  }

  async search(query: string, opts: MemorySearchOpts): Promise<MemoryEntry[]> {
    const limit = opts.limit ?? 10;
    // FTS5 trigram tokenizer: wrap query for substring match
    const ftsQuery = `"${query.replace(/"/g, '""')}"`;
    let sql = `
      SELECT m.* FROM memory m
      JOIN memory_fts f ON m.id = f.rowid
      WHERE memory_fts MATCH ?
        AND m.agent_name = ?
        AND m.user_id = ?
    `;
    const params: any[] = [ftsQuery, opts.agentName, opts.userId];
    if (opts.type) { sql += ' AND m.type = ?'; params.push(opts.type); }
    sql += ' ORDER BY rank LIMIT ?';
    params.push(limit);
    const rows = this.db.prepare(sql).all(...params);
    return rows.map(toEntry);
  }

  async list(opts: MemorySearchOpts): Promise<MemoryEntry[]> {
    let sql = 'SELECT * FROM memory WHERE agent_name = ? AND user_id = ?';
    const params: any[] = [opts.agentName, opts.userId];
    if (opts.type) { sql += ' AND type = ?'; params.push(opts.type); }
    sql += ' ORDER BY updated_at DESC LIMIT ?';
    params.push(opts.limit ?? 10);
    const rows = this.db.prepare(sql).all(...params);
    return rows.map(toEntry);
  }

  async getById(id: number): Promise<MemoryEntry | null> {
    const row = this.db.prepare('SELECT * FROM memory WHERE id = ?').get(id);
    return row ? toEntry(row) : null;
  }

  async close(): Promise<void> {
    this.db.close();
  }
}
```

- [ ] **Step 2: Verify no TypeScript errors**

```bash
cd packages/backend && npx tsc --noEmit
```

- [ ] **Step 3: Commit**

```bash
git add src/modules/netaclaw/memory/sqlite_provider.ts
git commit -m "feat(memory): implement SqliteMemoryProvider with FTS5 trigram search"
```

---

### Task 5: Create factory function

**Files:**
- Create: `src/modules/netaclaw/memory/factory.ts`

- [ ] **Step 1: Implement createMemoryProvider**

Create `src/modules/netaclaw/memory/factory.ts`:

```typescript
import { Repository } from 'typeorm';
import { NetaClawMemoryEntity } from '../entity/memory.js';
import { MemoryProvider, AgentMemoryConfig } from './provider.js';
import { MysqlMemoryProvider } from './mysql_provider.js';
import { SqliteMemoryProvider } from './sqlite_provider.js';

export function createMemoryProvider(
  config: AgentMemoryConfig,
  mysqlRepo?: Repository<NetaClawMemoryEntity>,
): MemoryProvider {
  if (config.backend === 'sqlite') {
    return new SqliteMemoryProvider(config.sqlitePath);
  }
  if (!mysqlRepo) {
    throw new Error('MysqlMemoryProvider requires a TypeORM repository');
  }
  return new MysqlMemoryProvider(mysqlRepo);
}
```

- [ ] **Step 2: Commit**

```bash
git add src/modules/netaclaw/memory/factory.ts
git commit -m "feat(memory): add createMemoryProvider factory"
```

---

### Task 6: Create prefetch module

**Files:**
- Create: `src/modules/netaclaw/memory/prefetch.ts`

- [ ] **Step 1: Implement prefetchMemory and formatMemoryContext**

Create `src/modules/netaclaw/memory/prefetch.ts`:

```typescript
import { MemoryProvider, MemoryEntry, MemorySearchOpts } from './provider.js';

export function formatMemoryContext(entries: MemoryEntry[]): string {
  if (entries.length === 0) return '';
  const lines = entries.map(e => `[${e.type}] ${e.name}\n${e.content}`);
  return `以下是与当前对话可能相关的长期记忆：\n\n${lines.join('\n\n')}`;
}

export async function prefetchMemory(
  provider: MemoryProvider,
  userMessage: string,
  agentName: string,
  userId: string,
  limit = 5,
): Promise<string> {
  const opts: MemorySearchOpts = { agentName, userId, limit };
  let entries: MemoryEntry[];
  try {
    entries = await provider.search(userMessage, opts);
  } catch {
    // search 失败时 fallback 到 list（例如 query 为空或 FTS 语法错误）
    entries = await provider.list(opts);
  }
  return formatMemoryContext(entries);
}
```

- [ ] **Step 2: Commit**

```bash
git add src/modules/netaclaw/memory/prefetch.ts
git commit -m "feat(memory): add prefetchMemory with formatted context output"
```

---

### Task 7: Create memory tools (memory_save + memory_recall)

**Files:**
- Create: `src/modules/netaclaw/tools/builtin/memory.ts`

- [ ] **Step 1: Implement memory tools**

Create `src/modules/netaclaw/tools/builtin/memory.ts`:

```typescript
import { Type, Static } from '@sinclair/typebox';
import { AnyAgentTool } from '../common.js';
import { MemoryProvider, MemoryType } from '../../memory/provider.js';

const MemoryTypeSchema = Type.Union([
  Type.Literal('user'),
  Type.Literal('project'),
  Type.Literal('feedback'),
  Type.Literal('reference'),
]);

// --- memory_save ---

const memorySaveParams = Type.Object({
  action: Type.Union([Type.Literal('create'), Type.Literal('update'), Type.Literal('delete')]),
  name: Type.String({ description: '记忆标题' }),
  type: MemoryTypeSchema,
  content: Type.Optional(Type.String({ description: '记忆正文' })),
  description: Type.Optional(Type.String({ description: '一行描述' })),
  id: Type.Optional(Type.Number({ description: '更新/删除时的记忆 ID' })),
});

type MemorySaveParams = Static<typeof memorySaveParams>;

export function createMemorySaveTool(
  provider: MemoryProvider,
  agentName: string,
  userId: string,
): AnyAgentTool {
  return {
    name: 'memory_save',
    label: '保存记忆',
    description: '存储、更新或删除长期记忆。记忆会在未来对话中自动召回。',
    parameters: memorySaveParams,
    async execute(_id: string, params: MemorySaveParams): Promise<string> {
      if (params.action === 'create') {
        const entry = await provider.save({
          agentName,
          userId,
          type: params.type as MemoryType,
          name: params.name,
          content: params.content ?? '',
          description: params.description ?? '',
        });
        return `记忆已保存 (id=${entry.id}): ${entry.name}`;
      }
      if (params.action === 'update') {
        if (!params.id) return '错误: 更新操作需要提供 id';
        const entry = await provider.update(params.id, {
          name: params.name,
          type: params.type as MemoryType,
          content: params.content,
          description: params.description,
        });
        return `记忆已更新 (id=${entry.id}): ${entry.name}`;
      }
      if (params.action === 'delete') {
        if (!params.id) return '错误: 删除操作需要提供 id';
        await provider.delete(params.id);
        return `记忆已删除 (id=${params.id})`;
      }
      return '错误: 未知操作';
    },
  };
}

// --- memory_recall ---

const memoryRecallParams = Type.Object({
  query: Type.String({ description: '搜索关键词' }),
  type: Type.Optional(MemoryTypeSchema),
  limit: Type.Optional(Type.Number({ description: '返回条数，默认 5', default: 5 })),
});

type MemoryRecallParams = Static<typeof memoryRecallParams>;

export function createMemoryRecallTool(
  provider: MemoryProvider,
  agentName: string,
  userId: string,
): AnyAgentTool {
  return {
    name: 'memory_recall',
    label: '检索记忆',
    description: '搜索长期记忆中的相关信息。',
    parameters: memoryRecallParams,
    async execute(_id: string, params: MemoryRecallParams): Promise<string> {
      const entries = await provider.search(params.query, {
        agentName,
        userId,
        type: params.type as MemoryType | undefined,
        limit: params.limit ?? 5,
      });
      if (entries.length === 0) return '未找到相关记忆。';
      return entries.map(e => `[${e.type}] (id=${e.id}) ${e.name}\n${e.content}`).join('\n\n');
    },
  };
}
```

- [ ] **Step 2: Verify no TypeScript errors**

```bash
cd packages/backend && npx tsc --noEmit
```

- [ ] **Step 3: Commit**

```bash
git add src/modules/netaclaw/tools/builtin/memory.ts
git commit -m "feat(memory): add memory_save and memory_recall agent tools"
```

---

### Task 8: Modify runtime/agent.ts to support memoryContext injection

**Files:**
- Modify: `src/modules/netaclaw/runtime/agent.ts`

- [ ] **Step 1: Add memoryContext to AgentRunParams and inject into messages**

In `src/modules/netaclaw/runtime/agent.ts`:

Add `memoryContext?: string;` to the `AgentRunParams` interface (after `history?: LLMMessage[];`):

```typescript
export interface AgentRunParams {
  agentConfig: AgentConfig;
  tools: AnyAgentTool[];
  userMessage: string;
  history?: LLMMessage[];
  memoryContext?: string;
  onToken?: (text: string) => void;
  onThinking?: (text: string) => void;
  onToolCall?: (name: string, args: Record<string, unknown>) => void;
  onToolResult?: (name: string, result: string) => void;
}
```

Update the `runAgent` function destructuring to include `memoryContext`:

```typescript
const { agentConfig, tools, userMessage, history = [], memoryContext,
  onToken, onThinking, onToolCall, onToolResult } = params;
```

Update the messages array — append memoryContext into the single system prompt (Anthropic provider only reads the first system message, so a second system message would be silently dropped):

```typescript
const systemContent = memoryContext
  ? `${agentConfig.systemPrompt}\n\n<memory-context>\n${memoryContext}\n</memory-context>`
  : agentConfig.systemPrompt;

const messages: LLMMessage[] = [
  { role: 'system', content: systemContent },
  ...history,
  { role: 'user', content: userMessage },
];
```

- [ ] **Step 2: Verify no TypeScript errors**

```bash
cd packages/backend && npx tsc --noEmit
```

- [ ] **Step 3: Commit**

```bash
git add src/modules/netaclaw/runtime/agent.ts
git commit -m "feat(memory): inject memoryContext into agent message assembly"
```

---

### Task 9: Integrate memory into controller/chat.ts

**Files:**
- Modify: `src/modules/netaclaw/controller/chat.ts`

- [ ] **Step 1: Add imports, inject repo, wire up prefetch + tools**

In `src/modules/netaclaw/controller/chat.ts`, add imports at the top:

```typescript
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { NetaClawMemoryEntity } from '../entity/memory.js';
import { NetaClawAgentService } from '../service/agent.js';
import { AnyAgentTool } from '../tools/common.js';
import { AgentMemoryConfig } from '../memory/provider.js';
import { createMemoryProvider } from '../memory/factory.js';
import { prefetchMemory } from '../memory/prefetch.js';
import { createMemorySaveTool, createMemoryRecallTool } from '../tools/builtin/memory.js';
```

Add the repository and service injections inside the class (after `skillLoader`):

```typescript
@InjectEntityModel(NetaClawMemoryEntity)
memoryRepo: Repository<NetaClawMemoryEntity>;

@Inject()
agentService: NetaClawAgentService;
```

Update the `chat` method. First, add `userId` to the body type:

```typescript
async chat(@Body() body: { sessionId?: string; message: string; agentName?: string; userId?: string }) {
```

After `const agentName = ...` (line 30), load the agent entity from DB:

```typescript
const agentEntity = await this.agentService.agentRepo.findOneBy({ name: agentName });
```

Then after `const systemPrompt = ...` and before `const agentConfig = ...`, add memory logic:

```typescript
// --- 记忆系统 ---
const memoryConfig: AgentMemoryConfig | undefined =
  (agentEntity?.config as any)?.memory as AgentMemoryConfig | undefined;

let memoryContext: string | undefined;
let memoryTools: AnyAgentTool[] = [];

if (memoryConfig?.enabled) {
  const provider = createMemoryProvider(memoryConfig, this.memoryRepo);
  const userId = body.userId ?? 'anonymous';

  memoryContext = await prefetchMemory(provider, body.message, agentName, userId, memoryConfig.prefetchLimit);
  memoryTools = [
    createMemorySaveTool(provider, agentName, userId),
    createMemoryRecallTool(provider, agentName, userId),
  ];
}
```

Update the systemPrompt to append memory instructions when enabled:

```typescript
let finalSystemPrompt = systemPrompt;
if (memoryConfig?.enabled) {
  finalSystemPrompt += MEMORY_SYSTEM_PROMPT;
}
```

Add the constant at the top of the file (after imports):

```typescript
const MEMORY_SYSTEM_PROMPT = `

## 记忆系统
你拥有长期记忆能力。使用 memory_save 工具存储重要信息，使用 memory_recall 工具检索过往记忆。

记忆类型：
- user: 用户画像（偏好、角色、习惯）
- project: 项目知识（进展、决策、约束）
- feedback: 行为反馈（用户对你行为的纠正或确认）
- reference: 引用（外部资源链接、文档地址）

存储原则：
- 当用户透露个人偏好、角色、习惯时，存为 user 类型
- 当了解到项目进展、决策、约束时，存为 project 类型
- 当用户纠正或确认你的行为时，存为 feedback 类型
- 当提到外部资源链接时，存为 reference 类型
- 更新已有记忆而非创建重复条目
- 只存储对未来对话有价值的信息`;
```

Update the agentConfig to use `finalSystemPrompt`:

```typescript
const agentConfig: AgentConfig = {
  name: agentName,
  systemPrompt: finalSystemPrompt,
  // ... rest unchanged
};
```

Update the tools array to include memory tools:

```typescript
const tools = [bashTool, readFileTool, writeFileTool, listDirTool, ...memoryTools];
```

Update the runAgent call to pass memoryContext:

```typescript
const result = await runAgent({
  agentConfig,
  tools,
  userMessage: body.message,
  history: history.slice(0, -1),
  memoryContext,
});
```

- [ ] **Step 2: Verify no TypeScript errors**

```bash
cd packages/backend && npx tsc --noEmit
```

- [ ] **Step 3: Commit**

```bash
git add src/modules/netaclaw/controller/chat.ts
git commit -m "feat(memory): integrate memory prefetch + tools into chat controller"
```

---

### Task 10: Final verification

- [ ] **Step 1: Verify full TypeScript compilation**

```bash
cd packages/backend && npx tsc --noEmit
```
Expected: No errors.

- [ ] **Step 2: Verify all new files exist**

```bash
ls -la src/modules/netaclaw/memory/
ls -la src/modules/netaclaw/tools/builtin/memory.ts
ls -la src/modules/netaclaw/entity/memory.ts
```

Expected: provider.ts, factory.ts, mysql_provider.ts, sqlite_provider.ts, prefetch.ts, migration.sql in memory/; memory.ts in tools/builtin/; memory.ts in entity/.

- [ ] **Step 3: Commit all remaining changes**

```bash
git add -A && git status
git commit -m "feat(memory): complete long-term memory system implementation"
```
