From 25ed043117a2d9ee349fd4e4929ecd2ca2302afb Mon Sep 17 00:00:00 2001 From: Jiayuan Date: Mon, 30 Mar 2026 18:08:58 +0800 Subject: [PATCH] fix(daemon): auto-discover new workspaces without restart The daemon now periodically fetches the user's workspace list from the API (every 30s) and adds any new workspaces to the watched config. The existing config-watch loop then picks up the change and registers runtimes. This fixes the issue where workspaces created after `multica login` were not discovered until the daemon was restarted. --- server/internal/daemon/client.go | 15 +++++++++ server/internal/daemon/config.go | 5 +-- server/internal/daemon/daemon.go | 57 ++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 2 deletions(-) diff --git a/server/internal/daemon/client.go b/server/internal/daemon/client.go index 878007d4..1c34afb2 100644 --- a/server/internal/daemon/client.go +++ b/server/internal/daemon/client.go @@ -146,6 +146,21 @@ func (c *Client) ReportPingResult(ctx context.Context, runtimeID, pingID string, return c.postJSON(ctx, fmt.Sprintf("/api/daemon/runtimes/%s/ping/%s/result", runtimeID, pingID), result, nil) } +// WorkspaceInfo holds minimal workspace metadata returned by the API. +type WorkspaceInfo struct { + ID string `json:"id"` + Name string `json:"name"` +} + +// ListWorkspaces fetches all workspaces the authenticated user belongs to. +func (c *Client) ListWorkspaces(ctx context.Context) ([]WorkspaceInfo, error) { + var workspaces []WorkspaceInfo + if err := c.getJSON(ctx, "/api/workspaces", &workspaces); err != nil { + return nil, err + } + return workspaces, nil +} + func (c *Client) Deregister(ctx context.Context, runtimeIDs []string) error { return c.postJSON(ctx, "/api/daemon/deregister", map[string]any{ "runtime_ids": runtimeIDs, diff --git a/server/internal/daemon/config.go b/server/internal/daemon/config.go index f9e942ad..756560da 100644 --- a/server/internal/daemon/config.go +++ b/server/internal/daemon/config.go @@ -16,8 +16,9 @@ const ( DefaultHeartbeatInterval = 15 * time.Second DefaultAgentTimeout = 2 * time.Hour DefaultRuntimeName = "Local Agent" - DefaultConfigReloadInterval = 5 * time.Second - DefaultHealthPort = 19514 + DefaultConfigReloadInterval = 5 * time.Second + DefaultWorkspaceSyncInterval = 30 * time.Second + DefaultHealthPort = 19514 DefaultMaxConcurrentTasks = 20 ) diff --git a/server/internal/daemon/daemon.go b/server/internal/daemon/daemon.go index a25eea95..c4431d14 100644 --- a/server/internal/daemon/daemon.go +++ b/server/internal/daemon/daemon.go @@ -85,6 +85,9 @@ func (d *Daemon) Run(ctx context.Context) error { // Start config watcher for hot-reload. go d.configWatchLoop(ctx) + // Start workspace sync loop to discover newly created workspaces. + go d.workspaceSyncLoop(ctx) + go d.heartbeatLoop(ctx) go d.usageScanLoop(ctx) go d.serveHealth(ctx, healthLn, time.Now()) @@ -276,6 +279,60 @@ func (d *Daemon) configWatchLoop(ctx context.Context) { } } +// workspaceSyncLoop periodically fetches the user's workspaces from the API +// and adds any new ones to the CLI config. The configWatchLoop will then +// detect the config change and register runtimes for the new workspaces. +func (d *Daemon) workspaceSyncLoop(ctx context.Context) { + ticker := time.NewTicker(DefaultWorkspaceSyncInterval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + d.syncWorkspacesFromAPI(ctx) + } + } +} + +// syncWorkspacesFromAPI fetches all workspaces the user belongs to and adds +// any missing ones to the CLI config's watched list. +func (d *Daemon) syncWorkspacesFromAPI(ctx context.Context) { + apiCtx, cancel := context.WithTimeout(ctx, 15*time.Second) + defer cancel() + + workspaces, err := d.client.ListWorkspaces(apiCtx) + if err != nil { + d.logger.Debug("workspace sync: failed to list workspaces", "error", err) + return + } + + cfg, err := cli.LoadCLIConfig() + if err != nil { + d.logger.Warn("workspace sync: failed to load config", "error", err) + return + } + + var added int + for _, ws := range workspaces { + if cfg.AddWatchedWorkspace(ws.ID, ws.Name) { + added++ + d.logger.Info("workspace sync: discovered new workspace", "workspace_id", ws.ID, "name", ws.Name) + } + } + + if added == 0 { + return + } + + if err := cli.SaveCLIConfig(cfg); err != nil { + d.logger.Warn("workspace sync: failed to save config", "error", err) + return + } + d.logger.Info("workspace sync: added new workspace(s) to config", "count", added) +} + // reloadWorkspaces reconciles the active workspace set with the config file. // NOTE: Token changes (e.g. re-login as a different user) are not picked up; // the daemon must be restarted for a new auth token to take effect.