* fix: return fetched content to LLM in web_fetch tool
WebFetchTool.Execute was setting ForLLM to a summary string
("Fetched N bytes from URL ...") instead of the actual extracted
text. This meant the LLM never saw the page content and could not
answer questions based on fetched web pages.
Return the extracted text in ForLLM so the model can use it.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: put full JSON result in ForLLM, summary in ForUser
Accept suggestion from afjcjsbx: the LLM should receive the full JSON
result (including extracted text) while the user sees a short summary.
Update tests to match the new field assignment.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix(pkg/providers):do regex precompile insteadd on the fly
* fix(providers): replace HTTP-specific regex with standalone status code matcher
The precompiled HTTP regex used uppercase "HTTP" which never matched
because ClassifyError lowercases the input. Replace it with a
case-insensitive word-boundary pattern that matches any standalone
3-digit status code (300-599), which also subsumes the HTTP/x.x case.
Add test case for standalone status code extraction.
* fix(providers): restore http regex and add standalone status code matcher
Restore the http-prefixed regex (without unnecessary (?i) flag since
input is already lowercased by ClassifyError) as a mid-priority pattern
to reduce false positives. Add a standalone word-boundary matcher as a
fallback for bare status codes like "429". Fix test to use lowercased
input matching the actual calling convention.
* perf(tools): move path regex compilation from per-call to package init
The path regex in guardCommand was compiled on every call. Hoist it
to a package-level var (absolutePathPattern) alongside defaultDenyPatterns
in a single var block, so it is compiled once at init time.
* style(tools): move inline comment to fix golines formatting error
* fix(tools): close resp.Body on retry cancel and cache http.Client instances
Fix resp.Body leak in DoRequestWithRetry where req.Body (request) was
incorrectly closed instead of resp.Body (response) on context cancel.
Cache http.Client on web search/fetch provider structs and channel
adapters (WeCom, LINE) to avoid per-call allocation overhead.
* fix(channels): preserve original http client timeouts for LINE and WeCom
Split LINE single 60s client into infoClient (10s) for bot info lookups
and apiClient (30s) for messaging API calls. Lower WeCom cached client
base timeout from 60s to 30s (matching uploadMedia), and ensure it is
always >= the configured ReplyTimeout so the per-request context
deadline remains the effective limit.
* refactor(tools): extract timeout consts and deduplicate WebFetchTool constructors
Address PR review feedback from xiaket:
- Define searchTimeout, perplexityTimeout, fetchTimeout, defaultMaxChars,
and maxRedirects as package-level consts instead of magic numbers.
- Remove misleading "No proxy" comment in NewWebFetchTool.
- Deduplicate NewWebFetchTool by delegating to NewWebFetchToolWithProxy.
* test(utils): add context cancellation test for DoRequestWithRetry
Verify that resp.Body is properly closed when the context is canceled
during retry sleep, covering the C8 resp.Body leak fix.
* fix(utils): close resp in test to satisfy bodyclose linter
* fix(utils): eliminate flakiness in context cancellation retry test
Synchronize cancellation using an onRoundTrip callback from the
transport wrapper instead of a timing-based context timeout. This
ensures the first client.Do completes before cancel fires, so
cancellation always hits during sleepWithCtx.
- MCPTool.Name(): append FNV-32a hash of original (unsanitized) server+tool
names whenever sanitization is lossy or total length exceeds 64 chars,
ensuring names that differ only in disallowed characters remain distinct
- ToolRegistry.Register(): emit warn log when a tool registration overwrites
an existing tool with the same name, making collisions observable
- scripts/test-docker-mcp.sh: switch shebang from #/bin/bash /Users/yuchou/Work/klook-calendar/klook-google-cal-sync/src/googlecalconversrv/bin/start.sh to # for portability on minimal distros and Nix environments
- Avoid logging sensitive cfg.Args in ConnectServer; log args_count instead
- Sanitize server/tool name components in MCPTool.Name() to ensure valid
identifiers for downstream providers (lowercase, [a-z0-9_-] only)
- Add slack as 5th MCP server example in config.example.json
- Move Dockerfile.full and docker-compose.full.yml into docker/ directory
for consistency with existing docker/Dockerfile and docker/docker-compose.yml
- Fix all Makefile docker-* targets to reference correct compose file paths
- Fix docker/docker-compose.full.yml build context (.. ) and volume paths
- Fix scripts/test-docker-mcp.sh compose file path and replace cowsay test
with actual @modelcontextprotocol/server-filesystem MCP server test
Avoid rebuilding the entire system prompt on every BuildMessages() call
by caching the static portion (identity, bootstrap, skills summary,
memory) and only recomputing it when workspace source files change.
Key changes:
- ContextBuilder caches the static prompt behind an RWMutex with
double-checked locking. Source file changes are detected via cheap
os.Stat mtime checks so no explicit invalidation is needed.
- Track file existence at cache time (existedAtCache map) so that
newly created or deleted bootstrap/memory files also trigger a
rebuild — the old modifiedSince() silently returned false on
os.IsNotExist.
- Walk the skills directory recursively with filepath.WalkDir to
catch content-only edits at any nesting depth; directory mtime
alone misses in-place file modifications on most filesystems.
- ToolRegistry.sortedToolNames() sorts tool names before iteration,
ensuring deterministic tool definition order across calls — a
prerequisite for LLM-side prefix/KV cache reuse.
- Merge all context (static + dynamic + summary) into a single
system message for provider compatibility: the Anthropic adapter
extracts messages[0] as the top-level system parameter, and Codex
reads only the first system message as instructions.
- Fix a data race in BuildMessages() where cachedSystemPrompt was
read without holding the lock in a debug log statement.
- Add tests: single system message invariant, mtime auto-invalidation,
new-file creation detection, skill file content change, explicit
InvalidateCache, cache stability, concurrent access (20 goroutines
x 50 iterations, passes go test -race), and a benchmark.
The spawn tool accepts empty strings as valid task arguments, which
causes a subagent to run with no meaningful work. The subagent's
completion message is then routed back to the originating channel
(e.g. Signal, Discord), where the main agent processes it and may
hallucinate an unrelated response that gets sent to users.
Validate that the task parameter is non-empty after trimming whitespace.
Related: #545
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* cleanup dead code.
Signed-off-by: Kai Xia <kaix+github@fastmail.com>
* add these two back with flag.
Signed-off-by: Kai Xia <kaix+github@fastmail.com>
* fix ci
Signed-off-by: Kai Xia <kaix+github@fastmail.com>
* remove this confusing line
Signed-off-by: Kai Xia <kaix+github@fastmail.com>
* make fmt
Signed-off-by: Kai Xia <kaix+github@fastmail.com>
* remove unused method.
picked up by golangci-lint run
Signed-off-by: Kai Xia <kaix+github@fastmail.com>
---------
Signed-off-by: Kai Xia <kaix+github@fastmail.com>
- Drain buffered messages in MessageBus.Close() so they aren't silently lost
- Replace all context.TODO() with context.WithTimeout(5s) across 7 call sites
- Fix OneBot pending channel leak: send nil sentinel in Stop() and handle
nil response in sendAPIRequest() to unblock waiting goroutines
* chore: Update default host bindings from 0.0.0.0 to 127.0.0.1 for various services and examples.
* config: Update default host bindings to 0.0.0.0 for improved Docker accessibility and add related documentation.
* refactor: reimplement filesystem tools with `os.OpenRoot` for enhanced security and simplified path validation.
* chore: revert other PR content from this branch
* docs: Update Chinese README.
* docs: Update Chinese README.
* docs: Update Chinese README.
* refactor: Reorder filesystem helper functions, extract directory entry formatting logic, and enhance `WriteFileTool`'s result message.
* feat: Enhance `mkdirAllInRoot` to prevent creating directories over existing files and add tests for directory creation functionality.
* Refactor filesystem tools to use a `fileReadWriter` interface for both host and sandboxed I/O, improving atomic writes and error handling.
* refactor: unify filesystem read/write operations with atomic write guarantees and clearer naming.
* refactor: rename `appendFileWithRW` function to `appendFile`
* refactor: unify filesystem access by introducing a `fileSystem` interface and updating tools to use it directly, removing `os.Root` dependency from `sandboxFs`.
* chore: run make fmt
* fix: `validatePath` now returns an error when the workspace is empty.
Add outbound media sending capability so the agent can publish media
attachments (images, files, audio, video) through channels via the bus.
- Add MediaPart and OutboundMediaMessage types to bus
- Add PublishOutboundMedia/SubscribeOutboundMedia bus methods
- Add MediaSender interface discovered via type assertion by Manager
- Add media dispatch/worker in Manager with shared retry logic
- Extend ToolResult with Media field and MediaResult constructor
- Publish outbound media from agent loop on tool results
- Implement SendMedia for Telegram, Discord, Slack, LINE, OneBot, WeCom
PublishInbound/PublishOutbound held RLock during blocking channel sends,
deadlocking against Close() which needs a write lock when the buffer is
full. ConsumeInbound/SubscribeOutbound used bare receives instead of
comma-ok, causing zero-value processing or busy loops after close.
Replace sync.RWMutex+bool with atomic.Bool+done channel so Publish
methods use a lock-free 3-way select (send / done / ctx.Done). Add
context.Context parameter to both Publish methods so callers can cancel
or timeout blocked sends. Close() now only sets the atomic flag and
closes the done channel—never closes the data channels—eliminating
send-on-closed-channel panics.
- Remove dead code: RegisterHandler, GetHandler, handlers map,
MessageHandler type (zero callers across the whole repo)
- Add ErrBusClosed sentinel error
- Update all 10 caller sites to pass context
- Add msgBus.Close() to gateway and agent shutdown flows
- Add pkg/bus/bus_test.go with 11 test cases covering basic round-trip,
context cancellation, closed-bus behavior, concurrent publish+close,
full-buffer timeout, and idempotent Close
* feat: integrate Tavily search
* fix: set include_raw_content to false in Tavily search as wealready get relevant data inside content
* refactor: update Go type declarations to `any`, apply formatting fixes.
Add comprehensive unit tests for the ToolRegistry covering registration,
lookup, execution, context injection, async callbacks, schema generation,
provider definition conversion, and concurrent access.
Fix a defensive edge case in Truncate where a negative maxLen would cause
a slice bounds panic, and add table-driven tests covering boundary
conditions, zero/negative lengths, and Unicode handling.
Co-authored-by: Cursor <cursoragent@cursor.com>
CallTool can return (nil, nil) if the underlying MCP library misbehaves.
Without a nil check, result.IsError would panic. Return an explicit error
ToolResult instead.
Implements SearXNG as a third web search provider to address Oracle Cloud
datacenter IP blocking issues and provide a cost-free, self-hosted alternative
to commercial search APIs.
Changes:
- Add SearXNGConfig struct with Enabled, BaseURL, and MaxResults fields
- Implement SearXNGSearchProvider with JSON API integration
- Update provider priority: Perplexity > Brave > SearXNG > DuckDuckGo
- Wire SearXNG configuration through agent tool registration
- Add default config values (disabled by default, empty BaseURL)
Benefits:
- Solves DuckDuckGo datacenter IP blocking (138 bytes redirect responses)
- Zero-cost alternative to Brave Search API ($5/1000 queries)
- Self-hosted solution with 70+ aggregated search engines
- Privacy-focused with no rate limits or API keys required
- Ideal for Oracle Cloud, GCP, AWS, and Azure VM deployments
The implementation follows the existing provider interface pattern and
maintains backward compatibility with all existing search providers.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Resolved conflicts:
- pkg/config/config.go: Removed duplicate DefaultConfig() (already in defaults.go)
- pkg/config/defaults.go: Updated Temperature to *float64 (nil default)
Upstream changes included:
- Temperature changed from float64 to *float64 (nil means use provider default)
- New HeartbeatConfig and DevicesConfig
- Various agent and tool improvements
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add MaxTokens and Temperature fields to AgentInstance and update related logic
* feat: add MaxTokens and Temperature options to SubagentManager and update tool loop logic
* feat: add default temperature handling and update related tests
* feat: allow temperature 0 and distinguish unset
* fix: format MockLLMProvider struct in subagent_tool_test.go
- Move provider creation logic to factory_provider.go with protocol-based approach
- Add OpenAIProviderConfig with WebSearch support and embedded ProviderConfig
- Add maxTokensField to OpenAI-compatible provider for configurable token field
- Introduce new providers: Ollama, DeepSeek, GitHubCopilot, Antigravity, Qwen
- Remove redundant CreateProvider function from factory.go
- Add ThoughtSignature field to FunctionCall for tool response handling
- Remove duplicate Name field assignment in tool loop
- Update tests to reflect new provider configuration structure
Resolved conflicts in:
- config/config.example.json: Added empty MCP config block
- pkg/config/config.go: Added MCP config structures to new ToolsConfig
- pkg/agent/loop.go: Integrated MCP tools with new AgentRegistry architecture
MCP tools now register to all agents in the registry during startup.
- Preserve user's configured model during config migration (issue #5)
- Simplify ExtractProtocol using strings.Cut
- Extract NormalizeToolCall to shared utility, removing ~70 lines of duplicate code
- Clean up unused fields in providerMigrationConfig struct
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resolve conflicts in pkg/providers/types.go and pkg/agent/loop.go:
- types.go: use protocoltypes aliases from PR #213, keep fallback types
- loop.go: drop old single-agent createToolRegistry (replaced by multi-agent pattern)
Refactor to align with PR #213 patterns:
- instance.go: use NewExecToolWithConfig (accept full config for deny patterns)
- registry.go: pass full config to NewAgentInstance
- loop.go: add Perplexity web search options to registerSharedTools
Replace unconditional WithTimeout usage with conditional context creation
based on timeout configuration. Zero values now bypass timeout enforcement,
using WithCancel for graceful cancellation while preserving existing timeout
behavior for positive values. Simplifies CronTool initialization by removing
unnecessary conditional timeout assignment.
Resolve conflicts:
- pkg/agent/loop.go: integrate context compression, command handling,
utf8 token estimation, and summarization notification into
multi-agent routing architecture
- pkg/config/config_test.go: merge imports from both branches
- pkg/agent/loop_test.go: update test to use registry-based sessions
- Handle json.RawMessage and []byte types by direct unmarshal
- Use JSON marshal/unmarshal for struct types to preserve schema
- Add test case for json.RawMessage schema
- Fixes issue where non-map schemas returned empty object
This fixes GitHub Copilot feedback that Parameters() was dropping
tool schema when InputSchema wasn't already map[string]interface{}
- Change NewMCPTool to accept MCPManager interface instead of concrete *mcp.Manager
- Remove unused mcpPkg import from mcp_tool.go
- Remove newMCPToolForTest helper function as NewMCPTool now accepts interface
- Update all tests to use NewMCPTool directly with MockMCPManager
- Improves testability and follows dependency inversion principle
- write config and cron store with 0600 instead of 0644
- check allow list in Slack slash commands and app mentions
- pass workspace restrict flag to cron exec tool
Closes#179
* feat: add Codex CLI provider for OpenAI subprocess integration
Add CodexCliProvider that wraps `codex exec --json` as a subprocess,
analogous to the existing ClaudeCliProvider pattern. This enables using
OpenAI's Codex CLI tool as a local LLM backend.
- CodexCliProvider: subprocess wrapper parsing JSONL event stream
- Credential reader for ~/.codex/auth.json with token expiry detection
- Factory integration: provider "codex-cli" and auth_method "codex-cli"
- Fix tilde expansion in workspace path for CLI providers
- 37 unit tests covering parsing, prompt building, credentials, and mocks
* fix: add tool call extraction to Codex CLI provider
- Extract shared tool call parsing into tool_call_extract.go
(extractToolCallsFromText, stripToolCallsFromText, findMatchingBrace)
- Both ClaudeCliProvider and CodexCliProvider now share the same
tool call extraction logic for PicoClaw-specific tools
- Fix cache token accounting: include cached_input_tokens in total
- Add 2 new tests for tool call extraction from JSONL events
- Update existing tests for corrected token calculations
* fix(docker): update Go version to match go.mod requirement
Dockerfile used golang:1.24-alpine but go.mod requires go >= 1.25.7.
This caused Docker builds to fail on all branches with:
"go: go.mod requires go >= 1.25.7 (running go 1.24.13)"
Update to golang:1.25-alpine to match the project requirement.
* fix: handle codex CLI stderr noise without losing valid stdout
Codex writes diagnostic messages to stderr (e.g. rollout errors) which
cause non-zero exit codes even when valid JSONL output exists on stdout.
Parse stdout first before checking exit code to avoid false errors.
* style: fix gofmt formatting and update web search API in tests
- Remove trailing whitespace in web.go and base_test.go
- Update config_test.go and web_test.go for WebSearchToolOptions API
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.
Implement comprehensive MCP support with stdio/HTTP/SSE transports, environment variable configuration (env and envFile), custom headers, tool registration, and automatic resource cleanup. Includes full test coverage and VSCode-compatible configuration.
- Added pkg/mcp/manager.go for server lifecycle management
- Added pkg/tools/mcp_tool.go for tool wrapping
- Integrated into agent loop with cleanup
- Support for envFile loading (.env format)
- Headers injection for HTTP/SSE authentication
- Example configs for filesystem, github, brave-search, postgres
Update registerSharedTools to use new WebSearchToolOptions API and
add hardware tools (I2C, SPI) from upstream. Accept upstream's
new web tools config test.
* add I2C and SPI tools for hardware interaction
- Implemented I2CTool for I2C bus interaction, including device scanning, reading, and writing.
- Implemented SPITool for SPI bus communication, supporting device listing, data transfer, and reading.
- Added platform-specific implementations for Linux and stubs for non-Linux platforms.
- Updated agent loop to register new I2C and SPI tools.
- Created documentation for hardware skills, including usage examples and pinmux setup instructions.
* Remove build constraints for Linux from I2C and SPI tool files.
Fix web_test.go and config_test.go to use current function signatures
after merging upstream changes (WebSearchToolOptions, BraveConfig).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resolve conflicts in loop.go, config.go, config_test.go,
spawn.go, and subagent.go. Integrate upstream ToolResult/AsyncTool
pattern with multi-agent routing features. Rename mockProvider
to mockRegistryProvider in registry_test.go to avoid redeclaration
with upstream's loop_test.go.
Merge upstream/main into bugfix/fix-duplicate-telegram-messages.
Conflict resolutions:
- pkg/agent/loop.go: Adopt upstream's processSystemMessage which removes
runAgentLoop call entirely (subagents now communicate via message tool
directly). Keep PR's HasSentInRound() check in Run() for normal
message processing path.
- pkg/tools/message.go: Merge both changes - keep sentInRound tracking
from PR and adopt upstream's *ToolResult return type with Silent: true.