diff --git a/docker-compose.managed.yml b/docker-compose.managed.yml index dfab93ea..7a03d7c9 100644 --- a/docker-compose.managed.yml +++ b/docker-compose.managed.yml @@ -32,6 +32,7 @@ services: - GOCLAW_POSTGRES_DSN=postgres://${POSTGRES_USER:-goclaw}:${POSTGRES_PASSWORD:-goclaw}@postgres:5432/${POSTGRES_DB:-goclaw}?sslmode=disable volumes: - goclaw-skills:/app/skills + - goclaw-workspace:/app/.goclaw volumes: postgres-data: diff --git a/docker-compose.yml b/docker-compose.yml index a8510397..f24a7ad2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -23,6 +23,7 @@ services: - GOCLAW_CONFIG=/app/data/config.json volumes: - goclaw-data:/app/data + - goclaw-workspace:/app/workspace # standalone mode workspace security_opt: - no-new-privileges:true cap_drop: @@ -40,3 +41,4 @@ services: volumes: goclaw-data: + goclaw-workspace: diff --git a/internal/agent/loop.go b/internal/agent/loop.go index 8af1027e..74632193 100644 --- a/internal/agent/loop.go +++ b/internal/agent/loop.go @@ -341,7 +341,9 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error) effectiveWorkspace := l.workspace if req.UserID != "" { effectiveWorkspace = filepath.Join(l.workspace, sanitizePathSegment(req.UserID)) - os.MkdirAll(effectiveWorkspace, 0755) + if err := os.MkdirAll(effectiveWorkspace, 0755); err != nil { + slog.Warn("failed to create user workspace directory", "workspace", effectiveWorkspace, "user", req.UserID, "error", err) + } } ctx = tools.WithToolWorkspace(ctx, effectiveWorkspace) } diff --git a/internal/agent/resolver.go b/internal/agent/resolver.go index 95fd901c..f726b17f 100644 --- a/internal/agent/resolver.go +++ b/internal/agent/resolver.go @@ -122,7 +122,9 @@ func NewManagedResolver(deps ResolverDeps) ResolverFunc { if !filepath.IsAbs(workspace) { workspace, _ = filepath.Abs(workspace) } - os.MkdirAll(workspace, 0755) + if err := os.MkdirAll(workspace, 0755); err != nil { + slog.Warn("failed to create agent workspace directory", "workspace", workspace, "agent", agentKey, "error", err) + } } // Per-agent custom tools (clone registry if agent has custom tools) diff --git a/internal/tools/context_file_interceptor.go b/internal/tools/context_file_interceptor.go index 99e57b86..1515806d 100644 --- a/internal/tools/context_file_interceptor.go +++ b/internal/tools/context_file_interceptor.go @@ -38,6 +38,9 @@ var contextFileSet = map[string]bool{ // isContextFile checks if a path refers to a workspace-root context file. // Handles both relative ("SOUL.md") and absolute ("/workspace/SOUL.md") paths. +// Also matches absolute paths under per-user workspace subdirectories +// (e.g. "/workspace//USER.md") since context files at any depth +// under the workspace root should be routed to DB in managed mode. func isContextFile(path, workspace string) (fileName string, ok bool) { base := filepath.Base(path) if !contextFileSet[base] { @@ -50,11 +53,12 @@ func isContextFile(path, workspace string) (fileName string, ok bool) { return base, true } - // Absolute path at workspace root: "/path/to/workspace/SOUL.md" + // Absolute path under workspace (includes per-user subdirectories): + // "/workspace/SOUL.md" or "/workspace//SOUL.md" if workspace != "" && filepath.IsAbs(path) { cleanPath := filepath.Clean(path) cleanWS := filepath.Clean(workspace) - if filepath.Dir(cleanPath) == cleanWS { + if strings.HasPrefix(filepath.Dir(cleanPath), cleanWS) { return base, true } } diff --git a/ui/web/src/pages/traces/trace-detail-dialog.tsx b/ui/web/src/pages/traces/trace-detail-dialog.tsx index 88780885..d86af5ab 100644 --- a/ui/web/src/pages/traces/trace-detail-dialog.tsx +++ b/ui/web/src/pages/traces/trace-detail-dialog.tsx @@ -124,8 +124,8 @@ export function TraceDetailDialog({ traceId, onClose, getTrace }: TraceDetailDia )} {trace.error && ( -
-

{trace.error}

+
+

{trace.error}

)} @@ -232,7 +232,7 @@ function SpanTreeNode({ node, depth }: { node: SpanNode; depth: number }) {
)} {span.error && ( -

{span.error}

+

{span.error}

)} )}