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.
This commit is contained in:
Jiayuan 2026-03-30 18:08:58 +08:00
parent 457a3eb555
commit 25ed043117
3 changed files with 75 additions and 2 deletions

View file

@ -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,

View file

@ -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
)

View file

@ -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.