Problem:
During subturn context limit or truncation recoveries, the recovery loops repeatedly
called `runAgentLoop` with the same or modified `UserMessage`. Because `runAgentLoop`
unconditionally adds the `UserMessage` to the session history, this resulted in:
1. Duplicate User Messages polluting the history upon `context_length_exceeded` retries.
2. The possibility of injecting empty User Messages if `opts.UserMessage` was artificially blanked out to work around the duplication.
3. Messy or duplicate entries during `finish_reason="truncated"` recovery injections.
Solution:
- Introduce `SkipAddUserMessage` boolean to `processOptions` to explicitly control whether the agent loop should write the user prompt to history.
- Add an explicit `opts.UserMessage != ""` check in `runAgentLoop` to prevent polluting history with empty message content.
- In `subturn.go`'s recovery loop, set `SkipAddUserMessage: contextRetryCount > 0` to skip writing the user message on context
* 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.
* 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.
Message splitting is exclusively a Manager responsibility. Moving it
into the channels package eliminates the cross-package dependency and
aligns with the refactoring plan.
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 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>
- Accept hard upper limit (maxLen) instead of pre-subtracted value
- Caller now passes actual platform limit (e.g., 2000 for Discord)
- Internal buffer of 500 chars is handled within message.go
- Preferred split at maxLen - 500, may extend to maxLen for code blocks
- Never exceeds maxLen, no more mental math for callers
- Move FindLast, findLast, and SplitMessage from discord.go to pkg/utils/message.go
- Update discord.go to use utils.SplitMessage()
- Makes splitting logic reusable across other channels
Extract common file download and audio detection logic to utils package,
implement consistent temp file cleanup with defer, add allowlist checks
before downloading attachments, and improve context management across
Discord, Slack, and Telegram channels. Replace logging with structured
logger and prevent context leaks in transcription and thinking animations.
Code review fixes:
- Use map for O(n) job lookup in cron service (was O(n²))
- Set DeleteAfterRun=true for one-time cron tasks
- Restore context compression/summarization to prevent context overflow
- Add pkg/utils/string.go with Unicode-aware Truncate function
- Simplify setupCronTool to return only CronService
- Change Chinese comments to English in context.go
Refactoring:
- Replace toolsSummary callback with SetToolsRegistry setter pattern
- This makes dependency injection clearer and easier to test
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>