picoclaw/todo/TASKS-3.md
dj-oyu c95dee28ef feat(session): add /session CLI commands and Mini App graph UI (TASKS-3 Phase 3)
- Refactor handleSessionCommand into subcommand dispatcher (list/graph/fork/reset)
- Add SessionGraphNode type and GetSessionGraph() for Mini App API
- Add /miniapp/api/sessions/graph endpoint with SSE integration
- Add session tree rendering in Mini App frontend
- Mark TASKS-3 fully complete (Phase 0-3)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 23:22:14 +09:00

8.1 KiB

TASKS-3: Session DAG (SQLite Store)

セッション管理を JSON ファイルベースの線形スライスから SQLite ベースの Turn DAG に移行する。 他トラックへの依存なし — LegacyAdapter で既存コードとの後方互換を維持しながら段階移行。

設計原則

  1. セッション内は線形、セッション間が DAG — per-message DAG は過剰。ターン間の因果は順序で十分。
  2. SQLite single-file backend — microSD 書き込み最小化、WAL モードでクラッシュ耐性。
  3. サブエージェント報告は user role — system role の権威性バイアスを回避。conductor が評価・反論できる。
  4. "merge" は特別な操作ではない — 報告を受けて会話を続ける通常のターン。

DAG 構造

Conductor session:  turn1 → turn2 → turn3 → report(scout-1) → turn4 → report(coder-1) → turn5
                               ↓ fork                          ↑ report
Scout-1 session:              turn1 → turn2 → turn3 ──────────┘
                                        ↓ fork
Coder-1 session:                       turn1 → turn2 → turn3 ──────────┘

SQLite Schema

CREATE TABLE sessions (
    key          TEXT PRIMARY KEY,
    parent_key   TEXT REFERENCES sessions(key),
    fork_turn_id TEXT,
    status       TEXT NOT NULL DEFAULT 'active',
    label        TEXT NOT NULL DEFAULT '',
    summary      TEXT NOT NULL DEFAULT '',
    created_at   INTEGER NOT NULL,
    updated_at   INTEGER NOT NULL
);

CREATE TABLE turns (
    id          TEXT PRIMARY KEY,
    session_key TEXT NOT NULL REFERENCES sessions(key) ON DELETE CASCADE,
    seq         INTEGER NOT NULL,
    kind        INTEGER NOT NULL DEFAULT 0,
    messages    TEXT NOT NULL,
    origin_key  TEXT,
    summary     TEXT,
    author      TEXT NOT NULL DEFAULT '',
    created_at  INTEGER NOT NULL,
    meta        TEXT,
    UNIQUE(session_key, seq)
);

CREATE INDEX idx_turns_session_seq ON turns(session_key, seq);
CREATE INDEX idx_sessions_parent ON sessions(parent_key);

Go Interface

type TurnKind int

const (
    TurnNormal    TurnKind = iota
    TurnReport
    TurnForkPoint
)

type Turn struct {
    ID        string
    Seq       int
    Kind      TurnKind
    Messages  []providers.Message
    OriginKey string
    Summary   string
    Author    string
    CreatedAt time.Time
    Meta      map[string]string
}

type SessionInfo struct {
    Key        string
    ParentKey  string
    ForkTurnID string
    Status     string
    Label      string
    Summary    string
    TurnCount  int
    CreatedAt  time.Time
    UpdatedAt  time.Time
}

type SessionStore interface {
    Create(key string, opts *CreateOpts) error
    Get(key string) (*SessionInfo, error)
    List(filter *ListFilter) ([]*SessionInfo, error)
    SetStatus(key, status string) error
    SetSummary(key, summary string) error
    Delete(key string) error
    Children(key string) ([]*SessionInfo, error)

    Append(sessionKey string, turn *Turn) error
    Turns(sessionKey string, sinceSeq int) ([]*Turn, error)
    LastTurn(sessionKey string) (*Turn, error)
    TurnCount(sessionKey string) (int, error)

    Compact(sessionKey string, upToSeq int, summary string) error
    Fork(parentKey, childKey string, opts *CreateOpts) error

    Prune(olderThan time.Duration) (int, error)
    Close() error
}

高レベルラッパー

type SessionGraph struct {
    store   SessionStore
    buffers sync.Map // sessionKey → *turnBuffer
    views   sync.Map // sessionKey → *cachedView
}

func (g *SessionGraph) Messages(sessionKey string) ([]providers.Message, error)
func (g *SessionGraph) BeginTurn(sessionKey string, kind TurnKind) *TurnWriter

type TurnWriter struct { ... }
func (tw *TurnWriter) Add(msg providers.Message)
func (tw *TurnWriter) SetOrigin(sessionKey string)
func (tw *TurnWriter) Commit() error
func (tw *TurnWriter) Discard()

サブエージェント報告フロー

1. conductor が spawn → store.Fork(conductorSession, subagentSession)
2. subagent 実行中 → store.Append(subagentSession, Turn{Kind: TurnNormal, ...})
3. subagent 完了 → store.SetStatus(subagentSession, "completed")
4. conductor 側に report ターン:
     tw := graph.BeginTurn(conductorSession, TurnReport)
     tw.SetOrigin(subagentSession)
     tw.Add(Message{Role: "user", Content: "[scout-1] 調査結果..."})
5. conductor が応答:
     tw.Add(Message{Role: "assistant", Content: "なるほど、JWTで十分..."})
     tw.Commit()

タスク一覧

Phase 0: SQLite SessionStore + LegacyAdapter

既存動作を維持したまま裏側を差し替える。

  1. SQLite SessionStore 実装

    • pkg/session/sqlite.go: SessionStore interface の SQLite 実装
    • WAL モード、modernc.org/sqlite (CGO なし、ARM クロスコンパイル容易)
    • schema migration (CREATE TABLE IF NOT EXISTS)
  2. LegacyAdapter 実装

    • pkg/session/legacy_adapter.go
    • 既存の GetHistory / SetHistory / AddMessage / MarkDirty を SessionStore 経由で実装
    • 既存テスト全パス + テーブル駆動で JSON/SQLite 両方を同一アサーションで検証
  3. JSON → SQLite lazy migration

    • pkg/session/migrate.go: 起動時に sessions/*.json を検出 → SQLite に import → .json.migrated にリネーム
    • 個別ファイル失敗はログ警告して続行 (次回起動で再試行)
  4. AgentLoop 配線

    • pkg/agent/instance.go: Sessions 型を *LegacyAdapter に変更
    • loop.go / loop_test.go は変更なし — メソッドシグネチャ同一

Phase 1: Fork/Report ターン導入

  1. Fork 操作

    • SessionStore.Fork() 実装
    • 親セッションに TurnForkPoint を追記、子セッションを parent_key 付きで作成
  2. Report ターン

    • TurnReport の Append/Turns/Messages 対応
    • origin_key でどのセッションの報告かを追跡
    • user role でメッセージ格納 (system role 禁止)
  3. サブエージェントセッション永続化

    • サブエージェント実行中のターンを SQLite に記録
    • 完了後にセッション status を "completed" に変更

Phase 2: Compaction + Session Lifecycle + SessionGraph Prep

  1. SessionGraph 薄ラッパー (段階化: 既存 call site 変更なし)

    • pkg/session/graph.go: SessionGraph + BeginTurn() / TurnWriter
    • LegacyAdapter.Graph() アクセサ追加
    • 将来の段階移行の準備のみ — LegacyAdapter は廃止しない
  2. CompactOldTurns

    • LegacyAdapter.CompactOldTurns(key, keepLast, summary) — SQLite 直接 compact
    • summarizeSession() から呼び出し (fallback 付き)
    • full-rewrite (replaced=true → Compact+Append) を回避
  3. セッションライフサイクル

    • 起動時 store.Prune(DefaultPruneTTL) — 7日超過セッション削除
    • flushLoop に定期 prune ticker (6h 間隔)
    • AgentLoop.gcLoop() — 30分ごとに sessionLocks の idle エントリ GC
    • AgentLoop.done チャネル + Close() で GC ループ停止

Phase 3: UI & Commands

  1. Mini App セッショングラフ可視化

    • /miniapp/api/sessions/graph REST endpoint + SSE session event に graph 含む
    • フロントエンドで tree rendering (CSS + JS)
  2. CLI コマンド

    • /session list — 全セッション一覧 (ステータス、ターン数、経過時間)
    • /session fork [label] — 現在のセッションを fork
    • /session graph — DAG 構造を ASCII tree 表示
    • /session (default) — DAG summary + token stats + usage hint
    • /session reset — stats リセット

完了基準

  • go test ./... 全パス
  • 既存の JSON セッションが SQLite に自動マイグレーション
  • Phase 0 完了時点で既存動作に変化なし (LegacyAdapter 透過)
  • サブエージェント報告が user role ターンとして記録
  • conductor が報告を個別に評価・応答するフロー動作
  • microSD 書き込み頻度が既存比で削減