Ctrl K
ComunidadGuías…

docs/plan/2026-09-27-side-task-context-mode-design.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# 边路任务上下文继承模式技术方案

- 日期:2026-09-27
- 状态:已与需求方达成共识,待实施
- 涉及端:backend-ts(后端)、desktop(桌面 / Web / 安卓共用 UI)
- 不涉及:admin(管理后台)、android 原生壳、agent-cli、Electron 主进程

---

## 1. 需求背景

当前新建边路任务时,用户通过一个 checkbox("继承主任务上下文")选择是否注入主任务的上下文摘要。该设计只有"注入摘要"和"不注入"两种状态,无法满足"完整继承主会话上下文并看到历史消息"的场景。

用户希望在新建边路任务时提供三选一的单选控件,将上下文继承策略扩展为三种模式:

| 模式 | 说明 |
| --- | --- |
| **Fork 主会话** | 物理复制主会话的全部消息、file_change、compaction 记录到边路会话,边路 Agent 获得完整上下文,用户在对话页面能看到历史消息 |
| **主会话摘要** | 保持现有注入逻辑:取主会话最近 10 条消息、每条截断 300 字,拼成摘要注入 system prompt |
| **不继承** | 全新对话,不注入任何上下文 |

---

## 2. 需求描述

### 2.1 要做的

1. **前端(desktop)**:
   - 将 `SideChatPanel.vue` 中现有的 `el-checkbox`(继承主任务上下文)替换为 `el-radio-group`,三个选项:Fork 主会话 / 主会话摘要 / 不继承。
   - 默认选中"不继承"。
   - 该选择器仅在边路任务首条消息发送前显示(`v-if="!hasRealSession && displayMessages.length === 0"`),发送后隐藏,与现有逻辑一致。
   - `useStreamWS.ts` 的 `createSideSession()` 函数参数从 `inheritContext: boolean` 改为 `contextMode: 'fork' | 'summary' | 'none'`,WS 消息 `data.inheritContext` 字段替换为 `data.contextMode`。
   - Fork 模式下,前端在收到 `side_session_created` 事件后需重新拉取边路会话的消息列表(`getMessagesByRounds`),以展示被复制过来的历史消息。

2. **后端(backend-ts)**:
   - `StreamingWsHandler.handleCreateSideSession()`:解析 `data.contextMode`(`'fork' | 'summary' | 'none'`),替换现有的 `data.inheritContext === true` 逻辑。
   - `HarnessService.executeSideFirstMessage()`:参数从 `inheritContext: boolean` 改为 `contextMode: string`,根据模式执行不同逻辑:
     - `fork`:调用新方法 `forkParentMessages()`,物理复制主会话全部消息、file_change、compaction 记录到边路会话,然后正常构建上下文执行(不额外注入摘要,因为消息已经在会话内)。
     - `summary`:保持现有 `generateContextSummary()` 逻辑不变。
     - `none`:跳过任何上下文注入。
   - 新增 `forkParentMessages(parentSessionId, sideSessionId)` 方法,在事务内复制:
     - 主会话全部 `messages`(维护 `messageIdMap` 映射旧 ID → 新 ID)
     - 主会话全部 `file_change`(通过 `messageIdMap` 重映射 `messageId`)
     - 主会话 `session_compaction` 记录(将 `sessionId` 替换为边路会话 ID)
   - Fork 完成后,再保存用户输入的边路任务指令消息,然后触发 Agent 执行。

### 2.2 明确不做的

| 不做项 | 说明 |
| --- | --- |
| 数据库 schema 变更 | 不在 sessions 表新增 context_mode 列,不新增 Flyway 迁移 |
| 旧字段 inheritContext 兼容 | 不保留旧字段,不做后端兼容映射,前后端同步替换 |
| Fork 消息来源标记 | 复制过来的消息与新消息无差别展示,不加"来自主会话"等视觉标记 |
| Fork 消息数量限制 | 不限制复制条数,有多少复制多少 |
| 摘要逻辑改动 | "主会话摘要"模式保持现有逻辑(最近 10 条、每条 300 字截断),不改用 LLM 生成 |
| promote 功能改动 | 不改动现有的 promoteSideTaskToMainSession 逻辑 |
| 边路任务删除/归档等周边 | 不做其他边路任务管理功能的调整 |
| admin 管理后台改动 | 不在管理后台展示或筛选 contextMode |

---

## 3. 技术选型

| 决策点 | 选型 | 理由 |
| --- | --- | --- |
| Fork 实现方式 | 物理复制消息 | 与现有 `promoteSideTaskToMainSession` 的消息复制逻辑一致,边路会话自包含,后续操作互不影响 |
| Fork 复制范围 | 全部消息 + file_change + compaction | 完整继承主会话上下文,Agent 和用户都能看到一致的历史 |
| 前端控件 | el-radio-group | 三选一场景最直观,比下拉省一次点击 |
| 默认选项 | 不继承 | 与现有 `inheritContext = ref(false)` 行为一致 |
| WS 字段 | `contextMode: 'fork' \| 'summary' \| 'none'` 直接替换 `inheritContext` | 同一套代码库前后端同步改,无需兼容旧客户端 |
| Fork 执行时机 | 用户发送边路任务指令时,后端先复制再执行 | 一气呵成,用户体验最好 |
| Fork 逻辑位置 | 后端 `HarnessService` | 与现有 `executeSideFirstMessage` 流程一致,前端只需传 mode |

---

## 4. 实现步骤

### 4.1 后端(backend-ts)

#### 步骤 1:修改 `HarnessService.executeSideFirstMessage`

**文件**:`backend-ts/src/harness/core/harness-service.ts:539-556`

将方法签名从:

```typescript
async executeSideFirstMessage(
  parentSessionId: number,
  sideSessionId: number,
  inheritContext: boolean,
  listener: AgentEventListener,
  cancelFlag?: AtomicBoolean | null,
): Promise<void>
```

改为:

```typescript
async executeSideFirstMessage(
  parentSessionId: number,
  sideSessionId: number,
  contextMode: 'fork' | 'summary' | 'none',
  listener: AgentEventListener,
  cancelFlag?: AtomicBoolean | null,
): Promise<void>
```

方法体逻辑:

```typescript
const context = await this.buildContext(sideSessionId, listener, cancelFlag);

if (contextMode === 'fork') {
  await this.forkParentMessages(parentSessionId, sideSessionId);
  // fork 后消息已在会话内,buildContext 已包含完整历史,无需额外注入
  // 但需要重建 context(因为 fork 后消息变了)
  context = await this.buildContext(sideSessionId, listener, cancelFlag);
} else if (contextMode === 'summary') {
  const contextSummary = await this.generateContextSummary(parentSessionId);
  if (hasText(contextSummary)) {
    context.systemPrompt = (context.systemPrompt ?? '')
      + '\n\n<主任务背景摘要>\n' + contextSummary + '\n</主任务背景摘要>\n'
      + '以上是主任务的最近对话摘要,本次边路任务的结果不需要反馈到主任务。';
    context.preparedRequest = null;
  }
}
// contextMode === 'none': 跳过任何注入

const persistenceCallback = this.createPersistenceCallback(sideSessionId, context);
await this.agentLoop.execute(context, listener, persistenceCallback);
if (cancelFlag != null) this.agentLoop.removeCancelFlag(sideSessionId);
```

#### 步骤 2:新增 `forkParentMessages` 方法

**文件**:`backend-ts/src/harness/core/harness-service.ts`

```typescript
/**
 * 物理复制主会话的全部消息、file_change、compaction 记录到边路会话。
 * 复制逻辑参考 promoteSideTaskToMainSession 中的消息复制。
 */
private async forkParentMessages(
  parentSessionId: number,
  sideSessionId: number,
): Promise<void> {
  // 1. 复制消息(维护 messageIdMap)
  const messages = await this.sessionService.getMessages(parentSessionId);
  const messageIdMap = new Map<number, number>();
  for (const m of messages) {
    const copy: Message = {
      sessionId: sideSessionId,
      role: m.role,
      content: m.content,
      thinkingContent: m.thinkingContent,
      toolCallId: m.toolCallId,
      toolCalls: m.toolCalls,
      tokenCount: m.tokenCount,
      modelId: m.modelId,
      metadata: m.metadata,
      sourceSessionId: parentSessionId,
    };
    const newId = await this.sessionService.saveMessage(
      sideSessionId, copy.role, copy.content, null,
      copy.toolCallId, copy.toolCalls, copy.tokenCount ?? 0, copy.modelId,
    );
    if (m.id != null && newId != null) messageIdMap.set(m.id, newId);
  }

  // 2. 复制 file_change(通过 messageIdMap 重映射 messageId)
  // 3. 复制 compaction 记录(将 sessionId 替换为 sideSessionId)
}
```

> 具体实现需参考 `session.service.ts:530` 中 `promoteSideTaskToMainSession` 的事务内复制模式,通过 `sessionRepo.transaction()` 在单事务内完成全部复制操作。

#### 步骤 3:修改 `StreamingWsHandler.handleCreateSideSession`

**文件**:`backend-ts/src/session/ws/streaming-ws-handler.ts:773`

将:

```typescript
const inheritContext = data.inheritContext === true;
```

改为:

```typescript
const contextMode = data.contextMode === 'fork' || data.contextMode === 'summary'
  ? data.contextMode
  : 'none';
```

并将 `handleCreateSideSession` 中传递给 `executeSideFirstMessage` 的参数从 `inheritContext` 改为 `contextMode`:

**文件**:`backend-ts/src/session/ws/streaming-ws-handler.ts:859`

```typescript
await this.deps.harnessService.executeSideFirstMessage(
  parentSessionId, sideSessionId, contextMode, listener, flag
);
```

#### 步骤 4:修改 `StreamingWsHandler` 的接口定义

**文件**:`backend-ts/src/session/ws/streaming-ws-handler.ts:53`

```typescript
executeSideFirstMessage(
  parentId: number, sideId: number,
  contextMode: 'fork' | 'summary' | 'none',
  listener: AgentEventListener,
  cancelFlag: { get(): boolean; set(v: boolean): void }
): Promise<void>;
```

#### 步骤 5:更新测试

**文件**:`backend-ts/src/harness/core/harness-service.spec.ts:384-390`

- 将现有 `executeSideFirstMessageAppendsParentSummary` 测试中的 `true` 参数改为 `'summary'`,断言不变。
- 新增测试 `executeSideFirstMessageForkCopiesMessages`:传入 `'fork'`,断言 `getMessages` 被调用于 parent,消息被复制到 side session。
- 新增测试 `executeSideFirstMessageNoneDoesNotInject`:传入 `'none'`,断言 systemPrompt 不含"主任务背景摘要"。

**文件**:`backend-ts/src/session/ws/streaming-ws-handler.spec.ts:243,414,441`

- 将 `inheritContext: true` 改为 `contextMode: 'summary'`(或对应的 `'fork'` / `'none'`)。

### 4.2 前端(desktop)

#### 步骤 1:修改 `SideChatPanel.vue` 模板

**文件**:`desktop/src/components/chat/SideChatPanel.vue:77-81`

将:

```html
<div v-if="!hasRealSession && displayMessages.length === 0" class="inherit-bar">
  <el-checkbox v-model="inheritContext" size="small">
    继承主任务上下文
  </el-checkbox>
</div>
```

替换为:

```html
<div v-if="!hasRealSession && displayMessages.length === 0" class="inherit-bar">
  <el-radio-group v-model="contextMode" size="small">
    <el-radio value="none">不继承</el-radio>
    <el-radio value="summary">主会话摘要</el-radio>
    <el-radio value="fork">Fork 主会话</el-radio>
  </el-radio-group>
</div>
```

#### 步骤 2:修改 `SideChatPanel.vue` 脚本

**文件**:`desktop/src/components/chat/SideChatPanel.vue:176`

将:

```typescript
const inheritContext = ref(false)
```

改为:

```typescript
const contextMode = ref<'none' | 'summary' | 'fork'>('none')
```

**文件**:`desktop/src/components/chat/SideChatPanel.vue:655`

将 `inheritContext.value` 改为 `contextMode.value`:

```typescript
const created = await createSideSession(
  parentSessionId,
  resolvedText,
  contextMode.value,  // 替换原来的 inheritContext.value
  currentModelId.value,
  localSkills,
  agentsMdContent,
  imageUrls,
  sidePermissionLevel.value
)
```

#### 步骤 3:修改 `useStreamWS.ts`

**文件**:`desktop/src/composables/useStreamWS.ts:494-510`

将 `createSideSession` 的参数从 `inheritContext: boolean` 改为 `contextMode: 'fork' | 'summary' | 'none'`,WS payload 中 `inheritContext` 替换为 `contextMode`:

```typescript
async function createSideSession(
  parentSessionId: string,
  content: string,
  contextMode: 'fork' | 'summary' | 'none',
  modelId?: number,
  localSkills?: LocalSkillReport[],
  agentsMdContent?: string,
  images?: string[],
  permissionLevel?: string
): Promise<boolean> {
  const payload = {
    type: 'create_side_session',
    sessionId: Number(parentSessionId),
    data: {
      content,
      contextMode,
      images: images || [],
      // ... 其余字段不变
    },
  };
  // ...
}
```

#### 步骤 4:Fork 模式下刷新消息列表

**文件**:`desktop/src/components/chat/SideChatPanel.vue`(`handleChatSend` 中 `createSideSession` 返回后的逻辑)

当 `contextMode.value === 'fork'` 时,在收到 `side_session_created` 事件后,调用消息分页接口重新拉取边路会话消息,以展示被复制的历史消息。现有逻辑在 `side_session_created` 后已有 `fetchMessages` 调用,需确认该调用能正确加载 fork 过来的消息。

---

## 5. 落地清单

### 5.1 后端改动文件

| 文件 | 改动 |
| --- | --- |
| `backend-ts/src/harness/core/harness-service.ts` | `executeSideFirstMessage` 签名改为 `contextMode`,新增 `forkParentMessages` 方法 |
| `backend-ts/src/harness/core/harness-service.spec.ts` | 修改现有测试参数,新增 fork 和 none 模式测试 |
| `backend-ts/src/session/ws/streaming-ws-handler.ts` | `handleCreateSideSession` 解析 `contextMode`,接口定义更新 |
| `backend-ts/src/session/ws/streaming-ws-handler.spec.ts` | WS 测试中 `inheritContext` 改为 `contextMode` |

### 5.2 前端改动文件

| 文件 | 改动 |
| --- | --- |
| `desktop/src/components/chat/SideChatPanel.vue` | checkbox → radio-group,`inheritContext` → `contextMode`,Fork 模式下刷新消息 |
| `desktop/src/composables/useStreamWS.ts` | `createSideSession` 参数和 WS payload 字段替换 |

### 5.3 不改动的文件

| 文件 | 说明 |
| --- | --- |
| `backend-ts/db/migration/` | 无数据库 schema 变更 |
| `backend-ts/src/session/session.service.ts` | `promoteSideTaskToMainSession` 不改,但 `forkParentMessages` 会复用其消息复制模式 |
| `admin/` | 不涉及管理后台 |
| `agent-cli/` | 不涉及终端 CLI |
| `android/` | 不涉及安卓原生壳 |

### 5.4 验证方式

| 验证项 | 方法 |
| --- | --- |
| 后端单测 | `cd backend-ts && npm test`(harness-service.spec.ts + streaming-ws-handler.spec.ts) |
| 前端构建 | `cd desktop && npm run build`(vue-tsc 类型检查) |
| 手动验证 - Fork | 新建边路任务选 Fork → 对话页面显示主会话历史消息 → Agent 执行时上下文包含历史 |
| 手动验证 - 摘要 | 新建边路任务选主会话摘要 → 对话页面无历史消息 → Agent system prompt 含摘要 |
| 手动验证 - 不继承 | 新建边路任务选不继承 → 全新对话 → Agent 无主会话上下文 |

### 5.5 CHANGELOG

在 `CHANGELOG.md` 顶部 `## x.y.z` 的 **前端** 小节新增:

> 边路任务新建时上下文继承方式从勾选框改为三选一单选:Fork 主会话(完整继承历史消息)/ 主会话摘要 / 不继承。