# 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; 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 extends SessionTreeEntryBase { type: 'compaction'; summary: string; firstKeptEntryId: string; tokensBefore: number; details?: T; fromHook?: boolean; } export interface BranchSummaryEntry extends SessionTreeEntryBase { type: 'branch_summary'; fromId: string; summary: string; details?: T; fromHook?: boolean; } export interface CustomEntry extends SessionTreeEntryBase { type: 'custom'; customType: string; data?: T; } export interface CustomMessageEntry 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; 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。