backend: add unified restart-required signature logic

- Add bootConfigSignature to gateway state for tracking restart-sensitive config
- Implement computeConfigSignature() to capture model and tool enabled states
- Add gatewayRestartRequiredBySignature() for unified restart detection
- Update gatewayStatusData() to use signature-based comparison
- Preserve backward compatibility with existing boot_default_model field
This commit is contained in:
SiYue-ZO 2026-03-25 01:00:22 +08:00
parent 5f50ae5e76
commit 538415bca5

View file

@ -25,13 +25,14 @@ import (
// gateway holds the state for the managed gateway process. // gateway holds the state for the managed gateway process.
var gateway = struct { var gateway = struct {
mu sync.Mutex mu sync.Mutex
cmd *exec.Cmd cmd *exec.Cmd
owned bool // true if we started the process, false if we attached to an existing one owned bool // true if we started the process, false if we attached to an existing one
bootDefaultModel string bootDefaultModel string
runtimeStatus string bootConfigSignature string
startupDeadline time.Time runtimeStatus string
logs *LogBuffer startupDeadline time.Time
logs *LogBuffer
}{ }{
runtimeStatus: "stopped", runtimeStatus: "stopped",
logs: NewLogBuffer(200), logs: NewLogBuffer(200),
@ -187,6 +188,95 @@ func gatewayRestartRequired(configDefaultModel, bootDefaultModel, gatewayStatus
return configDefaultModel != bootDefaultModel return configDefaultModel != bootDefaultModel
} }
func computeConfigSignature(cfg *config.Config) string {
if cfg == nil {
return ""
}
var parts []string
defaultModel := strings.TrimSpace(cfg.Agents.Defaults.GetModelName())
if defaultModel != "" {
parts = append(parts, "model:"+defaultModel)
}
toolSignatures := []string{}
if cfg.Tools.ReadFile.Enabled {
toolSignatures = append(toolSignatures, "read_file")
}
if cfg.Tools.WriteFile.Enabled {
toolSignatures = append(toolSignatures, "write_file")
}
if cfg.Tools.ListDir.Enabled {
toolSignatures = append(toolSignatures, "list_dir")
}
if cfg.Tools.EditFile.Enabled {
toolSignatures = append(toolSignatures, "edit_file")
}
if cfg.Tools.AppendFile.Enabled {
toolSignatures = append(toolSignatures, "append_file")
}
if cfg.Tools.Exec.Enabled {
toolSignatures = append(toolSignatures, "exec")
}
if cfg.Tools.Cron.Enabled {
toolSignatures = append(toolSignatures, "cron")
}
if cfg.Tools.Web.Enabled {
toolSignatures = append(toolSignatures, "web")
}
if cfg.Tools.WebFetch.Enabled {
toolSignatures = append(toolSignatures, "web_fetch")
}
if cfg.Tools.Message.Enabled {
toolSignatures = append(toolSignatures, "message")
}
if cfg.Tools.SendFile.Enabled {
toolSignatures = append(toolSignatures, "send_file")
}
if cfg.Tools.FindSkills.Enabled {
toolSignatures = append(toolSignatures, "find_skills")
}
if cfg.Tools.InstallSkill.Enabled {
toolSignatures = append(toolSignatures, "install_skill")
}
if cfg.Tools.Spawn.Enabled {
toolSignatures = append(toolSignatures, "spawn")
}
if cfg.Tools.SpawnStatus.Enabled {
toolSignatures = append(toolSignatures, "spawn_status")
}
if cfg.Tools.I2C.Enabled {
toolSignatures = append(toolSignatures, "i2c")
}
if cfg.Tools.SPI.Enabled {
toolSignatures = append(toolSignatures, "spi")
}
if cfg.Tools.MCP.Enabled {
toolSignatures = append(toolSignatures, "mcp")
}
if cfg.Tools.MCP.Discovery.Enabled {
toolSignatures = append(toolSignatures, "mcp_discovery")
}
if cfg.Tools.MCP.Discovery.UseRegex {
toolSignatures = append(toolSignatures, "mcp_discovery_regex")
}
if cfg.Tools.MCP.Discovery.UseBM25 {
toolSignatures = append(toolSignatures, "mcp_discovery_bm25")
}
if len(toolSignatures) > 0 {
parts = append(parts, "tools:"+strings.Join(toolSignatures, ","))
}
return strings.Join(parts, ";")
}
func gatewayRestartRequiredBySignature(bootSignature, currentSignature, gatewayStatus string) bool {
if gatewayStatus != "running" {
return false
}
if bootSignature == "" || currentSignature == "" {
return false
}
return bootSignature != currentSignature
}
func isCmdProcessAliveLocked(cmd *exec.Cmd) bool { func isCmdProcessAliveLocked(cmd *exec.Cmd) bool {
if cmd == nil || cmd.Process == nil { if cmd == nil || cmd.Process == nil {
return false return false
@ -228,10 +318,11 @@ func attachToGatewayProcessLocked(pid int, cfg *config.Config) error {
gateway.owned = false // We didn't start this process gateway.owned = false // We didn't start this process
setGatewayRuntimeStatusLocked("running") setGatewayRuntimeStatusLocked("running")
// Update bootDefaultModel from config // Update bootDefaultModel and bootConfigSignature from config
if cfg != nil { if cfg != nil {
defaultModelName := strings.TrimSpace(cfg.Agents.Defaults.GetModelName()) defaultModelName := strings.TrimSpace(cfg.Agents.Defaults.GetModelName())
gateway.bootDefaultModel = defaultModelName gateway.bootDefaultModel = defaultModelName
gateway.bootConfigSignature = computeConfigSignature(cfg)
} }
logger.InfoC("gateway", fmt.Sprintf("Attached to gateway process (PID: %d)", pid)) logger.InfoC("gateway", fmt.Sprintf("Attached to gateway process (PID: %d)", pid))
@ -419,6 +510,7 @@ func (h *Handler) startGatewayLocked(initialStatus string, existingPid int) (int
gateway.cmd = cmd gateway.cmd = cmd
gateway.owned = true // We started this process gateway.owned = true // We started this process
gateway.bootDefaultModel = defaultModelName gateway.bootDefaultModel = defaultModelName
gateway.bootConfigSignature = computeConfigSignature(cfg)
setGatewayRuntimeStatusLocked(initialStatus) setGatewayRuntimeStatusLocked(initialStatus)
pid = cmd.Process.Pid pid = cmd.Process.Pid
logger.InfoC("gateway", fmt.Sprintf("Started picoclaw gateway (PID: %d) from %s", pid, execPath)) logger.InfoC("gateway", fmt.Sprintf("Started picoclaw gateway (PID: %d) from %s", pid, execPath))
@ -439,6 +531,7 @@ func (h *Handler) startGatewayLocked(initialStatus string, existingPid int) (int
if gateway.cmd == cmd { if gateway.cmd == cmd {
gateway.cmd = nil gateway.cmd = nil
gateway.bootDefaultModel = "" gateway.bootDefaultModel = ""
gateway.bootConfigSignature = ""
if gateway.runtimeStatus != "restarting" { if gateway.runtimeStatus != "restarting" {
setGatewayRuntimeStatusLocked("stopped") setGatewayRuntimeStatusLocked("stopped")
} }
@ -784,11 +877,14 @@ func (h *Handler) gatewayStatusData() map[string]any {
} }
} }
bootDefaultModel, _ := data["boot_default_model"].(string)
gatewayStatus, _ := data["gateway_status"].(string) gatewayStatus, _ := data["gateway_status"].(string)
data["gateway_restart_required"] = gatewayRestartRequired( currentConfigSignature := computeConfigSignature(cfg)
configDefaultModel, gateway.mu.Lock()
bootDefaultModel, bootConfigSignature := gateway.bootConfigSignature
gateway.mu.Unlock()
data["gateway_restart_required"] = gatewayRestartRequiredBySignature(
bootConfigSignature,
currentConfigSignature,
gatewayStatus, gatewayStatus,
) )