From 76690ff8d3cdf99736099c51047c6f60bbbe8049 Mon Sep 17 00:00:00 2001 From: decker Date: Tue, 24 Feb 2026 07:03:00 +0800 Subject: [PATCH 1/4] fix: implement atomic file writes in saveStoreUnsafe to prevent data loss on crash --- pkg/cron/service.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pkg/cron/service.go b/pkg/cron/service.go index e699a44b5..101971009 100644 --- a/pkg/cron/service.go +++ b/pkg/cron/service.go @@ -340,7 +340,15 @@ func (cs *CronService) saveStoreUnsafe() error { return err } - return os.WriteFile(cs.storePath, data, 0o600) + // Write atomically: write to a temp file then rename. + // os.WriteFile truncates the file before writing, so a crash between + // truncation and completion leaves an empty or partial file. + // os.Rename on the same filesystem is atomic on Linux. + tmpPath := cs.storePath + ".tmp" + if err := os.WriteFile(tmpPath, data, 0600); err != nil { + return err + } + return os.Rename(tmpPath, cs.storePath) } func (cs *CronService) AddJob( From d878802fcb9ebd8f8c577a80c2954ab5960f6e3e Mon Sep 17 00:00:00 2001 From: mantrahq502 Date: Wed, 25 Feb 2026 08:49:51 +0800 Subject: [PATCH 2/4] fix(telegram): implement custom HTTP transport with TCP keepalive to prevent connection drops --- pkg/channels/telegram.go | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/pkg/channels/telegram.go b/pkg/channels/telegram.go index 5cd51e8bc..1176f34aa 100644 --- a/pkg/channels/telegram.go +++ b/pkg/channels/telegram.go @@ -3,6 +3,7 @@ package channels import ( "context" "fmt" + "net" "net/http" "net/url" "os" @@ -48,25 +49,31 @@ func NewTelegramChannel(cfg *config.Config, bus *bus.MessageBus) (*TelegramChann var opts []telego.BotOption telegramCfg := cfg.Channels.Telegram + // Always use a custom transport with TCP keepalive to prevent "unexpected EOF" + // errors caused by NAT/firewall silently dropping idle long-poll connections. + transport := &http.Transport{ + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + } + if telegramCfg.Proxy != "" { proxyURL, parseErr := url.Parse(telegramCfg.Proxy) if parseErr != nil { return nil, fmt.Errorf("invalid proxy URL %q: %w", telegramCfg.Proxy, parseErr) } - opts = append(opts, telego.WithHTTPClient(&http.Client{ - Transport: &http.Transport{ - Proxy: http.ProxyURL(proxyURL), - }, - })) + transport.Proxy = http.ProxyURL(proxyURL) } else if os.Getenv("HTTP_PROXY") != "" || os.Getenv("HTTPS_PROXY") != "" { - // Use environment proxy if configured - opts = append(opts, telego.WithHTTPClient(&http.Client{ - Transport: &http.Transport{ - Proxy: http.ProxyFromEnvironment, - }, - })) + transport.Proxy = http.ProxyFromEnvironment } + opts = append(opts, telego.WithHTTPClient(&http.Client{ + Transport: transport, + })) + bot, err := telego.NewBot(telegramCfg.Token, opts...) if err != nil { return nil, fmt.Errorf("failed to create telegram bot: %w", err) From bec8e13cf7431dcc092b93857666e3d5b2b43026 Mon Sep 17 00:00:00 2001 From: decker Date: Tue, 24 Feb 2026 08:37:29 +0800 Subject: [PATCH 3/4] fix: update file permission syntax in saveStoreUnsafe for consistency --- pkg/cron/service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cron/service.go b/pkg/cron/service.go index 101971009..dc0e0ab0a 100644 --- a/pkg/cron/service.go +++ b/pkg/cron/service.go @@ -345,7 +345,7 @@ func (cs *CronService) saveStoreUnsafe() error { // truncation and completion leaves an empty or partial file. // os.Rename on the same filesystem is atomic on Linux. tmpPath := cs.storePath + ".tmp" - if err := os.WriteFile(tmpPath, data, 0600); err != nil { + if err := os.WriteFile(tmpPath, data, 0o600); err != nil { return err } return os.Rename(tmpPath, cs.storePath) From fae3cef6aa30808ae5f590ba862d6eb394056888 Mon Sep 17 00:00:00 2001 From: mantrahq502 Date: Wed, 25 Feb 2026 11:16:34 +0800 Subject: [PATCH 4/4] fix(cron): publish agent response to outbound bus after ProcessDirectWithChannel ProcessDirectWithChannel bypasses the AgentLoop bus, so the Run() loop's PublishOutbound never fires. The response was silently discarded with _ = response, causing cron-triggered agent replies to never reach the Telegram channel. Fix by explicitly calling PublishOutbound with the response when non-empty, consistent with the deliver=true and command execution paths. --- pkg/tools/cron.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pkg/tools/cron.go b/pkg/tools/cron.go index 562fffc84..65d93dd5c 100644 --- a/pkg/tools/cron.go +++ b/pkg/tools/cron.go @@ -327,7 +327,12 @@ func (t *CronTool) ExecuteJob(ctx context.Context, job *cron.CronJob) string { return fmt.Sprintf("Error: %v", err) } - // Response is automatically sent via MessageBus by AgentLoop - _ = response // Will be sent by AgentLoop + if response != "" { + t.msgBus.PublishOutbound(bus.OutboundMessage{ + Channel: channel, + ChatID: chatID, + Content: response, + }) + } return "ok" }