feat(cron): add configurable execution timeout for cron jobs

Add a new configuration option `exec_timeout_minutes` under the `tools.cron`
section to control the maximum execution time for cron jobs. The default
timeout is set to 5 minutes, which is appropriate for LLM operations.

The configuration can be set in the config file or via the
`PICOCLAW_TOOLS_CRON_EXEC_TIMEOUT_MINUTES` environment variable. A value of
0 disables the timeout entirely.

This change improves system reliability by preventing cron jobs from running
indefinitely in case of unexpected failures or hanging processes.
This commit is contained in:
yinwm 2026-02-15 18:41:39 +08:00
parent fb7455f661
commit a7f7b5489b
7 changed files with 36 additions and 6 deletions

View file

@ -195,6 +195,9 @@ picoclaw onboard
"api_key": "YOUR_BRAVE_API_KEY", "api_key": "YOUR_BRAVE_API_KEY",
"max_results": 5 "max_results": 5
} }
},
"cron": {
"exec_timeout_minutes": 5
} }
}, },
"heartbeat": { "heartbeat": {
@ -646,6 +649,9 @@ HEARTBEAT_OK 応答 ユーザーが直接結果を受け取る
"search": { "search": {
"apiKey": "BSA..." "apiKey": "BSA..."
} }
},
"cron": {
"exec_timeout_minutes": 5
} }
}, },
"heartbeat": { "heartbeat": {

View file

@ -697,6 +697,9 @@ picoclaw agent -m "Hello"
"search": { "search": {
"api_key": "BSA..." "api_key": "BSA..."
} }
},
"cron": {
"exec_timeout_minutes": 5
} }
}, },
"heartbeat": { "heartbeat": {

View file

@ -217,6 +217,9 @@ picoclaw onboard
"api_key": "YOUR_BRAVE_API_KEY", "api_key": "YOUR_BRAVE_API_KEY",
"max_results": 5 "max_results": 5
} }
},
"cron": {
"exec_timeout_minutes": 5
} }
} }
} }
@ -625,6 +628,9 @@ picoclaw agent -m "你好"
"search": { "search": {
"api_key": "BSA..." "api_key": "BSA..."
} }
},
"cron": {
"exec_timeout_minutes": 5
} }
}, },
"heartbeat": { "heartbeat": {

View file

@ -669,7 +669,7 @@ func gatewayCmd() {
}) })
// Setup cron tool and service // Setup cron tool and service
cronService := setupCronTool(agentLoop, msgBus, cfg.WorkspacePath()) cronService := setupCronTool(agentLoop, msgBus, cfg.WorkspacePath(), time.Duration(cfg.Tools.Cron.ExecTimeoutMinutes)*time.Minute)
heartbeatService := heartbeat.NewHeartbeatService( heartbeatService := heartbeat.NewHeartbeatService(
cfg.WorkspacePath(), cfg.WorkspacePath(),
@ -1069,14 +1069,14 @@ func getConfigPath() string {
return filepath.Join(home, ".picoclaw", "config.json") return filepath.Join(home, ".picoclaw", "config.json")
} }
func setupCronTool(agentLoop *agent.AgentLoop, msgBus *bus.MessageBus, workspace string) *cron.CronService { func setupCronTool(agentLoop *agent.AgentLoop, msgBus *bus.MessageBus, workspace string, execTimeout time.Duration) *cron.CronService {
cronStorePath := filepath.Join(workspace, "cron", "jobs.json") cronStorePath := filepath.Join(workspace, "cron", "jobs.json")
// Create cron service // Create cron service
cronService := cron.NewCronService(cronStorePath, nil) cronService := cron.NewCronService(cronStorePath, nil)
// Create and register CronTool // Create and register CronTool
cronTool := tools.NewCronTool(cronService, agentLoop, msgBus, workspace) cronTool := tools.NewCronTool(cronService, agentLoop, msgBus, workspace, execTimeout)
agentLoop.RegisterTool(cronTool) agentLoop.RegisterTool(cronTool)
// Set the onJob handler // Set the onJob handler

View file

@ -98,6 +98,9 @@
"api_key": "YOUR_BRAVE_API_KEY", "api_key": "YOUR_BRAVE_API_KEY",
"max_results": 5 "max_results": 5
} }
},
"cron": {
"exec_timeout_minutes": 5
} }
}, },
"heartbeat": { "heartbeat": {

View file

@ -173,8 +173,13 @@ type WebToolsConfig struct {
Search WebSearchConfig `json:"search"` Search WebSearchConfig `json:"search"`
} }
type CronToolsConfig struct {
ExecTimeoutMinutes int `json:"exec_timeout_minutes" env:"PICOCLAW_TOOLS_CRON_EXEC_TIMEOUT_MINUTES"` // 0 means no timeout
}
type ToolsConfig struct { type ToolsConfig struct {
Web WebToolsConfig `json:"web"` Web WebToolsConfig `json:"web"`
Cron CronToolsConfig `json:"cron"`
} }
func DefaultConfig() *Config { func DefaultConfig() *Config {
@ -262,6 +267,9 @@ func DefaultConfig() *Config {
MaxResults: 5, MaxResults: 5,
}, },
}, },
Cron: CronToolsConfig{
ExecTimeoutMinutes: 5, // default 5 minutes for LLM operations
},
}, },
Heartbeat: HeartbeatConfig{ Heartbeat: HeartbeatConfig{
Enabled: true, Enabled: true,

View file

@ -28,12 +28,16 @@ type CronTool struct {
} }
// NewCronTool creates a new CronTool // NewCronTool creates a new CronTool
func NewCronTool(cronService *cron.CronService, executor JobExecutor, msgBus *bus.MessageBus, workspace string) *CronTool { func NewCronTool(cronService *cron.CronService, executor JobExecutor, msgBus *bus.MessageBus, workspace string, execTimeout time.Duration) *CronTool {
execTool := NewExecTool(workspace, false)
if execTimeout > 0 {
execTool.SetTimeout(execTimeout)
}
return &CronTool{ return &CronTool{
cronService: cronService, cronService: cronService,
executor: executor, executor: executor,
msgBus: msgBus, msgBus: msgBus,
execTool: NewExecTool(workspace, false), execTool: execTool,
} }
} }