From e4a905c8411e81190675e3cd9e9b3c6c8318115e Mon Sep 17 00:00:00 2001 From: yushen Date: Thu, 26 Mar 2026 17:22:12 +0800 Subject: [PATCH] fix(daemon): improve error handling in auth and workspace loading Co-Authored-By: Claude Opus 4.6 (1M context) --- server/internal/daemon/daemon.go | 12 +++++++++++- server/internal/daemon/execenv/execenv_test.go | 8 ++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/server/internal/daemon/daemon.go b/server/internal/daemon/daemon.go index f6b51b1f..e770a6e3 100644 --- a/server/internal/daemon/daemon.go +++ b/server/internal/daemon/daemon.go @@ -73,7 +73,10 @@ func (d *Daemon) Run(ctx context.Context) error { // resolveAuth loads the auth token from the CLI config. func (d *Daemon) resolveAuth() error { - cfg, _ := cli.LoadCLIConfig() + cfg, err := cli.LoadCLIConfig() + if err != nil { + return fmt.Errorf("load CLI config: %w", err) + } if cfg.Token == "" { d.logger.Warn("not authenticated — run 'multica auth login' to authenticate, then restart the daemon") return fmt.Errorf("not authenticated: run 'multica auth login' first") @@ -94,6 +97,7 @@ func (d *Daemon) loadWatchedWorkspaces(ctx context.Context) error { return fmt.Errorf("no watched workspaces configured: run 'multica watch ' to add one") } + var registered int for _, ws := range cfg.WatchedWorkspaces { runtimes, err := d.registerRuntimesForWorkspace(ctx, ws.ID) if err != nil { @@ -109,8 +113,12 @@ func (d *Daemon) loadWatchedWorkspaces(ctx context.Context) error { d.workspaces[ws.ID] = &workspaceState{workspaceID: ws.ID, runtimeIDs: runtimeIDs} d.mu.Unlock() d.logger.Info("watching workspace", "workspace_id", ws.ID, "name", ws.Name, "runtimes", len(runtimes)) + registered++ } + if registered == 0 { + return fmt.Errorf("failed to register runtimes for any of the %d watched workspace(s)", len(cfg.WatchedWorkspaces)) + } return nil } @@ -238,6 +246,8 @@ func (d *Daemon) reloadWorkspaces(ctx context.Context) { } // Remove workspaces no longer in config. + // NOTE: runtimes are not deregistered server-side; they will go offline + // after heartbeats stop arriving (within HeartbeatInterval). for id := range currentIDs { if _, ok := newIDs[id]; !ok { d.mu.Lock() diff --git a/server/internal/daemon/execenv/execenv_test.go b/server/internal/daemon/execenv/execenv_test.go index 1526c53d..9568bd5d 100644 --- a/server/internal/daemon/execenv/execenv_test.go +++ b/server/internal/daemon/execenv/execenv_test.go @@ -96,7 +96,7 @@ func TestPrepareDirectoryMode(t *testing.T) { env, err := Prepare(PrepareParams{ WorkspacesRoot: workspacesRoot, - RepoPath: reposRoot, + RepoPath: reposRoot, TaskID: "a1b2c3d4-e5f6-7890-abcd-ef1234567890", AgentName: "Test Agent", Task: TaskContextForEnv{ @@ -176,7 +176,7 @@ func TestPrepareGitWorktreeMode(t *testing.T) { env, err := Prepare(PrepareParams{ WorkspacesRoot: workspacesRoot, - RepoPath: reposRoot, + RepoPath: reposRoot, TaskID: "b2c3d4e5-f6a7-8901-bcde-f12345678901", AgentName: "Code Reviewer", Task: TaskContextForEnv{ @@ -334,7 +334,7 @@ func TestCleanupGitWorktree(t *testing.T) { env, err := Prepare(PrepareParams{ WorkspacesRoot: workspacesRoot, - RepoPath: reposRoot, + RepoPath: reposRoot, TaskID: "c3d4e5f6-a7b8-9012-cdef-123456789012", AgentName: "Cleanup Test", Task: TaskContextForEnv{IssueTitle: "Cleanup test"}, @@ -477,7 +477,7 @@ func TestCleanupPreservesLogs(t *testing.T) { env, err := Prepare(PrepareParams{ WorkspacesRoot: workspacesRoot, - RepoPath: t.TempDir(), // not a git repo + RepoPath: t.TempDir(), // not a git repo TaskID: "d4e5f6a7-b8c9-0123-defa-234567890123", AgentName: "Preserve Test", Task: TaskContextForEnv{IssueTitle: "Preserve test"},