- Replace manual temp+rename in writeMeta and rewriteJSONL with the
project's standard fileutil.WriteFileAtomic. This adds fsync before
rename, which is important for flash storage on embedded devices
where power loss can leave zero-length files after an unsynced rename.
- Log a warning when readMessages skips a corrupt line, so operators
can see that data was lost after a crash instead of silently dropping it.
- Document the lossy sanitizeKey mapping (telegram:123 → telegram_123)
as an intentional tradeoff.
* 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
Cancel the constructor-created context before overwriting in Start()
to prevent the original cancel function from becoming unreachable.
Move len(processedMsgs) check inside the write lock to eliminate a
data race, and re-insert the current msgID after map reset to prevent
duplicate processing of the in-flight message.
Applies to both WeComBotChannel and WeComAppChannel.
The ctx field was only set in Start(), so tests calling handleMessageCallback
without Start() caused a nil pointer dereference in MessageBus.PublishInbound.
The HTTP request context is canceled as soon as the handler returns the
response, causing PublishInbound to fail with "context canceled" when
processMessage runs asynchronously in a goroutine. Use the channel's
long-lived context (c.ctx) instead.
A standalone web-based tool for managing picoclaw configuration, OAuth
authentication providers, and gateway process lifecycle. Features include
a sidebar layout with i18n (en/zh) and theme support, real-time gateway
log streaming, startup prerequisites checks, and Windows icon embedding.
Co-authored-by: wj-xiao <meetwenjie@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- WhatsApp Start(): use deferred cleanup to nil out c.client/c.container
and disconnect/close resources on any error after struct fields are
assigned, preventing stale references and double-close in Stop()
- handleReasoning: treat bus.ErrBusClosed as an expected condition
(DEBUG level) alongside context timeout/cancel, avoiding WARN noise
during normal shutdown
- WhatsApp Send(): detect unpaired state (Store.ID == nil) and return
ErrTemporary instead of attempting to send while QR login is pending
- handleReasoning: check the returned error type (DeadlineExceeded /
Canceled) instead of ctx.Err() to decide log level, so pubCtx
timeouts on a full bus are correctly classified as expected
- Test: fill bus with a short-timeout loop instead of hardcoding the
buffer size (64), making the test resilient to buffer size changes
Move the stopping check and wg.Add(1) inside reconnectMu in
eventHandler, and set the stopping flag under the same lock in Stop().
This makes the two operations atomic with respect to each other,
preventing the race where:
1. eventHandler checks stopping (false)
2. Stop() sets stopping=true and enters wg.Wait() (wg is 0)
3. eventHandler calls wg.Add(1) → panic or goroutine leak
- Use c.runCtx for GetQRChannel so the QR producer is canceled on Stop()
- Add atomic stopping guard to prevent wg.Add/wg.Wait race in eventHandler
- Make Stop() context-aware: disconnect client before waiting, respect ctx deadline
- Reduce reasoning publish log noise: use debug level for expected ctx errors
- Add test for handleReasoning when outbound bus is full (timeout path)
Add a 5-second timeout to handleReasoning's PublishOutbound call so
fire-and-forget goroutines do not block indefinitely when the outbound
bus channel is full. Reasoning output is best-effort; on timeout the
publish is abandoned with a warning log instead of holding the
goroutine alive.
Fixes goroutine leak introduced in #802.
- Move runCtx/runCancel creation before event handler registration and
QR loop so Stop() can cancel at any point during startup
- Replace blocking QR event loop in Start() with a background goroutine
that selects on runCtx.Done(), preventing Start() from hanging
indefinitely when waiting for QR scan
- Track all background goroutines (QR handler, reconnect) with
sync.WaitGroup; Stop() waits for them to finish before releasing
client/container resources
- Cancel runCtx on error paths in Start() to avoid leaked contexts
Fixes resource leak introduced in #655.
Define PlaceholderCapable, TypingCapable, and ReactionCapable interfaces
and have BaseChannel.HandleMessage auto-detect and trigger all three as
independent pipelines on inbound messages. This replaces the scattered
manual orchestration code in each channel's handleMessage with a single
unified dispatch in the framework layer.
Changes:
- Add PlaceholderCapable interface to interfaces.go
- Add ReactionCapable + RecordReactionUndo to interfaces.go
- BaseChannel.HandleMessage auto-triggers Typing → Reaction → Placeholder
- Manager gains reactionUndos sync.Map with TTL janitor cleanup
- Telegram: extract SendPlaceholder from manual code, add StartTyping
- Discord: add SendPlaceholder + StartTyping
- Pico: add SendPlaceholder (uses Pico Protocol message.create)
- Slack: extract ReactToMessage from manual code
- OneBot: extract ReactToMessage, remove leaked pendingEmojiMsg sync.Map
- LINE: move group-chat guard into StartTyping, remove manual orchestration
- Config: add Placeholder to PicoConfig; remove from Slack/LINE/OneBot
(no MessageEditor, so placeholder config was dead code)
Port changes that were applied to the old pkg/channels/*.go files on main
to their new locations in channel subpackages:
- telegram: precompile regex, var transcribedText, GetModelName()
- discord: var transcribedText declaration
- onebot: resp.Body.Close(), "canceled" spelling, remove empty line
- slack: named return values in parseSlackChatID
- wecom: remove sendMarkdownMessage dead code
- whatsapp: resp.Body.Close() after Dial
- gateway/helpers: remove unused errors import
Prevents potential backpressure under load when multiple channels
publish concurrently in gateway mode, where SDK callbacks blocking
on a full buffer can cause message loss or timeouts.
- Replace stdlib log.Printf with logger.InfoCF/WarnCF for consistency
with the rest of the codebase (addresses @nikolasdehor review point #3)
- ReleaseAll: clean refToScope/refs mappings even if refs entry is missing
- CleanExpired: guard refToScope lookup before scope cleanup
- Add TestReleaseAllCleansMappingsIfRefsMissing for robustness
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address review feedback from @alexhoshina and Codex:
- Log sendLoading errors in ticker goroutine instead of discarding
- Only start typing indicator when PlaceholderRecorder is available
to avoid wasted API calls and unnecessary goroutine creation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(skills): add retry mechanism for HTTP requests
Implement a retry mechanism with exponential backoff for HTTP requests in the skill installer. This improves reliability when fetching skills from GitHub by automatically retrying failed requests up to 3 times.
Add comprehensive tests to verify retry behavior under different scenarios including success on different attempts and proper delay between retries.
* fix: improve http request retry logic with status code checks
Add shouldRetry helper function to determine retryable status codes.
Close response body between retry attempts and break early for non-retryable status codes.
* refactor: remove unused BuiltinSkill struct
The struct was not being used anywhere in the codebase, so it's safe to remove it to reduce clutter and improve maintainability.
* refactor(http): move retry logic to utils package
Extract HTTP retry functionality from skills package to utils for better reusability
Add context-aware sleep function and comprehensive tests
* refactor(http): extract retry delay unit to variable
Extract hardcoded retry delay unit to a variable for better testability and flexibility. Update tests to use milliseconds for faster execution while maintaining the same behavior.
* test(http_retry): remove t.Parallel from test cases
* test(http_retry): remove redundant test cases for retry success
The removed test cases for success on second and third attempts were redundant since the retry logic is already covered by other tests. This simplifies the test suite while maintaining coverage.
Change the default value of session.dm_scope from "main" to
"per-channel-peer" to provide better conversation isolation by
default. This prevents context leakage between different users
and channels.
MigrateFromJSON previously called AddFullMessage in a loop, then
renamed the .json file to .json.migrated. If the process crashed
after appending some messages but before the rename, a retry would
re-read the same .json and append all messages again — duplicating
whatever was written before the crash.
Switch to SetHistory which atomically replaces the session contents.
A retry after crash overwrites the partial data instead of appending.
In SetHistory and Compact, the JSONL file was rewritten before updating
the meta file. If the process crashed between the two writes, the meta
still had a large Skip value pointing past the now-shorter JSONL file,
causing GetHistory to return empty — effectively data loss.
Reverse the order: write meta (with Skip=0) first, then rewrite JSONL.
On crash between the two writes, the old uncompacted file is still
intact and GetHistory reads from line 1, returning stale-but-complete
data. The next operation self-corrects.
A crash between the JSONL append and the meta update in addMsg can
leave meta.Count stale (e.g. file has 101 lines but meta says 100).
The previous code only reconciled when Count==0, so a nonzero stale
count was silently trusted, causing keepLast/skip to be calculated
against the wrong total.
Now TruncateHistory always counts the actual lines on disk. This is
cheap (scan without unmarshal) and TruncateHistory is not a hot path.
Implement TypingCapable interface for LINE channel using the
loading animation API (1:1 chats only, no group support).
- Add StartTyping() with 50s periodic refresh and context-based stop
- Integrate PlaceholderRecorder.RecordTypingStop in processEvent
- Skip RecordPlaceholder (LINE has no message edit API)
- Change sendLoading to accept context and return error
- Relax callAPI status check from 200 to 2xx range
Design consulted with Codex (GPT-5.2).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address feedback from @yinwm for long-running daemon use:
- Replace sync.Map with a fixed-size sharded lock array (64 mutexes).
Keys are mapped via FNV hash, so memory is O(1) regardless of how
many sessions are created over the process lifetime.
- Increase scanner buffer cap from 1 MB to 10 MB. Tool results
(read_file on large files, web search responses) can easily exceed
1 MB. The scanner still starts at 64 KB and only grows as needed.
Address Codex (GPT-5.2) review feedback:
- Start: guard against Interval<=0 or MaxAge<=0 to prevent
time.NewTicker panic on misconfiguration
- ReleaseAll: split into two phases (collect under lock, delete
after unlock) matching CleanExpired pattern
- ReleaseAll: log file removal errors
- Add TestStartZeroIntervalNoPanic and TestStartZeroMaxAgeNoPanic
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- CleanExpired: split into two phases — collect expired entries under
lock, then delete files after releasing the lock to minimize contention
- CleanExpired: guard against zero MaxAge (no-op if unconfigured)
- CleanExpired: log file removal errors instead of silently ignoring
- Start: protect with startOnce to prevent multiple goroutines
- Stop: rename once -> stopOnce for clarity
- cmd_gateway: call mediaStore.Stop() on error path after Start()
- Add TestCleanExpiredZeroMaxAge and double-Start test
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address review feedback from @Zhaoyikaiii:
- Replace map[string]*sync.Mutex + separate mu with sync.Map.LoadOrStore
for simpler, lock-free session lock management.
- Add skip parameter to readMessages so callers (GetHistory, Compact)
can skip truncated lines without paying the json.Unmarshal cost.
- Add countLines helper for TruncateHistory's count reconciliation,
avoiding full deserialization when only the line count is needed.
Address file growth concern from #711 review: logical truncation via
skip offset is fast but leaves dead lines on disk indefinitely.
Compact() rewrites the JSONL file keeping only active messages, using
the same temp+rename pattern for crash safety. No-op when skip == 0.
The caller (lifecycle manager or agent loop) decides when to trigger
compaction — e.g. when skipped lines exceed active lines.
Read existing sessions/*.json files, convert to JSONL format, and
rename originals to .json.migrated as backup. The migration is
idempotent — second runs skip already-migrated files.
Session keys are read from JSON content (not filenames) so that
sanitized names like telegram_123 correctly map back to telegram:123.
Cover all Store interface methods plus edge cases:
- Basic roundtrip, ordering, empty session, tool calls
- Logical truncation (keep last N, keep zero, keep more than exist)
- SetHistory replacing all + resetting skip offset
- Crash recovery with partial JSON lines
- Persistence across store instances
- Concurrent add+read (10 goroutines x 20 msgs)
- Simulated #704 race (summarizer vs main loop)
- Benchmarks for AddMessage and GetHistory (100/1000 msgs)
Add JSONLStore that persists sessions as .jsonl files (one message per
line) plus .meta.json for summary and truncation offset.
Key design decisions:
- Append-only writes — no full-file rewrites on AddMessage
- Logical truncation via skip offset instead of physical deletion
- Per-session mutex for safe concurrent access
- Crash recovery: malformed trailing lines are silently skipped
- Atomic metadata writes using temp+rename
Zero new dependencies — pure stdlib.
Refs #711
Introduce a backend-agnostic Store interface in pkg/memory/ that maps
one-to-one with the current SessionManager API. Each method is atomic
— no separate Save() call needed.
Refs #711
* fix: remove redundant tools definitions from system prompt
Tools are already provided to the LLM via JSON schema through
ToProviderDefs(), so the text-based tools section in the system
prompt is redundant.
This removes the buildToolsSection() logic and the tools field
from ContextBuilder, reducing system prompt length while maintaining
the "ALWAYS use tools" rule reminder.
Fixes#731
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: correct spelling 'initialized' (was 'initialised')
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Whitespace is a linter that checks for unnecessary newlines at the start and end of functions, if, for, etc.
Signed-off-by: Kai Xia <kaix+github@fastmail.com>
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.
Rename fastID() to uniqueID() with a security caveat comment clarifying
the ID is not cryptographically secure, and add unit tests for the
refactored index-based split helper functions.
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>
HTTP timeouts (context deadline exceeded, Client.Timeout) were
incorrectly classified as context window errors, triggering useless
history compression. Replace broad substring checks ("context",
"token", "length") with specific patterns for real context limit
errors and explicitly exclude timeout errors from that path.
Additionally, timeout errors were not retried at all — the retry
loop only handled context window errors. Now timeouts are retried
up to 2 times with exponential backoff (5s, 10s).
* 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>
Walk backwards over preceding tool messages to find the nearest assistant
with ToolCalls, instead of only checking the immediate predecessor. Add
unit tests for sanitizeHistoryForProvider covering key edge cases.
Add background TTL-based cleanup (L2 safety net) directly into
FileMediaStore so file deletion and in-memory ref removal happen
atomically under the same mutex, preventing dangling references.
- Add storedAt timestamp and refToScope reverse map to mediaEntry
- Add CleanExpired() for atomic TTL-based expiration
- Add Start()/Stop() for background goroutine lifecycle
- Add MediaCleanupConfig (enabled, max_age, interval) to config
- Wire up in cmd_gateway.go with config-driven defaults
- Add 8 new tests including concurrent cleanup safety
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Translate Chinese comments to English in qq, slack, and telegram channel
implementations, following the translation work done in PR #697. The
original PR modified the old parent package files, but these have been
moved to subpackages during the refactor, so translations are applied
to the new locations.
* 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.
* chore: resolve conflict
* chore: remove link
* docs: Add a tip for Docker users regarding gateway host configuration to the French and Vietnamese READMEs.
* fix: typo issue
* docs: Update Chinese README.zh.md.
- Update Dockerfile to use golang:1.25-alpine to match go.mod (go 1.25.7)
- Optimize logger by avoiding string concatenation in file writes
- Add explicit empty string assignment for fieldStr when no fields
These changes improve build consistency and reduce memory allocations
in the hot logging path, which is important for the project's goal
of running on resource-constrained devices (<10MB RAM).
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.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.
The configuration field for specifying the model has been renamed from
"model" to "model_name" for better clarity and consistency with the
model_list configuration.
A GetModelName() accessor method has been added to maintain backward
compatibility. Existing configurations using the old "model" field will
continue to work correctly.
This change affects:
- Configuration structure (AgentDefaults struct)
- All references across the codebase
- Documentation in all language variants
- Example configuration files
Introduce SenderInfo struct and pkg/identity package to standardize user
identification across all channels. Each channel now constructs structured
sender info (platform, platformID, canonicalID, username, displayName)
instead of ad-hoc string IDs. Allow-list matching supports all legacy
formats (numeric ID, @username, id|username) plus the new canonical
"platform:id" format. Session key resolution also handles canonical
peerIDs for backward-compatible identity link matching.
- MediaStore: use full UUID to prevent ref collisions, preserve and
expose metadata via ResolveWithMeta, include underlying OS errors
- Agent loop: populate MediaPart Type/Filename/ContentType from
MediaStore metadata so channels can dispatch media correctly
- SplitMessage: fix byte-vs-rune index mixup in code block header
parsing, remove dead candidateStr variable
- Pico auth: restrict query-param token behind AllowTokenQuery config
flag (default false) to prevent token leakage via logs/referer
- HandleMessage: replace context.TODO with caller-propagated ctx,
log PublishInbound failures instead of silently discarding
- Gateway shutdown: use fresh 15s timeout context for StopAll so
graceful shutdown is not short-circuited by the cancelled parent ctx
Message splitting is exclusively a Manager responsibility. Moving it
into the channels package eliminates the cross-package dependency and
aligns with the refactoring plan.
Phase 10: Define TypingCapable, MessageEditor, PlaceholderRecorder interfaces.
Manager orchestrates outbound typing stop and placeholder editing via preSend.
Migrate Telegram, Discord, Slack, OneBot to register state with Manager instead
of handling locally in Send. Phase 7: Add native WebSocket Pico Protocol channel
as reference implementation of all optional capability interfaces.
Add unified ShouldRespondInGroup to BaseChannel, replacing scattered
per-channel group filtering logic. Introduce GroupTriggerConfig (with
mention_only + prefixes), TypingConfig, and PlaceholderConfig types.
Migrate Discord MentionOnly, OneBot checkGroupTrigger, and LINE
hardcoded mention-only to the shared mechanism. Add group trigger
entry points for Slack, Telegram, QQ, Feishu, DingTalk, and WeCom.
Legacy config fields are preserved with automatic migration.
Remove SetTranscriber and inline transcription logic from 4 channels
(Telegram, Discord, Slack, OneBot) and the gateway wiring. Voice/audio
files are still downloaded and stored in MediaStore with simple text
annotations ([voice], [audio: filename], [file: name]). The pkg/voice
package is preserved for future Agent-level transcription middleware.
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
Merge 3 independent channel HTTP servers (LINE :18791, WeCom Bot :18793,
WeCom App :18792) and the health server (:18790) into a single shared
HTTP server on the Gateway address. Channels implement WebhookHandler
and/or HealthChecker interfaces to register their handlers on the shared
mux. Also change Gateway default host from 0.0.0.0 to 127.0.0.1 for
security.
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.
Define sentinel error types (ErrNotRunning, ErrRateLimit, ErrTemporary,
ErrSendFailed) so the Manager can classify Send failures and choose the
right retry strategy: permanent errors bail immediately, rate-limit
errors use a fixed 1s delay, and temporary/unknown errors use exponential
backoff (500ms→1s→2s, capped at 8s, up to 3 retries). A per-channel
token-bucket rate limiter (golang.org/x/time/rate) throttles outbound
sends before they hit the platform API.
Channels previously deleted downloaded media files via defer os.Remove,
racing with the async Agent consumer. Introduce MediaStore to decouple
file ownership: channels register files on download, Agent releases them
after processing via ReleaseAll(scope).
- New pkg/media with MediaStore interface + FileMediaStore implementation
- InboundMessage gains MediaScope field for lifecycle tracking
- BaseChannel gains SetMediaStore/GetMediaStore + BuildMediaScope helper
- Manager injects MediaStore into channels; AgentLoop releases on completion
- Telegram, Discord, Slack, OneBot, LINE channels migrated from defer
os.Remove to store.Store() with media:// refs
Move message splitting from individual channels (Discord) to the Manager
layer via per-channel worker goroutines. Each channel now declares its
max message length through BaseChannelOption/MessageLengthProvider, and
the Manager automatically splits oversized outbound messages before
dispatch. This prevents one slow channel from blocking all others.
- Add WithMaxMessageLength option and MessageLengthProvider interface
- Set platform-specific limits (Discord 2000, Telegram 4096, Slack 40000, etc.)
- Convert SplitMessage to rune-aware counting for correct Unicode handling
- Replace single dispatcher goroutine with per-channel buffered worker queues
- Remove Discord's internal SplitMessage call (now handled centrally)
Add bus.Peer struct and explicit Peer/MessageID fields to InboundMessage,
replacing the implicit peer_kind/peer_id/message_id metadata convention.
- Add Peer{Kind, ID} type to pkg/bus/types.go
- Extend InboundMessage with Peer and MessageID fields
- Change BaseChannel.HandleMessage signature to accept peer and messageID
- Adapt all 12 channel implementations to pass structured peer/messageID
- Simplify agent extractPeer() to read msg.Peer directly
- extractParentPeer unchanged (parent_peer still via metadata)
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>
Mistral's API strictly validates tool_calls in assistant messages and
rejects non-standard fields. The ToolCall struct had Name and Arguments
as top-level JSON fields, duplicating data already in Function.Name
and Function.Arguments. OpenAI silently ignored these extras but
Mistral returns 422.
Change json tags to "-" so these internal fields are no longer
serialized to API payloads while remaining available in Go code.
Add Mistral as a first-class provider alongside the 17 existing ones.
Mistral uses the OpenAI-compatible API at https://api.mistral.ai/v1
with provider-specific model prefix stripping (mistral/model → model).
Changes:
- Add Mistral to ProvidersConfig, IsEmpty(), HasProvidersConfig()
- Add mistral entry in default model_list (defaults.go)
- Add mistral protocol in factory_provider.go and getDefaultAPIBase()
- Add mistral prefix stripping in openai_compat normalizeModel()
- Add mistral case in legacy factory.go resolveProviderSelection()
- Add mistral migration entry in ConvertProvidersToModelList()
- Add mistral to supported providers in migrate/config.go
- Add mistral section in config.example.json
- Update AllProviders test (17 → 18 providers)
Tested end-to-end with mistral-small-latest model.
Models like Moonshot kimi-k2.5 and DeepSeek-R1 return a
reasoning_content field in assistant messages. When thinking is enabled,
the API requires this field to be echoed back in subsequent requests.
PicoClaw was silently dropping it, causing 400 errors on tool-call
round-trips.
- Add ReasoningContent to Message and LLMResponse types
- Parse reasoning_content in openai_compat parseResponse()
- Carry reasoning_content through assistant tool-call messages
- Add unit test for reasoning_content parsing
Fixes#588
A race could occur when Close() called conn.Session.Close() concurrently
with an in-flight conn.Session.CallTool(), leading to undefined behavior.
Fix by adding a sync.WaitGroup to Manager:
- CallTool increments the WaitGroup while holding the read lock (after
checking m.closed), ensuring no new calls are counted after Close sets
the flag
- Close sets m.closed=true, releases the write lock, then waits for all
in-flight calls to finish via wg.Wait() before closing sessions
ListAgentIDs() was called on every iteration of the inner tool loop,
causing repeated allocations. Capture the slice once and reuse it for
both agentCount and the registration loop.
Previously Close() discarded all underlying errors and returned only
'failed to close N server(s)', making debugging impossible.
Now each error wraps the server name and original cause, and all errors
are joined so callers can inspect the full failure list.
A line like '=value' would result in envVars[""] = "value", producing
an invalid environment entry for the child process. Return an error
instead when the key is empty.
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.