Merge pull request #194 from multica-ai/fix/daemon-auto-discover-new-workspaces

fix(daemon): auto-discover new workspaces without restart
This commit is contained in:
Jiayuan Zhang 2026-03-30 19:34:04 +08:00 committed by GitHub
commit 9c3ff52363
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 78 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,63 @@ 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) {
// Run immediately on startup before entering the periodic loop.
d.syncWorkspacesFromAPI(ctx)
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.