mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-03 19:20:07 +02:00
fix(daemon/execenv): refuse to write .gc_meta.json when issue_id is empty (#2077)
A non-trivial fraction of completed task workdirs (~28% in field reports) end up with .gc_meta.json files containing issue_id: "". Empty issue_id defeats the daemon's own GC loop (gc.go:139 calls GetIssueGCCheck(meta.IssueID)) and external retention scripts that cross-reference issue status before deleting orphaned workdirs. Refuse to write the file when issueID is empty, logging a Warn so operators have a starting point for debugging the upstream race condition. Skip is preferred over a sentinel-marker file: it keeps the data invariant clean (a .gc_meta.json file always carries a valid issue_id) and matches the repo CLAUDE.md preference for not preserving dual-state behavior. WriteGCMeta now takes a *slog.Logger so it can emit the warning. The package already uses log/slog (Prepare/reuseEnv), and daemon.go:884 has taskLog in scope at the only call site. Closes #1913 Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
This commit is contained in:
@@ -1461,7 +1461,7 @@ func (d *Daemon) handleTask(ctx context.Context, task Task, slot int) {
|
||||
// can look up the issue later. Written last so that a mid-task crash
|
||||
// leaves the directory as an orphan (cleaned up by GCOrphanTTL).
|
||||
if result.EnvRoot != "" {
|
||||
if err := execenv.WriteGCMeta(result.EnvRoot, task.IssueID, task.WorkspaceID); err != nil {
|
||||
if err := execenv.WriteGCMeta(result.EnvRoot, task.IssueID, task.WorkspaceID, taskLog); err != nil {
|
||||
taskLog.Warn("write gc meta failed (non-fatal)", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ type TaskContextForEnv struct {
|
||||
ProjectTitle string // human-readable project title
|
||||
ProjectResources []ProjectResourceForEnv // resources attached to the project
|
||||
ChatSessionID string // non-empty for chat tasks
|
||||
AutopilotRunID string // non-empty for autopilot run_only tasks
|
||||
AutopilotRunID string // non-empty for autopilot run_only tasks
|
||||
AutopilotID string
|
||||
AutopilotTitle string
|
||||
AutopilotDescription string
|
||||
@@ -214,7 +214,11 @@ type GCMeta struct {
|
||||
const gcMetaFile = ".gc_meta.json"
|
||||
|
||||
// WriteGCMeta writes GC metadata into the given directory.
|
||||
func WriteGCMeta(envRoot, issueID, workspaceID string) error {
|
||||
func WriteGCMeta(envRoot, issueID, workspaceID string, logger *slog.Logger) error {
|
||||
if issueID == "" {
|
||||
logger.Warn("execenv: skipping .gc_meta.json write: issue_id is empty", "envRoot", envRoot, "workspaceID", workspaceID)
|
||||
return nil
|
||||
}
|
||||
if envRoot == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package execenv
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -14,6 +15,10 @@ func testLogger() *slog.Logger {
|
||||
return slog.Default()
|
||||
}
|
||||
|
||||
func discardLogger() *slog.Logger {
|
||||
return slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||
}
|
||||
|
||||
func TestShortID(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
@@ -1916,7 +1921,7 @@ func TestWriteReadGCMeta(t *testing.T) {
|
||||
issueID := "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
wsID := "ws-test-001"
|
||||
|
||||
if err := WriteGCMeta(dir, issueID, wsID); err != nil {
|
||||
if err := WriteGCMeta(dir, issueID, wsID, discardLogger()); err != nil {
|
||||
t.Fatalf("WriteGCMeta: %v", err)
|
||||
}
|
||||
|
||||
@@ -1938,11 +1943,23 @@ func TestWriteReadGCMeta(t *testing.T) {
|
||||
|
||||
func TestWriteGCMeta_EmptyRoot(t *testing.T) {
|
||||
t.Parallel()
|
||||
if err := WriteGCMeta("", "issue", "ws"); err != nil {
|
||||
if err := WriteGCMeta("", "issue", "ws", discardLogger()); err != nil {
|
||||
t.Fatalf("expected nil for empty root, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteGCMeta_EmptyIssueID(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := t.TempDir()
|
||||
|
||||
if err := WriteGCMeta(dir, "", "ws", discardLogger()); err != nil {
|
||||
t.Fatalf("expected nil for empty issue ID, got %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, gcMetaFile)); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected gc meta file to be absent, got err=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadGCMeta_NoFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := t.TempDir()
|
||||
|
||||
Reference in New Issue
Block a user