multica/server/cmd/multica/cmd_daemon.go
yushen 4210fe69f4 feat(daemon): multi-workspace support with hot-reload
- Add `multica workspace watch/unwatch/list` CLI commands
- Daemon watches multiple workspaces from config's `watched_workspaces`
- Registers runtimes per workspace, polls all runtime IDs in round-robin
- Hot-reload: daemon detects config file changes every 5s and
  adds/removes workspaces without restart
- Remove `--workspace-id` flag from daemon (workspace selection is now
  purely config-driven via `multica workspace watch`)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:52:22 +08:00

70 lines
2.1 KiB
Go

package main
import (
"context"
"errors"
"os/signal"
"syscall"
"github.com/spf13/cobra"
"github.com/multica-ai/multica/server/internal/cli"
"github.com/multica-ai/multica/server/internal/daemon"
logger_pkg "github.com/multica-ai/multica/server/internal/logger"
)
var daemonCmd = &cobra.Command{
Use: "daemon",
Short: "Run the local agent runtime daemon",
Long: "Start the daemon process that polls for tasks and executes them using local agent CLIs (Claude, Codex).",
RunE: runDaemon,
}
func init() {
f := daemonCmd.Flags()
f.String("daemon-id", "", "Unique daemon identifier (env: MULTICA_DAEMON_ID)")
f.String("device-name", "", "Human-readable device name (env: MULTICA_DAEMON_DEVICE_NAME)")
f.String("runtime-name", "", "Runtime display name (env: MULTICA_AGENT_RUNTIME_NAME)")
f.Duration("poll-interval", 0, "Task poll interval (env: MULTICA_DAEMON_POLL_INTERVAL)")
f.Duration("heartbeat-interval", 0, "Heartbeat interval (env: MULTICA_DAEMON_HEARTBEAT_INTERVAL)")
f.Duration("agent-timeout", 0, "Per-task timeout (env: MULTICA_AGENT_TIMEOUT)")
}
func runDaemon(cmd *cobra.Command, _ []string) error {
overrides := daemon.Overrides{
ServerURL: cli.FlagOrEnv(cmd, "server-url", "MULTICA_SERVER_URL", ""),
DaemonID: flagString(cmd, "daemon-id"),
DeviceName: flagString(cmd, "device-name"),
RuntimeName: flagString(cmd, "runtime-name"),
}
if d, _ := cmd.Flags().GetDuration("poll-interval"); d > 0 {
overrides.PollInterval = d
}
if d, _ := cmd.Flags().GetDuration("heartbeat-interval"); d > 0 {
overrides.HeartbeatInterval = d
}
if d, _ := cmd.Flags().GetDuration("agent-timeout"); d > 0 {
overrides.AgentTimeout = d
}
cfg, err := daemon.LoadConfig(overrides)
if err != nil {
return err
}
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
logger := logger_pkg.NewLogger("daemon")
d := daemon.New(cfg, logger)
if err := d.Run(ctx); err != nil && !errors.Is(err, context.Canceled) {
return err
}
return nil
}
func flagString(cmd *cobra.Command, name string) string {
val, _ := cmd.Flags().GetString(name)
return val
}