Skip to content

Commit 53d4ff0

Browse files
authored
make opencode persiting sessions (#5)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
1 parent 3b34126 commit 53d4ff0

5 files changed

Lines changed: 850 additions & 31 deletions

File tree

opencode/Dockerfile

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,26 @@ COPY --from=builder /app/opencode-mcp-server /usr/local/bin/opencode-mcp-server
2929
ENV OPENCODE_SESSION_DIR=/root
3030
ENV OPENCODE_BINARY=/usr/local/bin/opencode
3131
ENV OPENCODE_MAX_SESSIONS=10
32-
ENV OPENCODE_LOG_RETENTION_HOURS=24
33-
ENV OPENCODE_FORMAT=json
3432
ENV OPENCODE_SHARE=false
3533

34+
# Persistence and retention settings
35+
ENV OPENCODE_PERSISTENCE_ENABLED=true
36+
ENV OPENCODE_AUTO_SAVE_INTERVAL=30s
37+
ENV OPENCODE_DEFAULT_RETENTION_HOURS=168
38+
ENV OPENCODE_COMPLETED_RETENTION_HOURS=168
39+
ENV OPENCODE_FAILED_RETENTION_HOURS=24
40+
ENV OPENCODE_STOPPED_RETENTION_HOURS=48
41+
42+
# Logging
43+
ENV OPENCODE_LOG_LEVEL=INFO
44+
3645
# Optional environment variables (uncomment to use):
3746
# ENV OPENCODE_MODEL=openai/gpt-4
3847
# ENV OPENCODE_AGENT=
3948
# ENV OPENCODE_VARIANT=
4049
# ENV OPENCODE_ATTACH=
4150
# ENV OPENCODE_PORT=0
51+
# ENV OPENCODE_LOG_FILE=/var/log/opencode/opencode-mcp.log
4252

4353
# Run the MCP server
4454
ENTRYPOINT ["opencode-mcp-server"]

opencode/logging.go

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"os"
7+
"path/filepath"
8+
"runtime/debug"
9+
"time"
10+
)
11+
12+
// Logger provides structured logging with different levels
13+
type Logger struct {
14+
logger *log.Logger
15+
logLevel LogLevel
16+
logFile *os.File
17+
}
18+
19+
// LogLevel represents the logging level
20+
type LogLevel int
21+
22+
const (
23+
DEBUG LogLevel = iota
24+
INFO
25+
WARN
26+
ERROR
27+
FATAL
28+
)
29+
30+
func (l LogLevel) String() string {
31+
switch l {
32+
case DEBUG:
33+
return "DEBUG"
34+
case INFO:
35+
return "INFO"
36+
case WARN:
37+
return "WARN"
38+
case ERROR:
39+
return "ERROR"
40+
case FATAL:
41+
return "FATAL"
42+
default:
43+
return "UNKNOWN"
44+
}
45+
}
46+
47+
var defaultLogger *Logger
48+
49+
// InitLogger initializes the default logger
50+
func InitLogger() {
51+
logLevel := getEnvLogLevel("OPENCODE_LOG_LEVEL", INFO)
52+
logFile := getEnv("OPENCODE_LOG_FILE", "")
53+
54+
var writer *os.File
55+
if logFile != "" {
56+
// Ensure log directory exists
57+
logDir := filepath.Dir(logFile)
58+
if err := os.MkdirAll(logDir, 0755); err != nil {
59+
fmt.Fprintf(os.Stderr, "Failed to create log directory: %v\n", err)
60+
writer = os.Stderr
61+
} else {
62+
var err error
63+
writer, err = os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
64+
if err != nil {
65+
fmt.Fprintf(os.Stderr, "Failed to open log file: %v\n", err)
66+
writer = os.Stderr
67+
}
68+
}
69+
} else {
70+
writer = os.Stderr
71+
}
72+
73+
defaultLogger = &Logger{
74+
logger: log.New(writer, "", log.LstdFlags),
75+
logLevel: logLevel,
76+
logFile: writer,
77+
}
78+
}
79+
80+
// GetLogger returns the default logger
81+
func GetLogger() *Logger {
82+
if defaultLogger == nil {
83+
InitLogger()
84+
}
85+
return defaultLogger
86+
}
87+
88+
// log logs a message at the specified level
89+
func (l *Logger) log(level LogLevel, format string, args ...interface{}) {
90+
if level < l.logLevel {
91+
return
92+
}
93+
94+
msg := fmt.Sprintf(format, args...)
95+
l.logger.Printf("[%s] %s", level.String(), msg)
96+
}
97+
98+
// Debug logs a debug message
99+
func (l *Logger) Debug(format string, args ...interface{}) {
100+
l.log(DEBUG, format, args...)
101+
}
102+
103+
// Info logs an info message
104+
func (l *Logger) Info(format string, args ...interface{}) {
105+
l.log(INFO, format, args...)
106+
}
107+
108+
// Warn logs a warning message
109+
func (l *Logger) Warn(format string, args ...interface{}) {
110+
l.log(WARN, format, args...)
111+
}
112+
113+
// Error logs an error message
114+
func (l *Logger) Error(format string, args ...interface{}) {
115+
l.log(ERROR, format, args...)
116+
}
117+
118+
// Fatal logs a fatal message and exits
119+
func (l *Logger) Fatal(format string, args ...interface{}) {
120+
l.log(FATAL, format, args...)
121+
os.Exit(1)
122+
}
123+
124+
// Close closes the log file if opened
125+
func (l *Logger) Close() {
126+
if l.logFile != os.Stderr && l.logFile != nil {
127+
l.logFile.Close()
128+
}
129+
}
130+
131+
// WithRecovery wraps a function with panic recovery
132+
func WithRecovery(operation string, fn func()) {
133+
defer func() {
134+
if r := recover(); r != nil {
135+
logger := GetLogger()
136+
logger.Error("Panic in %s: %v\nStack trace:\n%s", operation, r, debug.Stack())
137+
}
138+
}()
139+
fn()
140+
}
141+
142+
// getEnvLogLevel parses log level from environment
143+
func getEnvLogLevel(key string, defaultValue LogLevel) LogLevel {
144+
value := os.Getenv(key)
145+
switch value {
146+
case "DEBUG":
147+
return DEBUG
148+
case "INFO":
149+
return INFO
150+
case "WARN", "WARNING":
151+
return WARN
152+
case "ERROR":
153+
return ERROR
154+
case "FATAL":
155+
return FATAL
156+
default:
157+
return defaultValue
158+
}
159+
}
160+
161+
// SessionMetrics tracks session-related metrics
162+
type SessionMetrics struct {
163+
TotalSessionsCreated int
164+
TotalSessionsCompleted int
165+
TotalSessionsFailed int
166+
ActiveSessions int
167+
StartTime time.Time
168+
}
169+
170+
var metrics = &SessionMetrics{
171+
StartTime: time.Now(),
172+
}
173+
174+
// RecordSessionCreated records a new session creation
175+
func RecordSessionCreated() {
176+
metrics.TotalSessionsCreated++
177+
metrics.ActiveSessions++
178+
}
179+
180+
// RecordSessionCompleted records a session completion
181+
func RecordSessionCompleted() {
182+
metrics.TotalSessionsCompleted++
183+
metrics.ActiveSessions--
184+
if metrics.ActiveSessions < 0 {
185+
metrics.ActiveSessions = 0
186+
}
187+
}
188+
189+
// RecordSessionFailed records a session failure
190+
func RecordSessionFailed() {
191+
metrics.TotalSessionsFailed++
192+
metrics.ActiveSessions--
193+
if metrics.ActiveSessions < 0 {
194+
metrics.ActiveSessions = 0
195+
}
196+
}
197+
198+
// GetMetrics returns current metrics
199+
func GetMetrics() SessionMetrics {
200+
return *metrics
201+
}
202+
203+
// LogMetrics logs current metrics
204+
func LogMetrics() {
205+
logger := GetLogger()
206+
logger.Info("Session metrics - Created: %d, Completed: %d, Failed: %d, Active: %d, Uptime: %s",
207+
metrics.TotalSessionsCreated,
208+
metrics.TotalSessionsCompleted,
209+
metrics.TotalSessionsFailed,
210+
metrics.ActiveSessions,
211+
time.Since(metrics.StartTime).Round(time.Second),
212+
)
213+
}

opencode/main.go

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,12 @@ type Session struct {
2929

3030
// SessionManager manages all opencode sessions
3131
type SessionManager struct {
32-
sessions map[string]*Session
33-
mutex sync.RWMutex
34-
sessionDir string
35-
maxSessions int
32+
sessions map[string]*Session
33+
mutex sync.RWMutex
34+
sessionDir string
35+
maxSessions int
36+
store *JSONSessionStore
37+
retentionHours map[string]int // retention hours by status
3638
}
3739

3840
// Global session manager
@@ -57,10 +59,19 @@ func getEnvInt(key string, defaultValue int) int {
5759
}
5860

5961
func main() {
62+
// Initialize logger first
63+
InitLogger()
64+
logger := GetLogger()
65+
66+
logger.Info("Starting OpenCode MCP server")
67+
6068
// Initialize session manager
6169
sessionDir := getEnv("OPENCODE_SESSION_DIR", "/tmp/opencode-sessions")
6270
maxSessions := getEnvInt("OPENCODE_MAX_SESSIONS", 10)
6371

72+
logger.Info("Session directory: %s", sessionDir)
73+
logger.Info("Max sessions: %d", maxSessions)
74+
6475
// Ensure session directory exists
6576
if err := os.MkdirAll(sessionDir, 0755); err != nil {
6677
log.Fatalf("Failed to create session directory: %v", err)
@@ -70,6 +81,28 @@ func main() {
7081
sessions: make(map[string]*Session),
7182
sessionDir: sessionDir,
7283
maxSessions: maxSessions,
84+
retentionHours: map[string]int{
85+
"completed": getEnvInt("OPENCODE_COMPLETED_RETENTION_HOURS", getEnvInt("OPENCODE_DEFAULT_RETENTION_HOURS", 168)), // 7 days default
86+
"failed": getEnvInt("OPENCODE_FAILED_RETENTION_HOURS", 24),
87+
"stopped": getEnvInt("OPENCODE_STOPPED_RETENTION_HOURS", 48),
88+
"running": 0, // Never clean up running sessions
89+
"starting": 0, // Never clean up starting sessions
90+
},
91+
}
92+
93+
// Initialize persistence store
94+
globalSessionManager.store = NewJSONSessionStore(sessionDir, globalSessionManager)
95+
96+
// Start auto-save
97+
autoSaveInterval := getEnvDuration("OPENCODE_AUTO_SAVE_INTERVAL", 30*time.Second)
98+
globalSessionManager.store.StartAutoSave(autoSaveInterval)
99+
defer globalSessionManager.store.StopAutoSave()
100+
101+
// Load existing sessions on startup
102+
if err := globalSessionManager.LoadSessions(); err != nil {
103+
log.Printf("Warning: Failed to load existing sessions: %v", err)
104+
} else {
105+
log.Printf("Loaded %d existing sessions", len(globalSessionManager.sessions))
73106
}
74107

75108
// Create MCP server

0 commit comments

Comments
 (0)