* feat(feishu): support mention_map for outbound bot-to-bot @ resolution
* feat(feishu): support mention_map for outbound bot-to-bot @ resolution (Fixes#1322)
* fix(feishu): update stale @mention test and doc comment for text-format output
After 40406328 made resolveMentionsInContent always emit the MsgTypeText
at syntax (<at user_id="...">), the legacy TestResolveMentions_CardFormat
assertion (expecting the card variant <at id=...>) started failing. Rename
the test to TestResolveMentions_MarkdownContent and assert the text-format
output instead — the behavior under test (markdown @name resolution) is
unchanged, only the expected at-tag format moved.
Also update the doc comment on resolveMentionsInContent that still
referenced "predicted message type" (that branch was removed).
No production behavior change.
* update comment of TestResolveMentions_MarkdownContent fun
* fix(feishu): restore lost test bodies from broken merge conflict resolution
* fix(feishu): restore missing closing braces broken by merge conflict resolution
The merge of main (26d897c) re-introduced the lost-brace bug: the new
TestNewPlatform_ImageBatchWindow was spliced in before the closing braces
of TestSendWithStatusFooter_NoFallbackOnNonMentionAt, leaving the for-loop
and function unclosed. This caused 10 typecheck errors (expected '(',
found TestXxx) and failed CI lint.
Add the two missing closing braces so the file compiles. All feishu
package tests pass (go test ./...).
* style(feishu): apply gofmt to fix trailing whitespace and map alignment
- platform_test.go: remove trailing whitespace on line 1789 comment
- feishu.go: drop blank line in footerElements map literal and realign
colon spacing (gofmt); main is clean so this issue was introduced in
this PR, not pre-existing
* docs(changelog): fix Feishu mention_map issue ref (PR #1341 -> Issue #1322)
The upstream dingtalk-stream-sdk-go v0.9.1 has a race condition in
processLoop (client.go:161) that causes "send on closed channel" panic
after overnight idle. The fix exists in upstream PRs but is not yet
merged/released.
Add defer/recover around streamClient.Start() so the panic is caught
and the existing reconnect loop can re-establish the connection instead
of crashing the entire cc-connect process.
Closes#1390
Co-authored-by: Cursor <cursoragent@cursor.com>
Surfaced during v1.4.0-beta.1 QA: when an agent produced a ~7.3 KB
Markdown reply in Slack, the streaming card (#1333) silently failed
mid-stream with `msg_too_long`, then Finalize tried to chat.update the
full reply and also failed, forcing the engine onto its emergency
fallback path. The user perceived the turn as a non-streaming "one-shot
output preceded by ~30s of silence".
Root cause: Slack's `chat.update` text parameter is capped at ~4000
bytes (server-side, despite docs saying chars). The streaming card had
no size guard and Update() also swallowed errors with no log, so the
half-broken state was invisible from logs until Finalize blew up.
Fix:
- Introduce `slackUpdateMaxText = 3500` (conservative; leaves headroom
for multi-byte CJK content where Go's len() overshoots Slack's limit).
- `Update()`: once rendered exceeds the limit, silently skip the
chat.update tick (leave the streaming card at its last fitting
snapshot) and emit a Debug log instead of staying invisible.
- `Update()`: also Debug-log transient render errors (previously
swallowed entirely).
- `Finalize()`: when the final payload exceeds the limit AND a card has
already been posted, deliver the full reply via a *fresh*
chat.postMessage (which has a 40 KB ceiling), so the engine sees
success and skips its duplicate-fallback emission. The partial
streaming card stays visible above the new full-content message.
- Extract `postFresh()` helper to share the postMessage logic between
the lazy first-post path and the new overflow path.
Tests:
- `TestStreamingCard_FinalizeFallsBackToFreshPostOnOversize`: oversized
Finalize must NOT touch chat.update and must postMessage the full
payload (and not mark the card failed).
- `TestStreamingCard_UpdateSkipsOversizedPayload`: oversized Update
tick must silently no-op (no chat.update call).
Net behaviour:
- Short replies (< ~3500 B mrkdwn): unchanged — chat.update streaming
keeps working as #1333 intended.
- Long replies: no more `msg_too_long` errors, no more duplicate
fallback messages from the engine. UX is "partial streaming card +
one final full-content message" instead of "broken card + duplicate
full-content message".
Closes the third user-visible bug surfaced during v1.4.0-beta.1 QA
pairing (after #1395 image batch window default and the nav.cron i18n
miss).
Co-authored-by: Cursor <cursoragent@cursor.com>
The hardcoded 150ms `imageBatchWindow` introduced in #1395 turned out to be
too narrow for real-world Feishu mobile sending behaviour. In QA we
observed ~330ms between consecutive image sends from the mobile client
when a user taps "send" repeatedly, which exceeds 150ms and defeats the
coalescing logic — each image was dispatched as its own turn, and turns
after the first hit the engine's "busy" guard and returned the
"will process after the current task finishes" notice.
Changes:
- Promote `imageBatchWindow` from a package const to a per-Platform field
populated from a new platform option `image_batch_window_ms`.
- Bump the default from 150ms to 500ms based on observed mobile send
intervals.
- Add `batchWindow()` helper so zero-initialised Platforms (constructed
directly in tests) still fall back to the default and never schedule a
zero-duration timer.
- Validate the option: reject negative or non-numeric values.
- Document the new knob in `config.example.toml` and `docs/feishu.md`.
- Add `TestNewPlatform_ImageBatchWindow` covering default, custom, zero
(fallback) and validation paths; update existing batch tests to read
the effective window via `batchWindow()`.
Operators who need a longer or shorter window can override per-platform:
[projects.platforms.options]
image_batch_window_ms = 800
Closes follow-up surfaced during v1.4.0-beta.1 QA pairing.
Co-authored-by: Cursor <cursoragent@cursor.com>
The Feishu mobile client sends N batch-selected images as N separate
message events with very close create_time values. Dispatching each
event immediately caused core/engine's create_time watermark (PR #1168)
to drop the oldest image, so the agent only ever saw N-1 images
(issue #1395).
A per-session image buffer with a 150ms quiet window now merges the
burst into one core.Message carrying all images in send order. The
buffer only coalesces plain image messages (no parent_id) — quoted
images and replies keep the existing single-message path so the
PR #944 quoted-image behavior is preserved.
Stop() now flushes any pending batches synchronously so buffered
images aren't lost on shutdown.
Tests:
- TestDispatchMessageCoalescesImageBatch (2/3/4 image subtests)
- TestDispatchMessageSingleImageRegression
- TestDispatchMessageQuotedImageNotBatched (PR #944 path)
- TestFlushImageBatchesStopsPendingTimers
- TestFlushImageBatchesEmptySafe
Co-authored-by: dev-claudecode <dev-claudecode@cc-connect.local>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
text_color is valid per Feishu docs only for plain_text elements.
The upstream PR #1204 incorrectly added it to a markdown (lark_md)
element, causing Feishu to reject the card JSON with error:
unknown property text_color, path: ...(tag: markdown)
Keep text_color on all plain_text elements where it is correct per
Feishu card API documentation.
Co-authored-by: tanghongliang <tanghongliang@citos.cn>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
When a cmd: button value includes an after_click object with a non-empty
title, onCardAction now returns a CardActionTriggerResponse that replaces
the original card in-place, while still dispatching the command to the
agent as before.
after_click fields:
- title (required): card header text
- color (optional, default "green"): header template color
- markdown (optional): body markdown content
- link_text + link_url (optional): appended as a markdown link after a divider
If after_click is absent or has no title, behavior is unchanged (returns nil).
Related: https://github.com/chenhg5/cc-connect/issues/871
Related: https://github.com/chenhg5/cc-connect/issues/936
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(slack): real-time streaming preview via chat.update
Slack was the only major platform without cc-connect's streaming-preview
interfaces (discord/feishu/telegram/max all have them), so Slack replies
arrived only as one message at turn end. This adds the minimal pair the
engine needs (same as telegram):
- SendPreviewStart (core.PreviewStarter): posts the initial preview message
(threaded like a normal reply) and returns a {channel, ts} handle.
- UpdateMessage (core.MessageUpdater): edits it in place via chat.update as
the engine streams.
No config needed (stream preview is on by default); the engine throttles
edits (~1500ms/30 chars) so this stays within chat.update rate limits, the
per-turn footer is preserved (appended inline at finish), and there's no
double-post (engine finalizes via the preview, only falling back to Send when
the preview is inactive).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(slack): aggregated streaming turn card (StreamingCardPlatform)
Implements core.StreamingCardPlatform/StreamingCard for Slack: the whole agent
turn (thinking + tool steps + answer, assembled by the engine's buildCardContent)
renders as ONE Slack message that updates in place — like DingTalk's AI Card.
chat.update is throttled internally (~1.2s). When active it takes precedence over
the plain streaming preview (engine routes via handledByStreamCard, mutually
exclusive — no double-post); the preview remains the fallback.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(slack): lazy-post the streaming card + throttle to 3s
Post the card on first content (not at turn start) so a native "is thinking…"
status can show until the bot has something to say, and bump the chat.update
throttle 1.2s → 3s to respect Slack's streaming rate-limit guidance.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(matrix): add Matrix platform adapter using mautrix-go
Implements a full Matrix protocol adapter so users can interact with
their coding agent through any Matrix homeserver (matrix.org, self-hosted
Synapse/Dendrite, etc.). Uses mautrix-go SDK for Client-Server API.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(matrix): add Matrix platform documentation
Update README.md, README.zh-CN.md, INSTALL.md, CLAUDE.md and add
docs/matrix.md with setup guide, config reference and FAQ.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test(matrix): add unit tests and fix session key parsing
35 tests covering config validation, message handling, lifecycle,
concurrency, and error paths. Race detector clean.
Fixes two bugs found by tests:
- stripBotMention: strip matrix.to URL before plain user ID to avoid
partial replacement inside the URL path
- ReconstructReplyCtx: room IDs contain colons, so colon-based SplitN
corrupted them; use ":@" boundary detection instead
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(matrix): add Chinese setup guide
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(matrix): add E2EE support via mautrix cryptohelper
Add Olm/Megolm encryption support using mautrix-go's built-in
cryptohelper. Encrypted rooms are now fully supported for both
sending and receiving messages.
Key implementation details:
- Crypto DB stored per device ID (~/.cc-connect/matrix-crypto-<device>.db)
- Auto-recovers from stale device keys by force-uploading new keys
- All outbound messages encrypted via sendRoomEvent when room has E2EE
- DecryptErrorCallback logs decryption failures through slog
- Cleanup client stores on failed crypto init to prevent DB panics
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(matrix): update setup guide with E2EE instructions
- Recommend creating dedicated device via curl login (avoids E2EE issues)
- Add E2EE section with startup log example showing device_id
- Add FAQ entries for common E2EE problems and troubleshooting
- Document crypto DB location and cleanup steps
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(matrix): add SAS verification and cross-signing support
Enable auto-accept SAS key verification so users can verify the bot's
device from Element. Bootstrap cross-signing to eliminate "encrypted by
a device not verified by its owner" warning on bot messages.
- Add verification helper with auto-accept/auto-confirm SAS flow
- Workaround mautrix MAC verification bug for cross-user verification
- Fix in-room verification transaction ID lookup from m.relates_to
- Persist cross-signing seeds to disk for stable keys across restarts
- Support m.login.password UIA fallback for publishing cross-signing keys
- Add auto_verify and cross_signing_password config options
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(matrix): add verification and cross-signing documentation
Add SAS verification and cross-signing setup instructions to both
English and Chinese Matrix guides. Document new config options
(auto_verify, cross_signing_password) and FAQ entries for device
verification and the red exclamation mark warning.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(matrix): fix inaccurate documentation and capability table
- Fix Matrix "Markdown / cards" from ✅ to ⚠️ (no CardSender)
- Change "red exclamation mark" to "red question mark" per actual UI
- Recommend curl with dedicated device_id for token (not Element)
- Add cross-signing seeds file to E2EE reset instructions
- Clarify that cross-signing may need cross_signing_password
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(matrix): reduce verbose logging in production
Downgrade per-message and internal workaround logs from Info to Debug,
and use structured fields for SAS verification log.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(matrix): fix formatting, rename field, and add token redaction
- Run gofmt to fix struct definition and literal alignment
- Rename shareSessionInChan to shareSessionInChannel for consistency
with other platforms (Telegram, Discord, Slack)
- Add core.RedactToken() in connectLoop error logging to prevent
access token leakage in logs
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: install libolm-dev for Matrix E2EE crypto
The mautrix-go crypto package depends on libolm (C library) for
end-to-end encryption. The CI runners don't have it pre-installed,
causing golangci-lint to fail when loading the CGO package.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(matrix): use build-tagged E2EE to avoid CGO/libolm dependency
Split E2EE (end-to-end encryption) code behind a `goolm` build tag so
the default build compiles without CGO or libolm-dev. This fixes CI
lint failures caused by the mautrix-go crypto package pulling in the
CGO-based libolm backend by default.
- Default build: no E2EE, no CGO, CI lint passes
- Build with `-tags goolm`: full E2EE using pure-Go olm backend
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build: enable goolm tag by default in Makefile
Matrix E2EE requires the goolm build tag. Add it as a default
tag in the Makefile so `make build` includes E2EE automatically.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(matrix): suppress unused field lint for cryptoHelper
The cryptoHelper field is only accessed in e2ee.go (goolm build tag),
so the default build's linter flags it as unused. Add //nolint:unused
since this is intentional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(matrix): add Matrix platform adapter using mautrix-go
Implements a full Matrix protocol adapter so users can interact with
their coding agent through any Matrix homeserver (matrix.org, self-hosted
Synapse/Dendrite, etc.). Uses mautrix-go SDK for Client-Server API.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(matrix): add Matrix platform documentation
Update README.md, README.zh-CN.md, INSTALL.md, CLAUDE.md and add
docs/matrix.md with setup guide, config reference and FAQ.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test(matrix): add unit tests and fix session key parsing
35 tests covering config validation, message handling, lifecycle,
concurrency, and error paths. Race detector clean.
Fixes two bugs found by tests:
- stripBotMention: strip matrix.to URL before plain user ID to avoid
partial replacement inside the URL path
- ReconstructReplyCtx: room IDs contain colons, so colon-based SplitN
corrupted them; use ":@" boundary detection instead
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(matrix): add Chinese setup guide
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(matrix): add E2EE support via mautrix cryptohelper
Add Olm/Megolm encryption support using mautrix-go's built-in
cryptohelper. Encrypted rooms are now fully supported for both
sending and receiving messages.
Key implementation details:
- Crypto DB stored per device ID (~/.cc-connect/matrix-crypto-<device>.db)
- Auto-recovers from stale device keys by force-uploading new keys
- All outbound messages encrypted via sendRoomEvent when room has E2EE
- DecryptErrorCallback logs decryption failures through slog
- Cleanup client stores on failed crypto init to prevent DB panics
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(matrix): update setup guide with E2EE instructions
- Recommend creating dedicated device via curl login (avoids E2EE issues)
- Add E2EE section with startup log example showing device_id
- Add FAQ entries for common E2EE problems and troubleshooting
- Document crypto DB location and cleanup steps
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(matrix): add SAS verification and cross-signing support
Enable auto-accept SAS key verification so users can verify the bot's
device from Element. Bootstrap cross-signing to eliminate "encrypted by
a device not verified by its owner" warning on bot messages.
- Add verification helper with auto-accept/auto-confirm SAS flow
- Workaround mautrix MAC verification bug for cross-user verification
- Fix in-room verification transaction ID lookup from m.relates_to
- Persist cross-signing seeds to disk for stable keys across restarts
- Support m.login.password UIA fallback for publishing cross-signing keys
- Add auto_verify and cross_signing_password config options
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(matrix): add verification and cross-signing documentation
Add SAS verification and cross-signing setup instructions to both
English and Chinese Matrix guides. Document new config options
(auto_verify, cross_signing_password) and FAQ entries for device
verification and the red exclamation mark warning.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(matrix): fix inaccurate documentation and capability table
- Fix Matrix "Markdown / cards" from ✅ to ⚠️ (no CardSender)
- Change "red exclamation mark" to "red question mark" per actual UI
- Recommend curl with dedicated device_id for token (not Element)
- Add cross-signing seeds file to E2EE reset instructions
- Clarify that cross-signing may need cross_signing_password
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(matrix): reduce verbose logging in production
Downgrade per-message and internal workaround logs from Info to Debug,
and use structured fields for SAS verification log.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(matrix): fix formatting, rename field, and add token redaction
- Run gofmt to fix struct definition and literal alignment
- Rename shareSessionInChan to shareSessionInChannel for consistency
with other platforms (Telegram, Discord, Slack)
- Add core.RedactToken() in connectLoop error logging to prevent
access token leakage in logs
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci: install libolm-dev for Matrix E2EE crypto
The mautrix-go crypto package depends on libolm (C library) for
end-to-end encryption. The CI runners don't have it pre-installed,
causing golangci-lint to fail when loading the CGO package.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(matrix): use build-tagged E2EE to avoid CGO/libolm dependency
Split E2EE (end-to-end encryption) code behind a `goolm` build tag so
the default build compiles without CGO or libolm-dev. This fixes CI
lint failures caused by the mautrix-go crypto package pulling in the
CGO-based libolm backend by default.
- Default build: no E2EE, no CGO, CI lint passes
- Build with `-tags goolm`: full E2EE using pure-Go olm backend
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* build: enable goolm tag by default in Makefile
Matrix E2EE requires the goolm build tag. Add it as a default
tag in the Makefile so `make build` includes E2EE automatically.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(matrix): suppress unused field lint for cryptoHelper
The cryptoHelper field is only accessed in e2ee.go (goolm build tag),
so the default build's linter flags it as unused. Add //nolint:unused
since this is intentional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(matrix): resolve lint errors for CI (errcheck + staticcheck)
* feat(agent): add GitHub Copilot as a first-class agent (#865)
* chore: ignore web/tsconfig.tsbuildinfo (TS incremental build cache)
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(qqbot): add inline keyboard buttons and INTERACTION_CREATE event handling (#1131)
* feat(qqbot): add support for inline keyboard buttons and interaction events
- Update default intents to include INTERACTION_CREATE
- Implement SendWithButtons to send messages with inline keyboard buttons
- Encode permission decisions and session key in button_data for routing callbacks
- Handle INTERACTION_CREATE events to process button clicks as permission responses
- Create synthetic message for permission decisions and forward to engine
- Add ackInteraction to acknowledge INTERACTION_CREATE events via API call
- Add extensive tests for sending buttons, handling interactions, and edge cases
* fix(qqbot): ignore ackInteraction error and correct test URL check
- Suppress error from ackInteraction to avoid unused error variable warning
- Update test to expect corrected interaction URL path "/interactions/interact-1" instead of "/v2/interactions/interact-1"
* - Add sessionKey field to replyContext to embed in button_data and session routing
- Refactor SendWithButtons to use sessionKey from replyContext and validate emptiness
- Enhance replyContext construction with sessionKey in various message scenarios
- Implement new tests for sharing session keys in channel, empty session key errors,
and interaction_create event permission routing
- Update config.example.toml to document new required intents for interaction support
- Modify changelog with instructions on enabling INTERACTION_CREATE intent (bit 26) for QQ Bot
- Permission requests now use clickable inline buttons instead of text replies, requiring
enabling the INTERACTION capability in QQ Open Platform settings
* test(qqbot): ignore errors and returned values in test HTTP handlers
* feat: integrate Google Antigravity CLI (agy) agent
* Harden antigravity session discovery and attachment handling to preserve resume correctness under contention
Constraint: Keep external behavior and interfaces unchanged while improving runtime robustness
Rejected: Full process-level session correlation | Requires upstream CLI contract not available here
Confidence: medium
Scope-risk: narrow
Directive: Keep session-id detection conservative unless agy exposes explicit correlation metadata
Tested: go test ./agent/antigravity/... && go test ./core/... && go test ./cmd/cc-connect/...
Not-tested: Real concurrent agy process race on multi-session host
* Fix antigravity prompt execution and shutdown safety without changing user-facing flow
Constraint: Preserve existing antigravity integration surface and core event handling contracts
Rejected: Introduce a custom permission event parser for agy stdout | No stable upstream protocol contract yet
Confidence: medium
Scope-risk: narrow
Directive: If agy publishes structured permission I/O, replace y/n terminal fallback with typed request/response framing
Tested: go test ./agent/antigravity/... && go test ./core/... && go test ./cmd/cc-connect/...
Not-tested: End-to-end interactive agy permission prompt against a live CLI binary
* Stabilize antigravity permission flow so Discord approvals consistently drive CLI execution
Constraint: Preserve current antigravity stream/event contracts while improving permission reliability
Rejected: Full structured agy protocol parser | Upstream schema is not yet stable/public
Confidence: medium
Scope-risk: narrow
Directive: Replace regex prompt detection with typed protocol once agy exposes machine-readable permission events
Tested: go test ./agent/antigravity/... && go test ./core/... && go test ./cmd/cc-connect/... && go build ./cmd/cc-connect
Not-tested: Live Discord permission prompts across all locales/terminal themes
* Prevent agy hangs and unsupported flag crashes in cross-platform antigravity runs
Constraint: Keep antigravity adapter behavior compatible across Telegram/Discord while avoiding CLI-specific crashes
Rejected: Keep passing -m and rely on user wrapper scripts | Breaks agy v1.0.2 directly for normal users
Confidence: high
Scope-risk: narrow
Directive: Re-enable explicit model flag only after agy documents stable model flag support
Tested: go test ./agent/antigravity/... && go test ./core/... && go test ./cmd/cc-connect/... && go build ./cmd/cc-connect
Not-tested: Live agy v1.0.2 interactive permission prompts on Telegram with real long-running tool tasks
* Fix lint-blocking errcheck findings in antigravity adapter without behavior changes
Constraint: Keep runtime behavior unchanged while satisfying CI lint gates
Rejected: Broader refactor around file/stream lifecycle | Unnecessary for this blocking lint failure
Confidence: high
Scope-risk: narrow
Directive: Keep close/remove calls explicitly checked or intentionally ignored with clear intent in this package
Tested: go test ./agent/antigravity/... && go test ./core/... && go test ./cmd/cc-connect/...
Not-tested: Full GitHub Actions rerun output
* Fix remaining errcheck close-handling findings from CI lint
Constraint: Resolve lint blockers without behavioral changes
Rejected: Broad lifecycle refactor | unnecessary for this CI-only failure class
Confidence: high
Scope-risk: narrow
Directive: Keep file/stream close sites explicitly checked or intentionally ignored for errcheck
Tested: go test ./agent/antigravity/... && go test ./core/... && go test ./cmd/cc-connect/...
Not-tested: GitHub Actions rerun not yet executed
* docs: update MiniMax banner to M3 release
- Replace minimax-en.jpeg and minimax-zh.jpeg with new M3 PNG banners
- Update MiniMax description in both READMEs to reflect M3 benchmarks
(SWE-Bench Pro 59.0, Terminal Bench 2.1 66.0, VIBE V2 60.1, etc.)
- Update tagline: "Build, Learn & Ship" / "Mini 价格 Max 性能"
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: update MiniMax model to M3 in provider presets
- Switch primary model from MiniMax-M2.7 to MiniMax-M3
- Add MiniMax-M3-highspeed variant
- Keep M2.7 as fallback for compatibility
- Update descriptions to reflect M3 features (Sparse Attention, Multimodal)
- Bump updated_at to 2026-06-03
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(core): /stop preserves AgentSessionID so next message can resume (#1196)
`cmdStop` no longer clears `AgentSessionID`; the next message can `--resume` the
conversation, matching the card-button Stop path and eliminating the
inconsistency described in #1189.
- Session-ID write-back now always follows the live forked ID Claude reports on
every `--resume` (was: skip if already set); name binding still fires only on
first assignment to avoid polluting `sessionNames`
- Removed `clearStaleSessionID` helper and `CompareAndSetAgentSessionID` guard;
replaced with unconditional `SetAgentSessionID` + `wasEmpty` name-binding gate
- Updated / renamed affected tests:
- TestSessionIDWriteback_TracksLiveForkedID
- TestInteractiveWriteBack_TracksForkedSessionID
- TestInteractiveWriteBack_NamingBindsOnlyOnFirstAssignment
- TestCmdStop_PreservesAgentSessionID
Closes#1189
* fix: three bug fixes — WPS ChannelKey, Telegram text_link, workspace bind path
fix(wps-xiezuo): set ChannelKey on inbound messages for per-group session
isolation (#1217). Without ChannelKey, messages from different WPS groups
were routed to the same session.
fix(telegram): forward text_link entity URLs to agent as [label](url) (#1207).
Telegram passes inline hyperlinks as entities (not in the plain text), so the
agent never saw the URL. enrichTextLinks() rewrites text_link spans in-place
using UTF-16 offsets (Telegram's coordinate system).
fix(core): splitCommandArgs() respects quoted paths in /workspace bind (#1211).
strings.Fields splits on every space, truncating paths like
'/workspace bind "/my project/foo"'. The new parser honours single and
double quotes so space-containing paths work correctly.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(pi): correct agent directory paths (SkillDirs + GlobalMemoryFile) (#1206)
* fix(pi): correct SkillDirs paths for pi agent skill discovery
cc-connect's pi agent reported wrong skill directories (~/.pi/skills/
instead of ~/.pi/agent/skills/), causing SkillRegistry to never find
pi skills. This meant slash commands like /caveman were not intercepted
by cc-connect's command routing — they fell through to pi's print mode
as raw text, and the LLM never received the skill instructions.
Fix:
- ~/.pi/skills/ -> ~/.pi/agent/skills/ (pi's default agent dir)
- Add ~/.agents/skills/ (common shared skill dir)
- Add /skills/ if env var is set
* fix(pi): correct GlobalMemoryFile path
pi loads global AGENTS.md from getAgentDir() which defaults to
~/.pi/agent/, not ~/.pi/. Respect if set.
* chore(pi): consistent homeDir variable naming in SkillDirs
* fix(pi): add SetWorkDir to enable /dir command (#1177)
pi agent implemented GetWorkDir() but not SetWorkDir(), so the
WorkDirSwitcher interface was incomplete and /dir always returned
'current agent does not support dynamic work directory switching'.
* fix(pi): pipe prompt via stdin to avoid CLI option parsing (#1185)
Reply chain headers start with "---", which pi's CLI parser
interprets as an option flag, producing "Unknown option" errors.
Pass the prompt via stdin instead of as a positional argument,
consistent with how gemini and codex agents handle it.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* fix(pi): read enabledModels from settings.json for /model (#1178)
* fix(pi): read enabledModels from settings.json for /model
AvailableModels() now reads ~/.pi/agent/settings.json enabledModels
instead of returning nil. New() also falls back to defaultModel
from settings when opts don't specify model.
Add helpers: piSettingsDir, settingsPath, readSettings,
readSettingsModels, readDefaultModel. Respects PI_CODING_AGENT_DIR.
* fix(pi): check err return values in tests to pass errcheck linter
* fix(pi): silence errcheck for os.Setenv/Unsetenv in defer cleanup blocks
* feat(dingtalk): add reaction emoji support (#1213)
* feat(dingtalk): add reaction emoji support
* fix(dingtalk): satisfy emotion lint
* feat(slack,tmux): per-thread session scope and per-session tmux windows (#1179)
Add `session_scope` to the Slack platform ("user" | "channel" | "thread").
"thread" keys each Slack thread to its own cc-connect session, so a new
top-level message starts a fresh conversation while replies in the same
thread continue it. Backward compatible: when unset, behaviour is
unchanged (share_session_in_channel maps to "channel").
Add `window_per_session` to the tmux agent: each cc-connect session gets
its own tmux window (and its own init_command/agent instance) instead of
sharing one pane. Required for real isolation when the platform splits
sessions per thread; otherwise concurrent threads interleave into a single
shared agent. The allocated window name doubles as the agent session ID so
resumes reuse the same window.
Both options default off. Includes unit tests and config.example.toml docs.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* Reply to unauthorized IM senders (#1190)
* feat(relay): configure group visibility (#1209)
* feat(send): 支持 --at-users / --at-all 命令行参数,DingTalk @mention 通知 (#1188)
* feat(send): add --at-users and --at-all support for DingTalk @mention
- Add --at-users and --at-all flags to cc-connect send command
- Add AtMentionSender optional interface for platforms that support @mention
- Implement ReplyWithAt in DingTalk platform using text msgtype
- Add CheckLinger stub for macOS compatibility
* fix: update test callers for SendToSessionWithAttachments new signature
* feat(send): add --at-users and --at-all support for DingTalk @mention
- Add --at-users and --at-all flags to cc-connect send command
- Add AtMentionSender optional interface for platforms that support @mention
- Implement ReplyWithAt in DingTalk platform using text msgtype
- Add CheckLinger stub for macOS compatibility
- Update all test callers for new signature
* fix(dingtalk): check resp.Body.Close return value for errcheck
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: wen_guoxing <wen_guoxing@itrus.com.cn>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* feat(cron): align manual trigger command with exec (#1201)
* feat: add manual cron run support
* feat(cron): align manual trigger command with exec
* fix(cron): guard shell manual triggers
* test(cron): accept manual run output before trigger ack
* test(release): align footer expectations without markdown italics
* fix(core): keep full reply footer paths
* test(core): normalize local dir path expectation
* fix(cron): check exec response body close
---------
Co-authored-by: aoko <aokodesuka@gmail.com>
Co-authored-by: 张彧 <aaron@mac.tail449498.ts.net>
Co-authored-by: 张彧 <aaron@Aaron-MacBook-Pro-14.local>
* feat(codex): support request_user_input app-server events (#1200)
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(feishu): refresh rich card rendering and panel handling (#1204)
Squash merge of PR #1204 (rebased onto main, minor conflicts resolved).
Adds structured per-turn reply footer, cardkit-v1 streaming, status footer
interface, claude context usage tracking, and rich card body improvements.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(feishu): send audio and video attachments as media (#1202)
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(opencode): pass --agent flag when agent config option is set (#1210)
Adds optional 'agent' config option under [projects.agent.options] for
opencode. When set, the value is passed as --agent to every 'opencode run'
invocation, enabling plugin-defined agents (e.g. oh-my-openagent's
'Sisyphus - Ultraworker') to work correctly without falling back to the
default agent.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(core): remove leftover conflict markers in engine_test.go from #1202 merge
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: three bug fixes for #1184, #1139, and #1176
fix(core): multiSelect AskUserQuestion on card platforms (closes#1184)
- For multiSelect questions, render options as a numbered text list
instead of instant-resolve buttons (buttons resolved on first click,
preventing multi-selection)
- Add new i18n key MsgAskQuestionNoteMulti with per-language hint to
reply with comma-separated numbers (e.g. 1,3)
- Single-select questions keep the existing button UX unchanged
fix(config): allow cc-connect web with agent-only config (closes#1139)
- Add LoadPermissive() / validatePermissive() that skip the
"at least one platform" requirement
- runWeb now uses LoadPermissive so the web admin UI starts even
before any platforms are configured, enabling first-time setup
fix(weixin): fail fast on ret=-2 when context_token cannot be refreshed (closes#1176)
- When sendMessage returns ret=-2 and the stored token is the same as
the current one (no inbound message has refreshed it), stop retrying
immediately instead of burning 3 retries on the same stale token
- Return a clear actionable error: "user must send a new message to
refresh the session token"
- Applied to both text (weixin.go) and media (media_outbound.go) paths
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(matrix): support MATRIX_CROSS_SIGNING_PASSWORD env var and document crypto DB paths
- Add environment variable fallback for cross_signing_password (env var
takes precedence over config file, avoiding plaintext password in config)
- Document E2EE crypto data storage location (~/.cc-connect/) and file
purpose (crypto DB + cross-signing seeds) in both English and Chinese
setup guides
- Add data directory comment in config.example.toml
Addresses P3 items from PR #834 review.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: m3 <Marvae@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: 7a3lv7 <hjkl931031@gmail.com>
Co-authored-by: JazzuLu <27945372+JazzuLu@users.noreply.github.com>
Co-authored-by: shellus <353358601@qq.com>
Co-authored-by: Han <hanzr.nju@outlook.com>
Co-authored-by: Marco <48539922+MMMarcinho@users.noreply.github.com>
Co-authored-by: masakasu <32926719+MASAKASUNO1@users.noreply.github.com>
Co-authored-by: Rael <realraelmail@gmail.com>
Co-authored-by: Yu Zhang <34849476+AaronZ345@users.noreply.github.com>
Co-authored-by: wgx521 <wgx521@139.com>
Co-authored-by: wen_guoxing <wen_guoxing@itrus.com.cn>
Co-authored-by: aoko <aokodesuka@gmail.com>
Co-authored-by: 张彧 <aaron@mac.tail449498.ts.net>
Co-authored-by: 张彧 <aaron@Aaron-MacBook-Pro-14.local>
The WeChat ilink API requires 'ilink_user_id' field in getconfig request,
but getConfigReq was using 'user_id' which caused typing ticket retrieval
to fail with 'ilink_user_id required' error.
Align with sendTypingReq which correctly uses 'ilink_user_id'.
* fix(wps-xiezuo): preserve newlines in outbound messages
WPS Open Platform v7 messages API renders content as markdown only when
BOTH the outer message Type AND Content.Text.Type are "markdown". The
outer field gates the renderer: with outer "text", everything (including
all newlines) gets flattened to a single-line plain text.
After that, the markdown renderer follows CommonMark, where a single
"\n" between non-empty lines is collapsed into a space. Per WPS docs, a
real line break requires either "two trailing spaces + \n" or a blank
line ("\n\n").
Net effect of the old code (outer="text", inner="markdown"): every
cc-connect engine message that uses bare "\n" — /status, /help,
/sessions, /history, tool output — rendered as a single unreadable line
in WPS chat.
Fix:
- Set outer Type to "markdown" to match Content.Text.Type so the
markdown renderer actually fires.
- Convert bare "\n" → " \n" (markdown hard line break) before sending,
with an idempotency guard so already-hard-broken content
(" \n") is not double-spaced.
Reported manually via /status in a WPS chat during beta.5 QA:
expected multi-line output, got one long line with all fields
space-separated.
Tests:
- New TestSendWPSMessage_NewlinesConvertedToHardBreaks: simulates the
engine /status output and asserts the on-wire content has " \n"
instead of bare "\n".
- New TestApplyWPSLineBreaks_Idempotent: 6 subtests covering empty,
no-newline, bare, already-hard-broken, mixed, and paragraph-break
inputs, plus an idempotency assertion (apply twice == apply once).
- Existing TestSendWPSMessage_Success extended to also assert
req.Type == "markdown" as a regression guard for the outer field.
All existing wps-xiezuo tests still pass.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(wps-xiezuo): keep outer type=text (API rejects markdown), only transform \n via inner markdown opt-in
QA in WPS chat returned 400000002 invalid open_v7_message_type when
the outer Type was changed to 'markdown'. WPS v7 messages API outer
Type is a strict enum (text/rich_text/image/file/audio/video/card),
and 'markdown' is not in the enum.
The markdown rendering opt-in is via the INNER Content.Text.Type
field instead. With Content.Text.Type="markdown", WPS renders the
content via its CommonMark subset, in which a single '\n' is still
collapsed into a space — the trailing-spaces transform
applyWPSLineBreaks() is what actually fixes the rendering, the outer
Type change was wrong and is reverted here.
Regression guard test updated to assert outer Type=='text' and
explicitly document why.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(wps-xiezuo): check json.Encode error in newline-hardbreak regression test
Fixes errcheck lint failure introduced in this PR:
platform/wps-xiezuo/wpsxiezuo_test.go:990:29: Error return value of
`(*encoding/json.Encoder).Encode` is not checked (errcheck)
Other historical occurrences in this file are pre-existing and ignored
by `--new-from-rev` style lint scope, so only the new line is changed.
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: qa-cursor <qa-cursor@cc-connect.local>
* fix(send): route --audio/--video to dedicated AudioSender/VideoSender path
PR #1202 added `cc-connect send --audio` / `--video` CLI flags but the
parser appended the loaded files into req.Files, so the engine routed
them through SendFile — bypassing AudioSender / VideoSender entirely:
- Feishu mp3 → FileTypeStream → MsgTypeFile (generic file, no voice
bubble) instead of going through SendAudio's ffmpeg → opus pipeline.
- Feishu mp4 happened to render correctly because SendFile's
detectFeishuFileType already maps .mp4 to MsgTypeMedia, but the
intent was opaque to the platform layer (no way to add codec
normalisation without diverging file vs video paths).
The user-visible result: 258 lines of CLI plumbing landed but the
feature was indistinguishable from --file. SendAudio()'s transcoder
was only reachable via --tts.
This change splits the dispatch so --audio / --video exercise their
intended code paths:
* core.SendRequest gains Audios + Videos []FileAttachment fields
alongside the existing Files slice.
* cmd/cc-connect/send.go populates them separately instead of
appending into Files.
* core.VideoSender is added (parallel to the existing AudioSender).
* core.Engine gets SendAudiosToSession / SendVideosToSession that
prefer AudioSender / VideoSender, with an automatic SendFile
fallback (and a slog.Warn) when the platform doesn't implement
the dedicated interface — preserving delivery on platforms that
haven't wired up native media yet.
* platform/feishu adds SendVideo (FileTypeMp4 + MsgTypeMedia) so the
Feishu route now matches the contract; SendAudio is unchanged
except for diagnostic slog.Debug lines that track which API
(Reply vs Create) was used — needed to investigate the
QA-reported intermittent "audio renders as file" in P2P reply
mode.
* core/bridge.BridgePlatform implements VideoSender too so the
bridge protocol can advertise the capability.
* core.AgentSystemPrompt documents --audio / --video and explicitly
tells the agent NOT to downgrade the user's request to --file
(the previous prompt only mentioned --image / --file / --tts so
agents quietly substituted --file when the user asked for --audio).
Tests cover:
- TestParseSendArgs_AudioPopulatesAudios / Video / Mixed (CLI no
longer leaks audio/video into Files).
- TestSendAudiosToSession_RoutesToSendAudio_NotSendFile (engine
prefers AudioSender) and the fallback case
PlatformWithoutAudioSender_FallsBackToFile.
- Same coverage for video (RoutesToSendVideo / FallsBackToFile).
- TestAudioFormatHint covers ext-wins + mime fallback + codec strip.
- TestAgentSystemPrompt_DocumentsAudioVideoFlags pins both flags
AND the anti-regression "Do NOT downgrade" line so a future
prompt rewrite can't silently drop them again.
Closes internal task t-20260615-cqjbk1 (see qa-cursor note
2026-06-15-pr1202-incomplete-agent-prompt.md). Supersedes the
prompt-only sub-task t-20260614-nftm6b — that scope is included
here.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore: trigger CI
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(dingtalk): handle msgtype=file inbound (route to FileAttachment)
DingTalk delivers user-uploaded files (PDF / txt / docx / …) via
msgtype="file" callbacks that carry a downloadCode + fileName in
the content payload. cc-connect previously had no branch for this
in onRawMessage's dispatcher: the message fell through to the
default text handler with an empty Content and no Files attached,
so the engine silently dropped the user message and the agent later
replied "I did not receive a file" — extremely confusing UX.
This is the file half of the same #981 family that PR #1128 already
fixed for image/picture msgtypes.
Fix mirrors handleImageMessage:
- Route msgtype="file" to new handleFileMessage
- Parse downloadCode + fileName from content map
- Reuse the existing getDownloadURL / HTTP download path
- Cap at 50 MiB (DingTalk's per-file limit is 100 MiB; 50 is
plenty for agent inputs without OOM risk)
- Build core.Message with Files: []core.FileAttachment{...}
preserving FileName so the agent sees the original name
Tests
- TestOnRawMessage_FileMsgTypeNotDroppedAsEmptyText (negative
regression: handler must never see empty file inbound)
- TestHandleFileMessage_BuildsFileAttachmentWithName (positive:
mocks accessToken + download URL + file body, asserts the
dispatched Message carries a FileAttachment with the correct
name, bytes, and mime)
- TestOnRawMessage_PictureMsgTypeNotDroppedAsEmptyText still
passes (no regression on the image branch)
Verified manually
- Sent .txt + "read this file" to DingTalk private chat with
release-test cc-connect binary built from this branch
- Before: msg silently dropped, agent had nothing to read
- After: "dingtalk: file downloaded successfully size=N
file_name=xxx.txt" + agent correctly described file contents
Discovered by qa-cursor while running release-gate UI-P0-19
(designed as a known-gap case that was expected to fail).
* test(dingtalk): silence errcheck on recover() in file test
---------
Co-authored-by: qa-cursor <qa-cursor@spaceship.local>
Before: A single EOF / network blip on the very first session.Open() call
returned the error directly from Platform.Start, the engine logged
"platform start failed" and "engine started with partial readiness", and
Discord stayed offline until cc-connect was manually restarted (release-gate
2026-06-14, "discord: open gateway: ... EOF" with no retry over 10 minutes).
Telegram already handled this case via an async recovery loop. Discord did
not.
This change implements core.AsyncRecoverablePlatform on the Discord
platform:
- Platform.Start now launches a background goroutine and returns nil
immediately. The engine treats Discord as "pending" and waits for
OnPlatformReady, instead of treating the first failure as permanent.
- A new connectLoop tries session.Open() with exponential backoff (5s →
10s → 20s → … → 5m cap), rebuilding the discordgo session each attempt
so a half-open handshake cannot poison subsequent retries.
- Once the first connect succeeds, discordgo's own internal heartbeat /
reconnect logic keeps the gateway alive; the loop blocks on ctx.Done()
and tears the session down on shutdown.
- Stop now cancels the loop, marks the platform as stopping, and closes
any open session.
- SetLifecycleHandler is now wired up so the engine receives ready /
unavailable transitions (matching Telegram's contract).
The session field is now guarded by Platform.mu to keep the recovery
goroutine and external callers (Stop, message senders) from racing during
reconnect.
Out of scope (suggested follow-up):
- Extracting the connect-loop pattern into a shared core helper. Telegram
and Discord now share the same shape but are still implemented twice;
a common helper (with hooks for "build session" + "block until
disconnect") would let future platforms (e.g. Slack when it grows
UpdateMessage support) drop in without re-deriving the backoff logic.
Tests:
- TestPlatformImplementsAsyncRecoverable — *Platform now satisfies
core.AsyncRecoverablePlatform so engine.Start picks the recovery path.
- TestStartReturnsImmediatelyAndStopCancelsLoop — Start() does not block
on the gateway handshake; Stop() unwinds the goroutine deterministically.
- TestStopBeforeFirstConnectAttempt — calling Stop() before Start() (or
racing the very first attempt) is safe.
- All existing platform/discord and core tests pass:
go test -count=1 ./platform/discord/... ./core/...
- golangci-lint run --new-from-rev=origin/main ./platform/discord/...
reports 0 new issues.
Refs internal task t-20260614-es2jl5; QA evidence at
projects/cc-connect/agents/qa-cursor/release-gate notes.
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Both Telegram and Discord implement MessageUpdater.UpdateMessage so the
engine can stream partial agent output by editing one message in place.
However:
- Telegram never declared ProgressStyle(), so progressStyleForPlatform()
fell back to "legacy". Long replies (33k+ chars in a recent QA run) were
buffered and sent in a single final p.Send — users saw "spinner spins
for 3 minutes, then a wall of text appears".
- Discord declared progress_style only when the user explicitly set it, and
the default value was "legacy". Same symptom on long replies.
This change:
1. Telegram: add `progress_style` option (legacy|compact|card; "card"
normalized to "compact" since Telegram has no rich card UI) and a
ProgressStyle() method on *Platform. Default is "compact".
2. Discord: change the default from "legacy" to "compact". `progress_style`
parsing is unchanged; users who relied on the old behavior can opt back
in with `progress_style = "legacy"`.
Out of scope (separate follow-up suggested):
- Extracting the bridge's capability-driven progressStyleForAdapter
fallback (core/bridge.go:380) into a shared helper so all
MessageUpdater-supporting platforms get a sensible default automatically.
This PR only fixes Telegram + Discord directly.
- Slack does not implement UpdateMessage, so it is unaffected.
- Feishu/Lark default is intentionally left as "legacy" because the right
default is "card" (rich) which is a bigger UX change; the proper fix is
base config + 02-gen-config.sh per the QA release-gate note's TODO-2.
Tests:
- platform/telegram: added TestNewProgressStyleDefault (compact),
TestNewProgressStyleOptions (compact/legacy/card/uppercase/whitespace/
empty/invalid), TestProgressStyleProviderInterface.
- platform/discord: added TestNew_DefaultProgressStyleIsCompact; existing
TestDispatchMessage_LegacyPlatformFallsBackToBasePlatform and
TestNew_LegacyProgressStyleDoesNotEnableProgressInterfaces updated to
pass `progress_style = "legacy"` explicitly (their intent is to verify
the legacy code path, not the default).
- All four affected packages pass `go test -count=1`.
Refs internal task t-20260614-y6xjnz; QA evidence at
projects/cc-connect/agents/qa-cursor/release-gate/notes/2026-06-14-streaming-investigation.md
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(qq): implement FileSender via OneBot HTTP API
Add file sending support (core.FileSender) to the QQ platform.
Files are base64-encoded to avoid local-path issues across
Windows/WSL/Docker boundaries.
- SendFile: group via upload_group_file, private via send_private_msg
with file message segment
- callHTTPAPI: new method that calls OneBot v11 HTTP endpoints,
preferred over WebSocket for large file payloads
- New config option `http_url`: points to the OneBot HTTP API
(e.g. NapCat/Lagrange HTTP server). When set, file operations
route through HTTP; otherwise falls back to WebSocket.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(qq): implement file receiving with NapCat best-practice fallback chain
Add file attachment receiving support to the QQ platform. When a user
sends a file (PDF, docx, etc.), parseMessage now extracts it as a
core.FileAttachment so the agent can read it.
Download strategy follows NapCat recommended best practices:
1. Direct download from segment URL (120s timeout for large files)
2. get_group_file_url / get_private_file_url for fresh CDN links
(original URLs expire quickly and have download count limits)
3. get_file as last resort (NapCat local download or base64)
Key details:
- get_group_file_url requires param "group" (string), not "group_id"
- downloadLargeFile with 120s timeout (vs 30s for images)
- HTTP status code validation to fail fast on 404 expired URLs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Gou Lingyun <62201655+ganansuan647@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Permission responses synthesized by inline-button / card-action paths
(Telegram callback_query, Feishu card_action, QQBot interaction button,
bridge web admin card_action) used to fall through to the agent's prompt
stream as the literal string "allow" / "deny" when the interactive state
or pending permission was missing — typically because the user tapped an
old card after a session reset, bot restart, or card-message redelivery.
Add an IsPermissionResponse flag on core.Message, set it on every
synthesized permission callback path, and have
handlePendingPermission drop such messages silently (returning true) when
no matching interactive state / pending request exists. Plain text
"allow" / "deny" typed by a real user continues to flow through the
normal message handler.
This rebuilds owner PR #826 against current main; the original commit
only covered Telegram + Feishu and missed the QQBot + bridge paths
flagged in review. Request-ID validation is deferred as a follow-up: it
requires changing button data formats across every platform and the
engine signature, which is larger than a focused bug fix.
Co-authored-by: Claude <noreply@anthropic.com>
* feat(wecom): implement SendFile for WebSocket mode
The WeCom AI Bot WebSocket protocol supports file/image/video msgtypes via
the chunked aibot_upload_media_* upload flow. WSPlatform implemented
SendImage but not SendFile, so cc-connect's FileSender capability check
returned false for WeCom and `cc-connect send --file` from the agent fell
back to a plain-text "I cannot send files" reply.
This adds:
- WSPlatform.SendFile — mirrors SendImage, calls uploadWSMedia with
mediaType="file" and sendWSMediaMessage to deliver the resulting media_id
- wsFileFileName helper — sensible filename fallback derived from MimeType
when FileAttachment.FileName is empty
- core.FileSender interface assertion to surface the capability to engine
- TestWSPlatformSendFile_UploadsAndSendsMedia — mirrors the existing image
test, validating init/chunk/finish/send WS frames
Webhook mode (Platform in wecom.go) still lacks SendFile — that can be a
follow-up PR; this change unblocks WebSocket-mode users immediately.
* fix(wecom): satisfy errcheck on conn.Close in SendFile test
The CI lint job (golangci-lint v2.11.4 with --new-from-rev origin/main)
flagged two `defer conn.Close()` calls in the new
TestWSPlatformSendFile_UploadsAndSendsMedia test as errcheck violations,
which blocked all subsequent test jobs. Wrap them in `defer func() { _ =
conn.Close() }()` to match the project's existing pattern (see
agent/copilot/integration_test.go, agent/antigravity/session_test.go,
platform/qqbot/qqbot.go).
No behavior change.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* fix(dingtalk): extractAtUserIds regex 放宽匹配 4+ 位用户 ID
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(dingtalk): hoist atUserID regexp to package level, add tests, fix gofmt
Addresses QA review P2 items for PR #1237:
- Move regexp.MustCompile to package-level var to avoid recompiling on each Reply
- Add blank line between extractAtUserIds and preprocessDingTalkMarkdown (gofmt)
- Add TestExtractAtUserIds (12 sub-cases), TestReply_IncludesExtractedAtUserIds,
and TestReply_NoAtUserIdsWhenNoMention
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: wen_guoxing <wen_guoxing@itrus.com.cn>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Adds three explicit tests in platform/feishu/platform_test.go to prove that
the IsOldMessage + create_time filtering behaves correctly after a restart:
1. TestOnMessage_OldMessageAfterRestartIsFiltered – message with create_time
10 minutes before StartTime is silently dropped (Feishu replay prevention).
2. TestOnMessage_NewMessageAfterRestartIsProcessed – message with create_time
2 minutes after StartTime reaches the handler (regression guard for #972).
3. TestOnMessage_GracePeriodMessageIsProcessed – message 1 second before
StartTime passes through the 2-second grace window.
Tests set core.StartTime to a controlled value and restore it via defer, so
they are safe to run in parallel or alongside existing tests.
No production code changes: the current IsOldMessage logic (time.Time
comparison with -2s grace) is correct for the reported scenario.
Co-authored-by: root <root@UYQQVGRAEKQKNQP>
Co-authored-by: Cursor <cursoragent@cursor.com>
## P1 fixes
- core/engine_test: fix stubCompactProgressPlatform.BuildRichCard signature
The last parameter was still `elapsed time.Duration` (old interface) after
#1204 changed RichCardSupporter to `statusFooter string`. The mismatch
caused the stub to silently NOT satisfy the interface at runtime, so all
tests using it were bypassing the rich-card path entirely.
- platform/slack: warn when session_scope=thread without window_per_session
Thread-scoped sessions require per-session isolation at the agent level.
When using tmux, window_per_session=true must also be set or concurrent
threads will share a single pane and their output will interleave.
## P2 fixes
- core/relay: warn on unknown visibility mode instead of silent fallback
normalizeRelayVisibility previously fell back to "full" silently for
unrecognised values. Now logs a structured Warn with valid options listed.
- platform/feishu: map common video formats to FileTypeMp4 (Media message)
CLI accepted .webm/.mov/.avi/.mkv as video, but detectFeishuFileType only
recognised .mp4, sending others as generic file downloads. Expanded the
detection to cover all common video MIME types and extensions so they
render as native Feishu video player bubbles. Playback compatibility
(e.g. webm/mkv) depends on the Feishu client platform.
- agent/{claudecode,qoder}: surface root bypass-downgrade warning to IM user
When running as root, bypassPermissions (Claude) and yolo (Qoder) modes
are silently downgraded. Users were confused why the agent kept asking for
permissions. Added StartupWarner interface (core/interfaces.go) so the
engine can emit a one-time IM notification after session start.
Co-authored-by: root <root@UYQQVGRAEKQKNQP>
Co-authored-by: Cursor <cursoragent@cursor.com>
When Markdown tables are converted to HTML with column alignment padding,
the output can exceed Telegram's 4096-char limit even when the original
Markdown was under the engine's 4000-char split threshold.
Changes:
- Reply/Send: detect "message is too long" and chunk the HTML content
- SendWithButtons: chunk with buttons on first message only
- SendPreviewStart: fall back to plain text (preview shouldn't be chunked)
- Add sendChunked and sendChunkedWithButtons helper functions
- Use SplitMessageCodeFenceAware to respect code block boundaries
Fixes#866
Co-authored-by: Claude <noreply@anthropic.com>
The bug report in #1087 says context_token is only kept in memory. The
fix is already on main (tokensPath, loadTokens, persistTokens,
setContextToken / getContextToken — all from the original weixin
feature #257), but there was no test covering the on-disk round-trip.
Add three tests to lock the behaviour in:
- TestContextToken_PersistAndReload: stores tokens, verifies the JSON
file exists with the expected shape, then reloads from disk via a
fresh Platform struct and confirms getContextToken /
ReconstructReplyCtx (the cron / cc-connect send path) still work.
- TestContextToken_LoadMissingFile: a missing file is a no-op, not an
error.
- TestReconstructReplyCtx_MissingToken: the cron / send path returns
the actionable "no stored context_token" error when a peer has
never messaged the bot.
Pure test addition, no production code change.
Co-authored-by: Claude <claude@anthropic.com>
The v1.3.3-beta.4 weixin platform reported "platform ready" the moment
Start() returned, even though pollLoop could be silently wedged (e.g. on
HTTP 500 storms, paused sessions, or token errors). With no later
signal, the engine declared the platform usable while no messages were
ever delivered to the agent (issue #906).
Make weixin implement core.AsyncRecoverablePlatform. pollLoop now treats
the first successful getUpdates round-trip as the authoritative
ready-for-poll signal and notifies the engine exactly once via
OnPlatformReady. The "platform ready" log therefore reflects a session
that has actually accepted the ilink token, not a Start()-time promise.
A new unit test spins an httptest ilink server and asserts that
OnPlatformReady is fired on first success, never repeated on subsequent
successes, and is suppressed while getUpdates keeps failing.
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
When migrating to go-telegram/bot library (commit 1104739), the drain
step that clears pending updates was removed. This causes 409 Conflict
errors on restart because Telegram still considers the previous instance's
long-poll request as active.
The fix adds drainPendingUpdates() which calls getUpdates with offset=-1
before starting the polling loop. This terminates any outstanding
long-poll request from a previous instance.
Fixes#422
Co-authored-by: Claude <noreply@anthropic.com>
In WeCom group chat, @-mentions are embedded as plain text using the
bot's display name (the WS aibot protocol does not provide structured
mention metadata like Feishu's larkim.MentionEvent). The previous
fallback used strings.Fields(s)[0] as the mention token, which silently
failed when:
- the bot's display name contains spaces (e.g. "Claude Code")
- the user @-mentioned multiple parties before the command
(e.g. "@somebody @bot /list")
Both cases left non-slash content at the start of the message, so the
command dispatch gate at core/engine.go:2079 (HasPrefix(content, "/"))
treated the input as natural-language prompt and bypassed the command.
Replace the fields-based parser with a forward scan for the first '/'
or '!' on a whitespace boundary. Adds regression tests for both failure
modes plus URL/fraction safety cases.
Feishu may redeliver messages with a new message_id but the original
create_time after WS disconnect. Track a per-session watermark from
completed, in-flight, and queued user messages so older redeliveries
are discarded before processing or enqueueing.
Co-authored-by: Cursor <cursoragent@cursor.com>
fix(core): multiSelect AskUserQuestion on card platforms (closes#1184)
- For multiSelect questions, render options as a numbered text list
instead of instant-resolve buttons (buttons resolved on first click,
preventing multi-selection)
- Add new i18n key MsgAskQuestionNoteMulti with per-language hint to
reply with comma-separated numbers (e.g. 1,3)
- Single-select questions keep the existing button UX unchanged
fix(config): allow cc-connect web with agent-only config (closes#1139)
- Add LoadPermissive() / validatePermissive() that skip the
"at least one platform" requirement
- runWeb now uses LoadPermissive so the web admin UI starts even
before any platforms are configured, enabling first-time setup
fix(weixin): fail fast on ret=-2 when context_token cannot be refreshed (closes#1176)
- When sendMessage returns ret=-2 and the stored token is the same as
the current one (no inbound message has refreshed it), stop retrying
immediately instead of burning 3 retries on the same stale token
- Return a clear actionable error: "user must send a new message to
refresh the session token"
- Applied to both text (weixin.go) and media (media_outbound.go) paths
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(send): add --at-users and --at-all support for DingTalk @mention
- Add --at-users and --at-all flags to cc-connect send command
- Add AtMentionSender optional interface for platforms that support @mention
- Implement ReplyWithAt in DingTalk platform using text msgtype
- Add CheckLinger stub for macOS compatibility
* fix: update test callers for SendToSessionWithAttachments new signature
* feat(send): add --at-users and --at-all support for DingTalk @mention
- Add --at-users and --at-all flags to cc-connect send command
- Add AtMentionSender optional interface for platforms that support @mention
- Implement ReplyWithAt in DingTalk platform using text msgtype
- Add CheckLinger stub for macOS compatibility
- Update all test callers for new signature
* fix(dingtalk): check resp.Body.Close return value for errcheck
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: wen_guoxing <wen_guoxing@itrus.com.cn>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Add `session_scope` to the Slack platform ("user" | "channel" | "thread").
"thread" keys each Slack thread to its own cc-connect session, so a new
top-level message starts a fresh conversation while replies in the same
thread continue it. Backward compatible: when unset, behaviour is
unchanged (share_session_in_channel maps to "channel").
Add `window_per_session` to the tmux agent: each cc-connect session gets
its own tmux window (and its own init_command/agent instance) instead of
sharing one pane. Required for real isolation when the platform splits
sessions per thread; otherwise concurrent threads interleave into a single
shared agent. The allocated window name doubles as the agent session ID so
resumes reuse the same window.
Both options default off. Includes unit tests and config.example.toml docs.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
fix(wps-xiezuo): set ChannelKey on inbound messages for per-group session
isolation (#1217). Without ChannelKey, messages from different WPS groups
were routed to the same session.
fix(telegram): forward text_link entity URLs to agent as [label](url) (#1207).
Telegram passes inline hyperlinks as entities (not in the plain text), so the
agent never saw the URL. enrichTextLinks() rewrites text_link spans in-place
using UTF-16 offsets (Telegram's coordinate system).
fix(core): splitCommandArgs() respects quoted paths in /workspace bind (#1211).
strings.Fields splits on every space, truncating paths like
'/workspace bind "/my project/foo"'. The new parser honours single and
double quotes so space-containing paths work correctly.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(qqbot): add support for inline keyboard buttons and interaction events
- Update default intents to include INTERACTION_CREATE
- Implement SendWithButtons to send messages with inline keyboard buttons
- Encode permission decisions and session key in button_data for routing callbacks
- Handle INTERACTION_CREATE events to process button clicks as permission responses
- Create synthetic message for permission decisions and forward to engine
- Add ackInteraction to acknowledge INTERACTION_CREATE events via API call
- Add extensive tests for sending buttons, handling interactions, and edge cases
* fix(qqbot): ignore ackInteraction error and correct test URL check
- Suppress error from ackInteraction to avoid unused error variable warning
- Update test to expect corrected interaction URL path "/interactions/interact-1" instead of "/v2/interactions/interact-1"
* - Add sessionKey field to replyContext to embed in button_data and session routing
- Refactor SendWithButtons to use sessionKey from replyContext and validate emptiness
- Enhance replyContext construction with sessionKey in various message scenarios
- Implement new tests for sharing session keys in channel, empty session key errors,
and interaction_create event permission routing
- Update config.example.toml to document new required intents for interaction support
- Modify changelog with instructions on enabling INTERACTION_CREATE intent (bit 26) for QQ Bot
- Permission requests now use clickable inline buttons instead of text replies, requiring
enabling the INTERACTION capability in QQ Open Platform settings
* test(qqbot): ignore errors and returned values in test HTTP handlers
- extractPostPlainText / extractPostParts: format `a` elements as
[text](href) instead of dropping the href. Agents (e.g. Codex)
can now follow alert-card links, log URLs, and rich-text hyperlinks.
- extractCardElements: handle `button` tag — extract label text and
the first `open_url` behavior URL, formatted as [label](url).
Buttons without a URL still forward their label text.
- Update two existing tests that asserted the old (href-dropping)
behaviour.
- Also: web/headless-Linux (#1148) — check for xdg-open availability
before trying to open a browser; improve the fallback message to
include the full login URL and a note to ensure cc-connect is running.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(dingtalk): use senderStaffId for single-chat card openSpaceId
Single-chat cards were using robotCode as the openSpaceId identifier,
which caused incorrect card delivery. Now uses senderStaffId to match
the expected IM_ROBOT space format.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix:修复go.sum;无业务
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
- fix(core): reject empty/whitespace content as AskUserQuestion answers (#1086)
Platform delivery receipts arriving within ~500ms were accepted as valid answers,
resolving the tool immediately with empty answers before the user could respond.
- fix(feishu): treat require_mention=false as alias for group_reply_all=true (#1141)
Platform read group_reply_all from opts but users set require_mention=false;
group messages without @mention were silently dropped.
- fix(dingtalk): accept msgtype="picture" as image message (#1128)
DingTalk delivers image messages as either "image" or "picture" depending on client.
"picture" fell through to the text handler (empty content) and was dropped.
- feat(core): add max_turn_time_mins config option for #1091
Absolute wall-clock cap per agent turn that does NOT reset on tool-call events.
Prevents long-running bash commands from permanently locking the session.
Two-phase shutdown: soft stop (10s grace) then force-kill.
Session is preserved; next message resumes via --resume.
Co-authored-by: Cursor <cursoragent@cursor.com>
In group chats with thread_isolation, any message without an explicit @bot
mention was dropped before image/file download. Users naturally drop images
into a thread after their opening @bot text, so all of those follow-up
images were silently lost — the agent only ever saw the text and could
never see the screenshots, despite Feishu having delivered them.
Track threads that have already been engaged by an @bot message and, while
the @bot filter is otherwise unchanged, allow image/file/audio messages to
pass through inside an active thread. Plain text and rich-text posts still
require an explicit mention so unrelated thread chatter does not leak into
the agent.
Adds unit tests for the new helpers and an end-to-end onMessage test
covering the three relevant transitions (mention establishes thread,
in-thread attachment is admitted, cross-thread attachment is still dropped,
in-thread text without mention is still dropped).
When sending messages to Weixin platform, if context_token is missing
(the API didn't provide it or stored token is empty), the send fails
silently and users don't receive authorization prompts.
Changes:
- Add prominent error logging in sendChunks with hint for users
- Add truncatePreview helper for content preview in logs
- Improve error message to include actionable guidance
- Add special handling in engine for context_token missing errors
This helps operators diagnose the issue and understand that the user
needs to send a new message to refresh the context_token.
Fixes#929
Co-authored-by: Claude <noreply@anthropic.com>
IsForum is not reliably present in the Chat object embedded in a
CallbackQuery message, causing forum group callbacks (permission
buttons, command buttons) to resolve threadID=0 and build a wrong
session key. Use MessageThreadID != 0 instead, which is always
populated when the message is in a topic thread.
Fixes#1002
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>