# Neta Agent 运行时内核实施计划

> **给自动化实施代理：** 必须使用子技能：`superpowers:subagent-driven-development`（推荐）或 `superpowers:executing-plans`，按任务逐项实施本计划。步骤使用复选框（`- [ ]`）语法跟踪进度。

**目标：** 建立 Pi-first 树状会话内核，包括 Pi 兼容 session entry tree、`SessionTreeProvider` 抽象、默认 file provider、MySQL provider、leaf 驱动上下文构建、snapshot 和 agent 级 session provider 选择。

**架构：** 第一原则是“先复刻 Pi 的 session 语义，再适配 Neta provider”。File provider 必须采用 Pi 单文件 JSONL session 协议：第一行 session header，后续每行 session entry；entry 通过 `id/parentId` 组成树；`leafId` 只表示当前位置；历史 entry 原则上 append-only。MySQL provider 必须复刻同一语义，不能反过来要求 file provider 适配数据库表思维。

**技术栈：** Midway/NestJS 风格 service、TypeScript、TypeORM、MySQL、Node fs JSONL、Jest、pnpm。

---

## 架构硬约束

本计划实施时必须遵守以下约束：

- 不兼容旧线性 message/session 接口。
- 旧历史数据允许删除，不做迁移脚本。
- 数据库结构修改实施阶段使用 MCP 直接执行，不新增 SQL 文件作为主路径。
- `file` 与 `mysql` provider 必须通过同一组契约测试。
- `file` provider 必须高强度移植 Pi 的 `SessionManager` 文件协议和纯逻辑。
- 不得把 file provider 设计成 `session.json + entries.jsonl` 双文件。
- 不得把第一版事实源拆成独立 `tool_call/tool_result` entry；tool 状态作为 `AgentMessage` payload 或前端流式投影事件处理。
- 不得只用 `content/summary` 字段表示消息；必须保留完整 `message` payload。
- completed entry 默认不可修改；`updateEntry()` 只允许用于 in-flight 流式补写或明确的 patch 事件。

## Pi 直接复用范围

优先从以下 Pi 文件移植：

- `C:/Users/lixin/Desktop/RZYX_ZT/pi-mono-main/packages/coding-agent/src/core/session-manager.ts`
- `C:/Users/lixin/Desktop/RZYX_ZT/pi-mono-main/packages/coding-agent/src/core/compaction/compaction.ts`
- `C:/Users/lixin/Desktop/RZYX_ZT/pi-mono-main/packages/coding-agent/src/core/compaction/branch-summarization.ts`
- `C:/Users/lixin/Desktop/RZYX_ZT/pi-mono-main/packages/coding-agent/src/core/compaction/utils.ts`

必须移植或等价实现：

- `SessionHeader` / `SessionEntry` union。
- 单文件 JSONL session 协议。
- `parseSessionEntries()`、`loadEntriesFromFile()` 的容错解析。
- leaf-to-root path 构建。
- `buildSessionContext()` 的 compaction、branch summary、custom message、thinking/model setting 处理。
- `branch()`、`resetLeaf()`、`branchWithSummary()`、`createBranchedSession()` 的纯逻辑。
- compaction 的 `firstKeptEntryId`、split turn、previous summary 合并策略。

## 文件结构

新增后端内核模块：

- `packages/backend/src/modules/netaclaw/session-tree/types.ts`
  - 定义 Pi 兼容 session header、entry union、provider DTO、snapshot DTO。
- `packages/backend/src/modules/netaclaw/session-tree/id.ts`
  - 定义 session id 与 entry id 生成器。
- `packages/backend/src/modules/netaclaw/session-tree/path.ts`
  - 定义 entry index、leaf path、tree、common ancestor、label 解析纯函数。
- `packages/backend/src/modules/netaclaw/session-tree/context_builder.ts`
  - 移植 Pi `buildSessionContext()` 语义。
- `packages/backend/src/modules/netaclaw/session-tree/snapshot.ts`
  - 构建前端/API snapshot payload。
- `packages/backend/src/modules/netaclaw/session-tree/provider.ts`
  - 定义 `SessionTreeProvider` 契约。
- `packages/backend/src/modules/netaclaw/session-tree/pi_session_file.ts`
  - 封装 Pi 单文件 JSONL session 文件读写与纯逻辑。
- `packages/backend/src/modules/netaclaw/session-tree/file_provider.ts`
  - 将 `pi_session_file.ts` 适配为 `SessionTreeProvider`。
- `packages/backend/src/modules/netaclaw/session-tree/mysql_provider.ts`
  - 将 MySQL 表适配为同一 `SessionTreeProvider`。
- `packages/backend/src/modules/netaclaw/session-tree/provider_factory.ts`
  - 解析 agent 配置中的 `sessionProvider`。

新增 TypeORM 实体：

- `packages/backend/src/modules/netaclaw/entity/agent_session.ts`
- `packages/backend/src/modules/netaclaw/entity/agent_session_entry.ts`

修改现有文件：

- `packages/backend/src/modules/netaclaw/entity/agent.ts`
- `packages/backend/src/entities.ts`

新增测试：

- `packages/backend/test/session_tree_types.test.ts`
- `packages/backend/test/session_tree_path.test.ts`
- `packages/backend/test/session_tree_context_builder.test.ts`
- `packages/backend/test/session_tree_file_provider.test.ts`
- `packages/backend/test/session_tree_mysql_provider.test.ts`
- `packages/backend/test/session_tree_provider_contract.test.ts`
- `packages/backend/test/session_tree_provider_factory.test.ts`
- `packages/backend/test/entity_exports.test.ts`

## 任务 1：定义 Pi 兼容会话类型

**文件：**

- 新增：`packages/backend/src/modules/netaclaw/session-tree/types.ts`
- 测试：`packages/backend/test/session_tree_types.test.ts`

- [ ] **步骤 1：编写失败的类型消费测试**

```typescript
import type {
  SessionTreeEntry,
  SessionTreeHeader,
  SessionTreeSession,
} from '../src/modules/netaclaw/session-tree/types.js';

describe('session tree types', () => {
  it('represents a Pi-compatible session header and entries', () => {
    const header: SessionTreeHeader = {
      type: 'session',
      version: 1,
      id: 's1',
      timestamp: '2026-04-19T00:00:00.000Z',
      cwd: 'C:/workspace',
    };

    const entry: SessionTreeEntry = {
      type: 'message',
      id: 'e1',
      parentId: null,
      timestamp: '2026-04-19T00:00:00.000Z',
      message: { role: 'user', content: 'hello' },
    };

    const session: SessionTreeSession = {
      sessionId: header.id,
      provider: 'file',
      rootEntryId: entry.id,
      leafEntryId: entry.id,
      cwd: header.cwd,
      status: 'active',
      createdAt: header.timestamp,
      updatedAt: header.timestamp,
    };

    expect(session.leafEntryId).toBe(entry.id);
  });
});
```

- [ ] **步骤 2：运行测试确认失败**

```bash
pnpm --filter @neta/backend test -- --runInBand test/session_tree_types.test.ts
```

预期：

- 失败，原因是 `session-tree/types.js` 尚不存在。

- [ ] **步骤 3：实现 `types.ts`**

核心类型必须包含：

```typescript
export type SessionTreeProviderKind = 'file' | 'mysql';

export interface SessionTreeHeader {
  type: 'session';
  version: number;
  id: string;
  timestamp: string;
  cwd: string;
  parentSession?: string;
}

export interface SessionTreeEntryBase {
  type: string;
  id: string;
  parentId: string | null;
  timestamp: string;
}

export interface SessionTreeMessage {
  role: string;
  content?: unknown;
  provider?: string;
  model?: string;
  usage?: Record<string, unknown>;
  stopReason?: string;
  timestamp?: number;
  [key: string]: unknown;
}

export interface SessionMessageEntry extends SessionTreeEntryBase {
  type: 'message';
  message: SessionTreeMessage;
}

export interface ThinkingLevelChangeEntry extends SessionTreeEntryBase {
  type: 'thinking_level_change';
  thinkingLevel: string;
}

export interface ModelChangeEntry extends SessionTreeEntryBase {
  type: 'model_change';
  provider: string;
  modelId: string;
}

export interface CompactionEntry<T = unknown> extends SessionTreeEntryBase {
  type: 'compaction';
  summary: string;
  firstKeptEntryId: string;
  tokensBefore: number;
  details?: T;
  fromHook?: boolean;
}

export interface BranchSummaryEntry<T = unknown> extends SessionTreeEntryBase {
  type: 'branch_summary';
  fromId: string;
  summary: string;
  details?: T;
  fromHook?: boolean;
}

export interface CustomEntry<T = unknown> extends SessionTreeEntryBase {
  type: 'custom';
  customType: string;
  data?: T;
}

export interface CustomMessageEntry<T = unknown> extends SessionTreeEntryBase {
  type: 'custom_message';
  customType: string;
  content: unknown;
  details?: T;
  display: boolean;
}

export interface LabelEntry extends SessionTreeEntryBase {
  type: 'label';
  targetId: string;
  label: string | undefined;
}

export interface SessionInfoEntry extends SessionTreeEntryBase {
  type: 'session_info';
  name?: string;
}

export type SessionTreeEntry =
  | SessionMessageEntry
  | ThinkingLevelChangeEntry
  | ModelChangeEntry
  | CompactionEntry
  | BranchSummaryEntry
  | CustomEntry
  | CustomMessageEntry
  | LabelEntry
  | SessionInfoEntry;

export interface SessionTreeSession {
  sessionId: string;
  provider: SessionTreeProviderKind;
  rootEntryId: string | null;
  leafEntryId: string | null;
  cwd?: string | null;
  sessionFile?: string | null;
  parentSessionId?: string | null;
  agentId?: number | null;
  userId?: string | null;
  title?: string | null;
  status: 'active' | 'archived' | 'deleted';
  metadata?: Record<string, unknown>;
  createdAt: string;
  updatedAt: string;
}
```

- [ ] **步骤 4：运行测试确认通过**

```bash
pnpm --filter @neta/backend test -- --runInBand test/session_tree_types.test.ts
```

预期：

- 通过。

- [ ] **步骤 5：提交变更**

```bash
git add packages/backend/src/modules/netaclaw/session-tree/types.ts packages/backend/test/session_tree_types.test.ts
git commit -m "feat(agent-runtime): add pi-compatible session tree types"
```

## 任务 2：实现 ID 与树路径纯函数

**文件：**

- 新增：`packages/backend/src/modules/netaclaw/session-tree/id.ts`
- 新增：`packages/backend/src/modules/netaclaw/session-tree/path.ts`
- 测试：`packages/backend/test/session_tree_path.test.ts`

- [ ] **步骤 1：编写路径测试**

```typescript
import {
  buildEntryIndex,
  findCommonAncestorEntryId,
  getPathToLeaf,
  groupChildrenByParent,
  resolveLatestLabels,
} from '../src/modules/netaclaw/session-tree/path.js';
import type { SessionTreeEntry } from '../src/modules/netaclaw/session-tree/types.js';

const entry = (id: string, parentId: string | null): SessionTreeEntry => ({
  type: 'message',
  id,
  parentId,
  timestamp: `2026-04-19T00:00:0${id.length}.000Z`,
  message: { role: 'user', content: id },
});

describe('session tree path utilities', () => {
  it('builds root-to-leaf path', () => {
    expect(getPathToLeaf([entry('a', null), entry('b', 'a'), entry('c', 'b')], 'c').map(e => e.id)).toEqual([
      'a',
      'b',
      'c',
    ]);
  });

  it('rejects missing leaf and cycles', () => {
    expect(() => getPathToLeaf([entry('a', null)], 'missing')).toThrow('Entry missing not found');
    expect(() => getPathToLeaf([entry('a', 'c'), entry('b', 'a'), entry('c', 'b')], 'c')).toThrow('Cycle detected');
  });

  it('finds common ancestor', () => {
    const entries = [entry('a', null), entry('b', 'a'), entry('c', 'b'), entry('d', 'b')];
    expect(findCommonAncestorEntryId(entries, 'c', 'd')).toBe('b');
  });

  it('groups children and resolves latest labels', () => {
    const label1: SessionTreeEntry = {
      type: 'label',
      id: 'l1',
      parentId: 'a',
      timestamp: '2026-04-19T00:00:02.000Z',
      targetId: 'a',
      label: 'old',
    };
    const label2: SessionTreeEntry = {
      type: 'label',
      id: 'l2',
      parentId: 'l1',
      timestamp: '2026-04-19T00:00:03.000Z',
      targetId: 'a',
      label: 'new',
    };
    expect(groupChildrenByParent([entry('a', null), entry('b', 'a')]).__root__).toEqual(['a']);
    expect(resolveLatestLabels([entry('a', null), label1, label2]).get('a')?.label).toBe('new');
    expect(buildEntryIndex([entry('a', null)]).get('a')?.id).toBe('a');
  });
});
```

- [ ] **步骤 2：实现 `id.ts` 与 `path.ts`**

实现要求：

- `createSessionTreeSessionId()` 使用 `uuidv7()` 或等价时间有序 UUID。
- `createSessionTreeEntryId(existingIds)` 生成短 ID，并检查碰撞。
- `getPathToLeaf()` 必须检测 missing leaf、missing parent、cycle。
- `groupChildrenByParent()` 必须以 append 顺序稳定排序。
- `resolveLatestLabels()` 必须以后写 label 覆盖旧 label。

- [ ] **步骤 3：运行测试确认通过**

```bash
pnpm --filter @neta/backend test -- --runInBand test/session_tree_path.test.ts
```

预期：

- 通过。

- [ ] **步骤 4：提交变更**

```bash
git add packages/backend/src/modules/netaclaw/session-tree/id.ts packages/backend/src/modules/netaclaw/session-tree/path.ts packages/backend/test/session_tree_path.test.ts
git commit -m "feat(agent-runtime): add session tree path utilities"
```

## 任务 3：移植上下文构建语义

**文件：**

- 新增：`packages/backend/src/modules/netaclaw/session-tree/context_builder.ts`
- 测试：`packages/backend/test/session_tree_context_builder.test.ts`

- [ ] **步骤 1：编写上下文构建测试**

```typescript
import { buildSessionContext } from '../src/modules/netaclaw/session-tree/context_builder.js';
import type { SessionTreeEntry } from '../src/modules/netaclaw/session-tree/types.js';

const message = (id: string, parentId: string | null, role: string, content: string): SessionTreeEntry => ({
  type: 'message',
  id,
  parentId,
  timestamp: `2026-04-19T00:00:00.000Z`,
  message: { role, content },
});

describe('session tree context builder', () => {
  it('walks the active path and excludes sibling branches', () => {
    const entries = [
      message('u1', null, 'user', 'root'),
      message('a1', 'u1', 'assistant', 'branch a'),
      message('a2', 'u1', 'assistant', 'branch b'),
    ];
    expect(buildSessionContext(entries, 'a2').messages.map(m => m.content)).toEqual(['root', 'branch b']);
  });

  it('handles compaction using firstKeptEntryId', () => {
    const entries: SessionTreeEntry[] = [
      message('u1', null, 'user', 'old'),
      message('u2', 'u1', 'user', 'kept'),
      {
        type: 'compaction',
        id: 'c1',
        parentId: 'u2',
        timestamp: '2026-04-19T00:00:01.000Z',
        summary: 'summary',
        firstKeptEntryId: 'u2',
        tokensBefore: 1000,
      },
      message('a1', 'c1', 'assistant', 'after'),
    ];
    expect(buildSessionContext(entries, 'a1').messages.map(m => m.role)).toEqual([
      'compactionSummary',
      'user',
      'assistant',
    ]);
    expect(buildSessionContext(entries, 'a1').messages.map(m => m.content ?? m.summary)).toEqual([
      'summary',
      'kept',
      'after',
    ]);
  });

  it('tracks thinking and model settings without adding them as messages', () => {
    const entries: SessionTreeEntry[] = [
      message('u1', null, 'user', 'hello'),
      { type: 'thinking_level_change', id: 't1', parentId: 'u1', timestamp: '2026-04-19T00:00:01.000Z', thinkingLevel: 'high' },
      { type: 'model_change', id: 'm1', parentId: 't1', timestamp: '2026-04-19T00:00:02.000Z', provider: 'openai', modelId: 'gpt-x' },
    ];
    const context = buildSessionContext(entries, 'm1');
    expect(context.messages).toEqual([{ role: 'user', content: 'hello' }]);
    expect(context.thinkingLevel).toBe('high');
    expect(context.model).toEqual({ provider: 'openai', modelId: 'gpt-x' });
  });
});
```

- [ ] **步骤 2：实现 `context_builder.ts`**

实现要求：

- 以 Pi `buildSessionContext(entries, leafId, byId)` 为基准移植。
- 必须先从 leaf 回溯到 root 得到 path。
- 必须解析 `thinking_level_change`、`model_change`、assistant message provider/model。
- 必须识别 active path 上最新 compaction。
- 有 compaction 时输出：compaction summary、`firstKeptEntryId` 到 compaction 前的保留消息、compaction 后消息。
- 无 compaction 时输出：message、custom_message、branch_summary。
- label、session_info、custom、thinking_level_change、model_change 不直接生成 LLM message。
- 返回值包含 `messages`、`thinkingLevel`、`model`、`sourceEntryIds`。

- [ ] **步骤 3：运行测试确认通过**

```bash
pnpm --filter @neta/backend test -- --runInBand test/session_tree_context_builder.test.ts
```

预期：

- 通过。

- [ ] **步骤 4：提交变更**

```bash
git add packages/backend/src/modules/netaclaw/session-tree/context_builder.ts packages/backend/test/session_tree_context_builder.test.ts
git commit -m "feat(agent-runtime): add pi-style session context builder"
```

## 任务 4：定义 Provider 契约与 Snapshot

**文件：**

- 新增：`packages/backend/src/modules/netaclaw/session-tree/provider.ts`
- 新增：`packages/backend/src/modules/netaclaw/session-tree/snapshot.ts`
- 测试：`packages/backend/test/session_tree_provider_contract.test.ts`

- [ ] **步骤 1：编写 provider contract 工厂**

契约测试必须作为函数导出，由 file/mysql provider 测试复用：

```typescript
import type { SessionTreeProvider } from '../src/modules/netaclaw/session-tree/provider.js';

export function runSessionTreeProviderContract(name: string, createProvider: () => SessionTreeProvider) {
  describe(`${name} session tree provider contract`, () => {
    it('creates a session, appends messages, and advances leaf', async () => {
      const provider = createProvider();
      await provider.createSession({ sessionId: 's1', provider: name === 'mysql' ? 'mysql' : 'file', cwd: 'C:/workspace' });
      const user = await provider.appendMessage('s1', { role: 'user', content: 'hello' });
      const assistant = await provider.appendMessage('s1', { role: 'assistant', content: 'hi' });
      expect((await provider.getSession('s1'))?.leafEntryId).toBe(assistant.id);
      expect((await provider.getActivePath('s1')).map(e => e.id)).toEqual([user.id, assistant.id]);
    });

    it('branches without deleting sibling history', async () => {
      const provider = createProvider();
      await provider.createSession({ sessionId: 's1', provider: name === 'mysql' ? 'mysql' : 'file' });
      const root = await provider.appendMessage('s1', { role: 'user', content: 'root' });
      const branchA = await provider.appendMessage('s1', { role: 'assistant', content: 'a' });
      await provider.switchLeaf('s1', root.id);
      const branchB = await provider.appendMessage('s1', { role: 'assistant', content: 'b' });
      expect((await provider.listEntries('s1')).map(e => e.id).sort()).toEqual([root.id, branchA.id, branchB.id].sort());
      expect((await provider.getActivePath('s1')).map(e => e.id)).toEqual([root.id, branchB.id]);
    });

    it('supports branch summary, compaction, labels, and resetLeaf', async () => {
      const provider = createProvider();
      await provider.createSession({ sessionId: 's1', provider: name === 'mysql' ? 'mysql' : 'file' });
      const root = await provider.appendMessage('s1', { role: 'user', content: 'root' });
      await provider.appendLabelChange('s1', root.id, '入口');
      const kept = await provider.appendMessage('s1', { role: 'user', content: 'kept' });
      await provider.appendCompaction('s1', { summary: 'summary', firstKeptEntryId: kept.id, tokensBefore: 1000 });
      await provider.switchLeaf('s1', root.id);
      const summary = await provider.appendBranchSummary('s1', { branchFromEntryId: root.id, summary: 'branch summary' });
      expect(summary.type).toBe('branch_summary');
      await provider.resetLeaf('s1');
      const newRoot = await provider.appendMessage('s1', { role: 'user', content: 'new root' });
      expect(newRoot.parentId).toBeNull();
      expect((await provider.getSnapshot('s1')).labelsByEntryId[root.id]?.label).toBe('入口');
    });
  });
}
```

- [ ] **步骤 2：实现 `provider.ts` 与 `snapshot.ts`**

Provider 必须包含：

- `createSession()`
- `getSession()`
- `updateSession()`
- `deleteSession()`
- `listEntries()`
- `appendEntry()`
- `appendMessage()`
- `appendThinkingLevelChange()`
- `appendModelChange()`
- `appendCompaction()`
- `appendBranchSummary()`
- `appendLabelChange()`
- `appendSessionInfo()`
- `updateInFlightEntry()`
- `switchLeaf()`
- `resetLeaf()`
- `createBranchedSession()`
- `getActivePath()`
- `getSnapshot()`

Snapshot 必须包含：

- `session`
- `entries`
- `activePath`
- `childrenByParentId`
- `labelsByEntryId`
- `runtimeContext`

- [ ] **步骤 3：运行契约 scaffold 测试**

```bash
pnpm --filter @neta/backend test -- --runInBand test/session_tree_provider_contract.test.ts
```

预期：

- 如果只是导出 contract 工厂且未实例化 provider，则通过。

- [ ] **步骤 4：提交变更**

```bash
git add packages/backend/src/modules/netaclaw/session-tree/provider.ts packages/backend/src/modules/netaclaw/session-tree/snapshot.ts packages/backend/test/session_tree_provider_contract.test.ts
git commit -m "feat(agent-runtime): define session tree provider contract"
```

## 任务 5：实现 Pi 单文件 JSONL File Provider

**文件：**

- 新增：`packages/backend/src/modules/netaclaw/session-tree/pi_session_file.ts`
- 新增：`packages/backend/src/modules/netaclaw/session-tree/file_provider.ts`
- 测试：`packages/backend/test/session_tree_file_provider.test.ts`

- [ ] **步骤 1：编写 file provider 测试**

测试必须验证真实文件协议：

```typescript
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { FileSessionTreeProvider } from '../src/modules/netaclaw/session-tree/file_provider.js';
import { runSessionTreeProviderContract } from './session_tree_provider_contract.js';

describe('FileSessionTreeProvider', () => {
  let dir: string;

  beforeEach(() => {
    dir = mkdtempSync(join(tmpdir(), 'neta-session-tree-'));
  });

  afterEach(() => {
    rmSync(dir, { recursive: true, force: true });
  });

  runSessionTreeProviderContract('file', () => new FileSessionTreeProvider({ rootDir: dir }));

  it('stores one JSONL file with session header as first line', async () => {
    const provider = new FileSessionTreeProvider({ rootDir: dir });
    await provider.createSession({ sessionId: 's1', provider: 'file', cwd: 'C:/workspace' });
    await provider.appendMessage('s1', { role: 'user', content: 'hello' });
    const session = await provider.getSession('s1');
    const content = readFileSync(session!.sessionFile!, 'utf-8').trim().split(/\r?\n/).map(line => JSON.parse(line));
    expect(content[0].type).toBe('session');
    expect(content[1].type).toBe('message');
    expect(content[1].parentId).toBeNull();
  });
});
```

- [ ] **步骤 2：实现 `pi_session_file.ts`**

实现要求：

- 迁移 Pi 的 `parseSessionEntries()`、`loadEntriesFromFile()`、`getTree()`、`branch()`、`resetLeaf()`、`branchWithSummary()`、`createBranchedSession()` 思路。
- 单 session 对应一个 `.jsonl` 文件。
- header 第一行只写一次。
- append entry 只追加一行。
- label 通过 label entry 解析，不修改 target entry。
- `updateInFlightEntry()` 不得频繁重写整个文件；首版可以将 in-flight entry 在内存中合并，完成时追加 completed entry。

- [ ] **步骤 3：实现 `file_provider.ts`**

实现要求：

- `FileSessionTreeProvider` 只做 `SessionTreeProvider` 到 `pi_session_file.ts` 的适配。
- `appendMessage()` 等语义方法必须调用同一 append entry 通道。
- `createBranchedSession()` 必须复制 root 到 leaf 路径并保留路径内 label。

- [ ] **步骤 4：运行测试确认通过**

```bash
pnpm --filter @neta/backend test -- --runInBand test/session_tree_file_provider.test.ts
```

预期：

- 通过。

- [ ] **步骤 5：提交变更**

```bash
git add packages/backend/src/modules/netaclaw/session-tree/pi_session_file.ts packages/backend/src/modules/netaclaw/session-tree/file_provider.ts packages/backend/test/session_tree_file_provider.test.ts
git commit -m "feat(agent-runtime): add pi-style file session provider"
```

## 任务 6：新增 MySQL 实体并通过 MCP 更新数据库

**文件：**

- 新增：`packages/backend/src/modules/netaclaw/entity/agent_session.ts`
- 新增：`packages/backend/src/modules/netaclaw/entity/agent_session_entry.ts`
- 修改：`packages/backend/src/entities.ts`
- 测试：`packages/backend/test/entity_exports.test.ts`

- [ ] **步骤 1：扩展实体导出测试**

```typescript
import { entities } from '../src/entities.js';
import { NetaClawAgentSessionEntity } from '../src/modules/netaclaw/entity/agent_session.js';
import { NetaClawAgentSessionEntryEntity } from '../src/modules/netaclaw/entity/agent_session_entry.js';

describe('entities exports', () => {
  it('exports session tree entities', () => {
    expect(entities).toContain(NetaClawAgentSessionEntity);
    expect(entities).toContain(NetaClawAgentSessionEntryEntity);
  });
});
```

- [ ] **步骤 2：新增实体**

实体要求：

- `agent_session.ts` 字段：`sessionId`、`provider`、`rootEntryId`、`leafEntryId`、`cwd`、`sessionFile`、`parentSessionId`、`agentId`、`userId`、`title`、`status`、`metadata`。
- `agent_session_entry.ts` 字段：`sessionId`、`entryId`、`parentEntryId`、`entrySeq`、`type`、`message`、`summary`、`firstKeptEntryId`、`tokensBefore`、`thinkingLevel`、`provider`、`modelId`、`customType`、`data`、`display`、`targetEntryId`、`label`、`details`、`sourceRuntime`、`status`。
- 两个实体继承 `packages/backend/src/modules/base/entity/base.ts` 中的 `BaseEntity`。
- `sessionId + entryId` 必须唯一。
- `entrySeq` 用于稳定排序，不能只依赖 `createTime` 字符串。

- [ ] **步骤 3：更新实体导出**

在 `packages/backend/src/entities.ts` 中按现有风格导入并展开两个实体模块。

- [ ] **步骤 4：使用 MCP 修改数据库**

执行要求：

- 使用 MCP MySQL 工具直接删除旧历史会话/消息数据。
- 使用 MCP MySQL 工具创建或重建 `neta_agent_session` 与 `neta_agent_session_entry`。
- 不新增 SQL migration 文件作为主路径。
- 修改后用 MCP `describe_table` 确认字段存在。

- [ ] **步骤 5：运行测试确认通过**

```bash
pnpm --filter @neta/backend test -- --runInBand test/entity_exports.test.ts
```

预期：

- 通过。

- [ ] **步骤 6：提交变更**

```bash
git add packages/backend/src/modules/netaclaw/entity/agent_session.ts packages/backend/src/modules/netaclaw/entity/agent_session_entry.ts packages/backend/src/entities.ts packages/backend/test/entity_exports.test.ts
git commit -m "feat(agent-runtime): add session tree mysql entities"
```

## 任务 7：实现 MySQL Provider

**文件：**

- 新增：`packages/backend/src/modules/netaclaw/session-tree/mysql_provider.ts`
- 测试：`packages/backend/test/session_tree_mysql_provider.test.ts`

- [ ] **步骤 1：编写 MySQL provider 测试**

测试要求：

- 复用 `runSessionTreeProviderContract('mysql', ...)`。
- 使用 repository mock 或 Midway mock repository。
- 验证 `entrySeq` 稳定排序。
- 验证 `message` JSON 无损保存。
- 验证 compaction 与 file provider 的 context 结果一致。

- [ ] **步骤 2：实现 `mysql_provider.ts`**

实现要求：

- MySQL provider 是 Pi entry log 的表映射，不是独立语义。
- `appendEntry()` 创建新 row，默认推进 leaf。
- `appendBranchSummary()` 等价于 Pi `branchWithSummary()`。
- `appendLabelChange()` 追加 label entry，不修改 target entry。
- `createBranchedSession()` 复制 root 到 leaf path 到新 session。
- completed entry 禁止普通 update。

- [ ] **步骤 3：运行测试确认通过**

```bash
pnpm --filter @neta/backend test -- --runInBand test/session_tree_mysql_provider.test.ts
```

预期：

- 通过。

- [ ] **步骤 4：提交变更**

```bash
git add packages/backend/src/modules/netaclaw/session-tree/mysql_provider.ts packages/backend/test/session_tree_mysql_provider.test.ts
git commit -m "feat(agent-runtime): add mysql session tree provider"
```

## 任务 8：接入 Agent 配置中的 Session Provider

**文件：**

- 新增：`packages/backend/src/modules/netaclaw/session-tree/provider_factory.ts`
- 修改：`packages/backend/src/modules/netaclaw/entity/agent.ts`
- 测试：`packages/backend/test/session_tree_provider_factory.test.ts`

- [ ] **步骤 1：编写 provider factory 测试**

```typescript
import { resolveSessionProviderKind } from '../src/modules/netaclaw/session-tree/provider_factory.js';

describe('session tree provider factory', () => {
  it('defaults to file provider', () => {
    expect(resolveSessionProviderKind(null)).toBe('file');
    expect(resolveSessionProviderKind({})).toBe('file');
  });

  it('accepts file and mysql only', () => {
    expect(resolveSessionProviderKind({ sessionProvider: 'file' })).toBe('file');
    expect(resolveSessionProviderKind({ sessionProvider: 'mysql' })).toBe('mysql');
    expect(resolveSessionProviderKind({ sessionProvider: 'redis' })).toBe('file');
  });
});
```

- [ ] **步骤 2：实现 provider factory 与 agent 字段**

实现要求：

- `resolveSessionProviderKind()` 只接受 `file` 与 `mysql`。
- `NetaClawAgentEntity` 新增 `sessionProvider` 字段，默认 `file`。
- 字段注释使用中文，避免现有乱码注释继续扩散。

- [ ] **步骤 3：运行测试确认通过**

```bash
pnpm --filter @neta/backend test -- --runInBand test/session_tree_provider_factory.test.ts
```

预期：

- 通过。

- [ ] **步骤 4：提交变更**

```bash
git add packages/backend/src/modules/netaclaw/session-tree/provider_factory.ts packages/backend/src/modules/netaclaw/entity/agent.ts packages/backend/test/session_tree_provider_factory.test.ts
git commit -m "feat(agent-runtime): add session provider selection"
```

## 任务 9：运行聚焦验证

**文件：**

- 不预期修改源码。

- [ ] **步骤 1：运行新 session-tree 测试**

```bash
pnpm --filter @neta/backend test -- --runInBand test/session_tree_types.test.ts test/session_tree_path.test.ts test/session_tree_context_builder.test.ts test/session_tree_file_provider.test.ts test/session_tree_mysql_provider.test.ts test/session_tree_provider_factory.test.ts test/entity_exports.test.ts
```

预期：

- 全部通过。

- [ ] **步骤 2：运行相关旧测试**

```bash
pnpm --filter @neta/backend test -- --runInBand test/netaclaw_session.test.ts test/chat_orchestrator.test.ts test/subagent_service.test.ts
```

预期：

- 全部通过，或明确记录旧接口将被后续计划替换导致的失败原因。

- [ ] **步骤 3：运行后端构建**

```bash
pnpm --filter @neta/backend build
```

预期：

- 构建成功。

- [ ] **步骤 4：检查工作区**

```bash
git status --short
```

预期：

- 只包含本任务相关变更。

## 自检

- 设计覆盖：本计划覆盖运行时内核子集，包括 Pi 兼容 entry tree、file provider、MySQL provider、context builder、snapshot 和 agent provider 选择。
- 直接复用：明确要求移植 Pi `SessionManager`、context builder、branch、compaction 关键语义。
- 范围控制：不包含对话 WebSocket 协议、前端 UI、subagent 进程编排、skill/tool/model 资源层、管理后台页面和视觉系统。
- 占位内容检查：没有 `TBD`、`TODO`、`implement later`。
- 类型一致性：统一使用 `id/parentId` 表达 Pi entry，使用 `sessionId/rootEntryId/leafEntryId` 表达 Neta session meta。
- 架构防错：明确禁止 `session.json + entries.jsonl`、禁止 completed entry 任意 update、禁止把 tool_call/tool_result 作为第一版事实源 entry。
