Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
190 lines
6.9 KiB
Go
190 lines
6.9 KiB
Go
package storesqlite
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// ErrInteractionTreeRootRequired reports that an interaction-tree query was
|
|
// addressed to a child Session. Tree ownership always starts at a root Session.
|
|
var ErrInteractionTreeRootRequired = errors.New("interaction tree query requires a root session")
|
|
|
|
// ErrInteractionTreeRootTurnNotFound reports that an explicitly selected root
|
|
// Turn is missing, belongs to another Session, or is retracted.
|
|
var ErrInteractionTreeRootTurnNotFound = errors.New("interaction tree root turn was not found")
|
|
|
|
const sessionInteractionTreeSnapshotSQL = `
|
|
WITH descendant_sessions AS (
|
|
SELECT workspace_id, agent_session_id
|
|
FROM workspace_agent_sessions
|
|
WHERE workspace_id = ?
|
|
AND session_kind = 'child'
|
|
AND root_agent_session_id = ?
|
|
AND root_turn_id = ?
|
|
AND deleted_at_unix_ms = 0
|
|
),
|
|
ranked_descendant_turns AS (
|
|
SELECT turn.workspace_id, turn.agent_session_id, turn.turn_id,
|
|
ROW_NUMBER() OVER (
|
|
PARTITION BY turn.workspace_id, turn.agent_session_id
|
|
ORDER BY turn.updated_at_unix_ms DESC, turn.created_at_unix_ms DESC,
|
|
turn.started_at_unix_ms DESC, turn.turn_id DESC
|
|
) AS rank
|
|
FROM workspace_agent_turns turn
|
|
JOIN descendant_sessions session
|
|
ON session.workspace_id = turn.workspace_id
|
|
AND session.agent_session_id = turn.agent_session_id
|
|
WHERE NOT EXISTS (
|
|
SELECT 1 FROM workspace_agent_turn_history history
|
|
WHERE history.workspace_id = turn.workspace_id
|
|
AND history.agent_session_id = turn.agent_session_id
|
|
AND history.turn_id = turn.turn_id
|
|
AND history.history_state = 'retracted'
|
|
)
|
|
),
|
|
selected_turns AS (
|
|
SELECT ? AS workspace_id, ? AS agent_session_id, ? AS turn_id
|
|
UNION ALL
|
|
SELECT workspace_id, agent_session_id, turn_id
|
|
FROM ranked_descendant_turns
|
|
WHERE rank = 1
|
|
)
|
|
SELECT interaction.workspace_id,
|
|
interaction.agent_session_id,
|
|
interaction.request_id,
|
|
interaction.turn_id,
|
|
interaction.kind,
|
|
interaction.status,
|
|
interaction.tool_name,
|
|
interaction.input_json,
|
|
interaction.output_json,
|
|
interaction.metadata_json,
|
|
interaction.created_at_unix_ms,
|
|
interaction.updated_at_unix_ms
|
|
FROM workspace_agent_interactions interaction
|
|
JOIN selected_turns selected
|
|
ON selected.workspace_id = interaction.workspace_id
|
|
AND selected.agent_session_id = interaction.agent_session_id
|
|
AND selected.turn_id = interaction.turn_id
|
|
ORDER BY interaction.agent_session_id ASC,
|
|
interaction.turn_id ASC,
|
|
interaction.request_id ASC
|
|
`
|
|
|
|
// GetSessionInteractionTreeSnapshot reads a root Turn and all descendant
|
|
// Sessions' latest-Turn interactions from one SQLite snapshot. Descendants are
|
|
// selected by their immutable root identity, so traversal depth does not
|
|
// affect the query cost.
|
|
func (s *Store) GetSessionInteractionTreeSnapshot(
|
|
ctx context.Context,
|
|
query SessionInteractionTreeQuery,
|
|
) (SessionInteractionTreeSnapshot, bool, error) {
|
|
if s == nil || s.db == nil {
|
|
return SessionInteractionTreeSnapshot{}, false, errors.New("workspace database is not initialized")
|
|
}
|
|
query.WorkspaceID = strings.TrimSpace(query.WorkspaceID)
|
|
query.RootAgentSessionID = strings.TrimSpace(query.RootAgentSessionID)
|
|
query.RootTurnID = strings.TrimSpace(query.RootTurnID)
|
|
if query.WorkspaceID == "" || query.RootAgentSessionID == "" {
|
|
return SessionInteractionTreeSnapshot{}, false, nil
|
|
}
|
|
|
|
tx, err := s.db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
|
|
if err != nil {
|
|
return SessionInteractionTreeSnapshot{}, false, fmt.Errorf("begin interaction tree read: %w", err)
|
|
}
|
|
defer func() { _ = tx.Rollback() }()
|
|
|
|
var kind string
|
|
err = tx.QueryRowContext(ctx, `
|
|
SELECT session_kind
|
|
FROM workspace_agent_sessions
|
|
WHERE workspace_id = ? AND agent_session_id = ? AND deleted_at_unix_ms = 0
|
|
`, query.WorkspaceID, query.RootAgentSessionID).Scan(&kind)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return SessionInteractionTreeSnapshot{}, false, nil
|
|
}
|
|
if err != nil {
|
|
return SessionInteractionTreeSnapshot{}, false, fmt.Errorf("read interaction tree root session: %w", err)
|
|
}
|
|
if kind != SessionKindRoot {
|
|
return SessionInteractionTreeSnapshot{}, false, ErrInteractionTreeRootRequired
|
|
}
|
|
|
|
rootTurnID := query.RootTurnID
|
|
if rootTurnID != "" {
|
|
var validatedRootTurnID string
|
|
err = tx.QueryRowContext(ctx, `
|
|
SELECT turn.turn_id
|
|
FROM workspace_agent_turns turn
|
|
WHERE turn.workspace_id = ? AND turn.agent_session_id = ? AND turn.turn_id = ?
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM workspace_agent_turn_history history
|
|
WHERE history.workspace_id = turn.workspace_id
|
|
AND history.agent_session_id = turn.agent_session_id
|
|
AND history.turn_id = turn.turn_id
|
|
AND history.history_state = 'retracted'
|
|
)
|
|
`, query.WorkspaceID, query.RootAgentSessionID, rootTurnID).Scan(&validatedRootTurnID)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return SessionInteractionTreeSnapshot{}, false, ErrInteractionTreeRootTurnNotFound
|
|
}
|
|
if err != nil {
|
|
return SessionInteractionTreeSnapshot{}, false, fmt.Errorf("validate interaction tree root turn: %w", err)
|
|
}
|
|
rootTurnID = validatedRootTurnID
|
|
} else {
|
|
err = tx.QueryRowContext(ctx, `
|
|
SELECT turn_id
|
|
FROM workspace_agent_turns turn
|
|
WHERE workspace_id = ? AND agent_session_id = ?
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM workspace_agent_turn_history history
|
|
WHERE history.workspace_id = turn.workspace_id
|
|
AND history.agent_session_id = turn.agent_session_id
|
|
AND history.turn_id = turn.turn_id
|
|
AND history.history_state = 'retracted'
|
|
)
|
|
ORDER BY updated_at_unix_ms DESC, created_at_unix_ms DESC,
|
|
started_at_unix_ms DESC, turn_id DESC
|
|
LIMIT 1
|
|
`, query.WorkspaceID, query.RootAgentSessionID).Scan(&rootTurnID)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return SessionInteractionTreeSnapshot{}, true, nil
|
|
}
|
|
if err != nil {
|
|
return SessionInteractionTreeSnapshot{}, false, fmt.Errorf("resolve interaction tree root turn: %w", err)
|
|
}
|
|
}
|
|
|
|
rows, err := tx.QueryContext(ctx, sessionInteractionTreeSnapshotSQL,
|
|
query.WorkspaceID, query.RootAgentSessionID, rootTurnID,
|
|
query.WorkspaceID, query.RootAgentSessionID, rootTurnID)
|
|
if err != nil {
|
|
return SessionInteractionTreeSnapshot{}, false, fmt.Errorf("read interaction tree interactions: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
snapshot := SessionInteractionTreeSnapshot{RootTurnID: rootTurnID}
|
|
for rows.Next() {
|
|
interaction, scanErr := scanAgentInteraction(rows)
|
|
if scanErr != nil {
|
|
return SessionInteractionTreeSnapshot{}, false, scanErr
|
|
}
|
|
snapshot.Interactions = append(snapshot.Interactions, interaction)
|
|
if interaction.Status == InteractionStatusPending {
|
|
snapshot.PendingInteractions = append(snapshot.PendingInteractions, interaction)
|
|
}
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return SessionInteractionTreeSnapshot{}, false, fmt.Errorf("iterate interaction tree interactions: %w", err)
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
return SessionInteractionTreeSnapshot{}, false, fmt.Errorf("finish interaction tree read: %w", err)
|
|
}
|
|
return snapshot, true, nil
|
|
}
|