Skip to content

Commit 8addb6e

Browse files
committed
there is no session delete, so fallback to sqlite
Entire-Checkpoint: 03bc551af447
1 parent a88a36a commit 8addb6e

3 files changed

Lines changed: 85 additions & 27 deletions

File tree

cmd/entire/cli/agent/opencode/cli_commands.go

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,35 +5,12 @@ import (
55
"errors"
66
"fmt"
77
"os/exec"
8-
"strings"
98
"time"
109
)
1110

1211
// openCodeCommandTimeout is the maximum time to wait for opencode CLI commands.
1312
const openCodeCommandTimeout = 30 * time.Second
1413

15-
// runOpenCodeSessionDelete runs `opencode session delete <sessionID>` to remove
16-
// a session from OpenCode's database. Treats "Session not found" as success
17-
// (nothing to delete).
18-
func runOpenCodeSessionDelete(sessionID string) error {
19-
ctx, cancel := context.WithTimeout(context.Background(), openCodeCommandTimeout)
20-
defer cancel()
21-
22-
cmd := exec.CommandContext(ctx, "opencode", "session", "delete", sessionID)
23-
output, err := cmd.CombinedOutput()
24-
if err != nil {
25-
if ctx.Err() == context.DeadlineExceeded {
26-
return fmt.Errorf("opencode session delete timed out after %s", openCodeCommandTimeout)
27-
}
28-
// Treat "Session not found" as success — nothing to delete.
29-
if strings.Contains(string(output), "Session not found") {
30-
return nil
31-
}
32-
return fmt.Errorf("opencode session delete failed: %w (output: %s)", err, string(output))
33-
}
34-
return nil
35-
}
36-
3714
// runOpenCodeExport runs `opencode export <sessionID>` to export a session
3815
// from OpenCode's database. Returns the JSON export data as bytes.
3916
func runOpenCodeExport(sessionID string) ([]byte, error) {

cmd/entire/cli/agent/opencode/opencode.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -246,12 +246,15 @@ func (a *OpenCodeAgent) WriteSession(session *agent.AgentSession) error {
246246
// For rewind (session already exists), the session is deleted first so the
247247
// reimport replaces it with the checkpoint-state messages.
248248
func (a *OpenCodeAgent) importSessionIntoOpenCode(sessionID string, exportData []byte) error {
249-
// Delete the session first so reimport replaces it cleanly.
249+
// Delete existing messages first so reimport replaces them cleanly.
250250
// opencode import uses ON CONFLICT DO NOTHING, so existing messages
251251
// would be skipped without this step (breaking rewind).
252-
// runOpenCodeSessionDelete treats "not found" as success.
253-
if err := runOpenCodeSessionDelete(sessionID); err != nil {
254-
return fmt.Errorf("failed to delete existing session: %w", err)
252+
// Uses direct SQLite delete since OpenCode CLI has no session delete command.
253+
if err := deleteMessagesFromSQLite(sessionID); err != nil {
254+
// Non-fatal: DB might not exist yet (first session), or sqlite3 not installed.
255+
// Import will still work for new sessions; only rewind of existing sessions
256+
// would have stale messages.
257+
fmt.Fprintf(os.Stderr, "warning: could not clear existing messages: %v\n", err)
255258
}
256259

257260
// Write export JSON to a temp file for opencode import
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package opencode
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"os/exec"
8+
"path/filepath"
9+
"time"
10+
)
11+
12+
// getOpenCodeDBPath returns the path to OpenCode's SQLite database.
13+
// OpenCode always uses ~/.local/share/opencode/opencode.db (XDG default)
14+
// regardless of platform — it does NOT use ~/Library/Application Support on macOS.
15+
//
16+
// XDG_DATA_HOME overrides the default on all platforms.
17+
func getOpenCodeDBPath() (string, error) {
18+
dataDir := os.Getenv("XDG_DATA_HOME")
19+
if dataDir == "" {
20+
home, err := os.UserHomeDir()
21+
if err != nil {
22+
return "", fmt.Errorf("failed to get home directory: %w", err)
23+
}
24+
dataDir = filepath.Join(home, ".local", "share")
25+
}
26+
return filepath.Join(dataDir, "opencode", "opencode.db"), nil
27+
}
28+
29+
// runSQLiteQuery executes a SQL query against OpenCode's SQLite database.
30+
// Returns the combined stdout/stderr output.
31+
func runSQLiteQuery(query string, timeout time.Duration) ([]byte, error) {
32+
dbPath, err := getOpenCodeDBPath()
33+
if err != nil {
34+
return nil, fmt.Errorf("failed to get OpenCode DB path: %w", err)
35+
}
36+
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
37+
return nil, fmt.Errorf("OpenCode database not found: %w", err)
38+
}
39+
40+
ctx, cancel := context.WithTimeout(context.Background(), timeout)
41+
defer cancel()
42+
43+
//nolint:gosec // G204: query is constructed from sanitized inputs (escapeSQLiteString)
44+
cmd := exec.CommandContext(ctx, "sqlite3", dbPath, query)
45+
output, err := cmd.CombinedOutput()
46+
if err != nil {
47+
return output, fmt.Errorf("sqlite3 query failed: %w", err)
48+
}
49+
return output, nil
50+
}
51+
52+
// deleteMessagesFromSQLite removes all messages (and cascading parts) for a session.
53+
// This is used before reimporting a session during rewind so that `opencode import`
54+
// can insert the checkpoint-state messages (import uses ON CONFLICT DO NOTHING).
55+
func deleteMessagesFromSQLite(sessionID string) error {
56+
// Enable foreign keys so CASCADE deletes work (parts are deleted with messages).
57+
query := fmt.Sprintf(
58+
"PRAGMA foreign_keys = ON; DELETE FROM message WHERE session_id = '%s';",
59+
escapeSQLiteString(sessionID),
60+
)
61+
if output, err := runSQLiteQuery(query, 5*time.Second); err != nil {
62+
return fmt.Errorf("failed to delete messages from OpenCode DB: %w (output: %s)", err, string(output))
63+
}
64+
return nil
65+
}
66+
67+
// escapeSQLiteString escapes single quotes in a string for safe use in SQLite queries.
68+
func escapeSQLiteString(s string) string {
69+
result := make([]byte, 0, len(s))
70+
for i := range len(s) {
71+
if s[i] == '\'' {
72+
result = append(result, '\'', '\'')
73+
} else {
74+
result = append(result, s[i])
75+
}
76+
}
77+
return string(result)
78+
}

0 commit comments

Comments
 (0)