Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
258 changes: 198 additions & 60 deletions docs/superpowers/specs/2026-06-19-provider-facade-design.md

Large diffs are not rendered by default.

598 changes: 598 additions & 0 deletions internal/parser/db_backed_provider.go

Large diffs are not rendered by default.

491 changes: 491 additions & 0 deletions internal/parser/db_backed_provider_test.go

Large diffs are not rendered by default.

76 changes: 4 additions & 72 deletions internal/parser/forge.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"database/sql"
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"strings"
Expand All @@ -15,12 +14,6 @@ import (

const forgeDBFilename = ".forge.db"

// ForgeSession bundles a parsed session with its messages.
type ForgeSession struct {
Session ParsedSession
Messages []ParsedMessage
}

// ForgeSessionMeta is lightweight metadata for a session,
// used to detect changes without parsing messages.
type ForgeSessionMeta struct {
Expand All @@ -29,8 +22,8 @@ type ForgeSessionMeta struct {
FileMtime int64
}

// FindForgeDBPath returns the Forge SQLite database path when present.
func FindForgeDBPath(dir string) string {
// forgeDBPath returns the Forge SQLite database path when present.
func forgeDBPath(dir string) string {
if dir == "" {
return ""
}
Expand Down Expand Up @@ -83,41 +76,8 @@ func ListForgeSessionMeta(dbPath string) ([]ForgeSessionMeta, error) {
return metas, rows.Err()
}

// ParseForgeDB opens the Forge SQLite database read-only and returns
// all conversations with parsed messages.
func ParseForgeDB(dbPath, machine string) ([]ForgeSession, error) {
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
return nil, nil
}

db, err := openForgeDB(dbPath)
if err != nil {
return nil, err
}
defer db.Close()

convos, err := loadForgeConversations(db)
if err != nil {
return nil, fmt.Errorf("loading forge conversations: %w", err)
}

var results []ForgeSession
for _, c := range convos {
parsed, msgs, err := buildForgeSession(c, dbPath, machine)
if err != nil {
log.Printf("forge conversation %s: %v", c.id, err)
continue
}
if parsed == nil {
continue
}
results = append(results, ForgeSession{Session: *parsed, Messages: msgs})
}
return results, nil
}

// ParseForgeSession parses a single conversation by ID from the Forge database.
func ParseForgeSession(dbPath, conversationID, machine string) (*ParsedSession, []ParsedMessage, error) {
// parseForgeSession parses a single conversation by ID from the Forge database.
func parseForgeSession(dbPath, conversationID, machine string) (*ParsedSession, []ParsedMessage, error) {
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
return nil, nil, fmt.Errorf("forge db not found: %s", dbPath)
}
Expand Down Expand Up @@ -153,34 +113,6 @@ type forgeConversationRow struct {
metrics string
}

func loadForgeConversations(db *sql.DB) ([]forgeConversationRow, error) {
rows, err := db.Query(`
SELECT conversation_id,
COALESCE(title, ''),
COALESCE(context, ''),
created_at,
COALESCE(updated_at, created_at),
COALESCE(metrics, '')
FROM conversations
WHERE context IS NOT NULL
ORDER BY COALESCE(updated_at, created_at)
`)
if err != nil {
return nil, err
}
defer rows.Close()

var convos []forgeConversationRow
for rows.Next() {
var c forgeConversationRow
if err := rows.Scan(&c.id, &c.title, &c.context, &c.createdAt, &c.updatedAt, &c.metrics); err != nil {
return nil, err
}
convos = append(convos, c)
}
return convos, rows.Err()
}

func loadOneForgeConversation(db *sql.DB, conversationID string) (forgeConversationRow, error) {
row := db.QueryRow(`
SELECT conversation_id,
Expand Down
46 changes: 23 additions & 23 deletions internal/parser/forge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ func TestParseForgeDB_StandardConversation(t *testing.T) {
defer db.Close()
seedForgeConversation(t, seeder)

sessions, err := ParseForgeDB(dbPath, "testmachine")
sessions, err := parseForgeAll(dbPath, "testmachine")
require.NoError(t, err, "ParseForgeDB")

assertEq(t, "sessions len", len(sessions), 1)
Expand Down Expand Up @@ -200,8 +200,8 @@ func TestParseForgeSession_SingleConversation(t *testing.T) {
defer db.Close()
seedForgeConversation(t, seeder)

sess, msgs, err := ParseForgeSession(dbPath, "conv-001", "testmachine")
require.NoError(t, err, "ParseForgeSession")
sess, msgs, err := parseForgeSession(dbPath, "conv-001", "testmachine")
require.NoError(t, err, "parseForgeSession")
require.NotNil(t, sess, "expected non-nil session")

assertEq(t, "ID", sess.ID, "forge:conv-001")
Expand Down Expand Up @@ -266,8 +266,8 @@ func TestCollectForgeToolCalls_TaskSubagentIDPrefixed(t *testing.T) {
"2026-05-02 10:00:00", "2026-05-02 10:00:01", "",
)

sess, msgs, err := ParseForgeSession(dbPath, "parent-conv", "m")
require.NoError(t, err, "ParseForgeSession")
sess, msgs, err := parseForgeSession(dbPath, "parent-conv", "m")
require.NoError(t, err, "parseForgeSession")
require.NotNil(t, sess, "expected non-nil session")
require.NotEmpty(t, msgs, "expected messages")
var taskCall *ParsedToolCall
Expand All @@ -282,13 +282,13 @@ func TestCollectForgeToolCalls_TaskSubagentIDPrefixed(t *testing.T) {
assertEq(t, "SubagentSessionID", taskCall.SubagentSessionID, "forge:child-conv-001")
}

func TestFindForgeDBPath(t *testing.T) {
func TestForgeDBPath(t *testing.T) {
dir := t.TempDir()
assertEq(t, "not found", FindForgeDBPath(dir), "")
assertEq(t, "not found", forgeDBPath(dir), "")

dbPath, _, db := newForgeTestDB(t)
defer db.Close()
assertEq(t, "found", FindForgeDBPath(filepath.Dir(dbPath)), dbPath)
assertEq(t, "found", forgeDBPath(filepath.Dir(dbPath)), dbPath)
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -341,7 +341,7 @@ func TestForgeTokenFallbacks(t *testing.T) {
"", // empty metrics → accumulateMessageTokenUsage fallback
)

sessions, err := ParseForgeDB(dbPath, "m")
sessions, err := parseForgeAll(dbPath, "m")
require.NoError(t, err, "ParseForgeDB")
require.Len(t, sessions, 1)
s := sessions[0].Session
Expand Down Expand Up @@ -387,7 +387,7 @@ func TestForgeTokenFallbacks(t *testing.T) {
`{"output_tokens": 42}`,
)

sessions, err := ParseForgeDB(dbPath, "m")
sessions, err := parseForgeAll(dbPath, "m")
require.NoError(t, err, "ParseForgeDB")
require.Len(t, sessions, 1)
s := sessions[0].Session
Expand Down Expand Up @@ -426,7 +426,7 @@ func TestForgeTokenFallbacks(t *testing.T) {
"",
)

sessions, err := ParseForgeDB(dbPath, "m")
sessions, err := parseForgeAll(dbPath, "m")
require.NoError(t, err, "ParseForgeDB")
require.Len(t, sessions, 1)
msgs := sessions[0].Messages
Expand Down Expand Up @@ -460,7 +460,7 @@ func TestForgeTokenFallbacks(t *testing.T) {
"",
)

sessions, err := ParseForgeDB(dbPath, "m")
sessions, err := parseForgeAll(dbPath, "m")
require.NoError(t, err, "ParseForgeDB")
require.Len(t, sessions, 1)
msgs := sessions[0].Messages
Expand Down Expand Up @@ -514,7 +514,7 @@ func TestForgeDegenerate(t *testing.T) {
"",
)

sessions, err := ParseForgeDB(dbPath, "m")
sessions, err := parseForgeAll(dbPath, "m")
require.NoError(t, err, "ParseForgeDB")
require.Len(t, sessions, 1)
// Only the user message; empty assistant was skipped.
Expand Down Expand Up @@ -556,7 +556,7 @@ func TestForgeDegenerate(t *testing.T) {
"",
)

sessions, err := ParseForgeDB(dbPath, "m")
sessions, err := parseForgeAll(dbPath, "m")
require.NoError(t, err, "ParseForgeDB")
require.Len(t, sessions, 1)
// Only the user message; tool result with empty call_id was skipped.
Expand Down Expand Up @@ -589,7 +589,7 @@ func TestForgeDegenerate(t *testing.T) {
"",
)

sessions, err := ParseForgeDB(dbPath, "m")
sessions, err := parseForgeAll(dbPath, "m")
require.NoError(t, err, "ParseForgeDB")
require.Len(t, sessions, 1)
msgs := sessions[0].Messages
Expand Down Expand Up @@ -628,7 +628,7 @@ func TestForgeCwdEdgeCases(t *testing.T) {
"",
)

sessions, err := ParseForgeDB(dbPath, "m")
sessions, err := parseForgeAll(dbPath, "m")
require.NoError(t, err, "ParseForgeDB")
require.Len(t, sessions, 1)
s := sessions[0].Session
Expand Down Expand Up @@ -670,7 +670,7 @@ func TestForgeCwdEdgeCases(t *testing.T) {
"",
)

sessions, err := ParseForgeDB(dbPath, "m")
sessions, err := parseForgeAll(dbPath, "m")
require.NoError(t, err, "ParseForgeDB")
require.Len(t, sessions, 1)
assertEq(t, "Cwd", sessions[0].Session.Cwd, "")
Expand Down Expand Up @@ -710,7 +710,7 @@ func TestForgeCwdEdgeCases(t *testing.T) {
"",
)

sessions, err := ParseForgeDB(dbPath, "m")
sessions, err := parseForgeAll(dbPath, "m")
require.NoError(t, err, "ParseForgeDB")
require.Len(t, sessions, 1)
assertEq(t, "Cwd", sessions[0].Session.Cwd, "")
Expand Down Expand Up @@ -741,7 +741,7 @@ func TestForgeCwdEdgeCases(t *testing.T) {
"",
)

sessions, err := ParseForgeDB(dbPath, "m")
sessions, err := parseForgeAll(dbPath, "m")
require.NoError(t, err, "ParseForgeDB")
require.Len(t, sessions, 1)
s := sessions[0].Session
Expand Down Expand Up @@ -814,7 +814,7 @@ func TestForgeEndedAtFallback(t *testing.T) {
_, err := db.Exec("UPDATE conversations SET updated_at = NULL WHERE conversation_id = 'ended-fallback'")
require.NoError(t, err, "update")

sessions, err := ParseForgeDB(dbPath, "m")
sessions, err := parseForgeAll(dbPath, "m")
require.NoError(t, err, "ParseForgeDB")
require.Len(t, sessions, 1)
s := sessions[0].Session
Expand Down Expand Up @@ -864,7 +864,7 @@ func TestForgeToolOutputText(t *testing.T) {
"",
)

sessions, err := ParseForgeDB(dbPath, "m")
sessions, err := parseForgeAll(dbPath, "m")
require.NoError(t, err, "ParseForgeDB")
require.Len(t, sessions, 1)
msgs := sessions[0].Messages
Expand Down Expand Up @@ -936,7 +936,7 @@ func TestForgeSkillToolName(t *testing.T) {
"",
)

sessions, err := ParseForgeDB(dbPath, "m")
sessions, err := parseForgeAll(dbPath, "m")
require.NoError(t, err, "ParseForgeDB")
require.Len(t, sessions, 1)
var skillCall *ParsedToolCall
Expand Down Expand Up @@ -994,7 +994,7 @@ func TestForgeReasoningNoText(t *testing.T) {
"",
)

sessions, err := ParseForgeDB(dbPath, "m")
sessions, err := parseForgeAll(dbPath, "m")
require.NoError(t, err, "ParseForgeDB")
require.Len(t, sessions, 1)
msgs := sessions[0].Messages
Expand Down
Loading