Files
wehub-resource-sync f99010fae1
Desktop Artifacts / Desktop Build (Linux) (push) Waiting to run
Desktop Artifacts / Desktop Build (Windows) (push) Waiting to run
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Waiting to run
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

681 lines
17 KiB
Go

package parser
import (
"database/sql"
"encoding/json"
"fmt"
"os"
"path/filepath"
"slices"
"strings"
"time"
)
// PiebaldDBFilename is the Piebald session store filename inside its data dir.
const PiebaldDBFilename = "app.db"
// PiebaldSessionMeta is lightweight metadata for a Piebald chat.
type PiebaldSessionMeta struct {
SessionID string
VirtualPath string
FileMtime int64
}
// piebaldDBPath returns the Piebald SQLite database path when present.
func piebaldDBPath(dir string) string {
if dir == "" {
return ""
}
path := filepath.Join(dir, PiebaldDBFilename)
info, err := os.Stat(path)
if err != nil || info.IsDir() {
return ""
}
return path
}
// ListPiebaldSessionMeta returns lightweight metadata for all non-empty chats.
func ListPiebaldSessionMeta(dbPath string) ([]PiebaldSessionMeta, error) {
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
return nil, nil
}
db, err := openPiebaldDB(dbPath)
if err != nil {
return nil, err
}
defer db.Close()
rows, err := db.Query(`
SELECT id,
COALESCE(updated_at, created_at)
FROM chats
WHERE COALESCE(is_deleted, 0) = 0
AND message_count > 0
`)
if err != nil {
return nil, fmt.Errorf("listing piebald chats: %w", err)
}
defer rows.Close()
var metas []PiebaldSessionMeta
for rows.Next() {
var id int64
var updatedAt string
if err := rows.Scan(&id, &updatedAt); err != nil {
return nil, fmt.Errorf("scanning piebald chat meta: %w", err)
}
metas = append(metas, PiebaldSessionMeta{
SessionID: fmt.Sprintf("%d", id),
VirtualPath: fmt.Sprintf("%s#%d", dbPath, id),
FileMtime: parsePiebaldTimestamp(updatedAt).UnixNano(),
})
}
return metas, rows.Err()
}
// parsePiebaldSessionResults parses a single Piebald chat and any large
// message-DAG branches as fork child sessions.
func parsePiebaldSessionResults(dbPath, chatID, machine string) ([]ParseResult, error) {
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
return nil, fmt.Errorf("piebald db not found: %s", dbPath)
}
db, err := openPiebaldDB(dbPath)
if err != nil {
return nil, err
}
defer db.Close()
c, err := loadOnePiebaldChat(db, chatID)
if err != nil {
return nil, fmt.Errorf("loading piebald chat %s: %w", chatID, err)
}
return buildPiebaldSessionResults(db, c, dbPath, machine)
}
func openPiebaldDB(dbPath string) (*sql.DB, error) {
dsn := "file:" + sqliteURIPath(dbPath) + "?mode=ro&_busy_timeout=3000"
db, err := sql.Open("sqlite3", dsn)
if err != nil {
return nil, fmt.Errorf("opening piebald db %s: %w", dbPath, err)
}
return db, nil
}
type piebaldChatRow struct {
id int64
title string
createdAt string
updatedAt string
messageCount int
currentDirectory string
worktreePath string
branchName string
projectDirectory string
projectName string
}
func loadOnePiebaldChat(db *sql.DB, chatID string) (piebaldChatRow, error) {
row := db.QueryRow(piebaldChatSelect(`
WHERE c.id = ?
AND COALESCE(c.is_deleted, 0) = 0
`), chatID)
return scanPiebaldChat(row)
}
func piebaldChatSelect(where string) string {
return `
SELECT c.id,
COALESCE(c.title, ''),
c.created_at,
COALESCE(c.updated_at, c.created_at),
c.message_count,
COALESCE(c.current_directory, ''),
COALESCE(c.worktree_path, ''),
COALESCE(c.branch_name, ''),
COALESCE(p.directory, ''),
COALESCE(p.name, '')
FROM chats c
LEFT JOIN projects p ON p.id = c.project_id
` + where
}
type piebaldChatScanner interface {
Scan(dest ...any) error
}
func scanPiebaldChat(scanner piebaldChatScanner) (piebaldChatRow, error) {
var c piebaldChatRow
err := scanner.Scan(
&c.id, &c.title, &c.createdAt, &c.updatedAt,
&c.messageCount, &c.currentDirectory, &c.worktreePath,
&c.branchName, &c.projectDirectory, &c.projectName,
)
return c, err
}
type piebaldMessageRow struct {
id int64
parentChatID int64
parentMessageID sql.NullInt64
isEnabled int
role string
model string
createdAt string
updatedAt string
inputTokens sql.NullInt64
outputTokens sql.NullInt64
reasoningTokens sql.NullInt64
cacheReadTokens sql.NullInt64
cacheWriteTokens sql.NullInt64
status string
finishReason string
errorText string
}
type piebaldPartRow struct {
id int64
partType string
partIndex int
}
type piebaldToolCallRow struct {
providerToolUseID string
toolName string
toolInput string
toolResult sql.NullString
toolError sql.NullString
toolState string
subAgentChatID sql.NullInt64
}
func buildPiebaldSessionResults(db *sql.DB, c piebaldChatRow, dbPath, machine string) ([]ParseResult, error) {
messageRows, err := loadPiebaldMessages(db, c.id)
if err != nil {
return nil, err
}
if len(messageRows) == 0 {
return nil, nil
}
branches := splitPiebaldBranches(messageRows)
if len(branches) == 0 {
return nil, nil
}
var results []ParseResult
baseID := fmt.Sprintf("piebald:%d", c.id)
for i, b := range branches {
messages, firstMsg, realUserCount, err := buildPiebaldMessages(db, b.rows)
if err != nil {
return nil, err
}
if len(messages) == 0 {
continue
}
startedAt := parsePiebaldTimestamp(c.createdAt)
if startedAt.IsZero() || i > 0 {
startedAt = messages[0].Timestamp
}
endedAt := parsePiebaldTimestamp(c.updatedAt)
if endedAt.IsZero() || i > 0 {
endedAt = messages[len(messages)-1].Timestamp
}
sess := buildPiebaldSessionMeta(c, dbPath, machine)
sess.ID = baseID
if i > 0 {
sess.ID = fmt.Sprintf("%s-%d", baseID, b.firstRowID)
sess.ParentSessionID = b.parentID
if sess.ParentSessionID == "" {
sess.ParentSessionID = baseID
}
sess.RelationshipType = RelFork
}
sess.FirstMessage = firstMsg
sess.StartedAt = startedAt
sess.EndedAt = endedAt
sess.MessageCount = len(messages)
sess.UserMessageCount = realUserCount
accumulateMessageTokenUsage(&sess, messages)
results = append(results, ParseResult{Session: sess, Messages: messages})
}
return results, nil
}
func buildPiebaldMessages(db *sql.DB, rows []piebaldMessageRow) ([]ParsedMessage, string, int, error) {
var (
messages []ParsedMessage
firstMsg string
realUserCount int
)
for _, mr := range rows {
msg, ok, err := buildPiebaldMessage(db, mr, len(messages))
if err != nil {
return nil, "", 0, err
}
if !ok {
continue
}
if firstMsg == "" && msg.Role == RoleUser && strings.TrimSpace(msg.Content) != "" {
firstMsg = truncate(strings.ReplaceAll(msg.Content, "\n", " "), 300)
}
if msg.Role == RoleUser && len(msg.ToolResults) == 0 {
realUserCount++
}
messages = append(messages, msg)
}
return messages, firstMsg, realUserCount, nil
}
func buildPiebaldSessionMeta(c piebaldChatRow, dbPath, machine string) ParsedSession {
cwd := firstNonEmptyPiebald(c.worktreePath, c.currentDirectory, c.projectDirectory)
project := c.projectName
if project == "" && c.projectDirectory != "" {
project = filepath.Base(c.projectDirectory)
}
if project == "" && cwd != "" {
project = filepath.Base(cwd)
}
if project == "" {
project = "piebald"
}
return ParsedSession{
Project: project,
Machine: machine,
Agent: AgentPiebald,
Cwd: cwd,
GitBranch: c.branchName,
SourceSessionID: fmt.Sprintf("%d", c.id),
SourceVersion: "piebald-appdb-v1",
SessionName: c.title,
File: FileInfo{
Path: fmt.Sprintf("%s#%d", dbPath, c.id),
Mtime: parsePiebaldTimestamp(c.updatedAt).UnixNano(),
},
}
}
func loadPiebaldMessages(db *sql.DB, chatID int64) ([]piebaldMessageRow, error) {
rows, err := db.Query(`
SELECT id,
parent_chat_id,
parent_message_id,
enabled,
role,
COALESCE(model, ''),
created_at,
COALESCE(updated_at, created_at),
input_tokens,
output_tokens,
reasoning_tokens,
cache_read_tokens,
cache_write_tokens,
COALESCE(status, ''),
COALESCE(finish_reason, ''),
COALESCE(error, '')
FROM messages
WHERE parent_chat_id = ?
ORDER BY created_at, id
`, chatID)
if err != nil {
return nil, err
}
defer rows.Close()
var messages []piebaldMessageRow
for rows.Next() {
var m piebaldMessageRow
if err := rows.Scan(
&m.id, &m.parentChatID, &m.parentMessageID, &m.isEnabled,
&m.role, &m.model, &m.createdAt, &m.updatedAt,
&m.inputTokens, &m.outputTokens, &m.reasoningTokens,
&m.cacheReadTokens, &m.cacheWriteTokens, &m.status,
&m.finishReason, &m.errorText,
); err != nil {
return nil, err
}
messages = append(messages, m)
}
return messages, rows.Err()
}
type piebaldBranch struct {
rows []piebaldMessageRow
parentID string
firstRowID int64
}
func splitPiebaldBranches(rows []piebaldMessageRow) []piebaldBranch {
rowByID := make(map[int64]piebaldMessageRow, len(rows))
children := make(map[int64][]piebaldMessageRow, len(rows))
for _, row := range rows {
rowByID[row.id] = row
if row.parentMessageID.Valid {
children[row.parentMessageID.Int64] = append(children[row.parentMessageID.Int64], row)
}
}
var roots []piebaldMessageRow
for _, row := range rows {
if !row.parentMessageID.Valid {
roots = append(roots, row)
continue
}
if _, ok := rowByID[row.parentMessageID.Int64]; !ok {
roots = append(roots, row)
}
}
if len(roots) != 1 {
return []piebaldBranch{{rows: rows}}
}
var forkBranches []piebaldBranch
var walk func(row piebaldMessageRow, ownerID string) []piebaldMessageRow
walk = func(row piebaldMessageRow, ownerID string) []piebaldMessageRow {
path := []piebaldMessageRow{row}
current := row
for {
kids := children[current.id]
if len(kids) == 0 {
break
}
if len(kids) == 1 {
current = kids[0]
path = append(path, current)
continue
}
mainIdx := len(kids) - 1
for i, v := range slices.Backward(kids) {
if v.enabled() {
mainIdx = i
break
}
}
for i, kid := range kids {
if i == mainIdx {
continue
}
forkID := fmt.Sprintf("piebald:%d-%d", kid.parentChatID, kid.id)
// Evaluate walk() before append so nested forks
// added by the recursive call aren't lost to
// unspecified Go evaluation order.
branchRows := walk(kid, forkID)
forkBranches = append(forkBranches, piebaldBranch{
rows: branchRows,
parentID: ownerID,
firstRowID: kid.id,
})
}
current = kids[mainIdx]
path = append(path, current)
}
return path
}
mainParent := ""
mainRows := walk(roots[0], mainParent)
branches := []piebaldBranch{{rows: mainRows}}
branches = append(branches, forkBranches...)
return branches
}
func (m piebaldMessageRow) enabled() bool {
return m.isEnabled != 0
}
func buildPiebaldMessage(db *sql.DB, mr piebaldMessageRow, ordinal int) (ParsedMessage, bool, error) {
parts, err := loadPiebaldMessageParts(db, mr.id)
if err != nil {
return ParsedMessage{}, false, err
}
var (
contentParts []string
thinking []string
toolCalls []ParsedToolCall
toolResults []ParsedToolResult
)
for _, part := range parts {
switch part.partType {
case "text":
text, isThinking, err := loadPiebaldTextPart(db, part.id)
if err != nil {
return ParsedMessage{}, false, err
}
if strings.TrimSpace(text) == "" {
continue
}
if isThinking {
thinking = append(thinking, text)
} else {
contentParts = append(contentParts, text)
}
case "tool_call":
call, result, err := loadPiebaldToolCall(db, part.id)
if err != nil {
return ParsedMessage{}, false, err
}
if call.ToolUseID != "" {
toolCalls = append(toolCalls, call)
}
if result.ToolUseID != "" {
toolResults = append(toolResults, result)
}
}
}
content := strings.TrimSpace(strings.Join(contentParts, "\n"))
thinkingText := strings.TrimSpace(strings.Join(thinking, "\n"))
role := piebaldRole(mr.role)
if content == "" && thinkingText == "" && len(toolCalls) == 0 && len(toolResults) == 0 {
return ParsedMessage{}, false, nil
}
msg := ParsedMessage{
Ordinal: ordinal,
Role: role,
Content: content,
ThinkingText: thinkingText,
Timestamp: parsePiebaldTimestamp(mr.createdAt),
HasThinking: thinkingText != "",
HasToolUse: len(toolCalls) > 0,
ContentLength: len(content) + len(thinkingText),
ToolCalls: toolCalls,
ToolResults: toolResults,
Model: mr.model,
StopReason: mr.finishReason,
}
applyPiebaldTokenUsage(&msg, mr)
return msg, true, nil
}
func loadPiebaldMessageParts(db *sql.DB, messageID int64) ([]piebaldPartRow, error) {
rows, err := db.Query(`
SELECT id, part_type, part_index
FROM message_parts
WHERE parent_chat_message_id = ?
ORDER BY part_index, id
`, messageID)
if err != nil {
return nil, err
}
defer rows.Close()
var parts []piebaldPartRow
for rows.Next() {
var p piebaldPartRow
if err := rows.Scan(&p.id, &p.partType, &p.partIndex); err != nil {
return nil, err
}
parts = append(parts, p)
}
return parts, rows.Err()
}
func loadPiebaldTextPart(db *sql.DB, partID int64) (string, bool, error) {
var isThinking bool
if err := db.QueryRow(`
SELECT COALESCE(is_thinking, 0)
FROM message_part_text
WHERE message_part_id = ?
`, partID).Scan(&isThinking); err != nil {
return "", false, err
}
rows, err := db.Query(`
SELECT COALESCE(mnt.content, '')
FROM message_content_nodes mcn
JOIN message_node_text mnt ON mnt.node_id = mcn.id
WHERE mcn.parent_text_part_id = ?
AND mcn.node_type = 'text'
ORDER BY mcn.node_index, mcn.id
`, partID)
if err != nil {
return "", false, err
}
defer rows.Close()
var chunks []string
for rows.Next() {
var chunk string
if err := rows.Scan(&chunk); err != nil {
return "", false, err
}
chunks = append(chunks, chunk)
}
return strings.Join(chunks, ""), isThinking, rows.Err()
}
func loadPiebaldToolCall(db *sql.DB, partID int64) (ParsedToolCall, ParsedToolResult, error) {
row := db.QueryRow(`
SELECT provider_tool_use_id,
tool_name,
COALESCE(tool_input, ''),
tool_result,
tool_error,
COALESCE(tool_state, ''),
sub_agent_chat_id
FROM message_part_tool_call
WHERE message_part_id = ?
`, partID)
var t piebaldToolCallRow
if err := row.Scan(
&t.providerToolUseID, &t.toolName, &t.toolInput,
&t.toolResult, &t.toolError, &t.toolState, &t.subAgentChatID,
); err != nil {
return ParsedToolCall{}, ParsedToolResult{}, err
}
call := ParsedToolCall{
ToolUseID: t.providerToolUseID,
ToolName: t.toolName,
Category: NormalizeToolCategory(t.toolName),
InputJSON: normalizeJSON(t.toolInput),
}
if t.subAgentChatID.Valid {
call.SubagentSessionID = fmt.Sprintf("piebald:%d", t.subAgentChatID.Int64)
}
resultText := ""
if t.toolResult.Valid {
resultText = t.toolResult.String
} else if t.toolError.Valid {
resultText = t.toolError.String
}
if resultText == "" && t.toolState != "" && t.toolState != "completed" {
resultText = "[" + t.toolState + "]"
}
if resultText == "" {
return call, ParsedToolResult{}, nil
}
quoted, _ := json.Marshal(resultText)
return call, ParsedToolResult{
ToolUseID: t.providerToolUseID,
ContentLength: len(resultText),
ContentRaw: string(quoted),
}, nil
}
func applyPiebaldTokenUsage(msg *ParsedMessage, mr piebaldMessageRow) {
msg.tokenPresenceKnown = true
inputTokens := 0
outputTokens := 0
cacheReadTokens := 0
cacheWriteTokens := 0
if mr.inputTokens.Valid {
inputTokens = int(mr.inputTokens.Int64)
msg.ContextTokens += inputTokens
msg.HasContextTokens = true
}
if mr.cacheReadTokens.Valid {
cacheReadTokens = int(mr.cacheReadTokens.Int64)
msg.ContextTokens += cacheReadTokens
msg.HasContextTokens = true
}
if mr.cacheWriteTokens.Valid {
cacheWriteTokens = int(mr.cacheWriteTokens.Int64)
msg.ContextTokens += cacheWriteTokens
msg.HasContextTokens = true
}
if mr.outputTokens.Valid {
outputTokens = int(mr.outputTokens.Int64)
msg.OutputTokens += outputTokens
msg.HasOutputTokens = true
}
if mr.reasoningTokens.Valid {
outputTokens += int(mr.reasoningTokens.Int64)
msg.OutputTokens += int(mr.reasoningTokens.Int64)
msg.HasOutputTokens = true
}
if !msg.HasContextTokens && !msg.HasOutputTokens {
return
}
normalized := map[string]int{
"input_tokens": inputTokens,
"output_tokens": outputTokens,
"cache_read_input_tokens": cacheReadTokens,
"cache_creation_input_tokens": cacheWriteTokens,
}
j, err := json.Marshal(normalized)
if err != nil {
return
}
msg.TokenUsage = j
}
func piebaldRole(raw string) RoleType {
switch strings.ToLower(raw) {
case "assistant":
return RoleAssistant
default:
return RoleUser
}
}
func parsePiebaldTimestamp(raw string) time.Time {
return parseTimestamp(raw)
}
func normalizeJSON(raw string) string {
trimmed := strings.TrimSpace(raw)
if trimmed == "" || json.Valid([]byte(trimmed)) {
return trimmed
}
quoted, _ := json.Marshal(trimmed)
return string(quoted)
}
func firstNonEmptyPiebald(values ...string) string {
for _, value := range values {
if value != "" {
return value
}
}
return ""
}