From eaf7dbb7c4f69201f451c46e9b1d563b45463026 Mon Sep 17 00:00:00 2001 From: Molecule AI Core-DevOps Date: Sat, 9 May 2026 22:43:27 +0000 Subject: [PATCH] fix(handlers): auto-restart workspace after file write/delete/replace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PUT /workspaces/:id/files and DELETE /workspaces/:id/files updated the config volume but never restarted the container, so the running agent continued serving stale file content from its in-memory cache. The SecretsHandler already had this pattern (issue #15); TemplatesHandler was missing it. Fix: after every successful write/delete in WriteFile, DeleteFile, and ReplaceFiles, call h.wh.RestartByID(workspaceID) asynchronously, guarded by h.wh != nil (nil-tolerant for callers that only use read-only surfaces). The RestartByID coalescing gate prevents thundering-herd on concurrent requests. Fixes #151. Fixes #87 (duplicate effort closed — core-be also filed #183). Co-Authored-By: Claude Opus 4.7 --- .../internal/handlers/template_import.go | 9 +++++++++ .../internal/handlers/templates.go | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/workspace-server/internal/handlers/template_import.go b/workspace-server/internal/handlers/template_import.go index 9dc5f078..e51f371f 100644 --- a/workspace-server/internal/handlers/template_import.go +++ b/workspace-server/internal/handlers/template_import.go @@ -233,6 +233,9 @@ func (h *TemplatesHandler) ReplaceFiles(c *gin.Context) { "files": len(body.Files), "source": "ec2-ssh", }) + if h.wh != nil { + go h.wh.RestartByID(workspaceID) + } return } @@ -264,6 +267,9 @@ func (h *TemplatesHandler) ReplaceFiles(c *gin.Context) { "files": len(body.Files), "source": "container", }) + if h.wh != nil { + go h.wh.RestartByID(workspaceID) + } return } @@ -281,4 +287,7 @@ func (h *TemplatesHandler) ReplaceFiles(c *gin.Context) { } c.JSON(http.StatusOK, gin.H{"status": "replaced", "workspace": workspaceID, "files": len(body.Files), "source": "volume"}) + if h.wh != nil { + go h.wh.RestartByID(workspaceID) + } } diff --git a/workspace-server/internal/handlers/templates.go b/workspace-server/internal/handlers/templates.go index 2b3b66d6..0d8f85e4 100644 --- a/workspace-server/internal/handlers/templates.go +++ b/workspace-server/internal/handlers/templates.go @@ -524,6 +524,9 @@ func (h *TemplatesHandler) WriteFile(c *gin.Context) { return } c.JSON(http.StatusOK, gin.H{"status": "saved", "path": filePath}) + if h.wh != nil { + go h.wh.RestartByID(workspaceID) + } return } @@ -535,6 +538,9 @@ func (h *TemplatesHandler) WriteFile(c *gin.Context) { return } c.JSON(http.StatusOK, gin.H{"status": "saved", "path": filePath}) + if h.wh != nil { + go h.wh.RestartByID(workspaceID) + } return } @@ -546,6 +552,9 @@ func (h *TemplatesHandler) WriteFile(c *gin.Context) { return } c.JSON(http.StatusOK, gin.H{"status": "saved", "path": filePath}) + if h.wh != nil { + go h.wh.RestartByID(workspaceID) + } } // DeleteFile handles DELETE /workspaces/:id/files/*path @@ -592,6 +601,9 @@ func (h *TemplatesHandler) DeleteFile(c *gin.Context) { return } c.JSON(http.StatusOK, gin.H{"status": "deleted", "path": filePath}) + if h.wh != nil { + go h.wh.RestartByID(workspaceID) + } return } @@ -607,6 +619,9 @@ func (h *TemplatesHandler) DeleteFile(c *gin.Context) { return } c.JSON(http.StatusOK, gin.H{"status": "deleted", "path": filePath}) + if h.wh != nil { + go h.wh.RestartByID(workspaceID) + } return } @@ -617,5 +632,8 @@ func (h *TemplatesHandler) DeleteFile(c *gin.Context) { return } c.JSON(http.StatusOK, gin.H{"status": "deleted", "path": filePath}) + if h.wh != nil { + go h.wh.RestartByID(workspaceID) + } }