StartSession in agent/qoder and agent/cursor read a.workDir without
holding a.mu, while SetWorkDir writes a.workDir under the lock. With
/dir live workspace switching the two paths run concurrently, which
the Go race detector flags as a data race.
Move the field read into the existing critical section alongside the
other captured agent state (mode, model, cmd, sessionEnv), then pass
the local copy down to newQoderSession / newCursorSession. Adds a
regression test per agent that fires SetWorkDir and StartSession in
parallel; under -race the buggy code deterministically fails with
"race detected during execution of test", while the fix passes
cleanly.
The session constructors only initialise structs (the CLI is not
spawned until Send), so the new tests run without requiring qodercli
or the Cursor agent binary on PATH.
* feat(dingtalk): add AI Card streaming support
- Add StreamingCard and StreamingCardPlatform optional interfaces (core/interfaces.go)
- Implement DingTalk AI Card service with throttling, degradation, and fallback (platform/dingtalk/card.go)
- Integrate card lifecycle into engine event loop: create card at turn start, stream updates for thinking/tool/text events, finalize on result (core/engine.go)
- Support both group chat (IM_GROUP) and private chat (IM_ROBOT) card delivery
- Check deliverResults success and fallback to normal text messages on card failure
- Use ConversationType field for reliable group/private chat detection
* feat(dingtalk): add proactive messaging, reply-quote support, and raw JSON parsing
- Implement ReplyContextReconstructor for proactive message sending via REST API
- Add sendProactiveMessage with support for both group and direct messages
- Switch from RegisterChatBotCallbackRouter to RegisterRouter to access raw JSON
- Parse text.isReplyMsg and text.repliedMsg from raw callback JSON to recover
quoted message content that the SDK's BotCallbackDataTextModel silently drops
- Add formatReplyContent to prepend quoted text to user messages
- Add richTextContent, repliedMessage, repliedTextContent types for structured parsing
* fix(dingtalk): encode conversation type in session key to fix proactive routing
ReconstructReplyCtx hard-coded isGroup=true, causing 1:1 sessions with
a non-empty conversationId to be incorrectly routed to groupMessages/send.
Changes:
- Encode conversation type ('g' for group, 'd' for direct) in session key
format: dingtalk:{convType}:{conversationId}[:{senderStaffId}]
- Parse convType in ReconstructReplyCtx to correctly set isGroup
- Add unit tests for session-key parsing, reply-quote formatting, and
proactive routing (group vs 1:1)
Fixes: chenhg5/cc-connect#702 review feedback
* feat: add instant reply confirmation before agent processing
Add a configurable instant reply feature that sends an immediate
confirmation message (e.g. '🤔 Thinking...') when a user message is
received, before the agent starts processing. This gives users quick
feedback that their message was received.
Features:
- Configurable via [instant_reply] in config.toml
- Custom reply content or i18n default ('⏳ Processing...')
- Automatically skipped for StreamingCardPlatform (e.g. DingTalk AI Card)
- Automatically skipped for slash commands
- Supports hot-reload via /reload command
- Unit tests for all scenarios
* fix: use runtime check for instant reply instead of static type assertion
The instant reply was never sent for DingTalk because the static
interface type assertion (StreamingCardPlatform) always returned true,
even when card creation would fail at runtime. Move the instant reply
logic into processInteractiveMessageWith, after the actual streaming
card creation attempt, so it fires when the card is not available.
🤖 Generated with [Qoder][https://qoder.com]
* fix(dingtalk): restore correct chat_id extraction after session key format change
The convType tag ("g"/"d") added in be7537d broke extractChannelID, which
assumed a 3-segment session key. Now:
- DingTalk sets ChannelKey on all Messages so core uses the correct value
- buildSenderPrompt accepts channelKey param, preferring it over sessionKey
- extractChannelID/extractUserID handle 4-segment keys as fallback
- whoami uses effectiveChannelID(msg) instead of raw extraction
🤖 Generated with [Qoder][https://qoder.com]
* fix(qoder): adapt stream-json parsing for qodercli 0.2.x format changes
qodercli 0.2.x introduced two breaking changes to its stream-json output
that caused cc-connect to return empty responses via DingTalk:
1. assistant events: the `message.status` field (previously "finished")
is now null; completion is indicated by `message.stop_reason`
("end_turn" or "tool_use") instead.
2. result events: the final response text moved from
`message.content[].text` to a top-level `result` field, and
`message` is now null.
Both extraction paths in handleAssistant() and handleResult() failed
silently, producing response_len=36 (metadata only, no content).
Changes:
- Add `Result` field to streamEvent and `StopReason` to streamMessage
- handleAssistant: accept stop_reason="end_turn"/"tool_use" in addition
to the legacy status="finished"
- handleResult: fall back to top-level `result` field when message is
nil or contains no text
- Add 7 unit tests covering old format, new format, and priority logic
* fix: add suppressDuplicate variable definition after merge conflict resolution
* Merge remote-tracking branch 'origin/main' into feat/dingtalk-ai-card
Conflicts resolved:
- core/engine.go: in EventText handler, keep streamCard (HEAD) priority path
while adopting main's isEllipsisOnly() guard for the outer condition; fall
through to the original hasRichCard / silentHold path when no streaming
card is active.
Add FileAttachment type and wire up file handling across the stack so
that files sent in Feishu are downloaded and forwarded to agents.
Claude Code saves files locally for tool-based reading; Gemini passes
them as temp file references. Other agents log a warning and ignore.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>