From 5893245b45e33025635ffad207997226639b6bb2 Mon Sep 17 00:00:00 2001 From: Milan Lesichkov Date: Thu, 12 Feb 2026 18:33:39 +0000 Subject: [PATCH 1/7] Update launch announcement in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3819982..11bb8cd 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ ## 📢 News -2026-02-09 🎉 PicoClaw Launched! Built in 1 day to bring AI Agents to $10 hardware with <10MB RAM. 🦐 皮皮虾,我们走! +2026-02-09 🎉 PicoClaw Launched! Built in 1 day to bring AI Agents to $10 hardware with <10MB RAM. 🦐 PicoClaw,Let's Go! ## ✨ Features From 132fe7db516ecbdc3af89881519e9be8a3599603 Mon Sep 17 00:00:00 2001 From: Zhaoyikaiii Date: Fri, 13 Feb 2026 14:41:21 +0800 Subject: [PATCH 2/7] bugfix: fix duplicate Telegram message sending Two issues caused duplicate messages to be sent to users: 1. System messages (from subagent): processSystemMessage set SendResponse:true, causing runAgentLoop to publish outbound. Then Run() also published outbound using the returned response string, resulting in two identical messages. Fix: processSystemMessage now returns empty string since runAgentLoop already handles the send. 2. Message tool double-send: When LLM called the "message" tool during processing, it published outbound immediately. Then Run() published the final response again. Fix: Track whether MessageTool sent a message in the current round (sentInRound flag, reset on each SetContext call). Run() checks HasSentInRound() before publishing to avoid duplicates. --- pkg/agent/loop.go | 29 ++++++++++++++++++++++------- pkg/tools/message.go | 8 ++++++++ 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index fac2856..dad920d 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -128,11 +128,22 @@ func (al *AgentLoop) Run(ctx context.Context) error { } if response != "" { - al.bus.PublishOutbound(bus.OutboundMessage{ - Channel: msg.Channel, - ChatID: msg.ChatID, - Content: response, - }) + // Check if the message tool already sent a response during this round. + // If so, skip publishing to avoid duplicate messages to the user. + alreadySent := false + if tool, ok := al.tools.Get("message"); ok { + if mt, ok := tool.(*tools.MessageTool); ok { + alreadySent = mt.HasSentInRound() + } + } + + if !alreadySent { + al.bus.PublishOutbound(bus.OutboundMessage{ + Channel: msg.Channel, + ChatID: msg.ChatID, + Content: response, + }) + } } } } @@ -218,8 +229,10 @@ func (al *AgentLoop) processSystemMessage(ctx context.Context, msg bus.InboundMe // Use the origin session for context sessionKey := fmt.Sprintf("%s:%s", originChannel, originChatID) - // Process as system message with routing back to origin - return al.runAgentLoop(ctx, processOptions{ + // Process as system message with routing back to origin. + // SendResponse: true means runAgentLoop will publish the outbound message itself, + // so we return empty string to prevent Run() from publishing a duplicate. + _, err := al.runAgentLoop(ctx, processOptions{ SessionKey: sessionKey, Channel: originChannel, ChatID: originChatID, @@ -228,6 +241,8 @@ func (al *AgentLoop) processSystemMessage(ctx context.Context, msg bus.InboundMe EnableSummary: false, SendResponse: true, // Send response back to original channel }) + // Return empty string: response was already sent via bus in runAgentLoop + return "", err } // runAgentLoop is the core message processing logic. diff --git a/pkg/tools/message.go b/pkg/tools/message.go index e090234..7cb8ff7 100644 --- a/pkg/tools/message.go +++ b/pkg/tools/message.go @@ -11,6 +11,7 @@ type MessageTool struct { sendCallback SendCallback defaultChannel string defaultChatID string + sentInRound bool // Tracks whether a message was sent in the current processing round } func NewMessageTool() *MessageTool { @@ -49,6 +50,12 @@ func (t *MessageTool) Parameters() map[string]interface{} { func (t *MessageTool) SetContext(channel, chatID string) { t.defaultChannel = channel t.defaultChatID = chatID + t.sentInRound = false // Reset send tracking for new processing round +} + +// HasSentInRound returns true if the message tool sent a message during the current round. +func (t *MessageTool) HasSentInRound() bool { + return t.sentInRound } func (t *MessageTool) SetSendCallback(callback SendCallback) { @@ -83,5 +90,6 @@ func (t *MessageTool) Execute(ctx context.Context, args map[string]interface{}) return fmt.Sprintf("Error sending message: %v", err), nil } + t.sentInRound = true return fmt.Sprintf("Message sent to %s:%s", channel, chatID), nil } From 896eae4c5604615ed9189bbe0fff598cf4c25480 Mon Sep 17 00:00:00 2001 From: coohu <1257817341@qq.com> Date: Fri, 13 Feb 2026 15:55:59 +0800 Subject: [PATCH 3/7] =?UTF-8?q?feat:=20add=20ShengSuanYun(=E8=83=9C?= =?UTF-8?q?=E7=AE=97=E4=BA=91)=20as=20a=20models=20provider.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/config/config.go | 5 +++++ pkg/providers/http_provider.go | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/pkg/config/config.go b/pkg/config/config.go index 56f1e19..bacf5c5 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -143,6 +143,7 @@ type ProvidersConfig struct { Gemini ProviderConfig `json:"gemini"` Nvidia ProviderConfig `json:"nvidia"` Moonshot ProviderConfig `json:"moonshot"` + ShengSuanYun ProviderConfig `json:"shengsuanyun"` } type ProviderConfig struct { @@ -242,6 +243,7 @@ func DefaultConfig() *Config { Gemini: ProviderConfig{}, Nvidia: ProviderConfig{}, Moonshot: ProviderConfig{}, + ShengSuanYun: ProviderConfig{}, }, Gateway: GatewayConfig{ Host: "0.0.0.0", @@ -327,6 +329,9 @@ func (c *Config) GetAPIKey() string { if c.Providers.VLLM.APIKey != "" { return c.Providers.VLLM.APIKey } + if c.Providers.ShengSuanYun.APIKey != "" { + return c.Providers.ShengSuanYun.APIKey + } return "" } diff --git a/pkg/providers/http_provider.go b/pkg/providers/http_provider.go index 7179c4c..b1d0c03 100644 --- a/pkg/providers/http_provider.go +++ b/pkg/providers/http_provider.go @@ -289,6 +289,14 @@ func CreateProvider(cfg *config.Config) (LLMProvider, error) { apiKey = cfg.Providers.VLLM.APIKey apiBase = cfg.Providers.VLLM.APIBase } + case "shengsuanyun": + if cfg.Providers.ShengSuanYun.APIKey != "" { + apiKey = cfg.Providers.ShengSuanYun.APIKey + apiBase = cfg.Providers.ShengSuanYun.APIBase + if apiBase == "" { + apiBase = "https://router.shengsuanyun.com/api/v1" + } + } case "claude-cli", "claudecode", "claude-code": workspace := cfg.Agents.Defaults.Workspace if workspace == "" { From 584a3dcc87acf06444cc6b5495c52f74604ec56f Mon Sep 17 00:00:00 2001 From: Meng Zhuo Date: Fri, 13 Feb 2026 17:51:47 +0800 Subject: [PATCH 4/7] misc: fmt code --- pkg/agent/context.go | 10 ++-- pkg/agent/loop.go | 16 +++--- pkg/agent/loop_test.go | 8 +-- pkg/agent/memory.go | 8 +-- pkg/channels/dingtalk.go | 16 +++--- pkg/channels/slack.go | 6 +-- pkg/config/config.go | 62 +++++++++++------------ pkg/migrate/config.go | 2 +- pkg/migrate/migrate_test.go | 14 ++--- pkg/providers/claude_cli_provider.go | 28 +++++----- pkg/providers/claude_cli_provider_test.go | 6 +-- pkg/tools/cron.go | 8 +-- pkg/tools/message.go | 2 +- pkg/tools/shell.go | 1 - pkg/tools/subagent.go | 30 +++++------ 15 files changed, 108 insertions(+), 109 deletions(-) diff --git a/pkg/agent/context.go b/pkg/agent/context.go index e32e456..cf5ce29 100644 --- a/pkg/agent/context.go +++ b/pkg/agent/context.go @@ -170,8 +170,8 @@ func (cb *ContextBuilder) BuildMessages(history []providers.Message, summary str // Log system prompt summary for debugging (debug mode only) logger.DebugCF("agent", "System prompt built", map[string]interface{}{ - "total_chars": len(systemPrompt), - "total_lines": strings.Count(systemPrompt, "\n") + 1, + "total_chars": len(systemPrompt), + "total_lines": strings.Count(systemPrompt, "\n") + 1, "section_count": strings.Count(systemPrompt, "\n\n---\n\n") + 1, }) @@ -193,9 +193,9 @@ func (cb *ContextBuilder) BuildMessages(history []providers.Message, summary str // --- INICIO DEL FIX --- //Diegox-17 for len(history) > 0 && (history[0].Role == "tool") { - logger.DebugCF("agent", "Removing orphaned tool message from history to prevent LLM error", - map[string]interface{}{"role": history[0].Role}) - history = history[1:] + logger.DebugCF("agent", "Removing orphaned tool message from history to prevent LLM error", + map[string]interface{}{"role": history[0].Role}) + history = history[1:] } //Diegox-17 // --- FIN DEL FIX --- diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 1a63734..96e2c8f 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -33,14 +33,14 @@ type AgentLoop struct { provider providers.LLMProvider workspace string model string - contextWindow int // Maximum context window size in tokens + contextWindow int // Maximum context window size in tokens maxIterations int sessions *session.SessionManager state *state.Manager contextBuilder *ContextBuilder tools *tools.ToolRegistry running atomic.Bool - summarizing sync.Map // Tracks which sessions are currently being summarized + summarizing sync.Map // Tracks which sessions are currently being summarized } // processOptions configures how a message is processed @@ -296,9 +296,9 @@ func (al *AgentLoop) processSystemMessage(ctx context.Context, msg bus.InboundMe if constants.IsInternalChannel(originChannel) { logger.InfoCF("agent", "Subagent completed (internal channel)", map[string]interface{}{ - "sender_id": msg.SenderID, - "content_len": len(content), - "channel": originChannel, + "sender_id": msg.SenderID, + "content_len": len(content), + "channel": originChannel, }) return "", nil } @@ -307,9 +307,9 @@ func (al *AgentLoop) processSystemMessage(ctx context.Context, msg bus.InboundMe // Don't forward result here, subagent should use message tool to communicate with user logger.InfoCF("agent", "Subagent completed", map[string]interface{}{ - "sender_id": msg.SenderID, - "channel": originChannel, - "content_len": len(content), + "sender_id": msg.SenderID, + "channel": originChannel, + "content_len": len(content), }) // Agent only logs, does not respond to user diff --git a/pkg/agent/loop_test.go b/pkg/agent/loop_test.go index 6c0ad04..c182202 100644 --- a/pkg/agent/loop_test.go +++ b/pkg/agent/loop_test.go @@ -18,7 +18,7 @@ type mockProvider struct{} func (m *mockProvider) Chat(ctx context.Context, messages []providers.Message, tools []providers.ToolDefinition, model string, opts map[string]interface{}) (*providers.LLMResponse, error) { return &providers.LLMResponse{ - Content: "Mock response", + Content: "Mock response", ToolCalls: []providers.ToolCall{}, }, nil } @@ -364,7 +364,7 @@ type simpleMockProvider struct { func (m *simpleMockProvider) Chat(ctx context.Context, messages []providers.Message, tools []providers.ToolDefinition, model string, opts map[string]interface{}) (*providers.LLMResponse, error) { return &providers.LLMResponse{ - Content: m.response, + Content: m.response, ToolCalls: []providers.ToolCall{}, }, nil } @@ -475,7 +475,7 @@ func TestToolResult_SilentToolDoesNotSendUserMessage(t *testing.T) { SenderID: "user1", ChatID: "chat1", Content: "read test.txt", - SessionKey: "test-session", + SessionKey: "test-session", } response := helper.executeAndGetResponse(t, ctx, msg) @@ -517,7 +517,7 @@ func TestToolResult_UserFacingToolDoesSendMessage(t *testing.T) { SenderID: "user1", ChatID: "chat1", Content: "run hello", - SessionKey: "test-session", + SessionKey: "test-session", } response := helper.executeAndGetResponse(t, ctx, msg) diff --git a/pkg/agent/memory.go b/pkg/agent/memory.go index f27882d..3f6896f 100644 --- a/pkg/agent/memory.go +++ b/pkg/agent/memory.go @@ -40,8 +40,8 @@ func NewMemoryStore(workspace string) *MemoryStore { // getTodayFile returns the path to today's daily note file (memory/YYYYMM/YYYYMMDD.md). func (ms *MemoryStore) getTodayFile() string { - today := time.Now().Format("20060102") // YYYYMMDD - monthDir := today[:6] // YYYYMM + today := time.Now().Format("20060102") // YYYYMMDD + monthDir := today[:6] // YYYYMM filePath := filepath.Join(ms.memoryDir, monthDir, today+".md") return filePath } @@ -104,8 +104,8 @@ func (ms *MemoryStore) GetRecentDailyNotes(days int) string { for i := 0; i < days; i++ { date := time.Now().AddDate(0, 0, -i) - dateStr := date.Format("20060102") // YYYYMMDD - monthDir := dateStr[:6] // YYYYMM + dateStr := date.Format("20060102") // YYYYMMDD + monthDir := dateStr[:6] // YYYYMM filePath := filepath.Join(ms.memoryDir, monthDir, dateStr+".md") if data, err := os.ReadFile(filePath); err == nil { diff --git a/pkg/channels/dingtalk.go b/pkg/channels/dingtalk.go index 5c6f29f..263785c 100644 --- a/pkg/channels/dingtalk.go +++ b/pkg/channels/dingtalk.go @@ -20,12 +20,12 @@ import ( // It uses WebSocket for receiving messages via stream mode and API for sending type DingTalkChannel struct { *BaseChannel - config config.DingTalkConfig - clientID string - clientSecret string - streamClient *client.StreamClient - ctx context.Context - cancel context.CancelFunc + config config.DingTalkConfig + clientID string + clientSecret string + streamClient *client.StreamClient + ctx context.Context + cancel context.CancelFunc // Map to store session webhooks for each chat sessionWebhooks sync.Map // chatID -> sessionWebhook } @@ -109,8 +109,8 @@ func (c *DingTalkChannel) Send(ctx context.Context, msg bus.OutboundMessage) err } logger.DebugCF("dingtalk", "Sending message", map[string]interface{}{ - "chat_id": msg.ChatID, - "preview": utils.Truncate(msg.Content, 100), + "chat_id": msg.ChatID, + "preview": utils.Truncate(msg.Content, 100), }) // Use the session webhook to send the reply diff --git a/pkg/channels/slack.go b/pkg/channels/slack.go index b3ac12e..d86d08a 100644 --- a/pkg/channels/slack.go +++ b/pkg/channels/slack.go @@ -282,9 +282,9 @@ func (c *SlackChannel) handleMessageEvent(ev *slackevents.MessageEvent) { } logger.DebugCF("slack", "Received message", map[string]interface{}{ - "sender_id": senderID, - "chat_id": chatID, - "preview": utils.Truncate(content, 50), + "sender_id": senderID, + "chat_id": chatID, + "preview": utils.Truncate(content, 50), "has_thread": threadTS != "", }) diff --git a/pkg/config/config.go b/pkg/config/config.go index 3d213d7..391120e 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -58,13 +58,13 @@ type AgentsConfig struct { } type AgentDefaults struct { - Workspace string `json:"workspace" env:"PICOCLAW_AGENTS_DEFAULTS_WORKSPACE"` - RestrictToWorkspace bool `json:"restrict_to_workspace" env:"PICOCLAW_AGENTS_DEFAULTS_RESTRICT_TO_WORKSPACE"` - Provider string `json:"provider" env:"PICOCLAW_AGENTS_DEFAULTS_PROVIDER"` - Model string `json:"model" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL"` - MaxTokens int `json:"max_tokens" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS"` - Temperature float64 `json:"temperature" env:"PICOCLAW_AGENTS_DEFAULTS_TEMPERATURE"` - MaxToolIterations int `json:"max_tool_iterations" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"` + Workspace string `json:"workspace" env:"PICOCLAW_AGENTS_DEFAULTS_WORKSPACE"` + RestrictToWorkspace bool `json:"restrict_to_workspace" env:"PICOCLAW_AGENTS_DEFAULTS_RESTRICT_TO_WORKSPACE"` + Provider string `json:"provider" env:"PICOCLAW_AGENTS_DEFAULTS_PROVIDER"` + Model string `json:"model" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL"` + MaxTokens int `json:"max_tokens" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS"` + Temperature float64 `json:"temperature" env:"PICOCLAW_AGENTS_DEFAULTS_TEMPERATURE"` + MaxToolIterations int `json:"max_tool_iterations" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"` } type ChannelsConfig struct { @@ -140,15 +140,15 @@ type HeartbeatConfig struct { } type ProvidersConfig struct { - Anthropic ProviderConfig `json:"anthropic"` - OpenAI ProviderConfig `json:"openai"` - OpenRouter ProviderConfig `json:"openrouter"` - Groq ProviderConfig `json:"groq"` - Zhipu ProviderConfig `json:"zhipu"` - VLLM ProviderConfig `json:"vllm"` - Gemini ProviderConfig `json:"gemini"` - Nvidia ProviderConfig `json:"nvidia"` - Moonshot ProviderConfig `json:"moonshot"` + Anthropic ProviderConfig `json:"anthropic"` + OpenAI ProviderConfig `json:"openai"` + OpenRouter ProviderConfig `json:"openrouter"` + Groq ProviderConfig `json:"groq"` + Zhipu ProviderConfig `json:"zhipu"` + VLLM ProviderConfig `json:"vllm"` + Gemini ProviderConfig `json:"gemini"` + Nvidia ProviderConfig `json:"nvidia"` + Moonshot ProviderConfig `json:"moonshot"` ShengSuanYun ProviderConfig `json:"shengsuanyun"` } @@ -181,13 +181,13 @@ func DefaultConfig() *Config { return &Config{ Agents: AgentsConfig{ Defaults: AgentDefaults{ - Workspace: "~/.picoclaw/workspace", + Workspace: "~/.picoclaw/workspace", RestrictToWorkspace: true, - Provider: "", - Model: "glm-4.7", - MaxTokens: 8192, - Temperature: 0.7, - MaxToolIterations: 20, + Provider: "", + Model: "glm-4.7", + MaxTokens: 8192, + Temperature: 0.7, + MaxToolIterations: 20, }, }, Channels: ChannelsConfig{ @@ -240,15 +240,15 @@ func DefaultConfig() *Config { }, }, Providers: ProvidersConfig{ - Anthropic: ProviderConfig{}, - OpenAI: ProviderConfig{}, - OpenRouter: ProviderConfig{}, - Groq: ProviderConfig{}, - Zhipu: ProviderConfig{}, - VLLM: ProviderConfig{}, - Gemini: ProviderConfig{}, - Nvidia: ProviderConfig{}, - Moonshot: ProviderConfig{}, + Anthropic: ProviderConfig{}, + OpenAI: ProviderConfig{}, + OpenRouter: ProviderConfig{}, + Groq: ProviderConfig{}, + Zhipu: ProviderConfig{}, + VLLM: ProviderConfig{}, + Gemini: ProviderConfig{}, + Nvidia: ProviderConfig{}, + Moonshot: ProviderConfig{}, ShengSuanYun: ProviderConfig{}, }, Gateway: GatewayConfig{ diff --git a/pkg/migrate/config.go b/pkg/migrate/config.go index d7fa633..2a6f8f5 100644 --- a/pkg/migrate/config.go +++ b/pkg/migrate/config.go @@ -27,7 +27,7 @@ var supportedChannels = map[string]bool{ "whatsapp": true, "feishu": true, "qq": true, - "dingtalk": true, + "dingtalk": true, "maixcam": true, } diff --git a/pkg/migrate/migrate_test.go b/pkg/migrate/migrate_test.go index d93ea28..be2360a 100644 --- a/pkg/migrate/migrate_test.go +++ b/pkg/migrate/migrate_test.go @@ -44,8 +44,8 @@ func TestConvertKeysToSnake(t *testing.T) { "apiKey": "test-key", "apiBase": "https://example.com", "nested": map[string]interface{}{ - "maxTokens": float64(8192), - "allowFrom": []interface{}{"user1", "user2"}, + "maxTokens": float64(8192), + "allowFrom": []interface{}{"user1", "user2"}, "deeperLevel": map[string]interface{}{ "clientId": "abc", }, @@ -256,11 +256,11 @@ func TestConvertConfig(t *testing.T) { data := map[string]interface{}{ "agents": map[string]interface{}{ "defaults": map[string]interface{}{ - "model": "claude-3-opus", - "max_tokens": float64(4096), - "temperature": 0.5, - "max_tool_iterations": float64(10), - "workspace": "~/.openclaw/workspace", + "model": "claude-3-opus", + "max_tokens": float64(4096), + "temperature": 0.5, + "max_tool_iterations": float64(10), + "workspace": "~/.openclaw/workspace", }, }, } diff --git a/pkg/providers/claude_cli_provider.go b/pkg/providers/claude_cli_provider.go index 242126a..a917957 100644 --- a/pkg/providers/claude_cli_provider.go +++ b/pkg/providers/claude_cli_provider.go @@ -254,22 +254,22 @@ func findMatchingBrace(text string, pos int) int { // claudeCliJSONResponse represents the JSON output from the claude CLI. // Matches the real claude CLI v2.x output format. type claudeCliJSONResponse struct { - Type string `json:"type"` - Subtype string `json:"subtype"` - IsError bool `json:"is_error"` - Result string `json:"result"` - SessionID string `json:"session_id"` - TotalCostUSD float64 `json:"total_cost_usd"` - DurationMS int `json:"duration_ms"` - DurationAPI int `json:"duration_api_ms"` - NumTurns int `json:"num_turns"` - Usage claudeCliUsageInfo `json:"usage"` + Type string `json:"type"` + Subtype string `json:"subtype"` + IsError bool `json:"is_error"` + Result string `json:"result"` + SessionID string `json:"session_id"` + TotalCostUSD float64 `json:"total_cost_usd"` + DurationMS int `json:"duration_ms"` + DurationAPI int `json:"duration_api_ms"` + NumTurns int `json:"num_turns"` + Usage claudeCliUsageInfo `json:"usage"` } // claudeCliUsageInfo represents token usage from the claude CLI response. type claudeCliUsageInfo struct { - InputTokens int `json:"input_tokens"` - OutputTokens int `json:"output_tokens"` - CacheCreationInputTokens int `json:"cache_creation_input_tokens"` - CacheReadInputTokens int `json:"cache_read_input_tokens"` + InputTokens int `json:"input_tokens"` + OutputTokens int `json:"output_tokens"` + CacheCreationInputTokens int `json:"cache_creation_input_tokens"` + CacheReadInputTokens int `json:"cache_read_input_tokens"` } diff --git a/pkg/providers/claude_cli_provider_test.go b/pkg/providers/claude_cli_provider_test.go index 4d75e60..063530d 100644 --- a/pkg/providers/claude_cli_provider_test.go +++ b/pkg/providers/claude_cli_provider_test.go @@ -967,9 +967,9 @@ func TestFindMatchingBrace(t *testing.T) { {`{"a":1}`, 0, 7}, {`{"a":{"b":2}}`, 0, 13}, {`text {"a":1} more`, 5, 12}, - {`{unclosed`, 0, 0}, // no match returns pos - {`{}`, 0, 2}, // empty object - {`{{{}}}`, 0, 6}, // deeply nested + {`{unclosed`, 0, 0}, // no match returns pos + {`{}`, 0, 2}, // empty object + {`{{{}}}`, 0, 6}, // deeply nested {`{"a":"b{c}d"}`, 0, 13}, // braces in strings (simplified matcher) } for _, tt := range tests { diff --git a/pkg/tools/cron.go b/pkg/tools/cron.go index 3f2042e..0ef745e 100644 --- a/pkg/tools/cron.go +++ b/pkg/tools/cron.go @@ -1,4 +1,4 @@ - package tools +package tools import ( "context" @@ -147,8 +147,8 @@ func (t *CronTool) addJob(args map[string]interface{}) *ToolResult { if hasAt { atMS := time.Now().UnixMilli() + int64(atSeconds)*1000 schedule = cron.CronSchedule{ - Kind: "at", - AtMS: &atMS, + Kind: "at", + AtMS: &atMS, } } else if hasEvery { everyMS := int64(everySeconds) * 1000 @@ -194,7 +194,7 @@ func (t *CronTool) addJob(args map[string]interface{}) *ToolResult { if err != nil { return ErrorResult(fmt.Sprintf("Error adding job: %v", err)) } - + if command != "" { job.Payload.Command = command // Need to save the updated payload diff --git a/pkg/tools/message.go b/pkg/tools/message.go index 2801b39..abedb13 100644 --- a/pkg/tools/message.go +++ b/pkg/tools/message.go @@ -98,6 +98,6 @@ func (t *MessageTool) Execute(ctx context.Context, args map[string]interface{}) // Silent: user already received the message directly return &ToolResult{ ForLLM: fmt.Sprintf("Message sent to %s:%s", channel, chatID), - Silent: true, + Silent: true, } } diff --git a/pkg/tools/shell.go b/pkg/tools/shell.go index d352192..1ca3fc3 100644 --- a/pkg/tools/shell.go +++ b/pkg/tools/shell.go @@ -13,7 +13,6 @@ import ( "time" ) - type ExecTool struct { workingDir string timeout time.Duration diff --git a/pkg/tools/subagent.go b/pkg/tools/subagent.go index 97b1303..efa1d33 100644 --- a/pkg/tools/subagent.go +++ b/pkg/tools/subagent.go @@ -22,27 +22,27 @@ type SubagentTask struct { } type SubagentManager struct { - tasks map[string]*SubagentTask - mu sync.RWMutex - provider providers.LLMProvider - defaultModel string - bus *bus.MessageBus - workspace string - tools *ToolRegistry + tasks map[string]*SubagentTask + mu sync.RWMutex + provider providers.LLMProvider + defaultModel string + bus *bus.MessageBus + workspace string + tools *ToolRegistry maxIterations int - nextID int + nextID int } func NewSubagentManager(provider providers.LLMProvider, defaultModel, workspace string, bus *bus.MessageBus) *SubagentManager { return &SubagentManager{ - tasks: make(map[string]*SubagentTask), - provider: provider, - defaultModel: defaultModel, - bus: bus, - workspace: workspace, - tools: NewToolRegistry(), + tasks: make(map[string]*SubagentTask), + provider: provider, + defaultModel: defaultModel, + bus: bus, + workspace: workspace, + tools: NewToolRegistry(), maxIterations: 10, - nextID: 1, + nextID: 1, } } From 1516cb57b4391e4cd976bdb085d0252829ea4d1e Mon Sep 17 00:00:00 2001 From: Meng Zhuo Date: Fri, 13 Feb 2026 18:03:25 +0800 Subject: [PATCH 5/7] Change Docker build trigger to on release published --- .github/workflows/docker-build.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 5f8bbd3..90ff635 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -1,9 +1,8 @@ name: 🐳 Build & Push Docker Image on: - push: - branches: [main] - tags: ["v*"] + release: + types: [published] env: REGISTRY: ghcr.io From 000e64c55a4e7715626c37cee2e05a31af238abe Mon Sep 17 00:00:00 2001 From: Meng Zhuo Date: Fri, 13 Feb 2026 18:12:24 +0800 Subject: [PATCH 6/7] gh: add pull request check --- .github/workflows/build.yml | 1 - .github/workflows/pr.yml | 50 +++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/pr.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index aad0f32..465d1d6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,7 +3,6 @@ name: build on: push: branches: ["main"] - pull_request: jobs: build: diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 0000000..02f8feb --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,50 @@ +name: pr-check + +on: + pull_request: + +jobs: + fmt-check: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Check formatting + run: | + make fmt + git diff --exit-code || (echo "::error::Code is not formatted. Run 'make fmt' and commit the changes." && exit 1) + + vet: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Run go vet + run: go vet ./... + + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Run go test + run: go test ./... + From b484d3fcf38722a3c6f9ce862c79943bbf10e006 Mon Sep 17 00:00:00 2001 From: Meng Zhuo Date: Fri, 13 Feb 2026 18:46:00 +0800 Subject: [PATCH 7/7] Add dependencies for fmt-check in vet and test jobs --- .github/workflows/pr.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 02f8feb..35ad87a 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -22,6 +22,7 @@ jobs: vet: runs-on: ubuntu-latest + needs: fmt-check steps: - name: Checkout uses: actions/checkout@v4 @@ -36,6 +37,7 @@ jobs: test: runs-on: ubuntu-latest + needs: fmt-check steps: - name: Checkout uses: actions/checkout@v4