169 lines
3.6 KiB
Go
169 lines
3.6 KiB
Go
package core
|
|
|
|
import (
|
|
"log/slog"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// normalizeWorkspacePath cleans and resolves a workspace path to prevent
|
|
// mismatches caused by trailing slashes, symlinks, or relative segments.
|
|
// If the path cannot be resolved (e.g. doesn't exist yet), falls back to
|
|
// filepath.Clean only.
|
|
func normalizeWorkspacePath(path string) string {
|
|
cleaned := filepath.Clean(path)
|
|
resolved, err := filepath.EvalSymlinks(cleaned)
|
|
if err != nil {
|
|
return cleaned
|
|
}
|
|
if resolved != path {
|
|
slog.Debug("workspace path normalized", "original", path, "normalized", resolved)
|
|
}
|
|
return resolved
|
|
}
|
|
|
|
// workspaceState holds the runtime state for a single workspace.
|
|
type workspaceState struct {
|
|
mu sync.Mutex
|
|
workspace string
|
|
sessions *SessionManager
|
|
agent Agent
|
|
lastActivity time.Time
|
|
activeTurns int
|
|
}
|
|
|
|
func newWorkspaceState(workspace string) *workspaceState {
|
|
return &workspaceState{
|
|
workspace: workspace,
|
|
lastActivity: time.Now(),
|
|
}
|
|
}
|
|
|
|
func (ws *workspaceState) Touch() {
|
|
ws.mu.Lock()
|
|
ws.lastActivity = time.Now()
|
|
ws.mu.Unlock()
|
|
}
|
|
|
|
func (ws *workspaceState) BeginTurn() {
|
|
ws.mu.Lock()
|
|
ws.activeTurns++
|
|
ws.lastActivity = time.Now()
|
|
ws.mu.Unlock()
|
|
}
|
|
|
|
func (ws *workspaceState) EndTurn() {
|
|
ws.mu.Lock()
|
|
if ws.activeTurns > 0 {
|
|
ws.activeTurns--
|
|
}
|
|
ws.lastActivity = time.Now()
|
|
ws.mu.Unlock()
|
|
}
|
|
|
|
func (ws *workspaceState) HasActiveTurn() bool {
|
|
ws.mu.Lock()
|
|
defer ws.mu.Unlock()
|
|
return ws.activeTurns > 0
|
|
}
|
|
|
|
func (ws *workspaceState) LastActivity() time.Time {
|
|
ws.mu.Lock()
|
|
defer ws.mu.Unlock()
|
|
return ws.lastActivity
|
|
}
|
|
|
|
// workspacePool manages a set of workspace states with idle reaping.
|
|
type workspacePool struct {
|
|
mu sync.RWMutex
|
|
states map[string]*workspaceState // workspace path -> state
|
|
idleTimeout time.Duration
|
|
}
|
|
|
|
func newWorkspacePool(idleTimeout time.Duration) *workspacePool {
|
|
return &workspacePool{
|
|
states: make(map[string]*workspaceState),
|
|
idleTimeout: idleTimeout,
|
|
}
|
|
}
|
|
|
|
// Get returns the state for a workspace.
|
|
func (p *workspacePool) Get(workspace string) *workspaceState {
|
|
p.mu.RLock()
|
|
state := p.states[workspace]
|
|
p.mu.RUnlock()
|
|
if state != nil {
|
|
return state
|
|
}
|
|
|
|
normalized := normalizeWorkspacePath(workspace)
|
|
if normalized == workspace {
|
|
return nil
|
|
}
|
|
|
|
p.mu.RLock()
|
|
defer p.mu.RUnlock()
|
|
return p.states[normalized]
|
|
}
|
|
|
|
// GetOrCreate returns or creates state for a workspace.
|
|
func (p *workspacePool) GetOrCreate(workspace string) *workspaceState {
|
|
p.mu.Lock()
|
|
if s, ok := p.states[workspace]; ok {
|
|
p.mu.Unlock()
|
|
return s
|
|
}
|
|
p.mu.Unlock()
|
|
|
|
normalized := normalizeWorkspacePath(workspace)
|
|
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
if s, ok := p.states[workspace]; ok {
|
|
return s
|
|
}
|
|
if normalized != workspace {
|
|
if s, ok := p.states[normalized]; ok {
|
|
return s
|
|
}
|
|
workspace = normalized
|
|
}
|
|
|
|
s := newWorkspaceState(workspace)
|
|
p.states[workspace] = s
|
|
return s
|
|
}
|
|
|
|
// ReapIdle removes and returns workspace paths that have been idle longer than idleTimeout.
|
|
// A zero idleTimeout disables reaping entirely.
|
|
func (p *workspacePool) ReapIdle() []string {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
if p.idleTimeout <= 0 {
|
|
return nil
|
|
}
|
|
cutoff := time.Now().Add(-p.idleTimeout)
|
|
var reaped []string
|
|
for path, state := range p.states {
|
|
if state.HasActiveTurn() {
|
|
continue
|
|
}
|
|
if state.LastActivity().Before(cutoff) {
|
|
reaped = append(reaped, path)
|
|
delete(p.states, path)
|
|
}
|
|
}
|
|
return reaped
|
|
}
|
|
|
|
func (p *workspacePool) All() map[string]*workspaceState {
|
|
p.mu.RLock()
|
|
defer p.mu.RUnlock()
|
|
result := make(map[string]*workspaceState, len(p.states))
|
|
for k, v := range p.states {
|
|
result[k] = v
|
|
}
|
|
return result
|
|
}
|