# 機能設計書 22-セッションフォーク

## 概要

本ドキュメントは、OpenCodeにおけるセッションフォーク機能の設計を定義する。セッションフォークは、既存のセッションの会話履歴を特定のメッセージまでコピーし、新しいセッションとして分岐させる機能である。

### 本機能の処理概要

セッションフォーク機能は、既存のセッションから派生した新しいセッションを作成し、指定されたメッセージまでの会話履歴を複製する機能である。

**業務上の目的・背景**：開発作業中に複数のアプローチを試したい場合や、特定の時点から別の方向性で作業を進めたい場合に、既存の文脈を保持したまま新しい会話を開始できるようにする。バージョン管理のブランチに似た概念を会話に適用する。

**機能の利用シーン**：ユーザーが「ここまでの作業は保持しつつ、別のアプローチを試したい」という場合に利用される。例えば、AIが提案した複数の解決策を並行して検証する場合や、過去の特定時点の状態に戻りたい場合に使用する。

**主要な処理内容**：
1. 元セッションの取得と検証
2. フォーク用タイトルの生成（"(fork #N)"サフィックス付与）
3. 新規セッションの作成
4. 指定メッセージまでの会話履歴のコピー
5. メッセージIDマッピングの維持（親子関係の再構築）
6. パーツ（ツール呼び出し結果等）のコピー

**関連システム・外部連携**：特に外部システムとの連携はなく、内部のデータベース操作のみ。

**権限による制御**：元セッションの読み取り権限が必要。フォーク後のセッションは新規セッションとして権限が独立する。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 26 | セッション | 主画面 | DialogForkFromTimelineによるセッションフォーク |

## 機能種別

データ登録処理（CREATE） / データ複製処理

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| sessionID | string | Yes | フォーク元セッションのID | session_で始まるIdentifier形式 |
| messageID | string | No | フォーク終点のメッセージID | message_で始まるIdentifier形式 |

### 入力データソース

- SessionTable: 元セッション情報
- MessageTable: 元セッションのメッセージ一覧
- PartTable: 各メッセージのパーツ情報

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| id | string | 新規セッションの一意識別子 |
| slug | string | 新規セッションのスラッグ |
| title | string | フォークタイトル（元タイトル + "(fork #N)"） |
| projectID | string | 関連プロジェクトのID |
| directory | string | 作業ディレクトリパス |
| time.created | number | 作成日時 |
| time.updated | number | 更新日時 |

### 出力先

- SQLiteデータベース（SessionTable、MessageTable、PartTable）
- Busイベント（session.created、message.updated、message.part.updated）

## 処理フロー

### 処理シーケンス

```
1. 元セッションの取得
   └─ Session.get(sessionID)で元セッション情報を取得

2. フォークタイトルの生成
   └─ getForkedTitle関数で"(fork #N)"サフィックスを付与

3. 新規セッションの作成
   └─ createNext関数で新しいセッションを生成

4. メッセージ一覧の取得
   └─ Session.messages({sessionID})で元セッションの全メッセージを取得

5. メッセージの複製ループ
   ├─ 終点メッセージIDが指定されている場合、そこで停止
   ├─ 新しいメッセージIDを生成
   ├─ 親子関係のIDマッピングを維持
   └─ メッセージとパーツをコピー

6. 新規セッション情報の返却
```

### フローチャート

```mermaid
flowchart TD
    A[開始] --> B[元セッション取得]
    B --> C{セッション存在?}
    C -->|No| D[エラー: session not found]
    C -->|Yes| E[フォークタイトル生成]
    E --> F[新規セッション作成]
    F --> G[元メッセージ一覧取得]
    G --> H[メッセージループ開始]
    H --> I{終点に到達?}
    I -->|Yes| J[ループ終了]
    I -->|No| K[新メッセージID生成]
    K --> L[IDマッピング登録]
    L --> M{アシスタントメッセージ?}
    M -->|Yes| N[親IDをマッピングから取得]
    M -->|No| O[親IDなし]
    N --> P[メッセージ複製]
    O --> P
    P --> Q[パーツ複製ループ]
    Q --> R[新パーツID生成]
    R --> S[パーツ保存]
    S --> H
    J --> T[新セッション返却]
    T --> U[終了]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-22-01 | フォークタイトル生成 | 元タイトルに"(fork #N)"を付与。既にforkサフィックスがある場合は番号をインクリメント | 常時 |
| BR-22-02 | 終点指定 | messageIDが指定された場合、そのID以降のメッセージはコピーしない | messageIDパラメータ指定時 |
| BR-22-03 | ID再生成 | メッセージとパーツのIDは新規生成し、元のIDとは異なる値となる | 常時 |
| BR-22-04 | 親子関係維持 | アシスタントメッセージのparentIDは、IDマッピングを使って新しいIDに変換 | アシスタントメッセージのコピー時 |

### 計算ロジック

フォークタイトルの生成ロジック（getForkedTitle関数）:
```typescript
// 既存のforkサフィックスをチェック
const match = title.match(/^(.+) \(fork #(\d+)\)$/)
if (match) {
  // 番号をインクリメント
  return `${base} (fork #${num + 1})`
}
// 初回フォーク
return `${title} (fork #1)`
```

## データベース操作仕様

### 操作別データベース影響一覧

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| フォーク元取得 | session | SELECT | 元セッション情報の取得 |
| 新セッション作成 | session | INSERT | フォークセッションの作成 |
| メッセージ取得 | message, part | SELECT | 元メッセージとパーツの取得 |
| メッセージ複製 | message | INSERT/UPDATE | メッセージの複製（onConflictDoUpdate） |
| パーツ複製 | part | INSERT/UPDATE | パーツの複製（onConflictDoUpdate） |

### テーブル別操作詳細

#### MessageTable

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | id | Identifier.ascending("message")で新規生成 | 元IDとは異なる |
| INSERT | session_id | 新セッションのID | 新セッションに紐付け |
| INSERT | data | 元メッセージのデータ（parentIDは変換済み） | JSONカラム |

#### PartTable

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | id | Identifier.ascending("part")で新規生成 | 元IDとは異なる |
| INSERT | message_id | 新メッセージのID | 新メッセージに紐付け |
| INSERT | session_id | 新セッションのID | 新セッションに紐付け |
| INSERT | data | 元パーツのデータ | JSONカラム |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | Error | 元セッションが存在しない | "session not found"エラーをスロー |

### リトライ仕様

リトライ機構は実装されていない。エラー発生時は処理を中断する。

## トランザクション仕様

個々のデータベース操作はDatabase.use()で実行されるが、fork関数全体をラップするトランザクションは実装されていない。途中で失敗した場合、部分的なデータが残る可能性がある。

## パフォーマンス要件

- メッセージ数に比例した処理時間
- 大規模セッション（100+メッセージ）のフォークは数秒かかる可能性あり

## セキュリティ考慮事項

- フォーク操作は元セッションと同じプロジェクトコンテキストで実行される
- 新セッションのIDは予測不可能な形式で生成される

## 備考

- フォークは親セッション（parentID）の設定は行わない。完全に独立したセッションとして作成される
- 共有設定は新セッションには引き継がれない

---

## コードリーディングガイド

本機能を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: データ構造を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | session.sql.ts | `packages/opencode/src/session/session.sql.ts` | MessageTable、PartTableの定義 |
| 1-2 | message-v2.ts | `packages/opencode/src/session/message-v2.ts` | MessageV2.Info、MessageV2.Part型定義 |

**読解のコツ**: メッセージとパーツの関係（1:N）を理解する。パーツにはテキスト、ツール呼び出し、ファイル添付など様々な種類がある。

#### Step 2: エントリーポイントを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | index.ts | `packages/opencode/src/session/index.ts` | Session.fork関数（230-270行目） |

**主要処理フロー**:
1. **230-234行目**: 入力スキーマ定義
2. **236-237行目**: 元セッション取得とバリデーション
3. **238行目**: フォークタイトル生成
4. **239-242行目**: 新セッション作成
5. **243行目**: 元メッセージ取得
6. **244行目**: IDマッピング用Map初期化
7. **246-266行目**: メッセージ・パーツ複製ループ

#### Step 3: ヘルパー関数を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | index.ts | `packages/opencode/src/session/index.ts` | getForkedTitle関数（106-114行目） |
| 3-2 | index.ts | `packages/opencode/src/session/index.ts` | Session.messages関数（512-526行目） |
| 3-3 | index.ts | `packages/opencode/src/session/index.ts` | Session.updateMessage関数（670-690行目） |
| 3-4 | index.ts | `packages/opencode/src/session/index.ts` | Session.updatePart関数（735-756行目） |

**主要処理フロー**:
- **106-114行目**: 正規表現でforkサフィックスを検出し、番号をインクリメント
- **512-526行目**: ストリーミング形式でメッセージを取得
- **670-690行目**: onConflictDoUpdateでメッセージをupsert
- **735-756行目**: onConflictDoUpdateでパーツをupsert

### プログラム呼び出し階層図

```
Session.fork (public API)
    │
    ├─ Session.get(sessionID)
    │      └─ 元セッション取得
    │
    ├─ getForkedTitle(original.title)
    │      └─ タイトル生成
    │
    ├─ Session.createNext()
    │      └─ 新セッション作成
    │
    ├─ Session.messages({sessionID})
    │      └─ 元メッセージ取得
    │
    └─ ループ処理
           │
           ├─ Identifier.ascending("message")
           │      └─ 新メッセージID生成
           │
           ├─ Session.updateMessage()
           │      └─ メッセージ保存
           │
           └─ Session.updatePart() [各パーツ]
                  └─ パーツ保存
```

### データフロー図

```
[入力]                    [処理]                         [出力]

sessionID ──────┐                                    ┌─> 新Session.Info
messageID ──────┼──▶ Session.fork() ────────────────┼─> DB (session)
                │           │                        ├─> DB (message)
                │           ├─ getForkedTitle()      └─> DB (part)
                │           │      └─ タイトル変換
                │           │
元Session ──────┤           ├─ createNext()
元Messages ─────┤           │      └─ 新セッション
元Parts ────────┘           │
                            └─ メッセージ複製ループ
                                   │
                                   ├─ IDマッピング
                                   │      └─ 親子関係維持
                                   │
                                   └─ パーツ複製
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| index.ts | `packages/opencode/src/session/index.ts` | ソース | セッション管理のメインモジュール、fork関数の実装 |
| session.sql.ts | `packages/opencode/src/session/session.sql.ts` | ソース | データベーステーブル定義 |
| message-v2.ts | `packages/opencode/src/session/message-v2.ts` | ソース | メッセージ・パーツの型定義 |
| id.ts | `packages/opencode/src/id/id.ts` | ソース | 識別子生成ユーティリティ |
