- 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>
8.1 KiB
8.1 KiB
TASKS-3: Session DAG (SQLite Store)
セッション管理を JSON ファイルベースの線形スライスから SQLite ベースの Turn DAG に移行する。 他トラックへの依存なし — LegacyAdapter で既存コードとの後方互換を維持しながら段階移行。
設計原則
- セッション内は線形、セッション間が DAG — per-message DAG は過剰。ターン間の因果は順序で十分。
- SQLite single-file backend — microSD 書き込み最小化、WAL モードでクラッシュ耐性。
- サブエージェント報告は user role — system role の権威性バイアスを回避。conductor が評価・反論できる。
- "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 ✅
既存動作を維持したまま裏側を差し替える。
-
✅ SQLite SessionStore 実装
pkg/session/sqlite.go: SessionStore interface の SQLite 実装- WAL モード、
modernc.org/sqlite(CGO なし、ARM クロスコンパイル容易) - schema migration (CREATE TABLE IF NOT EXISTS)
-
✅ LegacyAdapter 実装
pkg/session/legacy_adapter.go- 既存の
GetHistory/SetHistory/AddMessage/MarkDirtyを SessionStore 経由で実装 - 既存テスト全パス + テーブル駆動で JSON/SQLite 両方を同一アサーションで検証
-
✅ JSON → SQLite lazy migration
pkg/session/migrate.go: 起動時にsessions/*.jsonを検出 → SQLite に import →.json.migratedにリネーム- 個別ファイル失敗はログ警告して続行 (次回起動で再試行)
-
✅ AgentLoop 配線
pkg/agent/instance.go:Sessions型を*LegacyAdapterに変更loop.go/loop_test.goは変更なし — メソッドシグネチャ同一
Phase 1: Fork/Report ターン導入 ✅
-
✅ Fork 操作
SessionStore.Fork()実装- 親セッションに
TurnForkPointを追記、子セッションをparent_key付きで作成
-
✅ Report ターン
TurnReportの Append/Turns/Messages 対応origin_keyでどのセッションの報告かを追跡- user role でメッセージ格納 (system role 禁止)
-
✅ サブエージェントセッション永続化
- サブエージェント実行中のターンを SQLite に記録
- 完了後にセッション status を "completed" に変更
Phase 2: Compaction + Session Lifecycle + SessionGraph Prep ✅
-
✅ SessionGraph 薄ラッパー (段階化: 既存 call site 変更なし)
pkg/session/graph.go:SessionGraph+BeginTurn()/TurnWriterLegacyAdapter.Graph()アクセサ追加- 将来の段階移行の準備のみ — LegacyAdapter は廃止しない
-
✅ CompactOldTurns
LegacyAdapter.CompactOldTurns(key, keepLast, summary)— SQLite 直接 compactsummarizeSession()から呼び出し (fallback 付き)- full-rewrite (
replaced=true → Compact+Append) を回避
-
✅ セッションライフサイクル
- 起動時
store.Prune(DefaultPruneTTL)— 7日超過セッション削除 flushLoopに定期 prune ticker (6h 間隔)AgentLoop.gcLoop()— 30分ごとにsessionLocksの idle エントリ GCAgentLoop.doneチャネル +Close()で GC ループ停止
- 起動時
Phase 3: UI & Commands ✅
-
✅ Mini App セッショングラフ可視化
/miniapp/api/sessions/graphREST endpoint + SSEsessionevent に graph 含む- フロントエンドで tree rendering (CSS + JS)
-
✅ 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 書き込み頻度が既存比で削減