* fix: persist per-workspace dir overrides across agent recreation In multi-workspace mode, /dir overrides were lost after daemon restart or workspace pool eviction because getOrCreateWorkspaceAgent always recreated agents with work_dir=workspace root, ignoring the override stored in project state. Changes: - Add WorkspaceDirOverrides map to ProjectStateStore for per-workspace dir override persistence - Apply persisted override when recreating workspace agents in getOrCreateWorkspaceAgent - Store workspace-specific overrides in dirApply when in multi-workspace mode (set and reset) - Auto-clear invalid overrides (missing/non-directory paths) Backward compatible: existing work_dir_override field preserved for single-workspace mode. * fix: use per-channel dir overrides instead of per-workspace Two channels bound to the same workspace were sharing /dir overrides. Change the override key from workspace path to interactiveKey (workspace:sessionKey) so each channel maintains independent dir state. - Remove override from getOrCreateWorkspaceAgent (shared agent layer) - Add applyChannelDirOverride helper, called in handleMessage and commandContext (per-channel routing layer) - dirApply now stores/clears overrides keyed by interactiveKey * refactor: centralize multi-workspace context resolution Use a shared workspaceContext helper so commandContext and message routing build the same workspace agent/session/interactiveKey state in one place. This keeps the per-channel dir override fix unchanged while removing duplicate override application logic. * fix: route multi-workspace dir overrides by effective work dir Avoid mutating shared workspace agents when applying per-channel /dir overrides. Resolve the effective work dir from project state and route each channel to an agent context keyed by that dir instead. This removes cross-channel work_dir races while preserving existing /dir persistence and reset behavior.
118 lines
2.8 KiB
Go
118 lines
2.8 KiB
Go
package core
|
|
|
|
import (
|
|
"encoding/json"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
)
|
|
|
|
type projectStateData struct {
|
|
WorkDirOverride string `json:"work_dir_override,omitempty"`
|
|
WorkspaceDirOverrides map[string]string `json:"workspace_dir_overrides,omitempty"`
|
|
}
|
|
|
|
// ProjectStateStore persists lightweight runtime state for one project.
|
|
type ProjectStateStore struct {
|
|
mu sync.RWMutex
|
|
storePath string
|
|
state projectStateData
|
|
}
|
|
|
|
func NewProjectStateStore(path string) *ProjectStateStore {
|
|
ps := &ProjectStateStore{storePath: path}
|
|
if path != "" {
|
|
ps.load()
|
|
}
|
|
return ps
|
|
}
|
|
|
|
func (ps *ProjectStateStore) WorkDirOverride() string {
|
|
ps.mu.RLock()
|
|
defer ps.mu.RUnlock()
|
|
return ps.state.WorkDirOverride
|
|
}
|
|
|
|
func (ps *ProjectStateStore) SetWorkDirOverride(dir string) {
|
|
ps.mu.Lock()
|
|
defer ps.mu.Unlock()
|
|
ps.state.WorkDirOverride = dir
|
|
}
|
|
|
|
func (ps *ProjectStateStore) WorkspaceDirOverride(workspace string) string {
|
|
ps.mu.RLock()
|
|
defer ps.mu.RUnlock()
|
|
if ps.state.WorkspaceDirOverrides == nil {
|
|
return ""
|
|
}
|
|
return ps.state.WorkspaceDirOverrides[workspace]
|
|
}
|
|
|
|
func (ps *ProjectStateStore) SetWorkspaceDirOverride(workspace, dir string) {
|
|
ps.mu.Lock()
|
|
defer ps.mu.Unlock()
|
|
if ps.state.WorkspaceDirOverrides == nil {
|
|
ps.state.WorkspaceDirOverrides = make(map[string]string)
|
|
}
|
|
ps.state.WorkspaceDirOverrides[workspace] = dir
|
|
}
|
|
|
|
func (ps *ProjectStateStore) ClearWorkspaceDirOverride(workspace string) {
|
|
ps.mu.Lock()
|
|
defer ps.mu.Unlock()
|
|
if ps.state.WorkspaceDirOverrides == nil {
|
|
return
|
|
}
|
|
delete(ps.state.WorkspaceDirOverrides, workspace)
|
|
if len(ps.state.WorkspaceDirOverrides) == 0 {
|
|
ps.state.WorkspaceDirOverrides = nil
|
|
}
|
|
}
|
|
|
|
func (ps *ProjectStateStore) ClearWorkDirOverride() {
|
|
ps.SetWorkDirOverride("")
|
|
}
|
|
|
|
func (ps *ProjectStateStore) Save() {
|
|
ps.mu.RLock()
|
|
defer ps.mu.RUnlock()
|
|
ps.saveLocked()
|
|
}
|
|
|
|
func (ps *ProjectStateStore) saveLocked() {
|
|
if ps.storePath == "" {
|
|
return
|
|
}
|
|
|
|
data, err := json.MarshalIndent(ps.state, "", " ")
|
|
if err != nil {
|
|
slog.Error("project_state: failed to marshal", "error", err)
|
|
return
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(ps.storePath), 0o755); err != nil {
|
|
slog.Error("project_state: failed to create dir", "path", ps.storePath, "error", err)
|
|
return
|
|
}
|
|
if err := AtomicWriteFile(ps.storePath, data, 0o644); err != nil {
|
|
slog.Error("project_state: failed to write", "path", ps.storePath, "error", err)
|
|
}
|
|
}
|
|
|
|
func (ps *ProjectStateStore) load() {
|
|
data, err := os.ReadFile(ps.storePath)
|
|
if err != nil {
|
|
if !os.IsNotExist(err) {
|
|
slog.Error("project_state: failed to read", "path", ps.storePath, "error", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
var state projectStateData
|
|
if err := json.Unmarshal(data, &state); err != nil {
|
|
slog.Error("project_state: failed to unmarshal", "path", ps.storePath, "error", err)
|
|
return
|
|
}
|
|
ps.state = state
|
|
}
|